這次的 HW 是語者身份辨識任務,對輸入的一段語音,模型需要能辨識出說話人的身份類別,也是個分類任務,使用的數據集是 VoxCeleb,其具體情況如下:
課程目標是學習怎麼樣使用 Transformer,其將 RNN 考慮整個序列的優點和 CNN 並行計算處理的優點結合起來。
本次 hw 使用的模型的基礎框架如下:
具體的任務要求如下:
Hints#
Simple#
注意給出的代碼裡面使用 wget 下載數據集的鏈接裡已經沒東西了,可以在官方給的這個鏈接處進行下載
本實驗使用的數據是已經對原始波形進行預處理後得到的梅爾頻譜圖。
由於不同的音頻的長度可能不同,所以訓練時將不同長度的輸入音頻並將其分割為固定長度的片段(Segment)供模型學習。
即
注意,上述過程中長度小於所選 segment,在後續會被 pad(填充)處理:
Simple 的要求如下,只需要直接 run 它的 sample code 即可
結果如下:
Medium#
Medium 的要求如下:
首先改了一下 pred_layer 中的 hidden_layer 的維度,雖然在 train_set 和 val_set 的結果很好,但是放在 kaggle 上只提高了 0.2 左右。
在此基礎上,進行了較多改動
但是發現在 40000 多 epoch 處 train_acc 就能到 1 了,但是最後 val_acc 只有 0.85,kaggle 上結果只有 0.66 左右,再試試調整一下模型架構。
再將 nhead 和 num_layer 均提升到 4 以後,val_acc 提到了 0.86 左右,kaggle 上結果提升到了 0.7 左右,只差一點點微調了。
在此基礎上在 pred_layer 中的 relu 後面加了一層 dropout (0.1),最後 val_acc 提升到了 0.87 左右,kaggle 上結果提升到了 0.71 左右,過了 medium。
Strong#
Strong 要求構建 Conformer:
單獨使用 Transformer 難以充分挖掘語音信號中的局部特徵,而單獨使用 CNN 又難以高效地捕捉全局依賴關係。因此,如何將 Transformer 和 CNN 的優勢結合起來,設計出一種能夠同時高效建模語音信號局部和全局特徵的模型架構非常重要,Conformer 就是一種這樣的架構,自己手動實現太麻煩了,同時也沒有再去看結構和代碼,此處就使用了 torchaudio 裡面的Conformer予以實現,具體改動如下:
將 encoder 改成
self.encoder = models.Conformer(input_dim=d_model, num_heads=4, ffn_dim=4*d_model, num_layers=6, depthwise_conv_kernel_size=31, dropout=dropout)
同時由於在 Transformer 及其衍生模型(如 Conformer)中,使用了 key_padding_mask ,其是一種關鍵的自注意力掩碼機制,用於處理變長序列輸入,因此在 forward 時,我們需要輸入每個序列的長度,因此需要修改獲得 batch 的方法:
- 訓練和驗證 batch:
def collate_batch(batch):
# Process features within a batch.
"""Collate a batch of data."""
mel, speaker = zip(*batch)
lengths = torch.FloatTensor([m.size(0) for m in mel])
# Because we train the model batch by batch, we need to pad the features in the same batch to make their lengths the same.
mel = pad_sequence(mel, batch_first=True, padding_value=-20) # pad log 10^(-20) which is very small value.
# mel: (batch size, length, 40)
return mel, lengths, torch.FloatTensor(speaker).long()
- 測試 batch:
def inference_collate_batch(batch):
"""Collate a batch of data."""
feat_paths, mels = zip(*batch)
lengths = torch.FloatTensor([m.size(0) for m in mels])
return feat_paths, lengths, torch.stack(mels)
注意然後對應修改 model_fn 處獲得 batch 的地方就行了。
同時由於模型變大了,一般來說學習率應該下降一點,此處設置為 5e-4,同時由於學習率調低了,因此此處延長了訓練時間,到 140000 個 epoch,warmup 也變成了 2000epoch,訓練結果顯示,val_acc 提高到了 0.92 左右:
kaggle 上結果如下
成功過掉 strong,還超了一點。
Boss#
boss 要求如下:
使用了開源的self-attention pooling和additive margin Softmax,並修改放入模型中,針對模型的改動如下:
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchaudio.models as models
class Classifier(nn.Module):
def __init__(self, d_model=300, n_spks=600, dropout=0.15):
super().__init__()
# Project the dimension of features from that of input into d_model.
self.prenet = nn.Linear(40, d_model)
# TODO:
# Change Transformer to Conformer.
# https://arxiv.org/abs/2005.08100
# self.encoder_layer = nn.TransformerEncoderLayer(
# d_model=d_model, dim_feedforward=4*d_model, nhead=4, dropout=dropout
# )
# self.encoder = nn.TransformerEncoder(self.encoder_layer, num_layers=4)
self.encoder = models.Conformer(input_dim=d_model, num_heads=6, ffn_dim=4*d_model, num_layers=8, depthwise_conv_kernel_size=31, dropout=dropout)
self.pooling = SelfAttentionPooling(d_model)
# Project the the dimension of features from d_model into speaker nums.
self.pred_layer = nn.Sequential(
nn.Linear(d_model, 4*d_model),
nn.BatchNorm1d(4*d_model),
nn.ReLU(),
)
self.loss = AdMSoftmaxLoss(embedding_dim=4*d_model, no_classes=n_spks, scale=1, margin=0.4)
def forward(self, mels, lengths, labels=None):
"""
args:
mels: (batch size, length, 40)
return:
out: (batch size, n_spks)
"""
# out: (batch size, length, d_model)
out = self.prenet(mels)
# # out: (length, batch size, d_model)
# out = out.permute(1, 0, 2)
# The encoder layer expect features in the shape of (length, batch size, d_model).
out, _ = self.encoder(out, lengths)
# # out: (batch size, length, d_model)
# out = out.transpose(0, 1)
# mean pooling
# stats = out.mean(dim=1)
stats = self.pooling(out)
# out: (batch, n_spks)
out = self.pred_layer(stats)
logits, err = self.loss(out, labels)
return logits, err
SelfAttentionPooling 為:
import torch
from torch import nn
class SelfAttentionPooling(nn.Module):
"""
Implementation of SelfAttentionPooling
Original Paper: Self-Attention Encoding and Pooling for Speaker Recognition
https://arxiv.org/pdf/2008.01077v1.pdf
"""
def __init__(self, input_dim):
super(SelfAttentionPooling, self).__init__()
self.W = nn.Linear(input_dim, 1)
def forward(self, batch_rep):
"""
input:
batch_rep : size (N, T, H), N: batch size, T: sequence length, H: Hidden dimension
attention_weight:
att_w : size (N, T, 1)
return:
utter_rep: size (N, H)
"""
softmax = nn.functional.softmax
att_w = softmax(self.W(batch_rep).squeeze(-1)).unsqueeze(-1)
utter_rep = torch.sum(batch_rep * att_w, dim=1)
return utter_rep
AMSoftmaxLoss 為:
import torch
import torch.nn as nn
import torch.nn.functional as F
class AdMSoftmaxLoss(nn.Module):
def __init__(self, embedding_dim, no_classes, scale = 30.0, margin=0.4):
'''
Additive Margin Softmax Loss
Attributes
----------
embedding_dim : int
Dimension of the embedding vector
no_classes : int
Number of classes to be embedded
scale : float
Global scale factor
margin : float
Size of additive margin
'''
super(AdMSoftmaxLoss, self).__init__()
self.scale = scale
self.margin = margin
self.embedding_dim = embedding_dim
self.no_classes = no_classes
self.embedding = nn.Embedding(no_classes, embedding_dim, max_norm=1)
self.loss = nn.CrossEntropyLoss()
def forward(self, x, labels=None):
'''
Input shape (N, embedding_dim)
'''
n, m = x.shape
assert m == self.embedding_dim
if labels != None:
assert n == len(labels)
assert torch.min(labels) >= 0
assert torch.max(labels) < self.no_classes
x = F.normalize(x, dim=1)
w = self.embedding.weight
cos_theta = torch.matmul(w, x.T).T
psi = cos_theta - self.margin
logits = None
err = None
if labels != None:
onehot = F.one_hot(labels, self.no_classes)
logits = self.scale * torch.where(onehot == 1, psi, cos_theta)
err = self.loss(logits, labels)
else:
logits = cos_theta
return logits, err
訓練 350000 個 epoch,初始 lr1e-3,warmup5000 個 epoch,最後訓練結果還差一點,可能把初始 lr 調低一點訓練長一點的 epoch 或結合 ensemble 會能過 boss,還差一點就沒再調了。
結果如下:
觀察到 train_acc 很早就到 1 了,我想採取一定數據增強可能會有所改善。