tensorflowのC++APIで学習をさせる

matrices.io

 上の記事を参考にC++で3層ニューラルネットワークの学習を書いた。2変数の2次形式で表される関数を近似するというプログラムになる。

 基本的にはコメントの通りだが、Tensorからデータを取り出す、Tensorにデータを入れる部分に関してはよくわかっていない。matrixやscalarなど、適切な形と型を指定して取り出し、それに対して(i, j)でアクセスするということなのだと理解している。

//これがないとエラーになる
#define COMPILER_MSVC

#include "tensorflow/cc/client/client_session.h"
#include "tensorflow/cc/ops/standard_ops.h"
#include "tensorflow/core/framework/tensor.h"
#include "tensorflow/cc/framework/gradients.h"

using namespace tensorflow;
using namespace tensorflow::ops;

double targetFunction(double x1, double x2) {
    return 2.0 * x1 * x2 - 2.4 * x1 * x1 + 3.1 * x2 * x2 + 2.7 * x1 - 10.0 * x2 + 5.0;
}

int main() {
    int unit_num;
    std::cout << "ユニット数 : ";
    std::cin >> unit_num;
    float learning_rate;
    std::cout << "学習率 : ";
    std::cin >> learning_rate;
    int batch_size;
    std::cout << "バッチサイズ : ";
    std::cin >> batch_size;
    int epoch_num;
    std::cout << "エポック数 : ";
    std::cin >> epoch_num;

    Scope scope = Scope::NewRootScope();

    //入力,教師データのPlaceholder
    auto x = Placeholder(scope, DT_FLOAT);
    auto y = Placeholder(scope, DT_FLOAT);

    //中間層への重み、バイアス
    auto w1 = Variable(scope, { unit_num, 2 }, DT_FLOAT);
    auto assign_w1 = Assign(scope, w1, RandomNormal(scope, { unit_num, 2 }, DT_FLOAT));
    auto b1 = Variable(scope, { unit_num, 1 }, DT_FLOAT);
    auto assign_b1 = Assign(scope, b1, RandomNormal(scope, { unit_num, 1 }, DT_FLOAT));

    //出力層への重み、バイアス
    auto w2 = Variable(scope, { 1, unit_num  }, DT_FLOAT);
    auto assign_w2 = Assign(scope, w2, RandomNormal(scope, { 1, unit_num }, DT_FLOAT));
    auto b2 = Variable(scope, { 1, 1 }, DT_FLOAT);
    auto assign_b2 = Assign(scope, b2, RandomNormal(scope, { 1, 1 }, DT_FLOAT));

    //中間層
    auto hidden_layer = Relu(scope, Add(scope, MatMul(scope, w1, x), b1));

    //出力層
    auto output_layer = Add(scope, MatMul(scope, w2, hidden_layer), b2);

    //損失
    auto loss = ReduceMean(scope, Square(scope, Sub(scope, output_layer, y)), {0, 1});

    //勾配
    std::vector<Output> grad_outputs;
    TF_CHECK_OK(AddSymbolicGradients(scope, { loss }, { w1, w2, b1, b2 }, &grad_outputs));

    //勾配降下を各変数に適用
    auto apply_w1 = ApplyGradientDescent(scope, w1, Cast(scope, learning_rate, DT_FLOAT), { grad_outputs[0] });
    auto apply_w2 = ApplyGradientDescent(scope, w2, Cast(scope, learning_rate, DT_FLOAT), { grad_outputs[1] });
    auto apply_b1 = ApplyGradientDescent(scope, b1, Cast(scope, learning_rate, DT_FLOAT), { grad_outputs[2] });
    auto apply_b2 = ApplyGradientDescent(scope, b2, Cast(scope, learning_rate, DT_FLOAT), { grad_outputs[3] });
    
    //入力、教師データのPlaceholderに流す実際のデータ
    Tensor x_data(DT_FLOAT, TensorShape{ 2, batch_size });
    Tensor y_data(DT_FLOAT, TensorShape{ 1, batch_size });

    //x_dataとy_dataにbatch_size分のランダムデータを格納する関数
    auto setData = [&x_data, &y_data](int batch_size) {
        static std::random_device seed_gen;
        static std::default_random_engine engine(seed_gen());
        static std::uniform_real_distribution<> dist(-10.0, 10.0);

        for (int i = 0; i < batch_size; i++) {
            double x1 = dist(engine), x2 = dist(engine);
            x_data.matrix<float>()(0, i) = (float)x1;
            x_data.matrix<float>()(1, i) = (float)x2;

            y_data.matrix<float>()(0, i) = (float)targetFunction(x1, x2);
        }
    };

    //出力を受け取るための変数
    std::vector<Tensor> outputs;

    //セッションを作成
    ClientSession session(scope);

    //重みとバイアスを初期化
    TF_CHECK_OK(session.Run({ assign_w1, assign_w2, assign_b1, assign_b2 }, nullptr));

    for (int e = 0; e <= epoch_num; e++) {
        //検証
        setData(batch_size);
        TF_CHECK_OK(session.Run({ { x, x_data }, { y, y_data } }, { loss }, &outputs));

        printf("epoch = %5d, loss = %12.1f\n", e, outputs[0].scalar<float>()());
        if (e == epoch_num) {
            break;
        }

        //学習
        setData(batch_size);
        TF_CHECK_OK(session.Run({ { x, x_data },{ y, y_data } }, { apply_w1, apply_b1, apply_w2, apply_b2 }, nullptr));
    }
}

 簡単に実験した設定は以下のような値となる。

変数
ユニット数 1000
学習率 0.00001
バッチサイズ 100000
エポック数 50000

 最初100エポックの学習曲線は次の通り(縦軸は対数スケール)。順調に学習できていることがわかる。

f:id:tokumini:20180615123600p:plain

 50000エポックほど回すと最終的に損失は39.7になった。

 現状C++APIを使う上での問題点は

  • 変数のsave/store方法がわからない
  • ノードに名前を付ける方法がわからない
  • コンパイルに時間がかかる

などであり、現実的に運用していくにはまだ課題が多い。

評価関数の追加学習2

 前回から自己対局による強化学習を行ってます。今回も前回と同じ条件で引き続き学習を継続させてみました。

 以下が学習前のパラメータに対する学習後のパラメータの対局結果となります。

モデル 対局数 勝利 引き分け 敗北 勝率
3回アップデート後 1736 828 154 754 52.1
6回アップデート後 422 206 47 169 54.4
8回アップデート後 1082 601 118 363 61.0

 8回アップデートが起きてからは勝率が5割以下でずっと推移するようになってしまったので学習を打ち切りました。

 8回アップデート時点では学習前に対してEloレート差約77.7となりました。

 強くなってはいるのですが、アップデートをかける条件が勝率55%であることを考えると、アップデート回数に対して伸びが小さいようにも見えます。100局ごとの勝率測定では誤差も大きく、強くなっていないのにアップデートがかかる場合もあることが原因なのではないかと思われますが、詳しくは不明です。

 8回アップデートしたものとWCSC28版を対局させてみました。探索部側にも変更があるので純粋な評価関数の比較とはなりませんが、現時点でのkkp_kpptでの最強バージョンとWCSC28版(kp_pp)との対局ということになります。

 1手2秒で対局させた結果(kkp_kppt視点)が以下となります。

対局数 勝利 引き分け 敗北 勝率
1075 378 47 650 37.3

 Eloレートでは約-90.2です。そこそこ強くなってきてはいるんですが、まだ足りてませんね。この段階で伸びが止まってしまったのが悩みどころです。学習時のハイパーパラメータをいじればまだ伸びるか、次元下げを導入することで伸びてくれるか、なんにせよもっと学習部をいじる必要がありそうです。

評価関数の追加学習

 前回は入玉対策として手動で適当にパラメータを変更しました。今回はそれを基に自己対局からの強化学習を行ってみます。

 100局単位で学習を行い、指数移動平均を取った勝率が55%を超えたら強くなったとしてパラメータをアップデートします。

 局面の多様性を確保するために最初の10手は各合法手を1手読みした値について温度を200としたボルツマン分布から手をサンプリングしています。

 教師局面を生成する探索深さは3で、学習率は0.2から100回連続アップデートが起こらなかったら1/2ずつにしていくという方式を取りました。

 12時間ほど学習させた指数移動平均勝率の推移は次のようになります。

f:id:tokumini:20180531120849p:plain

 30%になっている直前で一度アップデートが発生しているため、今回は147回後に1回アップデートが発生し,その後36回後にまたアップデートが発生したということになります。最初のアップデートまで100回以上過ぎているため学習率0.2は大きすぎ、学習率が0.1になってからでは47回および36回でアップデートが発生しているため、適切なのではないかと感じます。

 実際にこの評価関数を用いて学習前の評価関数と1スレッド1手2秒で対局させてみたところ、新しいパラメータ側から見て

対局数 勝利 引き分け 敗北 勝率(引き分けは0.5勝0.5敗)
606 355 77 174 64.9%

 となりました。レート差にして約106.8といったところでしょうか。うまく学習できていることがわかります。

 これでもまだWCSC28バージョンにはおそらく届かないくらいの棋力であり、自己対局のみによる学習の難しさを感じます。

 余談ですが、この自己対局検証はShogiGUIを4つ並列に起動して行いました。そこで気になる点として、それぞれで大きく勝率が違ったことが挙げられます。実際に606局の内訳は

番号 対局数 勝利 引き分け 敗北 勝率(引き分け除外)
1 147 85 26 36 70%
2 151 83 21 47 63%
3 149 97 14 38 71%
4 159 90 16 53 62%

 となっていました。

 ShogiGUI上では連続対局中、引き分けを除外した勝率が表示されるのですが、150局近い段階でもここまで大きくぶれるとなると少なくとも500局ほどは欲しいのかなと感じます。このぶれは、検証の対局においても6手までSoftmaxランダムで指させている影響が大きいのかもしれません。1スレッドで行っている関係上、指し手のランダム性が低いように思えるため初期局面でのばらつきを大きくしているわけですが、ここもちゃんと検証するべきポイントですね。

 学習中における100局ずつでの勝率計算もどれほど信用できるのか怪しい気もしてくるため、指数移動平均を取る係数を調節するなど工夫が必要かもしれません。推移グラフを見てもかなり不安定で、運の偏りによって55%を超えることもあるのかなと思えます。

 また学習用自己対局の生成速度ももっと改善したいところです。今回は約12時間で約2万局だったため、おおそよ0.46局/秒ほどの速度でした。枝刈り等の問題なのか探索深さを4などにすると相当遅くなってしまい、8やら10やらというのは夢のような話となっています。実際の対局にも関わってくる部分ですし、なんとか高速化はしていきたいところです。

入玉に関する点数付け

 kkp_kpptの特徴量に変えてからいくらか改良をして自己対局をさせているところですが、対局内容を見ていると相手玉を上部に逃がしても駒得だけを考慮して優勢と主張するシーンが多くみられました。まだ宣言勝ち機能を導入していないこともあり、また256手を超える自己対局は引き分けとして学習データに含めないようにしているため、これらを自己対局から学習するのは難しいのではないかと思います。

 よって多少不本意ではありますが、手動でkppの玉が5段目から1段目までについて点数を付けることで改善されるのではないかと考えました。具体的には5段目以上のkを含む各kppに対してセンチポーン単位で(6 - 段数)点を加点するように直接kppパラメータに変更を加えることで、相手玉を上部に逃がすことを抑制し、また自玉は上部に逃げていくことを期待します。

 pは38要素あるので、1局面についてkppは{}_{38} C_2 = 703要素が足されます。つまり変更前に比べて5段目では703点、4段目では1406点、...、1段目では3515点多く評価することになります。

 1手1秒で対局させました。変更後のバージョンから見た結果は次のようになります。

対局数 勝利数 引き分け数 敗北数
721 344 75 302

 引き分けを0.5勝0.5敗と考えると勝率は約52.9%で、レート差としては20.2ほどとなります。

 ほんの少しですが強くなったので、この評価パラメータをベースに学習を進めていこうと思います。

AtCoder Grand Contest 024 参加ログ

結果

 A,B,Cの3完で597位。C問題で7WAも出してしまったのが反省点だけど、簡単めとはいえ700点問題を通せたので個人的には悪くない結果だと思った。

 パフォーマンスは1621、レートは1598(+3)で、惜しくも青コーダー復帰とはならず。 f:id:tokumini:20180521093647p:plain

思考ログ

 解法はほぼ公式の解説PDFの通りなので、そこに至るまでの思考過程を中心に記録。

A問題

 300点問題なのでちょっとひねるのかなとは思いつつ、まずは愚直に式変形をしてみるとそのまま法則性が見えたのですぐAC。単純に設問の操作通り式変形して法則性が見える問題というのも多くはないだろうけど、初手としてやる価値はあるんだろう。

B問題

 抽象的に考えるのは難しそうだったので入力例を紙の上で解いてみてまずは解法を探る。

 やっていると、先頭に入れていく数字は1ずつ小さくする、末尾に入れていく数字は1ずつ大きくすることで整列した形で入れられることに気づく。そうなると挿入しないでも整列している数字が何個あるかがわかれば解けそうだという考えに至った。

 その後すぐは、もとの数列内で単調増加する最長の部分列かなと考えたけど、入力例3などでそれはダメ。で、ちょっと考えているとより厳しく1ずつ増えていく最長の部分列じゃないかという発想に至り、理屈を考えてもそれでうまくいきそうだと確認した。つまり先頭にx, x-1, ..., 1、末尾にy, y+1, ..., Nを入れていくことになるわけで、x+1 から y-1がもとの数列で整列していれば良いということ。

 あとはその実装なんだけど、競技プログラミングに慣れて一番伸びたのはこういう実装がすぐにできるところなんじゃないかと思う。

num[i] := iから始まる1ずつ増加する部分列の長さ

として、数列の後ろから見ていけばいいと考えてAC。25分弱で解けてなかなか良いペースだと感じた。

#include"bits/stdc++.h"
using namespace std;

int main() {
    int N;
    cin >> N;
    vector<int> P(N);
    for (int i = 0; i < N; i++) {
        cin >> P[i];
    }

    vector<int> num(N + 2, 0);
    for (int i = N - 1; i >= 0; i--) {
        num[P[i]] = num[P[i] + 1] + 1;
    }

    cout << N - *max_element(num.begin(), num.end()) << endl;
}

C問題

 やっぱり入力例を手で解いていくわけだけど、まず先頭は0でないといけないということはすぐわかる。また連続した2つの差が2以上あるのもダメだなというのは気づいた。

 これで多分ダメなケースは弾けたと感じて最小回数の方を考えてみると、やはり後ろから山を作っていくイメージで最小になるんじゃないかと考えた。

 最終的にみるとこれでおおむね合っていたわけだけど、最小回数を求める方の実装がバグっておりWA。しかしダメな例の判定の方がバグっていそうと感じてそっちを修正しに行ってしまった。0の次に2が来る場合だけとか、0からの距離が数字以上に離れていたらダメとか、変なことを考えてWAを重ねていく。

 そのせいで最小回数を求める方のバグに気づいても負例の除去がバグっていてさらにWA。なにもわからなくなってしまいassertを仕込んで負例の除去がちゃんとできているか確認したところ、どうもそれがおかしいと気づいて、一番最初の条件に戻してやっとAC。

 一回WAを出したあと、もうちょっと冷静にどこが間違っているのか考える必要がある。これは今までに何回も思っていることなんだけど、性格にもかかわることでそう簡単に直せるものでもないなぁ。

 数列を後ろから見ていくという発想がB問題と同じだったのが多少幸運だったか。

#include"bits/stdc++.h"
using namespace std;

int main() {
    int64_t N;
    cin >> N;
    vector<int64_t> A(N);
    for (int i = 0; i < N; i++) {
        cin >> A[i];
    }

    if (A[0] != 0) {
        cout << -1 << endl;
        return 0;
    }

    for (int i = 1; i < N; i++) {
        if (A[i] - A[i - 1] > 1) {
            cout << -1 << endl;
            return 0;
        }
    }

    uint64_t ans = 0;
    vector<int64_t> X(N, 0);
    for (int64_t i = N - 1; i >= 1; i--) {
        if (X[i] != A[i]) {
            ans += A[i];
        }
        X[i - 1] = A[i] - 1;
    }

    cout << ans << endl;
}

D問題

 問題の意味さえあまりわからなかった。なんか対称性? を作ればいいのかなという感じに思ったけど、それをグラフとしてどう実装するのかもさっぱりわからず、残り時間は座っているだけだった。1100点という数字を見るだけで思考が停止してしまうようなところがあり、ダメだなぁと感じる。

将棋ソフトの自己対局による強化学習

 評価関数の特徴量をkkp_kpptに変更し、駒割のみのゼロベクトルから自己対局による強化学習を行っています。学習用局面をsfenなどで出力することなく、ある程度の対局数を貯めてミニバッチとして更新しています。教師探索深さは3であり、ミニバッチサイズは100としました。

 このようにするメリットは自己対局によって強くなっていることを確認しながら学習を進められることであり、以下が勝率のグラフとなります。

f:id:tokumini:20180519094517p:plain

 指数移動平均を取っている勝率が52.88%を超えたら対局相手とするパラメータをアップデートしています。並列化の関係でアップデートした直後にも高い勝率が発生してしまうため、アップデートが起きたら平均を取る初期値を30%から再スタートしています。つまり上のグラフで溝の個数がパラメータのアップデート回数であり、今回は

学習時間 対局数 更新回数
7時間51分 21600局 11回

 という結果になりました。まだバグがあるようで、ここでエラーが発生しプログラムが止まってしまいました……。

 52.88%はEloレートでいうと+20であり、これが11回ということで理想的には+220、勝率では78.01%となってほしいところです。これを1手1秒,全100局による自己対局により検証してみると、

勝ち 引き分け 負け
82 13 5

 となりました。引き分けは0.5勝0.5敗とすると勝率は88.5%、Eloレートでいうと354.5の差ということになります。多少高めに出てしまいましたが、もとが駒割りのみということで、そういうこともあるのかもしれません。

 問題はこの先ちゃんと強くなるかどうかで、手元ではあまりうまくいっていません。やはり棋譜を出力していく形式でないと難しいのでしょうか。まだ教師あり学習を施した2駒関係のプログラムには勝ち越せないので、しばらくはそれを超えることを目標にやっていきたいと思います。

SIMDを用いて評価関数計算を高速化したかった話

 コンピュータ将棋において評価関数を計算する際にSIMDによって高速化する手法が知られています。

 基本的には野田さんのブログが詳しいので、深く知りたい方はそちらを参照してください。

 今僕が開発している将棋ソフトでは手番なしの絶対2駒(kp, pp)のみを評価項目として利用しており、パラメータはすべてint型(32bit)で確保しているという違いがあるため、そのままコピペでは動きません。

 命令レベルの高速化なんてさっぱりやったことないのですが、まぁやるしかないようです。

 とりあえずいきなり将棋ソフトに実装する前にテスト用のコードを書いてみました。

#include<vector>
#include<random>
#include<iostream>
#include<chrono>
#include<cassert>
#include<immintrin.h>

using namespace std;
using namespace chrono;

//疑似的なKP, PP
constexpr int K_NUM = 81, P_NUM = 1534;
int kp[K_NUM][P_NUM];
int pp[P_NUM][P_NUM];
constexpr int LIST_SIZE = 38;

int calcByForLoop(int k1, int k2, const vector<int>& list) {
    //forループで計算
    int sum = 0;
    for (int i = 0; i < LIST_SIZE; i++) {
        //KPの計算
        sum += kp[k1][list[i]];
        sum += kp[k2][list[i]];

        for (int j = i; j < LIST_SIZE; j++) {
            sum += pp[list[i]][list[j]];
        }
    }
    return sum;
}

int calcBySIMD(int k1, int k2, const vector<int>& list) {
    //SIMD計算
    int sum = 0;

    //0初期化したものを準備
    __m256i sum256 = _mm256_setzero_si256();
    for (int i = 0; i < LIST_SIZE; i++) {
        //kpはそのまま足す
        sum += kp[k1][list[i]];
        sum += kp[k2][list[i]];

        int j = i;
        for (; j + 8 < LIST_SIZE; j += 8) {
            //まずはlist[j]から8要素ロード
            __m256i indexes = _mm256_load_si256(reinterpret_cast<const __m256i*>(&list[j]));

            //indexesをオフセットとしてppから8要素ギャザー
            __m256i gathered = _mm256_i32gather_epi32(reinterpret_cast<const int*>(&pp[list[i]]), indexes, 4);

            //足し合わせる
            sum256 = _mm256_add_epi32(sum256, gathered);
        }

        for (; j + 4 < LIST_SIZE; j += 4) {
            //list[j]から4要素ロード
            __m128i indexes = _mm_load_si128(reinterpret_cast<const __m128i*>(&list[j]));

            //indexesをオフセットとしてppから4要素ギャザー
            __m128i gathered = _mm_i32gather_epi32(reinterpret_cast<const int*>(&pp[list[i]]), indexes, 4);

            //256bitに拡張
            __m256i expanded = _mm256_insertf128_si256(_mm256_setzero_si256(), gathered, 0);

            //足し合わせる
            sum256 = _mm256_add_epi32(sum256, expanded);
        }
        for (; j < LIST_SIZE; j++) {
            sum += pp[list[i]][list[j]];
        }
    }

    //64bitずらして足し合わせる
    sum256 = _mm256_add_epi32(sum256, _mm256_srli_si256(sum256, 8));

    //128bitずらして足し合わせる
    __m128i sum128 = _mm_add_epi32(_mm256_extracti128_si256(sum256, 0), _mm256_extracti128_si256(sum256, 1));

    //上位64bitを32bitずつにバラして足す
    int sum32[2];
    _mm_storel_epi64(reinterpret_cast<__m128i*>(sum32), sum128);
    sum += sum32[0];
    sum += sum32[1];

    return sum;
}

int main() {
    //適当にkp,ppの値を設定
    random_device seed_gen;
    default_random_engine engine(seed_gen());
    uniform_int_distribution<int> dist_param(-2000, 2000);

    int cnt = 1;
    for (int i = 0; i < K_NUM; i++) {
        for (int j = 0; j < P_NUM; j++) {
            kp[i][j] = dist_param(engine);
        }
    }
    for (int i = 0; i < P_NUM; i++) {
        for (int j = 0; j < P_NUM; j++) {
            pp[i][j] = dist_param(engine);
        }
    }

    //ランダムにK, Pを設定
    uniform_int_distribution<int> dist_K(0, 81);
    uniform_int_distribution<int> dist_P(0, 1534);

    constexpr int num = 1000000;

    for (int i = 0; i < num; i++) {
        int k1 = dist_K(engine), k2 = dist_K(engine);
        vector<int> list(LIST_SIZE);
        for (int i = 0; i < LIST_SIZE; i++) {
            list[i] = dist_P(engine);
        }
        assert(calcByForLoop(k1, k2, list) == calcBySIMD(k1, k2, list));
    }

    double time_of_ForLoop = 0, time_of_SIMD = 0;
    for (int i = 0; i < num; i++) {
        int k1 = dist_K(engine), k2 = dist_K(engine);
        vector<int> list(LIST_SIZE);
        for (int i = 0; i < LIST_SIZE; i++) {
            list[i] = dist_P(engine);
        }

        auto start = high_resolution_clock::now();
        calcByForLoop(k1, k2, list);
        auto end = high_resolution_clock::now();
        auto elapsed = end - start;
        time_of_ForLoop += duration_cast<duration<double>>(elapsed).count();
    }

    for (int i = 0; i < num; i++) {
        int k1 = dist_K(engine), k2 = dist_K(engine);
        vector<int> list(LIST_SIZE);
        for (int i = 0; i < LIST_SIZE; i++) {
            list[i] = dist_P(engine);
        }

        auto start = high_resolution_clock::now();
        calcBySIMD(k1, k2, list);
        auto end = high_resolution_clock::now();
        auto elapsed = end - start;
        time_of_SIMD += duration_cast<duration<double>>(elapsed).count();
    }

    cout << "ForLoop : " << time_of_ForLoop / num << endl;
    cout << "SIMD    : " << time_of_SIMD / num << endl;
}

 値が同じかどうかのチェックは抜けてくれるので計算としては合っているようです。しかし速度の結果は

ForLoop : 1.787e-08
SIMD : 1.566e-08

 と、ものすごく速くなっているわけではない感じでした。キャッシュの影響とかがあるかもしれないので導入してみないとわからないところですが、"NPS全体で約9%の高速化"というほどには至らないような気がします。手番なしの2駒関係だと恩恵が薄いということなのでしょうか。プログラムが遅いと学習にも時間がかかるためできるだけ高速化したいところですが、なかなか上手くはいかないようです。

DeepLearningによる将棋の学習12~7ブロック化~

 前回はResidualBlockを6個に増やしたら性能が上がることを確認しました。

 ならばと今回は7個にして実験してみました。結果は次の通りです。

Epoch Total Loss Policy Loss Value Loss Policy Accuracy Value Accuracy
1 3.6059 3.4564 0.5978 0.2957 0.6376
2 3.1807 3.0401 0.5622 0.3370 0.6677
3 3.0033 2.8649 0.5535 0.3581 0.6759
4 2.8979 2.7613 0.5465 0.3694 0.6809
5 2.8407 2.7050 0.5426 0.3766 0.6856
6 2.8037 2.6686 0.5403 0.3814 0.6893
7 2.7849 2.6495 0.5417 0.3831 0.6886
8 2.7692 2.6336 0.5426 0.3866 0.6889
9 2.7701 2.6347 0.5416 0.3868 0.6898
10 2.7635 2.6285 0.5400 0.3873 0.6925
11 2.7778 2.6427 0.5405 0.3889 0.6914
12 2.7783 2.6424 0.5439 0.3892 0.6895
13 2.7879 2.6518 0.5444 0.3898 0.6908

f:id:tokumini:20180502102246p:plain

 Policyの性能は下がってしまいました。これまでの結果を表にまとめるとこうなります。

ブロック数 Total Loss Policy Loss Value Loss Policy Accuracy Value Accuracy
5 2.4231 2.2876 0.5421 0.3957 0.6874
6 2.3999 2.2634 0.5458 0.4013 0.6883
7 2.7635 2.6285 0.5400 0.3873 0.6925

 Valueは良くなっているようにも見え、またPolicyの一致率はまだ上がりそうであることを考えると、損失比をいじることでまだ改善はできるかもしれません。ブロック数を変えると最適な損失比も変わるとなると実験が大変ですが……。そこまでこだわる精度の差でもない気はするので、選手権後はもっと大きな改善に取り組みたいと思います。

 NN版はリモートで動かせない環境だと思っていたのですが、試行錯誤していると動くようになったので選手権にも合議として参加させる可能性が高くなってきました。ルールの確認等をして問題がなさそうならば線形評価関数版の海底とNN版との楽観合議で参加したいと思います。

DeepLearningによる将棋の学習11~6ブロック化と対局結果~

 前回は現状で最も良いと考えられるモデルで学習させてみました。

 今回はResidualBlockを一つ増やして6個として実験してみました。

f:id:tokumini:20180501110203p:plain

Epoch Total Loss Policy Loss Value Loss Policy Accuracy Value Accuracy
1 3.1680 3.0230 0.5797 0.3130 0.6512
2 2.7987 2.6588 0.5595 0.3539 0.6681
3 2.6395 2.5019 0.5506 0.3677 0.6759
4 2.5260 2.3894 0.5464 0.3832 0.6814
5 2.4690 2.3340 0.5400 0.3911 0.6868
6 2.4389 2.3043 0.5384 0.3939 0.6881
7 2.4099 2.2750 0.5397 0.3983 0.6903
8 2.4086 2.2732 0.5418 0.3995 0.6887
9 2.3999 2.2634 0.5458 0.4013 0.6883
10 2.4045 2.2693 0.5407 0.4032 0.6883
11 2.4163 2.2765 0.5590 0.4033 0.6850
12 2.4187 2.2826 0.5441 0.4025 0.6902

 結果は指し手一致率が40%を超え、良い性能を持つと考えられます。

 ここで海底の最新版(WCSC28へ出すもの:レート1100ほど?)と100局対局させてみました。海底側は1手5秒、NN側は8スレッドで10000回のプレイアウト(という言い方が適切なのかわかりませんが)となっています。この条件だとおおむねNN側も5秒から多くとも7秒ほどで着手するため、それなりに平等な条件となっていると考えられます。

 結果はNN側から見て34勝 2引き分け 64敗でした。引き分けを0.5勝0.5敗と考えてELOレート差を計算すると107.5となりました。思ったより強いという印象を持っています。

 しかし実は純粋にNNだけを利用しているのではなく、Valueを計算している時間を利用して海底が持っている評価関数で静止探索を行いそれらの平均値をValueとしているので、これが多少強くなっている一因なのではないかと感じます。そもそもモンテカルロ木探索自体を線形評価関数に適用してもそれほど悪くないのではと思っており、実験してみたいところです。

DeepLearningによる将棋の学習10~現状の最高性能~

 前回は損失の比を調整することでPolicyの損失曲線とValueの損失曲線がだいたい同じタイミングで底を打つようにできるのではないかということを実験しました。

 今回はそれを踏まえて現状での最高性能を出す条件で実験してみました。

 ResidualBlock5つ、フィルタ数192、フィルタサイズ3、バッチサイズ128で、損失比はPolicy:Value = 1: 0.25、optimizerはNesterovとしました。結果は以下の様になります。

Epoch Total Loss Policy Loss Value Loss Policy Accuracy Value Accuracy
1 3.2068 3.0597 0.5885 0.3082 0.6442
2 2.8337 2.6939 0.5595 0.3465 0.6683
3 2.6670 2.5285 0.5538 0.3637 0.6728
4 2.5773 2.4384 0.5555 0.3755 0.6759
5 2.5006 2.3640 0.5463 0.3846 0.6810
6 2.4791 2.3428 0.5451 0.3869 0.6830
7 2.4457 2.3097 0.5443 0.3912 0.6827
8 2.4357 2.2999 0.5434 0.3948 0.6849
9 2.4231 2.2876 0.5421 0.3957 0.6874
10 2.4307 2.2953 0.5418 0.3977 0.6872
11 2.4323 2.2964 0.5436 0.3984 0.6856
12 2.4357 2.2991 0.5466 0.3971 0.6862

f:id:tokumini:20180429100859p:plain

 Policyの損失が最小になったタイミングとValueの損失が最小になったタイミングに1エポックしかズレがなく、そこそこ良い学習になっているのではないでしょうか。性能としてもPolicyの一致率39.57%はこれまでにない高い確率となっています。SGDからNesterovへ変更した恩恵も大きそうです。

 ちなみに本家dlshogiはWCSC28のアピール文書にあるデータと比較すると(使用データがfloodgateではなくelmoで生成した自己対局データのようですが)

Model Policy Accuracy Value Accuracy
dlshogi 45.6% 78.1%
今回の実験結果 39.6% 68.7%

 とのことで、やはり精度は段違いのように思えます。

 今回の実験で得たモデルを既存の海底最新版と対局させてみたところ、中盤まではかなり良い勝負をする、優位を築くことさえあるとわかりました。しかし、やはり最終盤での詰みが関わってくる局面で弱さを見せ、なかなか勝ち切るには至りませんでした。理想的にはニューラルネットワークの質を高めて終盤にも対応できるようにするのが良いのでしょうが、現実的にはWCSC28のアピール文書でいくらかのチームが提示していたように、終盤では使うエンジンを切り替えるなどの工夫をしていくのが良いのかもしれません。まぁ僕は今回のWCSC28でDeepLearningを使用したエンジンを使う気はないのですが……。