---
title: 【Pythonで学ぶ音声認識】第6章：DNN-HMMによる音声認識（6.4節）
tags: 
author: [京都大学人工知能研究会KaiRA](https://convert.docswell.com/user/kyoto-kaira)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/L735XNX5E5.jpg?width=480
description: Pythonで学ぶ音声認識の輪読会第8回の発表スライドです。 2023年12月7日(木) 18:30～
published: December 07, 23
canonical: https://convert.docswell.com/s/kyoto-kaira/K7VDR7-2023-12-07-153733
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/L735XNX5E5.jpg)

後期輪読会#8
DNN-HMMの実装
京都大学 理学部 １回生
千葉
一世
0


# Page. 2

![Page Image](https://bcdn.docswell.com/page/87DD2W2Y7G.jpg)

DNN-HMMの実装
目次
1. 学習データの正解ラベル作成
2. DNNモデル, Dataset作成
3. DNNの学習
4. DNN-HMMによる孤立単語音声認識
githubのソースコードURL
1


# Page. 3

![Page Image](https://bcdn.docswell.com/page/VJPX191278.jpg)

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


# Page. 4

![Page Image](https://bcdn.docswell.com/page/2EVLYMYXJQ.jpg)

DNNの正解ラベル:状態アライメントの推定
上記のように出力
音素数35 × 状態数3 =105 個の状態を 0~104の数字に対応付ける
この正解ラベルから、各状態の出現回数をカウントする
事前発生確率
𝑝
𝑃(𝑠𝑗 )
を求めるのに用いる
(ゼロ除算を防ぐために、一度も出現しない状態の出現回数は1する)
3


# Page. 5

![Page Image](https://bcdn.docswell.com/page/57GX9G9REL.jpg)

DNNの実装
4


# Page. 6

![Page Image](https://bcdn.docswell.com/page/4EQ8WXWYJP.jpg)

Pytorchのtensor
Pytorch では、Tensor型のデータを扱う
Tensorは基本的な部分はnumpyのndarrayなどと同じ
Tensorを用いる利点
⚫GPUを用いた計算が可能
計算の高速化
⚫勾配の情報を保持する
自動微分など、逆伝播が容易
5


# Page. 7

![Page Image](https://bcdn.docswell.com/page/KJ42K6KZE1.jpg)

DNNモデル
Pytorchのtorch.nn.Moduleを継承して、以下のようなモデルを作成
今回は
dim_in
= 13 * 11
dim_hidden = 1024
dim_out = 35*5
num_layers = 4
LeCunの初期値を用いる
6


# Page. 8

![Page Image](https://bcdn.docswell.com/page/LE1MXVXD7G.jpg)

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


# Page. 9

![Page Image](https://bcdn.docswell.com/page/GEW3NLN8E2.jpg)

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


# Page. 10

![Page Image](https://bcdn.docswell.com/page/47ZV3M39E3.jpg)

Dataset
Torch.utils.data.Datasetを継承し、
ミニバッチデータを作成するためのクラスを作成する
torch.utils.data.Dataloader を用いてミニバッチを作成するためには
__len__と、__getitem__を定義する必要がある
__len__ は、len(Dataset) としたときの挙動を定める
__getitem__ は、Dateset[id] としたときの挙動を定める
9


# Page. 11

![Page Image](https://bcdn.docswell.com/page/YJ65XNXD7V.jpg)

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=&#039;constant&#039;, constant_values=self.pad_index)
ミニバッチ処理のために
ラベルのサイズをそろえる
損失を求めるときには、nn.CrossEntropyLoss(ignore_index=pad_index)として、
labelがpad_imdexになっている部分は計算しないようにする
10


# Page. 12

![Page Image](https://bcdn.docswell.com/page/GJ5YX5X8J4.jpg)

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 &lt; 0: # 前にずらした場合は終端nフレームを0にする
tmp[n:] = 0
elif n &gt; 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


# Page. 13

![Page Image](https://bcdn.docswell.com/page/LE35XNXGJ5.jpg)

スプライシング
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


# Page. 14

![Page Image](https://bcdn.docswell.com/page/8EDD2W2NEG.jpg)

DNNの学習
オプティマイザをモメンタムSGD 損失関数をクロスエントロピーとし 、
Learning rate decay と Early stopping を用いて学習する
Learning rate decay :
振動を防ぎ、収束を早めるために、
開発データの損失値が下がりに
くくなった段階で、学習率を小さ
くする手法
Early stopping :
過学習を防ぐために、開発デー
タの損失値が下がらなくなった段
階で学習を止める手法
13


# Page. 15

![Page Image](https://bcdn.docswell.com/page/V7PX191NE8.jpg)

DNN学習のプログラム概要
for epoch in range(max_num_epoch): #エポックの数だけループ
if early_stop_flag: #early_stop_flagがTrueになったら学習をやめる
break
#trainフェーズとvalidationフェーズを交互に実施す
for phase in [&#039;train&#039;, &#039;validation’]:
#各フェーズのDataLoaderからミニバッチを１つずつ取り出して処理する
for (features, labels, feat_len, label_len, utt_ids) in dataset_loader[phase]:
損失の計算
if phase == ‘train’: #trainフェーズの時
パラメータの更新
if phase == &#039;validation’:#trainフェーズの時
if epoch+1 &gt;= lr_decay_start_epoch: #一定回数は学習しているか
if epoch == 0 or best_loss &gt; epoch_loss: #損失値が最低値を更新した場合
その時のモデルを保存し、 counter_for_early_stopをリセット
else: #最低値を更新していない場合
if counter_for_early_stop+1 &gt;= early_stop_threshold: #一定回数連続しているか
early_stop_flag = True
else:
学習率の更新, counter_for_early_stopを+1
14


# Page. 16

![Page Image](https://bcdn.docswell.com/page/2JVLYMYY7Q.jpg)

学習の準備
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(&#039;cuda&#039;)
else:
#GPUかCPUにモデルをのせる
device = torch.device(&#039;cpu&#039;)
model = model.to(device)
model.train() #モデルを学習モードにする
15


# Page. 17

![Page Image](https://bcdn.docswell.com/page/5EGX9G9WJL.jpg)

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 == &#039;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


# Page. 18

![Page Image](https://bcdn.docswell.com/page/4JQ8WXWQ7P.jpg)

DNN-HMMによる音声認識
17


# Page. 19

![Page Image](https://bcdn.docswell.com/page/K742K6KYJ1.jpg)

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


# Page. 20

![Page Image](https://bcdn.docswell.com/page/LJ1MXVXNEG.jpg)

DNN-HMMによる孤立単語音声認識
10単語の認識結果
「とお」のDNNとGMMでの比較
DNN
10個とも正しく認識している
GMM
DNNでは、GMMより正解の音素の確率が
他の音素の確率より高くなっている
19


# Page. 21

![Page Image](https://bcdn.docswell.com/page/GJW3NLKMJ2.jpg)

20


