LEGO Mindstorms×AI　機械学習その3　分類編

本記事の内容はベータ公開としているので内容が変更される場合があります。

---

本記事では、教育版 [LEGO® MINDSTORMS EV3](https://afrel.co.jp/product/ev3-introduction)(以降EV3)とPython環境を利用して色の識別を分類を用いて行っていく。  
環境構築に関しては環境構築編の記事を参照してほしい。
また、前回の記事ではライントレースと線形回帰を合わせた内容を行っている。  

EV3で機械学習その1　環境構築編：[こちら](https://qiita.com/Hiroki-Fujimoto/items/6ce278411ca151fee750)
EV3で機械学習その2　線形回帰編：[こちら](https://qiita.com/Hiroki-Fujimoto/items/6dae8c407e56a38625cf)
EV3で機械学習その3　分類編：本記事  

## 参考
本記事の内容は以下の書籍を参考にしている。
Pythonを使ったEV3の基本的な制御等については以下に網羅されている。

[ロボットではじめるAI入門](https://ai.afrel.co.jp/pythonai/)
上記書籍との内容の関係は以下の通りになる。

~~~
・概要
・PC,EV3のセットアップ　⬅　NEW(初回)
・プログラム作成方法　⬅　NEW(初回)
・モーター,センサーの利用
・ライントレース制御
・ライントレースフィードバック
・線形回帰　⬅　NEW(前回)
・分類　⬅　NEW(本記事)
~~~
## 本記事内での環境

* PC
Windows10
Python 3.7.3
開発環境 VisualStudioCode(以降VSCode)

* EV3
python-ev3dev2(以降ev3dev)

## 目次  
1. 分類手法
2. やること
3. EV3のモデルと読み取る色
4. プログラムの作成
5. プログラムの実行
6. 実行結果
7. まとめ

## 分類手法
### SVM(サポートベクターマシン)  
データの散布があったとき、それらを分けるための境界線を引くための手法。

境界線を引く＝分類の予測をする　ということになるが、予測には過去のデータを利用する。
その際に外れ値のような余計なデータまで使ってしまうと精度が下がる可能性がある。
そこでSVMは本当に予測に必要となる一部のデータのみ利用する。予測に必要となるデータのことをサポートベクトルと呼び、サポートベクトルを用いた機械学習法がサポートベクターマシンとなる。  
<img width="400" alt="Figure_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/1d055bc3-7a25-6dcd-a975-19454cbce50b.png">



## やること
今回はカラーセンサーでRGB(赤緑青の度合い)と何色かのラベル番号をセットで採取する。EV3のカラーセンサーはRGB値は0~255の数値で取得できる。採取したデータ群をもとにそのデータが何色なのか境界を定めさせデータをもとに推論を行う。

以下は今回採取するデータの例
<img width="500" alt="20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/56acb100-c3c7-27f4-dead-1e63da9b8f99.png">


## EV3のモデルと読み取る色
前回使用したコースの端にあるカラーを読み取っていく。プログラム内でラベルの定義を変更することで、ある程度の色を学習データとして利用することは可能なので好きな色を分類させることができる。  
<img width="250" alt="19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/a476282c-93e6-fa0e-99e3-595a5a98bc49.png">　　　　　　<img width="230" alt="0930.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/a9c8375d-7a90-ffcb-b9f5-fc370660d10e.jpeg">

また今回も前回と同様のEV3のモデル「ベースロボ」を利用する。今回はモーターを動かすわけではないのでインテリジェントブロックとカラーセンサーさえあれば問題ないが、カラーセンサーは読み取る面から0.5cm~1cm程度の隙間が空いている必要があるため安定してカラーセンサーを固定できるモデルを利用する。
<img width="200" alt="0890.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/053cd6c1-31e7-da40-1117-baa788f5fe6d.jpeg">　　　　　<img width="200" alt="0891.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/7b2cff80-17ce-1aab-f528-a2e1c02804fa.jpeg">

## プログラムの作成  
今回は以下の2つのプログラムを作成する。

* EV3側プログラム`data_get_color.py`
* PC側プログラム`Classification.py`  

前回同様データの処理や推論自体はPC側プログラムで行い、EV3側ではカラーのRGB値の取得や送信を行う。
それぞれのプログラムの関係性を以下の図に示す。
<img width="700" alt="18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/b015a245-696c-f173-ab07-e1878dde2229.png">

### EV3側プログラム
EV3側プログラム`data_get_color.py`はVSCode上のワークスペースで作成する。ワークスペースの作成やEV3への転送方法については以下の記事を参照してほしい。  
[EV3×Pyrhon　機械学習その1　環境構築編](https://qiita.com/Hiroki-Fujimoto/items/6ce278411ca151fee750)

~~~ data_get_color.py
import time
import socket
import sys
from ev3dev2.button import Button
from ev3dev2.sensor import INPUT_3
from ev3dev2.sensor.lego import ColorSensor


def dataget(color_dic, color_num):
    _rgb_data = color.raw
    _rgb_data = list(_rgb_data)
    _rgb_data.append(color_num)
    _rgb_data_str = ','.join(map(str, _rgb_data))
    s.send(_rgb_data_str.encode())
    print('\n'+'rgb_color = {}'.format(_rgb_data_str))


def predict():
    _rgb_data = color.raw
    _rgb_data = list(_rgb_data)
    _rgb_data_str = ','.join(map(str, _rgb_data))
    s.send(_rgb_data_str.encode())
    pre_color = s.recv(1024).decode()
    print('\n'+'predict_color = {}'.format(color_dic[int(pre_color[0])]))


# sensors&motors definition
button = Button()
color = ColorSensor(INPUT_3)

# gyro initialize
color.mode = 'RGB-RAW'

# variable initialize
color_dic = {
            1: 'RED',
            2: 'GREEN',
            3: 'BLUE'
            }

color_num = 1
color_max = len(color_dic)

# get gyrodate and into array
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(('169.254.85.105', 50010))    # your PC's Bluetooth IP & PORTpy
    for cnt in range(color_max):
        s.send((color_dic[cnt+1]).encode())
        time.sleep(0.1)
    s.send(('END').encode())
    print('Start program...')
    while not(button.backspace):
        if button.up:
            color_num += 1
            time.sleep(0.1)
            if color_num > color_max:
                color_num = color_max
        elif button.down:
            color_num -= 1
            time.sleep(0.1)
            if color_num < 1:
                color_num = 1
        elif button.right:
            msg = 'save'
            s.send(msg.encode())
            dataget(color_dic, color_num)
        elif button.left:
            msg = 'predict'
            s.send(msg.encode())
            predict()
        print('\r'+'save_color = {}   '.format(color_dic[color_num]), end='')
        time.sleep(0.1)

print('\n'+'End program')
sys.exit()
~~~

※EV3側環境は日本語でコメントアウトすると文字コードの性質上エラーが発生するので注意が必要。

後半に記述しているs.connect(('169.254.207.161', 50010))は、前回と同様に環境に応じて書き換える。  

以下のRED、GREEN、BLUEの箇所を変更することでデータとして記録する際のラベル名を変更できる。
今回は赤、緑、青を読み取るので以下のままにする。

~~~ data_get_color.py
# variable initialize
color_dic = {
            1: 'RED',
            2: 'GREEN',
            3: 'BLUE'
            }
~~~  


### PC側プログラム
PC側プログラムではEV3から送られてくるカラーの値とラベルをセットにCSVファイルに記録するのと、EV3側から推論のメッセージが送られてきたらデータ群を元に現在見ている色が何色か推論を行い結果を表示する。

前回同様`program`フォルダに`Classification.py`をテキストドキュメントとして作成し、以下の内容を記述する。  

~~~ Classification.py
import socket
import sys
import csv
import numpy as np
import pandas as pd
import os.path
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.multiclass import OneVsRestClassifier
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.metrics import accuracy_score


# setting svm
C = 1.
kernel = 'rbf'
gamma = 0.01
estimator = SVC(C=C, kernel=kernel, gamma=gamma)
clf = OneVsRestClassifier(estimator)

x_data = np.zeros(0)
y_data = np.zeros(0)

color_elements = None

color_dic = {}
color_cnt = 1

# データファイルの作成
if os.path.exists('color_data.csv') == False:
    writedata = ['red', 'green', 'blue', 'color']
    f = open('color_data.csv', 'w', newline='')  # ファイルを開く
    writer = csv.writer(f)
    writer.writerow(writedata)          # データの書き込み
    f.close()
data = pd.read_csv("color_data.csv", sep=",")  # csvファイルの読み込み

# 読み込んだデータを入力データとラベルに分ける
x_data = data.loc[:, ["red", "green", "blue"]]
y_data = data.loc[:, "color"]

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(('169.254.85.105', 50010))  # your PC's Bluetooth IP & PORT
    s.listen(1)
    print('Start program...')
    while True:
        conn, addr = s.accept()
        with conn:
            # EV3側のプログラムで作成した色の辞書と同じものを作成
            while True:
                color_elements = conn.recv(1024).decode()
                if color_elements == 'END':
                    break
                color_dic[color_cnt] = color_elements
                color_cnt += 1
            print('color_dic = {}'.format(color_dic))

            # メッセージによって動作を変化
            while True:
                rgb_data = conn.recv(1024).decode()
                if not rgb_data:
                    break
                # 溜めたデータを用いて、推論後、値をEV3に送信
                elif rgb_data == 'predict':
                    train_x, test_x, train_y, test_y = train_test_split(x_data,
                                                                        y_data)
                    clf.fit(train_x, train_y)
                    y_pred = clf.predict(test_x)
                    acc_score = accuracy_score(test_y, y_pred)
                    print('acc_score = {}'.format(acc_score))
                    rgb_data = conn.recv(1024).decode()
                    rgb_data = rgb_data.split(',')
                    pre_color = clf.predict([rgb_data])
                    print('predict_color = {}'.format(color_dic[pre_color[0]]))
                    conn.send(str(pre_color[0]).encode())
                # EV3から送られてきたデータを保存
                elif rgb_data == 'save':
                    rgb_data = conn.recv(1024).decode()
                    rgb_data = rgb_data.split(',')
                    print('rgb_data = {}'.format(rgb_data))
                    np.append(y_data, rgb_data[0:2])
                    np.append(y_data, int(rgb_data[3]))
                    writedata = rgb_data
                    f = open('color_data.csv', 'a', newline='')
                    writer = csv.writer(f)
                    writer.writerow(writedata)
                    f.close()
            print('End program')
            sys.exit()
~~~  
今回は3色の分類を行うため`clf = OneVsRestClassifier(estimator)`で多クラス分類を行うモデルを利用する。



後半に記述している`s.bind(('169.254.207.161', 50010))`はEV3側プログラム同様環境に合わせて変更する。
環境の確認と変更方法は[前回の記事](https://qiita.com/Hiroki-Fujimoto/items/6dae8c407e56a38625cf#%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E4%BD%9C%E6%88%90)もしくは以下を確認してほしい。  
[ソケット通信のIP設定](https://qiita.com/Hiroki-Fujimoto/items/6dae8c407e56a38625cf#%E3%82%BD%E3%82%B1%E3%83%83%E3%83%88%E9%80%9A%E4%BF%A1%E3%81%AEip%E8%A8%AD%E5%AE%9A)
## プログラムの実行  
2つのプログラムを作成できたらそれぞれ実行していく。

1. コマンドプロンプトから`cd Desktop\program`を実行する(\は¥マークと同義)
<img width="600" alt="10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/ea1cb72c-6821-9ac7-f7f6-28ec47bec07e.png">

2. 続けてコマンドプロンプトにて`python Classification.py`を実行する  
※実行後Start program...と表示され待機状態になる
<img width="700" alt="11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/3b580d95-b9f3-b9ea-78a7-05344772bcbd.png">

3. VSCode上で接続しているEV3のSSHターミナルを開き`cd ev3workspace/`を実行する
4. SSHターミナルにて`python3 data_get_gyro.py`を実行する
<img width="600" alt="12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/18a2a369-2c43-c593-fccb-85e8c5954208.png">

5. コースに付属している色の上にEV3を設置し、カラーセンサーでRGB値を取得しPCに送信する。EV3の各ボタンに操作が設定されているため応じてボタンを押下してデータを採取する。  
<img width="300" alt="0933.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/a9311cd3-b4c4-5756-6d84-3c386461352d.jpeg">　　<img width="160" alt="17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/42137274-d60e-8655-4311-4dd35cdafec1.png">

  - 上：保存する色（ラベル）を切り替える
  - 下：保存する色（ラベル）を切り替える
  - 右：ボタンが押されたら、その時のカラーセンサーの値とラベルをPCに送信する(データ採取)
  - 左：ボタンがおされたら、その時のカラーセンサーの値をPCに送信する(推論)

上記のように操作が設定されているので、採取したい色に上下ボタンでラベルを合わせて、右ボタンを押してデータを採取する。  
上下ボタンを押すとVSCode上で採取するラベルの名前が切り替わる。  
<img width="500" alt="12-1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/b8e10aaf-cd7d-0e35-9383-c1cc53e89caa.png">
<img width="500" alt="12-2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/d8187608-1626-f871-8994-89cd0152d41a.png">

右ボタンでカラーのデータを採取するとVSCode上とコマンドプロンプト上に以下のようにRGB値とラベルが出力され、、CSVファイルに保存される。
<img width="500" alt="12-3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/d9fe1f17-1eba-f314-a69f-04221f4e2def.png">
<img width="500" alt="12-4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/bbe380c5-df23-d415-b67f-b821b4ed7737.png">

同じように各ラベルである程度データを集める。  
<img width="500" alt="12-5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/205f7921-ff06-77b7-c68a-e97af98ee156.png">



## 実行結果  
ある程度データを採取できたらEV3の左上のボタンを押して一度プログラムを終了する。
<img width="160" alt="17-1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/91530d17-9fe2-04a4-013c-fc0923593a21.png">

CSVが作成されているのを確認する。
<img width="400" alt="12-6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/a5834bda-ebd6-5a96-47ff-861092977ddc.png">

再度同じ手順でPC側、EV3側のプログラムをそれぞれ実行し、
それぞれ採取した色の上で左ボタンを押して推論結果をPC側で確認する。  
<img width="160" alt="17-3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/e6478754-1ba5-48a3-0de7-c0ade998a253.png">　　　　<img width="500" alt="12-7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/274fe0a0-ee2c-50f7-aa96-7e327a69bb7f.png">

それぞれの色が判別できているのが確認できる。
また実際のカラーデータで判別する前に採取したデータ群の一部をテストデータとして用いて
正確率を表示している。

## まとめ  
実際にロボットから受け取ったデータで分類を行うことができた。今回はSVMの手法を用いたが、Scikit-learnのモデル指定の記述を変更することでランダムフォレストなど他の分類手法で機械学習を実装することも可能だ。
昨今の機械学習ではPC内で簡潔してしまうものが多いが、今回のようにエッジ(ロボット側)で実行結果を確認できる点や自分で採取したデータを利用する点においては身近に感じることができるのではないだろうか。  


## Appendix  
### 3次元データのプロット  
今回取得したRGBのような3次元のデータを以下のようなプログラムでプロットする。
プロットには前回の記事でインストールしたライブラリ`matpolotlib`必要なので注意

color_data.csvがあるフォルダ下に`3d_graph.py`を作成する。

~~~ 3d_graph.py
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
from sklearn.svm import SVC
from sklearn.metrics import r2_score
import math
import itertools
from sklearn.multiclass import OneVsRestClassifier
from sklearn.metrics import accuracy_score

file = 'color_data.csv'

def main():
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')

    data = np.loadtxt(file,delimiter=",",skiprows=1)
    print('data = {}'.format(data))
    print('data max= {}'.format(np.amax(data,axis=0)))
    print('data min= {}'.format(np.amin(data,axis=0)))

    for xs, xy, zs, c in data:
        #print('xs = {}, xy = {}, zs = {}, c = {}'.format(xs, xy, zs, c))
        if c == 1:
            color = "r"
            marker="o"
        elif c == 2:
            color = "g"
            marker="^"
        elif c == 3:
            color = "b"
            marker=","
        ax.scatter(xs, xy, zs, c=color, marker=marker)

    ax.set_xlabel('red')
    ax.set_ylabel('green')
    ax.set_zlabel('blue')
    plt.show()


if __name__ == '__main__':
    main()
~~~

コマンドプロンプトより`cd コマンド`で作成したフォルダに移動する。 
`python 3d_graph.py`で実行するとcsvファイルを読み込み以下のように3次元のグラフをプロットすることができ、データの分布を確認することができる。
<img width="500" alt="22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/07838adc-45e6-756f-e247-370e0a982aa6.png">