Fast AIでドラえもんの画像認識

# Introduction
Fast AIの第２講のレッスンを受けたので、そこまでの知識をもって、ドラえもんのキャラクタ認識を実装してみます。

なおFast AIではTop-Downアプローチで、機械学習を学ぶことを推奨しています。
つまり理論を先に学ぶより、まずは機械学習を動かしてみる。
その体験をしてから、必要なものを学ぶスタイルです。

実践こそ一番学べると私自身共感しているので、この方針で学習を進めています。

# 前提
実行環境はColabで行っています。

画像のダウンロードに、BingSearchのv7を利用しています。
BingSearchは、Azureの無料アカウントが必要となります。
https://www.microsoft.com/en-us/bing/apis/bing-web-search-api

# 結果
ドラえもんの画像から、ドラえもんと予測できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97498/1f34419d-ed28-3dce-4320-878719cee73e.png)


```
learn_inf.predict(path/'doraemon.png')
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97498/b8a15494-0499-2858-0674-ddd7f9aef2b7.png)

# 実装
早速実装していきます。

## 初期設定
### マウント
Google DriveをColabにマウントします。

```
from google.colab import drive
drive.mount('/content/gdrive')
```

### インストール
fastbookモジュールをインストールします。
ただしColabにインストールしてしまうと、再度接続したときにモジュールが消えてしまいます。
そのため、マウントしたGoogle Drive上に、保存します。

```
!pip install --target "/content/gdrive/MyDrive/Colab Notebooks/modules" -Uqq fastbook
```

pythonでインポートする際、Google Driveからもモジュールを探し出せるように、パスに追加します。

```
import sys, os
nb_path = '/content/modules'
os.symlink('/content/gdrive/MyDrive/Colab Notebooks/modules', nb_path)
sys.path.insert(0, nb_path)
```

### インポート
必要なライブラリをインポートします。

```
import fastbook
fastbook.setup_book()
from fastbook import *
from fastai.vision.widgets import *
```

## 画像ダウンロード
BingSearchを利用して、画像をダウンロードします。

### Azureキーの設定
```
# azureのキー
key = os.environ.get('AZURE_SEARCH_KEY', '{{ Azureのキーを入れてください }}')
```

### 検索する関数
検索したい用語をパラメータとして、検索結果を返す関数を作成します。

```
def my_search_images_bing(key, search_term):
    search_url = "https://api.bing.microsoft.com/v7.0/images/search"
    headers = {"Ocp-Apim-Subscription-Key" : key}
    min_size = 128

    params  = {"q": search_term,
               "imageType": "photo",
               "minHeight": min_size,
               "minWidth": min_size,
               "count": 150}
    response = requests.get(search_url, headers=headers, params=params)
    response.raise_for_status()
    search_results = response.json()

    return L(search_results['value'])

search_images_bing = my_search_images_bing
```

### ダウンロード
検索する用語と、ダウンロード先の定義します。

```
image_types = ['doraemon','nobita', 'suneo', 'jaian']
path = Path('/content/gdrive/My Drive/Colab Notebooks/doraemon/images')
```

先ほど作成した、画像検索用の関数`my_search_images_bing`を利用して画像をダウンロードします。
またfastaiでは、URLから画像をダウンロードする関数`download_images`が用意されています。

```
if not path.exists():
    path.mkdir()
for o in image_types:
    dest = (path/o)
    dest.mkdir(exist_ok=True)
    results = search_images_bing(key, f'{o}')
    download_images(dest, urls=results.attrgot('contentUrl'))
```

ダウンロードした画像のパスを、取得します。
fastaiで`get_image_files`を利用することで、サブディレクトリを含め、画像ファイルのパスをすべて取得できます。

```
fns = get_image_files(path)
fns

=== 結果
[Path('/content/gdrive/My Drive/Colab Notebooks/doraemon/doraemon/00000000.jpg'),Path('/content/gdrive/My Drive/Colab Notebooks/doraemon/doraemon/00000009.jpg')....
===
```

### 破損ファイルの削除
残念ながらダウンロードした画像には、破損している画像がありえます。
fastaiで破損ファイルを特定し、削除する機能があります。


まずは破損ファイルを特定します。

```
failed = verify_images(fns)

=== 結果
[Path('/content/gdrive/My Drive/Colab Notebooks/doraemon/doraemon/00000012.jpg'),Path('/content/gdrive/My Drive/Colab Notebooks/doraemon/doraemon/00000052.jpg')....
===
```

特定したファイルを削除していきます。

```
failed.map(Path.unlink)
```


## 画像データからモデルを生成
画像データから、fastaiのモデルを作成します。

fastaiのモデルを作成するには、まずDataBlockクラスでモデルの作成方法を定義します。
次にDataBlockクラスを通して、実際にモデル作成するDataLoaderクラスを生成します。
最後DataLoaderクラスに、ファイルのパスを指定してあげて、モデル作成します。

### DataBlockの定義
DataBlockに、以下の情報を定義します。

1. どんな種類のデータを扱うか
2. どうやってリストを取得するのか
3. ラベリングのラベリングの方法
4. validationセットの作成方法

```
characterBlock = DataBlock(
    blocks=(ImageBlock, CategoryBlock), 
    get_items=get_image_files, 
    splitter=RandomSplitter(valid_pct=0.2, seed=42),
    get_y=parent_label)
```

細かく見ていきます。

独立変数（予測の元となる変数）と、従属変数（予測する変数）を定義します。
ここでは独立変数はイメージデータ、従属変数はカテゴリーデータになります。

```
blocks=(ImageBlock, CategoryBlock),
```

イメージファイルの取得方法を定義します。

```
get_items=get_image_files, 
```

ランダムにtrainingデータと、validationデータに分割します。
ここでは、２割をvalidationデータとします。
なお毎度実行するたびに、利用するvalidationデータが変わってしまうのを防ぐため、seedを指定しています。

```
splitter=RandomSplitter(valid_pct=0.2, seed=42),
```

どのようにラベルを決定づけるかを定義します。
`parent_label`は、fastaiで用意されている、親ディレクトリ名をカテゴリ名とする関数です。

```
get_y=parent_label,
```

### DataLoaderの生成
DataBlockからDataLoaderを生成します。

```
characters = characterBlock.new(
    item_tfms=RandomResizedCrop(224, min_scale=0.5),
    batch_tfms=aug_transforms())
dls = characters.dataloaders(path)
```

ここでも各パラメータを見ていきます。

各画像を解析するためには、画像を同じサイズにする必要があります。
ここでは224×224のサイズを作成するようにします。

また画像のリサイズ方法は、いくつかfastaiで用意されています。
ここでは画像をランダムな拡大率で拡大し、正方形切り取る`RandomResizedCrop`を使用していきます。

```
item_tfms=RandomResizedCrop(224, min_scale=0.5),
```

すべての画像が同一サイズになると、GPU上で複数の画像に対して、一気に加工できます。

すべての画像に対して、データ拡張します。
この処理は、画像のアングルを変えたり、コントラストや明るさなどを変更します。

これは、学習データと少しアングル等が違うだけで、全く予測できないような状況を防ぐことが目的です。


```
batch_tfms=aug_transforms()
```

最後、実際にパスを指定して、画像からモデルを生成します。

```
dls = characters.dataloaders(path)
```

## 学習
いよいよモデルに学習をさせていきます。
アーキテクチャとして、画像のニューラルネットワークの代表的なCNNを利用します。

```
learn = cnn_learner(dls, resnet18, metrics=error_rate)
```

`cnn_learner`は、fastaiで用意されているCNNのライブラリです。
`resnet18`は、ここではresnetを深堀しませんが、resnetという画像解析の手法を１８層使用することを指定しています。
`metrics`は、学習結果として失敗の割合を出せたり(error_rate)、正解率を出させたり（accuracy）と指定できます。

学習の方法が定義できたら、実際に学習をさせます。
今回は10epoch、つまり10周trainingとvalidationを行います。

```
learn.fine_tune(10)
```

## 学習結果
学習した結果は、fastaiのライブラリを利用することで、わかりやすく表示させることができます。

```
interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix()
```

![スクリーンショット 2021-03-18 072347.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97498/34a8584b-8ecc-7f95-d976-1f2d8ec6fd51.png)

このマトリックスは、行が正解を表し、列が予測を表します。
つまり今回は、実際はのび太なのに、ジャイアンと予測したケースが２１ケースありました。

## データクリーニング
画像データのクリーニング作業をします。
fastaiでは、実際にうまく予測できなかったイメージを確認し、
削除やラベルの変更するためのツールが用意されています。

```
cleaner = ImageClassifierCleaner(learn)
cleaner
```

![スクリーンショット 2021-03-18 072544.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97498/ce2ad68d-d53a-3401-8803-2de2b53d90d3.png)

今回は、複数キャラクタが写っている画像や、手書きの似顔絵などは削除していきます。
またスネ夫なのに、ドラえもんの訓練データに入っているものなどは、ラベルの変更します。

※プルダウンで選択したら、削除や移動がされるわけではありません。
以下のコードを実行して初めて、削除や移動されます。

```
import os
for idx in cleaner.delete(): cleaner.fns[idx].unlink()
for idx,cat in cleaner.change(): 
    filepath = str(cleaner.fns[idx])
    ext = os.path.splitext(filepath)[-1]
    dest = path/cat/os.path.basename(filepath)
    i = 1
    while True:
        if os.path.exists(dest):
            filename = os.path.splitext(os.path.basename(dest))[0] + "_" + str(i)
            dest = path/cat/(filename+ext)
            i += 1
        else:
            break
    shutil.move(str(cleaner.fns[idx]), dest)
```

## 再学習
データクリーニングが終わったので、再度学習したら、こうなりました。
![スクリーンショット 2021-03-20 070426.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97498/02b73bf6-8a94-8cb0-ba3d-a20afe37a076.png)

多少のび太の認識精度が悪いですが、どらえもんの予測には問題なさそうなので、このままいきます。


## 予測
ドラえもんの画像を読み込ませて、実際に予測させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97498/1f34419d-ed28-3dce-4320-878719cee73e.png)


```
learn_inf.predict(path/'doraemon.png')
```

予測した結果は、
1. ラベル名
2. ラベルのインデックス
3. 各ラベルの確率
が表示されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97498/b8a15494-0499-2858-0674-ddd7f9aef2b7.png)

# おわりに
機械学習全くの未経験の私ですが、理論からではなく実際にどんなことをしているかが理解できて、非常に楽しく学べました。

また初めて記事投稿しましたが、記事を書くうちに改めて調べなおしたりして、良い学習ができました。
今後もfast aiに準じて学習していくので、各チャプターごとに自分で何かを作って、記事投稿につなげていきたいです。

# 参考
[fastai] https://docs.fast.ai/
[bing search] https://www.microsoft.com/en-us/bing/apis/bing-web-search-api
[Github] https://github.com/iris-net/fastbook/blob/main/02_doraemon_identifier.ipynb