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通りか。

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