TadaoYamaokaの日記

山岡忠夫Homeで公開しているプログラムの開発ネタを中心に書いていきます。

ML.NETからTensorFlowを使う

前回C#からTensorFlowが使えるTensorFlow.NETというライブラリを試したが、まだ実装されていない機能があるので、他にTensorFlowをフルで使えるライブラリを探すことにした。

TensorFlowSharpが十分な機能が実装されているので、.NET Coreへポーティングすれば使えそうだ。
.NET Core 2.0にポーティングしている人もいるようだ。
https://github.com/jcapellman/TensorFlowSharp
ベースにしているコミットが少し古いため最新のコミットを反映するには作業が必要になりそう。

他にないか調べたところ、MicrosoftがリリースしているML.NETでもTensorFlowが使えることがわかった。
ただし、モデルはPythonでTensorFlowを使って記述して、.pb形式で保存したモデルを読み込む必要がある。
少し手順が煩雑になるが、その代わりTensorFlowの機能をフルに使ってモデルを記述できる。

ということで、ML.NETからTensorFlowを使ってみた。

モデル定義

まずはPythonでモデルを定義する。
今回はKerasで定義した。

import tensorflow as tf

model = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(input_shape=(28, 28)),
    tf.keras.layers.Dense(512, activation=tf.nn.relu),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Dense(10, activation=tf.nn.softmax)
])
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

def model_to_pb(model, output_dir="./", output_graph_name='model.pb', output_names=["dense_1/Softmax"]):
    ksess = tf.keras.backend.get_session()
    constants_graph = tf.graph_util.convert_variables_to_constants(ksess, ksess.graph.as_graph_def(), output_names)

    tf.io.write_graph(constants_graph, output_dir, output_graph_name, as_text=False)

    print(output_names)

model_to_pb(model)

実行すると、グラフ定義と初期パラメータが、model.pbに保存される。

.NET Coreでプロジェクト作成

mkdir MLDotNetMNIST
cd MLDotNetMNIST
dotnet new console
dotnet add package Microsoft.ML
dotnet add package Microsoft.ML.TensorFlow

C#のコード

model.pbを読み込んでMNISTのサンプルデータを学習する。

Program.cs
using System;
using System.Linq;
using Microsoft.ML;
using Microsoft.ML.Data;

namespace MLDotNetMNIST
{
    class Program
    {
        static void Main(string[] args)
        {
            var mlContext = new MLContext();
            var tensorFlowModel = mlContext.Model.LoadTensorFlowModel(@"model.pb");
            //var schema = tensorFlowModel.GetModelSchema();
            var data = GetTensorData();
            var idv = mlContext.Data.LoadFromEnumerable(data);

            var pipeline = tensorFlowModel.ScoreTensorFlowModel(
                new[] { "dense_1/Softmax" }, new[] { "flatten_input" }, addBatchDimensionInput: true);

            var model = pipeline.Fit(idv);

            var engine = mlContext.Model.CreatePredictionEngine<TensorData, OutputScores>(model);
            var result = engine.Predict(data[0]);
            var maxValue = result.Output.Max();
            var maxIndex = result.Output.ToList().IndexOf(maxValue);
            Console.WriteLine(maxIndex);
        }

        private static TensorData[] GetTensorData()
        {
            var data = new double[] {
            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.027450980392156862, 0.09411764705882353, 0.5019607843137255, 0.5450980392156862, 0.5411764705882353, 0.7490196078431373, 0.7058823529411765, 0.9921568627450981, 0.7490196078431373, 0.5411764705882353, 0.18823529411764706, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.16862745098039217, 0.1843137254901961, 0.47058823529411764, 0.7294117647058823, 0.9882352941176471, 0.9882352941176471, 0.9921568627450981, 0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0.9921568627450981, 0.9882352941176471, 0.8901960784313725, 0.11372549019607843, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.07450980392156863, 0.6431372549019608, 0.9647058823529412, 0.9921568627450981, 0.9882352941176471, 0.9882352941176471, 0.8901960784313725, 0.7176470588235294, 0.7215686274509804, 0.6352941176470588, 0.27058823529411763, 0.27058823529411763, 0.27058823529411763, 0.30980392156862746, 0.8901960784313725, 0.9882352941176471, 0.17647058823529413, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.27450980392156865, 0.9882352941176471, 0.9882352941176471, 0.9921568627450981, 0.9215686274509803, 0.30196078431372547, 0.11372549019607843, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.03529411764705882, 0.7607843137254902, 0.8901960784313725, 0.11372549019607843, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.027450980392156862, 0.2549019607843137, 0.5372549019607843, 0.788235294117647, 0.6823529411764706, 0.12549019607843137, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7098039215686275, 0.9882352941176471, 0.7176470588235294, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.03529411764705882, 0.5019607843137255,
1.0, 0.9764705882352941, 0.45098039215686275, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.4470588235294118, 0.9882352941176471, 0.9921568627450981, 0.5176470588235295, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5254901960784314, 0.9411764705882353, 0.9882352941176471, 0.47843137254901963, 0.09803921568627451, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.03529411764705882, 0.6509803921568628, 0.9411764705882353, 0.9882352941176471, 0.6588235294117647, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.20784313725490197, 0.7098039215686275, 0.9882352941176471, 0.9882352941176471, 0.4549019607843137, 0.00784313725490196, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.027450980392156862, 0.25882352941176473, 0.9529411764705882, 1.0, 0.9764705882352941, 0.24705882352941178, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.7294117647058823, 0.9882352941176471, 0.9882352941176471, 0.8549019607843137, 0.29411764705882354, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.19215686274509805, 0.8941176470588236, 0.9882352941176471, 0.9882352941176471, 0.8666666666666667, 0.12549019607843137, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5333333333333333, 0.9137254901960784, 0.9882352941176471, 0.8901960784313725, 0.4666666666666667, 0.09803921568627451, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.12549019607843137, 0.8235294117647058, 0.9803921568627451, 0.9921568627450981, 0.9058823529411765, 0.18823529411764706, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.34901960784313724, 0.8705882352941177, 0.9921568627450981, 0.9921568627450981, 0.6196078431372549, 0.0, 0.0, 0.0, 0.043137254901960784, 0.13333333333333333, 0.4627450980392157, 0.027450980392156862, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.6313725490196078, 0.9882352941176471, 0.9882352941176471, 0.41568627450980394, 0.0, 0.03529411764705882, 0.1843137254901961, 0.34901960784313724, 0.796078431372549, 0.9921568627450981, 0.9568627450980393, 0.2196078431372549, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.6313725490196078, 0.9882352941176471,
0.9882352941176471, 0.7450980392156863, 0.7254901960784313, 0.7725490196078432, 0.9882352941176471, 0.9882352941176471, 0.8666666666666667, 0.6784313725490196, 0.2196078431372549, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.47058823529411764, 0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0.9921568627450981, 0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 0.3764705882352941, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0196078431372549, 0.21176470588235294, 0.5372549019607843, 0.5372549019607843, 0.7450980392156863, 0.5372549019607843, 0.21176470588235294, 0.08627450980392157, 0.00784313725490196, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
            return new TensorData[] { new TensorData() { Input = data.Select(x => (float)x).ToArray() } };
        }
    }

    public class TensorData
    {
        [ColumnName("flatten_input")]
        [VectorType(28, 28)]
        public float[] Input { get; set; }
    }

    class OutputScores
    {
        [ColumnName("dense_1/Softmax")]
        [VectorType(10)]
        public float[] Output { get; set; }
    }
}

ソースは以下のサイトを参考にした。
https://www.cnblogs.com/kenwoo/p/10902431.html


損失関数がScoreTensorFlowModelに隠ぺいされていて、メソッド名も直感的にわかりにくい。
Softmax交差エントロピー以外の損失関数にするにはどうするかとか、損失の値を表示するにはどうするかも不明。

GPUで実行

デフォルトでは、CPUでしか実行できないが、ユーザフォルダの「.nuget\packages\scisharp.tensorflow.redist\1.14.0\runtimes\win-x64\native\tensorflow.dll」をTensorFlow for CのGPU有効ビルド済みdllに置き換えることで、GPUが使用できる。

TensorFlowのC#バインディング

TensorFlowのC#バインディングとして、検索するとTensorFlowSharpが見つかるが、更新が止まっているようで.NET Coreには対応していない(ビルドすれば可能そう)。

TensorFlow.NET

他のライブラリを探したところ、TensorFlow.NETというライブラリを見つけた。
最近でも活発にメンテされており、.NET Coreに対応しており、TensorFlowのバージョンも最新に対応している。
情報が少なく(日本語の情報は皆無)で、今後もメンテされ続けるか不安はあるが試してみた。

TensorFlow.NETの導入方法

.NET Coreのアプリに導入する方法を説明する。

プロジェクト作成

.NET Coreのコンソールプロジェクトを作成する。

mkdir example
cd example
dotnet new console
ライブラリ追加

プロジェクトにTensorFlow.NETを追加する。

dotnet add package TensorFlow.NET

CPUで実行する場合は、プロジェクトにSciSharp.TensorFlow.Redistを追加する。

dotnet add package SciSharp.TensorFlow.Redist


LinuxGPUを使用する場合は、TensorFlow for Cのビルド済みバイナリが別途必要になる。

GPU版のビルド済みバイナリをダウンロードして、適当な場所に解凍する。
※CUDA 10.0、cuDNN 7.4以上が必要(参考

wget https://storage.googleapis.com/tensorflow/libtensorflow/libtensorflow-gpu-linux-x86_64-1.13.1.tar.gz
tar xzf libtensorflow-gpu-linux-x86_64-1.13.1.tar.gz

/usr/local/lib/にコピーして、キャッシュを更新する。

cp lib/libtensorflow_framework.so /usr/local/lib/
cp lib/libtensorflow.so /usr/local/lib/
ldconfig


WindowsGPUで実行する場合は、プロジェクトにSciSharp.TensorFlow.Redist-Windows-GPUを追加する。

dotnet add package SciSharp.TensorFlow.Redist-Windows-GPU

サンプルコード

以下のように3層パーセプトロンで2値分類するコードを試した。

using System;
using NumSharp;
using Tensorflow;
using static Tensorflow.Binding;

namespace tensorflowcs
{
    class Program
    {
        static Tensor x, y;
        static Tensor loss;
        static Operation optimizer;

        static private Tensor fc_layer(Tensor x, int num_units, string name, bool use_relu = true)
        {
            var in_dim = x.shape[1];

            var initer = tf.truncated_normal_initializer(stddev: 0.01f);
            var W = tf.get_variable("W_" + name,
                        dtype: tf.float32,
                        shape: (in_dim, num_units),
                        initializer: initer);

            var initial = tf.constant(0f, num_units);
            var b = tf.get_variable("b_" + name,
                        dtype: tf.float32,
                        initializer: initial);

            var layer = tf.matmul(x, W) + b;
            if (use_relu)
                layer = tf.nn.relu(layer);

            return layer;
        }

        static private Tensor sigmoid_cross_entropy_with_logits(Tensor labels, Tensor logits)
        {
            return tf.add(logits - logits * labels, tf.log1p(tf.exp(-logits)));
        }

        static private void BuildGraph()
        {
            var graph = new Graph().as_default();

            x = tf.placeholder(tf.float32, shape: (-1, 1), name: "X");
            y = tf.placeholder(tf.float32, shape: (-1, 1), name: "Y");

            var fc1 = fc_layer(x, 3, "FC1", use_relu: true);
            var output_logits = fc_layer(fc1, 1, "OUT", use_relu: false);
            var entropy = sigmoid_cross_entropy_with_logits(labels: tf.reshape(y, new[] { -1 }), logits: tf.reshape(output_logits, new[] { -1 }));
            loss = tf.reduce_mean(entropy, name: "loss");
            optimizer = tf.train.AdamOptimizer(learning_rate: 0.1f).minimize(loss);
        }
        
        static void Main(string[] args)
        {
            BuildGraph();

            using (var sess = tf.Session())
            {
                Train(sess);
            }
        }

        static private void Train(Session sess)
        {
            var init = tf.global_variables_initializer();
            sess.run(init);

            var x_train = np.array(new float[4, 1] { {0.1f}, {0.2f}, {0.8f}, {0.9f} }, dtype: np.float32);
            var y_train = np.array(new float[4, 1] { {1}, {1}, {0}, {0} }, dtype: np.float32);

            foreach (var iteration in range(100))
            {
                var result = sess.run((optimizer, loss), (x, x_train), (y, y_train));

                float loss_val = result.Item2;

                print($"iter {iteration.ToString("000")}: Loss={loss_val.ToString("0.0000")}");
            }
        }
    }
}

実行結果

iter 000: Loss=0.6931
iter 001: Loss=0.6921
iter 002: Loss=0.6869
...
iter 097: Loss=0.0038
iter 098: Loss=0.0038
iter 099: Loss=0.0037

Pythonで同じ処理をした場合と同じ結果になることが確認できた。
tf.nn.sigmoid_cross_entropy_with_logitsは未実装のようで、動かなかったため、独自に実装した。
まだ、ライブラリは実装されていない部分が残っているようだ。

将棋AIで学ぶディープラーニングのJupyter NotebookをColab上で実行できるようにアップデート

先日【書籍】将棋AIで学ぶディープラーニングのJupyter Notebookを公開しましたが、Lesserkaiとの対局はローカルPCで将棋所を動かす必要があったため、試すハードルが少し高くなっていました。
そこで、先日USIエンジン同士の対局を実装したcshogiを使ってGoogle Colabのみで実行できるように内容を追記しました。

これで、書籍の内容をGPUを搭載したPCがなくても簡単に試せるようになったと思います。
colab.research.google.com

ref localでC#のコードを高速化する

.NET Frameworkと.NET CoreでDictionaryの性能が異なる。

ランダムな整数をキーとして、AddとTryGetValueを5000000回行った結果10回の平均時間(ms)は以下の通りである。

バージョン Add TryGetValue
.NET Framework 4.6 681.8 268.5
.NET Core 2.1 585.9 266.6

TryGetValueはほぼ同じだが、Addは.NET Core 2.1の方が1.16倍速い。

.NET Frameworkと.NET Coreで、Dictionaryの実装にどのような差があるか調べたところ、一つはハッシュエントリの使用方法が改良されていたのと、もう一つはref localを使う変更がされていた。
.NET Framework 4.8のソース
.NET Core 2.1のソース

以下は、ref localについて調べた内容である。

ref local

ref localは、例えば以下のようなコードで、

entries[index].hashCode = hashCode;
entries[index].next = buckets[targetBucket];
entries[index].key = key;
entries[index].value = value;

配列の要素に同じインデックスでアクセスしているところを

ref Entry entry = ref entries[index];
entry.hashCode = hashCode;
entry.next = buckets[targetBucket];
entry.key = key;
entry.value = value;

のように、C++の参照と同じように、参照を示す変数に格納してからアクセスする方法だ。
参考:Ref Local Reassignment - C# 7.3 language proposals | Microsoft Docs

なお、配列の要素にref localを使用するには、C# 7.2以上でないないとコンパイルエラーとなるため、.csprojに以下の設定を加える必要がある。(この例では、.NET Core 2.1はC# 7.3に対応しているので、7.3としている。)

  <PropertyGroup>
    <LangVersion>7.3</LangVersion>
  </PropertyGroup>

ref localの効果測定

次に、この部分だけを変更してみて性能にどれだけ差があるか調べてみた。

変更元のソースは、.NET Framework 4.8のソースとした。
https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs

上記の部分の変更前後で測定結果は以下の通りとなった。(変更箇所はAddのみ)

Add TryGetValue
変更前 617.6 262.5
変更後 608.6 261.8

わずか(1.01倍)だが、ref localを使用すると速くなる。

ILDasmで生成されたILを比較してみた。
上記の変更箇所は以下の通りとなっていた。

変更前
    IL_0133: ldloc.0
    IL_0134: stfld System.Int32 MyDictionary1.Dictionary`2/Entry<TKey,TValue>::hashCode
    IL_0139: ldarg.0
    IL_013a: ldfld MyDictionary1.Dictionary`2/Entry<TKey,TValue>[] MyDictionary1.Dictionary`2<TKey,TValue>::entries
    IL_013f: ldloc.2
    IL_0140: ldelema MyDictionary1.Dictionary`2/Entry<TKey,TValue>
    IL_0145: ldarg.0
    IL_0146: ldfld System.Int32[] MyDictionary1.Dictionary`2<TKey,TValue>::buckets
    IL_014b: ldloc.1
    IL_014c: ldelem.i4
    IL_014d: stfld System.Int32 MyDictionary1.Dictionary`2/Entry<TKey,TValue>::next
    IL_0152: ldarg.0
    IL_0153: ldfld MyDictionary1.Dictionary`2/Entry<TKey,TValue>[] MyDictionary1.Dictionary`2<TKey,TValue>::entries
    IL_0158: ldloc.2
    IL_0159: ldelema MyDictionary1.Dictionary`2/Entry<TKey,TValue>
    IL_015e: ldarg.1
    IL_015f: stfld TKey MyDictionary1.Dictionary`2/Entry<TKey,TValue>::key
    IL_0164: ldarg.0
    IL_0165: ldfld MyDictionary1.Dictionary`2/Entry<TKey,TValue>[] MyDictionary1.Dictionary`2<TKey,TValue>::entries
    IL_016a: ldloc.2
    IL_016b: ldelema MyDictionary1.Dictionary`2/Entry<TKey,TValue>
    IL_0170: ldarg.2
    IL_0171: stfld TValue MyDictionary1.Dictionary`2/Entry<TKey,TValue>::value
    IL_0176: ldarg.0
    IL_0177: ldfld System.Int32[] MyDictionary1.Dictionary`2<TKey,TValue>::buckets
変更後
    IL_0133: dup
    IL_0134: ldloc.0
    IL_0135: stfld System.Int32 MyDictionary1.Dictionary`2/Entry<TKey,TValue>::hashCode
    IL_013a: dup
    IL_013b: ldarg.0
    IL_013c: ldfld System.Int32[] MyDictionary1.Dictionary`2<TKey,TValue>::buckets
    IL_0141: ldloc.1
    IL_0142: ldelem.i4
    IL_0143: stfld System.Int32 MyDictionary1.Dictionary`2/Entry<TKey,TValue>::next
    IL_0148: dup
    IL_0149: ldarg.1
    IL_014a: stfld TKey MyDictionary1.Dictionary`2/Entry<TKey,TValue>::key
    IL_014f: ldarg.2
    IL_0150: stfld TValue MyDictionary1.Dictionary`2/Entry<TKey,TValue>::value

変更前は25行、変更後は14行と、ILが短くなっているのが確認できる。

ILDasmについて

なお、ILDasmは.NET FrameworkSDKに付属するが、.NET Coreの場合は、

dotnet tool install --global dotnet-ildasm

でインストールできる。
使う際は、

dotnet-ildasm CSDictionaryTest.dll >CSDictionaryTest.il

のようにして使う。結果は標準出力に出力されるのでリダイレクトするとよい。

まとめ

配列を同じインデックスで複数回アクセスしている場合、ref localを使うとわずかだが速くできる。

測定に使用したソースをGitHubに置いておいた。
GitHub - TadaoYamaoka/CSDictionaryTest
MyDictionary1.csが.NET Framework 4.8のDictionaryを元にしたソース。
MyDictionary2.csが.NET Core 2.1を元にしたソース。
MyDictionary3.csは他の実験したソースなのでこの記事とは無関係。

cshogiのビルド済みパッケージ

cshogi(Pythonの高速な将棋ライブラリ)をインストールするには、CythonとC++コンパイラが必要で、コンパイルに時間がかかっていた。
Google Colabでインストールを実行すると1分近く待つ場合があった。

そこで、待ち時間をなくしてすぐに試せるように、ビルド済みパッケージを用意した。

最終的にはPyPIに登録したいと思っているが、ひとまずGitHubのReleaseにwheelファイルを置いて、そこから取得するようにした。

OSの種類とPythonのバージョンごとに用意する必要があるため、Google Colabに合わせてPython3.6でビルドした。
ついでに、Python3.7用とWindows(Python3.7のみ)用ファイルもアップロードしておいた*1
Release v0.0.1 · TadaoYamaoka/cshogi · GitHub

インストール方法

GitHubのビルド済みパッケージ(wheelファイル)からインストールするには、以下のように実行する。

!pip install https://github.com/TadaoYamaoka/cshogi/releases/download/v0.0.1/cshogi-0.0.1-cp36-cp36m-linux_x86_64.whl

wheelファイルからインストールすることで、数秒でインストールできるようになった。

チュートリアル

cshogiチュートリアルも修正した。
簡単に試せるようになったので、ぜひ試してみてほしい。
colab.research.google.com

*1:AVX2を有効にしているので対応していないCPUではsetup.pyを修正してソースからビルドが必要

Google ColabでUSIエンジン同士の対局を行う

Pythonの高速な将棋ライブラリcshogiにUSIエンジン同士の対局機能を追加した。

先日、cshogiにJupyter Notebook上で盤面をSVGで表示できるようにしたが、その機能を使って対局中の盤面を表示するようにしている。
これによって、Google Colab上で、USIエンジン同士の対局を観戦できるようになる。


以下のようにして、Google Colab上でUSIエンジン同士の対局ができる。

インストール

!pip install git+https://github.com/TadaoYamaoka/cshogi

USIエンジン同士で対局する

from cshogi import cli

cli.main('/content/LesserkaiSrc/Lesserkai/Lesserkai', '/content/LesserkaiSrc/Lesserkai/Lesserkai')

以下のように1手ずつ盤面が表示され、終局すると結果が表示される。
(エンジンに送るUSIコマンドとエンジンからのinfo出力も、そのまま出力している。)

f:id:TadaoYamaoka:20190825182544p:plain

連続対局

連続対局することもできる。

# 設定例:対局数2回、秒読み1秒
cli.main('/content/LesserkaiSrc/Lesserkai/Lesserkai', '/content/LesserkaiSrc/Lesserkai/Lesserkai', games=2, byoyomi=1000)
手動操作

USIエンジンを手動で操作することもできる。

from cshogi.usi import Engine

engine = Engine('/content/LesserkaiSrc/Lesserkai/Lesserkai')

engine.isready()
engine.usinewgame()
engine.position()
engine.go()
engine.quit()

チュートリアル

Google Colabで使い方がわかるJupyter Nodebookを公開したので参考にして欲しい。
colab.research.google.com

cshogiの仕様変更

今まで指し手などを文字列を引数で渡す場合、バイト文字列としていたが、標準の文字列型に変更した。
互換性がなくなっているため、既存のプログラムの修正が必要になる。

以前に作成したdlshogi-zeroも修正が必要なため、後日対応する予定。

今後の予定

将棋AIで学ぶディープラーニングJupyter Notebookを、Google Colab上で対局できるようにする予定。

Pythonのタプルのリストから列を抽出する

Pythonで、タプルのリストから列を抽出する3種類の方法を比較してみた。

次のような要素が2つのタプルのリストからそれぞれの列を抽出する場合を考える。

抽出前

[('a', 1), ('b', 2), ('c', 3), ('d', 4)]

抽出後

(['a', 'b', 'c', 'd'], [1, 2, 3, 4])

方法1:for文を使った素朴な方法

def func1(samples):
    list1 = []
    list2 = []
    for a, b in samples:
        list1.append(a)
        list2.append(b)
    return list1, list2

Pythonのfor文は遅く、この方法は一般的に性能が良くない。

方法2:リスト内包表記を使った方法

def func2(samples):
    return [a for a, b in samples], [b for a, b in samples]

リスト内包表記を2回使って、それぞれの列を抽出している。
リスト内包表記のforを2回実行するが、方法1よりはましな結果になる。

方法3:zipとリスト内包表記を使った方法

def func3(samples):
    return [list(tup) for tup in zip(*original)]

一見、何をしているかわからないが、*でリストを展開して複数のタプルにして、zipで1番目の列のタプル、2番目の列のタプル、・・・として取り出している。
こうするとわかりやすい。

[tup for tup in zip(('a',1),('b',2),('c',3),('d',4))]

[('a', 'b', 'c', 'd'), (1, 2, 3, 4)]

この方法は、下記の記事で紹介されていた。
python - Transpose/Unzip Function (inverse of zip)? - Stack Overflow

性能測定

それぞれの方法の性能を比較してみた。
上記の例では要素数が少ないので、要素数をアルファベット26文字分に増やして測定する。

original = [(chr(i), i) for i in range(97, 97+26)]

import timeit
timeit.timeit(lambda: func1(original))
timeit.timeit(lambda: func2(original))
timeit.timeit(lambda: func3(original))

2.5222641000000294
1.7053095999999641
1.5412319000001844

方法3が一番速いことが確認できた。