Strive236

Strive236

生命的起伏要认可
github
zhihu

ML2022春季作業4

這次的 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 了,我想採取一定數據增強可能會有所改善。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。