MCTSにおける価値の漸進的更新

結論

 MCTSの行動価値を漸進的に更新する実装で、総和を保持して平均化する実装と同程度の性能を達成できた。

背景

 以前、MCTSにおいて行動価値を漸進的に更新する方法について記事を書いたが、性能が悪化してしまった。この記事で述べた通り、原因はおそらくバーチャルロスを探索した回数Nへ直接足しており、正しい探索回数になっていないためだと思われる。よってバーチャルロスを探索した回数Nとは別に保持するように変更した。

実装

置換表エントリ

 各行動についてバーチャルロスを保持する変数を置換表エントリに加える。単純にこの変数を増やしただけだと使用メモリ量が増えてしまうが、行動価値を漸進的に更新する場合、子孫ノードの価値の総和Wを保持する必要がなくなる。結局使用メモリ量はほぼ同じとなる。置換表エントリの主要なデータは次のようになる。std::vectorで保持されているもののサイズは合法手の数と一致する。

struct UctHashEntry {
    int32_t sum_N;                    //この局面から探索した回数
    int32_t virtual_sum_N;            //バーチャルロスの合計
    std::vector<Move> moves;          //この局面の合法手
    std::vector<Index> child_indices; //子局面の置換表におけるインデックス
    std::vector<int32_t> N;           //各行動を探索した回数
    std::vector<int32_t> virtual_N;   //各行動へのバーチャルロス
    std::vector<CalcType> nn_policy;  //ニューラルネットワークの出力Policy
    ValueType value;                  //状態価値:漸進的に更新される
};

行動価値の計算

 行動価値は次状態の価値を参照することで得る。次状態の価値を参照する際に、手番が変わるので反転させる必要があることに注意。

ValueType Searcher::QfromNextValue(const UctHashEntry& node, int32_t i) const {
    return -hash_table_[node.child_indices[i]].value;
}

選択ステップ

 選択ステップでは探索回数Nとバーチャルロスの和を用いてUCB値を計算する。

int32_t Searcher::selectMaxUcbChild(const UctHashEntry& node) {
    constexpr double C_PCUT = 1.0;
    const int32_t sum = node.sum_N + node.virtual_sum_N;

    int32_t max_index = -1;
    double max_value = MIN_SCORE - 1;
    for (int32_t i = 0; i < node.moves.size(); i++) {
        int32_t visit_num = node.N[i] + node.virtual_N[i];

        double Q = QfromNextValue(node, i);
        double U = std::sqrt(sum + 1) / (visit_num + 1);
        double ucb = Q + C_PUCT * node.nn_policy[i] * U;

        if (ucb > max_value) {
            max_value = ucb;
            max_index = i;
        }
    }
    return max_index;
}

バックアップ

 バックアップ時にバーチャルロスを0へと戻していく。実際の探索回数であるNやその総和は1足すだけで良い。ここで価値を漸進的に更新する。更新幅は探索回数 + 1の逆数となる。

void SearcherForPlay::backup(std::stack<int32_t>& indices, 
                             std::stack<int32_t>& actions) {
    int32_t leaf = indices.top();
    indices.pop();
    float leaf_value = hash_table_[leaf].value;

    while (!actions.empty()) {
        int32_t index = indices.top();
        indices.pop();
        int32_t action = actions.top();
        actions.pop();
        UctHashEntry& node = hash_table_[index];

        //手番が変わるので価値を反転
        leaf_value = MAX_SCORE + MIN_SCORE - leaf_value;

        //探索回数の更新
        node.N[action]++;
        node.sum_N++;
        node.virtual_sum_N -= node.virtual_N[action];
        node.virtual_N[action] = 0;

        //漸進的更新
        float alpha = 1.0f / (node.sum_N + 1);
        node.value += alpha * (leaf_value - node.value);
    }
}

気づいたこと

 以前の実装のようにバーチャルロスを直接Nに足していると、同じバッチ内で次にそのノードへ到達したときNが本来よりも大きくなっているのでQ = \frac{W}{N}が小さく評価されてしまう。今回のようにバーチャルロスを別に保持することでこの欠点も直すことができる。もっともこの欠点は探索回数が多くなれば無視できるほど小さい影響しかもたらさないだろうし、むしろ同じノードが選ばれにくくなるため良い効果があるかもしれない。

実験結果

 漸進的な更新へと変えた実装の探索部を用いて技巧2(深さ1)と1手1秒で200局対局させた。

探索手法 勝率
総和を保持(従来の実装) 81.0%
漸進的更新(今回の実装) 86.5%

 誤差の範囲内かもしれないが、漸進的更新の方がやや強くなる結果となった。強くなる要因としては、同一局面へ違う進行で合流した際に良い価値を返すことができている可能性が考えられる。従来の実装では保存しておいたニューラルネットワークの出力のValueをそのままバックアップしていくことになるが、今回の実装では漸進的に更新をしているため単なるニューラルネットワークの出力値よりは良い推定値になっていると考えられる。しかしそこまで大幅な性能向上でもなく、弱くなっていないことが確認できたことが重要となる。

補足

 置換表エントリにはバーチャルロスをint32_t型で確保したが、この変数は最大でも探索バッチ数までしか増えないので4Byteも使う必要はない。1Byteでは不安だが、2Byteあれば十分だと思われる。そうすることで従来の実装では価値の総和として4Byteの浮動小数点型を確保する必要があったのに対して多少のメモリ消費量削減となる。

 (個人的に)もっと重要な点は、Categorical分布を保持するときのメモリ消費量が抑えられることである。Miacisの手法では51分割していたためWを保持するために通常のAlphaZeroよりも約51倍のメモリが必要になっていた。これにより秒数制限よりも置換表のサイズ制限の方へ速く到達してしまうような状況だったのだが、今回の実装によって使用メモリ量がだいたい1/50となったため問題なくなった。今までは学習部においてもメモリ消費量の問題からActorの並列数を増やせなかったのだが、今後は並列数を増やすことで多少データ生成速度が上がるのではないかと思われる。

Policyの教師信号を分布にする

要約

 Policyの教師信号を探索回数の正規化した分布とした方が性能が向上した。

背景

 AlphaZero型の学習においてPolicyの教師信号にはルートノードから各行動について探索した回数をその総和で割った分布を利用している。MiacisではCPUのメモリ容量が足りないかと思って、実際に取った行動をOnehotベクトルとしたものを教師信号としていた。しかしちゃんと計算してみると問題はなかった。

 具体的には、Miacisでは通常2^{20} (=1M)局面分のサイズを持つリプレイバッファを用いて学習をしている。1局面分のPolicyの教師データが、今までは指した指し手のラベルを一つ持っていたので4Byteであったのが、合法手の数だけラベルとその行動の確率のペアとして保持すると合法手の数 × 8Byteとなる。合法手が平均100手とすると増加分は800MByteとなる。Policyの出力次元(81 × 27 = 2187)全て保持しておかなければならないかと思いこんでいたが、合法手のところだけ持っておき学習する時点で2187次元ベクトルに直せば良いだけの話であった。

実験

実験設定

 前回と同じ。

実験結果

 floodgateの棋譜に対する損失は次のようになった。教師信号を分布としたものがpropotionalである。

f:id:tokumini:20190519134937p:plainf:id:tokumini:20190519134943p:plain
左:Policy損失 右:Value損失

 技巧2(深さ1)と1手1秒で200局対局させた結果は次のようになった。

教師信号 勝率
onehot 79.0%
propotinal 81.0%

 ほとんど変わらないようにも思えるが、Policyの教師信号を探索回数を正規化した分布とした方が多少良い結果のように思える。

 また初期局面でのPolicyの出力分布を示す。

順位 onehot propotional
1 ▲2六歩(18.1%) ▲2六歩(32.2%)
2 ▲3八飛車(13.9%) ▲7八金(22.0%)
3 ▲5八玉(10.8%) ▲5八玉(17.0%)
4 ▲5八飛車(9.7%) ▲5八飛車(9.0%)
5 ▲2六歩(7.8%) ▲5六歩(6.0%)
その他 39.7% 13.8%

 教師信号を分布としたほうが手がばらけるかと思ったがこと初期局面に関しては単純にそうなるわけではなかった。またpropotinalは技巧との対局では中飛車が多くなったのだが、特にその傾向もこのPolicyからだけでは見られない。

 1秒探索した後の探索回数を正規化した分布についても調べた。

順位 onehot propotinal
1 ▲5八玉(52.5%) ▲7八金(74.0%)
2 ▲4八銀(21.2%) ▲2六歩(11.0%)
3 ▲7六歩(17.5%) ▲5八玉(5.5%)
4 ▲3八飛車(3.3%) ▲5八飛車(5.3%)
5 ▲5八飛車(0.9%) ▲5六歩(1.3%)
その他 4.6% 2.9%

 propotinalの▲7八金の後は読み筋で▲5八飛車と出るので、実質80%ほどが中飛車となると予想され、これは対局を見ていて受けた印象とも近い。

 また両者ともPolicyの偏りに比べて探索結果の偏りが大きすぎるのではないかと思われる。これはC(s)の値が小さいためだと考えられ、以前上げた記事では、AlphaZeroはValueを[0, 1]の範囲に直して探索しているという情報があり、そのままのC(s)では[-1,1]の範囲で探索しているMiacisにとっては小さすぎるのかもしれない。二つパラメータを調整するのは大変なのでC_{PUCT}に戻して実験してみたい。

バッチサイズとステップあたりの学習速度の関係〜強化学習編〜

結論

 強化学習でもバッチサイズとステップあたりの学習速度は比例しそうだ。あるデータ生成速度に対して学習可能な範囲でバッチサイズを上げていくことが学習の高速化に繋がるかもしれない。

前書き

 前回教師あり学習において、バッチサイズとステップあたりの学習速度には比例関係があるのではないかという結果を得た。今回は強化学習でもこの関係が成り立つかどうかを検証していく。

実験

実験設定

 Miacis(categorical)でAlphaZero方式の学習を行った。

項目 内容
バッチサイズ 32と64の二通り
ステップ数 200,000
リプレイバッファサイズ 2^{20}局面
学習率の初期値 0.01
学習率の減衰 100,000ステップ後に1/10
データ生成速度 1ステップあたり約30局面

実験結果

 floodgate2016年の棋譜を用いて計算した検証損失のグラフを次に示す。

f:id:tokumini:20190516152716p:plain

 今までの結果から損失はあまり棋力に相関しないと思われるが、このグラフにおいてもバッチサイズが大きいと学習が速いことがわかる。

 バッチサイズ32,64で学習されたパラメータを用いて技巧2Depth1と1手1秒で200局対局を行い、性能を評価した。

パラメータ 技巧2(D1)に対する勝率 相対Eloレート
バッチサイズ32 200kステップ時点 52.5% +17.4
バッチサイズ64 100kステップ時点 49.0% -6.9
バッチサイズ64 200kステップ時点 79.0% +230.2

 バッチサイズ32で200kステップ学習したパラメータの方がバッチサイズ64で100kステップ学習したパラメータよりやや強かったが、ほぼ同等と言える性能であった。そしてバッチサイズ64で200kステップ学習したパラメータの方がより高い性能となった。やはりバッチサイズが大きい方がステップあたりの学習速度が上がっているように思われる。

考察

 前回、計算時間がバッチサイズによらない条件として次の三つを導入した。

  1. Actorの数を\frac{1}{n}倍するならば、バッチサイズを\frac{1}{n}倍しなければならない
  2. バッチサイズを\frac{1}{n}倍するならば、ステップ数をn倍しなければならない
  3. バッチサイズによらず1ステップあたりにかかる時間は一定

 Learnerを1ステップごとにスリープさせている都合上(3)はほぼ成り立ってしまい、前回および今回の実験結果から(2)も成り立ちそうだが、(1)は多少制限が緩い可能性がある。

 まずAlphaZeroの1ステップあたりのデータ生成量を計算すると、以前山岡さんのブログで34285.71 ゲーム/チェックポイントと計算されていた。チェックポイントは1000ステップごとなので34.28571 ゲーム/ステップであり、AlphaZeroは早期に投了するようにしていることから1ゲーム平均112.5手(根拠の詳細は後述)とすると3857 局面/ステップとなる。個人的には、この値がバッチサイズ4096となかなか近い値であると判断できるのではないかと感じる。つまりだいたい1ステップあたりの生成量≒バッチサイズを目安として、バッチサイズあるいはスリープ時間を決定するのが良いのではないか。

 そして今回の実験では30局面/ステップほどの生成速度のままバッチサイズを64にしても(レートにして-25ほどの悪影響はあったかもしれないが)ほぼ問題なく学習できている。以前同じ設定でバッチサイズ4096にした場合上手く学習できなかったのでいくらでも大きくできるということはないと思われるが、できるだけ大きくすることで多少学習速度を上げることができる可能性はある。

補足:AlphaZeroの1ゲームあたりの平均手数

 AlphaZeroの論文のSupplementary MaterialsにあるTable S3に生成した総ゲーム数と1局面あたりの思考時間が記載されている。

f:id:tokumini:20190516155854p:plain

 24,000,000ゲームを5000基、12時間、1局面の思考時間約0.08秒で生成したことから1ゲームの平均手数は

\displaystyle 5000 \times 12 \times 3600 \div 0.08 \div 24000000 = 112.5

と求まる。

バッチサイズとステップあたりの学習速度の関係

要約

 バッチサイズとステップあたりの学習速度は比例関係にある(?)ため、強化学習の高速化としてバッチサイズを小さくすることは意味がない可能性がある。

前書き

 前回はLR Range Testによる学習率の決定法について書いた。これをもとに複数のバッチサイズで教師あり学習の実験を行い、そこからAlphaZeroアルゴリズムでのバッチサイズを検討したい。

実験

実験設定

  • データ
    floodgate2015年~2018年の棋譜から両対局ソフトのレートが2800以上、かつ手数が50手以上のもの計10,939,651局面(そのうち9割を学習データ、残りを検証データとして使用)
  • バッチサイズ
    4096, 512, 64, 32の4通り
  • 損失
    Policy損失とValue損失の1:1での和
  • 学習率減衰
    1エポックごとに検証データ全てを使って損失を計算し、前回までの検証損失の最小値より小さくなっていなければ学習率を0.9倍
  • 学習終了
    最小値を更新できなかったことが6回続いたタイミングで学習を打ち切り
  • 学習率の初期値
    各バッチサイズについて学習率の初期値をLR Range Testにより決定
バッチサイズ 学習率の初期値
4096 0.03
512 0.025
64 0.015
32 0.01

実験結果

 横軸にエポック数を取ったグラフを次に示す。

f:id:tokumini:20190515112200p:plain

 各バッチサイズについて損失最小値を次に示す。

バッチサイズ エポック 損失 Policy損失 Value損失
4096 44 2.18 1.76 0.422
512 38 2.14 1.73 0.413
64 40 2.12 1.71 0.410
32 62 2.11 1.71 0.405

 バッチサイズは小さい方が汎化性能が上がるという話1と一致した(この話には反論もあるようだが2)。バッチサイズ32のときだけ損失が最小となる際のエポック数が大きいが、グラフを見ると40エポックほどでもほとんど同程度の損失値になっており、同程度の性能に至るまでに必要なエポック数は同等と見なすことができると考えられる。

 バッチサイズによらず同じエポック数ではほぼ同等の性能と仮定すると、バッチサイズを\frac{1}{n}倍したとき1エポックあたりのステップ数はn倍になるため、ステップ数あたりの学習速度は\frac{1}{n}倍になるということになる。わかりやすく横軸にステップ数を取ったグラフを次に示す。

f:id:tokumini:20190515112216p:plain

考察:強化学習における学習時間

 強化学習でのステップ数と教師あり学習でのステップ数を同一視して良いものかはわからないが、とりあえず同一視できるものとする。今回の実験からは、バッチサイズを\frac{1}{n}倍した場合n倍のステップ数が必要になる可能性が示唆されている。

 AlphaZeroのようなActorとLearnerを並列に動かす手法では、バッチサイズとデータ生成速度のバランスが重要であると勝手に思っている(もちろんActorの数は可能ならば多ければ多いほど良い3)。AlphaZeroは第1世代TPUを5000基用いてデータ生成を行っているが、たとえばこれの\frac{1}{100}倍である50基しかマシンを用意できなかった場合に、バッチサイズを4096のまま学習させると同じデータを過剰に何度も学習することになりかねない。おそらくバッチサイズもActorの減る割合に合わせて\frac{1}{100}倍するのが良いのではないかと思われる(ここが線形かどうかは不明)。

 しかし今回の結果ではバッチサイズを\frac{1}{100}倍すると同じ性能に至るまで100倍のステップ数がかかることになる。バッチサイズによらず1ステップあたりにかかる時間が一定であるとすると100倍の時間がかかることになる。

 結局、次の三つの仮定

  1. Actorの数を\frac{1}{n}倍するならば、バッチサイズを\frac{1}{n}倍しなければならない
  2. バッチサイズを\frac{1}{n}倍するならば、ステップ数をn倍しなければならない
  3. バッチサイズによらず1ステップあたりにかかる時間は一定

を認めるならば、Actor-Learner型の手法で一定の性能を満たすのにかかる実時間はバッチサイズによらないことになる。バッチサイズをいじることによる小手先の高速化などは考えない方が良さそうだ。

補足

 3つ目の仮定について補足を加えると、当然ながらバッチサイズが大きい方が1ステップ学習するのにも時間がかかる。実際に今回の実験(RTX 2080ti)では以下のようであった。

バッチサイズ 1ステップあたりにかかる時間(秒)
4096 0.378
512 0.039
64 0.011
32 0.010

 しかしActor-Learner型の強化学習で、Learnerを1ステップごとに数秒スリープさせることで擬似的にActorの数を増やす場合、無視できる差になると思われる。Actorの数が少ない(計算資源が足りない)環境では1ステップあたりにかかる時間の差がそこまで効いてくることはなさそうだ。

LR Range Testによる学習率の決定

要約

 LR Range Testを行って損失が最小となるときの学習率を初期値として決定して良さそう。

前書き

 山岡さんの『ディープラーニングによる将棋AIの作り方3』を読んでいて、floodgateの2017年、2018年の棋譜http://wdoor.c.u-tokyo.ac.jp/shogi/x/から入手できることを知った。2015年から2018年までの棋譜をダウンロードし、R2800以上のソフト同士が投了で終了したものだけを残したところ約1千万局面ほどになった。もちろんデータ数としては足りないのだが、簡単な検証をする分には十分な量なのではないかと思ったのでしばらく教師あり学習で遊んでみたい。

 まずどのような実験をするにしても学習率を自動決定できれば嬉しい。Cyclical Learning Rate for Training Neural Networkという論文があり、主題は「学習率を上げたり下げたりしながら学習させることで学習が高速に進む」ことなのだが、その学習率の範囲を決定する方法として行われているLR Range Testが学習率の決定に利用できるのではないかと思ったので実装したみた。実のところ元論文はほとんど読んでおらず、

を読んでそれっぽい実装をしてみただけとなる。普通の教師あり学習を、10^{-5}という小さな学習率から始めて1ステップ学習が終わるごとに学習率を\alpha倍していき、学習時の損失を記録してくものとなっている。コードはMiacisのlearn.cppにある通りとなる。

実験

実験1:\alpha=1.1

 倍率を\alpha=1.1として各バッチサイズについてそれぞれ10回試行して平均を取った結果を示す。

f:id:tokumini:20190511113850p:plain

 上の記事では最下点における学習率よりも小さい値の方が良いとあるが、まず単純に最も損失が低くなっているときの学習率を見ていくと

バッチサイズ 最下点での学習率
8 0.000882
64 0.029991
512 0.032999
4096 0.020484

となっている。この結果をそのまま採用するならバッチサイズが小さくなったときに最適な学習率が大きくなるということになってしまうが、個人的には違和感がある。最下点あたりでの誤差で多少ぶれがあるのでもう少し試行回数を増やした方が良いかもしれない。

 また、経験的にはバッチサイズ4096ならば学習率を0.1〜0.2にしても学習は進む。学習率の上げ方が細かすぎて、良い学習率に到達する前に学習が進みすぎているのかもしれないと考え、学習率の上昇率を1.2倍にして同じ実験を行った。

実験2:\alpha=1.2

 倍率を\alpha=1.2として各バッチサイズについてそれぞれ15回以上試行して平均を取った結果を示す。

f:id:tokumini:20190511123306p:plain

 大きく結果が変わるわけではなかった。最も損失が低くなっているときの学習率は

バッチサイズ 最下点での学習率
8 0.0034182
64 0.0146977
512 0.0253977
4096 0.0304772

となった。試行回数を増やした影響かバッチサイズの大小と最下点での学習率の大小が連動しており、こちらのほうがもっともらしい結果だと感じる。

 最下点での学習率を良い学習率として採用して良いのかという問題はあるが、みたところ最下点での学習率もそこまで大きいものではないし、学習中にいくらか学習率は減衰させるので初期学習率は(発散しない程度ならば)多少大きくても良いのではないかと勝手に考えるものとして、とりあえずは最下点での学習率を初期値としてやっていこうと思う。気が向けばCyclical Learning Rateの手法自体も実装してみたい。

LibTorchにおける半精度浮動小数点演算

記事の要約

 LibTorchを使って半精度浮動小数点演算(FP16)を行うことで探索は速くなったが、学習は上手くいかなかった。どうもBatch Normalizationの部分はFP32で計算しなければならないようだ。

LibTorchによる半精度浮動小数点演算

 深層学習では厳密な精度よりも計算速度が求められる場合が多い。特に近頃のGPUTensorコアなるものによって半精度(16bit)の浮動小数点演算を高速に行うことができるようで、FP16で計算を行うプログラムを書くと高速になるとのことである。

 LibTorchではモデルをGPUに転送する際にtorch::kHalfを指定することで半精度浮動小数点演算ができるようだ。自作の将棋ソフト『Miacis』のneural_network.cppから、モデルをGPUへ転送する部分のコードを示す。以下示すコードはUSE_HALF_FLOATシンボルを立てている場合に半精度浮動小数点演算を行うようになっている。

    device_ = torch::Device(torch::kCUDA, gpu_id);
#ifdef USE_HALF_FLOAT
    to(device_, torch::kHalf);
#else
    to(device_);
#endif

 入力特徴量もGPUへ転送する際に同様の指定を行う。

std::pair<torch::Tensor, torch::Tensor> NeuralNetworkImpl::forward(const std::vector<float>& inputs) {
#ifdef USE_HALF_FLOAT
    torch::Tensor x = torch::tensor(inputs).to(device_, torch::kHalf);
#else
    torch::Tensor x = torch::tensor(inputs).to(device_);
#endif
    //以下ネットワークによる処理

    return { policy, value };
}

 ネットワークの出力をCPUで受け取る場合はtorch::Half型を用いる。たとえばbatch_size分のvaluestd::vector<float>で受け取るコードは次のようになる。

    auto y = forward(inputs);

    //CPUに持ってくる
    torch::Tensor value = y.second.cpu();

    //float型のstd::vectorで受け取る
    std::vector<float> values(batch_size);
#ifdef USE_HALF_FLOAT
    std::copy(value.data<torch::Half>(), value.data<torch::Half>() + batch_size, values.begin());
#else
    std::copy(value.data<float>(), value.data<float>() + batch_size, values.begin());
#endif

実験

 上記の改良により本当に速くなるか、精度の問題はないかを検証した。

探索速度

 初期局面を1GPU(2080ti)、2スレッド、512バッチで探索した。GPU処理部分を重くして差がわかりやすくなるようにResidualブロックにおけるCNNのチャネル数を256にして実験を行った。探索速度は次のようになった。

FP32 FP16
7322 NPS 11284 NPS

 FP16にすることで1.5倍ほどのNPSになった。

教師あり学習

 RTX2080tiを用いてfloodgate2016年の棋譜をもとに教師あり学習を行った。損失の推移を次に示す。

f:id:tokumini:20190509202901p:plainf:id:tokumini:20190509202910p:plain
左:Policy損失 右:Value損失

 FP16では上手く学習できていない。

 少し調べてみるとBatch Normalizationは32bitで計算しければいけないとの話があった。指数移動平均のところが原因と言われていたりもするが、どこまで信用できるものかはわからない。Qiitaにも同内容の記事があったので少なくともBatch NormalizationはFP32で計算しなければならないという説の信憑性は高そうだ。山岡さんのブログでもそのようにしていた。やはりBatch Normalizationの部分が問題であるようだ。

結論

 FP16によって探索は速くなったが学習での精度が低下してしまった。今後はモデルの一部だけ(Batch Normalization以外)をFP16で計算する方法などを探っていきたい。

持ち駒の正規化

記事の要約

 持ち駒は正規化した方が良さそう。

前書き

 WCSC29会場にて山岡さんから『ディープラーニングを使った将棋AIの作り方3』を購入させていただいた。AlphaZero的な強化学習ということで大枠は変わらないが、読んでいるといくらかMiacisの実装と異なる点があることに気づいた。計算資源との兼ね合いもあるが、簡単に検証できるものはできるだけしていきたい。

 今回は持ち駒の正規化について検証を行った。持ち駒の正規化とは、ニューラルネットワークへに対して持ち駒の枚数を入力するとき、各持ち駒の数を直接入力するのではなく最大枚数で割ることによって0から1の範囲に正規化することを指す。駒種による最大枚数の違いや、盤上の駒の有無を0,1で表現していることを考えると正規化する方が自然であると思われる。

 以前正規化を試したときは性能が悪化した記憶があり、ソースコード中にもそのような記述があったが、データが残っておらず信用できないと判断したためもう一度検証する。

実験

 上記の本では歩の枚数だけ例外的に最大枚数ではなく8で割った値を入力とすると記述されていたが、個人的な好みから歩も最大枚数である18で割った数を入力とした。

 floodgate2016年の棋譜を用いて教師あり学習を行った。主要な設定は

  • バッチサイズは4096、学習率は0.1
  • 学習データの1/10を検証データとし、1エポックごとに検証データに対して損失を計算。前回に比べて下がっていなかった場合には学習率を1/2に減衰
  • 6回連続で損失が下がらなかった時点で学習を終了

である。両手法ともランダムに初期化した同一のパラメータを初期値とし、乱数シードも固定し学習データのシャッフル方法も同一であるようにした。

 検証データに対する損失推移は以下のようになった。

f:id:tokumini:20190507180057p:plainf:id:tokumini:20190507180104p:plain
左:Policy損失 右:Value損失

 一番損失が低いエポックでの結果は次のようになった。

手法 Policy損失 Value損失
正規化あり 1.895 0.3868
正規化なし 1.909 0.3963

 1回の実験ではっきりしたことは言えないが、やや正規化ありの方が良い結果である。

 気になる点として、学習率を0.2にすると正規化なしでは学習できるのに対して正規化ありでは学習できなかった(value損失が常にほぼ2になってしまった。これはvalueが常に1または-1を出力するようになったことを示している。policyの学習は進んでいた)。入力を正規化するとそれに合わせて適切なパラメータも小さくなるはずであり、相対的に学習率が大きくなっているのかもしれない。ただの偶然であった可能性もある。

バッチサイズと性能の関係

前書き

 AlphaZeroが4096という大きなバッチサイズで学習しているのに対して、Miacisは64という小さいバッチサイズでの学習を行っている。AlphaZeroに比べて使える計算資源が少ないためデータの生成速度が小さく、バッチサイズが大きいと同じデータを何度も学習することになってしまい学習が上手く進まないのではないかと予想しているからである。今回はバッチサイズの違いによる性能の変化を実験により確かめてみた。

実験

 バッチサイズと学習率以外の条件は全て揃えて、カテゴリカル分布を出力するモデルについて200kステップの学習を行った。学習率はバッチサイズが4096のとき0.2(AlphaZeroの設定)、64のとき0.01とした。これは以前の実験から良いと考えられた値である。記事内の「バッチサイズに比例させるのが良い」という説から計算できる値0.2 \times \frac{64}{4096} = 0.003125よりは大きい値で設定している。その他パラメータ、実験設定も先の記事に準ずる。

 実験結果を次の表に示す。

バッチサイズ Policy損失 Value損失 技巧2(深さ1)に対する勝率 総学習時間 生成局数
4096 3.81 0.829 12% 62.4時間 125,600
64 3.01 0.781 63% 86.6時間 173,400

 バッチサイズが大きいと損失が大きいままになっており、実際に対局を行っても性能は悪化していることがわかる。バッチサイズを大きくすると学習に時間がかかるため、その間にActorが動く時間も増え、全体として生成される局数自体は増えている。それにも関わらず性能が悪化していることから、バッチサイズを大きくすることが悪影響を及ぼしていると考えられる。

今後の展望

リプレイバッファサイズとの関係

 本来はバッチサイズを大きくした分、リプレイバッファのサイズも大きくしなければならないのかもしれない。メモリ量の問題もあるためバッチサイズに比例するだけ大きくできるわけではないが、検証してみるべき要素ではあると思う。

 リプレイバッファサイズはバッチサイズとの関係を抜きにしても様々な検証をする価値があるパラメータである。Oracle Developersの記事では徐々に大きくしていく方式が取られており、そのような工夫も一考の余地がある。

小さいバッチサイズでの学習

 バッチサイズを64よりももっと小さくすることで小規模な計算資源しかない環境でも良い学習ができる可能性がある。その際に問題になるのはニューラルネットワーク内部で使われているバッチ正則化であり、バッチ正則化はバッチサイズが小さくなると性能が悪化することが知られている。それを改善する手法としてBatch Renormalization(日本語解説スライド)やWeight Standardization

という手法がある。これらの手法を採用することでバッチサイズを小さくしつつ上手く学習を進めることができるかもしれない。

【WCSC29】個人的に興味を惹かれたアピール文書集

 すでにuuunuuun氏が書かかれた全チームの簡単なまとめや、やねさんによる見どころ紹介がありますが、ここでは個人的に面白そうだなと思ったものについて妄想レベルの私見を交えながら触れていきたいと思います。自分がディープラーニング系のソフトを開発しているのもあってそちらに偏っているかもしれませんがご了承ください。

たこっと

 新しい特徴量を考えているということで面白そうです。ニューラルネットワークを利用しつつのものでありながらKKPARと名付けられているのでNNUEに近い感じではあるんでしょうか。

 あとは最適化手法としてAdaBoundを実装するとありますね。実際Optimizerの違いによる性能の差ってどの程度のもんなのかなぁと多少疑わしい気持ちで見ている面もあるわけですが、上手くいったら面白いなとは思います。

elmo

 ReLUを使うニューラルネットワークならなんでもHeの初期化で大丈夫だと勝手に思っていましたが、NNUEに初期化の問題があるとは知りませんでした。バイアスを0ではなく小さい正の値にするとかで解決する問題でもないんですかね。NNUE型のソフトには一切触ったことがないのでこの辺の挙動がよくわからないというのが正直なところです。

 兄弟局面の比較から評価値をいじるのは探索との兼ね合いという面が強くなっており、強化学習の枠組みに上手く混ぜていけるのかなぁというのが少し迷っているところです。まぁそもそも評価値を状態価値として考える(強化学習の枠組みで考える)ことにどこまで必然性があるのかも微妙なのかもしれません。

 NNUEへのCNN取り込みについては特に詳細が書かれていないので全然見当違いの理解をしているかもしれませんが、特徴量の入れ方、結合の組み方などを工夫すればそのようなことができるかもしれない? と少し思いました。

習甦

 評価値推移をフィードバックして推定された勝率の割引報酬を考える強化学習とのことですが、個人的には将棋の手数(せいぜい300手程度)で割引率を考える(1未満にする)必要があるのかどうかは疑問です。特に「絶対に勝ちだけど手数自体はかかる」ような局面に対して手数による割引をかけると不当に低い報酬と考えてしまうのではないかと心配しているのですが、現実的にはそういう局面は少ないし別にどうでもいいという可能性はありそうです。あるいは評価値が一定以下になった場合に投了を行うと問題ないのかもしれません。

dlshogi

 学習データ生成時の詰み探索の処理とかそういう細かい部分で完成度が高いという印象です。Pollicyの学習もREINFORCEの方が良いというのはそうなのかもしれません。

 ミニバッチサイズが64~1024で徐々に増やすというのは、学習率を上げる代わりにミニバッチサイズを上げた方が良いという論文を意識されてのことなんでしょうかね。学習率の方も下げてはいるようですが。

 dlshogiは一致率を指標としていることが多いように思えますが、損失よりこっちの方が良い指標なんでしょうか。自己対局による検証はコストがかかるのでこれで済むなら大きいのですが。

ねね将棋

 フレームワークとしてCNTKを使っているようで、僕はPyTorchのC++APIを使っているのですが結構苦労が多く、CNTKが安定しているならその方がいいのかもしれません。そもそもC++でネットワーク部分を書くことに拘る意味があるのかも怪しいところですが。本番でp3.16xlargeを使うということはtensorコアも有効活用できているということなんでしょうかね。

 評価値部分が2チャネルというのは少し不思議です。評価値と勝敗を別に予測する効果がどの程度あるものかすぐにはわかりませんが、学習を安定させられる効果があったりするのかなと思っています。手元での実験でもPolicyに比べてValueの学習は上手く進まないことが多いと感じていて、AlphaZeroを改良するならValue側を多少工夫するのが手っ取り早いかなという印象です。

芝浦将棋 Softmax

 モンテカルロ・ソフトマックス探索を採用しているという点が面白いと思います。細かい定義はもちろん違うんですが、ニューラルネットワークでPolicyを計算するとき、だいたいQ値にソフトマックス関数をかけているだけのように思えることもあります。実際にQ関数と方策の間に関係があることを示した話もあり、このあたりが統一的視点から眺められれば面白いです。MCTSとαβ探索は適当な仮定を入れれば一致するんじゃないかなとかは妄想しており、モンテカルロ・ソフトマックス探索もそこに加わる一種として記述できそうな気もします。

762alpha

 Flowboardの説明がよくわかりませんでしたが、なるほど局面のデータ構造を工夫するという案も当然ありますね。

 良い評価関数に期待される性質として挙げられるものも興味深いところです。ベイジアンフィルタには詳しくないので学習時の工夫における妥当性はいまいちピンとは来ませんが、一般化して学習時のみに使う対戦相手を用意するというのはありな話だとは思います。

Windfall

 正方形ではないカーネルを考えるのも確かにやってみたいことの一つではあります。角の利きを考えると斜めのカーネルもあった方が良いんでしょうか。それを上手く実装できるのかどうかはわかりませんが……。

 ネットワークを深くしながらの逐次的な学習ができれば良いんですが上手く実装する方法があまりわかっていません。Define by runのフレームワークならわりと素直にできるんでしょうか。

st34

 「局面の評価と詰みの評価で異なる評価関数を使用」という点が面白いと思いました。以前囲碁の方で補助タスクを入れた方が性能が良くなるという論文についての記事を上げたんですが、将棋だと詰みの有無を補助タスクとして与えることで同じような効果が得られないかなと考えたりします。詰みのある確率を出力できれば学習の補助だけではなく実践の探索時でも利用ができそうなので悪くないタスクなんじゃないかと勝手に考えていますが、実験などはまだしていません。

うさぴょん外伝

 入力情報の圧縮がすごそうです。10k時点での一致率が最終的な性能とほぼ同等という点も興味深く、確かにほとんど同じような条件で行っているなら馬鹿正直に最後まで回していく必要もないのかもしれません。

 そして真に興味深いのは追記以降。これは行動の表現を考えているということにも近いのだと思います。行動表現の獲得を評価値との学習に結びつけたこの構造は注目に値すると思います。得られた行動表現で似た行動が近い表現になっているのかどうかが気になります。この行動表現空間への写像として方策関数とかを考えられないものでしょうか。あるいは環境モデルにおいてこの行動表現で遷移するとか……。

 完全に自分の興味が先行してしまいましたが、これはかなり面白い仕組みだと思います。将来的にはDNN系将棋ソフトのスタンダードになってもおかしくないのではないかと考えているところで、ぜひとも今回だけでなく発展していった形を見たいところです。

おまけ1:Miacis

 手前味噌にはなりますが、Miacisも評価値の確率分布を考慮する少し珍しいプログラムになっています。単に確率分布を考えて見た目が面白いというだけではなく、それがちゃんと評価関数の精度向上(+450程度)につながっており、なおかつ探索でも有効利用できている(+R80程度ですが……)というのがささやかな自慢ポイントです。GUIなどは作っていないので将棋所のテキスト部分で簡易的な表示をすることしかできませんが、会場に来られる方はぜひ様子を見に来ていただけると嬉しいです。

おまけ2:GPGPU勢の使用フレームワーク

 個人的に興味があったので明示的に使用フレームワークを書いていたソフトだけ抽出しました。多種多様です。cuDNNのAPIを直接叩くのが一番迫力あると思っています。

ソフト名 フレームワーク
dlshogi Chainer, cuDNN
ひまわり Tensorflow
Crazy Shogi cuDNN
ねね将棋 CNTK
Miacis PyTorch(C++API)
Windfall PyTorch(C++API?)
AobaZero OpenCL
Google Colab TPU将棋 Tensorflow

追加学習による性能向上の検証

 今までは再現性や比較の観点から毎回ランダム初期化したパラメータから学習を行っていたが、WCSC29も迫ってきたということで最も強いパラメータに追加学習することで性能が伸びるか検証を行った。

 今まで最も性能が良いパラメータは評価値をカテゴリカル分布で出力するモデルを200kステップ学習したものとなる(アピール文書)。これをさらに200kステップの学習を3回追加で行った。つまり計800kステップの学習を行ったことになる。

 floodgate(2016年)のR2800以上同士の対局を検証データとした損失のグラフは次のようになった。

f:id:tokumini:20190428143636p:plainf:id:tokumini:20190428143643p:plain
左:Policy損失 右:Value損失

 わずがだが学習を重ねるほど損失が下がっている。Value損失は200kステップごとに(つまり学習をやり直すたびに)多少損失が上がっており、リプレイバッファでのデータのたまり方などが影響していると思われるが、詳しいことはわからない。

 Gikou2の探索深さを2に制限したもの(R1791)と対局することで性能の検証を行った。

累計学習ステップ数 勝率 相対レート
200k 53.5% 24.4
400k 52.0% 13.9
600k 60.2% 71.9
800k 71.0% 155.5

 200kから400kにおいて一度下がってしまってるが、これは100局という少ない局数での検証なのでブレの範囲内だと思われる。全体として学習を進めた方が勝率が上がっていっており、学習すればするほどまだ伸びる余地がありそうだ。また最後の200kステップではLearnerのスリープ時間を長く取ることで擬似的にActorの数を増やしている。そこでの伸びが大きいことも含め、やはり計算資源があればあるほど良いと結論付けられる。

おまけ:詳細な実験設定

 備忘録を兼ねて詳細な実験設定を記しておく。まず計4回の学習でのパラメータは次の表のようになる。

項目
Residualブロック数 10
CNNのフィルタ数 64
\lambda 0.925
バッチサイズ 64
学習率初期値 最初の3回は0.01、最後の1回は0.005
学習率減衰 100kステップ経つタイミングで1/10

 学習率が複数回の学習を通してどのような変化をしていたかをグラフとして残しておく。

f:id:tokumini:20190428143633p:plain

 また最初の3回はLearnerで1ステップ学習するごとに1秒の、最後の1回では2秒のスリープ時間を挟んだ。その間はActorがデータを生成することだけが行われるため、擬似的にActorの数を増やすことになる。学習を行ったマシンは2080tiを2枚搭載したものであり、2枚とも生成に当てているときは46.0 pos / secほどの生成速度となる。