コンピュータとオセロ対戦32　～深層学習で勝敗予測、実験2～

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

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

# 今回の目標
前回やり残していた実験を行います。
# ここから本編
ファイル構造などは前回と全く同じです。

## run.ipynb
### GradientHardClippingのパラメータ

```py
from chainer.optimizer_hooks import GradientHardClipping as GHC

from deep_learn import *
import Net_box.plane as plane

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

run = deep_learn()
run.Net = plane.Net

par_arr = [
    [-1., 1.],
    [0., 1.],
    [-1., 0.],
    [-2., 2.],
    [-0.5, 0.5],
    [-0.1, 0.1],
    [-3., 3.],
    [-5., 5.]
]
run.save_dir = "fig/parameter"
i = 1
MSE_test = []
MAE_test = []

run.set_data()

for par in par_arr:
    print("\r%d/%d" % (i, len(par_arr)), end="")
    run.hook_f = GHC(par[0], par[1])
    run.fig_name1 = "MSE of each epoch (parameter is %.1f, %.1f).png" % (par[0], par[1])
    run.fig_name2 = "MAE of each epoch (parameter is %.1f, %.1f).png" % (par[0], par[1])
    run.fit()
    run.plot()
    test_error = run.cal_test_error()
    MSE_test.append(test_error[0])
    MAE_test.append(test_error[1])
    i += 1

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

print(par_arr)
print("MSE: ", MSE_test)
print("MAE: ", MAE_test)
```

実行結果はこちら。

![MSE of each epoch (parameter is -1.0, 1.0).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/08a15ecb-ac72-4f61-dbca-8af27b713ae9.png)
![MSE of each epoch (parameter is 0.0, 1.0).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/20fe73bd-6f92-854b-a662-1178fa43970e.png)
![MSE of each epoch (parameter is -1.0, 0.0).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/44681355-3834-f24f-6804-430c986de877.png)
![MSE of each epoch (parameter is -2.0, 2.0).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/745e9e5d-ff16-78f6-f862-a33fd2384658.png)
![MSE of each epoch (parameter is -0.5, 0.5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/aa2fc894-bc2f-7d39-9b9b-87b3246b67e3.png)
![MSE of each epoch (parameter is -0.1, 0.1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/9e068c12-ba8d-0ef9-e8f7-ae86d58b00e2.png)
![MSE of each epoch (parameter is -3.0, 3.0).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/7150b1f2-2b26-97b7-10d1-4313788d9b30.png)
![MSE of each epoch (parameter is -5.0, 5.0).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/317dc676-0baa-47da-267b-2c354aca9541.png)

![MAE of each epoch (parameter is -1.0, 1.0).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/7c9f99cb-c53d-2ea7-0b60-950ffeb433a7.png)
![MAE of each epoch (parameter is 0.0, 1.0).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/06af3431-2d58-1440-e3b7-0f15844bed4d.png)
![MAE of each epoch (parameter is -1.0, 0.0).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/16d06d2c-190c-910e-a09a-504ad67c9b3f.png)
![MAE of each epoch (parameter is -2.0, 2.0).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/08d4f221-ce98-750f-5825-dcfaf0c1f74b.png)
![MAE of each epoch (parameter is -0.5, 0.5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/db152665-b69c-9e74-569b-c837af6fbc5e.png)
![MAE of each epoch (parameter is -0.1, 0.1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/286dcc72-c2b7-7632-1132-8109075ec2e3.png)
![MAE of each epoch (parameter is -3.0, 3.0).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/001b153c-92a0-68a7-25af-052688db86d6.png)
![MAE of each epoch (parameter is -5.0, 5.0).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/2c16a1c7-f3ec-2ca9-9158-331040cd98c0.png)

```
[[-1.0, 1.0], [0.0, 1.0], [-1.0, 0.0], [-2.0, 2.0], [-0.5, 0.5], [-0.1, 0.1], [-3.0, 3.0], [-5.0, 5.0]]
MSE:  [variable(138.40294), variable(540.5586), variable(530.9654), variable(158.3173), variable(155.85709), variable(248.41429), variable(152.12968), variable(163.57144)]
MAE:  [variable(7.6048646), variable(19.716103), variable(19.489712), variable(8.066726), variable(8.328745), variable(12.342105), variable(7.896567), variable(8.243191)]
```

全体を見ると、制限が小さすぎたり、負の数または正の数のみにしかなれなかったりするとうまく学習できないようです。また、制限が大きいと収束までの時間が短いこともわかりました。ただ、大きければいいというものでもないようです。
テストデータでの最小誤差を出したのは[-1., 1.]でした。

### 組み合わせ

```py
import Net_box.n_layer as n_layer
from chainer.optimizers import AdaBound
import pandas as pd

bs_arr = [100, 150, 200]
net_arr = [
    [2, n_layer.Net2],
    [3, n_layer.Net3],
    [4, n_layer.Net4]
]
lr_arr = [0.01, 0.025, 0.3]
par_arr = [[-i, i] for i in range(1, 6)]

run = deep_learn()
run.optimizer = AdaBound()
i = 1

data = {
    "batch_size": [],
    "num_of_layer": [],
    "lr": [],
    "lower_bound": [],
    "upper_bound": [],
    "MSE_test": [],
    "MAE_test": []
}

run.set_data()

for bs in bs_arr:
    print("\rbs[%d/%d]" % (i, len(bs_arr)), end="")
    run.batch_size = bs
    for net in net_arr:
        print(".", end="")
        run.Net = net[1]
        for lr in lr_arr:
            run.lr = lr
            for par in par_arr:
                run.hook_f = GHC(par[0], par[1])
                run.fit()
                test_error = run.cal_test_error()
                data["batch_size"].append(bs)
                data["num_of_layer"].append(net[0])
                data["lr"].append(lr)
                data["lower_bound"].append(par[0])
                data["upper_bound"].append(par[1])
                data["MSE_test"].append(test_error[0])
                data["MAE_test"].append(test_error[1])
    i += 1

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

data = pd.DataFrame(data)

data.to_csv("data.csv")
```

前回の学習で上手く行ったパラメータを使用し、バッチサイズ、層の数、学習率、そしてGradientHardClippingのパラメータに関しては少しずらした値も同時に検証しました。
最終的なテストデータでの誤差、そしてその時のパラメータをcsvファイルに保存しました。

## analyse.ipynb

```py
import pandas as pd
import numpy as np

data = pd.read_csv("data.csv")

data_sorted = data.sort_values(by="MAE_test", inplace=False)
data_sorted.head(30)
```

実行結果はこちら。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/561b68cc-c71d-5a23-1b6a-266fa9177184.png)

見事に全て4層でした。
このデータだけだと層の数以外は特徴が読めないので、トップ50のデータを取り出し特徴を探してみました。

```py
data_top = data_sorted.head(50)

for par_name in data.columns:
    if par_name == "Unnamed: 0" or par_name == "MSE_test" or par_name == "MAE_test":
        continue
    print(par_name)
    for par in data_top[par_name].unique():
        print("\t%s: %d" % (par, len(data_top.query("%s==%s" % (par_name, par)))))
```

実行結果はこちら。

```
batch_size
	150: 16
	100: 18
	200: 16
num_of_layer
	4: 45
	3: 5
lr
	0.3: 16
	0.025: 18
	0.01: 16
lower_bound
	-3: 11
	-1: 10
	-5: 10
	-4: 10
	-2: 9
upper_bound
	3: 11
	1: 10
	5: 10
	4: 10
	2: 9
```

層の数以外は接戦となりました。
まだよく分からないですね。
今度はワースト50の特徴を見てみました。

```py
data_worst = data.sort_values(by="MAE_test", inplace=False, ascending=False).head(50)

for par_name in data.columns:
    if par_name == "Unnamed: 0" or par_name == "MSE_test" or par_name == "MAE_test":
        continue
    print(par_name)
    for par in data_worst[par_name].unique():
        print("\t%s: %d" % (par, len(data_worst.query("%s==%s" % (par_name, par)))))
```

その結果は、

```
batch_size
	150: 17
	200: 17
	100: 16
num_of_layer
	2: 45
	3: 5
lr
	0.025: 16
	0.01: 16
	0.3: 18
lower_bound
	-3: 10
	-2: 9
	-4: 10
	-5: 10
	-1: 11
upper_bound
	3: 10
	2: 9
	4: 10
	5: 10
	1: 11
```

どれも平均的な結果となりました。
トップとワースト、両方の結果を見ると、

* バッチサイズ　200が望ましい、100は避けるべき
* 学習率　0.025が望ましい、0.01は避けるべき
* bound　-3～3が望ましい、-1～1は避けるべき

という特徴が見えてきました。といっても僅差ですので、「絶対に避けるべき！」というほどでもないとは思います。

:::note
lrは使っていませんでした。
:::

## model.ipynb
最後に、これまで求めたパラメータを用いて本番用のモデルを作成したいと思います。

### データ集め
データ量は5x5x60の1500です。

```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(1, 61)]
run = learn(0, 0, turn_vari, eva=eva)
i = 1

for black in PLAY_WAY:
    print("\r%d/%d" % (i, len(PLAY_WAY)), end="")
    for white in PLAY_WAY:
        run.setup()
        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])
    i += 1

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

### 学習部分

```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 GradientHardClipping

from chainer.dataset import concat_examples
```

#### ニューラルネットワーク
全結合層四層で、活性化関数はtanhを用います。

```py
class Net(chainer.Chain):
    def __init__(self):
        n_in = 128
        n_hidden = 128
        n_out = 1
        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_hidden)
            self.l4 = L.Linear(n_hidden, n_out)
        
    def __call__(self, x):
        h = F.tanh(self.l1(x))
        h = F.tanh(self.l2(h))
        h = F.tanh(self.l3(h))
        h = self.l4(h)

        return h
```

#### パラメータ
バッチサイズが200、GradientHardClippingに与える制限の絶対値が3、エポック数が100です。

```py
batch_size = 200
bound = 3
n_epoch = 100
```

#### データセット

```py
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)
```

#### 各手法
最適化手法はAdaBound、正規化手法はGradientHardClippingを用います。GradientHardClippingの引数については前述した説明の通りです。

```py
net = Net()

optimizer = optimizers.AdaBound()
optimizer.setup(net)

for param in net.params():
    if param.name != "b":
        param.update_rule.add_hook(GradientHardClipping(-bound, bound))
```

#### 学習開始

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

train_iter.reset()

for epoch in range(n_epoch):
    print("\r%d/%d" % (epoch + 1, n_epoch), end="")
    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 two_plot(y, xlabel, ylabel, title, save_dir):
    fig = plt.figure(figsize=(10, 10))
    plt.plot(y[0], label="train")
    plt.plot(y[1], label="valid")
    plt.minorticks_on()
    plt.grid(which="major")
    plt.grid(which="minor", linestyle="--")
    plt.title(title)
    plt.xlabel(xlabel)
    plt.ylabel(ylabel)
    plt.legend()
    plt.savefig(save_dir + "/" + title)
    plt.clf()
    plt.close()

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

two_plot(
    [results_train["MSE"], results_valid["MSE"]],
    "epoch",
    "mean squared error",
    "mean squared error each epoch",
    "fig/model"
)

two_plot(
    [results_train["MAE"], results_valid["MAE"]],
    "epoch",
    "mean absolute error",
    "mean absolute error each epoch",
    "fig/model"
)
```

結果はこちら。

![mean squared error each epoch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/bd2ff078-b6dd-829e-ca3f-5fc6122f9bc7.png)
![mean absolute error each epoch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/63597a0e-5e9a-1edc-99a3-36c0d0c72d2f.png)

#### 精度確認

```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: 80.2214
test MAE: 5.8251
```

[機械学習での最高記録](https://qiita.com/tt_and_tk/items/35ba790f0f285b170bb9#%E5%B9%B3%E5%9D%87%E6%A1%88)は、各ターンごとの平均絶対誤差の平均を求めると約13ですので、深層学習の方が高い精度を誇っています。

#### モデル保存

```py
chainer.serializers.save_npz("deep_AI.net", net)
```

# フルバージョン

https://github.com/TTaisei/osero/tree/main/deep_learn
# 次回は
やっと精度の高いモデルが完成しましたので、これと対戦できるプログラムを作成します。

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

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