Strive236

Strive236

生命的起伏要认可
github
zhihu

ML2022Spring HW4

这次的 HW 是语者身份辨识任务,对输入的一段语音,模型需要能辨识出说话人的身份类别,也是一个分类任务,使用的数据集是 VoxCeleb,其具体情况如下:

image

课程目标是学习怎么样使用 Transformer,其将 RNN 考虑整个序列的优点和 CNN 并行计算处理的优点结合起来。

本次 hw 使用的模型的基础框架如下:

image

具体的任务要求如下:

image

Hints#

image

Simple#

注意给出的代码里面使用 wget 下载数据集的链接里已经没东西了,可以在官方给的这个链接处进行下载

本实验使用的数据是已经已经对原始波形进行预处理后得到的梅尔频谱图。

由于不同的音频的长度可能不同,所以训练时将不同长度的输入音频​​并将其分割为固定长度的片段(Segment)供模型学习。

image

image

注意,上述过程中长度小于所选 segment,在后续会被 pad(填充)处理:

image

Simple 的要求如下,只需要直接 run 它的 sample code 即可

image

结果如下:

image

image

Medium#

Medium 的要求如下:

image

首先改了一下 pred_layer 中的 hidden_layer 的维度,虽然在 train_set 和 val_set 的结果很好,但是放在 kaggle 上只提高了 0.2 左右。

image

在此基础上,进行了较多改动

image

但是发现在 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。

image

image

Strong#

Strong 要求构建 Conformer:

image

单独使用 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 左右:

image

kaggle 上结果如下

image

成功过掉 strong,还超了一点。

Boss#

boss 要求如下:

image

使用了开源的self-attention poolingadditive 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,还差一点就没再调了。

结果如下:

1748793651930

观察到 train_acc 很早就到 1 了,我想采取一定数据增强可能会有所改善。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。