初心者によるKaggleメモ#5 落書き画像のクラス分けv2 本格的な計算作業

[前回(\#4)](https://qiita.com/simonritchie/items/25257d7d80d9f8d4b2d6)からの続きです。\#4までで、基本的な提出までのコードは書いたので、ここからは本格的にデータサイズを大きくしたり、エポックを増やしたり、チューニングなどを試してみます。

# 前準備

コードは前回までのKaggle Kernelのものをforkして使います。
forkしなくても、コミットの度にバージョン管理してくれますが、各バージョンのコードは見れるものの、Kaggle上でrebase的なことをして、コミットを無くしたりといったことが直接はできず少し面倒なのでforkしておきます。

# エポック数を増やして、且つearly stoppingを設定する

Kerasでearly stoppingのクラスを用意してくれているので、そちらをimportします。
多くのエポックを学習させている中で、変な感じに学習が進んでいった場合や、これ以上学習が進まなくなってきた際に、指定したエポック数以下で学習を止めてくれるようになります。

```py
from keras.callbacks import EarlyStopping
```

エポックは前回までの10倍の50を指定しておきましょう。
なお、Kaggle Kernelが停止したら、アウトプットも消えてしまう（停止する前に提出が必要）なのかな？と思っていましたが、そんなことはなく停止後も提出できそうな気配があったため、結構大きく増やします。

```py
EPOCH_NUM = 50
```

Kaggle KernelのGPUなら、余裕がありそうなのでバッチサイズも増やしてみます。（大きく増やしてみて、エラーで弾かれたら調整）
ひとまず前回の4倍くらいにします。

```py
BATCH_SIZE = 1024
```

early_stoppingはfit関数のcallbacksの個所に設定しておきます。

patience引数は、何エポック学習が動かなくなっから、停止させるか（デフォルトでは0）という値のようです。どのくらいがいいんだろう・・とりあえず5回学習が動かない状態になったら、それはもう止めていいか・・という判断で、今回は5を指定しました。

verboseは、1を指定しておくと、early_stoppingで止まった際にその旨をOutputに出力してくれます。

```py
early_stopping = EarlyStopping(patience=5, verbose=1)

model.compile(
    loss='categorical_crossentropy', optimizer=Adam(),
    metrics=['accuracy'])
history = model.fit(
    x=X_train, y=y_train, batch_size=BATCH_SIZE, epochs=EPOCH_NUM, verbose=2,
    shuffle=True, validation_data=(X_val, y_val),
    callbacks=[early_stopping])
```

```
Epoch 10/50
 - 16s - loss: 1.4150 - acc: 0.6506 - val_loss: 2.2421 - val_acc: 0.5032
Epoch 11/50
 - 16s - loss: 1.3283 - acc: 0.6702 - val_loss: 2.3010 - val_acc: 0.4952
Epoch 12/50
 - 16s - loss: 1.2460 - acc: 0.6878 - val_loss: 2.3622 - val_acc: 0.4947
Epoch 00012: early stopping
```

止まってしまった？
想定した動きとは少々異なるので、少し調査します。

学習自体は、バッチサイズを調整した結果なのか、早くなっていますね。

どうやら、ネットの記事やdocsring読んで勘違いしていたようで、patienceの値は学習がまったく進まないかどうかではなく、何エポック悪化していったか、で判断されるのでしょうか。

試しにpatience=2を指定して再度学習させたところ、val_accが変動しているにも関わらずすぐに学習が止まりました。
patience=10のときも、patience=5のときとあまり変わらないタイミングで停止しています・・。

データセットがまだ少なく、バリデーション精度が悪化したりしやすい（オーバーフィットしやすい）ことが影響しているのだろうか・・
まあでも、恐らくデータセットを増やしたらオーバーフィットしにくくなって改善するのでしょうから、5くらいで運用することにします。

データセットの方も、増やしてしまいます。

```py
DATA_ROW_NUM = 5000

df_list = []
for simplified_file_path in simplified_file_path_list:
    print(datetime.now(), simplified_file_path, 'started...')
    df = pd.read_csv(simplified_file_path, nrows=DATA_ROW_NUM)
    df['28px_img_arr'] = df['drawing'].apply(get_28px_img_arr)
    df_list.append(df)
```

これ以降はとても時間がかかるので、コミットして結果を待つようにします。

# 使用メモリを減らす

コミットしてしばらく放置していましたが、どうやらメモリエラーになったようです。
各所で、メモリを食っているところを調整していきます。

まずはusecolsを指定して、使わないカラムをデータフレームで読み込まないようにしておきます。

```py
DATA_ROW_NUM = 5000

df_list = []
for simplified_file_path in simplified_file_path_list:
    print(datetime.now(), simplified_file_path, 'started...')
    df = pd.read_csv(
        simplified_file_path, nrows=DATA_ROW_NUM,
        usecols=['drawing', 'key_id', 'word'])
    df['28px_img_arr'] = df['drawing'].apply(get_28px_img_arr)
    df_list.append(df)
```

また、scikit-learnのtrain_test_splitが、コピーされる？ようでメモリを余分に使ってしまうので、コピーせずに分割されるようにshuffleの引数をFalseにしておきます。

> As mentioned by @user1879926, I think shuffle is a main cause of memory exhaustion.
> And ,as 'Shuffle' is claimed to be an invalid parameter for model_selection.train_test_split cited, train_test_split in sklearn 0.19 has option disabling shuffle.
> So, I think you can escape from memory error by just adding shuffle=False option.
> [Memory efficient way to split large numpy array into train and test](https://stackoverflow.com/a/48094807)

データフレームの段階で、シャッフルしておくことにします。

```py
train_overall_df = train_overall_df.sample(frac=1, replace=True, random_state=42)
train_overall_df.reset_index(drop=True, inplace=True)
...
X_train, X_val, y_train, y_val = train_test_split(
    X, y, test_size=0.2, shuffle=False)

train_overall_df[:3]
```

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>drawing</th>
      <th>key_id</th>
      <th>word</th>
      <th>28px_img_arr</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>[[[0, 21, 73, 82, 87], [50, 49, 40, 68, 126]],...</td>
      <td>6058894906359808</td>
      <td>bridge</td>
      <td>[[0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, ...</td>
    </tr>
    <tr>
      <th>1</th>
      <td>[[[53, 49, 50, 58, 80, 146, 153, 152, 145, 113...</td>
      <td>5939724743081984</td>
      <td>stove</td>
      <td>[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...</td>
    </tr>
    <tr>
      <th>2</th>
      <td>[[[56, 81, 150, 168, 195, 225, 238, 250, 255, ...</td>
      <td>5054885764530176</td>
      <td>dog</td>
      <td>[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...</td>
    </tr>
  </tbody>
</table>

しかしtrain_test_splitの個所で同じエラー・・
shuffle関係ないのでしょうか？

以下を参考に再度調整します。
https://stackoverflow.com/a/31481675

```py
def train_test_split(X, y, test_proportion):
    ratio = int(X.shape[0] / test_proportion)
    X_train = X[ratio:, :]
    X_test =  X[:ratio, :]
    Y_train = y[ratio:, :]
    Y_test =  y[:ratio, :]
    return X_train, X_test, Y_train, Y_test

X_train, X_val, y_train, y_val = train_test_split(X=X, y=y, test_proportion=4)
```

これで、動きはしました。
ただ、使用メモリがこの時点で13GBなので些か心許ない・・
場合（再度別の個所でメモリが引っかかった場合など）によっては、float16などの利用を検討します。

```
Train on 1275000 samples, validate on 425000 samples
Epoch 1/50
 - 79s - loss: 2.6384 - acc: 0.4187 - val_loss: 2.0624 - val_acc: 0.5195
Epoch 2/50
 - 75s - loss: 1.8914 - acc: 0.5547 - val_loss: 1.8226 - val_acc: 0.5703
Epoch 3/50
 - 75s - loss: 1.6782 - acc: 0.5993 - val_loss: 1.7100 - val_acc: 0.5938
Epoch 4/50
 - 75s - loss: 1.5451 - acc: 0.6275 - val_loss: 1.6397 - val_acc: 0.6090
...
```

とりあえず出だしは順調に進んでいるのと、やはりデータセットが多いせいか、オーバーフィッティング的な状態にも最初の方はなっていないようです。
また、バッチサイズを大きくしたからなのか、データを5倍にしてもそのまま処理時間5倍、とはならないようですね。（3倍強）
コミットして放置します。

---

処理は流れたものの、どうやらエポック16あたりでearly stoppingによって止まった模様。
精度の内容を見るとまだ止めなくてもよくない・・？という印象です。
Kerasのearly stoppingがよく分かっていません・・ちょっと調べてみます。

[EarlyStopping not working properly](https://github.com/keras-team/keras/issues/2159)を見ると、patience=20くらいで設定している方がいるので実はそれくらいが妥当なのか？ということで、20に調整してみます。

とりあえずここまで試した時点でスコア0.611（106 / 186）。
わずかに改善しましたが、思ったよりよくならず・・
次の検証に移ります。

# ネットワークを深くする

ネットワークをもっと深くし、BatchNormalizationを挟んでみます。また、活性化関数をELUにしてみます。
うまくいくか微妙なので、先ほどまでのノートをforkして進めます。

データを用意するところは、最初はモデルが通るか確認するため、1クラス500画像程度の少な目なデータで進めます。バッチサイズももう少し大きめなものを試してみます。Dropoutは、不安定な印象を受けたので、一旦検証してみて今回は省きました。

```py
from keras.layers.normalization import BatchNormalization
...
BATCH_SIZE = 2048
```

```py
ACTIVATION_STR = 'elu'

def make_conv8_input():
    return Conv2D(
        filters=32, kernel_size=5, padding='same', activation=ACTIVATION_STR,
        input_shape=INPUT_SHAPE)

def make_conv8():
    return Conv2D(filters=8, kernel_size=5, padding='same', activation=ACTIVATION_STR)

def make_conv16():
    return Conv2D(filters=16, kernel_size=5, padding='same', activation=ACTIVATION_STR)

def make_conv32():
    return Conv2D(filters=32, kernel_size=5, padding='same', activation=ACTIVATION_STR)

def make_batch_norm():
    return BatchNormalization()

def make_max_pool():
    return MaxPooling2D(pool_size=(2, 2), strides=(2, 2))
```

```py
model = Sequential()

model.add(make_conv8_input())
model.add(make_batch_norm())
model.add(make_conv8())
model.add(make_batch_norm())
model.add(make_max_pool())

model.add(make_conv16())
model.add(make_batch_norm())
model.add(make_conv16())
model.add(make_batch_norm())
model.add(make_max_pool())

model.add(make_conv32())
model.add(make_batch_norm())
model.add(make_conv32())
model.add(make_batch_norm())
model.add(make_max_pool())

model.add(Flatten())
model.add(Dense(units=500, activation='relu'))

model.add(Dense(units=CLASSES_NUM))
model.add(Activation('softmax'))
```

エラーやざっとした精度をこのモデルで試してみて、一旦は通るようなので、1クラス辺りのデータ件数を5000に戻してコミットします。
結果が出るまでしばらく待ちます。

---

結果が出ました。


<img width="745" alt="20181007_1.png" src="https://qiita-image-store.s3.amazonaws.com/0/228778/cdbb42bf-c4de-508a-4d37-75c2b794fe88.png">

score が0.675で72位と、ぼちぼち精度が上がりました。（まだ参加者が少ないからですが、ギリギリブロンズメダル圏内）

まだ、色々改善点があるので、明日以降追加で試していこうと思います。

# メモ

- BatchNormalizationを入れたら、early stoppingで止まらなくなりました。安定感が増して、一時的に精度が悪い状態になって、early stoppingで止まる、といったことが無くなったからなのでしょうか？
- Dropout（25％）をデータが少ない時点で試してみて微妙だったので外しましたが、データが多い状態であれば、実は有効だったりするのでしょうか・・？要検証。
- メモリの処理時間の都合、まだデータのすべてを含められていません。この点は次回以降何とかならないか、検証を進めます（次元削減とか）。
- エポック50としましたが、エポックを増やすことでまだ伸びそうな気配はあります（収束しきっていない感）。ただ、計算時間をどうにかしないと・・