麻雀の牌情報をCNNに入れて打牌予測してみた(204)

# ※この記事は2020年に作成しました

# 使用したデータ
天鳳のある1日の鳳凰卓の対局データから、各打牌場面の「自分の手牌、自分の河、下家の河、対面の河、上家の河、自分からみた不明牌」の6×34=204次元の情報を入力とし、その直後の打牌を教師データとした。
入力データは牌が1つあれば1、4つあれば4となるようにしている。
教師データは打牌の種類（34種類）なので34クラスに分類するようto_categoricalをしている。
<img width="341" alt="fsdji.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436155/1c514634-04ac-ba22-455f-0f0b7495f985.png">

#作成したコード1
```python
# coding: UTF-8
import csv
import numpy as np
from keras.utils import to_categorical
from keras.models import Sequential
from keras.layers import Dense,Conv2D,MaxPooling2D,Dropout,Flatten,Activation,BatchNormalization
from keras.optimizers import Adam
num = 220629 #データ数

#前処理　ここから
x_train = np.zeros((num,204), dtype='float32')
with open('C:/sqlite/CNNdahai.csv', 'r') as fr:
    for i,row in enumerate(fr.readlines(),start=0):
        x_train[i] += np.array(list(map(np.float,row[:204])))

y_train = np.loadtxt("C:/sqlite/CNNdahaiLabel.csv",delimiter=",")
y_train = to_categorical(y_train,34)
x_train=x_train.reshape(num,6,34,1)
x_train = x_train/4.
#print('x_train.shape:',x_train.shape)
#print('y_train.shape:',ｙ_train.shape)

#前処理ここまで


model = Sequential()
#畳み込み層
model.add(
    Conv2D(
        filters=34,
        input_shape=(6,34,1),
        kernel_size=(3,3),
        strides=(1,1),
        padding='same',
        activation='relu'))
model.add(
    Conv2D(
        filters=34,
        kernel_size=(3,3),
        strides=(1,1),
        padding='same',
        activation='relu'))

#プーリング層
model.add(MaxPooling2D(pool_size=(2,2)))
#model.add(Dropout(0.15))
model.add((Flatten()))

#全結合層
model.add(Dense(units=400))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Dense(units=34))
model.add(BatchNormalization())
model.add(Activation('softmax'))

model.compile(
    optimizer=Adam(lr=0.001),
    loss='categorical_crossentropy',
    metrics=['accuracy']
)
history_modell=model.fit(
    x_train,y_train,
    batch_size=100,
    epochs=50,
    verbose=1,
    shuffle = True,
    validation_split=0.2,
)
```


##畳み込み層
filtersは出力チャンネル数（特徴マップ数）。filtersをどれくらいの大きさにしたらいいかわからない。
kernel_sizeはカーネルのサイズ。3×3や5×5など奇数の正方形が多い。
stridesはカーネルをずらす幅。
padding='same'はゼロパディングを適用している。ゼロパディングを使用しない場合は'valid'にする。

##プーリング層
マックスプーリング2×2を使用している。

##全結合層
中間層は400次元、出力は34次元。

##結果1
50epochの結果、val_accは8epochが一番高かった。

| 50epochの結果    |       値 |
|:-----------------|:------------------:|
| loss             |              0.9432 |
| acc           |            0.6703 |
| val_loss             |            1.5293   |
| val_acc               |               0.5252 |

| 8epoch目の結果    |       値 |
|:-----------------|:------------------:|
| val_loss             |            1.3339   |
| val_acc               |               0.5495 |

その後ユニットを500、カーネルを5×5にしてみたがあまり結果はよくなかった。

#作成したコード2
```python
# coding: UTF-8
import csv
import numpy as np
from keras.utils import to_categorical
from keras.models import Sequential
from keras.layers import Dense,Conv2D,MaxPooling2D,Dropout,Flatten,Activation,BatchNormalization
from keras.optimizers import Adam
num = 220629 #データ数

#前処理　ここから
x_train = np.zeros((num,204), dtype='float32')
with open('C:/sqlite/CNNdahai.csv', 'r') as fr:
    for i,row in enumerate(fr.readlines(),start=0):
        x_train[i] += np.array(list(map(np.float,row[:204])))

y_train = np.loadtxt("C:/sqlite/CNNdahaiLabel.csv",delimiter=",")
y_train = to_categorical(y_train,34)
x_train=x_train.reshape(num,6,34,1)
x_train = x_train/4.
#print('x_train.shape:',x_train.shape)
#print('y_train.shape:',ｙ_train.shape)

#前処理ここまで


model = Sequential()
#畳み込み層
model.add(
    Conv2D(
        filters=34,
        input_shape=(6,34,1),
        kernel_size=(3,3),
        strides=(1,1),
        padding='same',
        activation='relu'))
model.add(
    Conv2D(
        filters=34,
        kernel_size=(3,3),
        strides=(1,1),
        padding='same',
        activation='relu'))

#プーリング層
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(BatchNormalization())

#畳み込み層
model.add(
    Conv2D(
        filters=34,
        kernel_size=(3,3),
        strides=(1,1),
        padding='same',
        activation='relu'))
model.add(
    Conv2D(
        filters=34,
        kernel_size=(3,3),
        strides=(1,1),
        padding='same',
        activation='relu'))

#プーリング層
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(BatchNormalization())

#平坦化
model.add((Flatten()))

#全結合層
model.add(Dense(units=400))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Dense(units=34))
model.add(BatchNormalization())
model.add(Activation('softmax'))

model.compile(
    optimizer=Adam(lr=0.001),
    loss='categorical_crossentropy',
    metrics=['accuracy']
)
history_modell=model.fit(
    x_train,y_train,
    batch_size=100,
    epochs=10,
    verbose=1,
    shuffle = True,
    validation_split=0.2,
)
```
##コード1とコード2の違い
中間層のユニット数を400、カーネルサイズを3×3にして畳み込み層、プーリング層を追加した。

##結果2
10epochの結果、6epochがval_accが一番高かった。

| 10epochの結果    |       値 |
|:-----------------|:------------------:|
| loss             |              1.1091 |
| acc           |            0.6231 |
| val_loss             |            1.3295   |
| val_acc               |               0.5629 |

| 6epoch目の結果    |       値 |
|:-----------------|:------------------:|
| loss             |              1.2398 |
| acc           |            0.5821 |
| val_loss             |            1.3279   |
| val_acc               |               0.5695 |

##結果3
作成したコード2の畳み込み層のフィルターを102にした結果。自分のPパソでは1epoch約1200秒かかった。val_accは5epochが一番高かった。

| 10epochの結果    |       値 |
|:-----------------|:------------------:|
| loss             |              0.8032 |
| acc           |            0.7255 |
| val_loss             |            1.5132   |
| val_acc               |               0.5325 |

| 5epoch目の結果    |       値 |
|:-----------------|:------------------:|
| loss             |              1.1606 |
| acc           |            0.6097 |
| val_loss             |            1.2893   |
| val_acc               |               0.5712 |

##結果4
作成したコード2にさらに畳み込み層とプーリング層を追加し、フィルタを32、カーネルサイズを9×6、バッチサイズ500とした時の結果。

| 10epochの結果    |       値 |
|:-----------------|:------------------:|
| loss             |              1.1435 |
| acc           |            0.6154 |
| val_loss             |            1.9134   |
| val_acc               |               0.4544 |

##結果5
作成したコード2の学習係数0.0001、バッチサイズ500とした結果。

| 10epochの結果    |       値 |
|:-----------------|:------------------:|
| loss             |              1.3781 |
| acc           |            0.5937 |
| val_loss             |            1.5199   |
| val_acc               |               0.5413 |


##結果6
作成したコード2のフィルタを102、カーネルサイズを9×6、バッチサイズ100、学習係数0.001にした結果。自分のPパソでは1epochあたり約7000秒かかった。

| 10epochの結果    |       値 |
|:-----------------|:------------------:|
| loss             |              0.7616 |
| acc           |            0.7397 |
| val_loss             |            1.5380   |
| val_acc               |               0.5426 |

| 6epoch目の結果    |       値 |
|:-----------------|:------------------:|
| loss             |              1.1269 |
| acc           |            0.6186 |
| val_loss             |            1.3576   |
| val_acc               |               0.5559 |

##結果7
作成したコード2のx_trainを.reshape(num,34,6,1)にした結果。

| 10epochの結果    |       値 |
|:-----------------|:------------------:|
| loss             |              1.2826 |
| acc           |            0.5724 |
| val_loss             |            1.4713   |
| val_acc               |               0.5178 |

#考察
「CNN麻雀 -麻雀向けCNN構成の有効性-」という論文を参考にデータを作成した。論文では、データを立体的な構造にし、4つのフィルターを適用して順子と刻子を画像的に見て捉えることができるかもしれないとのこと。
今回作成したデータは単に牌の情報を平面的に入れているだけなので、順子や刻子の組み合わせを捉えられてるとは言えない。
作成したコード1と2では学習回数を増やすほど、学習データとの一致率は上がっていっているが、テストデータとの一致率は途中から下がって行っている。
テストデータとの一致率が一番高かったのが結果3の5epoch目で57.12%だった。