音声認識メモ(スペクトログラム)
spectrogramに関するメモ。
Wikipediaより
スペクトログラム(英: Spectrogram)とは、複合信号を窓関数に通して、周波数スペクトルを計算した結果を指す。
3次元のグラフ(時間、周波数、信号成分の強さ)で表される。
pythonのmatplotlibライブラリにある「specgram」関数を使ってみる。
プロットする対象は、フーリエ変換のメモで使った波形。
波形データ
specgram関数に渡すパラメータ
- Fs: サンプリング周波数、「16k」Hz
- NFFT : FFTのブロックサイズ、「512」
- window : 窓関数、使用しない(mlab.window_none)
spectrum, freqs, t, im = plt.specgram("波形データ", NFFT=512, Fs=16000, window=mlab.window_none) plt.colorbar(im)
250Hzと500Hzの周波数に該当する部分で、色の差異が出ている。
この色に該当する部分は"信号成分の強さ"を表す。
specgram関数の1番目の返り値を確認してみる。
マニュアル説明
Returns:
spectrum : 2-D array
Columns are the periodograms of successive segments.
# spectrum.shape (257, 1) # spectrum[0:20] array([[3.75250747e-31], [4.10801350e-32], [5.97746403e-31], [1.51855718e-30], [2.75157550e-31], [6.41012852e-31], [2.86066184e-30], [2.14148310e-30], [4.00000000e+01], # DFT index 8 corresponds to 250Hz [8.24355999e-30], [6.58668214e-31], [7.53947441e-31], [1.47302632e-30], [9.28820685e-30], [8.76544796e-30], [4.72905074e-29], [1.60000000e+02], # DFT index 16 corresponds to 500Hz [8.07289044e-29], [1.78670584e-29], [1.20023304e-29]])
ピリオドグラム法を実装した「psd」関数を使って確認してみる。
マニュアル説明
The power spectral density by Welch's average periodogram method.
関数を実行。
pxx, freqs = plt.psd("波形データ", NFFT=512, Fs=16000, window=mlab.window_none)
x軸とy軸のラベルは自動的に付与された。
# pxx.shape (257,) # pxx[0:20] array([3.75250747e-31, 4.10801350e-32, 5.97746403e-31, 1.51855718e-30, 2.75157550e-31, 6.41012852e-31, 2.86066184e-30, 2.14148310e-30, 4.00000000e+01, 8.24355999e-30, 6.58668214e-31, 7.53947441e-31, 1.47302632e-30, 9.28820685e-30, 8.76544796e-30, 4.72905074e-29, 1.60000000e+02, 8.07289044e-29, 1.78670584e-29, 1.20023304e-29])
マニュアル説明
Returns:
Pxx : 1-D array
The values for the power spectrum P_{xx} before scaling (real valued)
この値は、「specgram」関数の1番目の返り値と一致している。
この値pxxに対して、以下の計算を実行したものが"Power Spectral(Spectrum) Density"として、「specgram」関数および「psd」関数のグラフにプロットされる。
マニュアル説明
Notes
For plotting, the power is plotted as
for decibels, though Pxx itself is returned.
検算。
# DFT index 1 corresponds to 31.25Hz # pxx[1] : 4.10801350e-32 10 * math.log10(4.10801350e-32) -313.86368137867873 # DFT index 8 corresponds to 250Hz # pxx[8] : 40 10 * math.log10(40) 16.02059991327962 # DFT index 16 corresponds to 500Hz # pxx[16] : 160 10 * math.log10(160) 22.041199826559247
(論文読解) Looking to Listen at the Cocktail Party: A Speaker-Independent Audio-Visual Model for Speech Separation
Googleが出した論文
[1804.03619] Looking to Listen at the Cocktail Party: A Speaker-Independent Audio-Visual Model for Speech Separation
顔画像をもとにノイズマスキングを生成し、傾聴したい音声だけを抽出する。
ブログ記事
ai.googleblog.com
解説記事
tech.d-itlab.co.jp
モデル構成
Our network is implemented in TensorFlow
TensorFlowで実装した模様。
ソースは公開されていないので、論文をもとに内部の処理を追ってみる。
[step 1] インプットデータ準備
音声と動画それぞれ3秒分を1sample。
話者は2人と仮定。
動画
We resample the face embeddings from all videos to 25 frames-per-second (FPS) before training and inference by either removing or replicating embeddings.
This results in an input visual stream of 75 face embeddings.
When missing frames are encountered in a particular sample, we use a vector of zeros in lieu of a face embedding.
25FPSの動画3秒分をclip、75frameが得られる。
全体のピクセル数が「1024」なので、画像サイズは「32x32」と思われる。
モデル構成図から読み解いた最終的なtensor形状。
- Video(person A) --> (75frame x 1024pixel x 1)
- Video(person B) --> (75frame x 1024pixel x 1)
音声
All audio is resampled to 16kHz, and stereo audio is converted to mono by taking only the left channel.
STFT is computed using a Hann window of length 25ms, hop length of 10ms, and FFT size of 512, resulting in an input audio feature of 257 × 298 × 2 scalars.
Power-law compression is performed with p = 0.3 (A 0.3 , where A is the input/output audio spectrogram).use both the real and imaginary parts of a complex number
power-law compression to prevent loud audio from overwhelming soft audio
サンプリングレートは「16k」Hz、25msごとに窓関数(ハニング)をかけてFFTを実行、298frameが得られる。
モデル構成図から読み解いた最終的なtensor形状。
Audio --> (298frame x 2 x 257)
[step 2] CNN
画像
Note that "spatial" convolutions and dilations in the visual stream are performed over the temporal axis (not over the 1024-D face embedding channel).
畳み込みはframeの時間軸に対して実施。
To compensate for the sampling rate discrepancy between the audio and video signals, we upsample the output of the visual stream to match the spectrogram sampling rate (100 Hz).
画像と音声のサンプリングレートの差異を補完するため、画像ストリームに対してアップサンプリングを実施。
モデル構成図から読み解いた最終的なtensor形状。
Video1(person A) --> (298, 256)
Video2(person B) --> (298, 256)
音声
Audio --> (298, 8*257)
[step 3] Fusion
AV fusion.
The audio and visual streams are combined by concatenating the feature maps of each stream
モデル構成図から読み解いた最終的なtensor形状。
(298, (256*2)+(8*257)) = (298, 2568)
[step 4] Bidirectional LSTM
subsequently fed into a BLSTM
モデル構成図から読み解いた最終的なtensor形状。
(298, 400)
[step 5] Fully connect
followed by three FC layers
ReLU activations follow all network layers except for last (mask), where a sigmoid is applied.
最後の活性化関数は「sigmoid」
モデル構成図から読み解いたtensor形状の遷移
(298, 600)
(298, 600)
(2person, 298frame, 2, 257)
The final output consists of a complex mask (two-channels, real and imaginary) for each of the input speakers.
The output of our model is a multiplicative spectrogram mask, which describes the time-frequency relationships of clean speech to background interference.
話者ごとノイズマスキングが出力される。これがモデルのアウトプット。
The corresponding spectrograms are computed by complex multiplication of the noisy input spectrogram and the output masks.
ここで得られたマスキングデータとインプット音声(spectrogram)の乗算(complex multiplication)を計算し(この計算結果が、損失関数で使う「the enhanced spectrogram」と思われる)
The final output waveforms are obtained using ISTFT
逆変換で音声データに戻す。
これが、傾聴したい特定話者の発話にあたる(他者の発話およびノイズ分離済み)
トレーニングの設定
Batch normalization is performed after all convolutional layers.
Dropout is not used, as we train on a large amount of data and do not suffer from overfitting.
We use a batch size of 6 samples and train with Adam optimizer for 5 million steps (batches) with a learning rate of 3e−5 which is reduced by half every 1.8 million steps.
batch-sizeは「6」sample、オプティマイザは「Adam」、エポックは「500万」、学習率は「0.00003」(180万回ごとに半減)
The squared error (L2) between the power-law compressed clean spectrogram and the enhanced spectrogram is used as a loss function to train the network.
損失関数はL2ノルム、「the power-law compressed clean spectrogram」と「the enhanced spectrogram」の乖離を最小化する。
TensorFlowメモ(RNNその1)
RNN(Recurrent Neural Network)を試してみる。
以下のサイトを参考にさせていただいた。
第6回 リカレントニューラルネットワークの実装(2)|Tech Book Zone Manatee
正弦波の時系列データ5つ(tからt+4)の値をもとに、t+5の値を予測する。
インプットデータ
- 周波数0.01の純音データを200個分し、5個分を1セットとして、196セット
- トレーニング用に9割、検証用に1割を割り当てるとして、176セットが学習用、20セットが検証用
検証
- 検証用の「20」セットを使って評価
- 損失関数は学習と同じ(「最小二乗法」)
RNNの設定
- Cellは「tf.contrib.rnn.BasicRNNCell」
- 隠れ層のユニット数は「20」
- 最後の出力に対して重みとバイアスを与える
(参考)kerasで書き直したモデルをmodel.summary()で表示
学習で更新されるパラメータ数の内訳
# simple_rnn_1 Layer weight(input) : 1row * 20col = 20 weight(state) : 20row * 20col = 400 bias : 20 # dense_1 Layer weight : 20row * 1col bias : 1
認識精度は以下のとおり推移した。
('epoch:', 0, ' validation loss:', 0.36871427) ('epoch:', 1, ' validation loss:', 0.2549196) ('epoch:', 2, ' validation loss:', 0.13921228) ('epoch:', 3, ' validation loss:', 0.067716144) ('epoch:', 4, ' validation loss:', 0.05464934) ... ('epoch:', 95, ' validation loss:', 0.00034664402) ('epoch:', 96, ' validation loss:', 0.0004254885) ('epoch:', 97, ' validation loss:', 0.0003979026) ('epoch:', 98, ' validation loss:', 0.00039949064) ('epoch:', 99, ' validation loss:', 0.0003302602)
学習後、適当な入力を与えて予測させてみる。
t=30における入力と正解、予測値
# 入力 training value X[30] : [0.95105652 0.92977649 0.90482705 0.87630668 0.84432793] # 正解 training value Y[30] : 0.809016994375 # 予測 estimate value : 0.8363162
RNN Cell内部のパラメータの形状を確認してみる。
basic_cell = tf.contrib.rnn.BasicRNNCell(num_units=20) print(basic_cell.variables)
出力
[<tf.Variable 'RNN/basic_rnn_cell/kernel:0' shape=(21, 20) dtype=float32_ref>, <tf.Variable 'RNN/basic_rnn_cell/bias:0' shape=(20,) dtype=float32_ref>]
この部分に関しては以下のサイトが分かりやすい。
"inputに対する重み"と"stateに対する重み"を結合したものが"kernel"に該当する。
python - internal variables in BasicRNNCell - Stack Overflow
inputとkernelの演算を図にすると以下のとおり。
Wがinputに対する重み、Uがstateに対する重みに該当する。
inputと重みの演算結果にバイアスを加え、活性化関数(デフォルトはtanh)を通したものを次の時系列のインプットとする。
LSTMについては下記ご参照。
TensorFlowメモ(RNNその2) - ichou1のブログ
tensorflowメモ(手書き文字認識その4)
前回の続き。
精度を上げることを試みる。
tensorflowサンプル(「examples/tutorials/mnist/mnist_deep.py」)を参考にレイヤーを構成。
"Fully connected layer 1"(上表のfc1)のdownsamplingはMNISTサンプルをもとに適当に設定。
「256」(=2 * 2 * 64)featuresから「96」featuresへ。
MNISTサンプルだと、「3136」(=7 * 7 * 64)featuresから「1024」featuresへ。
(参考)kerasのmodel.summary()によるモデル表示
学習で更新されるパラメータ数の内訳
# conv2d_2 Layer filter: 3row * 3col * 32channel * 64channel = 18432 bias : 64 # dense_1 Layer weight : 256row * 96col = 24576 bias : 96col
以下のとおり設定して学習。
- "probability of dropout"(上表のdropout)は「0.5」
- エポックは「500」
- オプティマイザは「tf.train.GradientDescentOptimizer」
- 損失関数は「交差エントロピー誤差」
途中、畳み込み層のパラメータが「Nan」になってしまったが、最大で99%後半の精度が出た。
ラベル値と推測値が異なっていた画像を見てみる。
「5」とラベル付け、「4」と推測
「8」とラベル付け、「9」と推測
tensorflowメモ(手書き文字認識その3)
前回の続き。
モデルを畳み込みニューラルネットワーク (Convolutional Neural Network)に変更してみる。
チュートリアルなどを見ると、畳み込み層を2回通しているケースが多いが、まずは1層のモデルで試してみる。
パラメータは以下のとおりとした。
(参考)kerasのmodel.summary()によるモデル表示
学習で更新されるパラメータ数の内訳
# conv2d_1 Layer filter: 3row * 3col * 32channel = 288 bias : 32channel # dense_1 Layer (Fully connected Layer) weight : 512row * 10col = 5120 bias : 10col
エポックを500回に設定したところ、精度は98%まで出た。
学習後のフィルターの状態を可視化してみる。
フィルターの範囲確認
-1.241 # 最小値 1.417 # 最大値
前回は値"0"が中間の階調になるようスケーリングしたが、今回は最小値、最大値の範囲でグレースケールに変換。
ライブラリ「Matplotlib」を使って描画する。
plt.imshow(filter["filter_index"], cmap='gray', vmin='フィルター(最小値)', vmax='フィルター(最大値)', interpolation='none')
学習済みのフィルター(3行x3列、32個)
この各フィルタを、入力画像(0から1の範囲に正規化済み)
に対して適用すると以下の出力が得られる(バイアス項は加えていない状態)
白寄りの部分が正の値、黒寄りの部分が負の値であることを示している。
この状態からフィルタごとのバイアスを加算し、活性化関数(ReLU)を通して負の値を0にした後、最大プーリング処理が行われる。
結果、負の値(黒寄りの部分)は除かれ、正の値(白寄りの部分)が特徴の判定材料として残ることになる。
tensorflowメモ(手書き文字認識その2)
前回生成したモデルを掘り下げてみる。
重み付けを行うパラメータが「final_w」に入っているとする。
final_w.shape (64, 10) # 最小値 numpy.min(final_w) -1.68 # 最大値 numpy.max(final_w) 1.55
「final_w」をグレースケール変換して視覚化してみる。
変換手順は以下のとおり。
- 各値に、「1.68」を加算する(結果は 0 から 3.36(= 1.68 x 2)の範囲になる)
- 各値を「75.893」倍(= 255 / 3.36)する(結果は 0 から 255の範囲になる、元々のゼロは「127.5」になる)
- 各値の端数を切り捨てる
各クラス(0から9)ごとの重みパラメータの変換結果は以下のとおり。
重みパラメータを可視化
中間値が灰色だとすると、白に近い部分は正、黒に近い部分は負に該当する。
これをゼロとラベル付けした画像に適用してみる。
outputとして出力された値を足し上げて各クラスの尤度を判定する。
outputの白に近い部分は正の値、黒に近い部分は負の値として作用する。
入力画像をモデルに通した後の各クラスの値
0: 2.6651433 1: -1.6024852 2: -0.6885104 3: -0.2711072 4: -0.0937684 5: 0.1937362 6: -0.3147157 7: -0.5869334 8: 0.0727276 9: 0.6259142
クラス"0"の値が一番大きく、次いで、クラス"9"の値が大きくなっている。
クラス"9"の重みを見てみると、左下部分に入力があるとマイナスに作用するようになっている。
クラス"9"のoutputでも左下部分がマイナスとして出力されており、結果、インプット画像がクラス"9"であることの尤度を押し下げている。
(クラス"0"とクラス"9"の特徴分けをしているのは左下部分と言える)
tensorflowメモ(手書き文字認識その1)
ライブラリ「scikit-learn」の手書き文字データセット「digits」を使って、手書き文字認識を試してみる。
入力データ
データセットの構成は以下のとおり。
- 8 x 8ピクセルの画像
- 「0」から「9」までの手書き文字
- 明暗を0から16までの値で表現
- 画像数は1797
1番めの画像のデータ表現。
64個(=8x8)の値が1列に並んで格納されている。
array([ 0, 0, 5, 13, 9, 1, 0, 0, 0, 0, 13, 15, 10, 15, 5, 0, 0, 3, 15, 2, 0, 11, 8, 0, 0, 4, 12, 0, 0, 8, 8, 0, 0, 5, 8, 0, 0, 9, 8, 0, 0, 4, 11, 0, 1, 12, 7, 0, 0, 2, 14, 5, 10, 12, 0, 0, 0, 0, 6, 13, 10, 0, 0, 0])
画像にしてみる。
明度を反転。
これは数字のゼロにあたるものとして、ラベル付けをしている。
学習する際は、0から1の範囲になるようスケーリングする(16で割る)。
array([0. , 0. , 0.3125, 0.8125, 0.5625, 0.0625, 0. , 0. , 0. , 0. , 0.8125, 0.9375, 0.625 , 0.9375, 0.3125, 0. , 0. , 0.1875, 0.9375, 0.125 , 0. , 0.6875, 0.5 , 0. , 0. , 0.25 , 0.75 , 0. , 0. , 0.5 , 0.5 , 0. , 0. , 0.3125, 0.5 , 0. , 0. , 0.5625, 0.5 , 0. , 0. , 0.25 , 0.6875, 0. , 0.0625, 0.75 , 0.4375, 0. , 0. , 0.125 , 0.875 , 0.3125, 0.625 , 0.75 , 0. , 0. , 0. , 0. , 0.375 , 0.8125, 0.625 , 0. , 0. , 0. ])
全データ(1797個)のうち2割にあたる360個を検証用とし、残りの1437個をトレーニング用とする。
モデル
モデルを以下のとおり表現する。
trainY = X * W + b
- trainY : 計算結果を格納する行列(minibatch-size x 10)
- X : 入力データを格納する行列(minibatch-size x 64)
- W : 重みを格納する行列(64 x 10)
- b : バイアスを格納する1次元配列(要素数10)
tensorflowとしては以下のとおり定義する。
- X : tf.placeholder(tf.float32, [None, 64])
- W : tf.Variable(tf.zeros([64, 10]))
- b : tf.zeros([10])
「W」と「b」については、オール0で初期化しているが、これでも最適解は求まる。
学習
損失関数として、計算結果と正解ラベルとのクロスエントロピーを計算する。
loss = - tf.reduce_sum(tf.log(tf.nn.softmax(trainY)) * "正解ラベル(one-hot表現)", axis=1)
この値を最小化する問題として、オプティマイザに渡す(ここでは「勾配降下法」を使う)
tf.train.GradientDescentOptimizer("learning-rate").minimize(loss)
ミニバッチサイズを「128」、学習率を「0.001」、エポックを「100」回に設定してトレーニングを実施。
各エポックにおける、テストデータに対する認識精度は以下のとおりであった。
学習 1回目完了のモデル : accuracy = 80.6(%) 学習11回目完了のモデル : accuracy = 88.6(%) 学習21回目完了のモデル : accuracy = 90.8(%) 学習31回目完了のモデル : accuracy = 91.7(%) 学習41回目完了のモデル : accuracy = 93.1(%) 学習51回目完了のモデル : accuracy = 93.6(%) 学習61回目完了のモデル : accuracy = 93.9(%) 学習71回目完了のモデル : accuracy = 94.7(%) 学習81回目完了のモデル : accuracy = 95.0(%) 学習91回目完了のモデル : accuracy = 95.0(%)
モデルの初期パラメータをオールゼロに設定したが、1回の学習で80%、最大95%の認識精度が出ている。
バイアスはオールゼロのままで、更新されることはなかった。
損失計算の過程を追ってみる。
11回目の学習が完了したモデルに、冒頭の画像データ("ゼロ"とラベリングされたもの)を渡すとする。
入力画像1つをモデルに通した後の結果
data_0 = np.dot(np.reshape(X[0], (1,64)), train_W) + train_b [[ 2.6651433 -1.6024852 -0.6885104 -0.27110717 -0.09376842 0.19373618 -0.31471574 -0.5869334 0.07272757 0.62591416 ]]
softmax関数を通した結果
data_0_softmax = sess.run(tf.nn.softmax(data_0)) [[ 0.64753246 0.00907515 0.02263541 0.03436087 0.0410281 0.05469443 0.03289465 0.02505547 0.04846071 0.08426274 ]]
対数をとる
data_0_softmax_log = sess.run(tf.log(data_0_softmax) [[ -0.43458635 -4.702215 -3.78824 -3.3708367 -3.193498 -2.9059935 -3.4144452 -3.6866632 -3.0270019 -2.4738154 ]]
入力画像に紐付けられる正解ラベル(one-hot表現)
Y[0] [1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
計算結果と正解ラベルとのクロスエントロピーを計算
data_0_softmax_log_xent = data_0_softmax_log * np.reshape(Y[0], (1,10)) [[ -0.43458635 -0. -0. -0. -0. -0. -0. -0. -0. -0. ]]
2次元目のレベルで足し合わせ
data_0_softmax_log_xent_sum = sess.run(tf.reduce_sum(data_0_softmax_log_xent, axis=1)) [ -0.43458635 ]
ここで尤度計算は、確率同士の掛け算を扱うことになるが、アンダーフローを引き起こすので、対数尤度に置き換えて最適化を考えている。
正解に近いほど値は大きくなり(=絶対値が小さくなる)、正解から乖離するほど値は小さくなる(絶対値が大きくなる)。
マイナスを掛けて最小化問題に置き換える
[ 0.43458635 ]
ミニバッチサイズ分、これと同様の値が得られる。
学習においては、この値が小さくなるよう、モデルのパラメータをチューニングする。