Raspberry pi,Jetson Nanoでリアルタイム音声信号への機械学習モデルによるエフェクト(Raspberry pi ,Jetson Nanoでのリアルタイム推論編)

[前回](https://qiita.com/kmitsu76/items/714258bddde272621518)作成したMyChain.nnpを[環境構築](https://qiita.com/kmitsu76/items/426a03cb84ef8d895d28)したRaspberry pi ,Jetson Nanoへ転送させてPythonで推論させてみる。

オーディオ入出力は[Raspberry Pi, Jetson Nano向けオーディオ入出力ボードの作成(動作コード例編)](https://qiita.com/kmitsu76/items/0a0fac24d448ccf242fd)で完成したコーデックボードを使用する。

上記ページのボイラープレートコードへ、単純に推論部分を挿入してみると、すでに想定されていたように、推論の時間がかかりすぎ、リアルタイム処理に間に合わない。

リアルタイム処理に耐えられるように、試行錯誤で考えた処理モデルは下記の通り。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/c969a82f-9067-fdda-c7e6-83d08c7dbc95.png)
NNablaの学習モデルを各コアへ展開し、4並列で推論させて時間を稼ぐ。
出力は処理が終わった順にキューに積み、出力させる。
これを実現するポイントとなるコード部分が下記。

multiprocessingでタスクを振るためには、あらかじめpoolの初期化でNNablaの学習モデルを各コアにロードする重い処理を事前に行っておく必要がある。
そのために、
`p = Pool(processes=4 , initializer=init_worker)`
このようにして4プロセスをinit_workerで初期化する。

```py
def init_worker():

    import nnabla as nn
    import nnabla.functions as F
    import nnabla.parametric_functions as PF
    from nnabla.utils.nnp_graph import NnpLoader


    nnp = NnpLoader("MyChain.nnp")
    net = nnp.get_network("MyChain", batch_size)

    global xx
    global yy

    xx = net.inputs['xx']
    yy = net.outputs['yy']
```
関数内でNNablaをインポートするトリッキーなやり方だが、上記で、各コアへモデルロードができた。


sounddeviceのコールバックでは

```py
def callback(in_data, out_data, frames, time, status):

        try:
            res= p.apply_async(processing, args=(in_buffer, q_out ,))  # Create new process
            res.get(timeout=0)
        except TimeoutError as err:
            pass


        in_buffer = np.zeros(0, np.float32)  # Empty the input buffer
```

上記でapply_asyncで並列にタスクを投げ、結果は関数に戻すのではなくキューに積むため、返り値確認処理はいらないので、TimeoutError をpassさせる。


各コアで行われる処理は、

```py
def processing(indata, q ):

    x = sliding_window(padded, input_timesteps, output_timesteps)
    x = x[:, :, np.newaxis]
    xx.d = x

    yy.forward()

    time.sleep(0.4)

    q.put(yy.d[:, -output_timesteps:, :].reshape(-1)[:d_len])
```

スライディングウインドウを適用し、推論させ、キューに積む作業を行う。

メインでは、ストリームが安定して出せるまでのゼロフィルのキューを積んでおく。
これで、multiprocessingのCopy On Write処理でストリームが開始された後想定外の処理が走らないようにする意味もある。

```py
def main():

    for i in range(4):
        prefill = np.zeros(block_size, np.float32)
        p.apply(processing, args=(prefill, q_out ,))

    try:
        with sd.Stream(device=1,
                samplerate=RATE, blocksize=CHUNK,
                dtype=np.float32,
                channels=1,
                callback=callback,
                prime_output_buffers_using_stream_callback=True):
            print('#' * 80)
            print('press Return to quit')
            print('#' * 80)
            print(" ")
            input()
    except KeyboardInterrupt:
        parser.exit('')
        p.close()
    except Exception as e:
        parser.exit(type(e).__name__ + ': ' + str(e))

```

余談だが、multiprocessingはLinuxでは標準はforkモード動作なので、Windows,Macの場合、動作モードを変更する必要があるらしい。

[Python multiprocessing ドキュメント](https://docs.python.org/ja/3/library/multiprocessing.html)
>コンテキストと開始方式
>プラットフォームにもよりますが、multiprocessing はプロセスを開始するために 3 つの方法をサポートしています。それら 開始方式 は以下のとおりです

>spawn
>親プロセスは新たに python インタープリタープロセスを開始します。子プロセスはプロセスオブジェクトの run() メソッドの実行に必要なリソースのみ継承します。特に、親プロセスからの不要なファイル記述子とハンドルは継承されません。この方式を使用したプロセスの開始は fork や forkserver に比べ遅くなります。

>Unix と Windows で利用可能。Windows と macOS でのデフォルト。

>fork
>親プロセスは os.fork() を使用して Python インタープリターをフォークします。子プロセスはそれが開始されるとき、事実上親プロセスと同一になります。親プロセスのリソースはすべて子プロセスに継承されます。マルチスレッドプロセスのフォークは安全性に問題があることに注意してください。

>Unix でのみ利用可能。Unix でのデフォルト。

>forkserver
>プログラムを開始するとき forkserver 方式を選択した場合、サーバープロセスが開始されます。それ以降、新しいプロセスが必要になったときはいつでも、親プロセスはサーバーに接続し、新しいプロセスのフォークを要求します。フォークサーバープロセスはシングルスレッドなので os.fork() の使用に関しても安全です。不要なリソースは継承されません。

>Unix パイプを経由したファイル記述子の受け渡しをサポートする Unix で利用可能。

>バージョン 3.8 で変更: macOS では、 spawn 開始方式がデフォルトになりました。 fork 開始方法は、サブプロセスのクラッシュを引き起こす可能性があるため、安全ではありません。 bpo-33725 を参照。

>バージョン 3.4 で変更: すべての Unix プラットフォームで spawn が、一部のプラットフォームで forkserver が追加されました。Windows では親プロセスの継承可能な全ハンドルが子プロセスに継承されることがなくなりました。


この結果、Jetson Nanoでは、キュー余り3の余裕の動作を行い、リアルタイム推論が行えた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/0c092222-caf0-f285-0e83-a3f03e6435d6.png)


その際のtopによるシステム負荷状況。

top - 22:55:28 up 25 min,  2 users,  load average: 1.14, 1.16, 0.81
Tasks: 280 total,   2 running, 278 sleeping,   0 stopped,   0 zombie
%Cpu(s): 32.4 us,  3.3 sy,  0.0 ni, 63.7 id,  0.0 wa,  0.3 hi,  0.2 si,  0.0 st
KiB Mem :  4059472 total,  1346492 free,  1813648 used,   899332 buff/cache
KiB Swap:  2029728 total,  2029728 free,        0 used.  2069116 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 8670 kmwebnet  20   0  329388 217208  26360 S  29.0  5.4   2:28.37 python3
 8671 kmwebnet  20   0  329384 216988  26360 S  25.1  5.3   2:28.03 python3
 8672 kmwebnet  20   0  329384 216984  26360 R  41.9  5.3   2:28.66 python3
 8673 kmwebnet  20   0  329384 216972  26360 S  25.4  5.3   2:29.18 python3

各CPU負荷３０％くらい、メモリは各コア217MBくらいの動作。
せっかくのGPUを使えていないが、動作としては安定している。


Raspberry pi 3B+では、スワップファイルへの書き出しが起こるとリアルタイム処理に影響が出るのでスワップファイルを無効にした。メモリも1GBでぎりぎりなので以下の処理を行った。

[GPUメモリ削減-最低の16MBへ](http://denshikousaku.net/save-raspberry-pi-memory)

[SWAP止める](https://qiita.com/hinemoss/items/4bee2d15134cdee2e80c)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/8a2b11ff-576e-c609-19c8-e3d7c537c673.png)



システムの状態をtopで確認。
top - 16:50:11 up 20 min,  2 users,  load average: 3.00, 2.93, 2.04
Tasks: 110 total,   3 running, 107 sleeping,   0 stopped,   0 zombie
%Cpu(s): 73.6 us,  0.3 sy,  0.0 ni, 26.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :    975.6 total,    223.3 free,    667.3 used,     85.0 buff/cache
MiB Swap:   2048.0 total,   2044.5 free,      3.5 used.    253.8 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
  655 pi        20   0  219980 180608  23788 S  61.5  18.1  12:16.52 python3
  658 pi        20   0  219724 180332  23764 R  76.4  18.1  12:17.68 python3
  656 pi        20   0  219724 180012  23448 R  77.4  18.0  12:20.02 python3
  657 pi        20   0  219468 179764  23460 S  75.7  18.0  12:20.11 python3
  652 pi        20   0   85144  25956  13892 S   2.3   2.6   0:25.19 python3
  662 pi        20   0   83804  18100   5660 S   1.3   1.8   0:12.00 python3

これでq_size 1で回った。
各CPU負荷77％くらい、メモリは各コア180MBくらいの動作。
CPUは結構熱くなる。
おそらくRaspberry pi4 2GB以上のメモリ環境が望ましいだろう。

別のJetson Nanoからテストサウンドを入力させ、また別のJetson Nanoで録音し、エフェクトのかかり具合を確認した。念のため、Cakewalk上で通常通りプラグインに通してエフェクトをかけたものをエクスポートし、比較用に用意した。

エフェクト前　strat-riff_115bpm_E_minor.wav
[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/Lrwtn8dOVQg/0.jpg)](http://www.youtube.com/watch?v=Lrwtn8dOVQg)
エフェクト後　strat-riff_115bpm_E_minor-effected.wav
[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/iqqityj1LME/0.jpg)](http://www.youtube.com/watch?v=iqqityj1LME)
Cakewalkでエフェクトをかけてエクスポート　strat-riff_115bpm_E_minor-cakewalk-effected.wav
[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/Ahe12gzzPA0/0.jpg)](http://www.youtube.com/watch?v=Ahe12gzzPA0)

波形の比較も掲載。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/387653/c025ac5b-f560-886d-06eb-9a4c65c0dab4.png)


※サンプルの作り方が間違っていたので更新（3/17）

##考察
学習データの作成とRaspberry pi、Jetson Nanoへの持ち込みを行い、実装できた。
LSTM中間層セルの数、タイムステップ、バッチサイズ等チューニングの結果がnnpファイルの容量、推論環境へ要求するマシンスペックへどのくらい反映するかの基準を得ることもできた。
組込み向けにはC++や、Cでの実装が必要と想定していたが、Raspberry pi、Jetson NanoではPython 環境のほうが見通しがよく、アーキテクチャも構築しやすいと感じた。

応用として、音声信号の環境に合わせたノイズリダクション等が考えられそうだ。

全体のコードは[github](https://github.com/kmwebnet/Audio-Effect-Realtime-Inference)にて公開中。