Post Training Quantization(PTQ)の導入

要約

 PTQ(要するにINT8演算)の導入でR+30程度

実装

 ニューラルネットワークは基本的に浮動小数点演算(FP32)を用いている。今までは半精度浮動小数点演算(FP16)により高速化を行っていたが、Post Training Quantization(PTQ)という、FP32の範囲を絞ってINT8の演算に変換することで高速化する手法も存在する。TRTorchでもこれがサポートされているのでチュートリアルに従って試した。

 なかなか不安定なところもあったが、

  • Calibrationデータとして取り出すデータのミニバッチサイズを、コンパイルで指定する最適バッチサイズと同じにする
  • Calibrationデータの総数をミニバッチサイズの整数倍にする
  • CalibrationのアルゴリズムにはIInt8MinMaxCalibratorを使用する

あたりに気をつけることで上手くいくようになった感覚がある。ライブラリをそのまま使うだけなので実装自体の難易度は低く、下のように十数行でTorchScript形式のモデルをコンパイル・Calibrationできる。

強化学習で得た128chのモデルについて

損失計測

 floodgate2015年の棋譜を用いてCalibrationを行い、2019年の棋譜に検証損失の計算を行った。

 Calibrationに用いるデータ数(局面数)を変えながらINT8での計測を行った結果が以下となる。

データ数 Policy損失 Value損失
FP16(比較用) 1.8464 0.6429
64 1.8565 0.6450
128 1.8537 0.6435
256 1.8537 0.6442
512 1.8569 0.6437
1024 1.8583 0.6435
2048 1.8584 0.6438

 損失はいくらか悪化している。

 Calibrationに用いるデータ数が多ければ多いほど良いわけではなかった。後の256chでの結果も踏まえて、推論時のバッチサイズ(64)のちょうど2倍のデータ数(128)が最も良い値と見なすことにした。以降のNPSの測定ではそのデータ数でCalibrationを行っている。

NPSの測定

演算精度 初期局面 中盤の局面
fp16 35198 ± 606 30392 ± 1802
INT8 37191 ± 797 34403 ± 1003
(倍率) 1.057倍 1.131倍

 128chのモデルではもともと推論が比較的高速なので、INT8による推論にしたところで恩恵が大きくない。これだと損失の悪化の方が大きいのではないか。1.1倍程度ではまともなレート差になりえないので対局はスキップ。

教師あり学習(AobaZeroの棋譜)で得た256chのモデルについて

損失計測

データ数 Policy損失 Value損失
fp16(比較用) 1.8390 0.5804
64 1.8442 0.5855
128 1.8495 0.5829
256 1.8552 0.5846
512 1.8511 0.5849
1024 1.8501 0.5883
2048 1.8565 0.5896

 128chに比べて全体的に悪化幅が大きくなった。

NPSの測定

演算精度 初期局面 中盤の局面
fp16 18612 ± 188 16278 ± 474
INT8 26790 ± 481 24140 ± 836
(倍率) 1.439倍 1.482倍

 256chだとNPSの向上が大きい。NPS2倍でレート100とすると、1.4倍あれば+50程度は見込める。これなら測定できそうだと思ったため256chモデルについては対局も行った。

対局

 Miacisは1手0.5秒

 対戦相手の条件

  • 探索エンジン:やねうら王
  • 評価関数:Kristallweizen
  • NodesLimit:800000

 結果

演算精度 勝数 引分数 負数 勝率 相対Eloレート
FP16 382 186 432 47.5% -17.4
INT8 426 207 367 52.9% 20.5

 R+37.9となった。+50には届かなかったが、精度の悪化も含めるとこれでも伸びすぎかもしれないと思うくらいではある。

dlshogi(GCT)との比較〜その2〜

要約

 現状のMiacisとdlshogi(GCT:電竜戦ver)とのレート差約300の内訳は

  1. 評価関数の差で約200
  2. 探索速度の差で約100

と予想。

損失計測

 前回とほぼ同様の設定で、floodgateの2015年の棋譜だけでなく2016年から2020年までの棋譜それぞれについて損失を計測した。

 前回からの違いとして、終了ステータスがSENNNICHITEとなっている棋譜は計測対象から除外した。

 得られた局面数はMiacis側でもdlshogiのhcpeに変換するスクリプトでも変わらず、以下のようになった。

データ数
2015 2477935
2016 4049364
2017 4263861
2018 2981430
2019 6086236
2020 11552275

 また前回はMiacisの中でもAobaZeroの棋譜から教師あり学習したパラメータについてのみ比較したが、今回はランダムパラメータから強化学習のみで得られたパラメータとも比較する。これらのパラメータは対局を行うとほぼ同程度の性能である。つまり、性能として見ると「dlshogi(GCT)>>Miacis(教師あり学習)≒Miacis(強化学習)」というような形になっている。

 損失計測結果

f:id:tokumini:20210209172102p:plain

f:id:tokumini:20210209172109p:plain

 ここ2,3年の棋譜にはdlshogi、Miacis、あるいは学習データもとのAobaZeroが指した棋譜も含まれる。その影響か、あるいは単に戦型の流行か、Policy損失は低めに出る傾向が見られる。逆にValue損失は上がっている。

 しかし明確に順位が大きく入れ替わるような年があるわけではなく、概ねどのデータでもそれぞれのモデルの比較結果としては変わらないように思える。

 前回は失念していたが、今までの結果から得られる感覚として、同じ損失値でも教師あり学習で到達する場合と強化学習で到達する場合は対局時の性能が異なる。教師あり学習は(AobaZeroの棋譜からなのでfloodgateの棋譜とはやや違うが)それでも性能のわりに損失が小さくなりがちではある。赤線は正直当てにならないと思いたい。

 問題は強化学習の方(青線)である。これは明らかにValue損失が劣っている。以前の試算では

Value損失が0.01小さくなればレートが86上がる

という仮説を立てた。今回計測した結果では、Miacis(強化学習)とdlshogi(GCT)のValue損失の差は0.050 ~ 0.027なので、素直に当てはめればR差 429.1 ~ 234.8 となる。そこそこ妥当な数値ではあるかもしれない。

対局

 ふかうら王(評価関数:GCT)は3000ノードで水匠30万ノードとほぼ互角というデータが出ている。

 Miacisでも探索ノード数3000で対局を行った。使用したパラメータ上の損失計測でも用いた強化学習で得たパラメータである。

 探索部 : 最新のやねうら王 commit id b0a3a2a4f7565bbefb85999368df15e9c90c621f リンク

 評価関数パラメータ : 水匠3改(20210111) リンク

 対局設定における若干の相違点として

  1. 対局時のスレッド数は複数のまま
  2. 開始局面は互角局面集ではなく初期局面

がある。

 Miacisから見た結果

1000局 231勝 2引分 767敗 勝率23.2% レート差-207.9

 評価関数の差か、3000探索内の効率の差かはわからないが、これだけの差があるようだ。

 個人的にはほぼ同内容のMCTS(PUCT)・かつ3000ノードでそこまで差はつかないと思っているので、これがそのまま評価関数の差として見ても良いのではないか。先の損失計測から見たレート差からするとちょっと小さめではあるが。

NPS

 手元でもdlshogiのusiエンジンをビルドできたのでNPSを測定した。とりあえず面倒なので初期局面だけ。オプションなどはなにもいじっていない。

モデル NPS
Miacis 24580±1614
dlshogi 45638±600

 約2倍となっており、手元の計測では2倍でレート100という感覚ではある。

結論

 というわけでこれらの比較から冒頭の要約で示したような考えを持った。

 ちなみに根本的なレート差が約300であるというのはそれぞれ以下のfloodgateでのレートが根拠。ここからGCTはさらに評価関数が伸びているようなので、評価関数部分にもっと伸びる余地を探りたいところだ。

TensorRTの導入

要約

 TensorRT(TRTorch)を導入したことでNPS約1.3倍、R+70ほど。

実装等

 前回、同じデータについてMiacisとdlshogiの評価関数を比較した結果、そこまで精度に差がないのではないかという結果が得られた。この結果が正しいとすると、棋力の差は探索速度や効率に原因あるのではないかという仮説が立つ。

 GCTの加納さんから情報提供をいただき、少なくとも探索効率以前にNPSで差があることは明らかになった。

 ニューラルネットワークの推論速度についてはTensorRTが速いらしいのでそれを導入することにした。

 MiacisはPyTorch(およびそのC++APIであるLibTorch)を使っている。いくらか調べた結果、そこからTensorRTを用いたグラフのコンパイルをするためのライブラリとしてTRTorchというものが良さそうだったため、これを利用することにした。

 このライブラリではTorchScriptという形式を入力とすることでTensorRTを用いたグラフを構築することができる。というわけでまず、今までのモデルをTorchScriptへと変換する必要がある。C++内でこれを行う手法はよくわからなかったため、多少汚いものになったがPythonスクリプトを実装した。

 C++で定義しているニューラルネットワークと同じ構造のニューラルネットワークPython内で定義し、パラメータを一つ一つ手動で読み込んでくるといったものになっている。

 TorchScriptモデルが出力できれば、あとはTRTorchのチュートリアルが示すとおりに読み込み、コンパイルを行えば良い。以前強化学習によって得たパラメータをTensorRTでコンパイルし、全く同じ検証損失が得られることを確認できた。

NPSの測定

 初期局面と中盤の局面(sfen l2+P4l/7s1/p2ppkngp/9/2p6/PG7/K2PP+r+b1P/1S5P1/L7L w RBGS2N5Pgsn2p 82)の2つで計測を行った。それぞれ10秒の思考を10回行った平均値となる。

モデル NPS(初期局面) NPS(中盤局面)
LibTorch版(ランダムパラメータ) 21260 17862
TRTorch版(ランダムパラメータ) 26937 24745
LibTorch版(学習済みパラメータ) 6626 5922
TRTorch版(学習済みパラメータ) 24580 22299

 LibTorchの学習済みパラメータだけ異様にNPSが低く、妙である。ランダムパラメータで比較した場合TRTorchの導入でNPSは1.27倍だが、学習済みパラメータで比較すると3.71倍となる。

 完全に同じ学習済みパラメータではないが、1年ほど前に同じ関数で計測したNPSをツイートしており、そのときはもっと高かった。

そのときの詳細な結果が残っていたのでそれを見たところ、初期局面:18553, 中盤局面:15120であった。この結果で計算し直すとTRTorch版はLibTorch版の1.32倍であり、ランダムパラメータのときとだいたい同じくらいになる。

 学習済みパラメータを用いたLibTorch版での対局が弱いことはないので、NPSの表示だけがおかしいと見なすのがもっとも収まりがよく思える。この点に関しては要調査。

対局

 強化学習を経て得られたパラメータを用いて対局を行った。対戦相手は探索部:去年の5月時点Yaneuraou、評価関数パラメータ:Kristallweizenとなっている。Miacis側は1手0.25秒、Kristallweizen側はNodesLimit=400000。定跡はオフ。

モデル 勝数 引分数 負数 勝率 レート差
LibTorch版 558 1 441 55.9% 40.8
TRTorch版 655 0 345 65.5% 111.4

 R+70.6という結果になった。NPS約1.3倍にしてはレートの上がり方が大きいように思う。とはいえ伸びたので良し。

余談

 環境を新しくしたことで、最初は間違えて対戦相手のYaneuraouの探索部を最新のバージョンにしてしまっていた。そのときの対局結果が以下の通り。

モデル 勝数 引分数 負数 勝率 レート差
TRTorch版 451 0 549 45.1% -34.2

 先の結果と比べると-145.6であり、やねうら王の探索部は約1年でこれくらい上がっているようだ。

dlshogi(GCT)との損失比較

 同じデータに対して評価関数の性能比較を行った。

 使用データはfloodgate2015年の棋譜(リンク内のwdoor2015.7z)

であり、以下の条件でフィルターをかける。

  • 手数が60手以上
  • 対局両者のレートの大きい方が3000以上
  • 終了状態が%TORYO, %SENNICHITE, %KACHIのいずれかである

 使用する棋譜については1手目から投了直前の局面まで全てを利用する。Policyの教師は実際に指された手をOnehotとし、Valueの教師は最終的な勝敗とする(評価値の情報は使わない)。

 手元のプログラムでもdlshogiのcsa_to_hcp.pyを改造したプログラムでも同じく2,477,935局面を得た。

Miacisのバグ

 CSA形式の棋譜には必ず終了条件が書き込まれている(%から始まる行が絶対にある)という前提で実装していたのだが、どうも時間切れの対局など最後の終了状態が無い場合があるらしい。

 時間切れ負けが起きると優勢なのに負けになり、Valueの教師が局面の形勢とはかけ離れることになる。これはデータとして適当ではないため除外するべきだ。

 この修正により、Miacisが検証データとして取り出す局面数が、2,727,833局面→2,477,935局面となった。9%強が減っており、今までは異常な局面を多く検証データとして利用していたのかもしれない。

 128chで学習したパラメータについて修正前後で検証損失を計測した。

パラメータ Policy損失 Value損失
修正前 1.9499 0.6453
修正後 1.9439 0.5766

 Value損失がとても小さくなった。やはり今までのフィルターでは時間切れ負けなど局面の形勢とかけ離れたValueを示す局面が多かったのではないか。これまで取ってきたデータが全部信用ならなくなった。

dlshogiでの計測

データ整形

 基本的にはcsa_to_hcpをベースとした。

 出力をhcpeにした点以外では、次の

 for i, (move, score) in enumerate(zip(parser.moves, parser.scores)):

で回した後に、そのiを利用して書き込みを行ったり局面数を数えているが

        hcps[:i].tofile(f)
        num_positions += i

これでは書き込む・数えるデータ数が1少なくなっているように思う。意図的な動作なのかはちょっとわからなかったが、今回自分で使うには修正した方がわかりやすかったので直した。

実際に使用したスクリプト(長いので折りたたみ)

import numpy as np
from cshogi import *
import glob
import os.path

MAX_MOVE_COUNT = 512

# process csa
def process_csa(f, csa_file_list, filter_moves, filter_rating, limit_moves):
    board = Board()
    parser = Parser()
    num_games = 0
    num_positions = 0
    hcpes = np.empty(MAX_MOVE_COUNT, dtype=HuffmanCodedPosAndEval)
    for filepath in csa_file_list:
        parser.parse_csa_file(filepath)
        if parser.endgame not in ('%TORYO', '%SENNICHITE', '%KACHI') or len(parser.moves) < filter_moves:
            continue
        cond = (parser.ratings[0] < filter_rating and parser.ratings[1] < filter_rating)
        if filter_rating > 0 and cond:
            continue
        board.set_sfen(parser.sfen)
        assert board.is_ok(), "{}:{}".format(filepath, parser.sfen)
        # gameResult
        skip = False
        for i, move in enumerate(parser.moves):
            if not board.is_legal(move):
                print("skip {}:{}:{}".format(filepath, i, move_to_usi(move)))
                skip = True
                break

            # hcpe
            board.to_hcp(hcpes[i:i+1])
            hcpes[i:i+1]["bestMove16"] = move16(move)
            hcpes[i:i+1]["gameResult"] = parser.win

            board.push(move)

        if skip:
            continue

        # write data
        hcpes[:i + 1].tofile(f)

        num_positions += i + 1
        num_games += 1

    return num_games, num_positions

if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('csa_dir', help='directory stored CSA file')
    parser.add_argument('hcp', help='hcp file')
    parser.add_argument('--filter_moves', type=int, default=60, help='filter by move count')
    parser.add_argument('--filter_rating', type=int, default=3000, help='filter by rating')
    parser.add_argument('--recursive', '-r', action='store_true')
    parser.add_argument('--limit_moves', type=int, default=40000, help='upper limit of move count')
    parser.add_argument('--before_kachi_moves', type=int, help='output the position until N moves before sengen kachi')

    args = parser.parse_args()

    if args.recursive:
        dir = os.path.join(args.csa_dir, '**')
    else:
        dir = args.csa_dir
    csa_file_list = glob.glob(os.path.join(dir, '*.csa'), recursive=args.recursive)

    with open(args.hcp, 'wb') as f:
        num_games, num_positions = process_csa(f, csa_file_list, args.filter_moves, args.filter_rating, args.limit_moves)
        print(f"games : {num_games}")
        print(f"positions : {num_positions}")

損失計測

 GitHubで公開されている、電竜戦で優勝したGCTのパラメータmodel-model-0000167.onnxを使ってonnxのruntimeで呼び出す形式で行った。dlshogiはValueを[0,1]で出力するので、出力と教師を両方2倍してから1引いて[-1,1]の範囲に直し、自乗誤差を取った。

使用スクリプト(長いので折りたたみ)

import numpy as np
import torch
import torch.optim as optim
from dlshogi.common import *
from dlshogi import serializers
import cppshogi
import argparse
import os
import onnx
import onnxruntime as ort
import torch.nn.functional as F


parser = argparse.ArgumentParser()
parser.add_argument('test_data', type=str, help='test data file')
parser.add_argument('--testbatchsize', type=int, default=32, help='Number of positions in each test mini-batch')
args = parser.parse_args()

model_name = "model-0000167.onnx"
ort_session = ort.InferenceSession(model_name)
cross_entropy_loss = torch.nn.CrossEntropyLoss(reduction='none')
mse_loss = torch.nn.MSELoss()

# Init/Resume
test_data = np.fromfile(args.test_data, dtype=HuffmanCodedPosAndEval)

# mini batch
def mini_batch(hcpevec):
    features1 = np.empty((len(hcpevec), FEATURES1_NUM, 9, 9), dtype=np.float32)
    features2 = np.empty((len(hcpevec), FEATURES2_NUM, 9, 9), dtype=np.float32)
    move = np.empty((len(hcpevec)), dtype=np.int32)
    result = np.empty((len(hcpevec)), dtype=np.float32)
    value = np.empty((len(hcpevec)), dtype=np.float32)

    cppshogi.hcpe_decode_with_value(hcpevec, features1, features2, move, result, value)

    z = result.astype(np.float32) - value + 0.5

    return (features1,
            features2,
            move.astype(np.int64),
            result.reshape((len(hcpevec), 1)),
            z,
            value.reshape((len(value), 1))
            )

itr_test = 0
sum_test_loss1 = 0
sum_test_loss2 = 0
sum_test_loss = 0
with torch.no_grad():
    for i in range(0, len(test_data) - args.testbatchsize, args.testbatchsize):
        x1, x2, t1, t2, z, value = mini_batch(test_data[i:i+args.testbatchsize])
        y1, y2 = ort_session.run(None, {'input1': x1, 'input2': x2})

        t1 = torch.tensor(t1)
        t2 = torch.tensor(t2)
        z = torch.tensor(z)
        value = torch.tensor(value)
        y1 = torch.tensor(y1)
        y2 = torch.tensor(y2)

        itr_test += 1
        loss1 = (cross_entropy_loss(y1, t1)).mean()
        loss2 = mse_loss(y2 * 2 - 1, t2 * 2 - 1)
        loss = loss1 + loss2
        sum_test_loss1 += loss1.item()
        sum_test_loss2 += loss2.item()
        sum_test_loss += loss.item()

    print('test_loss = {:.08f}, {:.08f}, {:.08f} = {:.08f}, {:.08f}'.format(
        sum_test_loss1 / itr_test, sum_test_loss2 / itr_test, sum_test_loss / itr_test))

結果

パラメータ Policy損失 Value損失
Miacis 1.9439 0.5766
dlshogi 1.9273 0.5803

 意外にもほとんど変わらない値であり、Value損失ではMiacis(128ch)の方が低くなっている。

 現状のMiacisが2080ti1枚でfloodgate R3600程度であり、

 dlshogitestの4138とは相当差がある。この差がほとんど探索部分の影響というのはにわかには信じがたいところなのだが。

 説としては3通りか。

  • 実装ミスをしている
  • この検証データ・および損失計測方法は棋力との相関が小さい
  • 本当に推論・探索の速度や効率で大きな差がある

Sharpness-Aware Minimizationの検証

 Sharpness-Aware Minimizationという手法が提案されています。

 詳しい人が説明してくれています(僕もこれで知りました)。

 上記事の

パラメータ周辺での最大の損失を求めて、それが下がる方向でパラメータを更新する

というのが基本的なコンセプトでしょう。

 非公式ですが、再現実装も公開されています。

 それほど複雑でもなかったのでMiacisでも実装してみました。

 以下AobaZeroの棋譜を用いて実験して結果を掲載します。

Policy損失

f:id:tokumini:20210129100227p:plain

 最終的な検証損失

手法 時間(hhh:mm:ss)
通常のSGD 1.950
SAM(SGD) 1.864

 はっきりと改善が見られました。1.8台というのは強化学習を最後まで回しきってなんとか見られるかどうかという値であり、1.86というのはとても小さいという印象です。

 train損失は通常のSGDより悪い値となっていますが、SAMの方では2回目の損失、つまり近傍内で悪化する方向に移動してからの損失を表示しているので妥当なのかなと思います。

Value損失

f:id:tokumini:20210129100250p:plain

 最終的な検証損失

手法 時間(hhh:mm:ss)
通常のSGD 0.6453
SAM(SGD) 0.6520

 Value損失は途中までは良かったんですが、最終的な値は通常のSGDよりも悪化してしまいました。

検証対局

Miacis time = 250msec, YaneuraOu time = 250msec, YaneuraOu Threads = 4,NodesLimit=400000
手法 勝数 引分数 負数 勝率 相対レート
通常のSGD 488 117 395 54.6% 32.4
SAM(SGD) 411 109 480 46.6% -24.0

 残念ながら対局では性能が上がってないという結果になりました。個人的な印象ですが、他の実験を見ていてもPolicy損失よりValue損失の方が重要だという印象があります。

学習時間

 SAMは1回損失・勾配を計算して周囲の最も悪いところに移動した後、もう一度損失・勾配を計算するので単純に考えると2倍の時間がかかります。実際の学習時間は

手法 時間(hhh:mm:ss)
通常のSGD 080:44:58
SAM(SGD) 136:37:22

となり、2倍ではないにしろそこそこの増加はありました。しかし強化学習では「データ生成時間 >> 学習時間」なのでこの程度の増加は許容できるでしょう。

 一応Policy損失の方で良さそうな雰囲気は出ているので強化学習でも試してみようと思います。

256chでの教師あり学習

 普段使っているネットワークがそこまで大きくはないのでもっと大きいネットワークを試したいと思って実験したが、ブログを見るとほとんど同じような実験を以前にもしていた。自分で書いた記事を自分で忘れている。


 上の記事と同様に、普段128chである残差ブロック中のCNNを256chに増やして学習を行った。学習データはAobaZeroの棋譜、検証データはfloodgate2015年の棋譜を使っている。

 上の記事では1,000,000ステップしか学習を回していないところを、今回は3倍の3,000,000ステップまで回した。学習率の減衰は0.6Mステップごとに1/10にする形で行った。つまり学習が終わるまでに等間隔で計4回減衰が発生する。

Policy損失

f:id:tokumini:20210128104326p:plain

 Policyの検証損失はほぼ変わらない。が、点線の方であるtrain損失ではそこそこ差がある点は気になる。AobaZero棋譜への適応はチャンネル数を増やした分良くなったが、floodgateの棋譜はまた性質が異なるので検証損失は伸びなかったと解釈するのが普通だろうか。

Value損失

f:id:tokumini:20210128104333p:plain

初期局面での5秒検討

f:id:tokumini:20210128113128p:plainf:id:tokumini:20210128113143p:plain
左:128ch 右:256ch

 NPSは128chのとき8100.6、256chのとき5457.6

 探索深さにやや差が出ているのは気になるが、偶然の範囲内かどうか。

対局@2080tiマシン

~/Miacis/scripts/vsYaneuraOu.py --time1=250 --time2=250 --NodesLimit=400000 --game_num=1000

f:id:tokumini:20210128104318p:plain

 なぜか128chは途中からしか計測していなかったが、まぁ学習序盤はどうせ弱いのでどうでもいいだろう。

 最終的な対局による性能はほとんど変わらなかった。NPSの低下とValueの精度向上がちょうど打ち消し合うくらいだったということだろう。冒頭の記事でもだいたい同じ感じだったので128ch ~ 192ch ~ 256chでは同じ持ち時間での性能はほとんど変わらないというのがここまでの結果になる。

 では結局どのチャンネル数が良いかというと、それぞれのメリットを考えると

  • 128chにすると学習・生成が速いので実験サイクルは回しやすい
  • 256chの方が対局時のGPU依存部分が多いので、強力なGPUを使った場合での伸びが期待できる
  • 256chの方が探索ノード数が少なく、持ち時間を長くしたときの伸びは低ノード帯の方がやや大きいはず

(三番目の根拠についてはMuZeroの論文のFigure.3)

f:id:tokumini:20210128114702p:plain

という感じなので、大雑把にいろいろ実験するなら128ch、最高性能を目指すなら256chという感覚。

Transformerを用いた探索的NNの学習(成功?編)

 以下の続き。

 前回は上手くいかなかった学習をなんとか多少は改善することができた。修士論文としてはとりあえずこの方針の結果で許してもらいたい、という気持ちです。

 基本的な手法は冒頭で示した前回と変わらず、事前学習したモデルのPolicyが示す確率に従って行動をサンプリングし、局面の系列を得る。このときルート局面から3回動かしたらまたルート局面まで戻す。全体の系列長は10で行ったため、3回動かした局面の系列が3個+1回動かした局面という10個の表現ベクトルがTransformerのEncoder側に入力される。TransformerのDecoder側には常にルート局面の表現ベクトルが入力される。

 前回から変えた点は、

  • LibTorchの1.7.0リリースにより公式のTransformerが使えるようになったのでそれを利用するようにした
  • Positional Encodingをランダム初期化した学習可能な値を用いるようにした
  • 損失計算で今までは10回の系列入力について毎回損失を計算していたが、10系列まとめて入力した最後にだけ損失を計算するように変更した

の3点である。おそらく重要だったのが一番最後の点であり、これにより一気に傾向が変わったような印象がある。また損失計算を最後だけにすると計算も速くなり、事前学習のない場合もなんとか2日程度で学習ができるようになった。

結果

 実験はどれもオセロで行った。

事前学習あり

 まずシミュレータを行うための普通のFeedforwardネットワークを教師あり学習で学習させた。検証損失は以下の通り。

f:id:tokumini:20201115130907p:plain

 最終的に値は0.774083となった。

 パラメータを固定したこのモデルを用いて局面系列のサンプリングおよび入力する局面のエンコードをし、Transformerを学習させた結果が以下となる。(これらは同じ棋譜を用いて同じように検証損失を計算している)

f:id:tokumini:20201115131103p:plain

 オレンジ色の線が事前学習で得られたモデルであり、それより性能が良くなった。

 学習後、最終ステップ時点のモデルにおいて各系列長(探索回数)の時点で切り上げてTransformerに損失を計算させると次のようになった。

f:id:tokumini:20201115132815p:plain

 わりと最後の方でも系列が増えることによる性能向上が見られているので悪くないように思える。

事前学習なし

 事前学習なく、Transformerの学習と同時にサンプリング、エンコード用のモデルも学習させ、Transformerの勾配もエンコーダ側まで流すようにして実験を行った。

f:id:tokumini:20201115133142p:plain

 先程と異なり探索なしのオレンジ線も学習に伴って下がっていくが、事前学習よりも少し悪い程度で留まった(これは事前学習の方がステップ数が多かったという理由が大きそうではある)。Transformerの方の学習も事前学習ありの場合とそこまで変わらない。

 同様に学習済みモデルについて、各系列長(探索回数)の時点でTransformerに損失を計算させると次のようになった。

f:id:tokumini:20201115150650p:plain

 0回の時点でもそこまで悪くないのが多少異なる点ではある。単なる学習のブレか、本質的な違いかはわからない。

 ちなみに事前学習ありのものでも同様だが、系列長10で学習させたものに、学習後無理やり系列長20までサンプリングして与えてみたところ、当然のように性能は悪化した。

f:id:tokumini:20201115150855p:plain

 Positional Encodingのランダムなところが学習されてないので当たり前なのだが、これは本家Transformerの三角関数を用いたPositional Encodingに戻せば汎化できるのだろうか。

今後やること

  • 系列長を20で学習した場合にどうなるか(学習コストが大きいのでやれても30くらいまでかなというところ)
  • そもそもTransformerではなくてLSTMではダメなのか
  • Positional Encodingを三角関数を利用するものに戻してやりなおす

 なにか疑問点があればコメントいただければ僕の修士論文が少しだけ良くなるかもしれません。

教師あり学習をした後の強化学習

 カテゴリカルモデルで教師あり学習をすると勝ち負け引分に相当する値の領域にしか教師信号が発生しないため分布が有効活用できない問題がある。

 (無理やり割引率を導入すれば初期局面に対しても値を与えられるが、それは恣意的なのでできるだけしたくない)

 強化学習ではTD(λ)を用いることでこれを回避できるため、教師あり学習をした後に強化学習を用いて追加学習することを試みた。

教師あり学習

 AobaZeroの棋譜を用いて教師あり学習を行った(具体的な手順などは以前の記事を参照)

 Vadlidationにはfloodgate2015年の棋譜から片方の対局者のレート2800以上のものを抽出し、利用した。

結果

f:id:tokumini:20201106164704p:plainf:id:tokumini:20201106164707p:plain
左:Policy損失 右:Value損失

 以前行ったスカラーモデルでの結果と比べて、Policy損失はほぼ変わらず、Value損失はやや悪化となった。

 参考:同条件で学習した場合の最終ステップでの損失値

モデル Policy損失 Value損失
スカラー 1.949150 0.618593
カテゴリカル 1.949936 0.645331

強化学習

 AlphaZeroとほぼ同等の800回探索の自己対局を教師とする強化学習を行った。教師信号を作る際にTD(λ)を用いている。学習は継続中だが、とりあえず50000ステップでの結果が出たのでそれをまとめておく。

結果

 50000ステップでの損失値(この検証データは教師あり学習のときと完全に同一のfoodgate2015年データを用いて行っている)

モデル Policy損失 Value損失
強化学習 1.949936 0.645331
強化学習 1.902164 0.693806

 Policy損失はやや改善したが、Value損失がかなりはっきり悪化している。

初期局面での推論結果

f:id:tokumini:20201106171844p:plainf:id:tokumini:20201106171840p:plain
左:強化学習前 右:強化学習

 AobaZeroの棋譜教師あり学習を行うと初期局面での評価値がやけに先手に大きく振れるのだが、それが是正されている。また、分布の形状もかなり想定に近いものになっている。

検証対局

 持ち時間0.25秒でYaneuraOu(評価パラメータはKristallweizen・NodesLimit400000)と1000局の対局を行った結果

モデル 勝ち 引分 負け 勝率 Eloレート差
強化学習 433 131 436 49.85% -1.0
強化学習 348 1 651 34.85% -108.7

 レートが100以上下がった。探索込みではやはりValue損失が大きく効いてくる印象ではある。

 他に目を引くのが引分数の大幅な減少で、ほぼ引分が起こらないというのはゼロから強化学習をした場合とほぼ同じ傾向である。

 全体的にもとのパラメータの影響がかなり薄れている印象を受けた。もとの教師あり学習が300万ステップであるのに対して、5万ステップの強化学習をするだけでこれだけ変わってしまうのは学習率が大きすぎたのかもしれない。

Transformerを用いた探索的NNの学習(失敗編)

 以下の続き。

 前回までの手法では多少改善は得られたものの、探索回数を増やすほど性能が良くなるような性質は得られなかった。またこのやり方では本質的にゲームであることを利用しておらず、一般的に画像認識等でも利用できるものになっており、スコープが広すぎる気がしていた。よってここのところはシミュレータを利用してきちんと盤面遷移を含める手法について検討している(単に迷走中と言ったほうが適切かもしれない)。

f:id:tokumini:20201019095735p:plain

 この雑な図で伝わるかはわからないが、今は赤枠のあたりを探っている。

 問題を簡単にするため、探索部分は事前学習したモデルを用いることにした。

f:id:tokumini:20201019102446p:plain

f:id:tokumini:20201019102514p:plain

 注意点として、このやり方だと一つの読み筋を突き進んでいくことしかできない。よって行動はSoftmax層の出力から確率的にサンプリングすることにし、数手(今回は3手)進むごとにルート局面に強制的に戻すことで深さ3の読み筋をたくさん並べることにした。

f:id:tokumini:20201019102809p:plain

 これを時系列モデル、今回はTransformerに入力することで推論をする。src側に探索の系列、target側にルート局面の分散表現を入れて、ルート局面に対するPolicyを推論させる。

f:id:tokumini:20201019103033p:plain

実験

 オセロにおける教師あり学習で実験を行った。事前学習ではPolicy損失は0.776090になった。

 Transformerの学習推移は次のようになった。

f:id:tokumini:20201019103215p:plain

 n回探索は、src側の系列長がn個であることを意味する。

 線が重なっていてさっぱり違いがわからない。つまりsrc側の情報を有効活用できていないということなのだろう。

 最終ステップでの損失を取り出してプロットすると次のようになった。

f:id:tokumini:20201019103219p:plain

 探索しないほうがマシという結果になっている。

所感

 そこまで大幅な改善を期待していたわけではないが、少しくらいは良くなるのではないかと思っていたのでこの結果は意外だし、何か実装ミスがないか不安になるところではある。特にTransformer部分は

などからコピペして、LibTorch1.6.0環境ではそのままでは動かなかったのでちょっと改造して実装したわけだが、そのあたりでミスしている可能性がなくはない。特にLibTorch1.6.0ではMultiheadattentionモジュールにバグがあり、そこも手動で直したのでそのあたりがどうか……。

 LibTorch1.7.0では直っていると思われるのでそれを待ちたいところでもあるが、修論締め切りは待ってくれない。

AobaZeroの棋譜を用いた教師あり学習 192ch

 今回は普段128chである残差ブロック中のCNNを192chに増やして学習を行った。

学習結果

f:id:tokumini:20201005082025p:plainf:id:tokumini:20201005082029p:plain
左:Policy損失 右:Value損失

 どちらも学習損失では192chの方がやや良いのだが、検証損失ではそこまで差が出ていない。

 学習時間はやや長くなり45時間、約二日であった。

検証対局

Miacis側0.25秒-Kristallweizen側200Kノード

f:id:tokumini:20201005082125p:plain

 192chにするとNPSも落ちるので総合的に見て棋力が向上している感じはあまりしない。しかし学習量を増やした分の影響で少しだけレートが高くなっていそうではある。

Miacis側1秒-Kristallweizen0.4秒

 基本的にMiacisのパラメータはCategoricalモデルでチューニングしたものであり、Scalarモデルではもっと良いパラメータがある可能性が残されていたため、今回は特に C _ {PCUT}についていくつかの値を試してみた。

 C _ {PCUT} 勝数 引分数 負数 勝率 相対Eloレート
2000 262 101 637 31.2% -137.0
2500 159 76 341 34.2% -113.7
3000 315 132 553 38.1% -84.3
3500 331 117 552 39.0% -78.1
4000 315 168 517 39.9% -71.2
4500 309 153 538 38.6% -81.0

 MiacisではValueの学習をtanhを使い-1から1で学習していて、探索中も0から1に正規化することはなくそのまま使用しているため、正規化しているAlphaZeroに比べれば C _ {PCUT}は2倍になっていておかしくない。AlphaZeroで C _ {PCUT} = 2000と考えると、少し大きい気はするがそんなもんかなという気もする。

 最も良かった結果について、強化学習モデルとの比較をすると

 C _ {PCUT} 勝数 引分数 負数 勝率 相対Eloレート
教師あり学習モデル 315 168 517 39.9% -71.2
強化学習モデル 530 1 469 53.0% 21.2

となる。レート差は90ちょいといったところで、まだ同等というには少し遠い。

 注目すべきは引分数で、極端に異なっている。入玉形を目指しやすいかどうかというあたりの差だと予想しているが、検証対局はAyaneでの自動対局で行っており棋譜も残していなかったので正確なことはまだ不明。