Strive236

Strive236

生命的起伏要认可
github
zhihu

ML2022春 HW4

この HW は話者のアイデンティティ識別タスクであり、入力された音声の一部に対して、モデルは話者のアイデンティティのカテゴリを識別する必要があります。これは分類タスクであり、使用されるデータセットは VoxCeleb で、具体的な状況は以下の通りです:

image

コースの目標は、Transformer の使用方法を学ぶことであり、RNN が全体のシーケンスを考慮する利点と CNN の並列計算処理の利点を組み合わせています。

今回の HW で使用するモデルの基本フレームワークは以下の通りです:

image

具体的なタスクの要件は以下の通りです:

image

ヒント#

image

簡単#

提供されたコードの中で使用されている wget でデータセットをダウンロードするリンクにはすでに何もありませんので、公式が提供したこのリンクからダウンロードできます。

本実験で使用するデータは、元の波形を前処理して得られたメルスペクトログラムです。

異なる音声の長さが異なる可能性があるため、トレーニング時には異なる長さの入力音声を固定長のセグメントに分割してモデルが学習できるようにします。

image

つまり

image

注意:上記のプロセスで長さが選択したセグメントよりも短い場合、後でパディング処理が行われます:

image

簡単な要件は以下の通りで、サンプルコードを直接実行するだけで済みます。

image

結果は以下の通りです:

image

image

中級#

中級の要件は以下の通りです:

image

まず、pred_layer の hidden_layer の次元を変更しました。train_set と val_set の結果は良好でしたが、kaggle に置いたところ、約 0.2 しか向上しませんでした。

image

この基礎の上に、多くの変更を行いました。

image

しかし、40000 エポックを超えたところで train_acc が 1 に達しましたが、最終的な val_acc は 0.85 で、kaggle の結果は約 0.66 でした。モデルアーキテクチャを調整してみます。

nhead と num_layer をそれぞれ 4 に引き上げたところ、val_acc は約 0.86 に上昇し、kaggle の結果は約 0.7 に向上しました。微調整が必要です。

この基礎の上に、pred_layer の relu の後にドロップアウト (0.1) を追加したところ、最終的な val_acc は約 0.87 に上昇し、kaggle の結果は約 0.71 に向上し、中級をクリアしました。

image

image

強化#

強化の要件は Conformer の構築です:

image

単独で Transformer を使用すると、音声信号の局所的な特徴を十分に掘り下げることが難しく、単独で CNN を使用すると、グローバルな依存関係を効率的に捉えることが難しいです。したがって、Transformer と CNN の利点を組み合わせて、音声信号の局所的およびグローバルな特徴を同時に効率的にモデル化できるモデルアーキテクチャを設計することが非常に重要です。Conformer はそのようなアーキテクチャの一つであり、手動で実装するのは非常に面倒であり、構造やコードを再度確認することもありませんでした。このため、torchaudio 内のConformerを使用して実装しました。具体的な変更は以下の通りです:

エンコーダを次のように変更します。

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 時には各シーケンスの長さを入力する必要があるため、バッチを取得する方法を変更する必要があります:

  • トレーニングおよび検証バッチ:
def collate_batch(batch):
	# バッチ内の特徴を処理します。
	"""データのバッチをまとめます。"""
	mel, speaker = zip(*batch)
	lengths = torch.FloatTensor([m.size(0) for m in mel])
	# モデルをバッチごとにトレーニングするため、同じバッチ内の特徴の長さを同じにする必要があります。
	mel = pad_sequence(mel, batch_first=True, padding_value=-20)    # pad log 10^(-20)は非常に小さな値です。
	# mel: (バッチサイズ, 長さ, 40)
	return mel, lengths, torch.FloatTensor(speaker).long()
  • テストバッチ:
def inference_collate_batch(batch):
	"""データのバッチをまとめます。"""
	feat_paths, mels = zip(*batch)
	lengths = torch.FloatTensor([m.size(0) for m in mels])
	return feat_paths, lengths, torch.stack(mels)

その後、model_fn でバッチを取得する場所を対応して変更すれば大丈夫です。

また、モデルが大きくなったため、一般的に学習率は少し下げるべきです。ここでは 5e-4 に設定しました。また、学習率を下げたため、トレーニング時間を 140000 エポックに延長し、ウォームアップも 2000 エポックにしました。トレーニング結果は、val_acc が約 0.92 に向上しました:

image

kaggle の結果は以下の通りです。

image

強化を成功裏にクリアし、少し超えました。

ボス#

ボスの要件は以下の通りです:

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__()
		# 入力の特徴の次元をd_modelに投影します。
		self.prenet = nn.Linear(40, d_model)
		# TODO:
		#   Transformerを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)

		# d_modelからスピーカー数への特徴の次元を投影します。
		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: (バッチサイズ, 長さ, 40)
		return:
			out: (バッチサイズ, n_spks)
		"""
		# out: (バッチサイズ, 長さ, d_model)
		out = self.prenet(mels)
		# # out: (長さ, バッチサイズ, d_model)
		# out = out.permute(1, 0, 2)
		# エンコーダレイヤーは(長さ, バッチサイズ, d_model)の形状の特徴を期待します。
		out, _ = self.encoder(out, lengths)
		# # out: (バッチサイズ, 長さ, d_model)
		# out = out.transpose(0, 1)
		# 平均プーリング
		# stats = out.mean(dim=1)
		stats = self.pooling(out)

		# out: (バッチ, 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):
    """
    SelfAttentionPoolingの実装
    元の論文: 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):
        """
        入力:
            batch_rep : サイズ (N, T, H), N: バッチサイズ, T: シーケンス長, H: 隠れ次元
        
        注意重み:
            att_w : サイズ (N, T, 1)
        
        戻り値:
            utter_rep: サイズ (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


        属性
        ----------
        embedding_dim : int 
            埋め込みベクトルの次元
        no_classes : int
            埋め込むクラスの数
        scale : float
            グローバルスケールファクター
        margin : float
            加算マージンのサイズ        
        '''
        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):
        '''
        入力形状 (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 エポックをトレーニングし、初期 lr を 1e-3、ウォームアップを 5000 エポックに設定しました。最終的なトレーニング結果はまだ少し足りず、初期 lr を少し下げてトレーニングエポックを長くするか、アンサンブルを組み合わせることでボスをクリアできるかもしれませんが、もう少しで調整をやめました。

結果は以下の通りです:

1748793651930

train_acc が非常に早く 1 に達したため、一定のデータ拡張を行うことで改善が見込まれるかもしれません。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。