介绍
之前的博客中,翻译过 Go 语言可以通过 Tensorflow 的 go 客户端进行操作,但是其中有两个问题很容易在编码时遇到下面的问题。
- Scope:每次调用定义操作的函数时,Go API 并不会自动生成新的节点名称。会出现
panic: failed to add operation "Placeholder": Duplicate node name in graph: 'Placeholder'
- Typing system:
REGISTER_OP
宏对MatMul
定义接口的操作描述中,提到T
支持的类型有{half, float, double, int32, complex64, complex128}
,Go 绑定版tf
有自己的一套类型,基本与 Go 本身的类型 1:1 相映射,如果错误使用类型则出会错,例如使用int64
,则会出现
panic: failed to add operation "MatMul": Value for attr 'T' of int64 is not in the list of allowed values: bfloat16, half, float, double, int32, complex64, complex128;
第三方包
为了避免出现上述问题,在这里可以引入第三方包 galeone/tfgo
。 GitHub 地址为:
https://github.com/galeone/tfgo
主要解决问题:
- 作用域(Scope):每个新节点都有一个新的惟一名称;
- 类型系统(Typing system):属性将自动转换为受支持的类型,而不是在运行时抛出错误。
Tensorflow 的 Go 绑定的核心数据结构是 op.Scope
结构。包 galeone/tfgo
允许创建新的 *op
。解决了上面提到的作用域(Scope)问题。
既然我们定义了一个图,让我们从它的根开始(空图),代码示例:
root := tg.NewRoot()
现在可以将节点放入这个图中并将它们连接起来。假设要为一个列向量乘以一个矩阵,然后向结果中添加另一个列向量。
测试代码如下:
package main
import (
"fmt"
tg "galeone/tfgo"
tf "github.com/tensorflow/tensorflow/tensorflow/go"
)
func main() {
root := tg.NewRoot()
A := tg.NewTensor(root, tg.Const(root,[2][2]int32{{1, 2}, {-1, -2}}))
x := tg.NewTensor(root, tg.Const(root,[2][1]int32{{0}, {100}}))
// 矩阵 A * x
Y := A.MatMul(x.Output)
// 结果输出
results := tg.Exec(root, []tf.Output{Y.Output}, nil, &tf.SessionOptions{})
fmt.Println("Y: ", results[0].Value())
}
输出结果:
Y: [[200] [-200]]
下面是官方完整的源代码:
package main
import (
"fmt"
tg "github.com/galeone/tfgo"
tf "github.com/tensorflow/tensorflow/tensorflow/go"
)
func main() {
root := tg.NewRoot()
A := tg.NewTensor(root, tg.Const(root, [2][2]int32{{1, 2}, {-1, -2}}))
x := tg.NewTensor(root, tg.Const(root, [2][1]int64{{10}, {100}}))
b := tg.NewTensor(root, tg.Const(root, [2][1]int32{{-10}, {10}}))
Y := A.MatMul(x.Output).Add(b.Output)
// Please note that Y is just a pointer to A!
// If we want to create a different node in the graph, we have to clone Y
// or equivalently A
Z := A.Clone()
results := tg.Exec(root, []tf.Output{Y.Output, Z.Output}, nil, &tf.SessionOptions{})
fmt.Println("Y: ", results[0].Value(), "Z: ", results[1].Value())
fmt.Println("Y == A", Y == A) // ==> true
fmt.Println("Z == A", Z == A) // ==> false
}
结果如下:
Y: [[200] [-200]] Z: [[200] [-200]]
Y == A true
Z == A false
利用数据流图实现计算机视觉
Tensorflow 中有很多对图像执行操作的方法。tfgo
提供了图像包,允许使用 Go 绑定以一种优雅的方式执行计算机视觉任务。
例如,可以读取图像,计算其沿水平和垂直方向的方向导数,计算梯度并保存它。
下面的代码就是这样做的,显示了使用相关性和卷积操作得到的不同结果。
package main
import (
tg "github.com/galeone/tfgo"
"github.com/galeone/tfgo/image"
"github.com/galeone/tfgo/image/filter"
"github.com/galeone/tfgo/image/padding"
tf "github.com/tensorflow/tensorflow/tensorflow/go"
"os"
)
func main() {
root := tg.NewRoot()
grayImg := image.Read(root, "../image/1.png", 1)
grayImg = grayImg.Scale(0, 255)
// Edge detection using sobel filter: convolution
Gx := grayImg.Clone().Convolve(filter.SobelX(root), image.Stride{X: 1, Y: 1}, padding.SAME)
Gy := grayImg.Clone().Convolve(filter.SobelY(root), image.Stride{X: 1, Y: 1}, padding.SAME)
convoluteEdges := image.NewImage(root.SubScope("edge"), Gx.Square().Add(Gy.Square().Value()).Sqrt().Value()).EncodeJPEG()
Gx = grayImg.Clone().Correlate(filter.SobelX(root), image.Stride{X: 1, Y: 1}, padding.SAME)
Gy = grayImg.Clone().Correlate(filter.SobelY(root), image.Stride{X: 1, Y: 1}, padding.SAME)
correlateEdges := image.NewImage(root.SubScope("edge"), Gx.Square().Add(Gy.Square().Value()).Sqrt().Value()).EncodeJPEG()
results := tg.Exec(root, []tf.Output{convoluteEdges, correlateEdges}, nil, &tf.SessionOptions{})
file, _ := os.Create("convolved.png")
file.WriteString(results[0].Value().(string))
file.Close()
file, _ = os.Create("correlated.png")
file.WriteString(results[1].Value().(string))
file.Close()
}
用 Python 训练,用 Go 服务
只要深入研究下面的示例,就可以理解如何使用 tfgo
为经过训练的模型提供服务。
首先使用 Python 进行训练:
import sys
import tensorflow as tf
from dytb.inputs.predefined.MNIST import MNIST
from dytb.models.predefined.LeNetDropout import LeNetDropout
from dytb.train import train
def main():
"""main executes the operations described in the module docstring"""
lenet = LeNetDropout()
mnist = MNIST()
info = train(
model=lenet,
dataset=mnist,
hyperparameters={"epochs": 2},)
checkpoint_path = info["paths"]["best"]
with tf.Session() as sess:
# Define a new model, import the weights from best model trained
# Change the input structure to use a placeholder
images = tf.placeholder(tf.float32, shape=(None, 28, 28, 1), name="input_")
# define in the default graph the model that uses placeholder as input
_ = lenet.get(images, mnist.num_classes)
# The best checkpoint path contains just one checkpoint, thus the last is the best
saver = tf.train.Saver()
saver.restore(sess, tf.train.latest_checkpoint(checkpoint_path))
# Create a builder to export the model
builder = tf.saved_model.builder.SavedModelBuilder("export")
# Tag the model in order to be capable of restoring it specifying the tag set
# clear_device=True in order to export a device agnostic graph.
builder.add_meta_graph_and_variables(sess, ["tag"], clear_devices=True)
builder.save()
return 0
if __name__ == '__main__':
sys.exit(main())
使用 Go 进行服务,代码如下:
package main
import (
"fmt"
tg "github.com/galeone/tfgo"
tf "github.com/tensorflow/tensorflow/tensorflow/go"
)
func main() {
model := tg.LoadModel("test_models/export", []string{"tag"}, nil)
fakeInput, _ := tf.NewTensor([1][28][28][1]float32{})
results := model.Exec([]tf.Output{
model.Op("LeNetDropout/softmax_linear/Identity", 0),
}, map[tf.Output]*tf.Tensor{
model.Op("input_", 0): fakeInput,
})
predictions := results[0].Value().([][]float32)
fmt.Println(predictions)
}
之所以会这样,是因为用图来表示计算,并且用这种方式描述计算是很有挑战性的。
此外,tfgo
支持 GPU 计算,并允许编写并行代码,而且无需担心执行设备问题。