ichou1のブログ

主に音声認識、時々、データ分析のことを書く

音声認識メモ(DeepSpeech)その3

レーニングによるパラメータ更新を試してみる。

レーニングデータは、下記で公開されているATR音素バランス503文の発話データを使うことにする。
https://ja.osdn.net/projects/galateatalk/releases/22207

日本語コーパスを使った音素認識については下記のサイトを参考にさせていただいた。
Connectionist Temporal Classification (CTC) を用いた音素認識 - Qiita

公開データに含まれる音素ラベルは「42」種類。

{'n', 'gy', 'I', 'dy', 'w', 'ch', 's', 'g', 'sh', 'N', 'z', 'ry', 'p', 'E', 'py', 'e', 'u', 'O', 'y', 'my', 'ky', 'i', 'ts', 'f', 'b', 'A', 'k', 'ny', 'm', 'ch', 'r', 'pau', 'by', 'U', 'j', 'o', 'h', 'hy', 'sil', 't', 'a', 'd'}

参考サイトでは、促音(「っ」)を’ch’から'Q'に置き換えている。

DeepSpeechのモデルは前回見たように、「Dense」+「LSTM」+「Dense」の構造になる。
また、上記のQiita記事も「Dense」+「Bi-directional LSTM」+「Dense」になっている。


音声を音素レベルまで分解せず、"音のまとまり"として認識するならCNN(畳み込み)という選択肢もある(異常音の検知などはこちらになると思われる)
TensorFlowメモ(Simple Audio Recognition) - ichou1のブログ


今回は下記で公開されているモデルを参考に組み立ててみることにする。
GitHub - robmsmt/KerasDeepSpeech: A Keras CTC implementation of Baidu's DeepSpeech for model experimentation

モデル構造は6層。

def create_base_model(n_mfcc_bin,
                 n_class,
                 dropout=[0.1, 0.1, 0.1],
                 fc_size=2048,
                 n_lstm_unit=512):

    input_data = Input(name='input', shape=(None, n_mfcc_bin))
    # --> ([batch_size, time_step, n_mfcc_bin])

    init = tf.random_normal_initializer(stddev=0.046875)

    ### Layer 1
    x = TimeDistributed(Dense(units=fc_size,
                              kernel_initializer=init,
                              bias_initializer=init,
                              activation=clipped_relu),
                        name='dense_1')(input_data)

    x = TimeDistributed(Dropout(dropout[0]),
                        name='drop_1')(x)
    # --> ([batch_size, time_step, fc_size])

    ### Layer 2
    x = TimeDistributed(Dense(units=fc_size,
                              kernel_initializer=init,
                              bias_initializer=init,
                              activation=clipped_relu),
                        name='dense_2')(x)

    x = TimeDistributed(Dropout(dropout[0]),
                        name='drop_2')(x)
    # --> ([batch_size, time_step, fc_size])

    ### Layer 3
    x = TimeDistributed(Dense(units=fc_size,
                              kernel_initializer=init,
                              bias_initializer=init,
                              activation=clipped_relu),
                        name='dense_3')(x)

    x = TimeDistributed(Dropout(dropout[0]),
                        name='drop_3')(x)
    # --> ([batch_size, time_step, fc_size])

    ### Layer 4 BiDirectional RNN
    x = Bidirectional(LSTM(units=n_lstm_unit,
                           return_sequences=True,
                           dropout=dropout[1],
                           kernel_initializer='he_normal'),
                      merge_mode='sum',
                      name='Bi_LSTM')(x)
    # --> ([batch_size, time_step, n_lstm_unit])
    
    ### Layer 5
    # x = TimeDistributed(Dense(fc_size,
    #                           activation=clipped_relu,
    #                           kernel_initializer=init,
    #                           bias_initializer=init),
    #                     name='dense_3')(x)
    
    x = TimeDistributed(Dropout(dropout[2]),
                        name='drop_5')(x)
    # --> ([batch_size, time_step, n_lstm_unit])

    ### Layer 6 Softmax
    y_pred = TimeDistributed(Dense(units=n_class,
                                   kernel_initializer=init,
                                   bias_initializer=init,
                                   activation="softmax"),
                             name="softmax")(x)
    # --> ([batch_size, time_step, n_class])

    model = tf.keras.Model(inputs=input_data, outputs=y_pred)

    return model

レイヤ5のDenseは効果がないのか、コメントアウトされている。


インプットとなる特徴量の次元(n_mfcc_bin)と、アウトプットのclass数(n_class)を指定してモデルを定義する。

# n_symbol = 42(音素の種類)
base_model = create_base_model((n_mfcc_bin=26, n_class=n_symbol+1))
  • 「n_class」の指定で、プラス1としているのは、"いずれの音素でもない記号"(pseudo-charactor)の分。
  • 「26次元」となっているのは、MFCC13次元にdelta特徴量を加えた分。
mfcc = librosa.feature.mfcc(wav, sr=16000, n_mfcc=13, ...)
mfcc_d = librosa.feature.delta(mfcc)


モデルサマリを出力。

base_model.summary()
モデルサマリ出力結果
Layer (type)                 Output Shape              Param #   
=================================================================
input (InputLayer)           [(None, None, 26)]        0         
_________________________________________________________________
dense_1 (TimeDistributed)    (None, None, 2048)        55296     
_________________________________________________________________
drop_1 (TimeDistributed)     (None, None, 2048)        0         
_________________________________________________________________
dense_2 (TimeDistributed)    (None, None, 2048)        4196352   
_________________________________________________________________
drop_2 (TimeDistributed)     (None, None, 2048)        0         
_________________________________________________________________
dense_3 (TimeDistributed)    (None, None, 2048)        4196352   
_________________________________________________________________
drop_3 (TimeDistributed)     (None, None, 2048)        0         
_________________________________________________________________
Bi_LSTM (Bidirectional)      (None, None, 512)         10489856  
_________________________________________________________________
drop_5 (TimeDistributed)     (None, None, 512)         0         
_________________________________________________________________
softmax (TimeDistributed)    (None, None, 43)          22059     
=================================================================
Total params: 18,959,915
Trainable params: 18,959,915
Non-trainable params: 0


レーニングにおける損失関数はCTC(Connectionist Temporal Classification)を使う。
入力(MFCC)と出力(音素ラベル)のシーケンス長が異なる場合に適用でき、TensorFlow APIで、「tf.keras.backend.ctc_batch_cost」が公開されている。
Kerasドキュメントの方が分かりやすいと思われる)

「tf.keras.backend.ctc_batch_cost」を呼び出す自前の関数を作って、先程のモデル(base_model)の後ろに付け加える。

model = add_ctc_loss(base_model)
モデルサマリの出力結果
Layer (type)               Output Shape         Param #  Connected to     
==========================================================================
input (InputLayer)         [(None, None, 26)]   0                         
__________________________________________________________________________
dense_1 (TimeDistributed)  (None, None, 2048)   55296    input[0][0]      
__________________________________________________________________________

(途中省略)
__________________________________________________________________________
softmax (TimeDistributed)  (None, None, 43)     22059    drop_5[0][0]     
__________________________________________________________________________
real_y_true (InputLayer)   [(None, 123)]        0                         
__________________________________________________________________________
pred_length (InputLayer)   [(None, 1)]          0                         
__________________________________________________________________________
true_length (InputLayer)   [(None, 1)]          0                         
__________________________________________________________________________
my_ctc (Lambda)            (None, 1)            0        softmax[0][0]    
                                                         real_y_true[0][0]
                                                         pred_length[0][0]
                                                         true_length[0][0]
==========================================================================
Total params: 18,959,915
Trainable params: 18,959,915
Non-trainable params: 0

「123」というのは、正解となる音素ラベルの最大長

300エポックほど学習する。
最終的な"loss"は「0.17」ほど。

結果から先に書くと、テストデータに対する認識結果は下記のとおり。

テストデータ1

ちいさなうなぎやに、ねっきのようなものがみなぎる。

['_', 'sil', 'sil', 'sil', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'ch', 'ch', 'ch', 'ch', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 's', 's', 's', 's', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'a', 'a', 'a', 'a', 'a', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'n', 'n', 'n', '_', '_', '_', '_', '_', '_', '_', 'a', 'a', 'a', 'a', 'a', 'a', 'a', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'u', 'u', 'u', 'u', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'n', 'n', '_', '_', '_', '_', '_', '_', 'a', 'a', 'a', 'a', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'g', 'g', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'i', 'i', 'i', 'i', 'i', '_', '_', '_', '_', '_', '_', '_', '_', 'a', 'a', 'a', 'a', 'a', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'n', 'n', 'n', 'n', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'i', 'i', 'i', 'i', 'i', '_',
...
'sil', 'sil', 'sil', 'sil', 'sil', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'f', 'f', 'f', 'f', 'f']

長いので、"_"を除外して出力する。
結果。

['sil', 'sil', 'sil', 'ch', 'ch', 'ch', 'ch', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 'i', 's', 's', 's', 's', 'a', 'a', 'a', 'a', 'a', 'n', 'n', 'n', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'u', 'u', 'u', 'u', 'n', 'n', 'a', 'a', 'a', 'a', 'g', 'g', 'i', 'i', 'i', 'i', 'i', 'a', 'a', 'a', 'a', 'a', 'n', 'n', 'n', 'n', 'i', 'i', 'i', 'i', 'i', 'pau', 'pau', 'pau', 'pau', 'n', 'n', 'n', 'e', 'e', 'e', 'Q', 'Q', 'Q', 'Q', 'k', 'k', 'i', 'i', 'i', 'i', 'n', 'n', 'n', 'n', 'o', 'o', 'o', 'y', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'n', 'n', 'n', 'a', 'a', 'a', 'a', 'm', 'm', 'm', 'o', 'o', 'o', 'o', 'n', 'n', 'n', 'o', 'o', 'o', 'o', 'o', 'g', 'g', 'g', 'a', 'a', 'a', 'a', 'm', 'm', 'm', 'm', 'i', 'i', 'i', 'n', 'n', 'n', 'a', 'a', 'a', 'a', 'g', 'g', 'i', 'i', 'i', 'i', 'r', 'r', 'r', 'u', 'u', 'u', 'sil', 'sil', 'sil', 
...
'sil', 'sil', 'sil', 'sil', 'sil', 'f', 'f', 'f', 'f', 'f']

=> "うなぎやに"のところで"y"の音素が欠落、また、最後の方に余分な音素"f"が入ってしまっているが、概ねOK。

テストデータ2

どろぼうでもはいったかと、いっしゅんぼくはおもった。

['sil', 'sil', 'sil', 'sil', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'd', 'd', '_', '_', '_', '_', 'o', 'o', 'o', 'o', 'o', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'r', 'r', 'r', 'r', 'o', 'o', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'b', 'b', 'o', 'o', 'o', 'o', 'o', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'o', 'o', 'o', 'o', 'o', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'd', 'd', 'e', 'e', 'e', 'e', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'm', 'm', 'm', 'o', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'h', 'h', 'h', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'a', 'a', 'a', 'a', 'a', '_', '_', '_', '_', '_', '_', '_', 'i', 'i', 'i', 'i', '_', '_', '_', '_', '_', 'Q', 'Q', 'Q', 'Q', 'Q', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 't', 't', 't', '_', '_', '_', 'a', 'a', 'a', 'a', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'k', 'k', 'k', '_', '_', '_', '_', '_', '_', '_', 'a', 'a', 'a', 'a', 'a', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', 't', 't', 't', '_', '_', 'o', 'o', 'o', '_', '_', '_',
...
'sil', 'sil', 'sil', '_', '_', '_', '_', '_', '_', '_', '_', '_', 'sil', 'sil', 'sil', 'sil', 'f', 'f', 'f', 'f', 'u']

"_"を除外した出力結果。

['sil', 'sil', 'sil', 'sil', 'd', 'd', 'o', 'o', 'o', 'o', 'o', 'r', 'r', 'r', 'r', 'o', 'o', 'b', 'b', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'd', 'd', 'e', 'e', 'e', 'e', 'm', 'm', 'm', 'o', 'h', 'h', 'h', 'a', 'a', 'a', 'a', 'a', 'i', 'i', 'i', 'i', 'Q', 'Q', 'Q', 'Q', 'Q', 't', 't', 't', 'a', 'a', 'a', 'a', 'k', 'k', 'k', 'a', 'a', 'a', 'a', 'a', 't', 't', 't', 'o', 'o', 'o', 'pau', 'pau', 'pau', 'pau', 'i', 'i', 'i', 'i', 'sh', 'sh', 'sh', 'sh', 'sh', 'u', 'u', 'u', 'N', 'N', 'N', 'N', 'N', 'N', 'b', 'b', 'o', 'o', 'o', 'k', 'k', 'k', 'u', 'u', 'u', 'u', 'w', 'w', 'w', 'a', 'a', 'a', 'a', 'h', 'h', 'o', 'o', 'o', 'o', 'o', 'm', 'm', 'm', 'o', 'o', 'o', 'o', 'Q', 'Q', 'Q', 'Q', 't', 't', 't', 'a', 'a', 'a', 'a', 'sil', 'sil', 'sil', 'sil', 'sil', 'sil', 'sil', 'sil', 'sil', 'sil', 'sil', 'sil', 'sil', 'sil', 'sil',
...
'sil', 'sil', 'sil', 'f', 'f', 'f', 'f', 'u']

=>"ぼくはおもった"のところで余分な"h"の音素が入っている、また、最後の方に余分な音素"f"と"u"が入ってしまっているが、概ねOK。