这次的 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 了,我想采取一定数据增强可能会有所改善。