0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-23

前回

今回の目標

勝敗予測の精度を上げる

使用バージョン

  • Java 17.0.1
  • MyNet 1.2.4

ここから本編

ディレクトリ構成

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

01 とりあえず、様々なパターンのネットワークを試してみる

まず、前回の記事の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」と表しています。

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)

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

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

実行結果はこちら。
image.png

Ridgeでやった時よりも低い誤差になっています。
続いて、学習度が大きい順に並べます。
image.png

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

image.png

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

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

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

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

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");
    }
}

グラフ化プログラム。

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

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

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

print(df["loss"].min())
print(df["valLoss"].min())
7.951017
8.41695

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

03 誤差関数を変えてみる

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

df_14_epoch1000_MSE.png

最小誤差はこちら。

18140.143332
18160.186681

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

134.68534935916378
134.75973686899215

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

04 02のネットワークをいじる

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

df_14_epoch1000_bigdata.png

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

df_14_epoch1500_bigdata.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

理由は全く分かりませんが、まったく学習しませんでした。
中間層のノード数を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();

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

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

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

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

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

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

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

07 06のネットワークのエポック数を増やしてみる

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

70_history.png

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

8.076857
9.334738

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

まとめ

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

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

7.951017
8.41695

フルバージョン

dataフォルダはアップロードしていません。

次回は

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

次回

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?