Scratchできかいがくしゅう

# TL;DR

ScratchのPython拡張を使ってKerasを呼び出すことにより、Scratchを機械学習のクライアントとして使ってみました。
適当な文章なので詳細な内容はよくわからないかもしれませんが、**画像を見れば何となく変なことをしているのがわかると思います**。

## 背景

先日アプリコンテストに参加して、小学生がScratchを使って立派なアプリケーションを作成しているのを見て感化されました。そこで、小学生に負けないように、大人の威厳を見せつけてやろうというのが、この記事のモチベーションです（大人げない）。


# 環境構築

## Scratchのインストール

以下のリンクから必要なものをダウンロードしてオフラインエディタでScratchが動作するようにします
[Scratch 2.0 Offline Editor](https://scratch.mit.edu/download)

## Pythonのインストール

Python3をインストールします。( [Windows](https://www.python.jp/install/windows/install_py3.html) | [Mac](https://www.python.jp/install/macos/index.html) )

## ライブラリ群のインストール

pipを使って機械学習フレームワークのKerasをインストールします。
pipenvが使える場合は[ML-on-scratch](https://github.com/yoidea/ML-on-scratch)をcloneして`pipenv install`すると環境構築が完了します。

```shell
$ pip install tensorflow
$ pip install keras
```

Scratch用のPython拡張を作成するためのライブラリをインストールします。

```shell
$ pip install blockext
```

# ScratchのPython拡張を作成

## BlockExtの使い方

まずは、簡単なコードを書いて拡張機能が正しく動作することを確認してみます。
blockextから`run`, `reporter`, `command`をインポートして、デコレータ記法で使います。
`@command`はScratchの命令ブロックを作ることができます。
`@reporter`はScratchのデータブロックを作ることができます。
`run()`で最後の引数にポート番号を指定して実行するとサーバが立ち上がります。

```python:extension_test.py
from blockext import run, reporter, command


message = ""


@command("set message %s")
def set_message(m):
    global message
    message = m


@reporter("get message")
def get_message():
    return message


if __name__ == "__main__":
    run("Extension Test", "extension_test", 5678)
```

プログラムを実行するとブラウザなどからアクセスしてみて確認することができます。

```shell
$ python extension_test.py
Listening on 5678
```

<img width="303" alt="スクリーンショット 2018-12-17 14.37.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/65493/23b016da-7771-0a16-9069-71996492e282.png">

Download Scratch 2.0 extensionをクリックして`s2e`ファイルを保存します。

<img width="554" alt="スクリーンショット 2018-12-17 14.17.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/65493/d1faaa5c-21e6-c280-308c-b90f79e73156.png">

このあともサーバが必要なので、ダウンロードが完了してもプログラムを終了してはいけません。

## Scratchから読み込み

Scratchから`s2e`ファイルを読み込んでPython拡張を有効化します。
Shiftを押しながらファイルを開くと`実験的なHTTP拡張を読み込み`という項目が現れます。そこから保存した`s2e`を選択して読み込みます。

<img width="331" alt="スクリーンショット 2018-12-17 14.21.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/65493/e7473306-bfe2-fbda-65be-3a45c13c74e6.png">

すると、`その他`に自作の関数が現れます。

<img width="230" alt="スクリーンショット 2018-12-17 14.29.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/65493/9c8832c5-2838-b67d-38c4-38b88df4ed66.png">

`set message`で適当なテキストを設定して、`get message`で取得して出力するようにScratchのブロックを組んでみると、以下のように動作します。

<img width="763" alt="スクリーンショット 2018-12-17 14.49.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/65493/ba069393-18d7-6378-1ddd-8b2413e8062e.png">

# あとは機械学習するだけ！

あとは内容を機械学習に変えるだけで、Scratchから機械学習ができるようになります。

## XOR

機械学習の入門としてXORを解くプログラムを作ってみます。

|$x_{1}$|$x_{2}$|$y$|
|---|---|---|
|0|0|0|
|1|0|1|
|0|1|1|
|1|1|0|

```python:xor.py
from blockext import run, reporter, command, predicate
from keras.models import Sequential
from keras.layers import Dense, Activation
import numpy as np


class Model:
    def __init__(self):
        self.x = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
        self.y = np.array([0, 1, 1, 0])
        self.is_busy = False
        self.score = [0, 0]


    def build(self):
        self.model = Sequential()
        self.model.add(Dense(2, input_dim=2))
        self.model.add(Activation('sigmoid'))
        self.model.add(Dense(1))
        self.model.add(Activation('sigmoid'))


    def train(self):
        self.model.compile(loss='mse', optimizer='adam', metrics=['accuracy'])
        self.model.fit(self.x, self.y, epochs=10000)
        self.score = self.model.evaluate(self.x, self.y)


@command("start training")
def train():
    model.is_busy = True
    model.build()
    model.train()
    model.is_busy = False


@predicate("is busy")
def is_busy():
    return model.is_busy


@reporter("get score")
def get_score():
    return "%d" % (model.score[1] * 100)


if __name__ == "__main__":
    model = Model()
    run("XOR", "xor", 5678)
```

```shell
$ python xor.py
Using TensorFlow backend.
Listening on 5678
```

## Scratch側の処理

先程と同じように`s2e`ファイルを生成してScratchから読み込みます。

<img width="497" alt="スクリーンショット 2018-12-17 16.48.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/65493/6abc1f94-6188-195f-eb0b-bbf47024eb84.png">

こんな感じでブロックを並べてやると、旗をクリックするとXORの学習が始まって、完了すると猫が精度を教えてくれます。

<img width="485" alt="スクリーンショット 2018-12-17 16.49.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/65493/6c43b506-be54-176c-24b4-fa4472846196.png">

## 任意の入力で試せるように

せっかくなので、任意の入力を受け取ってそれに対する予測値を返せるようにしてみます。
予測を返す関数`predict()`と入力値をセットする関数`set_x1()`, `set_x2()`を追加しました。

```python:xor.py
from blockext import run, reporter, command, predicate
from keras.models import Sequential
from keras.layers import Dense, Activation
import numpy as np


class Model:
    def __init__(self):
        self.x = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
        self.y = np.array([0, 1, 1, 0])
        self.test = [0, 0]
        self.is_busy = False
        self.score = [0, 0]


    def build(self):
        self.model = Sequential()
        self.model.add(Dense(2, input_dim=2))
        self.model.add(Activation('sigmoid'))
        self.model.add(Dense(1))
        self.model.add(Activation('sigmoid'))


    def train(self):
        self.model.compile(loss='mse', optimizer='adam', metrics=['accuracy'])
        self.model.fit(self.x, self.y, epochs=10000)
        self.score = self.model.evaluate(self.x, self.y)


    def predict(self):
        if hasattr(self, 'model') and not self.is_busy:
            x = np.array([self.test])
            y = self.model.predict(x)
            return y[0][0]
        return "None"


@command("start training")
def train():
    model.is_busy = True
    model.build()
    model.train()
    model.is_busy = False
    print(model.predict())


@command("set x1 %n")
def set_x1(n):
    model.test[0] = n


@command("set x2 %n")
def set_x2(n):
    model.test[1] = n


@predicate("is busy")
def is_busy():
    return model.is_busy


@reporter("get score")
def get_score():
    return "%d" % (model.score[1] * 100)


@reporter("predict")
def predict():
    return model.predict()


if __name__ == "__main__":
    model = Model()
    run("XOR", "xor", 5678)
```

実行して、

```shell
$ python xor.py
Using TensorFlow backend.
Listening on 5678
```

Scratch側で以下のようにブロックを並べると、入力$x_{1}, x_{2}$に応じて$y$の値が出力できます。

<img width="960" alt="スクリーンショット 2018-12-17 18.14.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/65493/5410a96e-2103-3486-2b2c-2fa3df8551b4.png">

これで、XORを解く問題をScratchで実装することができました。本当はトレーニングデータも自由に入力できるようにしてANDやORも試せるようにしたかったのですが、時間的な問題からここまでで力尽きました。
とりあえず、Scratchを機械学習のクライアントに使えるということは証明できたのではないでしょうか。