この HW は話者のアイデンティティ識別タスクであり、入力された音声の一部に対して、モデルは話者のアイデンティティのカテゴリを識別する必要があります。これは分類タスクであり、使用されるデータセットは VoxCeleb で、具体的な状況は以下の通りです:
コースの目標は、Transformer の使用方法を学ぶことであり、RNN が全体のシーケンスを考慮する利点と CNN の並列計算処理の利点を組み合わせています。
今回の HW で使用するモデルの基本フレームワークは以下の通りです:
具体的なタスクの要件は以下の通りです:
ヒント#
簡単#
提供されたコードの中で使用されている wget でデータセットをダウンロードするリンクにはすでに何もありませんので、公式が提供したこのリンクからダウンロードできます。
本実験で使用するデータは、元の波形を前処理して得られたメルスペクトログラムです。
異なる音声の長さが異なる可能性があるため、トレーニング時には異なる長さの入力音声を固定長のセグメントに分割してモデルが学習できるようにします。
つまり
注意:上記のプロセスで長さが選択したセグメントよりも短い場合、後でパディング処理が行われます:
簡単な要件は以下の通りで、サンプルコードを直接実行するだけで済みます。
結果は以下の通りです:
中級#
中級の要件は以下の通りです:
まず、pred_layer の hidden_layer の次元を変更しました。train_set と val_set の結果は良好でしたが、kaggle に置いたところ、約 0.2 しか向上しませんでした。
この基礎の上に、多くの変更を行いました。
しかし、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 に向上し、中級をクリアしました。
強化#
強化の要件は Conformer の構築です:
単独で 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 に向上しました:
kaggle の結果は以下の通りです。
強化を成功裏にクリアし、少し超えました。
ボス#
ボスの要件は以下の通りです:
オープンソースの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__()
# 入力の特徴の次元を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 を少し下げてトレーニングエポックを長くするか、アンサンブルを組み合わせることでボスをクリアできるかもしれませんが、もう少しで調整をやめました。
結果は以下の通りです:
train_acc が非常に早く 1 に達したため、一定のデータ拡張を行うことで改善が見込まれるかもしれません。