この宿題は、Transformer を学習した後に完了する必要があります。
ディレクトリのタイトル
タスク
話者識別タスクでは、全部で 600 人の話者がいて、各話者の音声特徴がトレーニングのために与えられ、test_feature を通じて話者識別が実行されます。(本質的にはまだ分類タスク分類)
シンプル (0.60824): サンプル コードを実行し、変換器の使用方法を知っている 中
(0.70375): 変換器のパラメーターを調整する方法を知っている
強い (0.77750): コンフォマーを構築する
ボス (0.86500): 自己注意プーリングを実装するおよび加算マージンソフトマックス
ティーチングアシスタントのサンプルコード解釈
データセット分析
-
mapping.json ファイルは、
スピーカーの ID を 0 ~ 599 の数字にマップします。これは、音声を分類する必要がある 600 の異なるスピーカーがあるためです。 -
metadata.json ファイルにはトレーニング データが格納されます. この実験では特に検証データを設定しません.トレーニング データから
検証データ n_mels を分割する必要があります: 音声データを処理するときは、これを表す各時間次元から n_mels 次元を選択
機能
スピーカー: スピーカーの ID とすべての機能をキー値の形式で保存します (各スピーカーには複数の機能があります)
feature_path: この機能のファイル名
mel_len: 各機能の長さ (それぞれが異なる場合があり、後で処理されます) -
testdata.json ファイルは、
話者の識別を実行する必要があるメタデータ形式に似ています。発話: 談話; スピーチ
データセット
この実験のデータは、現実世界の中国語話者の音声である Voxceleb2 音声データ セットから取得されます. 600 人の話者が宿題で選択され、その音声で訓練されました.
import os
import json
import torch
import random
from pathlib import Path
from torch.utils.data import Dataset
from torch.nn.utils.rnn import pad_sequence
class myDataset(Dataset):
def __init__(self, data_dir, segment_len=128):
self.data_dir = data_dir
self.segment_len = segment_len
# Load the mapping from speaker neme to their corresponding id.
mapping_path = Path(data_dir) / "mapping.json" #mapping_path: Dataset\mapping.json
mapping = json.load(mapping_path.open())
#mapping: {'speaker2id': {'id00464': 0, 'id00559': 1,
self.speaker2id = mapping["speaker2id"]
#self.speaker2id: {'id00464': 0, 'id00559': 1, 'id00578': 2, 'id00905': 3,...
# Load metadata of training data.
metadata_path = Path(data_dir) / "metadata.json"
metadata = json.load(open(metadata_path))["speakers"] #metadata中存放的key是speaker_id,value是每个speaker的feature和对应长度
# Get the total number of speaker.
self.speaker_num = len(metadata.keys())
self.data = []
for speaker in metadata.keys(): #遍历每一个spearker_id
for utterances in metadata[speaker]: #通过speaker_id取出speaker的所有feature和len
"""
utterances格式:
{'feature_path': 'uttr-18e375195dc146fd8d14b8a322c29b90.pt', 'mel_len': 435}
{'feature_path': 'uttr-da9917d5853049178487c065c9e8b718.pt', 'mel_len': 490}...
"""
self.data.append([utterances["feature_path"], self.speaker2id[speaker]])
#self.data:[['uttr-18e375195dc146fd8d14b8a322c29b90.pt', 436],
# ['uttr-da9917d5853049178487c065c9e8b718.pt', 436],...
#一共600个speaker,436表示第436个speaker
def __len__(self):
return len(self.data)
def __getitem__(self, index):
feat_path, speaker = self.data[index] #feature和speaker编号[0,599]
# Load preprocessed mel-spectrogram.
mel = torch.load(os.path.join(self.data_dir, feat_path)) #加载feature
#mel.size():torch.Size([490, 40])
# Segmemt mel-spectrogram into "segment_len" frames.
if len(mel) > self.segment_len: #将feature切片成固定长度
# Randomly get the starting point of the segment.
start = random.randint(0, len(mel) - self.segment_len) #随机选取切片起始点
# Get a segment with "segment_len" frames.
mel = torch.FloatTensor(mel[start:start+self.segment_len])#截取长度为segment_len的片段 mel.size():torch.Size([128, 40])
else:
mel = torch.FloatTensor(mel) #为什么小于segment_len不填充? 填充在dataloader中完成
# Turn the speaker id into long for computing loss later.
speaker = torch.FloatTensor([speaker]).long() #将speaker的编号转为long类型
return mel, speaker
def get_speaker_number(self):
return self.speaker_num #600
データローダー
主なタスク: 1. 検証セットの分割 2. 長さが segment_len 未満のメルのパディング 3. データローダーの生成
import torch
from torch.utils.data import DataLoader, random_split
from torch.nn.utils.rnn import pad_sequence
def collate_batch(batch): #用于整理数据的函数,参数为dataloader中的一个batch
# Process features within a batch.
"""Collate a batch of data."""
mel, speaker = zip(*batch) #zip拆包,将一个batch中的mel和speaker分开,各自单独形成一个数组
# 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中元素长度不相同时,将所有的mel元素填充到最长的元素的长度,填充的值由padding_value决定
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, torch.FloatTensor(speaker).long()
def get_dataloader(data_dir, batch_size, n_workers):
"""Generate dataloader"""
dataset = myDataset(data_dir)
speaker_num = dataset.get_speaker_number()
# Split dataset into training dataset and validation dataset
trainlen = int(0.9 * len(dataset))
lengths = [trainlen, len(dataset) - trainlen]
trainset, validset = random_split(dataset, lengths) #无覆盖的随机划分训练集和验证集
train_loader = DataLoader(
trainset,
batch_size=batch_size,
shuffle=True,
drop_last=True,
num_workers=n_workers,
pin_memory=True,
collate_fn=collate_batch,
)
valid_loader = DataLoader(
validset,
batch_size=batch_size,
num_workers=n_workers,
drop_last=True,
pin_memory=True,
collate_fn=collate_batch,
)
return train_loader, valid_loader, speaker_num
モデル
最も重要な部分、Transformer による
Transformer インフラストラクチャの使用は、次の論文から得られます。Attention Is All You Need
論文の解釈: Master Li Mu の論文を読み、それを使用するのは良いことです
これが分類タスクです。公式の pytorch ドキュメントの Encoder 部分を使用するだけで済みます: torch.nn.TransformerEncoderLayer
import torch
import torch.nn as nn
import torch.nn.functional as F
class Classifier(nn.Module):
def __init__(self, d_model=80, n_spks=600, dropout=0.1):
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
#对于文本分类等下游任务,只需要用到Encoder部分即可
#nhead:multi_head_attention中head个数
#d_model:输入的feature的个数
#dim_feedforward:feedforward network的维度
#dropout默认0.1
self.encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model, dim_feedforward=256, nhead=2
)
# self.encoder = nn.TransformerEncoder(self.encoder_layer, num_layers=2)
# Project the the dimension of features from d_model into speaker nums.
self.pred_layer = nn.Sequential(
nn.Linear(d_model, d_model),
nn.ReLU(),
nn.Linear(d_model, n_spks),
)
def forward(self, mels):
"""
args:
mels: (batch size, length, 40)
return:
out: (batch size, n_spks)
"""
# out: (batch size, length, d_model) length=segment_len
out = self.prenet(mels)
# out: (length, batch size, d_model)
out = out.permute(1, 0, 2) #交换dim=0和dim=1
# The encoder layer expect features in the shape of (length, batch size, d_model).
out = self.encoder_layer(out)
# out: (batch size, length, d_model)
out = out.transpose(0, 1) #转置dim=0和dim=1
# mean pooling
stats = out.mean(dim=1) #可以理解为求平均并去除维度1 stats.size():(batch_size,d_model)
# out: (batch, n_spks)
out = self.pred_layer(stats)
return out
学習率スケジュール
バッチ設定が比較的大きい場合、通常は比較的大きな学習率が必要ですが (通常、batch_size は学習率に比例します)、トレーニングの開始時にパラメーターがランダムに初期化され、勾配が比較的大きいため、学習率はも比較的大きいため、トレーニングが不安定になります。
ウォーミングアップの方法は、反復の最初の数ラウンドで比較的小さい学習率を使用し、勾配が一定のレベルまで低下した後に初期の学習率を復元するというものです ------
「ニューラル ネットワークと深層学習」
import math
import torch
from torch.optim import Optimizer
from torch.optim.lr_scheduler import LambdaLR
def get_cosine_schedule_with_warmup(
optimizer: Optimizer,
num_warmup_steps: int,
num_training_steps: int,
num_cycles: float = 0.5,
last_epoch: int = -1,
):
"""
Create a schedule with a learning rate that decreases following the values of the cosine function between the
initial lr set in the optimizer to 0, after a warmup period during which it increases linearly between 0 and the
initial lr set in the optimizer.
Args:
optimizer (:class:`~torch.optim.Optimizer`):
The optimizer for which to schedule the learning rate.
num_warmup_steps (:obj:`int`):
The number of steps for the warmup phase.
num_training_steps (:obj:`int`):
The total number of training steps.
num_cycles (:obj:`float`, `optional`, defaults to 0.5):
The number of waves in the cosine schedule (the defaults is to just decrease from the max value to 0
following a half-cosine).
last_epoch (:obj:`int`, `optional`, defaults to -1):
The index of the last epoch when resuming training.
Return:
:obj:`torch.optim.lr_scheduler.LambdaLR` with the appropriate schedule.
"""
def lr_lambda(current_step):
# Warmup
if current_step < num_warmup_steps:
return float(current_step) / float(max(1, num_warmup_steps))
# decadence
progress = float(current_step - num_warmup_steps) / float(
max(1, num_training_steps - num_warmup_steps)
)
return max(
0.0, 0.5 * (1.0 + math.cos(math.pi * float(num_cycles) * 2.0 * progress))
)
return LambdaLR(optimizer, lr_lambda, last_epoch)
モデル機能
カスタム モデルの forward 部分を呼び出し、バッチがトラバースされるたびに model_fn を呼び出します
import torch
def model_fn(batch, model, criterion, device):
"""Forward a batch through the model."""
mels, labels = batch
#print("model_fn_mels.size():",mels.size())
# out:torch.Size([16, 128, 40]) [batch_size,segment_len,40]
mels = mels.to(device)
labels = labels.to(device)
outs = model(mels)
loss = criterion(outs, labels)
# Get the speaker id with highest probability.
preds = outs.argmax(1)
# Compute accuracy.
accuracy = torch.mean((preds == labels).float())
return loss, accuracy
検証
検証セットの精度を計算する
from tqdm import tqdm
import torch
def valid(dataloader, model, criterion, device):
"""Validate on validation set."""
model.eval()
running_loss = 0.0
running_accuracy = 0.0
#验证集5667个
pbar = tqdm(total=len(dataloader.dataset), ncols=0, desc="Valid", unit=" uttr")
for i, batch in enumerate(dataloader):
with torch.no_grad():
loss, accuracy = model_fn(batch, model, criterion, device)
running_loss += loss.item()
running_accuracy += accuracy.item()
pbar.update(dataloader.batch_size)
pbar.set_postfix(
loss=f"{
running_loss / (i+1):.2f}",
accuracy=f"{
running_accuracy / (i+1):.2f}",
)
pbar.close()
model.train()
return running_accuracy / len(dataloader)
メイン機能
以前の宿題とは異なり、モデルの実行を開始します。最初のいくつかのジョブは、エポック、つまり完全なトレーニング セットを実行し、次に検証セットの実行を開始します。ここでは、valid_steps バッチを実行し、検証セットを再度実行します。
from tqdm import tqdm
import torch
import torch.nn as nn
from torch.optim import AdamW
from torch.utils.data import DataLoader, random_split
def parse_args():
"""arguments"""
config = {
"data_dir": "./Dataset",
"save_path": "model.ckpt",
"batch_size": 16,
"n_workers": 0,
"valid_steps": 2000,
"warmup_steps": 1000,
"save_steps": 10000,
"total_steps": 70000,
}
return config
def main(
data_dir,
save_path,
batch_size,
n_workers,
valid_steps,
warmup_steps,
total_steps,
save_steps,
):
"""Main function."""
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"[Info]: Use {
device} now!")
train_loader, valid_loader, speaker_num = get_dataloader(data_dir, batch_size, n_workers)
train_iterator = iter(train_loader) #iter()生成迭代器,以batch为单位
#print("train_iterator:",train_iterator) #<torch.utils.data.dataloader._SingleProcessDataLoaderIter object at 0x000001FD07C558D0>
print(f"[Info]: Finish loading data!",flush = True)
model = Classifier(n_spks=speaker_num).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = AdamW(model.parameters(), lr=1e-3)
scheduler = get_cosine_schedule_with_warmup(optimizer, warmup_steps, total_steps) #上面定义的warm up函数
print(f"[Info]: Finish creating model!",flush = True)
best_accuracy = -1.0
best_state_dict = None
pbar = tqdm(total=valid_steps, ncols=0, desc="Train", unit=" step")
#train valid_steps个batch再跑验证集
for step in range(total_steps): #一共运行total_Steps轮,这里没有epoch的概念
# Get data
try:
batch = next(train_iterator) #next()返回迭代器的下一个项目,即下一个batch
#print("batch[0].size():",batch[0].size())
#out:torch.Size([16, 128, 40]) [batch_size,segment_len,40]
except StopIteration: # 不指定 default 且迭代器元素耗尽, 将引发 StopIteration 异常
train_iterator = iter(train_loader)
batch = next(train_iterator)
loss, accuracy = model_fn(batch, model, criterion, device) #计算当前batch的loss和acc
#print("loss:",loss) #tensor(6.3915, device='cuda:0', grad_fn=<NllLossBackward0>)
batch_loss = loss.item() # loss是张量,item()可以取出张量中的值
#print("batch_loss:",batch_loss) #batch_loss: 6.391468048095703
batch_accuracy = accuracy.item()
# Updata model 反向传播更新参数,每跑一个batch都会更新
loss.backward()
optimizer.step()
scheduler.step()
optimizer.zero_grad()
# Log
pbar.update() #打印当前loss和acc
pbar.set_postfix(
loss=f"{
batch_loss:.2f}",
accuracy=f"{
batch_accuracy:.2f}",
step=step + 1,
)
# Do validation
if (step + 1) % valid_steps == 0: #经过valid_steps开始跑验证集
pbar.close()
valid_accuracy = valid(valid_loader, model, criterion, device) #计算valid_acc
# keep the best model
if valid_accuracy > best_accuracy:
best_accuracy = valid_accuracy
best_state_dict = model.state_dict() #保存模型参数
pbar = tqdm(total=valid_steps, ncols=0, desc="Train", unit=" step")
# Save the best model so far.
if (step + 1) % save_steps == 0 and best_state_dict is not None: #每save_steps轮会保存一次当前最好模型
torch.save(best_state_dict, save_path)
pbar.write(f"Step {
step + 1}, best model saved. (accuracy={
best_accuracy:.4f})")
pbar.close()
if __name__ == "__main__":
main(**parse_args())
推論
推論: 推論、テスト データを
トレーニングに例えて実行するだけ
推論の主な機能
主な機能に類似
サンプルコードのスコア
中くらい
中程度のパラメーターを調整します
d_model=160
n_head=8
num_layers=2
線形レイヤー: 1 レイヤー
total_steps=100000
このラウンドのトレーニングの正解率は 100% ですが、13 ステップしか実行されていませんが、損失からオーバー フィッティングがあることがわかります。
強い
トランスフォーマー -> コンフォーマー
結果が最初に表示され、重大な
オーバーフィッティングはありません. トレーニング セットと検証セットの両方でオーバーフィッティング現象が見られます. 検証セットの正解率はテスト セットの正解率よりもはるかに高くなっています.
論文アドレス:コンフォーマー
コンフォーマーの考え方は非常にシンプルで、Transformer と CNN を組み合わせるというものです。理由:
1. Transformer のアテンション メカニズムにより、優れたグローバル性を備えています。
2. CNN は局所性が高く、きめ細かな情報を抽出できます。
この 2 つを組み合わせることで、音声に対する効果が向上します。具体的なモデル アーキテクチャについては、論文で説明されています。
- 最初の pip コンフォーマー パッケージ
!pip install conformer
- コンフォーマーパッケージをインポートする
from conformer import ConformerBlock
- モジュールの変更
import torch
import torch.nn as nn
import torch.nn.functional as F
class Classifier(nn.Module):
def __init__(self, d_model=512, n_spks=600, dropout=0.1):
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
#对于文本分类等下游任务,只需要用到Encoder部分即可
#nhead:multi_head_attention中head个数
#d_model:输入的feature的个数
#dim_feedforward:feedforward network的维度
#dropout默认0.1
#self.encoder_layer = nn.TransformerEncoderLayer(
#d_model=d_model, dim_feedforward=256, nhead=8
#)
#self.encoder = nn.TransformerEncoder(self.encoder_layer, num_layers=2)
self.conformer_block=ConformerBlock(
dim=d_model,
dim_head=64,
heads=8,
ff_mult=4,
conv_expansion_factor=2,
conv_kernel_size=31,
attn_dropout=dropout,
ff_dropout=dropout,
conv_dropout=dropout
)
# Project the the dimension of features from d_model into speaker nums.
self.pred_layer = nn.Sequential(
#nn.Linear(d_model, d_model),
#nn.ReLU(),
nn.Linear(d_model, n_spks),
)
def forward(self, mels):
"""
args:
mels: (batch size, length, 40)
return:
out: (batch size, n_spks)
"""
# out: (batch size, length, d_model) length=segment_len
out = self.prenet(mels)
# out: (length, batch size, d_model)
out = out.permute(1, 0, 2) #交换dim=0和dim=1
# The encoder layer expect features in the shape of (length, batch size, d_model).
out = self.conformer_block(out)
# out: (batch size, length, d_model)
out = out.transpose(0, 1) #转置dim=0和dim=1
# mean pooling
stats = out.mean(dim=1) #可以理解为求平均并去除维度1 stats.size():(batch_size,d_model)
# out: (batch, n_spks)
out = self.pred_layer(stats)
return out
自己注意プーリング
自己注意プーリングの論文は
主に論文中の自己注意プーリングのアーキテクチャに注目しています. 平均プーリングと比較して, 自己注意プーリングは学習可能なパラメータを介してプーリングを実行します. 平均プーリングと比較して, いくつかの情報を抽出することができます. コードを
説明するには、大物のビデオを参照してください。
#self attention pooling类实现
import torch.nn.functional as F
import torch.nn as nn
class Self_Attentive_Pooling(nn.Module):
def __init__(self,dim):
super(Self_Attentive_Pooling,self).__init__()
self.sap_linear=nn.Linear(dim,dim)
self.attention=nn.Parameter(torch.FloatTensor(dim,1))
def forward(self,x):
x=x.permute(0,2,1)
h=torch.tanh(self.sap_linear(x))
w=torch.matmul(h,self.attention).squeeze(dim=2)
w=F.softmax(w,dim=1).view(x.size(0),x.size(1),1)
x=torch.sum(x*w,dim=1)
return x
モデルを変更します。
import torch
import torch.nn as nn
import torch.nn.functional as F
class Classifier(nn.Module):
def __init__(self, d_model=512, n_spks=600, dropout=0.1):
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
#对于文本分类等下游任务,只需要用到Encoder部分即可
#nhead:multi_head_attention中head个数
#d_model:输入的feature的个数
#dim_feedforward:feedforward network的维度
#dropout默认0.1
#self.encoder_layer = nn.TransformerEncoderLayer(
#d_model=d_model, dim_feedforward=256, nhead=8
#)
#self.encoder = nn.TransformerEncoder(self.encoder_layer, num_layers=2)
self.conformer_block=ConformerBlock(
dim=d_model,
dim_head=64,
heads=8,
ff_mult=4,
conv_expansion_factor=2,
conv_kernel_size=31,
attn_dropout=dropout,
ff_dropout=dropout,
conv_dropout=dropout
)
# Project the the dimension of features from d_model into speaker nums.
self.pooling=Self_Attentive_Pooling(d_model)
self.pred_layer = nn.Sequential(
#nn.Linear(d_model, d_model),
#nn.ReLU(),
nn.Linear(d_model, n_spks),
)
def forward(self, mels):
"""
args:
mels: (batch size, length, 40)
return:
out: (batch size, n_spks)
"""
# out: (batch size, length, d_model) length=segment_len
out = self.prenet(mels)
# out: (length, batch size, d_model)
out = out.permute(1, 0, 2) #交换dim=0和dim=1
# The encoder layer expect features in the shape of (length, batch size, d_model).
out = self.conformer_block(out)
# out: (batch size, length, d_model)
#out = out.transpose(0, 1) #转置dim=0和dim=1
# mean pooling
#stats = out.mean(dim=1) #可以理解为求平均并去除维度1 stats.size():(batch_size,d_model)
out=out.permute(1,2,0)
stats=self.pooling(out)
# out: (batch, n_spks)
out = self.pred_layer(stats)
return out
total_steps=70000
total_steps=100000