同じデータに対して評価関数の性能比較を行った。
使用データは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程度であり、
Miacisが自前で強化学習した場合floodgateでレート3600弱で、AobaZeroから学習したパラメータも結構近いところに来ているhttps://t.co/cDgo9UlZgVhttps://t.co/sIelnptVX8
— t (@tokumini_ss) 2020年11月4日
dlshogitestの4138とは相当差がある。この差がほとんど探索部分の影響というのはにわかには信じがたいところなのだが。
説としては3通りか。
- 実装ミスをしている
- この検証データ・および損失計測方法は棋力との相関が小さい
- 本当に推論・探索の速度や効率で大きな差がある