コンピュータとオセロ対戦30　～深層学習で勝敗予測～

https://qiita.com/tt_and_tk/items/068f2afde6db637e189f

[前回](https://qiita.com/tt_and_tk/items/4006bce08ede7e975033)

# 今回の目標
[前回](https://qiita.com/tt_and_tk/items/4006bce08ede7e975033)作成したAIを、深層学習で作り直す。
# ここから本編
前回は機械学習を用いましたが、誤差が大きいためかあまり強くはありませんでした。
深層学習なら誤差を小さくできるかもしれないと考え、深層学習で作り直すことを考えました。

## 方針
このAIの考え方について再度説明しておきます。

オセロで最適な置き方を考えるとき、その優先度(どの場所を抑えておくべきか？　どの場所は取らないでおくべきか？)は状況によって細かく変化します。そのため、固定的に「これが最優先！」とは言えません。
なので、盤面から最終結果を予測し、その予測値が最も良いところに石を置くことを考えます。
この「盤面から最終結果を予測」をこれまでRidge回帰を用いて行っていましたが、今回はニューラルネットワークを用いて行います。

なお言語はPython、ライブラリはchainerを用います。数あるライブラリからこれを選んだ理由は、日本語のチュートリアルが存在するためです。

[chainer公式チュートリアル](https://tutorials.chainer.org/ja/tutorial.html)

## BitBoard.py
細かく説明すると長くなるので、一言だけ。
オセロをするための基本クラスが入っています。

## osero_learn.py
学習のためのデータ集めを行うためのクラスが入っています。

## run.ipynb
### データ集め
いつも通り、PLAY_WAYから人間が置く項目を消し、他の手法(ランダムや1handなど)のみを抜き出します。
また、評価値も作ります。
そのうえで各手法ごとの総当たり戦を10通り行いました。

```py
from copy import deepcopy

import numpy as np

from osero_learn import learn
from BitBoard import osero

################################

PLAY_WAY = deepcopy(osero.PLAY_WAY)
del PLAY_WAY["human"]
PLAY_WAY = PLAY_WAY.values()

eva = [[
     1.0, -0.6,  0.6,  0.4,  0.4,  0.6, -0.6,  1.0,
    -0.6, -0.8,  0.0,  0.0,  0.0,  0.0, -0.8, -0.6,
     0.6,  0.0,  0.8,  0.6,  0.6,  0.8,  0.0,  0.6,
     0.4,  0.0,  0.6,  0.0,  0.0,  0.6,  0.0,  0.4,
     0.4,  0.0,  0.6,  0.0,  0.0,  0.6,  0.0,  0.4,
     0.6,  0.0,  0.8,  0.6,  0.6,  0.8,  0.0,  0.6,
    -0.6, -0.8,  0.0,  0.0,  0.0,  0.0, -0.8, -0.6,
     1.0, -0.6,  0.6,  0.4,  0.4,  0.6, -0.6,  1.0
] for i in range(2)]

################################

data = []
result = []

turn_vari = [i for i in range(5, 61, 5)]
run = learn(0, 0, turn_vari, eva=eva)

for i in range(10):
    print("\r[" + "#" * (i+1) + " " * (10-i+1) + "]", end="")
    for black in PLAY_WAY:
        for white in PLAY_WAY:
            run.black_method = black
            run.white_method = white
            run.eva = eva
            data_ele, result_ele = run.play()
            for data_each_turn in data_ele:
                data.append(data_each_turn)
            for result_each_turn in result_ele:
                result.append([result_each_turn])

data = np.array(data).astype(np.float32)
result = np.array(result).astype(np.float32)

print("\r[" + "#" * 10 + "]")
```

### 学習準備
データを学習できる形に変更し、ニューラルネットワークを定義しました。今回はチュートリアルと全く同じ、全結合層三層で活性化関数はReluを使います。
入力ノード数はもちろん自分の駒が置いてある場所だけ立ててある配列+相手の駒が置いてある場所だけ立ててある配列の長さぶん、64+64=128個で、出力は回帰なので一つです。隠れ層のノード数はとりあえず入力ノード数と同じにしました。
最適化手法もチュートリアルと同じく確率的勾配降下法です。重み減衰も採用しました。また、データの正規化は行っていません。
また今回は機械学習時と違い、ターン数ごとに分けていません。理由は、ニューラルネットワークで手いっぱいだったので忘れていたというのが本音です。

```py
from chainer.datasets import TupleDataset
from chainer.datasets import split_dataset_random
from chainer.iterators import SerialIterator

import chainer
import chainer.links as L
import chainer.functions as F

from chainer import optimizers
from chainer.optimizer_hooks import WeightDecay

from chainer.dataset import concat_examples

################################

batch_size = 10

dataset = TupleDataset(data, result)

train_val, test = split_dataset_random(dataset, int(len(dataset) * 0.7), seed=0)
train, valid = split_dataset_random(train_val, int(len(train_val) * 0.7), seed=0)

train_iter = SerialIterator(train, batch_size=batch_size, repeat=True, shuffle=True)

################################

class Net(chainer.Chain):
    def __init__(self, n_in, n_hidden, n_out):
        super().__init__()
        with self.init_scope():
            self.l1 = L.Linear(n_in, n_hidden)
            self.l2 = L.Linear(n_hidden, n_hidden)
            self.l3 = L.Linear(n_hidden, n_out)
    
    def __call__(self, x):
        h = F.relu(self.l1(x))
        h = F.relu(self.l2(h))
        h = self.l3(h)

        return h

################################

net = Net(len(data[0]), len(data[0]), 1)

optimizer = optimizers.SGD(lr=0.01)
optimizer.setup(net)

for param in net.params():
    if param.name != "b":
        param.update_rule.add_hook(WeightDecay(0.00001))
```

### 学習
目的関数は平均二乗誤差を使っています。また、今回の学習における意味のある誤差は平均絶対誤差なので(実際の最終結果と予測値の差が必要であるため)、それもついでに求めています。
繰り返し回数は50回、バッチの大きさはトレーニングデータの数の十分の一に設定しました。

```py
n_epoch = 50
n_batch = int(len(train) / 10)

results_train = {
    "MSE": [],
    "MAE": []
}
results_valid = {
    "MSE": [],
    "MAE": []
}

train_iter.reset()

for epoch in range(n_epoch):
    while True:
        train_batch = train_iter.next()

        x_train, t_train = concat_examples(train_batch)

        y_train = net(x_train)
        MSE_train = F.mean_squared_error(y_train, t_train)
        MAE_train = F.mean_absolute_error(y_train, t_train)

        net.cleargrads()
        MSE_train.backward()

        optimizer.update()

        if train_iter.is_new_epoch:
            with chainer.using_config("train", False), chainer.using_config("enable_backprop", False):
                x_valid, t_valid = concat_examples(valid)
                y_valid = net(x_valid)
                MSE_valid = F.mean_squared_error(y_valid, t_valid)
                MAE_valid = F.mean_absolute_error(y_valid, t_valid)


            results_train["MSE"].append(MSE_train.array)
            results_train["MAE"].append(MAE_train.array)
            results_valid["MSE"].append(MSE_valid.array)
            results_valid["MAE"].append(MAE_valid.array)

            break
```

### 学習結果
プログラム自体はいつも通りです。

```py
import matplotlib.pyplot as plt


def plot(train, valid, xlabel, ylabel, fig_name, save_dir):
    fig = plt.figure(figsize=(10, 10))
    plt.plot(train, label="train")
    plt.plot(valid, label="valid")
    plt.legend()
    plt.xlabel(xlabel)
    plt.ylabel(ylabel)
    plt.title(fig_name)
    plt.savefig(save_dir + "/" + fig_name)
    plt.clf()
    plt.close()

################################

plot(
    results_train["MSE"],
    results_valid["MSE"],
    "epoch",
    "mean squared error",
    "mean squared error of each epoch",
    "fig"
)

plot(
    results_train["MAE"],
    results_valid["MAE"],
    "epoch",
    "mean absolute error",
    "mean absolute error of each epoch",
    "fig"
)
```

結果はこちら。

![mean squared error of each epoch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/a5214e81-faf8-62b9-d582-667b385b1005.png)

![mean absolute error of each epoch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/6f09b9a7-c105-285b-ecfa-e39d9ff70442.png)

波打っているのが気になりますが、一応学習は進んでいるようです。
また、ターン数ごとに分けたりはしていませんが精度が非常に高いです。この方法の方が学習しやすかった可能性もあるので何とも言えませんが、Ridge回帰と比べ非常に低い誤差で予測ができています。

テストデータでの誤差も出してみました。

```py
x_test, t_test = concat_examples(test)
with chainer.using_config("train", False), chainer.using_config("enable_backprop", False):
    y_test = net(x_test)
    MSE_test = F.mean_squared_error(y_test, t_test)
    MAE_test = F.mean_absolute_error(y_test, t_test)

print("test MSE: {:.4f}".format(MSE_test.array))
print("test MAE: {:.4f}".format(MAE_test.array))
```

結果はこちら。

```
test MSE: 11.6365
test MAE: 2.9128
```

平均絶対誤差が約3という結果に。
この精度はすごいですね。

# フルバージョン

https://github.com/TTaisei/osero/tree/main/deep_learn
# 次回は
満足のいく高い精度は出ましたが、「理由は分からないけど誤差小さいからこれで良し」ではつまらないので学習条件を色々変えて実験してみたいと思います。

[次回](https://qiita.com/tt_and_tk/items/a06e3c9ddf88b0213cb9)

https://qiita.com/tt_and_tk/items/068f2afde6db637e189f