tensorflowのC++APIで変数を保存する

 前回はtensorflowのC++APIを用いて学習をできるようなコードを書いた。

 今回は学習した変数をファイルに書き出し及び読み出しできるように変更した。なかなか難しく、まだ理解できていないところが多い。

書き込み

 Saveシグネチャ公式ドキュメントの通り

Save(const ::tensorflow::Scope & scope, ::tensorflow::Input filename, ::tensorflow::Input tensor_names, ::tensorflow::InputList data)

であり、引数に必要なものは

  • scope
  • filename
  • tensor_names
  • data

の4つである。したがっておおむね

auto save = Save(scope, "file_name", { "w1", "w2", "b1", "b2" }, { w1, w2, b1, b2 });

のような形でノードを生成し、Runで走らせれば良い。しかしfilename,tensor_namesstringであるため(?)直接埋め込むことはできず、またInputListもそのままでは各変数がInputOutputの2通りで解釈可能であるため初期化できないというエラーが出る。

 したがって、まず各stringに対してはデータを格納するPlaceholderと実際のデータであるTensorを準備する。

    //保存するファイル名
    auto file_name_op = Placeholder(scope, DT_STRING);
    Tensor file_name(DT_STRING, TensorShape{ 1 });
    file_name.scalar<string>()() = "model.ckpt";

    //保存する変数につける名前
    auto tensor_names_op = Placeholder(scope, DT_STRING);
    Tensor tensor_names(DT_STRING, TensorShape{ 4 });
    tensor_names.vec<string>()(0) = "w1";
    tensor_names.vec<string>()(1) = "w2";
    tensor_names.vec<string>()(2) = "b1";
    tensor_names.vec<string>()(3) = "b2";

 InputListにはInput(w1)などとするとコンパイルが通った。ここの理由はまだよくわかっていない。

    //保存する変数
    auto tensors = InputList({ Input(w1), Input(w2), Input(b1), Input(b2) });

 この順番はfile_namesで指定する順番と一致していなければならない。

 このように引数を準備してから(少なくともPlaceholderを準備してから)ノードを生成し、feed_dictでデータを与えつつセッションを走らせる。

    //保存するOp
    auto save = Save(scope, file_name_op, tensor_names_op, tensors);

    //実行
    TF_CHECK_OK(session.Run({ {file_name_op, file_name}, {tensor_names_op, tensor_names} }, {}, { save }, nullptr));

 fetchするものではないため(?)第2引数は空にし、第3引数にsaveを与える。

読み出し

 Resroreシグネチャ公式ドキュメントの通り

Restore(const ::tensorflow::Scope & scope, ::tensorflow::Input file_pattern, ::tensorflow::Input tensor_name, DataType dt) 

であり、必要な引数は

  • scope
  • file_pattern
  • tensor_name
  • dt

である。気を付けるべき点としては(筆者の理解では)、Restoreは一つのTensorを読み出すものであり、読み出したい変数の数だけ読み出しノードを作る必要がある。また読み出しただけでは変数に割り当てられていないため、Assignを用いて対応する変数ノードに割り当てる必要がある。

 引数についてはfile_patternにはSaveでのfile_nameと同じものを与えればよく、Saveと同様に型がstringであるtensor_nameについてはPlaceholderを作りTensorにデータを入れて実行時に流し込むということを行う。

    //1変数に対して1個ずつPlaceholderとTensorを作る
    auto w1_name_op = Placeholder(scope, DT_STRING);
    Tensor w1_name(DT_STRING, TensorShape{ 1 });
    w1_name.scalar<string>()() = "w1";
    auto w2_name_op = Placeholder(scope, DT_STRING);
    Tensor w2_name(DT_STRING, TensorShape{ 1 });
    w2_name.scalar<string>()() = "w2";
    auto b1_name_op = Placeholder(scope, DT_STRING);
    Tensor b1_name(DT_STRING, TensorShape{ 1 });
    b1_name.scalar<string>()() = "b1";
    auto b2_name_op = Placeholder(scope, DT_STRING);
    Tensor b2_name(DT_STRING, TensorShape{ 1 });
    b2_name.scalar<string>()() = "b2";

    //読みだすOp
    auto restore_w1 = Restore(scope, file_name_op, w1_name_op, DT_FLOAT);
    auto restore_assign_w1 = Assign(scope, w1, restore_w1);
    auto restore_w2 = Restore(scope, file_name_op, w2_name_op, DT_FLOAT);
    auto restore_assign_w2 = Assign(scope, w2, restore_w2);
    auto restore_b1 = Restore(scope, file_name_op, b1_name_op, DT_FLOAT);
    auto restore_assign_b1 = Assign(scope, b1, restore_b1);
    auto restore_b2 = Restore(scope, file_name_op, b2_name_op, DT_FLOAT);
    auto restore_assign_b2 = Assign(scope, b2, restore_b2);

    //実行
    TF_CHECK_OK(session.Run({ { file_name_op, file_name }, { w1_name_op, w1_name } }, { restore_assign_w1 }, nullptr));
    TF_CHECK_OK(session.Run({ { file_name_op, file_name }, { w2_name_op, w2_name } }, { restore_assign_w2 }, nullptr));
    TF_CHECK_OK(session.Run({ { file_name_op, file_name }, { b1_name_op, b1_name } }, { restore_assign_b1 }, nullptr));
    TF_CHECK_OK(session.Run({ { file_name_op, file_name }, { b2_name_op, b2_name } }, { restore_assign_b2 }, nullptr));

 こちらはfetchするものであるため(?)第二引数に与える。

 かなり冗長なコードになってしまい、可読性や保守性が低い。より大きいネットワークを構築しようとすると変数も多くなるため、効率的な書き方を模索する必要がある。

全体

 全体のコードは以下の様になった。

#define COMPILER_MSVC

#include "tensorflow/cc/client/client_session.h"
#include "tensorflow/core/public/session.h"
#include "tensorflow/cc/ops/standard_ops.h"
#include "tensorflow/core/framework/tensor.h"
#include "tensorflow/cc/framework/gradients.h"

using namespace tensorflow;
using namespace tensorflow::ops;

double targetFunction(double x1, double x2) {
    return 2.0 * x1 * x2 - 2.4 * x1 * x1 + 3.1 * x2 * x2 + 2.7 * x1 - 10.0 * x2 + 5.0;
}

int main() {
    int unit_num;
    std::cout << "ユニット数 : ";
    std::cin >> unit_num;
    float learning_rate;
    std::cout << "学習率 : ";
    std::cin >> learning_rate;
    int batch_size;
    std::cout << "バッチサイズ : ";
    std::cin >> batch_size;
    int epoch_num;
    std::cout << "エポック数 : ";
    std::cin >> epoch_num;
    int restore_flag;
    std::cout << "変数の設定(0->初期化, 1->ファイルから読み込む) : ";
    std::cin >> restore_flag;

    Scope scope = Scope::NewRootScope();

    //入力,教師データのPlaceholder
    auto x = Placeholder(scope.WithOpName("x"), DT_FLOAT);
    auto y = Placeholder(scope.WithOpName("y"), DT_FLOAT);

    //中間層への重み、バイアス
    auto w1 = Variable(scope, { unit_num, 2 }, DT_FLOAT);
    auto assign_w1 = Assign(scope, w1, RandomNormal(scope, { unit_num, 2 }, DT_FLOAT));
    auto b1 = Variable(scope, { unit_num, 1 }, DT_FLOAT);
    auto assign_b1 = Assign(scope, b1, RandomNormal(scope, { unit_num, 1 }, DT_FLOAT));

    //出力層への重み、バイアス
    auto w2 = Variable(scope, { 1, unit_num  }, DT_FLOAT);
    auto assign_w2 = Assign(scope, w2, RandomNormal(scope, { 1, unit_num }, DT_FLOAT));
    auto b2 = Variable(scope, { 1, 1 }, DT_FLOAT);
    auto assign_b2 = Assign(scope, b2, RandomNormal(scope, { 1, 1 }, DT_FLOAT));

    //中間層
    auto hidden_layer = Relu(scope, Add(scope, MatMul(scope, w1, x), b1));

    //出力層
    auto output_layer = Add(scope.WithOpName("output_layer"), MatMul(scope, w2, hidden_layer), b2);

    //損失
    auto loss = ReduceMean(scope, Square(scope, Sub(scope, output_layer, y)), {0, 1});

    //勾配
    std::vector<Output> grad_outputs;
    TF_CHECK_OK(AddSymbolicGradients(scope, { loss }, { w1, w2, b1, b2 }, &grad_outputs));

    //勾配降下を各変数に適用
    auto apply_w1 = ApplyGradientDescent(scope, w1, learning_rate, { grad_outputs[0] });
    auto apply_w2 = ApplyGradientDescent(scope, w2, learning_rate, { grad_outputs[1] });
    auto apply_b1 = ApplyGradientDescent(scope, b1, learning_rate, { grad_outputs[2] });
    auto apply_b2 = ApplyGradientDescent(scope, b2, learning_rate, { grad_outputs[3] });
    
    //入力、教師データのPlaceholderに流す実際のデータ
    Tensor x_data(DT_FLOAT, TensorShape{ 2, batch_size });
    Tensor y_data(DT_FLOAT, TensorShape{ 1, batch_size });

    //x_dataとy_dataにbatch_size分のランダムデータを格納する関数
    auto setData = [&x_data, &y_data](int batch_size) {
        static std::random_device seed_gen;
        static std::default_random_engine engine(seed_gen());
        static std::uniform_real_distribution<> dist(-10.0, 10.0);

        for (int i = 0; i < batch_size; i++) {
            double x1 = dist(engine), x2 = dist(engine);
            x_data.matrix<float>()(0, i) = (float)x1;
            x_data.matrix<float>()(1, i) = (float)x2;

            y_data.matrix<float>()(0, i) = (float)targetFunction(x1, x2);
        }
    };

    //出力を受け取るための変数
    std::vector<Tensor> outputs;

    //保存する変数
    auto tensors = InputList({ Input(w1), Input(w2), Input(b1), Input(b2) });

    //保存するファイル名 stringのデータは直接埋め込めないらしい?
    //learning_rateとかは直接floatを書けるのに
    //なのでいちいちPlaceholderとTensorを作って流し込むということをやる
    auto file_name_op = Placeholder(scope, DT_STRING);
    Tensor file_name(DT_STRING, TensorShape{ 1 });
    file_name.scalar<string>()() = "model.ckpt";

    //保存する変数につける名前
    auto tensor_names_op = Placeholder(scope, DT_STRING);
    Tensor tensor_names(DT_STRING, TensorShape{ 4 });
    tensor_names.vec<string>()(0) = "w1";
    tensor_names.vec<string>()(1) = "w2";
    tensor_names.vec<string>()(2) = "b1";
    tensor_names.vec<string>()(3) = "b2";

    //Restoreするときは1変数ずつやっていく必要があるので1個ずつPlaceholderとTensorを作る
    auto w1_name_op = Placeholder(scope, DT_STRING);
    Tensor w1_name(DT_STRING, TensorShape{ 1 });
    w1_name.scalar<string>()() = "w1";
    auto w2_name_op = Placeholder(scope, DT_STRING);
    Tensor w2_name(DT_STRING, TensorShape{ 1 });
    w2_name.scalar<string>()() = "w2";
    auto b1_name_op = Placeholder(scope, DT_STRING);
    Tensor b1_name(DT_STRING, TensorShape{ 1 });
    b1_name.scalar<string>()() = "b1";
    auto b2_name_op = Placeholder(scope, DT_STRING);
    Tensor b2_name(DT_STRING, TensorShape{ 1 });
    b2_name.scalar<string>()() = "b2";

    //保存するOp
    auto save = Save(scope, file_name_op, tensor_names_op, tensors);

    //読みだすOp
    auto restore_w1 = Restore(scope, file_name_op, w1_name_op, DT_FLOAT);
    auto restore_assign_w1 = Assign(scope, w1, restore_w1);
    auto restore_w2 = Restore(scope, file_name_op, w2_name_op, DT_FLOAT);
    auto restore_assign_w2 = Assign(scope, w2, restore_w2);
    auto restore_b1 = Restore(scope, file_name_op, b1_name_op, DT_FLOAT);
    auto restore_assign_b1 = Assign(scope, b1, restore_b1);
    auto restore_b2 = Restore(scope, file_name_op, b2_name_op, DT_FLOAT);
    auto restore_assign_b2 = Assign(scope, b2, restore_b2);

    //セッションを作成
    ClientSession session(scope);

    if (restore_flag == 0) {
        //重みとバイアスを初期化
        TF_CHECK_OK(session.Run({ assign_w1, assign_w2, assign_b1, assign_b2 }, nullptr));
    } else {
        //ファイルから読み込む
        TF_CHECK_OK(session.Run({ { file_name_op, file_name }, { w1_name_op, w1_name } }, { restore_assign_w1 }, nullptr));
        TF_CHECK_OK(session.Run({ { file_name_op, file_name }, { w2_name_op, w2_name } }, { restore_assign_w2 }, nullptr));
        TF_CHECK_OK(session.Run({ { file_name_op, file_name }, { b1_name_op, b1_name } }, { restore_assign_b1 }, nullptr));
        TF_CHECK_OK(session.Run({ { file_name_op, file_name }, { b2_name_op, b2_name } }, { restore_assign_b2 }, nullptr));
    }

    for (int e = 0; e <= epoch_num; e++) {
        //検証
        setData(batch_size);
        TF_CHECK_OK(session.Run({ { x, x_data }, { y, y_data } }, { loss }, &outputs));

        printf("epoch = %5d, loss = %12.1f\n", e, outputs[0].scalar<float>()());
        if (e == epoch_num) {
            break;
        }

        //学習
        setData(batch_size);
        TF_CHECK_OK(session.Run({ { x, x_data },{ y, y_data } }, { apply_w1, apply_b1, apply_w2, apply_b2 }, nullptr));
    }

    TF_CHECK_OK(session.Run({ {file_name_op, file_name}, {tensor_names_op, tensor_names} }, {}, { save }, nullptr));
}

 入力を

項目
ユニット数 1000
学習率 0.00001
バッチサイズ 1000
エポック数 100
変数の設定 0

のようにして実行するとmodel.ckptが生成され、その後変数の設定を1にして繰り返し実行すると直前で実行された損失とほとんど同じ損失(ランダム性があるのでぴったり同じとはならない)から始まり、着実に損失が減っていくことが確認できた。

tensorflowのC++APIで学習をさせる

matrices.io

 上の記事を参考にC++で3層ニューラルネットワークの学習を書いた。2変数の2次形式で表される関数を近似するというプログラムになる。

 基本的にはコメントの通りだが、Tensorからデータを取り出す、Tensorにデータを入れる部分に関してはよくわかっていない。matrixやscalarなど、適切な形と型を指定して取り出し、それに対して(i, j)でアクセスするということなのだと理解している。

//これがないとエラーになる
#define COMPILER_MSVC

#include "tensorflow/cc/client/client_session.h"
#include "tensorflow/cc/ops/standard_ops.h"
#include "tensorflow/core/framework/tensor.h"
#include "tensorflow/cc/framework/gradients.h"

using namespace tensorflow;
using namespace tensorflow::ops;

double targetFunction(double x1, double x2) {
    return 2.0 * x1 * x2 - 2.4 * x1 * x1 + 3.1 * x2 * x2 + 2.7 * x1 - 10.0 * x2 + 5.0;
}

int main() {
    int unit_num;
    std::cout << "ユニット数 : ";
    std::cin >> unit_num;
    float learning_rate;
    std::cout << "学習率 : ";
    std::cin >> learning_rate;
    int batch_size;
    std::cout << "バッチサイズ : ";
    std::cin >> batch_size;
    int epoch_num;
    std::cout << "エポック数 : ";
    std::cin >> epoch_num;

    Scope scope = Scope::NewRootScope();

    //入力,教師データのPlaceholder
    auto x = Placeholder(scope, DT_FLOAT);
    auto y = Placeholder(scope, DT_FLOAT);

    //中間層への重み、バイアス
    auto w1 = Variable(scope, { unit_num, 2 }, DT_FLOAT);
    auto assign_w1 = Assign(scope, w1, RandomNormal(scope, { unit_num, 2 }, DT_FLOAT));
    auto b1 = Variable(scope, { unit_num, 1 }, DT_FLOAT);
    auto assign_b1 = Assign(scope, b1, RandomNormal(scope, { unit_num, 1 }, DT_FLOAT));

    //出力層への重み、バイアス
    auto w2 = Variable(scope, { 1, unit_num  }, DT_FLOAT);
    auto assign_w2 = Assign(scope, w2, RandomNormal(scope, { 1, unit_num }, DT_FLOAT));
    auto b2 = Variable(scope, { 1, 1 }, DT_FLOAT);
    auto assign_b2 = Assign(scope, b2, RandomNormal(scope, { 1, 1 }, DT_FLOAT));

    //中間層
    auto hidden_layer = Relu(scope, Add(scope, MatMul(scope, w1, x), b1));

    //出力層
    auto output_layer = Add(scope, MatMul(scope, w2, hidden_layer), b2);

    //損失
    auto loss = ReduceMean(scope, Square(scope, Sub(scope, output_layer, y)), {0, 1});

    //勾配
    std::vector<Output> grad_outputs;
    TF_CHECK_OK(AddSymbolicGradients(scope, { loss }, { w1, w2, b1, b2 }, &grad_outputs));

    //勾配降下を各変数に適用
    auto apply_w1 = ApplyGradientDescent(scope, w1, Cast(scope, learning_rate, DT_FLOAT), { grad_outputs[0] });
    auto apply_w2 = ApplyGradientDescent(scope, w2, Cast(scope, learning_rate, DT_FLOAT), { grad_outputs[1] });
    auto apply_b1 = ApplyGradientDescent(scope, b1, Cast(scope, learning_rate, DT_FLOAT), { grad_outputs[2] });
    auto apply_b2 = ApplyGradientDescent(scope, b2, Cast(scope, learning_rate, DT_FLOAT), { grad_outputs[3] });
    
    //入力、教師データのPlaceholderに流す実際のデータ
    Tensor x_data(DT_FLOAT, TensorShape{ 2, batch_size });
    Tensor y_data(DT_FLOAT, TensorShape{ 1, batch_size });

    //x_dataとy_dataにbatch_size分のランダムデータを格納する関数
    auto setData = [&x_data, &y_data](int batch_size) {
        static std::random_device seed_gen;
        static std::default_random_engine engine(seed_gen());
        static std::uniform_real_distribution<> dist(-10.0, 10.0);

        for (int i = 0; i < batch_size; i++) {
            double x1 = dist(engine), x2 = dist(engine);
            x_data.matrix<float>()(0, i) = (float)x1;
            x_data.matrix<float>()(1, i) = (float)x2;

            y_data.matrix<float>()(0, i) = (float)targetFunction(x1, x2);
        }
    };

    //出力を受け取るための変数
    std::vector<Tensor> outputs;

    //セッションを作成
    ClientSession session(scope);

    //重みとバイアスを初期化
    TF_CHECK_OK(session.Run({ assign_w1, assign_w2, assign_b1, assign_b2 }, nullptr));

    for (int e = 0; e <= epoch_num; e++) {
        //検証
        setData(batch_size);
        TF_CHECK_OK(session.Run({ { x, x_data }, { y, y_data } }, { loss }, &outputs));

        printf("epoch = %5d, loss = %12.1f\n", e, outputs[0].scalar<float>()());
        if (e == epoch_num) {
            break;
        }

        //学習
        setData(batch_size);
        TF_CHECK_OK(session.Run({ { x, x_data },{ y, y_data } }, { apply_w1, apply_b1, apply_w2, apply_b2 }, nullptr));
    }
}

 簡単に実験した設定は以下のような値となる。

変数
ユニット数 1000
学習率 0.00001
バッチサイズ 100000
エポック数 50000

 最初100エポックの学習曲線は次の通り(縦軸は対数スケール)。順調に学習できていることがわかる。

f:id:tokumini:20180615123600p:plain

 50000エポックほど回すと最終的に損失は39.7になった。

 現状C++APIを使う上での問題点は

  • 変数のsave/store方法がわからない
  • ノードに名前を付ける方法がわからない
  • コンパイルに時間がかかる

などであり、現実的に運用していくにはまだ課題が多い。

評価関数の追加学習2

 前回から自己対局による強化学習を行ってます。今回も前回と同じ条件で引き続き学習を継続させてみました。

 以下が学習前のパラメータに対する学習後のパラメータの対局結果となります。

モデル 対局数 勝利 引き分け 敗北 勝率
3回アップデート後 1736 828 154 754 52.1
6回アップデート後 422 206 47 169 54.4
8回アップデート後 1082 601 118 363 61.0

 8回アップデートが起きてからは勝率が5割以下でずっと推移するようになってしまったので学習を打ち切りました。

 8回アップデート時点では学習前に対してEloレート差約77.7となりました。

 強くなってはいるのですが、アップデートをかける条件が勝率55%であることを考えると、アップデート回数に対して伸びが小さいようにも見えます。100局ごとの勝率測定では誤差も大きく、強くなっていないのにアップデートがかかる場合もあることが原因なのではないかと思われますが、詳しくは不明です。

 8回アップデートしたものとWCSC28版を対局させてみました。探索部側にも変更があるので純粋な評価関数の比較とはなりませんが、現時点でのkkp_kpptでの最強バージョンとWCSC28版(kp_pp)との対局ということになります。

 1手2秒で対局させた結果(kkp_kppt視点)が以下となります。

対局数 勝利 引き分け 敗北 勝率
1075 378 47 650 37.3

 Eloレートでは約-90.2です。そこそこ強くなってきてはいるんですが、まだ足りてませんね。この段階で伸びが止まってしまったのが悩みどころです。学習時のハイパーパラメータをいじればまだ伸びるか、次元下げを導入することで伸びてくれるか、なんにせよもっと学習部をいじる必要がありそうです。