TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

C#からPythonへポインタ渡しする

以前にPython for .NETを使用することで、C#Pythonを同一プロセスで動かせることを紹介した。

C#からPythonへの呼び出し時に、引数と戻り値は、自動的にマーシャリングが行われる。
ここで、C#の型からPythonのネイティブな型に変換されるのは、スカラ型とリスト型に限られる。
残りは、C#のクラスをラッピングしたクラスになり、実態はC#側で処理されるオブジェクトになる。

課題

Unityで作成されたC#のプログラムから、PyTorchなどのPythonフレームワークを使用してモデルの学習を行いたい場合を想定する。
このような場合、C#で作成した入力特徴量をPythonに渡す必要がある。

入力特徴量をPyTorchのTensorに変換するには、リスト型か連続したメモリ領域である必要がある。
リスト型にするには、C#からList型で渡せば、自動でマーシャリングされるため、

torch.as_array(input)

とすれば、明示的な変換なしに変換できる。

しかし、入力特徴量は通常サイズが大きく、呼び出しのたびに、List型からPythonのlistへのマーシャリングが発生すると遅くなる。

できれば、連続したメモリ領域をそのまま渡したい。

なお、C#の配列(float[])を渡した場合、Single[]というラッピングされたクラスになるため、torch.as_arrayには入力できない。

ポインタ渡しする方法

Pythonには、ctypesというC言語とのインターフェースのためのライブラリが用意されている。
これを使用することで、ポインタを扱うことができる。

C#にも、オブジェクトのメモリ領域をポインタとして扱う機能がある。
ただし、ポインタとして扱っている間は、ガベージコレクションの対象にならないように、注意が必要である。
具体的には、以下の2つの方法がある。

GCHandle.Allocを使う

以下のコード例のように、GCHandle.AllocにPinnedを指定して、ガベージコレクションの対象にならないようにしたオブジェクトのアドレスを取得するためのハンドルを取得し、AddrOfPinnedObject()でポインタを取得する。
ポインタの使用が終わったらFree()を呼び出す。

using System.Runtime.InteropServices;


var array = new float[] { 1.0f, 2.0f, 3.0f, 4.0f };
var handler = GCHandle.Alloc(array, GCHandleType.Pinned);
var pointer = handler.AddrOfPinnedObject();

// ポインタを使用
python_module.function((Int64)pointer, array.Length);

handler.Free();
fixedを使用する

C#には、ポインタを扱うためのfixedという言語機能がある。

var array = new float[] { 1.0f, 2.0f, 3.0f, 4.0f };
fixed (float* pointer = &array[0])
{
    // ポインタを使用
    python_module.function((Int64)pointer, array.Length);
}                  

Free()が必要ないため、解放漏れの心配がなくなる。
ただし、unsafeを指定することが必須になる。
なお、GCHandle.Allocを使用する場合も、unsafeなことを行っているので、unsafeを指定しておく方が正しいと思われる。

Python側でポインタを扱う

次に、Python側で数値として渡されたポインタを扱う方法について述べる。

上で述べたctypesを使用することで、ポインタを扱うことができるが、PyTorchでは直接ポインタを入力することができたいため、一旦Numpyのオブジェクトにする。
Numpyには、ポインタで渡されたメモリ領域をそのままNumpyのオブジェクトとして使用する方法がある。

import numpy as np
import ctypes

def function(pointer, size):
    array = (ctypes.c_float * size).from_address(pointer)
    data = np.ctypeslib.as_array(array)

ctypesのfrom_address()で、引数で渡された数値のポインタをPOINTER型に変換し、np.ctypeslib.as_array()でNumpyのオブジェクトに変換している。

これを、PyTorchのTensorに変換することができる。

    tensor = torch.as_tensor(data)

ここまでで、C#から渡されたメモリ領域のコピーが発生してないことに注意して欲しい。
つまり、

    tensor[0] = 10

とすると、C#側のarrayの値も変更されることになる。

まとめ

Python for .NETを使用して、C#からPythonへポインタ渡しする方法について紹介した。
ポインタ渡しすることで、マーシャリングやメモリコピーが発生しないため、高速に処理できる。
ポインタを扱えるのは、同一プロセスでランタイムを動かせるPython for .NETのメリットの一つである。


アプリケーションによっては、特徴量の加工の処理が多くなるので、Pythonで処理すると遅くなる。
C#側で加工を行い、そのままTensorにできる状態にして、メモリ領域をポインタで渡せばPython側での加工をなくせるので高速に処理できる。
Tensorは次元の大きい疎ベクトルになることもあるので、ここでマーシャリングが入ってしまうとC#側で加工するメリットが失われてしまう。
その意味でも、ポインタをそのまま渡せるのがベストである。

ソース

github.com

【将棋AI】N駒関係をMulti-Head Self-Attentionで学習する 続き3

前回、入力特徴量の与え方でMulti-Head Self-Attentionの精度を向上できることを確認したが、DCNN(Resnet 10ブロック)に比べて精度がでないことが分かった。

今回は、Multi-Head Self-Attentionの出力をDCNNに入力して、Multi-Head Self-AttentionとDCNNを組み合わせることで精度が改善できないか試した。

Multi-Head Self-AttentionとDCNNを組み合わせる方法

Multi-Head Self-Attentionの出力は、盤上の座標ごとの特徴ベクトル(64次元)となっているので、これを2次元平面の形にならべて、64チャネル、9×9の特徴マップに変換する。
持ち駒は、持ち駒の種類と枚数ごとの特徴ベクトル(64次元)となっているので、そのままでは2次元の平面状にすることができない。
そこで、各特徴ベクトルの平均をとって、9×9の各位置に同じものを並べるようにする。
これを、盤上の座標ごとの特徴ベクトルとチャネル方向に連結して、DCNNに入力する。
DCNNの入力層は、チャネル数を64×2→192に変換するための畳み込み層として、それを10ブロックのResNetに入力する。

結果

モデル 訓練平均損失(方策, 価値, 評価値, 合計) 評価損失(方策, 価値, 評価値, 合計) 正解率((方策, 価値)
Multi-Head Self-Attention+DCNN 1.088, 0.406, 0.435, 1.505 1.187, 0.707, 0.803, 1.927 0.334, 0.604
DCNNのみ 1.071, 0.406, 0.435, 1.487 1.202, 0.606, 0.670, 1.829 0.328, 0.651

DCNNのみの場合に比べて、方策の精度が少し上がって、価値の精度が下がっている。評価損失の合計は低下している。
Multi-Head Self-Attentionによってかえって精度が下がっていそうである。

持ち駒の入力方法の変更

持ち駒の特徴ベクトルを平均化していることが良くない可能性があるので、平均化しないで、出力ユニット数がDCNNのチャネル数(192)と同じ全結合層を通して、それを9×9の各位置に同じものを並べて、盤上の座標ごとの特徴ベクトルとの和をとるようにした。
それを10ブロックのResNetに入力する。

結果
モデル 訓練平均損失(方策, 価値, 評価値, 合計) 評価損失(方策, 価値, 評価値, 合計) 正解率((方策, 価値)
Multi-Head Self-Attention+DCNN 1.053, 0.403, 0.432, 1.466 1.167, 0.618, 0.695, 1.811 0.338, 0.643

評価損失の合計は、DCNNのみの場合よりも良くなっているが、誤差の範囲である。

追加で数回測定してみた。

モデル 訓練平均損失(方策, 価値, 評価値, 合計) 評価損失(方策, 価値, 評価値, 合計) 正解率((方策, 価値)
Multi-Head Self-Attention+DCNN(2回目) 1.051, 0.402, 0.431, 1.463 1.166, 0.613, 0.675, 1.800 0.339, 0.642
Multi-Head Self-Attention+DCNN(3回目) 1.046, 0.403, 0.432, 1.460 1.179, 0.644, 0.728, 1.851 0.334, 0.626

3回目は悪くなっているので、やはり誤差の範囲と言えそうである。

まとめ

Multi-Head Self-Attentionで、離れた位置にある駒の依存関係を学習することで、DCNN単体よりも精度が上がることを期待していたが、少なくとも悪くはならないが精度が大幅に改善することはなかった。
DCNN単体でも、層を深くすることでN駒の関係が学習できており、Multi-Head Self-Attentionを追加しても効果がなかったのかもしれない。


Multi-Head Self-Attentionは有望なアイディアと思っていたが、実験してみると、効果はあるものの、DCNNの方が強力だということがわかった。
2次元の座標で表せるゲームでは、積極的にDCNNを使った方が良さそうである。

カードゲームのような2次元の座標で表せないゲームでは、Multi-Head Self-Attentionも活用できそうである。
ハースストーンの勝率予測に、Multi-Head Self-Attentionを使えないか試してみたい。

【将棋AI】N駒関係をMulti-Head Self-Attentionで学習する 続き2

前回に引き続き、将棋AIへのMulti-Head Self-Attentionの適用を試してみた。

前回は、dlshogiと同じ入力特徴量を使用したが、Multi-Head Self-Attentionに合わせて以下の変更を行った。

  • 各位置の特徴ベクトルに位置の情報を入力する
  • 持ち駒の枚数の特徴ベクトルをすべて1ではなく、持ち駒の枚数に対応するワンホットベクトルとする
各位置の特徴ベクトルに位置の情報を入力する

段と筋をそれぞれ、9次元のワンホットベクトルとして、各位置の特徴ベクトルに連結する。

これは、自然言語処理のTransformerで、単語の位置をPositional Encodingに相当する。
Transformerの論文では、sin関数とcos関数を使用して埋め込みベクトルと同じ次元にエンコードしたベクトルを加算している。

将棋の場合は、2次元座標であることと、加算よりも連結の方が情報の欠落がないので、上記の通り連結することにした。

持ち駒の表現

持ち駒は、持ち駒の枚数(歩は最大8枚)に応じた要素のみを1としたワンホットベクトルで表す。
先後を別にして、各28次元で、合計56次元となる。
その特徴ベクトルを、持ち駒の枚数に対応した位置の特徴ベクトルに設定する。

特徴ベクトルは、盤上の位置でも持ち駒の位置でも同じ次元とするが、それぞれ値を設定する場所を分ける。
特徴ベクトルの次元は、137次元(=盤上62次元 + 座標 18次元 + 持ち駒 56次元)となる。

測定結果

入力特徴量を上記の通り変更して、前回と同じ条件で測定した。

モデル 訓練平均損失(方策, 価値, 評価値, 合計) 評価損失(方策, 価値, 評価値, 合計) 正解率((方策, 価値)
Multi-Head Self-Attention(変更後) 1.933, 0.440, 0.465, 2.382 1.849, 0.921, 1.011, 2.800 0.214, 0.540
Multi-Head Self-Attention(前回) 1.961, 0.438, 0.464, 2.408 1.687, 0.653, 0.714, 2.361 0.244, 0.622

精度が上がるという予想に反して、前回よりも精度が下がってしまった。
特徴ベクトルの次元が前回の64次元から136次元に増えるため、モデルのパラメータが大幅に増えていることが影響していそうである。

次元圧縮

そこで、特徴ベクトルの次元を前回と同じ64次元に、次元圧縮してみた。

具体的には、136次元の特徴ベクトルを全結合層を通して、64次元にする。
全結合層のバイアスと活性化関数はなしで、モデルの重みは、入力する単語の位置によらず共有する。

f:id:TadaoYamaoka:20200719142916p:plain

自然言語処理の単語の埋め込みと同じような処理を行っている。

変換した埋め込みベクトルをそのままTransformerに入力すると、学習が不安定になったため、Batch Normalizationを追加した。

次元圧縮して入力次元を前回と揃えた結果は、以下の通りになった。

モデル 訓練平均損失(方策, 価値, 評価値, 合計) 評価損失(方策, 価値, 評価値, 合計) 正解率((方策, 価値)
Multi-Head Self-Attention(次元圧縮後) 1.823, 0.425, 0.451, 2.257 1.613, 0.627, 0.692, 2.262 0.259, 0.634
Multi-Head Self-Attention(前回) 1.961, 0.438, 0.464, 2.408 1.687, 0.653, 0.714, 2.361 0.244, 0.622

次元圧縮を行うと、前回よりも精度が向上している。
疎ベクトルの入力を埋め込み表現にすることは効果があるようだ。


ここで、前回も次元64のままでも埋め込み表現にすれば精度が上がって、今回試した入力の特徴ベクトルの変更による改善ではないのではないかという疑問が生じる。
そこで、前回の入力特徴ベクトルを埋め込み表現にした場合も測定してみた。

モデル 訓練平均損失(方策, 価値, 評価値, 合計) 評価損失(方策, 価値, 評価値, 合計) 正解率((方策, 価値)
Multi-Head Self-Attention(前回+埋め込み表現) 1.860, 0.428, 0.454, 2.297 1.649, 0.654, 0.725, 2.328 0.251, 0.612

方策の精度が少し上がっているが、逆に価値の精度は下がっている、評価損失の合計はほとんど差がない。

したがって、精度向上は、埋め込み表現による向上よりも、入力特徴量の変更によるものと言える。

まとめ

入力特徴量に、位置を表す情報を加えて、持ち駒の枚数の表現を駒の種類と枚数を表すワンホットベクトルにすることで、精度が向上した。
ただし、入力特徴を次元の高い疎ベクトルのまま入力すると精度が下がる。
対策として、疎ベクトルを低次元に投影して、埋め込み表現にすると良い。


今回の結果でも、前回測定したdlshogiの10ブロックのResnetと比較すると精度が低い。
Transformerを多層にする、出力の全結合層の層数、ユニット数を増やすといったことも試してみたが、Resnetを超えることはできなかった。
将棋のような2次元座標で表せるゲームの場合、畳み込み層は強力なようだ。

次回は、Transformerの結果を、畳み込み層の入力とするということを試してみたい。

【将棋AI】N駒関係をMulti-Head Self-Attentionで学習する 続き

前回考察した将棋AIへのMulti-Head Self-Attentionの適用を試してみた。

実装を簡単するために、dlshogiで使用している入力特徴量と出力をそのまま使用した。

入力特徴量

  • 各駒の配置
  • 持ち駒の枚数
  • 駒の種類ごとの利き
  • マスごとの効き数

※王手の特徴量は除いた

これらを、位置ごとの特徴としてMulti-Head Self-Attentionに入力する。

位置は、盤上の81マスと、持ち駒の種類と枚数ごと×2(先後)として、各位置を自然言語処理の各単語のようにして扱う。
各位置には、その位置での上記の特徴量を表すベクトルを入力する。
14の駒の種類は先後で分けて、効きも先後に分けた駒ごと、効き数は3つまで、合計62次元となる。
ヘッド数で割り切れる必要があるため、パディングして64次元とする。

例えば、以下の局面の、
f:id:TadaoYamaoka:20200716213025p:plain
5五の位置の特徴ベクトルは、
f:id:TadaoYamaoka:20200716213354p:plain
となる。

持ち駒の位置は、歩の持ち駒は8枚までとして、先後合わせて持っている枚数と位置を対応させて、(8(歩)+4(香)+4(桂)+4(銀)+4(金)+2(角)+2(飛))×2=56個の位置で表す。
対応する持ち駒を持っていればすべて1のベクトルとする。

自然言語処理に例えると、64次元の埋め込みベクトルで表した単語を81+56=137個並べた文を入力するイメージとなる。

出力

指し手(方策)と勝率(価値)を出力する。
指し手は、移動先×移動方向(10方向+持ち駒の種類)で表す。
勝率は、[0, 1]のスカラ値とする。

モデルの実装

Multi-Head Self-Attentionは、PyTorchのTransformerEncoderと TransformerEncoderLayerを使用して実装した。
Multi-Head Self-Attentionの層数は1で、ヘッド数は8とした。
Multi-Head Self-Attentionの出力を、方策と価値に分岐させて、それぞれ全結合層(256ユニット)をつなげて出力層につなげた。

TransformerEncoderは、入力と出力の次元が、畳み込み層や全結合層などと異なり、(単語数, バッチサイズ, 特徴ベクトルの次元)となるので注意が必要である。
全結合層につなげる前に、permuteで、次元を交換する必要がある。

学習方法

dlshogiと同じ学習則とした(SWAを除く)。
MomentumSGD、学習率0.1、バッチサイズ1024で最適化した。

学習結果

dlshogiの強化学習で生成した2,048,000局面で訓練して、floodgateの棋譜からサンプリングした856,923局面で評価を行った。

比較のために、dlshogiの10ブロックのResnetで同一条件で学習した結果も記載する。

モデル 訓練平均損失(方策, 価値, 評価値, 合計) 評価損失(方策, 価値, 評価値, 合計) 正解率((方策, 価値)
Multi-Head Self-Attention 1.961, 0.438, 0.464, 2.408 1.687, 0.653, 0.714, 2.361 0.244, 0.622
Resnet(10ブロック) 1.071, 0.406, 0.435, 1.487 1.202, 0.606, 0.670, 1.829 0.328, 0.651

方策の正解率24%、価値の正解率62%となっており、ある程度学習できている。

Resnet(10ブロック)の方が、精度が高くなっているが、Multi-Head Self-Attentionの方が層数は少ない。
学習時間も短くなっている。

学習時間の比較
モデル 学習時間
Multi-Head Self-Attention 0:04:46
Resnet(10ブロック) 0:13:42
層数

Multi-Head Self-Attentionの層数を2に増やした場合についても測定した。

モデル 訓練平均損失(方策, 価値, 評価値, 合計) 評価損失(方策, 価値, 評価値, 合計) 正解率((方策, 価値)
Multi-Head Self-Attention(2層) 1.859, 0.430, 0.456, 2.298 1.608, 0.632, 0.697, 2.262 0.263, 0.633
モデル 学習時間
Multi-Head Self-Attention(2層) 0:07:07

2層にした方が少し精度が上がっている。
しかし、学習時間は約1.5倍に増える。

考察

Resnet(10ブロック)よりも精度が低いが、Multi-Head Self-Attentionでも将棋の学習ができることが確かめられた。
1層のMulti-Head Self-Attentionでも効果がある。

今回dlshogiの特徴量をそのまま使用したが、入力特徴量は、Multi-Head Self-Attentionに合わせて変更した方がよいと考える。

Positional Encorder

各位置の特徴ベクトルには、位置の情報を含んでおらず、同じ特徴ベクトルが異なる位置にあっても同じ出力ベクトルになる。
全結合層では各ユニットと位置が対応するので、モデル全体としては完全に位置を無視しているわけではない。

しかし、位置により駒の価値も異なるので、Multi-Head Self-Attentionへの入力にも位置を表す情報があった方がよいと考える。
Transformerでは、Positional Encorderの出力と単語の埋め込みベクトルの和を入力している。
将棋では9×9の座標で位置を表せるため、段と筋をそれぞれワンホットベクトルで表して連結するのが良さそうである。

持ち駒の特徴ベクトル

持ち駒の特徴ベクトルは、枚数に対応する位置の特徴ベクトルをすべて1にしたが、1枚目の歩、2枚目の歩で価値は異なるので、それらをワンホットベクトルで表した方が良いかもしれない。

これらを変更して精度が上がるか後日、検証してみたい。

検証に使用したソースコード

github.com

【将棋AI】N駒関係をMulti-Head Self-Attentionで学習する

こないだ参加したハースストーンのAIコンペの関連論文を読んでいて、個人的に興味深い論文があった。

Helping AI to Play Hearthstone using Neural Networks

この論文では、ハースストーンのゲーム状態からニューラルネットワークを使って勝率を予測する方法について提案している。
ゲーム状態は、単純化して、2人のプレイヤーがそれぞれフィールドに出した最大7体のミニオンで表現している。
ミニオンはヘルス、攻撃などの7つの属性に対応する特徴量で表現される。
これらのミニオンの特徴量をニューラルネットワークに入力して勝率を予測している。

この論文で面白いのは、ニューラルネットワークに畳み込み層を使用して、ミニオン同士の関係を学習できるようにしている点である。
具体的には、自分と相手のミニオンの位置(1~7)が同じミニオン同士をペアにして1次元のテンソルとし、各位置の1次元テンソルを行方向にならべることで2次元のテンソルとし、畳み込みで処理できるようにする。
次に、自分のミニオンの位置をサイクルさせて(1を2、2を3、・・・、7を1にする)、同様に2次元のテンソルを作り、このサイクルを6回繰り返してできた6個(最初のを合わせると7個)の2次元テンソルをチャネル方向に並べることで、3次元テンソルとして畳み込み層への入力としている。
サイクルさせる処理のことをPCS(partial cyclic shift)演算と呼んでいる。
f:id:TadaoYamaoka:20200712135847p:plain

畳み込み層のカーネルには、[1, 14(ミニオンのペアの特徴量), d(任意フィルタ数)]が使われる。
行方向のカーネルサイズは1なので、列方向(ミニオンのペア)にのみフィルタが適用される。
(隣のミニオンの同士の関係を学習するために[2,14,d]というフィルタもある)

論文には直接書かれていないが、これはつまり、自分のミニオンと相手のミニオンの2駒関係を畳み込み層で学習していると解釈できる。

将棋AIで試せないか

AlphaZeroのCNNでは、入力層は、盤面の座標に対応しており、カーネルサイズは3×3なので、畳み込み層では主に近くにある駒の関係が学習される。
深い層では離れた駒の関係を学習しているかもしれないが、CNNによる画像認識では局所的な特徴量が学習されるという指摘がある。
Deep convolutional networks do not classify based on global object shape

そのため、離れた駒の関係を直接学習できるようなニューラルネットワークの方が好ましいかもしれない。

そこで、将棋AIでも2駒関係を畳み込み層で学習することができないか考察してみた。

上記の論文と同じように2駒関係を畳み込み層で学習しようとすると、駒を14次元ワンホットベクトルで表して、自分と相手の駒のペアを28次元のベクトルで表し、座標に対応する駒のペアを行方向に並べればよい。
ただし、サイクルさせると自分の駒の位置の情報が消えてしまうため、位置を表す情報も自分の駒の表現に加える必要がある。
また、自分の駒同士、相手の駒同士の関係は学習できないため、駒のペアに自分の駒同士、相手の駒同士のペアも考慮した方がよさそうだ。

この方法では、持ち駒の位置も考慮すると行の数が多くなり、PCS演算でサイクルさせる回数も多くチャネル数も多くなり効率が悪そうである。
また、将棋ではある位置に駒がない場合の方が多いので、畳み込み層で処理するには無駄が多い。
(NNUEのように疎ベクトルを差分計算した方がよっぽど良い。)

CNNの代わりにSelf-Attentionを使う

そんなことを考えていてひらめいたのが、PCS演算ではなく、Multi-Head Self-Attentionを使えばどうだろうというアイディアだ。

Multi-Head Self-Attentionは、位置が離れた要素の関係も学習するCNNのようなものと解釈できる。

Multi-Head Attentionの説明に、Transformerの論文の図を引用する。
(Multi-Head Attentionの詳細は説明がめんどうなので省略)
f:id:TadaoYamaoka:20200712143048p:plain
Self-Attentionでは、この図のQKVが同じ入力になる。

右の図のQKVの後につながるLinearが学習する関数となり、畳み込み層のフィルタのパラメータに該当する。

入力には、すべての駒を駒の種類を表すワンホットベクトルと、座標と保持しているプレイヤーをPositional Encorderでエンコードしたものの和や連結で表現して入力することができる。
もしくは、Positional Encorderを使わずに、駒ごとの位置に対応する要素を駒がある場合に1にしたベクトルとしてもよいかもしれない。

これをMulti-Head Self-Attentionで処理することで、離れた駒であっても依存関係を学習できる。
また、2駒の依存関係に限らないため、N駒の関係を学習できる(と思う)。

まとめ

以上、つらつらとアイディアだけを書いてみた。
効果があるかは試してみないと分からないので、(やる気がでれば)後日試してみる予定である。

ハースストーンのAIコンペ

不完全情報ゲームのAIの論文を調べていて、たまたまハースストーンのAIコンペがあることを知ったので、試したいこともあったのでさくっとMCTSのプログラムを作って提出してみた。

ハースストーンは、相手の手札や山札は見ることができない不完全情報ゲームで相手の隠れた情報を予測するなどしないと探索を行うことができないという難しさがある。
また、行動の結果が、確率的な場合がある。

去年のプログラム

AIコンペは、2018年から毎年開催されているようで、昨年の優勝プログラムのソースコードも確認できる。
さぞかしすごいプログラムなのかと思って確認してみたら、拍子抜けするくらい簡単なコードだった。

ハースストーンは自分のターン内でも複数回行動が必要なため、自分のターン内だけでも探索するだけでも有効である。
昨年の優勝プログラムは、自分のターン内だけをMin-Max探索(自分ターン内だけなので実際Maxのみ)して、終端ノードで評価関数で評価するだけという単純なプログラムだった。
確率的状態遷移も考慮していない。
評価関数もヒーローのHP差、ミニオンのHPと攻撃の差をカウントしただけのシンプルなものである。


2位のプログラムは、MCTSを使用して、自分のターン内だけ探索するプログラムである。
ゲームの終端までシミュレーションを行わず、自分のターン内終わりで評価関数で評価した値をバックアップしている。
1ターンの途中で評価を行うと誤差が大きいため、自分のターン内終端まではロールアウトポリシーを使用してロールアウトを行っている。
評価関数とロールアウトポリシーのパラメータ数は、上記の優勝プログラムよりは多く、作りこまれている感じがする。

試したこと

AIコンペに気付いたのが締め切りの2週間前だったので、2位のMCTSのコードをベースにして、評価関数を優勝プログラムのものを使用して、試したかった内容だけ追加した。

試したのは、以下の内容である。

  • 自分のターン内でカードを使用する順番が違うだけで同じノードになることが多いため、ハッシュマップでノードを合流できるようにする
  • 自分のターン内ツリーの再利用する

ハッシュマップを使用することで、確率的な遷移がある場合も複数回シミュレーションされることで、親ノードには期待値がバックアップされることになる。
また、ツリーの再利用も行いやすくなる。

ハッシュコードの生成

将棋や囲碁では、座標と駒の組み合わせにゾブリストハッシュを使用して、XOR演算でハッシュコードを差分を計算していくことができる。
ハースストーンでは、ミニオンにHPやエンチャントや状態異常ステータスなどがある。
ハースストーンで同じようにゾブリストハッシュを使用しようとすると、位置×ミニオン、位置×HPの値、位置×エンチャントの種類などにゾブリストハッシュを割り当てることになる。

しかし、コンペでは、ゲームのシミュレータがエージェントからブラックボックスの扱いになっているので、差分計算のようなことはできない。
また、カードの種類も事前には分かっていない。
そのため、カードIDの文字列、HPの値、位置×エンチャントの種類などから、動的にハッシュコードを生成することにした。

具体的には、C#の文字列のハッシュコードを生成する処理を参考に、

((hash1 << 5) + hash1) ^ 値1
((hash2 << 5) + hash2) ^ 値2
...
return hash1 + hash2 *  * 1566083941

という処理で64bitのハッシュコードを生成するようにした。
1566083941は、線形合同法のランダム生成で使われる値で、この値に適当な数を掛けると上位ビットがランダム性のある値になる。

手札については順番が入れ替わっても同じ状態としたかったため、それぞれのカードのハッシュコードを生成して、それらの和を手札のハッシュコードとした。
(テストしたところ、ハッシュの衝突が起きることがわかったので、HPなどの値には1566083941を掛ける、「((hash2 << 5) + hash1)」としてhash1とhash2を関連させるなどして衝突がおきにくくした。)


ハッシュコードの衝突が起きないように実装できたところで力尽きたので、評価関数はいじることなく提出した。
去年の優勝プログラムには3割くらいしか勝てていないのでたいして強くないが、ハッシュコードについての知識が増えたので良しとしよう。

提出したコード:
HearthstoneAICompetition/MyAgent.cs at master · TadaoYamaoka/HearthstoneAICompetition · GitHub

C#のオブジェクトをダンプする

C#のオブジェクトに格納されている値を調べたいときに、VS Codeなどのデバッガを使えば確認することができるが、オブジェクトの階層が深い場合は、テキストファイルにダンプして確認したくなる。

ObjectDumper.NET

方法がないか調べたところ、「ObjectDumper.NET」というツールが見つかった。
.NET Coreに対応しており、「dotnet add package ObjectDumper.NET」でインストールできる。

dotnet search ObjectDumper」でNuGetのパッケージを検索すると、他にも見つかるが、階層の制限や、除外するプロパティの設定ができるのは、ObjectDumper.NETだけだった。

privateフィールドのダンプ

ObjectDumper.NETは、publicなフィールドしかダンプできないため、privateフィールドもダンプしたい場合は、ソースの修正が必要になる。

GitHubからソースをクローンして、「ObjectDumper/Internal/ObjectDumperConsole.cs」を以下の通り編集する。

var publicFields = element.GetType().GetRuntimeFields().Where(f => !f.IsPrivate);

var publicFields = element.GetType().GetRuntimeFields()/*.Where(f => !f.IsPrivate)*/;

また、除外設定が、フィールドに対しては効かないので、直後に以下の行を追加する。

if (this.DumpOptions.ExcludeProperties != null && this.DumpOptions.ExcludeProperties.Any())
{
    publicFields = publicFields
        .Where(p => !this.DumpOptions.ExcludeProperties.Contains(p.Name))
        .ToList();
}

ソースを修正したら、以下のようにして.csprojにプロジェクトを参照するか

  <ItemGroup>
    <ProjectReference Include="../ObjectDumper/ObjectDumper/ObjectDumper.csproj" />
  </ItemGroup>

dotnet build -c Release」でビルドして、DLLをプロジェクトのディレクトリにコピーして.csprojにDLLの参照を追加する。

  <ItemGroup>
    <Reference Include="ObjectDumper"><HintPath>ObjectDumping.dll</HintPath></Reference>
  </ItemGroup>

使用例

var options = new DumpOptions();
options.MaxLevel = 5; // 5階層に制限
options.ExcludeProperties.add("ignoreField"); // 除外フィールド
var dump = ObjectDumper.Dump(obj, options);