【ひらけごま】STFT,VGG16-like,そしてPyaudioで「エッジ音声認識アプリ」を作ってみた♬

#「ひらけごま」できた！
作ろうと思ったが、なんとなく時が過ぎてしまった。
そして最近、アレクサ使い始めてそれなりな（拒否が多い）ので、「ひらけごま」作ってみた。
今となっては、簡単そうだが、それでもそれなりに実現するのは難しい。
※エッジアプリの音声認識やエッジ利用のシーンもそれなりに有効だと考えられる
###やったこと
順序はやった順番ではないが、わかりやすいと思うので、以下の順に記載していこうと思う。
・「ひらけごま」と「おはよう」の識別をやってみた
・「ひらけごま」と「おはよう」をVGG16-likeモデルで学習
・Pyaudioで学習データを集めること
・「ひらけごまアプリ」の説明
###コードは以下に置きました
・[MuAuan/hirakegoma](https://github.com/MuAuan/hirakegoma)
###・「ひらけごま」と「おはよう」の識別をやってみた
識別といっても、マイクに話すと「ひらけごま」と「おはよう」を拾って、異なる反応をするというだけのもので、今回は単に同じ「ひらけごま」なら予め録音しておいたhirakegoma.wavを再生し、「おはよう」ならohayo.wavを再生する。一応、音は消えちゃうので履歴として標準出力に予測結果とhirakegomaとohayoと記載した。
動きは以下のとおりである。

```
①　マイク；ひらけごま
　　　↓
②　音声が閾値を超えると、Pyaudioで音声を拾う
　　　↓
③　STFTして画像ファイルfigure.jpgを./out_test/に出力
　　　↓
④　1秒ごとにfigure.jpgを読み込んで、VGG16-like cnnで推論
　　　↓
⑤　予測結果に応じてwavファイルで音を出力する
　画面に予測結果を出力
　　　↓
⑥　①に戻る
```
上記のフローから分かるように、今回は垂れ流しである。
###・「ひらけごま」と「おはよう」を[VGG16-likeモデル](https://github.com/MuAuan/hirakegoma/blob/master/vgg16_like.py)で学習
学習用コード
[hirakegoma/VGG16_originalData.py](https://github.com/MuAuan/hirakegoma/blob/master/VGG16_originalData.py)
データ取得コード
[hirakegoma/getDataSet.py](https://github.com/MuAuan/hirakegoma/blob/master/getDataSet.py)
まず、肝心な識別は以下のように実施した。

```
工夫点は以下のとおり
・学習用データはPyaudioで録音しつつSTFTして結果を図（スペクトログラム）で蓄積するが、座標等は一切表示しないこととした
・当初は、Augumentationによって900個に増やしたが、単に時間軸の移動と音の周波数の変換を実施したがうまくいかなかった。
そこで、「ひらけごま」と「おはよう」を何度も録音して、それぞれ実データを218個ずつ取得して180個を学習データ、38個を検証用データにした
※なお、学習データのバリエーションは、ほぼ綺麗に録音できたものをセレクトした
　つまり、「ゴマ」とか「ひらけ」とか「お」や「は」のみの録音データは入れていない
・VGG16-likeモデルは、深ければよいという感じではなく、今回はマシンのメモリーと収束性の関係で、入力サイズを(none,128,128,3)とし、
  block3までを使った。
・メモリーの関係で、学習はbatch-size=32で実行した。
・BatchNormalizationとdropout(0.5)は収束性と汎化性能のために残した
※なお、収束はぎりぎりな状況である
・つまり、全結合部分は、パラメータ数はdense(2*num_classes)位が少なくて済むが、dense(10*num_classes)位でないと収束しない。
block5まで利用した方が、パラメータ的（数的にも特徴量的）には有利であるが、（学習データが少なくかつデータ間の相関が高いため）
収束性とメモリーの関係でblock3までを利用した。
```
###・Pyaudioで学習データを集めること
学習コードは以下のコードで収集した。

```pyaudio_realtime_last.py
# -*- coding:utf-8 -*-
import pyaudio
import time
import matplotlib.pyplot as plt
import numpy as np
import wave
from scipy.fftpack import fft, ifft
from scipy import signal
```
以下の関数で音声が一定の閾値を超えると測定開始する。

```pyaudio_realtime_last.py
def start_measure():
    CHUNK=1024
    RATE=44100 #11025 #22050  #44100
    p=pyaudio.PyAudio()
    input = []
    stream=p.open(format = pyaudio.paInt16,
                  channels = 1,
                  rate = RATE,
                  frames_per_buffer = CHUNK,
                  input = True) 
    input =stream.read(CHUNK)
    sig1 = np.frombuffer(input, dtype="int16")/32768.0
    while True:
        if max(sig1) > 0.001:
            break
        input =stream.read(CHUNK)
        sig1 = np.frombuffer(input, dtype="int16")/32768.0
    stream.stop_stream()
    stream.close()
    p.terminate()
    return
```
pyaudioの設定をして、測定に入る。

```pyaudio_realtime_last.py
N=50
CHUNK=1024*N
RATE=44100 #11025 #22050  #44100
p=pyaudio.PyAudio()
stream=p.open(format = pyaudio.paInt16,
              channels = 1,
              rate = RATE,
              frames_per_buffer = CHUNK,
              input = True)
fig = plt.figure(figsize=(6, 5))

ax2 = fig.add_subplot(111)
#ax2.set_ylabel('Freq[Hz]')
#ax2.set_xlabel('Time [sec]')

start=time.time()
stop_time=time.time()
stp=stop_time
fr = RATE
fn=51200*N/50  #*RATE/44100
fs1=4.6439909297052155*N/50*11025/RATE
fs=fn/fr
print(fn,fs,fs1)
```
繰り返しは、for文で有限回数としている。
このスタートのsの値でファイル蓄積の最初のファイル名を変更している。

```pyaudio_realtime_last.py    
for s in range(0,900,1):
    start_measure()

    input = []
    start_time=time.time()
    input = stream.read(CHUNK)
    stop_time=time.time()
    print(stop_time-start_time)
        
    sig = np.frombuffer(input, dtype="int16")  /32768.0
    nperseg = 1024
    f, t, Zxx = signal.stft(sig, fs=fn, nperseg=nperseg)
    ax2.pcolormesh(fs*t, f/fs/2, np.abs(Zxx), cmap='hsv')
    ax2.set_xlim(0,fs)
    ax2.set_ylim(20,20000)
    ax2.set_yscale('log')
    ax2.set_axis_off()   #軸見出しなどを非表示    
    plt.pause(0.01)
    plt.savefig('train_images/0/figure' +str(s)+'.jpg') #0;ohayo 1;hirakegoma
stream.stop_stream()
stream.close()
p.terminate()
print( "Stop Streaming")
```

ということで、学習データの例
「おはよう」
![figure.jpg](https://qiita-image-store.s3.amazonaws.com/0/233744/b23a7023-ee10-ebeb-a328-d2fff1d422ad.jpeg)
「ひらけごま」
![figure2.jpg](https://qiita-image-store.s3.amazonaws.com/0/233744/a4710edc-35be-9d97-e36e-43fcc547b4aa.jpeg)
推論中は上記のコードの保存先を以下に変更するだけである。

```py
plt.savefig('out_test/figure.jpg')
```
###・「ひらけごまアプリ」の説明
ここまでくれば、あとは簡単である。
・[hirakegoma/out_onsei.py](https://github.com/MuAuan/hirakegoma/blob/master/out_onsei.py)
最初の部分でnp.random.seed(1337)が入っているが、当初安定性が悪かったのでこの初期値依存の可能性を排除するために入れていたが、以下の最終版では必要ないことが判明している。ただし、これを入れるとバグっているときの推論の予測結果のふらつきは無くなり原因特定に役立った。
【参考】
・[Each time I run the Keras, I get different result.  #2743 ](https://github.com/keras-team/keras/issues/2743)

```out_onsei.py
#-*- cording: utf-8 -*-
import numpy as np
#np.random.seed(1337) # for reproducibility
import wave
import pyaudio
from vgg16_like import model_family_cnn
from keras.preprocessing import image
import matplotlib.pyplot as plt
import keras
import time
```
prediction関数で予測している。なお、modelは呼び出し元で上記のmodel_family_cnnを利用している。
実は、以下ではimgのデータ処理を実施しているが、データ処理前には予測結果が無茶苦茶であった。
※学習を以下の処理後にやっているのでそのために必要である

```py
def prediction(imgSrc):
    #np.random.seed(1337) # for reproducibility
    img = np.array(imgSrc)
    img = img.reshape(1, img_rows,img_cols,3)
    img = img.astype('float32')
    img /= 255
    t0=time.time()
    y_pred = model.predict(img)
    return y_pred

num_classes = 2
img_rows,img_cols=128, 128
input_shape = (img_rows,img_cols,3)   #224, 224, 3)
model = model_family_cnn(input_shape, num_classes = num_classes)
# load the weights from the last epoch
model.load_weights('params_hirakegoma-900.hdf5', by_name=True) #params_hirakegoma-61.hdf5
print('Model loaded.')
while True:
    #np.random.seed(1337) # for reproducibility
    img_rows,img_cols=128,128
    imgSrc=[]
    imgSrc = image.load_img("./out_test/figure.jpg", target_size=(img_rows,img_cols))
    plt.imshow(imgSrc)
    plt.pause(1)
    plt.close()
    pred = prediction(imgSrc)
    print(pred[0])
    if pred[0][0]>=0.5: #二択なので、一応0.5以上で選択とした
        filename = "ohayo.wav"
        print("ohayo")
    else:               #おはよう以外は全てhirakegoma。追加してAlexaみたいに「わかりません」とかもありだが失敗しないので。。
        filename = "hirakegoma.wav"
        print("hirakegoma")    
    # チャンク数を指定
    CHUNK = 1024
    #filename = "hirakegoma.wav" #デバッグ用；マイク不調でreal timeで取得できない場合に利用
    wf = wave.open(filename, "rb")
    # PyAudioのインスタンスを生成
    p = pyaudio.PyAudio()
    # Streamを生成
    stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                channels=wf.getnchannels(),
                rate=wf.getframerate(),
                output=True)
    # データを1度に1024個読み取る
    input = wf.readframes(CHUNK)
    # 実行
    while stream.is_active():    
        output = stream.write(input)
        input = wf.readframes(CHUNK)
        if input==b'': #dataがなくなると、''ではなく、b''が返ってくる
            break
# ファイルが終わったら終了処理
stream.stop_stream()
stream.close()
p.terminate()
```

```
【ウワンの駄文的妄想】
このシーケンスで一番悩むのは、計測時間と計測間隔（トリガー）かもしれません。
それなりに長いセンテンスを一連として判断する場合は、もう少し長い計測時間が必要で、
今回は一定の閾値で動き始めますが、商用を考えると出だしは「Alexa」が必要なのが理解できます。
そして肝心なのはそのあとの待ち時間や会話終了判断等がある意味営業秘密になると思われます。
※Alexaも少し長文の質問だと結構混乱するので、たぶん短文ベースで変換⇒意味解釈⇒回答や検索や動作⇒出力
　をほとんどはCacheベースでやっているようです
```
###結果
今回のアプリを動かしてちょっと驚いたことは、「ひらけごま」と「おはよう」しか学習していないが、この二択だと「ひらけ」とか「らけごま」とか、「おはー」や「よう」くらいの短い単語でも数値的にほぼ100%の予測結果を返している。これは二択だと当たり前なのかもだけど、そんな感じに見えないので、もう少しカテゴリを増やして同様な事象となるか実験したいと思う。
上にしめしたような通常の周波数の音声しか入れていないが、以下のようにとっても高周波の「おはよう」や「ひらけごま」も認識する。つまり汎化性能は高そうだ。
「おはよう」
![figure.jpg](https://qiita-image-store.s3.amazonaws.com/0/233744/b705b2f1-3c24-06c6-ffa4-4d54cddadc0d.jpeg)
「ひらけごま」
![figure.jpg](https://qiita-image-store.s3.amazonaws.com/0/233744/610cbe2e-d088-75f4-c9b0-c4bea703081e.jpeg)
###まとめ
・「ひらけごま」と「おはよう」に反応するアプリを作成した
・今回はVGG16-likeなモデルで識別した
・Pyaudioの音声をリアルタイムなSTFT変換して識別に利用した

・機械学習でどこまで行けるかや100個くらいの文章まで拡張したいと思う
・このアプリ利用なTello制御もやってみたいと思う
・Unity連携もできそうである
・今回の結果は終始一貫ウワンの音声でやっているので一般化されたときの精度はどの程度なのかは試してみたい
⇒一応、Githubに学習済データを置いたので試したら教えてくださいm(__)m

###おまけ
出力例

```
[  1.61695224e-19   1.00000000e+00]
hirakegoma
[  1.61695224e-19   1.00000000e+00]
hirakegoma
[  2.75610277e-04   9.99724329e-01]
hirakegoma
[  9.99943733e-01   5.63181093e-05]
ohayo
[  9.99943733e-01   5.63181093e-05]
ohayo
[  9.99943733e-01   5.63181093e-05]
ohayo
[  9.99824941e-01   1.75072724e-04]
ohayo
[  9.99824941e-01   1.75072724e-04]
ohayo
[  9.99824941e-01   1.75072724e-04]
ohayo
[ 0.99702293  0.00297703]
ohayo
[ 0.99702293  0.00297703]
ohayo
[  2.03095806e-05   9.99979734e-01]
hirakegoma
[  2.03095806e-05   9.99979734e-01]
hirakegoma
[  1.00000000e+00   1.99542429e-23]
ohayo
```

VGG16-likeモデル

```
Model loaded.
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_1 (InputLayer)         (None, 128, 128, 3)       0
_________________________________________________________________
conv1_1 (Conv2D)             (None, 128, 128, 32)      896
_________________________________________________________________
conv1_2 (Conv2D)             (None, 128, 128, 32)      9248
_________________________________________________________________
batch_normalization_1 (Batch (None, 128, 128, 32)      128
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 64, 64, 32)        0
_________________________________________________________________
dropout_1 (Dropout)          (None, 64, 64, 32)        0
_________________________________________________________________
conv2_1 (Conv2D)             (None, 64, 64, 64)        18496
_________________________________________________________________
conv2_2 (Conv2D)             (None, 64, 64, 64)        36928
_________________________________________________________________
batch_normalization_2 (Batch (None, 64, 64, 64)        256
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 32, 32, 64)        0
_________________________________________________________________
dropout_2 (Dropout)          (None, 32, 32, 64)        0
_________________________________________________________________
conv3_1 (Conv2D)             (None, 32, 32, 256)       147712
_________________________________________________________________
conv3_2 (Conv2D)             (None, 32, 32, 256)       590080
_________________________________________________________________
conv3_3 (Conv2D)             (None, 32, 32, 256)       590080
_________________________________________________________________
conv3_4 (Conv2D)             (None, 32, 32, 256)       590080
_________________________________________________________________
batch_normalization_3 (Batch (None, 32, 32, 256)       1024
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 16, 16, 256)       0
_________________________________________________________________
dropout_3 (Dropout)          (None, 16, 16, 256)       0
_________________________________________________________________
flatten_1 (Flatten)          (None, 65536)             0
_________________________________________________________________
dense_1 (Dense)              (None, 40)                2621480
_________________________________________________________________
activation_1 (Activation)    (None, 40)                0
_________________________________________________________________
dropout_6 (Dropout)          (None, 40)                0
_________________________________________________________________
dense_2 (Dense)              (None, 2)                 82
_________________________________________________________________
activation_2 (Activation)    (None, 2)                 0
=================================================================
Total params: 4,606,490
Trainable params: 4,605,786
Non-trainable params: 704
_________________________________________________________________
WARNING:tensorflow:Variable *= will be deprecated. Use variable.assign_mul if you want assignment to the variable value or 'x = x * y' if you want a new python Tensor object.
Train on 360 samples, validate on 76 samples
```