TadaoYamaokaの開発日記

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

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

C#からPythonへポインタ渡しするとは逆に、Python for .NETを使ってPythonからC#へポインタ渡しする方法について紹介する。

Python for .NET

Python for .NETを使用すると、PythonからC#のコードを呼び出すことができる。
Pythonと同じプロセスで、.NETのランタイムを動かすことができ、C#で記述したコードをPythonのモジュールと同じように扱うことができる。

import clr
from System import String
from System.Collections import *

Python for .NETのインストール

pipコマンドで、インストールする。

pip install pythonnet

C#側のコード作成

.NET Core 3.1を使用して、C#のコードを作成する。

クラスライブラリのプロジェクトを作成する。

dotnet new classlib --name torch_sharp

以下の通りC#のコードを記述する。

using System;

namespace torch_sharp
{
    public class Class1
    {
        public unsafe void func(IntPtr data, int size)
        {
            float* array = (float*)data.ToPointer();
            for (int i = 0; i < size; ++i)
            {
                array[i]  = i;
            }
        }
    }
}
解説

Pythonからは、IntPtr型でポインタを受け取り、それをToPointer()でfloat*に型変換して、float型の配列として扱っている。
Python側でもfloat型になっていることを前提にしている。

Python側のコード作成

以下の通りPythonのコードを記述する。

import clr
clr.AddReference('torch_sharp')
from torch_sharp import Class1
from System import IntPtr

import numpy as np

obj = Class1()

data = np.empty(5, dtype=np.float32)
obj.func(IntPtr.op_Explicit(data.ctypes.data), len(data))

print(data)
解説
clr.AddReference('torch_sharp')
from torch_sharp import Class1

この部分で、C#のクラスライブラリをPythonモジュールとしてimportしている。

ビルドしたC#の.dllファイルは、Pythonモジュールと同じディレクトリに配置しておく必要がある。
C#のプロジェクトファイル(.csproj)で、

  <Target Name="CopyDLL" AfterTargets="AfterBuild">
    <Copy SourceFiles="$(OutDir)/torch_sharp.dll" DestinationFolder="../../torch_sharp/" />
  </Target>

のように記述しておくとよい。

Python側からC#にポインタ渡しするデータは、Numpyで作成する。

IntPtr.op_Explicit(data.ctypes.data)

の部分で、Numpyのデータをポインタとして取り出して、C#のIntPtr型に変換している。
ポインタを数値型のままC#に渡して、C#側でIntPtrにしてもよいが、IntPtrで渡した方が32bit、64bitかを気にしなくてよい。

まとめ

PythonからC#へポインタ渡しする方法について紹介した。

C#のアプリケーションで作成したデータを使って、Pythonでモデルを学習したいというようなケースで、C#のデータをシリアライズして、Python側で読み込みたいような場合に、PythonからC#のコードが呼べると便利である。
モデルに入力するためのデータの前処理を行うとデータ量が増えるため、シリアライズデータは加工前の状態にしたいが、Pythonで加工を行うと遅いため、速度を考慮するとC#で加工を行いたい。
しかし、PythonC#の間で、データのマーシャリングが発生すると呼び出しのオーバーヘッドが発生するので、ここで紹介した方法でポインタ渡しをするとオーバーヘッドを抑えられる。

dlshogiのdf-pnのテスト方法

dlshogiのdf-pnだけを動かす手順を書いておきます。

テスト局面追加

testプロジェクトのtest.cppの
「// DfPnテスト」と書いてあるところのmain()の

	vector<string> sfens = {
		// 詰み
		"9/9/+N8/p1p4p1/6p1p/1P7/3k3PP/2+p5L/6+rGK w R2B2G3Sgs3n3l9p 1",
		"1n1g3+Pl/k1p1s4/1ng5p/pSP1p1pp1/1n3p3/P1K3P1P/1P7/9/L1G5L b 2R2BG2SL5Pn 161", // mate 15
		// (略)
	};

の部分にsfenで局面を追加することでテストできます。

test.cppには、main()がたくさんありますが、テストしたいところを「#if 1」にしています。

https://github.com/TadaoYamaoka/DeepLearningShogi/blob/master/test/test.cpp#L359

探索上限変更

上限ノード数は

dfpn.set_max_search_node(1000000);

で変更、深さの上限は、

dfpn.set_maxdepth(29);

で変更できます。

ハッシュサイズ変更

ハッシュの衝突が起きると探索効率が落ちるため、置換表は十分なサイズが必要です。
dfpn.cppの以下の箇所で定数定義しています。

int64_t DfPn::HASH_SIZE_MB = 2048;

https://github.com/TadaoYamaoka/DeepLearningShogi/blob/master/usi/dfpn.cpp#L11

ハッシュの衝突が起きると上書きするため、長手数の詰み探索には向いていません。
長手数の詰み探索には、Small TreeGCとか使う方が正しいです。

ビルド方法

Visual Studioの場合は、ビルド構成をReleaseにしてtestプロジェクトを右クリックしてビルドでビルドできます。
ソリューションのビルドからは外しているため個別にビルドが必要です。

Gccの場合は、testディレクトリにMakefileがあります。

df-pnの関連記事

TLでdf-pnの話が上がっていたので、以前書いた記事のリンクをまとめておきます。

dlshogiのdf-pnの実装は、優越関係、証明駒、反証駒、先端ノードでの3手詰めルーチンを実装していて、ループの問題も処理していますが、対局時の利用に特化しているので、長手数の詰みはノード数上限、深さ上限に引っかかると不詰み扱いになります。
詰め将棋エンジンとしては使えません。

dlshogiでMagic Bitboardを有効にする

やねうら王が飛車と角の利きに、PEXTの代わりにMagic Bitboardを使用することで、Threadripperで高速化できたとのことなので、dlshogiでも高速化できるか試してみた。

dlshogiは合法手生成にAperyのソースを使用しており、AperyはMagic Bitboardを実装していて、HAVE_BMI2を無効にすることで有効化できる(有効にしているとPEXTが使われる)。

自己対局を実行しているPCは、Ryzen Threadripper 3970Xを使用しているので、HAVE_BMI2を無効にしてビルドしなおして、自己対局における局面生成速度を比較した。

現在自己対局では、3枚のGPUで、1GPUあたり3つの探索スレッドと1つの詰み探索スレッドを使用している。
GPUの処理は、効率的なバッチ処理NNキャッシュを使用しており、GPUよりもCPUがボトルネックになっているため、合法手生成の高速化の効果はそのまま局面生成速度に表れると予想している。

比較結果

10000局面を生成した際の局面生成速度を2回測定して比較した。

条件 1回目(NPS) 2回目(NPS) 平均(NPS)
変更前 66.67 66.61 66.64
変更後 75.62 74.44 75.03

平均で、12.6%高速化できた。

まとめ

Ryzen ThreadripperにおけるMagic Bitboardによる高速化は、dlshogiでも同じ効果が得られることがわかった。

大会ではintelのCPUを使用しているので大会での強さには影響なさそうだが、自己対局の生成速度があがるのはすぐにでも恩恵が得られそうである。

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の結果を、畳み込み層の入力とするということを試してみたい。