mac+eGPU で機械学習の環境構築からVison+CoreMLでの画像認識までの一通りの動作確認

　macに機械学習の環境をつくり、画像分類を学習させたモデルの作成とVison+CoreMLで画像を分類をするまでを試したのでそのメモ。

## この記事で参考になるかもしれない情報

　この記事は初心者が初めて機械学習をやってみた、という内容なので参考になるところはほとんどないと思いますが！

- tensorflow-macos を使えばintel+eGPU環境でもGPUを使ってトレーニング時間は短縮できるっぽい。

|eGPU<br>(Radeon RX 580)<br>接続|mlcompute<br>.set_mlc_device(device_name="gpu")|トレーニング<br>時間|
|:---:|:---:|:---:|
|✔️||41.2s|
|||231.5s|
||✔️|606.9s|
- coremltools 4.0 は tensorflow-macos 環境(pre-release tensorflow_macos 0.1alpha1)ではエラーになる。[coremltoolsのバグらしい。修正はされているがリリースはされていない](https://github.com/apple/coremltools/pull/1002)。いたしかたなく tensorflow-macos を使わない環境を作って回避した。

```
ERROR:root:Constant Propagation pass failed: invalid version number '2.4.0-rc0'
```

## 動作確認環境

- macOS Big Sur 11.1
- Mac mini(2018) 3.2GHz 6コア Intel Core i7 メモリ 16GB
- 内蔵GPU Intel UHD Graphics 630
- eGPU Radeon RX 580 8GB (+ Sonnet eGFX Breakaway Box 550)

## 動作確認の流れ

- 1. トレーニング環境構築
- 2. 学習用画像の準備
- 3. 学習用と検証用のデータの作成
- 4. トレーニング
- 5. mlmodel に変換
- 6. Vison+CoreMLで推論

## 1. トレーニング環境構築
### 1-1) tensorflow-macos インストール

　手順は参考にさせていただいたこちらの記事 [【Apple Silicon M1 でtensorflow-macosを実行したらめちゃくちゃ速かった。】](https://qiita.com/tomoyaeibu/items/46f2f3384a370df71d5e) と同じ。

```
Keras@mini2018 ~ % /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/apple/tensorflow_macos/master/scripts/download_and_install.sh)"

Installation script for pre-release tensorflow_macos 0.1alpha1.  Please visit https://github.com/apple/tensorflow_macos 
for instructions and license information.

This script will download tensorflow_macos 0.1alpha1 and needed binary dependencies, then install them into a new 
or existing Python 3.8 virtual enviornoment.
Continue [y/N]? y

(略)

./install_venv.sh will perform the following operations:
-> Create new python 3.8 virtual environment at /Users/Keras/tensorflow_macos_venv.
-> Install tensorflow_macos 0.1a1 into created virtual environment /Users/Keras/tensorflow_macos_venv.
-> Install bundled binary wheels for numpy-1.18.5, grpcio-1.33.2, h5py-2.10.0, scipy-1.5.4, tensorflow_addons-0.11.2+mlcompute into /Users/Keras/tensorflow_macos_venv.

Confirm [y/N]? y

(略)

TensorFlow and TensorFlow Addons with ML Compute for macOS 11.0 successfully installed.

To begin, activate the virtual environment:

   . "/Users/Keras/tensorflow_macos_venv/bin/activate" 

~
```


### 1-2) tensorflow-macosの仮想環境をアクティベート

```
Keras@mini2018 ~ % source /Users/Keras/tensorflow_macos_venv/bin/activate
(tensorflow_macos_venv) Keras@mini2018 ~ % 
```

### 1-3) pillow のインストール

引き続きコマンドラインから python で画像を扱うライブラリ `pillow` をインストールする。

```
(tensorflow_macos_venv) Keras@mini2018 ~ % pip install pillow
```

### 1-4) scikit-learn のインストール

Pythonの機械学習ライブラリの `scikit-learn` をインストールする。

```
(tensorflow_macos_venv) Keras@mini2018 ~ % pip install scikit-learn
```

### 1-5) keras のインストール

TensorFlowのラッパーライブラリ `keras` をインストールする。

```
(tensorflow_macos_venv) Keras@mini2018 ~ % pip install keras
```

#### 1-6) flickrapi のインストール

機械学習用の画像を入手するため、写真投稿コミュニティの[flickr](https://www.flickr.com) を利用する。Pythonから flickr API を利用するためのライブラリがあるので、これをインストールする。[提供元はこちらのサイト](https://stuvel.eu/software/flickrapi/)。

```
(tensorflow_macos_venv) Keras@mini2018 ~ % pip install flickrapi
```

## 2. 学習用画像の準備

機械学習に使う画像を 写真投稿コミュニティの[flickr](https://www.flickr.com) から入手する。

**画像取得までの流れ**

- flickr API key と secret を入手
- Python を使って画像を入手

#### 2-1) [flickr](https://www.flickr.com) にアクセスして Sign Up する
アカウントがなければここで作成する。
![flickr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/08d2e1e3-d141-934b-4ace-a1c3829425ad.png)

#### 2-2) App Garden にアクセスする
[The App Garden](https://www.flickr.com/services/api/)にアクセスすると、Flickrが提供するAPIの一覧を確認できる。
![appgarden.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/3570b5c9-4743-0ec3-0fc3-5660b78e106c.png)

ここから Flickr APIを利用するための Key と Secret を入手する。

#### 2-3) App Garden のページの左上の「Create an App」をクリック
![create_app.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/9392b2b5-ee84-ff64-38ce-dc00d1ebbe81.png)

#### 2-4) Get your API Key をクリック
![create_app_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/4bdd1a18-5097-5b6e-8eb4-68e1413a6324.png)

#### 2-5) 非商用か商用かを選択する。今回は非商用を選択

![create_app_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/49379466-4e11-ffa9-4819-285520f19711.png)

#### 2-6) アプリ名とアプリの目的を入力し「SUBMIT」をクリック。
![appname.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/cca1722d-c8ab-b224-1429-721a0f597ac4.png)

#### 2-7) KeyとSecretが表示されるのでメモる
![key_and_secret 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/9ecb5723-9941-0383-4619-ce4fc37b35bf.png)

#### 2-8) 欲しい画像のキーワードを引数にとって画像をダウンロードするプログラムを準備

```python:downloader.py
import urllib.request
import flickrapi
import time, os, sys

# ダウンロードしたい画像のキーワード
keyword = sys.argv[1]
# flickr アクセス用
api_key = "★★★入手した key をここに指定★★★"
api_secret = "★★★入手した secret をここに指定★★★"

# ダウンロードした画像の格納フォルダを作成
folder = keyword
if not os.path.exists(folder):
    os.mkdir(folder)
    
# Flickr API ライブラリ インスタンス化
flickr = flickrapi.FlickrAPI(api_key, api_secret, format='parsed-json')
# flickrからキーワードで写真一覧を取得
response = flickr.photos.search(
    text = keyword,     # 取得する写真のキーワード
    media = 'photos',   # 写真のみ
    safe_search = 1,    # 不適切なコンテンツを除くための指定。1が一番安全。
    per_page = 500,     # 欲しい画像数を指定(１ページあたり最大500枚まで)
    extras = 'url_q'    # 150x150の画像ファイルURLをresponseに含める指定。
)

photos = response['photos']
print('response pages:' + str(photos['pages']) + ' perpage:' + str(photos['perpage']))

for photo in photos['photo']:
    image_url = photo['url_q']
    print(image_url)
    filename = image_url.rsplit('/', 1)[1]
    filepath = folder + '/' + filename
    # URLからファイルをダウンロード
    urllib.request.urlretrieve(image_url, filepath)
    # スパム判定されないようにウェイトを入れておく
    time.sleep(0.5)

```

参考：

- [flickr.photos.search APIドキュメント](https://www.flickr.com/services/api/flickr.photos.search.html)
- [Python Flickr API ライブラリドキュメント](https://stuvel.eu/flickrapi-doc/2-calling.html?highlight=flickrapi)
- [flickr APIの使い方まとめ(サンプルコード付き)](https://syncer.jp/flickr-api-matome)

#### 2-9) 画像をダウンロードする

入手したい画像のキーワードを指定して画像をダウンロードする。

```
(tensorflow_macos_venv) Keras@mini2018 BotanicalPB % python downloader.py lilium
response pages:117 perpage:500
https://live.staticflickr.com/65535/50863926903_730d70a26d_q.jpg
https://live.staticflickr.com/1914/45868122311_b85a29090c_q.jpg
...
```

#### 2-10) 不適切な画像を削除する。
ダウンロードした画像ファイルの中には、想定した画像とは異なるものが含まれるので、これを手作業で削除する。

## 3. 学習用と検証用のデータの作成

入手した画像を学習用と検証用のデータに加工する。

#### 3-1) Numpy形式に加工するプログラムを準備

画像を読み込み、学習用と検証用のデータに分類し、それぞれnumpy形式に変換後、ファイルに保存するプログラムを準備。
**※この記事ではマーガレット、スターチス、ゆりの花の画像の分類を行うため、それぞれの画像を240枚準備済みの前提。**

```python:data_generator.py
from PIL import Image
from sklearn import model_selection
import os
import glob
import numpy as np

# マーガレット、スターチス、ゆり の画像分類
classes = ["marguerite", "statice", "lilium"]
# 全画像ファイル数
file_count = 240
# 検証用ファイル数
test_file_count = 40
# 50x50pxの画像で学習させる
image_size = 50
# 学習用のデータと検証用のデータの器
X_train = []    # 学習用画像
X_test = []     # 学習用のラベル
Y_train = []    # 検証用画像
Y_test = []     # 検証用のラベル

for class_index, class_label in enumerate(classes):
    # 分類ごとの画像ファイルを取得
    files = glob.glob("./" + class_label + "/*.jpg")
    files = files[:file_count]
    print("Label: " + class_label + " File Count: " + str(len(files)))
    
    for file_index, file in enumerate(files):
        # ファイル読み込み
        image = Image.open(file)
        # RGB（8bit x 3）に変換
        image = image.convert("RGB")
        # リサイズ
        image = image.resize((image_size, image_size))
        # 配列として扱えるようにする
        data = np.asarray(image)
        # 学習用と検証用に画像を振り分ける
        if file_index < test_file_count:
            X_test.append(data)
            Y_test.append(class_index)
        else:
            X_train.append(data)
            Y_train.append(class_index)

# 画像データをnumpy形式で保存する
X_train = np.array(X_train)
X_test = np.array(X_test)
y_train = np.array(Y_train)
y_test = np.array(Y_test)
# タプルで保存
xy = (X_train, X_test, y_train, y_test)
np.save("./botanical.npy", xy)
```

#### 3-2) Numpy形式に加工する

プログラムを実行する。

```
(tensorflow_macos_venv) Keras@mini2018 BotanicalPB % python data_generator.py 
Label: marguerite File Count: 240
Label: statice File Count: 240
Label: lilium File Count: 240
```

## 4. トレーニング

#### 4-1) トレーニングプログラムの準備

画像分類の学習済みモデルを作成するため、KerasのCIFAR-10のコードを流用したプログラムを準備する。

※この記事を書いている途中(2021/1月中旬くらい)にKerasのGithubから CIFAR-10のコードが削除(移動？)されてしまった模様。

```python:botanical_cnn.py
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras.utils import np_utils
import keras, time
import numpy as np

classes = ["marguerite", "statice", "lilium"]

def main():
    x_train, x_test, y_train, y_test = np.load("./botanical.npy", allow_pickle=True)
    # データの正規化。0〜255 -> 0〜1に変換
    x_train = x_train.astype("float") / 256
    x_test = x_test.astype("float") / 256
    # one-hotベクトルを作る
    y_train = np_utils.to_categorical(y_train, len(classes))
    y_test = np_utils.to_categorical(y_test, len(classes))
    # モデルを作る
    model = generate_model()
    model.summary()
    # トレーニング
    start = time.perf_counter()
    model.fit(x_train, y_train, batch_size=32, epochs=100)
    end = time.perf_counter()
    print("Elapsed time : {0} s.".format(end-start))
    # モデルの精度を測る
    test_loss, test_acc = model.evaluate(x_test, y_test, verbose=1)
    print('test_loss: ', test_loss)
    print('test_acc: ', test_acc)
    # モデルの保存
    model.save('./botanical_cnn.h5')

# モデルを作る(CIFAR-10)
def generate_model():

    model = Sequential()
    model.add(Conv2D(32, (3, 3), padding='same', input_shape=(50,50,3)))
    model.add(Activation('relu'))
    model.add(Conv2D(32, (3, 3)))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))

    model.add(Conv2D(64, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(64, (3, 3)))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))
    
    model.add(Flatten())
    model.add(Dense(512))
    model.add(Activation('relu'))
    model.add(Dropout(0.5))
    # 出力は"marguerite", "statice", "lilium"　の３つ
    model.add(Dense(3))
    model.add(Activation('softmax'))

    opt = keras.optimizers.RMSprop(lr=0.0001, decay=1e-6)
    model.compile(loss='categorical_crossentropy',
                  optimizer=opt,
                  metrics=['accuracy'])
    return model

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

#### 4-2) トレーニング実行

```
(tensorflow_macos_venv) Keras@mini2018 BotanicalPB % python botanical_cnn.py 
```

- **CPUでのトレーニング結果**

```
Elapsed time : 231.572187988 s.
4/4 [==============================] - 0s 12ms/step - loss: 2.6364 - accuracy: 0.7833
test_loss:  2.6363558769226074
test_acc:  0.7833333611488342
```
- **eGPU接続状態でのトレーニング結果**

```
Elapsed time : 41.286664788 s.
4/4 [==============================] - 0s 11ms/step - loss: 0.7714 - accuracy: 0.8750
test_loss:  0.7714245915412903
test_acc:  0.875
```
　下図はトレーニング時のGPU負荷の状況。eGPUに負荷がかかっていることがわかる。
![eGPU.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/1bd45202-1f8d-1b08-64e1-79f7328d09a3.png)

- **eGPU未接続・GPUトレーニング有効化**

　参考にさせていただいたこちらの記事 [【Apple Silicon M1 でtensorflow-macosを実行したらめちゃくちゃ速かった。】](https://qiita.com/tomoyaeibu/items/46f2f3384a370df71d5e) にあるようにtensorflowを動作させるデバイスを次のようにすることで選択できるので、`gpu` を指定して内蔵GPUでトレーニングさせる。

```:python
from tensorflow.python.compiler.mlcompute import mlcompute
mlcompute.set_mlc_device(device_name="gpu")
```

　結果はやたら遅い。

```
Elapsed time : 606.924531516 s.
4/4 [==============================] - 1s 113ms/step - loss: 0.7534 - accuracy: 0.8500
test_loss:  0.7534401416778564
test_acc:  0.8500000238418579
```

　内蔵GPUに負荷はかかっている。
![internal_gpu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/3bd3863e-bb3c-9727-d542-a10110c22047.png)
![internal_gpu_per.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/0101c222-7dca-34d4-ebe5-0e828f01a2fa.png)

# 5. mlmodel に変換

　学習済みのモデルを Vison+CoreML から利用できるように mlmodel 形式に変換する。
変換はAppleが提供している coremltools を利用する。

で、tensorflow-macos(0.1alpha1) ＋ coremltools4.0 環境では変換実行時にエラーになる。

```
(tensorflow_macos_venv) Keras@mini2018 BotanicalPB % pip list | grep coremltools
coremltools            4.0
```

```
(tensorflow_macos_venv) Keras@mini2018 BotanicalPB % python convert_h5.py 
WARNING:root:scikit-learn version 0.24.1 is not supported. Minimum required version: 0.17. Maximum required version: 0.19.2. Disabling scikit-learn conversion API.
WARNING:root:TensorFlow version 2.4.0-rc0 detected. Last version known to be fully compatible is 2.3.1 .
WARNING:root:Keras version 2.4.3 detected. Last version known to be fully compatible of Keras is 2.2.4 .
Running TensorFlow Graph Passes:   0%|                   | 0/5 [00:00<?, ? passes/s]ERROR:root:Constant Propagation pass failed: invalid version number '2.4.0-rc0'
Traceback (most recent call last):
  File "/Users/Keras/tensorflow_macos_venv/lib/python3.8/site-packages/coremltools/converters/mil/frontend/tensorflow/tf_graph_pass/constant_propagation.py", line 79, in _constant_propagation
    if tf.__version__ < _StrictVersion("1.13.1"):
  File "/usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/distutils/version.py", line 64, in __gt__
    c = self._cmp(other)
  File "/usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/distutils/version.py", line 168, in _cmp
    other = StrictVersion(other)
  File "/usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/distutils/version.py", line 40, in __init__
    self.parse(vstring)
  File "/usr/local/Cellar/python@3.8/3.8.5/Frameworks/Python.framework/Versions/3.8/lib/python3.8/distutils/version.py", line 137, in parse
    raise ValueError("invalid version number '%s'" % vstring)
ValueError: invalid version number '2.4.0-rc0'
```
　[この問題はすでにcoremltools側で修正されているようだが](https://github.com/apple/coremltools/pull/1002) 、リリースされていないので、ここでは、tensorflow-macos を使わない環境を別に用意する。

#### 5-1) coremltools利用環境を作る

 新たにPythonの仮想環境を作る。

```
Keras@mini2018 BotanicalPB % python3 -m venv env

Keras@mini2018 BotanicalPB % source env/bin/activate

(env) Keras@mini2018 BotanicalPB % pip install tensorflow

(env) Keras@mini2018 BotanicalPB % pip install keras 

(env) Keras@mini2018 BotanicalPB % pip install coremltools

(env) Keras@mini2018 BotanicalPB % pip install pillow
```

#### 5-2) 変換プログラムの準備

```python:convert_h5.py
import coremltools as ct
from PIL import Image

# ラベル定義
class_labels = ['marguerite', 'statice', 'lilium']
classifier_config = ct.ClassifierConfig(class_labels)

# mlmodelに変換
coreml_model = ct.converters.convert('botanical_cnn.h5',
    inputs=[ct.ImageType()],
    classifier_config=classifier_config,
    source='TensorFlow',)

# mlmodelを保存
coreml_model.save('./botanical_cnn.mlmodel')

# 推論して mlmodel に正しく変換できたか確認
# https://coremltools.readme.io/docs/model-prediction
test_image = Image.open("lily2.jpg").resize((50, 50))
data = {'conv2d_input': test_image}
out_dict = coreml_model.predict(data)
print(out_dict['classLabel'])
```

　`classifier_config=classifier_config` でラベルの配列を指定しないと、Vision+CoreML の実行時にエラーになるので重要。この部分、coremltools4.0での解決方法が見つからなくて[coremltoolsのドキュメント](https://coremltools.readme.io/docs/classifiers) を眺めていて解決。
　もう一点、推論に与える画像の指定で`'conv2d_input'` はmlmodel変換後にXcodeで確認できる（Xcodeで確認するなら、Preview タブで推論の動作確認できちゃうのでmlmodel変換時に推論確認する意義は小さいかも）。

![inputname.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/7ec35299-29e8-782c-11c5-54c555e58b74.png)

#### 5-3) 変換の実施

```
(env) Keras@mini2018 BotanicalPB % python convert_h5.py
WARNING:root:TensorFlow version 2.4.1 detected. Last version known to be fully compatible is 2.3.1 .
WARNING:root:Keras version 2.4.3 detected. Last version known to be fully compatible of Keras is 2.2.4 .
Running TensorFlow Graph Passes: 100%|███████| 5/5 [00:00<00:00, 16.11 passes/s]
Converting Frontend ==> MIL Ops: 100%|███████| 36/36 [00:00<00:00, 566.06 ops/s]
Running MIL optimization passes: 100%|████| 17/17 [00:00<00:00, 404.85 passes/s]
Translating MIL ==> MLModel Ops: 100%|███████| 53/53 [00:00<00:00, 113.73 ops/s]
marguerite
```

ワーニングが出まくっているが、一応変換はできている。
mlmodel変換後の推論もできている（マーガレットの画像を与えて分類できている）。

# 6. Vison+CoreMLで推論

変換したmlmodelを使って、任意の画像の推論をする。
Playgroundを使って試してみた結果は次の通り。

- mlmodelを読み込んで推論するコード

```swift
import UIKit
import CoreML
import Vision

guard let model = try? VNCoreMLModel(for: botanical_cnn(configuration: MLModelConfiguration()).model) else {
    fatalError()
}

let request = VNCoreMLRequest(model: model) { request, error in
    guard let results = request.results as? [VNClassificationObservation],
          let result = results.first else {
        fatalError()
    }
    
    print("Confidence: \(result.confidence * 100) % Label: \(result.identifier)")
}

guard let image = UIImage(named: "lily.jpg"),
      let cgimage = image.cgImage else { fatalError() }
let handler = VNImageRequestHandler(cgImage: cgimage, options: [:])

try? handler.perform([request])

```

- Playground の設定

変換した mlmodel と分類したい画像（ここでは「ゆり」）を組み込んでおく。
![playground.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/5a398978-c4b9-f377-f216-90bef0fdc191.png)

- 実行結果

![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71605/376cccf5-2db4-a57d-637d-53349cf8ec03.png)

画像分類ができた。


## 参考にさせていただいた記事
- [venv: Python 仮想環境管理](https://qiita.com/fiftystorm36/items/b2fd47cf32c7694adc2e)
- [Apple Silicon M1 でtensorflow-macosを実行したらめちゃくちゃ速かった。](https://qiita.com/tomoyaeibu/items/46f2f3384a370df71d5e)
- [coremltools 4.0でPyTorchモデルを変換する](https://qiita.com/shu223/items/6ddfbedb4fdfb2059a11)