2019/06/18


2016年、普段から現場でGoを使っている名立たるGoプログラマの皆さんと一緒に「みんなのGo言語」という書籍を執筆させて頂きました。

「みんなのGo言語」は他のリファレンス本とは異なり、Go言語の最新事情をお伝えする事に主眼を置いて書きました。

インストール方法や使い方、モダンなテストの書き方、ツールの使い方等も執筆時点での最新情報を書かせて頂きました。

これはとても意義がある事だった感じています。

しかしこれは逆に、時間が経つにつれ執筆した内容が次第に古くなってしまうというリスクを伴います。幾つかの内容は、3年経った現在に合わなくなっている物も出てきました。紹介したツールの中には開発が止まってしまっている物もあれば、執筆時点で制限事項と記したけれども現在では解消している物も出てきています。そればかりではなく新しく追加されたコマンドや機能、新しい制限事項もあります。特に Go Module の扱いは、今後Go言語を扱う開発者にとって知っておくべき最重要項目とも言えるでしょう。


今回、「みんなのGo言語」の改訂というチャンスを頂き、内容を再び更新させて頂く事になりました。古い情報をバッサリと書き直している章もあります。Go言語で開発をする上での最新のヒントも書かせて頂きました。特に今回、新しい章「第7章 データベースの扱い方」を執筆させて頂きました。データベースを使ったウェブアプリケーションの作り方を分かりやすく書かせて頂いたつもりです。

ぜひ新しくなった「みんなのGo言語」をお楽しみ下さい。

改訂2版 みんなのGo言語 改訂2版 みんなのGo言語
松木 雅幸, mattn, 藤原 俊一郎, 中島 大一, 上田 拓也, 牧 大輔, 鈴木 健太
技術評論社 単行本(ソフトカバー) / ¥2,398 (2019年08月01日)
 
発送可能時間:

Kindle 版はこちら

改訂2版 みんなのGo言語 改訂2版 みんなのGo言語
松木 雅幸, mattn, 藤原 俊一郎, 中島 大一, 上田 拓也, 牧 大輔, 鈴木 健太
技術評論社 Kindle版 / ¥2,278 (2019年08月01日)
 
発送可能時間:

Posted at by



2019/05/16


Go は最近のプログラミング言語にしては珍しくポインタを扱えるプログラミング言語。とはいってもC言語よりも簡単で、オブジェクトの初期化やメソッドの定義以外の場所ではおおよそポインタを使っている様には見えない。メソッドやフィールドへのアクセスも . で出来るし Duck Type によりインタフェースを満たしていれば実体であろうとポインタであろうとそれほど意識する必要はない。ところがこの便利さに乗っかってしまうと思わぬ所で足をすくわれてしまう。

package main

type foo struct {
    v int
}

func (f foo) add(v int) {
    f.v = v
}

func main() {
    var a foo

    a.add(3)

    println(a.v)
}

このコードは 0 が表示される。メソッドを呼び出す際にはレシーバのオブジェクトを得る必要があるが、foo は実体なのでコピーが生成される。例えばレシーバ f が引数であったと考えると理解しやすくなる。

package main

type foo struct {
    v int
}

func add(f foo, v int) {
    f.v += v
}

func main() {
    var a foo

    add(a, 3)

    println(a.v)
}

メソッドとレシーバの関係は、実は「単なる関数と第一引数」と考えると分かりやすい。特にC言語でオブジェクト指向をやる様な人達は訓練されているので、Go をやる上でもこの辺りの動作を意識せず扱えているのかもしれない。

実体のレシーバにメソッドを生やすメリットが無い訳ではない。例えばオブジェクトの値を明示的に変更させたくない場合がそれで、そういった設計にしたい場合は戻り値を使う。

package main

type foo struct {
    v int
}

func (f foo) add(v int) foo {
    f.v += v
    return f
}

func main() {
    var a foo

    a = a.add(3)

    println(a.v)
}

これとは別に、ポインタと実体をまぜて考えてしまうと失敗してしまう事もある。

この例のポイントは「for range の反復変数はループ毎に新しいコピーが作成されない」という事。つまり上書きになる。プログラマは一見このループが実行されると以下の様になると考えてしまう。

= append(b, c[0].f())
= append(b, c[1].f())

だが実際はこう。

var i a
= c[0]
= append(b, i.f())
= c[1]
= append(b, i.f())

The Go Playground

slice の b は一見、c の情報が詰め込まれている様に見えるが、実際は i の情報が詰め込まれている。なので2回目の i への代入時に「i が持っていたレシーバの情報 c[0]c[1] 上書きされてしてしまう事になり、結果 a, b ではなく b, b が表示される。Method values という仕組みは参考として見ておくと良い。i への代入時点でレシーバの情報が デリファレンス されコピーされた状態で代入されているのがポイント、f() の呼び出し時の話ではない。

package main

import (
    "fmt"
)

type a struct {
    N string
}

func (s *a) f() func() {
    return func() {
        fmt.Printf("%s\n", s.N)
    }
}

func main() {
    b := []func(){}
    c := []a{{"a"}, {"b"}}

    var i a

    i = c[0]
    b = append(b, i.f())

    b[0]()
    i = c[1]
    b[0]() // この時点で既に b になる
    b = append(b, i.f())
}

まとめ

混ぜるな危険

みんなのGo言語【現場で使える実践テクニック】 みんなのGo言語【現場で使える実践テクニック】
松木雅幸, mattn, 藤原俊一郎, 中島大一, 牧 大輔, 鈴木健太, 稲葉貴洋
技術評論社 大型本 / ¥189 (2016年09月09日)
 
発送可能時間:

Posted at by




これまで 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 boolerror {
    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 []float32error {
    b := make([][1][1]float32len(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
PythonとKerasによるディープラーニング PythonとKerasによるディープラーニング
Francois Chollet, 巣籠 悠輔, 株式会社クイープ
マイナビ出版 単行本(ソフトカバー) / ¥4,268 (2018年05月28日)
 
発送可能時間:

Posted at by