コンピュータとオセロ対戦48　～勝敗予測、改善～

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

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

# 今回の目標
勝敗予測の精度を上げる
# 使用バージョン
* Java 17.0.1
* MyNet 1.2.4
# ここから本編
## ディレクトリ構成

```
48
├── data   // 学習データの保存
└── pic    // グラフ保存
// プログラムなどはここに置いてある
```

## 01 とりあえず、様々なパターンのネットワークを試してみる
まず、[前回](https://qiita.com/tt_and_tk/items/22641b036d3b1e5fdbca)の記事のRun.javaのみ変更して、様々な条件で学習させてみました。
データ集め部分は前回と全く同じです。
学習の際、誤差の意味が分かりやすくなるよう64で割らず、平均絶対誤差を使用しました。
あてずっぽうで調べたところある程度の精度があった3層で、ノード数や活性化関数を変えながら調べてみます。また、バッチサイズも変更しながら学習し、その学習記録をファイル出力します。エポック数は多めに、30にします。

```Run01.java
import java.nio.channels.NetworkChannel;
import java.util.ArrayList;
import java.util.function.BiConsumer;

import org.MyNet.matrix.*;
import org.MyNet.network.*;
import org.MyNet.layer.*;
import org.MyNet.nodes.activationFunction.*;
import org.MyNet.optimizer.*;
import org.MyNet.costFunction.*;

public class Run01 {
    public static void main(String str[]){
        ArrayList<BiConsumer<long[], Boolean>> playMethod = new ArrayList<BiConsumer<long[], Boolean>>();
        playMethod.add(Osero::random);
        playMethod.add(Osero::nHand);
        playMethod.add(Osero::nHandCustom);
        playMethod.add(Osero::nLeast);
        playMethod.add(Osero::nMost);

        OseroData run = new OseroData();
        run.setReadGoal(2, 2);
        run.dataClear();

        for (BiConsumer<long[], Boolean> black: playMethod){
            for (BiConsumer<long[], Boolean> white: playMethod){
                run.setPlayMethod(black, white);
                run.addData(1);
            }
        }

        // Deep Learning
        Matrix X = new Matrix(run.history);
        Matrix T = new Matrix(run.result);
        // T.div(64.0);

        Input[] inputLayers = {
            new Input(150, AF.TANH), new Input(150, AF.SIGMOID), new Input(150, AF.RELU),
            new Input(100, AF.TANH), new Input(100, AF.SIGMOID), new Input(100, AF.RELU)
        };
        Dense[] denseLayers = {
            new Dense(100, AF.TANH), new Dense(100, AF.SIGMOID), new Dense(100, AF.RELU),
            new Dense(50, AF.TANH),  new Dense(50, AF.SIGMOID),  new Dense(50, AF.RELU)
        };

        int i = 1;
        for (Input input: inputLayers){
            System.out.printf("\r%d/%d", i, inputLayers.length);
            for (Dense dense: denseLayers){
                Network net = new Network(
                    192,
                    input,
                    dense,
                    new Output(1, AF.RELU)
                );
                Adam opt = new Adam(net, new MeanAbsoluteError());

                for (int batch = 10; batch <= 30; batch += 10){
                    String fileName = "data/01/";
                    fileName += "node" + Integer.toString(net.layers[0].nodes_num) + " ";
                    fileName += "act" + net.layers[0].nodes[0].aFunc.toString() + " ";
                    fileName += "node" + Integer.toString(net.layers[1].nodes_num) + " ";
                    fileName += "act" + net.layers[1].nodes[0].aFunc.toString() + " ";
                    fileName += "batch" + Integer.toString(batch);

                    opt.fit(X, T, 30, X.row / batch, fileName+".csv");
                }
            }

            i++;
        }

        System.out.println();
    }
}
```

結果を以下のプログラムでまとめました。
ここで、学習度という尺度を導入しました。これは、前回の学習で、エポック数が進んでも誤差があまり変わらなかったため、誤差の変化具合を調べる尺度が必要になると考えたためです。
具体的には、各エポックで「(前回の誤差-今回の誤差)/前回の誤差の二乗」を行い、その総和で優劣を比較します。今回全てのエポック数が同じなので平均などの操作はしていません。
なぜ分母で二乗しているかというと、誤差が大きい場合に学習度が大きくなることを防ぐためです。
誤差が大きすぎず、また、エポック数が進むと誤差がだんだん小さくなるものほど、この学習度は大きくなります。
プログラム内では「learning_degree」または「ld」と表しています。

```py
import glob
import re
import pandas as pd

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

# 学習度計算
def cal_learning_degree(loss_history):
    ld = 0
    
    for i in range(len(loss_history)-1):
        ld += (loss_history[i] - loss_history[i+1]) / (loss_history[i] * loss_history[i])

    return ld

# ファイル名から各層のノード数などの情報を抽出
def information(file_name):
    num = re.findall(r"\d+", file_name)
    act_ = re.findall(r"(ReLu)|(Sigmoid)|(Tanh)", file_name)
    act = []
    for a in act_:
        for name in a:
            if name:
                act.append(name)
    return num, act

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

full_data = []

for file_name in glob.glob("data/01/*.csv"):
    data = {}
    info = information(file_name)
    data["input_nodes_num"] = info[0][0]
    data["dense_nodes_num"] = info[0][1]
    data["batch_size"] = info[0][2]
    data["input_act"] = info[1][0]
    data["dense_act"] = info[1][1]

    with open(file_name, "r") as fp:
        data["history"] = pd.read_csv(fp)["loss"]
    
    data["ld"] = cal_learning_degree(data["history"])
    data["min_loss"] = data["history"].min()
    full_data.append(data)

df = pd.DataFrame(full_data)
```

誤差が小さい順に並べてみます。

```py
df_sorted = df.sort_values(by="min_loss", ascending=True)
df_sorted.head(10)
```

実行結果はこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/2109c5ea-c8b5-f6c5-47ad-0e2b9703e49a.png)

Ridgeでやった時よりも低い誤差になっています。
続いて、学習度が大きい順に並べます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/71fa4d36-ca01-cd03-c810-7d3b6bf3bfe5.png)

学習度と誤差の相関関係を調べたところ、以下のようになっていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/ef588dfe-553b-cc7f-b783-ec73efd97f3c.png)

負の相関になっていますので、一応「学習度が高いほど誤差は小さくなる」といえると思います。ただ、今回は雑に学習度の計算方法を設定したのでどの程度有効であるかは分かりません。分母を二乗していることが効いてこのような結果になっているだけかもしれません。

ここで、学習率がある程度高く、かつ誤差もある程度小さい14番のネットワークについて、グラフを作ってみました。

```py
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np


df_14 = pd.read_csv("data/01/node100 actReLu node50 actSigmoid batch30.csv")

sns.set()

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

plt.plot(df_14["loss"])
plt.savefig("pic/df_14.png")
plt.clf()
plt.close()
```

![df_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/512e9f0a-be94-c9d9-d823-cec67698a5fa.png)

一応学習はしてますが・・・。
いいのか悪いのかわからないですね。

## 02 df_14の、エポック数を増やしてみる
01でグラフ化したもので、エポック数を増やしてどこまで精度が上がるか見てみます。
条件は同じで、一気に1000エポックやります。

```Run02.java
import java.nio.channels.NetworkChannel;
import java.util.ArrayList;
import java.util.function.BiConsumer;

import org.MyNet.matrix.*;
import org.MyNet.network.*;
import org.MyNet.layer.*;
import org.MyNet.nodes.activationFunction.*;
import org.MyNet.optimizer.*;
import org.MyNet.costFunction.*;

public class Run02 {
    public static void main(String str[]){
        ArrayList<BiConsumer<long[], Boolean>> playMethod = new ArrayList<BiConsumer<long[], Boolean>>();
        playMethod.add(Osero::random);
        playMethod.add(Osero::nHand);
        playMethod.add(Osero::nHandCustom);
        playMethod.add(Osero::nLeast);
        playMethod.add(Osero::nMost);

        OseroData run = new OseroData();
        run.setReadGoal(2, 2);

        // train data
        run.dataClear();
        for (BiConsumer<long[], Boolean> black: playMethod){
            for (BiConsumer<long[], Boolean> white: playMethod){
                run.setPlayMethod(black, white);
                run.addData(1);
            }
        }
        Matrix X = new Matrix(run.history);
        Matrix T = new Matrix(run.result);

        // val data
        run.dataClear();
        run.setRandom(100);
        for (BiConsumer<long[], Boolean> black: playMethod){
            for (BiConsumer<long[], Boolean> white: playMethod){
                run.setPlayMethod(black, white);
                run.addData(1);
            }
        }
        Matrix valX = new Matrix(run.history);
        Matrix valT = new Matrix(run.result);

        Network net = new Network(
            192,
            new Input(100, AF.RELU),
            new Dense(50, AF.SIGMOID),
            new Output(1, AF.RELU)
        );
        Adam opt = new Adam(net, new MeanAbsoluteError());
        opt.fit(X, T, 1000, X.row / 30, valX, valT, "data/02/history1000.csv");
    }
}
```

グラフ化プログラム。

```py
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

sns.set()

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

df = pd.read_csv("data/02/history1000.csv")
plt.plot(df["loss"], label="train")
plt.plot(df["valLoss"], label="val")
plt.legend()
plt.savefig("pic/df_14_epoch1000.png")
plt.clf()
plt.close()
```

![df_14_epoch1000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/649bc9f3-5612-8eee-a3f6-7e66a56b013a.png)

全く同じ条件で学習させているはずですが、最初の誤差が非常に大きい・・・。
おそらくvalを追加したことで別のfitメソッドが呼ばれ、それによってランダム値が変わってしまったのだと思いますが、それにしてもここまで変わるものなのでしょうか。

最小値を調べたところ以下のようになりました。

```py
print(df["loss"].min())
print(df["valLoss"].min())
```

```
7.951017
8.41695
```

まあ、悪くはないですね。

## 03 誤差関数を変えてみる
02の誤差関数を平均二乗誤差に変えてみます。変更点はそれだけです。
結果はこちら。

![df_14_epoch1000_MSE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/f09be19a-41f1-2724-5b50-21288f828c5a.png)

最小誤差はこちら。

```
18140.143332
18160.186681
```

平均絶対誤差と単位を合わせるため平方根に入れると以下のようになりました。

```
134.68534935916378
134.75973686899215
```

平均絶対誤差よりも、誤差も大きいし収束も遅い結果に。
以前作成した足し算機とは逆の結果になりました。

## 04 02のネットワークをいじる
データ量を増やすと誤差も増えますが、02で示されたようにエポック数を増やすことで学習が進むのなら、結果的に低い誤差となるかもしれないと考えました。
データ量を02の10倍にし、エポック数はとりあえず1000で学習させてみました。他の条件は02と全く同じです。

![df_14_epoch1000_bigdata.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/1702e7fa-0ecf-d4eb-dd56-f6594dfcc036.png)

もう少し行けそうですね、1500エポックいきましょう。

![df_14_epoch1500_bigdata.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/b6d4fc45-7ffe-5a8e-660c-a3bf1e2a9a27.png)

```
9.124575
9.160001
```

1000エポック学習したモデルを保存しておけばよかったです。
そうすれば、追加で500エポック学習するだけで済んだのに・・・。
それはともかく、結果としては悪くないものになりました。少ないデータ量で学習させたものと比べ、最終的な誤差は大きいですが、おそらく汎用的に対応できるのではないかと思います。

## 05 02のネットワークのハイパーパラメータをいじる
少し話は変わりますが、教師データの正解値は平均して32ぐらいだと思います（盤面が64あるため）。
ここで02で使ったネットワークは、2層目の活性化関数がSigmoid、1層目ではReLuです。2層目は直接出力層につながる部分なので、0～1の値をとるSigmoidが適しているのかもしれません。ノード数50ですので、出力層へ50個の「0～1をとる実数」が入ってくるわけです。「0~64」の数字を作りやすい環境が整っているように見えます。
また1層目は、2層目でワンクッション置かれて出力層へ行くので、一般的によく使われるReLuが適していたのかもしれません。
そこで、2層目のノード数を64にして実験してみました。
エポック数は、1000も必要なさそうなので800とします。

結果はこちら。

![df_14_dense64.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/0fa9796c-7e38-97f4-4a43-b7eeb9321813.png)

理由は全く分かりませんが、まったく学習しませんでした。
中間層のノード数を64から63に変更し、10エポック学習させたところ以下のようになりました。

```
Epoch 1/10
loss: 18.1522 - valLoss: 17.9603
Epoch 2/10
loss: 17.9802 - valLoss: 17.7846
Epoch 3/10
loss: 17.8455 - valLoss: 17.6484
Epoch 4/10
loss: 17.7280 - valLoss: 17.5322
Epoch 5/10
loss: 17.6211 - valLoss: 17.4280
Epoch 6/10
loss: 17.5218 - valLoss: 17.3322
Epoch 7/10
loss: 17.4281 - valLoss: 17.2425
Epoch 8/10
loss: 17.3391 - valLoss: 17.1580
Epoch 9/10
loss: 17.2538 - valLoss: 17.0773
Epoch 10/10
loss: 17.1716 - valLoss: 16.9998
```

次に、65に変更したところ以下のようになりました。

```
Epoch 1/10
loss: 129.8353 - valLoss: 129.8753
Epoch 2/10
loss: 129.8254 - valLoss: 129.8654
Epoch 3/10
loss: 129.8153 - valLoss: 129.8553
Epoch 4/10
loss: 129.8049 - valLoss: 129.8449
Epoch 5/10
loss: 129.7944 - valLoss: 129.8344
Epoch 6/10
loss: 129.7838 - valLoss: 129.8238
Epoch 7/10
loss: 129.7729 - valLoss: 129.8129
Epoch 8/10
loss: 129.7619 - valLoss: 129.8019
Epoch 9/10
loss: 129.7508 - valLoss: 129.7908
Epoch 10/10
loss: 129.7395 - valLoss: 129.7795
```

ノード数が2違うだけなのに、ここまでの誤差が生じることも不思議ですが、64で全く学習しないのに63と65では若干学習が進んでいることも不思議です。

## 06 中間層のノード数
中間層のノード数を、5から100まで5刻みで調べてみます。
Javaのプログラムは学習部分のみ載せます。

```Run06.java
        int i = 1;
        for (int nodesNum = 5; nodesNum <= 100; nodesNum += 5){
            System.out.printf("\r%d", i);
            Network net = new Network(
                192,
                new Input(100, AF.RELU),
                new Dense(nodesNum, AF.SIGMOID),
                new Output(1, AF.RELU)
            );
            Adam opt = new Adam(net, new MeanAbsoluteError());
            opt.fit(X, T, 100, X.row / 30, valX, valT,
            "data/06/history"+Integer.toString(nodesNum)+".csv");
            // opt.fit(X, T, 10, X.row / 30, valX, valT);
            // net.save("df_14_dense64.net");
            i++;
        }
        System.out.println();
```

この結果を、以下のプログラムで見てみます。

```py
import glob
import re

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

sns.set()

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

def cal_learn_degree(arr):
    ld = 0
    for i in range(len(arr)-1):
        ld += (arr[i] - arr[i+1]) / (arr[i] * arr[i])

    return ld

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

full_data = []

for filename in glob.glob("data/06/history*.csv"):
    data = {}
    with open(filename, "r") as fp:
        df = pd.read_csv(fp)
    middle_num = re.findall(r"\d+", filename)
    data["nodes_num"] = int(middle_num[1])
    data["loss"] = list(df["loss"])
    data["val_loss"] = list(df["valLoss"])
    data["loss_min"] = df["loss"].min()
    data["val_loss_min"] = df["valLoss"].min()
    data["ld"] = cal_learn_degree(data["val_loss"])
    full_data.append(data)

full_data = sorted(full_data, key=lambda x: x["nodes_num"])

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

# 訓練と確認それぞれでの最小誤差を、ノード数毎に並べたグラフを作成。
x = np.array(list(map(lambda x: x["nodes_num"], full_data)))
y = list(map(lambda y: y["loss_min"], full_data))
val_y = list(map(lambda y: y["val_loss_min"], full_data))
width = 2

plt.bar(x-width/2, y, label="train", width=width)
plt.bar(x+width/2, val_y, label="val", width=width)
plt.legend()
plt.xlabel("Number of middle layer's nodes")
plt.ylabel("Mean absolute error")
plt.savefig("pic/middle_experiment.png", dpi=150)
plt.clf()
plt.close()
```

結果はこちら。

![middle_experiment.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/06ece706-fefe-3166-e3e3-859acb2c8d81.png)

ノード数と誤差の間に、因果関係はあまりなさそうなデータが取れてしまいました。
また、この中では35が最も小さな誤差になっているのが気になったので、35の時の学習推移を見てみました。

```py
loss_35 = [y["loss"] for y in full_data if 35 == y["nodes_num"]][0]
val_loss_35 = [y["val_loss"] for y in full_data if 35 == y["nodes_num"]][0]

plt.plot(loss_35, label="train")
plt.plot(val_loss_35, label="val")
plt.legend()
plt.xlabel("Number of epoch")
plt.ylabel("Mean absolute error")
plt.savefig("pic/middle_experiment_35.png", dpi=150)
plt.clf()
plt.close()
```

![middle_experiment_35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/3fa1994e-4fde-73d2-e0cd-f89ba48e2028.png)

・・・。
学習度が最大になったのはノード数が何の時だったかを調べます。

```py
top_ld = sorted(full_data, key=lambda x: x["ld"], reverse=True)[0]

print(top_ld["nodes_num"])
```

```
70
```

ノード数70のとき、もっとも学習度が高かったようです。
上の棒グラフを見ると、最小誤差はいい感じです。
学習履歴を、ノード数35のグラフを作ったものとほぼ同じプログラムでグラフ化しました。

![middle_experiment_70.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/f35f74d1-6f1c-906b-3771-ac9d2b4ddd36.png)

確かに減ってはいるので学習経過はいい感じですが、、、
もう少し制度上がりそうですね。

## 07 06のネットワークのエポック数を増やしてみる
上でグラフ化した、もっとも学習度が高かったネットワークのエポック数を増やそうと思います。
プログラムはすべて省略します。
実行結果はこちら。

![70_history.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/ffbec631-c040-23d2-58b7-fee549681b54.png)

訓練データと確認データの誤差はそれぞれ以下の通り。

```
8.076857
9.334738
```

02のものより若干悪い結果になりました。

## まとめ
結局02が最も優秀という結果になりましたので、02のネットワークを保存しました。
エポック数は1000です。

訓練データと確認データの平均絶対誤差をもう一度載せておきます。

```
7.951017
8.41695
```

# フルバージョン
dataフォルダはアップロードしていません。

https://github.com/TTaisei/osero/tree/main/combinationAI/48

# 次回は
今回作成したネットワークを利用して探索を行い、最善手に置くAIを作成します。

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

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