機械学習とRaspberry Piを用いてレーザー加工機の動作状態を判定する（第4回）

# はじめに

この記事は、機械学習とRaspberry Piを用いてレーザー加工機の動作状態を音で判定することに機械学習の非専門家がチャレンジするシリーズの（[IAMAS Advent Calendar 2017](https://qiita.com/advent-calendar/2017/iamas)としては）最終回です。前回までの記事は以下の通りです。

- [機械学習とRaspberry Piを用いてレーザー加工機の動作状態を判定する（第1回）](https://qiita.com/mayfair/items/60d12c5655238c7677c9)
- [機械学習とRaspberry Piを用いてレーザー加工機の動作状態を判定する（第2回）](https://qiita.com/mayfair/items/91b5408a58098ccf920c)
- [機械学習とRaspberry Piを用いてレーザー加工機の動作状態を判定する（第3回）](https://qiita.com/mayfair/items/e8d540ecb259aebea1e2)

前回までに引き続き、もしお気づきの点などあればコメント等で指摘していただけると助かります。

# サービス化

前回までで一通り動くようになりましたので、連続して運用するために`systemctl`を用いてサービス化しました。まず、サービスを定義したファイルを作成します。ここでは、作業ディレクトリの中にある`predict.py`を実行し、何らかの原因で終了した際、常に再起動するよう指定しています。

```bash:laser_machine_listener.service
Description=Laser Machine Listener  

[Service]
ExecStart=/bin/bash -c '/usr/bin/python3 -u predict.py'
WorkingDirectory=/home/pi/Desktop/ml          
Restart=always
User=pi

[Install]
WantedBy=multi-user.target
```

次に、作成したファイルを`/lib/systemd/system/`に移動し、`systemctl`を用いてサービスとして登録し、起動します。以上でコマンドラインから直接実行していたときと同様に動作し、何らかの問題で終了してしまった場合でも自動的に再起動するようになります。

```shell-session
$ sudo mv laser_machine_listener.service /lib/systemd/system/

$ sudo systemctl enable laser_machine_listener.service
Created symlink /etc/systemd/system/multi-user.target.wants/laser_machine_listener.service → /lib/systemd/system/laser_machine_listener.service.

$ sudo service laser_machine_listener start 
pi@kotobukipi:~/Desktop/ml $ sudo service laser_machine_listener status
● laser_machine_listener.service
   Loaded: loaded (/lib/systemd/system/laser_machine_listener.service; enabled; 
   Active: active (running) since Mon 2017-12-18 20:11:37 JST; 37s ago
 Main PID: 1796 (python3)
   CGroup: /system.slice/laser_machine_listener.service
           └─1796 /usr/bin/python3 -u predict.py

Dec 18 20:12:05 kotobukipi bash[1796]: 20:12:05 Background (0.736, processed in 
Dec 18 20:12:06 kotobukipi bash[1796]: 20:12:06 Background (0.763, processed in 
Dec 18 20:12:07 kotobukipi bash[1796]: 20:12:07 Background (0.535, processed in 
...
```

動作中のログは以下のようにすることで確認できます。実際に1週間程度連続で動かしてみたところ、2〜3日に1回程度何らかの原因によって異常終了してしまうことがありました。しかしながら、その後も自動的に再起動して継続的にログがとれていることが確認できました。

```shell-session
$ sudo journalctl -u laser_machine_listener -n 10 -f
-- Logs begin at Fri 2016-11-04 02:16:42 JST. --
Dec 18 20:16:05 kotobukipi bash[1796]: 20:16:05 Background (0.660, processed in 0.092 seconds)
Dec 18 20:16:06 kotobukipi bash[1796]: 20:16:06 Background (0.629, processed in 0.084 seconds)
Dec 18 20:16:07 kotobukipi bash[1796]: 20:16:07 Background (0.692, processed in 0.081 seconds)
...
```

# Chainerで試す

第1回から第3回まではTensorFlowをバックエンドとするKerasで実装してきました。せっかくなので、この機会に[Preferred Networks](https://www.preferred-networks.jp/ja/)が開発する[Chainer](https://chainer.org/)での実装も試してみることにしました。ちょうど「[Chainer Advent Calendar 2017](https://qiita.com/advent-calendar/2017/chainer)」の3日目に「[Chainer v3 ビギナー向けチュートリアル](https://qiita.com/mitmul/items/1e35fba085eb07a92560)」という非常にわかりやすい記事が公開されたため、この記事を参考にしながらKerasで実装していた部分をChainerに置き換えていきました。

## 学習

ニューラルネットワークの構造はKerasで実装したのと同じで3つの全結合層（入力層：1、隠れ層：1、出力層：1）から構成されるものとし、活性化関数はReLUにしました。Kerasと比較すると若干行数が増えるものの簡潔に記述できました。また、学習を実行したところKerasで実装したのと同程度の正解率となりました。

```py3:learn_chainer.py
import glob
import numpy as np
import matplotlib.pyplot as plt
import librosa

import chainer
import chainer.links as L
import chainer.functions as F

SAMPLING_RATE = 16000
FFT_SIZE = 256
STFT_MATRIX_SIZE = 1 + FFT_SIZE // 2

WAV_FILE_PATH = 'data/*.wav'

wav_file_names = []

# 音声データと正解ラベルを収めるための行列を初期化
data = np.empty((0, STFT_MATRIX_SIZE), dtype=np.float32)
index = np.empty(0, dtype=np.int32)

# 音声ファイルを読み込み、正規化したのちにSTFTを求め、同数の正解ラベルと共に登録
for i, wav_file_name in enumerate(sorted(glob.glob(WAV_FILE_PATH))):
    audio, sr = librosa.load(wav_file_name, sr=SAMPLING_RATE, duration=30)
    print('{} ({} Hz) '.format(wav_file_name, sr))
    d = np.abs(librosa.stft(librosa.util.normalize(audio),
                            n_fft=FFT_SIZE, window='hamming'))
    data = np.vstack((data, d.transpose()))
    index = np.hstack([index, [i] * d.shape[1]])
    wav_file_names.append(wav_file_name)

# Chainerで使用する学習データの形式に変換し、全体の80%を学習用、20%を検証用にする
dataset = chainer.datasets.TupleDataset(data, index)
train, test = chainer.datasets.split_dataset_random(dataset,
                                                    int(len(dataset) * 0.8),
                                                    seed=0)

# 3つの全結合層から成るニューラルネットワーク
class LaserMachineListener(chainer.Chain):

    def __init__(self, n_mid_units, n_out):
        super(LaserMachineListener, self).__init__()

        with self.init_scope():
            self.l1 = L.Linear(None, n_mid_units)
            self.l2 = L.Linear(None, n_mid_units // 2)
            self.l3 = L.Linear(None, n_out)

    def __call__(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        return self.l3(h2)

# GPUは使用せずCPUで処理
gpu_id = -1

# 乱数にシードを与えて固定
np.random.seed(1)

# ニューラルネットワークを分類問題用のClassifierで包んでロスの計算などをモデルに含める
# デフォルトのロス関数はF.softmax_cross_entropy
net = LaserMachineListener(n_mid_units=128, n_out=len(wav_file_names))
model = L.Classifier(net)

# 最適化手法としてAdamを選択
optimizer = chainer.optimizers.Adam(alpha=0.01)
optimizer.setup(model)

# バッチサイズは32
batchsize = 32

# Iterator
train_iter = chainer.iterators.SerialIterator(train, batchsize)
test_iter = chainer.iterators.SerialIterator(
    test, batchsize, repeat=False, shuffle=False)
updater = chainer.training.StandardUpdater(
    train_iter, optimizer, device=gpu_id)

# エポック数は10
max_epoch = 10

# Trainer
trainer = chainer.training.Trainer(updater, (max_epoch, 'epoch'), out='result')

# 学習中に出力するログを設定
from chainer.training import extensions

trainer.extend(extensions.LogReport())
trainer.extend(extensions.Evaluator(
    test_iter, model, device=gpu_id), name='val')
trainer.extend(extensions.PrintReport(
    ['epoch',
     'main/loss', 'main/accuracy', 'val/main/loss', 'val/main/accuracy',
     'elapsed_time']))
trainer.extend(extensions.PlotReport(
    ['main/loss', 'val/main/loss'], x_key='epoch', file_name='loss.png'))
trainer.extend(extensions.PlotReport(
    ['main/accuracy', 'val/main/accuracy'], x_key='epoch', file_name='accuracy.png'))

# 学習開始
trainer.run()

# 学習したモデルを保存
model.to_cpu()
chainer.serializers.save_npz('lml_chainer.model', model)
```

```shell-session
$ python learn_chainer.py
data/20171129_background.wav (16000 Hz)
data/20171129_cutting_in_focus.wav (16000 Hz)
data/20171129_cutting_out_focus.wav (16000 Hz)
data/20171129_marking.wav (16000 Hz)
data/20171129_sleeping.wav (16000 Hz)
data/20171129_waiting.wav (16000 Hz)
epoch       main/loss   main/accuracy  val/main/loss  val/main/accuracy  elapsed_time
1           0.402094    0.865148       0.196932       0.934707           2.3065
2           0.153533    0.948972       0.120953       0.959552           5.20568
3           0.124329    0.958917       0.129398       0.954566           8.60824
4           0.120485    0.957583       0.106607       0.959309           11.9062
5           0.109301    0.961028       0.10356        0.961325           15.4137
6           0.106137    0.963306       0.106838       0.960527           20.1581
7           0.111057    0.961833       0.112205       0.957868           24.9144
8           0.105334    0.963417       0.116692       0.958555           28.6653
9           0.102893    0.96381        0.0955747      0.96465            31.9724
10          0.10277     0.964611       0.132107       0.953103           34.8117
```

## 評価

次に、Raspberry Pi上で動かして評価しました。純粋にPythonだけで実装されているChainerのインストールはTensorFlowと比較すると簡単で、以下のように実行するだけでした。

```shell-session
$ sudo pip3 install chainer
Collecting chainer
...
Successfully installed chainer-3.2.0 filelock-2.0.13
```

大半の部分はKerasで実装したものと同じで、ニューラルネットワークに関する部分だけをChainerによる実装で置き換えました。学習の場合と同様に、Kerasと比較すると若干行数が増えるものの簡潔に記述できました。

```py3:predict_chainer.py
import numpy as np
import librosa
import pyaudio
import time
import requests
import apa102

import chainer
import chainer.links as L
import chainer.functions as F


IFTTT_EVENT = 'laser_machine_state'
IFTTT_KEY = '*********************'
IFTTT_URL = 'https://maker.ifttt.com/trigger/{event}/with/key/{key}'.format(
    event=IFTTT_EVENT, key=IFTTT_KEY)


def make_web_request(state):
    data = {}
    data['value1'] = state
    try:
        response = requests.post(IFTTT_URL, data=data)
        print('{0.status_code}: {0.text}'.format(response))
    except:
        print('Failed to make a web request')


leds = apa102.APA102(num_led=3)


def set_leds(state):
    if state == STATES.index('Background'):
        red, green, blue = 0, 0, 0
    elif state == STATES.index('Cutting in focus'):
        red, green, blue = 0, 0, 255
    elif state == STATES.index('Cutting out focus'):
        red, green, blue = 255, 0, 0
    elif state == STATES.index('Marking'):
        red, green, blue = 0, 255, 0
    elif state == STATES.index('Sleeping'):
        red, green, blue = 255, 255, 255
    elif state == STATES.index('Waiting'):
        red, green, blue = 255, 200, 80
    elif state == STATES.index('Unknown'):
        red, green, blue = 0, 0, 0

    for led_num in range(3):
        leds.set_pixel(led_num, red, green, blue)

    leds.show()


SAMPLING_RATE = 16000
CHUNK = 1 * SAMPLING_RATE
FFT_SIZE = 256
THRESHOLD = 0.8

STATES = ['Background',
          'Cutting in focus',
          'Cutting out focus',
          'Marking',
          'Sleeping',
          'Waiting',
          'Unknown']

NUM_STATES = len(STATES) - 1

last_state = STATES.index('Unknown')

# 学習時に用いたのと同じニューラルネットワーク
class LaserMachineListener(chainer.Chain):

    def __init__(self, n_mid_units, n_out):
        super(LaserMachineListener, self).__init__()

        with self.init_scope():
            self.l1 = L.Linear(None, n_mid_units)
            self.l2 = L.Linear(None, n_mid_units // 2)
            self.l3 = L.Linear(None, n_out)

    def __call__(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        return self.l3(h2)

# 保存した学習済みモデルを読み込む
model = L.Classifier(LaserMachineListener(n_mid_units=10,
                                          n_out=NUM_STATES))
chainer.serializers.load_npz('lml_chainer.model', model)

count = 0
predictions_in_60_sec = np.empty((0, NUM_STATES))

audio_interface = pyaudio.PyAudio()
audio_stream = audio_interface.open(format=pyaudio.paInt16,
                                    channels=1,
                                    rate=SAMPLING_RATE,
                                    input=True,
                                    frames_per_buffer=CHUNK)


try:
    while True:
        data = np.fromstring(audio_stream.read(CHUNK),
                             dtype=np.int16)

        audio_stream.stop_stream()

        start = time.time()

        state = last_state

        D = librosa.stft(librosa.util.normalize(data),
                         n_fft=FFT_SIZE,
                         window='hamming')
        magnitude = np.abs(D).transpose()

        predictions = F.softmax(model.predictor(magnitude)).data
        predictions_mean = predictions.mean(axis=0)

        elapsed_time = time.time() - start

        localtime = time.localtime(time.time())
        print('{0.tm_hour:02d}:{0.tm_min:02d}:{0.tm_sec:02d} {1:s} ({2:.3f}, processed in {3:.3f} seconds)'.format(
            localtime,
            STATES[predictions_mean.argmax()],
            predictions_mean.max(),
            elapsed_time))

        if predictions_mean.max() > THRESHOLD:
            state = predictions_mean.argmax()

        if last_state != state:
            print('CHANGED: {0} > {1}'.format(
                STATES[last_state], STATES[state]))
            set_leds(state)
            last_state = state

        if (count == 60):
            start = time.time()
            state_in_60_sec = predictions_in_60_sec.mean(axis=0).argmax()
            make_web_request(STATES[state_in_60_sec])
            predictions_in_60_sec = np.empty((0, NUM_STATES))
            count = 0
            elapsed_time = time.time() - start
            print('Made a request in {0:.3f} seconds)'.format(elapsed_time))

        audio_stream.start_stream()

except KeyboardInterrupt:
    print('Requested to terminate')

finally:
    audio_stream.stop_stream()
    audio_stream.close()
    audio_interface.terminate()
    leds.clear_strip()
    leds.cleanup()
    print('Terminated')
```

以下は実際にRaspberry Pi 3 Model Bで動作させたときのログの例です。期待通り動作しており、かつ処理時間は約0.09秒かかっていたKeras＋TensorFlowよりもさらに高速という結果になりました。

```shell-session
$ python3 predict_chainer.py 
...
23:26:14 Background (0.992, processed in 0.071 seconds)
CHANGED: Unknown > Background
23:26:15 Background (0.834, processed in 0.067 seconds)
23:26:17 Background (0.972, processed in 0.037 seconds)
23:26:18 Background (0.844, processed in 0.065 seconds)
23:26:19 Background (0.949, processed in 0.064 seconds)
```

実は、当初`model.predictor()`には1セットしか与えられないと勘違いしていて、単に

```py3:上記の評価を行った時の実装
predictions = F.softmax(model.predictor(magnitude)).data
```

とすればいいところを以下のようにしていました。

```py3:勘違いしていた時の実装
predictions = np.empty((0, NUM_STATES), dtype=np.float32)

for x in magnitude:
    y = F.softmax(model.predictor(x[None, ...])).data
    predictions = np.vstack((predictions, y))

predictions_mean = predictions.mean(axis=0)
```

この時は1秒分のオーディオサンプルに対してPython上でのループが発生するため、Keras＋TensorFlowの7倍程度の処理時間がかかっていました。この結果から、「やはりC++ベースで実装されているTensorFlowベースの方がPythonだけで実装されているChainerより速いのか…。」と思っていたのですが完全なる勘違いでした。モデルによってどちらが速いかは大きく変わってくると思いますので、次の機会があれば再度比較してみたいところです。

## Raspberry Pi Zero Wでの比較

（2017年12月25日追記）

これまで用いてきたRaspberry Pi 3 Model Bにくわえて、より小型で消費電力の少ないRaspberry Pi Zero Wでも試してみました。

![IMG_5223.JPG](https://qiita-image-store.s3.amazonaws.com/0/92577/2dd442b6-6306-d909-0a3f-a0972d211205.jpeg)

```shell-session:主なライブラリのバージョン
$ pip3 list
chainer (3.2.0)
Keras (2.1.2)
librosa (0.5.1)
numpy (1.13.3)
PyAudio (0.2.11)
scikit-learn (0.19.1)
scipy (1.0.0)
tensorflow (1.4.0)
```

```shell-session:Chainerでの実行結果
14:35:18 Cutting in focus (0.724, processed in 3.850 seconds)
14:35:23 Cutting in focus (0.809, processed in 3.832 seconds)
CHANGED: Unknown > Cutting in focus
14:35:28 Cutting in focus (0.815, processed in 3.830 seconds)
14:35:33 Cutting in focus (0.836, processed in 3.839 seconds)
14:35:38 Cutting in focus (0.808, processed in 3.823 seconds)
```

```shell-session:Keras＋TensorFlowでの実行結果
14:53:48 Background (0.992, processed in 0.656 seconds)
CHANGED: Unknown > Background
14:53:50 Background (0.718, processed in 0.257 seconds)
14:53:51 Sleeping (0.547, processed in 0.257 seconds)
14:53:52 Sleeping (0.571, processed in 0.261 seconds)
14:53:53 Background (0.665, processed in 0.262 seconds)
```

簡単に表にまとめると以下のようになります。Keras + TensorFlowでは3倍強の処理時間がかかったのに対して、Chainerでは50倍以上もの処理時間がかかりました。Raspberry Pi Zero Wに搭載されているCPUはベクトル演算ユニット「NEON」を搭載していないことによる影響もあるかとは思いますが、それにしても違いが大きすぎます。この件については引き続き調査していきたいと思います。

|                        | Keras + TensorFlow | Chainer | 
|:-----------------------|--------:|:------------------:|
| Raspberry Pi 3 Model B | 約0.09秒 | 約0.07秒 |
| Raspberry Pi Zero W    | 約0.3秒 | 約3.8秒 |

# さらなる発展に向けて

## MESHとの組み合わせ

IAMAS Advent Calendar 2017の記事「[Raspberry Pi向けMESHハブアプリとMESH SDKでlocalhostと通信する](https://qiita.com/mayfair/items/b5eb60f8356ffcb5e002)」で紹介したMESHと組み合わせることにより、音声だけではわからない領域までセンシングの幅を拡げることができます。例えば、[人感タグ](http://meshprj.com/jp/tag/MESH-100MD-Motion.html)と組み合わせることにより、レーザー加工機の周辺に人がいるかどうかを検出できます。その情報も合わせて記録することにより、準備から片付けまで含めた時間の中における加工時間の割合を求めるなど、より詳細に状況を判定することができるようになるでしょう。また、音声によって焦点が外れていることを検出した際、近くに人がいればLEDの光や音声で、近くに居ない場合には管理担当者へのメッセージやメールで、のように状況に応じて通知方法を変えることもできるでしょう。

## Movidiusでの実行

GoogleがAIY Projectsの一部として11月に発表した[Vision Kit](https://aiyprojects.withgoogle.com/vision/)は、ボード上にIntelの機械学習プラットフォーム「[Movidius](https://www.movidius.com/)」を搭載しています。Vision Kitという名前の通り、本来は画像認識で活用することを想定したものですが、音声の処理で活用することもできるでしょう。今回は時間軸上での変化が比較的少ない単純な入力を対象とし、かつ種類も6つという小規模な分類問題でしたが、より複雑な入力やより多くの種類を扱うことが今後あればぜひ試してみたいところです。

# おわりに

私自身は、今までにGoogle Cloud Speech APIなど学習済みモデルベースのサービスはしばしば活用してきたものの、チュートリアル以外で実際に自分で学習させてモデルをつくるのはほぼ初めてでした。それでも、いくつかのオンラインコース[^Udemy]で学びつつ手探りで実装することができました。高機能で使いやすいライブラリが提供されることにより、機械学習の非専門家が自ら学んで実装できるようになるというのはテクノロジーの「民主化」をあらためて実感できました[^Democratization]。現時点での実装はまだまだ不完全だと思いますが、今回の課題は国内外のファブ施設で共通だと思いますので、近日中にGitHubでも公開し、さらなる改良を継続できるようにしていきたいと思います。

[^Democratization]: この視点から[「ディープラーニング×きゅうり」の可能性に、たったひとりで取り組むエンジニア](https://geek-out.jp/column/entry/2017/11/23/110000)、[TensorFlowを使った機械学習を論文抽出に適用、ヒントは大学入試問題対策の裏ワザ：ディープラーニングでテキストマイニング](http://www.atmarkit.co.jp/ait/articles/1712/21/news028.html)などは注目すべき事例だと思います。

[^Udemy]: 今回直接参考になったのはUdemyで開講されている「[【キカガク流】人工知能・機械学習 脱ブラックボックス講座 - 初級編 -](https://www.udemy.com/kikagaku_blackbox_1/)」と「[Zero to Deep Learning™ with Python and Keras](https://www.udemy.com/zero-to-deep-learning/learn/)」でした。

# リファレンス

- [Chainer v3 ビギナー向けチュートリアル](https://qiita.com/mitmul/items/1e35fba085eb07a92560)
- [Raspbian jessieでSystemdを使った自動起動](https://qiita.com/yosi-q/items/55d6d3d6834c778ae2ea)
- Google. "Voice Kit." Voice. Accessed December 18, 2017. https://aiyprojects.withgoogle.com/voice.