TadaoYamaokaの開発日記

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

【将棋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);

ONNX Runtimeを使ってみる その3(DirectML)

ONNX RuntimeでDirectMLを使ってdlshogiのモデルの推論を行い、処理時間を比較してみた。

DirectMLを使えばAMDGPUでも推論が実行できるようになる。

DirectMLプロバイダの使用

NuGetからビルド済みバイナリが取得できる。
Microsoft.ML.OnnxRuntime.DirectMLをインストールする。

DirectMLプロバイダを使用するにはソースに以下の行を追加する。

#include <dml_provider_factory.h>

	session_options.DisableMemPattern();
	session_options.SetExecutionMode(ORT_SEQUENTIAL);
	OrtSessionOptionsAppendExecutionProvider_DML(session_options, 0);

DisableMemPattern()とSetExecutionMode(ORT_SEQUENTIAL)が必要である点は、
https://github.com/microsoft/onnxruntime/blob/master/docs/execution_providers/DirectML-ExecutionProvider.md
に記載されている。
この記述がないとSessionの初期化でエラーが発生する。

これで、ビルドは通るが実行にOrtSessionOptionsAppendExecutionProvider_DMLでメモリアクセス違反のエラーになった。

issuesを調べたところ同様のエラーの報告があり、対処方法が記載されていた。
https://github.com/microsoft/onnxruntime/issues/3360

ONNX Runtimeが使用するDirectML.dllは、Windows 10のシステムにインストールされているdirectml.dllと互換性がないため発生するようだ。
PATHに設定していても、システムフォルダが優先されるため、DirectML.dllを.exeのあるフォルダにコピーする必要がある。

onnxruntime.dllはちゃんと.exeのフォルダにコピーされるので、NuGetのパッケージの不備ではないかと思うが、「packages\Microsoft.ML.OnnxRuntime.DirectML.1.3.0\runtimes\win-x64\native\DirectML.dll」を手動でコピーした。
自動化するならVisual Studioのプロジェクトのビルド後の処理に設定するのが良いだろう。

処理時間測定

処理時間の測定結果は以下のようになった。
測定条件は、前回の記事と同じ。

比較のために、TensorRTとCPU(MKL-ML、スレッド数8)の結果も記載する。

条件 平均処理時間
DirectML 1.4秒
GPU(TensorRT) 0.3秒
CPU(MKL-ML、スレッド8) 31.3秒

TensorRTと比較すると、4.7倍遅くなっている。
TensorRTはTensorCoreを使いFP16で処理しているので、その違いがでていると思われる。

CPUと比較すると22.6倍速く、DirectMLを使用した方が速い。

まとめ

ONNX RuntimeのDirectMLプロバイダを使うことで、AMDGPUでも推論が動かせるようになる。
処理速度は、TensorRTには劣る(4.7倍遅い)が、CPUよりは断然に速い(22.6倍速い)。

ONNX Runtimeを使ってみる その2(性能測定)

先日、ONNX Runtimeを使って、MNISTの推論を試した。

今回は、dlshogiのResnet 10ブロック、192フィルタのモデルを使って、GPUで実行した場合の速度と、CPUで実行した場合の速度を比較した。

測定条件

GPUでのONNXの推論にはTensorRT(FP16)を使用する。
CPUの測定にはONNX Runtimeを使用し、デフォルトのCPUプロバイダと、MKL-MLを有効にしたCPUプロバイダ、DNNLプロバイダのそれぞれで測定した。

OSはWindows 10 64bit、GPUGeForce 2080Ti、CPUはCore i7-6700K(4コア、8スレッド、4GHz)を使用した。

推論の対象は、floodgateの棋譜からサンプリングした1万局面、バッチサイズは128とした。
それぞれの条件で、3回測定し、その平均時間を求める。

GPUで実行した場合の速度

条件 平均処理時間
GPU(TensorRT) 0.3秒

デフォルトCPUプロバイダ

NuGetでビルド済みバイナリが提供されているので、Microsoft.ML.OnnxRuntimeをインストールして使用した。

条件 平均処理時間
CPU(デフォルト) 32.6秒

GPUよりも、106.7倍遅い。

CPU使用率は、ほぼ100%で張り付いている。
f:id:TadaoYamaoka:20200606183918p:plain

なお、NuGetで取得できるCPUプロバイダはOpenMPが有効になっているので、スレッド数は自動で調整されている。

グラフ最適化のオプションを設定してみたが、変わらなかった。

session_options.SetGraphOptimizationLevel(ORT_ENABLE_ALL);

ORT_DISABLE_ALLにすると2倍くらい遅くなったため、デフォルトでグラフの最適化は有効になっているようだ。

MKL-MLを有効にしたCPUプロバイダ

NuGetから取得できるので、Microsoft.ML.OnnxRuntime.MKLMLをインストールして使用した。

条件 平均処理時間
CPU(MKL-ML) 37.5秒

デフォルトのCPUプロバイダより遅くなっている。

CPU使用率は、余裕が残っている。
f:id:TadaoYamaoka:20200606184158p:plain

CPUを使い切っていないので、スレッド数を明示的に設定してみた。

session_options.SetIntraOpNumThreads(8);

すると、CPU使用率は100%近くになった。
f:id:TadaoYamaoka:20200606184514p:plain

処理時間は、

条件 平均処理時間
CPU(MKL-ML、スレッド8) 31.3秒

となり、デフォルトCPUプロバイダより速くなった。

MKL-MLの場合は、スレッド数を調整した方が速くなる場合があるようだ。

なお、ONNX Runtimeのスレッド数の設定には、SetInterOpNumThreadsとSetIntraOpNumThreadsがあるが、前者はグラフ全体のスレッド数、後者はノード内の実行を並列化する際のスレッド数を設定する。
参考:https://github.com/microsoft/onnxruntime/issues/2177
SetInterOpNumThreadsの方も8に変えてみたが、処理時間に変化はなかった。

DNNLプロバイダ

ビルド済みバイナリが提供されていないため、ソースからビルドした。
スタートメニューから「x64 Native Tools Command Prompt for VS 2019」を起動して、GitHubのレポジトリをcloneして以下のようにビルドする。

git clone --recursive https://github.com/Microsoft/onnxruntime
cd onnxruntime
.\build.bat --config RelWithDebInfo --build_shared_lib --parallel --use_dnnl --cmake_generator "Visual Studio 16 2019"

「build\Windows\RelWithDebInfo\RelWithDebInfo」にビルド済みライブラリができる。

DNNLプロバイダを有効にするには、

#include <dnnl_provider_factory.h>

OrtSessionOptionsAppendExecutionProvider_Dnnl(session_options, 1);

のように、ソースコードでプロバイダを追加する処理が必要になる。

処理時間は、

条件 平均処理時間
CPU(DNNL) 35.0秒

となり、スレッド数を調整したMKL-MLよりも遅い。

CPU使用率は、ほぼ100%になっている。
f:id:TadaoYamaoka:20200606185652p:plain

まとめ

ONNX Runtimeを使用して、CPUでdlshogiのモデルの推論を行った。
GPUと比較した場合、100倍以上遅くdlshogiをCPUで実行しても、とりあえず動かせる程度で強さは期待できなそうだ。

CPUのプロバイダの比較では、MKL-MLを有効にしたプロバイダが一番速かった。
ただし、デフォルトの設定ではCPUを使い切らないため、スレッド数を調整する必要がある。


測定に使用したソースは以下の場所にある。
DeepLearningShogi/onnx_benchmark.cpp at feature/onnx_runtime · TadaoYamaoka/DeepLearningShogi · GitHub

C#からPythonを呼び出す

以前にC#からPythonディープラーニングフレームワークを呼び出すいくつかの方法を記事にした。

最後のlibTorchは、Pythonではないが、速度を出したい場合には選択肢になる。
しかし、モデルの定義をC++で実装する必要があり、メンテナンス性を考慮するとPythonからPyTorchを利用したい。

上記の他の方法は、機能が不足していたり、プロセス間通信のオーバーヘッドが大きすぎて速度がでなかったりで難がある。


最近、ハースストーンのAIの論文を読んでいたら、PythonNet(Python for .NET)で、C#からPythonを呼び出せることを知った。
てっきりPythonから.Netを利用するためだけのライブラリかと思ってスルーしていた。
公式ページの説明の下の方の「Embedding Python」を読めば、.NetからPythonを利用する方法について書かれていた。

メリット

PythonNetは、.NetによるPython実装ではなく、インストールされているPythonそのものが.Netのプロセスで実行される。
そのため、インストールされているPythonで動くものはすべて動くので互換性や機能不足で問題が起きない。

また、プロセス間通信がないため、速度面のボトルネックが少ない。
データのマーシャリングを自動で行ってくれるため、C#の変数をそのままPythonに渡せて、Pythonから結果をC#のオブジェクトとして受け取ることができる。

ということで、とても扱いやすいライブラリであることが分かった。


いろいろ試してみて、WindowsでもLinuxでも.Net Core 2.1から利用することができたので、インストール方法と使い方を記しておく。

インストール方法は、公式のWikiに記載があるが記述が少なくわかりにくかった。

Python環境構築

Python環境はすでに利用しているものが利用できる。
対応しているPythonのバージョンは、3.7なので3.8の場合は新たにPython環境の構築が必要になる。

Anacondaの場合は、アーカイブからAnaconda3-2019.10をインストールする。

PythonNetインストール

Windowsの場合

Windowsの場合、ビルド済みパッケージが利用できる。

.Net CoreのC#プロジェクトのディレクトリで、

dotnet add package Python.Runtime.NETStandard

を実行するだけでよい。

環境変数PATHにPythonをインストールしたディレクトリ(python37.dllのあるディレクトリ)を追加する。
Anaconda3のインストール時にPATHを設定している場合は不要。

Linuxの場合

※2020/11/1 追記 Linux向けには「pythonnet_netstandard_py37_linux」パッケージが使用できるため、下記記載のビルドは不要だった。
各プロットフォームと各Pythonのバージョンに対応したパッケージは、NuGet Galleryで検索できる。

Linuxの場合、ビルドが必要になる。
GitHubからリポジトリをクローンする。

git clone https://github.com/pythonnet/pythonnet

clangが必要なため、インストールされてない場合はインストールする。

sudo apt install clang

setup.pyでwheelを作成するとビルドされる。

python setup.py bdist_wheel --xplat

※「--xplat」オプションは.Net Coreから利用する場合に必要

Pythonから.Netを利用する場合は、ビルドされたwheelをインストールするが、.NetからPythonを利用する場合は必要ない。

build/lib.linux-x86_64-3.7/netcoreapp2.0
にビルドされたPython.Runtime.dllを使用する。

Python.Runtime.dllをC#プロジェクトのディレクトリにコピーして、.csprojに以下を追記する。

  <ItemGroup>
          <Reference Include="Python.Runtime"><HintPath>Python.Runtime.dll</HintPath></Reference>
  </ItemGroup>

環境変数LD_LIBRARY_PATHを設定する。

export LD_LIBRARY_PATH=/home/xxx/anaconda3/lib:$LD_LIBRARY_PATH

Pythonのインストール場所に合わせる。

サンプルコード

以下は、Python環境にインストールされたPyTorchをインポートしてバージョンを表示するC#コードの例である。
WindowsでもLinuxでも同じ。

using System;
using System.IO;
using Python.Runtime;

namespace pythonnettest
{
    class Program
    {
        static void Main(string[] args)
        {
            var PYTHON_HOME = Environment.ExpandEnvironmentVariables(@"C:\Anaconda3");
            PythonEngine.PythonHome = PYTHON_HOME;

            using (Py.GIL())
            {
                dynamic torch = Py.Import("torch");
                dynamic version = torch.__version__;
                Console.WriteLine(version);
            }
        }
    }
}

PYTHON_HOMEは、Pythonをインストールしたディレクトリに合わせる。

実行結果:

1.3.1

まとめ

PythonNetを使うと、C#からPython経由でディープラーニングフレームワークが簡単に利用できる。
インストールしているPython環境がそのまま利用でき、プロセス間通信のボトルネックがないので、これ以上よい方法はないのではないかと思う。

今回.Net Coreから利用したが、.Net Frameworkやmonoにも対応しているので、試していないがUnityから利用とかも可能だと思う。