2.6K Views
December 07, 23
スライド概要
Pythonで学ぶ音声認識の輪読会第8回の発表スライドです。
2023年12月7日(木) 18:30~
AI・機械学習を勉強したい学生たちが集まる、京都大学の自主ゼミサークルです。私たちのサークルに興味のある方はX(Twitter)をご覧ください!
後期輪読会#8 DNN-HMMの実装 京都大学 理学部 1回生 千葉 一世 0
DNN-HMMの実装 目次 1. 学習データの正解ラベル作成 2. DNNモデル, Dataset作成 3. DNNの学習 4. DNN-HMMによる孤立単語音声認識 githubのソースコードURL 1
DNNの正解ラベル : 状態アライメントの推定 5章で学習したHMMを用いて、学習データのアライメントを推定する 音素アライメントが状態アライメントになっている以外は5章と同じく viterbiアルゴリズムのバックトラックを用いる 今回は各音素につき3状態を考える for vp in viterbi_path: l = vp[0] # ラベル上の音素インデクスを取得 p = label[l] # 音素番号を音素リスト上の番号に変換 音素アライメント ph = self.phones[p] # 番号から音素記号に変換 phone_alignment.append(ph) # phone_alignmentの末尾に追加 for vp in viterbi_path: l = vp[0] # ラベル上の音素インデクスを取得 p = label[l] # 音素番号を音素リスト上の番号に変換 状態アライメント s = vp[1] # 音素内の状態番号を取得 # 出力時の状態番号は p * num_states + s とする state = p * self.num_states + s state_alignment.append(state) # state_alignmentの末尾に追加 2
DNNの正解ラベル:状態アライメントの推定 上記のように出力 音素数35 × 状態数3 =105 個の状態を 0~104の数字に対応付ける この正解ラベルから、各状態の出現回数をカウントする 事前発生確率 𝑝 𝑃(𝑠𝑗 ) を求めるのに用いる (ゼロ除算を防ぐために、一度も出現しない状態の出現回数は1する) 3
DNNの実装 4
Pytorchのtensor Pytorch では、Tensor型のデータを扱う Tensorは基本的な部分はnumpyのndarrayなどと同じ Tensorを用いる利点 ⚫GPUを用いた計算が可能 計算の高速化 ⚫勾配の情報を保持する 自動微分など、逆伝播が容易 5
DNNモデル Pytorchのtorch.nn.Moduleを継承して、以下のようなモデルを作成 今回は dim_in = 13 * 11 dim_hidden = 1024 dim_out = 35*5 num_layers = 4 LeCunの初期値を用いる 6
DNNモデル class MyDNN(nn.Module): def __init__(self, dim_in, dim_hidden, dim_out, num_layers=2): super(MyDNN, self).__init__() self.num_layers = num_layers self.inp = nn.Sequential( nn.Linear(dim_in, dim_hidden), nn.ReLU()) hidden = [] for n in range(self.num_layers): hidden.append(nn.Linear(dim_hidden, dim_hidden)) hidden.append(nn.ReLU()) self.hidden = nn.ModuleList(hidden) self.out = nn.Linear(in_features=dim_hidden, out_features=dim_out) lecun_initialization(self) #パラメータの初期化 def forward(self, frame): output = self.inp(frame) for n in range(self.num_layers): output = self.hidden[n](output) output = self.out(output) return output Pytorchでは、順伝播処理を定義すれば 自動で逆伝播処理もしてれる 7
LeCunの初期化 def lecun_initialization(model): for param in model.parameters(): data = param.data # パラメータの値を取り出す dim = data.dim() # パラメータの次元数を取り出す if dim == 1: # 次元数を元に処理を変える # dim = 1 の場合はバイアス成分 # ゼロに初期化する data.zero_() elif dim == 2: # dim = 2 の場合は線形射影の重み行列 # 入力次元数 = size(1) を得る n = data.size(1) # 入力次元数の平方根の逆数を # 標準偏差とする正規分布乱数で初期化 std = 1.0 / np.sqrt(n) data.normal_(0, std) LeCunの初期化 8
Dataset Torch.utils.data.Datasetを継承し、 ミニバッチデータを作成するためのクラスを作成する torch.utils.data.Dataloader を用いてミニバッチを作成するためには __len__と、__getitem__を定義する必要がある __len__ は、len(Dataset) としたときの挙動を定める __getitem__ は、Dateset[id] としたときの挙動を定める 9
Dataset __init__ class SequenceDataset(Dataset): def __init__(self, feat_scp, label_scp, feat_mean, feat_std, pad_index, splice=0): ≀ self.id_list # 各発話のID self.feat_list # 各発話の特徴量ファイルへのパス self.feat_len_list # 各発話の特徴量フレーム数 self.label_list # 各発話のラベル self.label_len_list # 各発話のラベルの長さ self.num_utts # 発話数 self.feat_mean, self.feat_std, self.splice feat_scp, label_scpのファイルから 抽出して保存しておく ≀ for n in range(self.num_utts): # 埋めるフレームの数 = 最大フレーム数 - 自分のフレーム数 pad_len = np.max(self.feat_len_list) - self.label_len_list[n] # pad_indexの値で埋める self.label_list[n] = np.pad( self.label_list[n], [0, pad_len], mode='constant', constant_values=self.pad_index) ミニバッチ処理のために ラベルのサイズをそろえる 損失を求めるときには、nn.CrossEntropyLoss(ignore_index=pad_index)として、 labelがpad_imdexになっている部分は計算しないようにする 10
Dataset __len__ , __getitem__ def __len__(self): return self.num_utts #データの総サンプル数=発話数を返す def __getitem__(self, idx): feat : 発話IDがidxの特徴量データを正規化したものを用意 # splicing: 前後 n フレームの特徴量を結合する org_feat = feat.copy() for n in range(-self.splice, self.splice+1): tmp = np.roll(org_feat, n, axis=0) # 元々の特徴量を n フレームずらす if n < 0: # 前にずらした場合は終端nフレームを0にする tmp[n:] = 0 elif n > 0: # 後ろにずらした場合は始端nフレームを0にする tmp[:n] = 0 else: continue # ずらした特徴量を次元方向に結合する feat = np.hstack([feat,tmp]) # 特徴量データのフレーム数を最大フレーム数に合わせるため,0で埋める # 特徴量,ラベル,フレーム数,ラベル長,発話IDを返す return (feat, label, feat_len, label_len, utt_id) feat以外はDataset内の 各リストから取り出すだけ 11
スプライシング feat = 𝑣1 , 𝑣2 , ・・・, 𝑣𝑖 , ・・・, 𝑣𝑛−1 , 𝑣𝑛 スプライシング feat = 𝑣1 𝑣1+𝑘 𝑣𝑘 ・ ・ ・ 0 0 0 ・・・ 𝑣𝑖 𝑣𝑖+𝑘 𝑣𝑖+𝑘−1 ・ 𝑣𝑖+1 𝑣𝑖−1 ・ 𝑣𝑖−𝑘+1 𝑣𝑖−𝑘 ・・・ 𝑣𝑛 0 0 ・ 𝑣𝑛−1 ・ 𝑣𝑛−𝑘+1 𝑣𝑛−𝑘 𝑣1 , 𝑣2 , ・・・, 𝑣𝑛−1 , 𝑣𝑛 kだけ右にずらす (𝑣𝑛−𝑘+1 , 𝑣𝑛−𝑘+2 , ・・・, 𝑣𝑛 , 𝑣1 , 𝑣2 , ・・・, 𝑣𝑛−𝑘−1 , 𝑣𝑛−𝑘 ) k番目以下を0にする (0 , 0 , ・・・ , 0 𝑣1 , 𝑣2 , ・・・, 𝑣𝑛−𝑘−1 , 𝑣𝑛−𝑘 ) 元のデータと結合 12
DNNの学習 オプティマイザをモメンタムSGD 損失関数をクロスエントロピーとし 、 Learning rate decay と Early stopping を用いて学習する Learning rate decay : 振動を防ぎ、収束を早めるために、 開発データの損失値が下がりに くくなった段階で、学習率を小さ くする手法 Early stopping : 過学習を防ぐために、開発デー タの損失値が下がらなくなった段 階で学習を止める手法 13
DNN学習のプログラム概要 for epoch in range(max_num_epoch): #エポックの数だけループ if early_stop_flag: #early_stop_flagがTrueになったら学習をやめる break #trainフェーズとvalidationフェーズを交互に実施す for phase in ['train', 'validation’]: #各フェーズのDataLoaderからミニバッチを1つずつ取り出して処理する for (features, labels, feat_len, label_len, utt_ids) in dataset_loader[phase]: 損失の計算 if phase == ‘train’: #trainフェーズの時 パラメータの更新 if phase == 'validation’:#trainフェーズの時 if epoch+1 >= lr_decay_start_epoch: #一定回数は学習しているか if epoch == 0 or best_loss > epoch_loss: #損失値が最低値を更新した場合 その時のモデルを保存し、 counter_for_early_stopをリセット else: #最低値を更新していない場合 if counter_for_early_stop+1 >= early_stop_threshold: #一定回数連続しているか early_stop_flag = True else: 学習率の更新, counter_for_early_stopを+1 14
学習の準備 hmm = MonoPhoneHMM() hmm.load_hmm(hmm_file) dim_out = hmm.num_phones * hmm.num_states #DNNの出力層の次元数=音素数×状態数 pad_index = dim_out #状態を表す数(0 ~ 104)以外なら何でも良い dim_in = feat_dim * (2*splice+1) #今回はsplice=5 model = MyDNN(dim_in, hidden_dim, dim_out, num_layers) #DNNモデルを作成 optimizer = optim.SGD(model.parameters(), lr=initial_learning_rate, momentum=0.99) train_dataset = SequenceDataset(train_feat_scp, train_label_file, feat_mean, feat_std, pad_index, splice) #データ作成 dev_dataset = SequenceDataset(dev_feat_scp, dev_label_file, feat_mean, feat_std, pad_index, splice) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4) dev_loader = DataLoader(dev_dataset, batch_size=batch_size, shuffle=False, num_workers=4) dataset_loader = {‘train’: train_loader, ‘validation’: dev_loader} #datloaderをまとめる criterion = nn.CrossEntropyLoss(ignore_index=pad_index) #損失関数はクロスエントロピー if torch.cuda.is_available(): device = torch.device('cuda') else: #GPUかCPUにモデルをのせる device = torch.device('cpu') model = model.to(device) model.train() #モデルを学習モードにする 15
DNNの学習 : 損失の計算, パラメータの更新 # 勾配をリセット optimizer.zero_grad() # モデルの出力を計算(フォワード処理) outputs = model(features) #[バッチサイズ, フレーム数, ラベル数]を、 #[サンプル数, ラベル数]に変換 b_size, f_size, _ = outputs.size() outputs = outputs.view(b_size*f_size,dim_out) #[バッチサイズ, フレーム数]を[サンプル数]に変換 labels = labels.view(-1) # 損失値を計算する loss = criterion(outputs, labels) if phase == 'train’: # 誤差逆伝播により勾配を計算する loss.backward() # 勾配を用いてパラメータを更新する optimizer.step() # 損失値を累積する total_loss += loss.item() # 処理した発話数をカウントする total_utt += b_size # フレーム単位の誤り率を計算する # 推定ラベルを得る _, hyp = torch.max(outputs, 1) # ラベルにpad_indexを埋めた # フレームを取り除く hyp = hyp[labels != pad_index] ref = labels[labels != pad_index] # 推定ラベルと正解ラベルが不一致なフレーム数を 得る error = (hyp != ref).sum() #誤りフレーム数を累積する total_error += error # 総フレーム数を累積する total_frames += len(ref) 16
DNN-HMMによる音声認識 17
DNN-HMMによる孤立単語音声認識の実装 model = MyDNN(dim_in, hidden_dim, dim_out, num_layers) #DNNモデルを作成 # 学習済みのDNNファイルからモデルのパラメータを読み込む model.load_state_dict(torch.load(dnn_file)) model.eval() # モデルを評価モードに設定する prior = count / np.sum(count) #HMM状態の出現回数から、事前発生確率を求める feat : datasetの__getitem__と同じく、正規化とスプライシングを行う feat = torch.tensor(feat) #tensor型に変換 output = model(feat) #モデルに通す output = F.softmax(output, dim=1) #softmax関数に通して確率に変換する #numpy array型に戻す detach()でtensorが持つ勾配のデータを持たないようにする output = output.detach().numpy() #対数尤度に変換し、HMMの各状態の出力確率に相当する形にする likelihood = np.log(output / prior) (result, detail) = hmm.recognize_with_dnn(likelihood, lexicon) hmm.recognize_with_dnnは、各状態の出力確率に DNNモデルで求めたものを用いる以外はGMM-HMMと同じく viterbiアルゴリズムを用いる likelihood は、[フレーム数 , (音素数 × 状態数)] になっているので、 [音素数 , 状態数 , フレーム数] のデータに変換する (set_out_prob) 18
DNN-HMMによる孤立単語音声認識 10単語の認識結果 「とお」のDNNとGMMでの比較 DNN 10個とも正しく認識している GMM DNNでは、GMMより正解の音素の確率が 他の音素の確率より高くなっている 19
20