SIGNATEの画像分類の練習問題をColabで行う

# 0.背景
* AI初心者。Kaggle の Titanic 問題をやってみたところ。
* 次のステップとして、画像分類問題を解いてみたかった。
	* SIGNATEの【練習問題】鋳造製品の欠陥検出 に取り組もうと考えた。 https://signate.jp/competitions/406
        * 見た目でわかりやすく、データ数が少なく、取り組みやすい。
* SIGNATEは、Kaggleと違って開発環境は用意されていないため、Google Colab 上で解くことを考えた。
    * とりあえず動かしてみたい人向け。


# 1. SIGNATEのAPIをインストールする
以下のサイトを参考にSignateのAPIを設定しました。
[Google ColabでSIGNATE APIを使う->Google ColaboratoryでAPIの認証を実行](https://qiita.com/insilicomab/items/d757339d97b44feaff01#google-colaboratory%E3%81%A7api%E3%81%AE%E8%AA%8D%E8%A8%BC%E3%82%92%E5%AE%9F%E8%A1%8C)


# 2.ファイルダウンロード
SIGNATEのAPIを使って学習用とテスト用の画像/CSVファイルをダウンロードして、Colabのフォルダに展開する。
```python
!signate download --competition-id=406

import zipfile
train_data = zipfile.ZipFile('train_data.zip')
train_data.namelist() # zip内のファイル名一覧を取得
train_data.extractall() # zip内容を出力

test_data = zipfile.ZipFile('test_data.zip')
test_data.namelist() # zip内のファイル名一覧を取得
test_data.extractall()
```
* Colabのフォルダに保存されるため、Colabのセッションが切れると削除される。
* 毎回ダウンロードしたくない場合は、Google driveに保存するのがよいが、まずは学習・推論を動かしてみたい。この練習問題はファイルサイズが小さいので毎回ダウンロードしてもそれほど時間は掛からない。
* ダウンロードしたら、以下のように画像データにアクセスできるか確認する。

```python
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
img = mpimg.imread('/content/train_data/cast_def_0_0.jpeg')
imgplot = plt.imshow(img)

```
うまくいけば以下のように画像が表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2549376/f1eaa056-5563-ffb1-ec1f-f942c95a883d.png)


# 3.問題を解く
## 3.1 学習

KerasでResnet50を使うことにした。
以下のWebサイトを参考にしています。
[ResNetをFine Tuningして自分が用意した画像を学習させる](http://pchun.work/resnet%E3%82%92fine-tuning%E3%81%97%E3%81%A6%E8%87%AA%E5%88%86%E3%81%8C%E7%94%A8%E6%84%8F%E3%81%97%E3%81%9F%E7%94%BB%E5%83%8F%E3%82%92%E5%AD%A6%E7%BF%92%E3%81%95%E3%81%9B%E3%82%8B/)

##### まずはCSVファイルを読み込む

```python
import numpy as np
import pandas as pd
train = pd.read_csv("train.csv")

train['target']=train['target'].astype(str) #あとで使うflow_from_dataframeがy_colがstrでないとエラーが出るため変換
train.head()
```
最後の`train.head()`はcsvがきちんと読み込めているかの確認用です。うまくいけばcsvファイルの最初の5行が表示されます。

##### trainデータを学習(train)用と検証(validation/test)用に分割する
* ImageDateGeneratorにもvalidation_splitがあるが、正解データの割合を均一にする機能（stratify）が無いため、sklearnで分割した
    * 正解データの割合を均一にする機能を使わないと、CSVデータは正常と欠陥がソートされているため、検証用データがすべて欠陥となり、すべて１と予測すればval_accuracyが1.0になってしまう。これだと、ResNet50がきちんと学習できているか検証できない。

```python
from sklearn.model_selection import train_test_split

train_train, train_test = train_test_split(train, test_size=0.1, stratify=train['target'], random_state=2)
```
* ImageDateGeneratorでデータを読み込む
```python
nb_classes=2

train_data_dir = '/content/train_data'

nb_train_samples = 225
nb_train_batch = 16
nb_validation_samples = 25 

img_width, img_height = 224, 224 #インプット画像は300x300だが学習済みのResNet50が224x224なのでそちらに合わせる

from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(
    rescale=1.0 / 255, # 各画素値の0~1への正規化
    rotation_range=180, 
    width_shift_range = 0.05,
    height_shift_range = 0.05,
    horizontal_flip=True, # 画像をランダムに左右反転
    vertical_flip=True
)

!mkdir /content/record_train_gen/

train_generator = train_datagen.flow_from_dataframe(
    train_train,
    train_data_dir,
    x_col = 'id',
    y_col = 'target',
    target_size=(img_width, img_height),
    color_mode='rgb',
    class_mode='categorical',
    batch_size=nb_train_batch,
    save_to_dir='/content/record_train_gen/')

test_datagen = ImageDataGenerator(
    rescale=1.0 / 255
    )
validation_generator = test_datagen.flow_from_dataframe(
    train_test,
    train_data_dir,
    x_col = 'id',
    y_col = 'target',
    target_size=(img_width, img_height),
    color_mode='rgb',
    class_mode='categorical',
    batch_size=25)

```
* 学習用のデータが少ないため、水増し（data augmentation）が必要です。```train_datagen = ImageDataGenerator(...```でdata augmentationを設定しています。
* 一方で検証用のデータは ```test_datagen = ImageDataGenerator(```で生成しており、こちらはdata augmentationはしていません。この```test_datagen```は学習後の推論でも使いまわしています。

##### 学習を行うモデル（ResNet50）を作成
```python
#学習を行うモデルを作る
from keras.applications.resnet import ResNet50
from keras.models import Sequential, Model
from keras.layers import Input, Flatten, Dense

input_tensor = Input(shape=(img_width, img_height, 3))
ResNet50 = ResNet50(include_top=False, weights='imagenet',input_tensor=input_tensor)
  #入力画像は224×224のRGB画像なので，これをinput_tensorとして指定．
  #今回FineTuningを行うにあたって，出力結果は2クラスの分類にしたいので，全結合層を変更します．
  #そのためにまず，include_top=Falseとすることで，全結合層を除いたResNet50をインポートします．
  #そしてこのとき， weights=’imagenet’とすることで学習済みのResNet50が読み込めます．逆に，weights=Noneとすると，ランダムな初期値から始まります．

#除去した全結合層の代わりに用いる新たな全結合層を作成
top_model = Sequential()
top_model.add(Flatten(input_shape=ResNet50.output_shape[1:]))
top_model.add(Dense(nb_classes, activation='softmax'))
  #2行目のコードで，ResNet50の出力を1次元化しています．
  #これを出力クラス数が5の全結合層に入力する形で，新たな全結合層を定義します．

model = Model(inputs=ResNet50.input, outputs=top_model(ResNet50.output))
#全結合層を取り払ったResNet50と，新たに作った全結合層をくっつけてモデルの形づくりは完了

#チェック用
#model.summary()
```

##### 損失関数など学習の条件を設定
```python
#学習処理を決めるコンパイル
from tensorflow import keras
from tensorflow.keras import optimizers
#from keras import optimizers

model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.SGD(learning_rate=1e-4, momentum=0.9),
              loss_weights=[0.3, 0.7],
              metrics=['accuracy'])
  #最適化アルゴリズムに確率的勾配降下法
```
* ```loss_weights=[0.3, 0.7]```:正常データに比べて欠陥データが少ないため、欠陥データでの学習の重みを少し大きくしている。
##### 学習
* ここは実行に時間が掛かります。ColabのランタイムタイプがNoneの場合は、GPUに切り替えてください。
* ランタイムタイプがGPUの場合で30分くらい掛かりました。
```python
history = model.fit_generator(
    train_generator,
    steps_per_epoch=nb_train_samples//nb_train_batch, 
    epochs=200,
    validation_data=validation_generator,
    validation_steps=1)
```
* ハマったポイント：```steps_per_epoch=nb_train_samples//nb_train_batch,```の```//nb_train_batch```がコメントだと思って入力していなかった。C言語だと```//```はコメントだが、Pythonだと整数の商。

##### 学習がうまくいっていそうかグラフ化して確認
```python
#学習が収束しているかグラフ化してみてみる
import matplotlib.pyplot as plt

plt.plot(history.history['accuracy'])
plt.plot(history.history['val_accuracy'])
plt.title('Model accuracy')
plt.ylabel('Accuracy')
plt.xlabel('Epoch')
plt.legend(['Train', 'Test'], loc='upper left')
plt.show()
```
* 例として私の結果は以下のような感じでした。
    * ハマったポイント：最初は20 epochくらいで試しに回してみたところ、val_accuracyが上がらないため、バグがあると思って探し回っていました。30 epochくらいから val_accuracyが上がり始めます。
        * この問題のトレーニングデータは、正常:欠陥=6:4なので、Accuracyが0.6以下だと何も予測できていないといえます（全部正常と予測すれば0.6のAccuracyになるため）。
        
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2549376/46ed9cf8-7740-6006-997a-9083682c8356.png)

## 3.2 推論

##### 学習したモデルでテストデータに対する推論を行う
```python
#学習結果からtestデータに対して予測する
sample_submission = pd.read_csv("sample_submission.csv", header=None)

test_data_dir = '/content/test_data/'

test_generator = test_datagen.flow_from_dataframe(
    sample_submission,
    test_data_dir,
    x_col=0,
    target_size=(img_width, img_height),
    color_mode='rgb',
    shuffle=False, 
    class_mode=None)

pred = model.predict_generator(test_generator,verbose=1)
```
地味にハマったポイント
`shuffle=False` をしないとデータの順番がシャッフルされて提出データの結合時にidと結果がずれてしまいました。

##### 推論結果をSIGNATEへの提出用のCSVファイルに出力
```python
#print(pred)
predicted_class_indices=np.argmax(pred,axis=1) #One-hotから最大値を取り出し

#通常は以下のように数字をラベルに置き換える。今回は{'0': 0, '1': 1}なので変換不要
#labels = (train_generator.class_indices)
#labels = dict((v,k) for k,v in labels.items())
#predictions = [labels[k] for k in predicted_class_indices]

my_submission=sample_submission
my_submission[1] = list(predicted_class_indices)
my_submission.to_csv("submission.csv", index=False, header=None)
```
# 4.投稿する
* Colabから一度ローカルにダウンロードして、SIGNATEのサイトにアップロードしていました。
    * 上記のResNet50で、評価は0.99でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2549376/ed377ec4-37a1-6eb7-7a91-f6db133b6298.png)