Raspberry PiとSimulinkでエッジAI

## この記事について
前回の記事"Raspberry PiとSimulinkでホームLED"で、MQTTベースのIoTシステムを構築しました。このRaspberry PiにAIアルゴリズムを搭載すれば、本格的なエッジAIシステムが作り放題！
なんらかの状態を検出するためデータは、音や振動、カメラ画像など考えられますが、今回は音をマイクで拾うことを考えます。

というわけで、Raspberry Piのマイク入力から音を判別するAIを搭載して、予知保全システムを構築したいと思います。
![model_AI_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/b8b8540e-1e87-fd6a-6e13-19cff07d370c.png)

## 異音判別
題材は工場設備の予知保全を想定して、コンプレッサーの異音判別システムとし、工場内分散型ネットワークのノードとしてRaspberry PiをエッジAIとして稼働させます。エッジAIなので、判別アルゴリズムはRaspberry Pi上で、スタンドアローンで動作します。

対象とする工場設備はシングルステージ レシプロ型 コンプレッサーとします。異常状態を含んだ学習用の音声データが公開されているので、こちらを用いてAIの学習を行います。ややレトロな装置で、確かにいろんなところが壊れそう(=音の分類クラスが多数)。このデータについては後述します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/f7d3542b-1d47-dc0a-fced-2af2fd2edf6d.png" width="300">

Raspberry-Piへの入力はUSBマイク、判定開始トリガや判別結果は前回のホームLEDと同様に、MQTTを経由したNode-REDのフロー画面から管制します。
![system.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/73dfefbc-9548-f075-5e33-fc801e74d71e.png)

コンプレッサーはもちろん実物がなくても大丈夫！音源に関してはスピーカーで代用します。Node-REDのフローで判別開始のトリガを掛ける際、選択した故障モードに応じた音源から、システム経由で音を鳴らすノードを通じて、Windows PCから再生されるようにします。
何か別の対象について独自にシステムを構築したいときは、スピーカーで音を再生するためのNode-REDのフローは必要ありません。実際のデータを採取して学習に使用します。

## ハードウエア
・Raspberry Pi 4B+ (4GB)

・USBマイク   (Raspberry–PiのUSB端子に接続して、ハードウエア入力とします)
![microphone.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/55285fce-d0f8-6378-3a31-5e37e235b0e5.png)

https://www.amazon.co.jp/%E3%82%A8%E3%83%AC%E3%82%B3%E3%83%A0-%E3%82%AF%E3%83%AA%E3%83%83%E3%83%97%E4%BB%98%E3%81%8D-%E3%82%A6%E3%82%A4%E3%83%B3%E3%83%89%E3%82%B8%E3%83%A3%E3%83%9E%E3%83%BC%E4%BB%98-%E5%8F%8E%E7%B4%8D%E3%83%9D%E3%83%BC%E3%83%81%E4%BB%98-HS-MC09UBK/dp/B09BN9N1P3

・USBスピーカー (コンプレッサー実物の代わりにWindowsから音源を再生)
![speaker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/6fbb451e-045a-11bf-0dc5-c32d1ee2abc9.png)

https://www.amazon.co.jp/gp/product/B075M7FHM1/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&th=1

スピーカーは、実機の代わりに音を出しているだけなので、自分で学習用のデータを収集する場合は不要です。またマイク、スピーカーとも、これがお勧めというわけではなく、単にお安かったからなのですが、スピーカーに関しては後述する理由で、アナログ入力ではないUSB接続のものが良いです。

## ソフトウエア
MQTT環境とMATLAB/Simulinkが必要です。セットアップについては"Raspberry PiとSimulinkでホームLED"と全く同じなので、以下の記事を参照ください。
Link

![system_mqtt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/16638309-0e7f-db4a-3c02-15e85567646d.png)

## Simulinkモデル、学習用MATLABコード、データ
関連するSimulinkモデル、学習用データ、学習用MATLABコードは以下を参照

https://github.com/kureta125/raspi_air_compressor_wavelet_scattering

・Raspi_LSTM.slx  ハードウエア動作用　Simulinkモデル
・Raspi_LSTM_Sim.slx  シミュレーション用　Simulinkモデル
・AirCompressorDatadet_ALSA  学習用データ　(再度　録音したもの)
・nefProjectedFinetuned.mat  学習済みLSTMネットワーク
・waveletScatteringLSTM_projection_HW　学習・サイズ調整用MATLABライブスクリプト

学習用データは後述する理由で、以下のように録音し直したものを使用し、掲載しています。
　<< オリジナル音源ファイル再生　-> スピーカー -> マイク-> ファイル録音 >>
再録音したものでなくオリジナルのデータを入手したい場合は、下記リンクの"Dataset"の項を参照ください。

https://jp.mathworks.com/help/wavelet/ug/air-compressor-fault-detection-using-wavelet-scattering.html?searchHighlight=compressor%20fault&s_tid=srchtitle_support_results_1_compressor%20fault#AirCompFaultWaveletScatteringExample-1


## アルゴリズム
Simulink環境ではAIアルゴリズムもブロックを配置して表現します。AIアルゴリズムを含んだ形でハードウェア全体を記述します。マイクで拾った音声出力をウェーブレット変換で前処理をして、LSTMのネットワークで結果を分類し、MQTTで結果をパブリッシュするという、ハードウェアを含んだシステム全体をブロックで表現しています。

![model_AI_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/56c12d30-4b4e-f6f8-f6a3-ca66e9d7b4fd.png)

ウェーブレット変換で特徴量を抽出する前処理を施すと精度が向上します。さらに前処理によってネットワークで扱う信号も圧縮され、ネットワークサイズの大幅な削減が見込めます。このあたりの詳細は、今回のテーマではないため扱いませんが、MATLABでのネットワークの学習についてはご興味がありましたら以下のリンクより関連する動画を視聴ください。


予知保全・異常検知のためのデータセントリックAI
https://www.youtube.com/watch?v=OoZmeFqYG_Q

王道! 時系列データで学ぶ6種の特徴抽出と異常検知
https://www.youtube.com/watch?v=CaF7d_V2JHQ&list=PLPQVx3HzGQWLSSPhWQ8dzynY1nUC_tid2&index=4

学習およびネットワークサイズの調整は、上記gitに含まれる学習・サイズ調整用MATLABスクリプトを参照ください。サイズ調整には投影という技法を用いています。
waveletScatteringLSTM_projection_HW　
精度とサイズ調整のトレードオフを探るために試行錯誤しますが、このあたりは別途記事化したいと思います。

一方、AIモデル(ネットワーク)の学習や調整はMATLABで行い、matファイルに取り込んだ学習済みのネットワークを読み込んでいます。上記ネットワークブロックをクリックして、matファイルのパスを指定します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/da539c77-395b-18a8-5444-97c3b26f9901.png" width=450>

## まず音声ファイルを使ってシミュレーション
Simulinkでは同一のモデルを使用してシミュレーション/実機動作の両方が可能です。ただ入出力ソースのブロックはシミュレーション用と実機動作用をそれぞれ用意して、モデル上で切り替える必要があります。
![model_sim.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/4195185d-8745-a1c1-103e-bd766e46d370.png)

まずはシミュレーションで、ハードウエアのマイク入力の代わりに音声ファイルからのデータを選択して再生し、アルゴリズムを確認します。音を分類する、この主要なアルゴリズムの内部については続く章で説明します。

いろいろな故障モードの音を選ぶのに"knob"というブロックを使用しています。また判定開始のトリガは実機の場合はMQTTのトピックですが、シミュレーション用にプッシュボタンのブロックを使用しています。

分類のアルゴリズムに相当する部分は、中ほどのCrassifireブロックに含まれます。先に述べたwavelet変換とネットワークで構成されています。実際にはメモリを節約するため、wavelet変換の前後でデータを間引いています。

また可視化のブロックを追加して、信号の様子をモニターすることができます。採音トリガと切り出した音声信号の波形観測、およびスペクトラムアナライザで音声信号をモニタしています。
Simulinkの"シミュレーション"タブの"実行"ボタンを押すと、シミュレーションを開始します。

![sim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/77fcbd68-afa2-831c-87f0-8fd1829c6c87.gif)

例えば1番を選んだときにボタンをトリガーとして結果を見ると、1番として結果が出ています。2番を選んで判定開始のトリガを掛けると2番を出力し、3番を再生すると3番、というように、シミュレーションベースでは正しく判別ができました!


## 実機動作- Node-REDフロー(管制用)
つぎにエクスターナルモードで実機での動作確認です。コンプレッサーの代わりにWindowsに接続されたスピーカーを、Rspberry Piに接続されたUSBマイクに対向させて配置します。
![ant.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/0355c64b-d242-4d80-3082-2a2914de2174.png)

Node-RED側をセットアップします。セットアップとNode-REDへのフローの読み込みは、"Raspberry PiとSimulinkでホームLED"を参照ください。

https://qiita.com/kureta90/items/edbbf899cd218fe5cdf5

.json形式のモデルは、以下のテキストをflows.jsonファイルとして保存し、Node-REDの"読み込み"-"ファイル選択"から指定してください(あるいは先のgitのutils/flows.jsonをコピー)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/8fd27be8-c2db-d841-6ef8-40608dca71e5.png" width="200")>

``` flows.json
[{"id":"589f99e2.f30b18","type":"tab","label":"フロー 1","disabled":false,"info":""},{"id":"540bb556.207f9c","type":"mqtt in","z":"589f99e2.f30b18","name":"from_rasp4B_state","topic":"home/rasp_4B/state","qos":"1","datatype":"auto","broker":"49b108fc.5ad008","x":370,"y":480,"wires":[["6490785b.726768"]]},{"id":"6490785b.726768","type":"debug","z":"589f99e2.f30b18","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":567,"y":480,"wires":[]},{"id":"174ee928.b993e7","type":"inject","z":"589f99e2.f30b18","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":90,"y":140,"wires":[["a9d443d0.1745c","6ece178e.7d6078"]]},{"id":"48e3ebac.64c544","type":"inject","z":"589f99e2.f30b18","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"2","payloadType":"num","x":90,"y":180,"wires":[["a9d443d0.1745c","6ece178e.7d6078"]]},{"id":"392fd29.924192e","type":"file in","z":"589f99e2.f30b18","name":"Bearing","filename":"ABSOLUTE_PATH_ON_YOUR_SYSTEM\\LSTM_Wavelet_MQTT\\AirCompressorDataset\\Bearing\\preprocess_Reading10.wav","format":"","chunk":false,"sendError":false,"encoding":"none","x":600,"y":80,"wires":[["6580cb5e.bfebe4"]]},{"id":"e46d174f.3af9c8","type":"file in","z":"589f99e2.f30b18","name":"Flywheel","filename":"ABSOLUTE_PATH_ON_YOUR_SYSTEM\\LSTM_Wavelet_MQTT\\AirCompressorDataset\\Flywheel\\preprocess_Reading20.wav","format":"","chunk":false,"sendError":false,"encoding":"none","x":600,"y":120,"wires":[["6580cb5e.bfebe4"]]},{"id":"61422f3e.10b95","type":"inject","z":"589f99e2.f30b18","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":90,"y":80,"wires":[["a9d443d0.1745c","6ece178e.7d6078"]]},{"id":"d13a4541.9944b8","type":"inject","z":"589f99e2.f30b18","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"3","payloadType":"num","x":90,"y":220,"wires":[["a9d443d0.1745c","6ece178e.7d6078"]]},{"id":"684f6d9d.15ed74","type":"inject","z":"589f99e2.f30b18","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"4","payloadType":"num","x":90,"y":260,"wires":[["a9d443d0.1745c","6ece178e.7d6078"]]},{"id":"948f8117.807db","type":"inject","z":"589f99e2.f30b18","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"5","payloadType":"num","x":90,"y":300,"wires":[["a9d443d0.1745c","6ece178e.7d6078"]]},{"id":"3f05c988.4bb0c6","type":"inject","z":"589f99e2.f30b18","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"6","payloadType":"num","x":90,"y":340,"wires":[["a9d443d0.1745c","6ece178e.7d6078"]]},{"id":"3edc6f2b.6e087","type":"inject","z":"589f99e2.f30b18","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"7","payloadType":"num","x":90,"y":380,"wires":[["a9d443d0.1745c","6ece178e.7d6078"]]},{"id":"7b53228e.3f1cbc","type":"file in","z":"589f99e2.f30b18","name":"Healthy","filename":"ABSOLUTE_PATH_ON_YOUR_SYSTEM\\LSTM_Wavelet_MQTT\\AirCompressorDataset\\Healthy\\preprocess_Reading2.wav","format":"","chunk":false,"sendError":false,"encoding":"none","x":600,"y":20,"wires":[["6580cb5e.bfebe4"]]},{"id":"62db83cc.a48e1c","type":"file in","z":"589f99e2.f30b18","name":"LIV","filename":"ABSOLUTE_PATH_ON_YOUR_SYSTEM\\LSTM_Wavelet_MQTT\\AirCompressorDataset\\LIV\\preprocess_Reading70.wav","format":"","chunk":false,"sendError":false,"encoding":"none","x":590,"y":160,"wires":[["6580cb5e.bfebe4"]]},{"id":"d3b77994.abf098","type":"file in","z":"589f99e2.f30b18","name":"LOV","filename":"ABSOLUTE_PATH_ON_YOUR_SYSTEM\\LSTM_Wavelet_MQTT\\AirCompressorDataset\\LOV\\preprocess_Reading2.wav","format":"","chunk":false,"sendError":false,"encoding":"none","x":590,"y":200,"wires":[["6580cb5e.bfebe4"]]},{"id":"72455b86.e1ad54","type":"file in","z":"589f99e2.f30b18","name":"NRV","filename":"ABSOLUTE_PATH_ON_YOUR_SYSTEM\\LSTM_Wavelet_MQTT\\AirCompressorDataset\\NRV\\preprocess_Reading95.wav","format":"","chunk":false,"sendError":false,"encoding":"none","x":590,"y":240,"wires":[["6580cb5e.bfebe4"]]},{"id":"38010934.aef676","type":"file in","z":"589f99e2.f30b18","name":"Piston","filename":"ABSOLUTE_PATH_ON_YOUR_SYSTEM\\LSTM_Wavelet_MQTT\\AirCompressorDataset\\Piston\\preprocess_Reading3.wav","format":"","chunk":false,"sendError":false,"encoding":"none","x":590,"y":280,"wires":[["6580cb5e.bfebe4"]]},{"id":"6816b245.3d74fc","type":"file in","z":"589f99e2.f30b18","name":"Riderbelt","filename":"ABSOLUTE_PATH_ON_YOUR_SYSTEM\\LSTM_Wavelet_MQTT\\AirCompressorDataset\\Riderbelt\\preprocess_Reading3.wav","format":"","chunk":false,"sendError":false,"encoding":"none","x":600,"y":320,"wires":[["6580cb5e.bfebe4"]]},{"id":"a9d443d0.1745c","type":"switch","z":"589f99e2.f30b18","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"num"},{"t":"eq","v":"1","vt":"num"},{"t":"eq","v":"2","vt":"num"},{"t":"eq","v":"3","vt":"num"},{"t":"eq","v":"4","vt":"num"},{"t":"eq","v":"5","vt":"num"},{"t":"eq","v":"6","vt":"num"},{"t":"eq","v":"7","vt":"num"}],"checkall":"true","repair":false,"outputs":8,"x":390,"y":80,"wires":[["7b53228e.3f1cbc"],["392fd29.924192e"],["e46d174f.3af9c8"],["62db83cc.a48e1c"],["d3b77994.abf098"],["72455b86.e1ad54"],["38010934.aef676"],["6816b245.3d74fc"]]},{"id":"6ece178e.7d6078","type":"delay","z":"589f99e2.f30b18","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":360,"y":400,"wires":[["693dbe2c.eae4e"]]},{"id":"693dbe2c.eae4e","type":"mqtt out","z":"589f99e2.f30b18","name":"to_rasp_4B/start","topic":"home/rasp_4B/start","qos":"1","retain":"true","broker":"49b108fc.5ad008","x":580,"y":400,"wires":[]},{"id":"6580cb5e.bfebe4","type":"play audio","z":"589f99e2.f30b18","name":"","voice":"0","x":810,"y":260,"wires":[]},{"id":"49b108fc.5ad008","type":"mqtt-broker","z":"","name":"mosquitto","broker":"192.168.1.4","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]
``` 
読み込みが完了するとフローが現れます
![flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/7d0f0708-de25-b82d-7ce9-a03d0457445f.png)

管制システムとして必要なのは、測定開始トリガの発行と、判定結果の受信ですが、開始トリガは先の音源選択のボタンとPublishノードを連結して、どれかボタンが押されたらトピックを発行するようにします。
送信トピック: to_rasp_4B/start

次章で立ち上げるSimulinkモデルは、トピックを受け取ると判定結果を返します。結果はNode-REDの画面右のデバックウィンドウ(およびSimulinkのデバック用のdisplay)で確認できます。
受信トピック: to_rasp_4B/state

## 実機動作
一方Simulinkは、先と同じシミュレーション用モデルRaspi_LSTM_Sim.slxのシミュレーション入力ブロックから実機ハードウエア入力に切り替えるか(スイッチのブロックをクリック)、あるいはRaspi_LSTM.slx (ハードウエア動作用モデル)を立ち上げます。Simulinkの"ハードウエア"タブの監視と調整ボタンを押すと、実機の動作を開始します。先ほどのシミュレーションとは異なり、モデルから生成したCコードによって、あくまでRaspberry Pi上で動作しています。
![ext_mode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/1bd86384-9540-2e0a-924e-1432c0ea1c32.png)


判定結果のトピック from_rasp_4B/state を読み込むSubsclibeノードからの検出結果を、Node-REDの右横のデバッグウィンドウで結果を確認します。
![mqtt.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583335/4d732f77-c708-adb6-d309-3b1ea1420f53.gif)


正常な音について実施して正常という結果が返ってきました。1番のベアリング異常の音に対しても、正しく1番という検出結果が表示されて、Node-REDのデバック画面にも1番が表示されています。このようにNode-REDをコントローラーとして、ネットワークで繋いだ状態で動作し、正しく判別できています！

## 音源のサンプリング周波数
以下、音声情報についての注意事項ですが、既存の学習データを使わず、自分で直に学習データを採取する場合は関係ありませんので読み飛ばしてください。

オリジナルの音源は16KHzなのに対して、Raspberry Piへのマイク入力では48KHzか44.1KHzを選べます。ALSAの設定で他の周波数も可能なのかもしれませんが、ここでは44.1KHzとしています。
モデルの実行周期は音声のサンプルをベースにしているので、シミュレーションと実機動作を切り替えて使用するには、両者が同じサンプリングレートである必要があります。次に述べる理由で学習用データは再録音していますが、その際ALSAに合わせて44.1KHzで録音しています。

## 音源の違い (ファイル読み取り vs.マイク経由)
当初、公開されている音源ファイルから直接利用して学習したLSTMネットワークをそのままマイク入力の音声信号に適用しようとしたところ、いまいち分類精度が上がりませんでした。音声ファイルを直接処理する場合に比べると、実機の構成では

 << ファイル再生　-> スピーカー -> マイク-> ファイル録音 >>
 
を経ることになり、音が変質してしまいます。そのため、今回はスピーカーとマイクを経由して再度録音し直したファイルを使用して、再学習しました。

 << ファイル再生(ラズパイALSA)　-> スピーカー -> マイク-> ファイル録音(ラズパイALSA) >>
 
本来ファイル再生は、実施時と同じくWindows側で行うべきですが、大量の音声ファイルごとに同期をとって再生&録音を行う手段が見つからず、共にRaspberry Pi上で行いました(ALSAでbashスクリプトを作成、上記giのutils/alsa_rw.sh)
再生側のドライバがWindowsのものとLinux ALSAで異なるわけですが、せめてアナログ処理部はWindows-Linuxで共通になるように、スピーカー内部でハードウエア処理するUSB出力タイプを選んでいます。

再録音したファイルはサンプリング周波数を3倍近く高くしたのにも関わらず、高音域がかなり欠損しているような...かなりお安めのデバイスだったのが良くないのかも...

もっと良いスピーカーおよびマイクを使用すれば、ファイルの音源を元に学習したネットワークを、マイク入力にもそのまま適用できたのかもしれません。

## エッジデバイスのリソースとAI精度のトレードオフ
予知保全をはじめとする、AIを使ったアルゴリズムの開発では、AIモデルの精度と、限られたメモリに収めるためのトレードオフを探る試行錯誤が欠かせません。実際には、一生懸命アルゴリズム作ってみたけどメモリサイズの制約からデバイスに実装できないことはよく起こります。
今回、所要メモリや処理負荷に関わる主なパラメータは、

・入力信号サブサンプリング 　もともと16KHzで判別できた、1/3に -> Wavelet処理負荷軽減
・Wavelet変換後のサブサンプリング　　1/2程度　-> LSTMへの入力減
・判別に使用する音の長さ　　処理フレーム長 = 2秒　-> Wavelet処理負荷軽減
・LSTMネットワークの投影(プロジェクション)　　-> LSTMネットワークサイズ ほぼ1/3

等があり、メモリが不足するとSegmentation faultエラーに、処理負荷は処理フレーム時間内に処理が完了しないとパイプラインが壊れるエラーに遭遇します。

今回も、
<< 実装->リソース不足->精度を犠牲に緩和して精度低下が許容範囲かシミュレーションで確認 >>
というフローを何回か繰り返しています。このあたりの経緯とトレードオフポイント探索の手法については、後日別の記事を発行する予定ですが、実機実行とシミュレーションを容易に切り替えられるのはとても大きなメリットです。