これまで Go で TensorFlow を扱うのは推論しかやった事が無かったけど、API を眺めていたら学習できそうだったので試してみた。
Go による機械学習 推論フレームワークの最新動向 2019 - Qiita
Golang で推論 昨今では「機械学習と言えば Python」「Python と言えば機械学習」と思われがちなのですが、推論用途であれば学習済みモデルを利用して色々なプログラミング言語から扱えます。...
https://qiita.com/mattn/items/b01f9bb5c2fa3678734a
ただし色々調べてみたのだけどグラフ定義を出力するのは難しかった。難しかったというか Protocol Buffer 形式のファイルを出力するまでは出来たのだけど Python 版でいうオペレータの名付けであったり勾配降下法(GradientDescentOptimizer)の作り方がいまいち分からなかった。また Go でグラフ定義を出力すると、Python が吐く様な init, train, save/control_dependency, save/restore_all といったモデルの保存オペレータが生成されない。なのでこの記事では Python でグラフ定義を出力し、そこから入力と出力を与える様なミニバッチを走らせ、checkpoint を保存するという形で学習させた。
ベースは asimshankar さんが書いたC言語による実装。
Training TensorFlow models in C - GitHub
Training TensorFlow models in C Python is the primary language in which TensorFlow models are typica...
https://gist.github.com/asimshankar/7c9f8a9b04323e93bb217109da8c7ad2
まず Python でグラフ定義を作る。
x = tf.placeholder(tf.float32, shape=[None,1,1], name='input')
y = tf.placeholder(tf.float32, shape=[None,1,1], name='target')
y_ = tf.identity(tf.layers.dense(x, 1), name='output')
loss = tf.reduce_mean(tf.square(y_ - y), name='loss')
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.01)
train_op = optimizer.minimize(loss, name='train')
init = tf.global_variables_initializer()
saver_def = tf.train.Saver().as_saver_def()
with open('graph.pb', 'wb') as f:
  f.write(tf.get_default_graph().as_graph_def().SerializeToString())
この Python が出力したグラフ定義には、モデルを操作する為のオペレータも一緒に出力されているので、それを以下の様に参照して呼び出せる様にする。
func createModel(graph_def_filename string) (*model_t, error) {
    model := &model_t{}
    model.graph = tf.NewGraph()
    var err error
    // create the session.
    sessionOpts := &tf.SessionOptions{}
    model.session, err = tf.NewSession(model.graph, sessionOpts)
    if err != nil {
        return nil, err
    }
    b, err := ioutil.ReadFile(graph_def_filename)
    if err != nil {
        return nil, err
    }
    err = model.graph.Import(b, "")
    if err != nil {
        return nil, err
    }
    model.input.Op = model.graph.Operation("input")
    model.input.Index = 0
    model.target.Op = model.graph.Operation("target")
    model.target.Index = 0
    model.output.Op = model.graph.Operation("output")
    model.output.Index = 0
    model.initOp = model.graph.Operation("init")
    model.trainOp = model.graph.Operation("train")
    model.saveOp = model.graph.Operation("save/control_dependency")
    model.restoreOp = model.graph.Operation("save/restore_all")
    model.checkpointFile.Op = model.graph.Operation("save/Const")
    model.checkpointFile.Index = 0
    return model, nil
}
これで init というオペレータを実行すると空のモデルとして初期化される。
func initializeModel(model *model_t) error {
    _, err := model.session.Run(
        nil,
        nil,
        []*tf.Operation{model.initOp})
    return err
}
また save や restore で checkpoint を更新したり学習済みモデルを保存する事ができる。
func createCheckpoint(model *model_t, checkpoint_prefix string, save bool) error {
    t, err := tf.NewTensor(checkpoint_prefix)
    if err != nil {
        return err
    }
    var op *tf.Operation
    if save {
        op = model.saveOp
    } else {
        op = model.restoreOp
    }
    _, err = model.session.Run(
        map[tf.Output]*tf.Tensor{model.checkpointFile: t},
        nil,
        []*tf.Operation{op})
    if err != nil {
        return err
    }
    return nil
}
ここまで出来ればあとはセッションを実行するだけになる。モデルとしては「3かけて2を足す」計算。3層なので配列操作が若干面倒。
func train(model *model_t) error {
    var inputs [10][1][1]float32
    var targets [10][1][1]float32
    for i := 0; i < len(inputs); i++ {
        inputs[i][0][0] = rand.Float32()
        targets[i][0][0] = 3.0*inputs[i][0][0] + 2.0
    }
    x, err := tf.NewTensor(inputs)
    if err != nil {
        return err
    }
    y, err := tf.NewTensor(targets)
    if err != nil {
        return err
    }
    _, err = model.session.Run(
        map[tf.Output]*tf.Tensor{
            model.input:  x,
            model.target: y,
        },
        nil,
        []*tf.Operation{model.trainOp})
    return err
}
また predict は以下の通り。
func predict(model *model_t, batch []float32) error {
    b := make([][1][1]float32, len(batch))
    for i, v := range batch {
        b[i][0][0] = v
    }
    t, err := tf.NewTensor(b)
    if err != nil {
        return err
    }
    result, err := model.session.Run(
        map[tf.Output]*tf.Tensor{model.input: t},
        []tf.Output{model.output},
        nil)
    if err != nil {
        return err
    }
    predictions := result[0].Value().([][][]float32)
    println("Predictions:")
    for i := 0; i < len(predictions); i++ {
        fmt.Printf("\t x = %f, predicted y = %f\n", batch[i], predictions[i][0][0])
    }
    return nil
}
このプログラムを動かすと、始めは計算を予測できないのでデタラメな結果が返る。
Initial predictions
Predictions:
         x = 1.000000, predicted y = 0.349265
         x = 2.000000, predicted y = 0.698529
         x = 3.000000, predicted y = 1.047794
上記の学習用ミニバッチ train を200回呼び出すと、おおよそ「3かけて2を足す」に近い結果がでる。
Training for a few steps
Updated predictions
Predictions:
         x = 1.000000, predicted y = 4.589828
         x = 2.000000, predicted y = 6.679979
         x = 3.000000, predicted y = 8.770130
このプログラムはミニバッチを実行した結果を checkpoint として保存するので、実行する毎に前回のモデルを参考に期待の結果に近い値を返す様になる。10回プログラムを実行(学習は200回ずつなので計2000回)した結果は以下の通り。
Predictions:
         x = 1.000000, predicted y = 4.947572
         x = 2.000000, predicted y = 7.833277
         x = 3.000000, predicted y = 10.718983
おおよそ「3かけて2を足す」に近い結果になった。グラフ定義の出力こそ出来ていないが、これは今後必要なインタフェースが無いと分かれば TensorFlow に pull-request を投げるかもしれない。
出来上がったソースコードは Gist に上げておいた。
main.go - GitHub
You signed in with another tab or window. Reload to refresh your session. You signed out in another ...
https://gist.github.com/5ae333847399209d75f1d3e52631f002
 
