ichou1のブログ

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

音声認識メモ(スペクトログラム)

spectrogramに関するメモ。

Wikipediaより

スペクトログラム(英: Spectrogram)とは、複合信号を窓関数に通して、周波数スペクトルを計算した結果を指す。
3次元のグラフ(時間、周波数、信号成分の強さ)で表される。

pythonのmatplotlibライブラリにある「specgram」関数を使ってみる。

プロットする対象は、フーリエ変換のメモで使った波形。

波形データ

f:id:ichou1:20190101110011p:plain

FFT変換後
numpy.fft.fft("波形データ")

f:id:ichou1:20190101112534p:plain

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)

f:id:ichou1:20190101120659p:plain

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)

f:id:ichou1:20190101125306p:plain
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



10 \times \log_{10} (P_{xx})

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

モデル構成
f:id:ichou1:20181229092637p:plain

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の値を予測する。
f:id:ichou1:20181227204147p:plain

インプットデータ
  • 周波数0.01の純音データを200個分し、5個分を1セットとして、196セット
  • レーニング用に9割、検証用に1割を割り当てるとして、176セットが学習用、20セットが検証用
学習
  • バッチサイズは「16」とし、イテレーション数は「11」回
  • 損失関数は「最小二乗法」
  • オプティマイザは「tf.train.AdamOptimizer」(学習率0.001)
  • エポック数は「100」
検証
  • 検証用の「20」セットを使って評価
  • 損失関数は学習と同じ(「最小二乗法」)
RNNの設定
  • Cellは「tf.contrib.rnn.BasicRNNCell」
  • 隠れ層のユニット数は「20」
  • 最後の出力に対して重みとバイアスを与える

(参考)kerasで書き直したモデルをmodel.summary()で表示
f:id:ichou1:20190105083809p:plain

学習で更新されるパラメータ数の内訳

# 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の演算を図にすると以下のとおり。
f:id:ichou1:20181228084830p:plain

Wがinputに対する重み、Uがstateに対する重みに該当する。
inputと重みの演算結果にバイアスを加え、活性化関数(デフォルトはtanh)を通したものを次の時系列のインプットとする。

LSTMについては下記ご参照。
TensorFlowメモ(RNNその2) - ichou1のブログ

tensorflowメモ(手書き文字認識その4)

前回の続き。

精度を上げることを試みる。

tensorflowサンプル(「examples/tutorials/mnist/mnist_deep.py」)を参考にレイヤーを構成。

f:id:ichou1:20181224215851p:plain

"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()によるモデル表示

f:id:ichou1:20190103095723p:plain

学習で更新されるパラメータ数の内訳

# 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」と推測

f:id:ichou1:20181226105340p:plain

「8」とラベル付け、「9」と推測

f:id:ichou1:20181224225034p:plain

tensorflowメモ(手書き文字認識その3)

前回の続き。
モデルを畳み込みニューラルネットワーク (Convolutional Neural Network)に変更してみる。

チュートリアルなどを見ると、畳み込み層を2回通しているケースが多いが、まずは1層のモデルで試してみる。

パラメータは以下のとおりとした。
f:id:ichou1:20181218163944p:plain

(参考)kerasのmodel.summary()によるモデル表示

f:id:ichou1:20190103091254p:plain

学習で更新されるパラメータ数の内訳

# 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個)

f:id:ichou1:20181218172517p:plain

この各フィルタを、入力画像(0から1の範囲に正規化済み)
f:id:ichou1:20181218172531p:plain

に対して適用すると以下の出力が得られる(バイアス項は加えていない状態)
f:id:ichou1:20181218172138p:plain

白寄りの部分が正の値、黒寄りの部分が負の値であることを示している。
この状態からフィルタごとのバイアスを加算し、活性化関数(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)ごとの重みパラメータの変換結果は以下のとおり。

重みパラメータを可視化

f:id:ichou1:20181211112830p:plain

中間値が灰色だとすると、白に近い部分は正、黒に近い部分は負に該当する。

これをゼロとラベル付けした画像に適用してみる。
f:id:ichou1:20181211113542p:plain

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])

画像にしてみる。
f:id:ichou1:20181208085216p:plain

明度を反転。
f:id:ichou1:20181208085521p:plain

これは数字のゼロにあたるものとして、ラベル付けをしている。

学習する際は、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 ]

ミニバッチサイズ分、これと同様の値が得られる。

学習においては、この値が小さくなるよう、モデルのパラメータをチューニングする。