jlearning.cn

如何重构TensorFlow模型

构建你的TensorFlow模型

翻译自:http://danijar.com/structuring-your-tensorflow-models/

Defining your models in TensorFlow can easily result in one huge wall of code. How to structure your code in a readable and reusable way? For the inpacient of you, here is the link to a working example gist.

第一句话不会翻译”wall of code”,算了,这不重要。

定义计算图(Compute Graph)

每个模型使用一个类作为开始是非常明智的。类的接口是什么?通常,你的模型连接一些输入数据和目标placeholders,并且提供训练、测试的操作和接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Model:
def __init__(self, data, target):
data_size = int(data.get_shape()[1])
target_size = int(target.get_shape()[1])
#truncated_normal返回正态分布的随机值
weight = tf.Variable(tf.truncated_normal([data_size, target_size]))
bias = tf.Variable(tf.constant(0.1, shape=[target_size]))
incoming = tf.matmul(data, weight) + bias
self._prediction = tf.nn.softmax(incoming)
#计算交叉熵,reduce_sum还能这样用?和文档写的不一样啊。
cross_entropy = -tf.reduce_sum(target, tf.log(self._prediction))
#使用RMSPropOptimizer以0.03为learning rate最小化交叉熵
self._optimize = tf.train.RMSPropOptimizer(0.03).minimize(cross_entropy)
mistakes = tf.not_equal(
tf.argmax(target, 1), tf.argmax(self._prediction, 1))
#求平均误差
self._error = tf.reduce_mean(tf.cast(mistakes, tf.float32))
@property
def prediction(self):
return self._prediction
@property
def optimize(self):
return self._optimize
@property
def error(self):
return self._error

这基本上是在TensorFlow中定义模型的基础代码。但是他有一些问题。最值得注意的是,这个图在单个函数(构造函数)中定义,非常不具备可读性和可重用性。

使用属性

仅仅将代码分割在函数中是不行的,因为每次函数被调用的时候,图都会被新的代码扩展。因此,我们需要确保这些操作只有在函数第一次调用的时候才添加到图里面。这就是基本的惰性加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Model:
def __init__(self, data, target):
self.data = data
self.target = target
self._prediction = None
self._optimize = None
self._error = None
@property
def prediction(self):
if not self._prediction:
data_size = int(self.data.get_shape()[1])
target_size = int(self.target.get_shape()[1])
weight = tf.Variable(tf.truncated_normal([data_size, target_size]))
bias = tf.Variable(tf.constant(0.1, shape=[target_size]))
incoming = tf.matmul(self.data, weight) + bias
self._prediction = tf.nn.softmax(incoming)
return self._prediction
@property
def optimize(self):
if not self._optimize:
cross_entropy = -tf.reduce_sum(self.target, tf.log(self.prediction))
optimizer = tf.train.RMSPropOptimizer(0.03)
self._optimize = optimizer.minimize(cross_entropy)
return self._optimize
@property
def error(self):
if not self._error:
mistakes = tf.not_equal(
tf.argmax(self.target, 1), tf.argmax(self.prediction, 1))
self._error = tf.reduce_mean(tf.cast(mistakes, tf.float32))
return self._error

这比第一个例子中的代码好多了。你的代码现在被构建在函数中,你现在可以分别关注他们。但是,由于延迟加载逻辑,代码仍然有一些臃肿。下面让我们改正它。

惰性属性装饰器

python是一个相当灵活的语言。现在让我想你展示如何剔除上一段代码中的冗余代码。我们将使用一个行为就像@property一样的装饰器,但是只执行函数一次。他会在被装饰的函数后面将结果储存在一个命名成员中,而且后面的调用中返回结果。如果你还没有用过自定义装饰器,你可能需要看一下这个教程

1
2
3
4
5
6
7
8
9
10
11
12
13
import functools
def lazy_property(function):
attribute = '_cache_' + function.__name__
@property
@functools.wraps(function)
def decorator(self):
if not hasattr(self, attribute):
setattr(self, attribute, function(self))
return getattr(self, attribute)
return decorator

使用这个装饰器后,我们的例子简化成为下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Model:
def __init__(self, data, target):
self.data = data
self.target = target
self.prediction
self.optimize
self.error
@lazy_property
def prediction(self):
data_size = int(self.data.get_shape()[1])
target_size = int(self.target.get_shape()[1])
weight = tf.Variable(tf.truncated_normal([data_size, target_size]))
bias = tf.Variable(tf.constant(0.1, shape=[target_size]))
incoming = tf.matmul(self.data, weight) + bias
return tf.nn.softmax(incoming)
@lazy_property
def optimize(self):
cross_entropy = -tf.reduce_sum(self.target, tf.log(self.prediction))
optimizer = tf.train.RMSPropOptimizer(0.03)
return optimizer.minimize(cross_entropy)
@lazy_property
def error(self):
mistakes = tf.not_equal(
tf.argmax(self.target, 1), tf.argmax(self.prediction, 1))
return tf.reduce_mean(tf.cast(mistakes, tf.float32))

注意,我们提到构造函数中的属性。这样可以确保完整的图在我们执行tf.initialize_variables()时被定义。

通过命名空间组织图

我们现在已经有一个清晰的方式去定义我们的模型,但是导致计算图非常拥挤。如果你想对图进行可视化,他将会有用很多互联的小节点。现在要在每个函数外面包裹tf.name_scope('name')或者tf.varizble_scope('name')。节点将会在图中分组。我们可以调整我们前面的装饰器去自动实现这些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import functools
def define_scope(function):
attribute = '_cache_' + function.__name__
@property
@functools.wraps(function)
def decorator(self):
if not hasattr(self, attribute):
with tf.variable_scope(function.__name):
setattr(self, attribute, function(self))
return getattr(self, attribute)
return decorator

我给这个装饰器一个新的名字(define_scope),因为除了添加惰性缓存之外他还有针对RensorFlow的特定功能。除此之外,这个模型看上去和前一个一样。

我们可以进一步的允许@define_scope装饰器forward arguments to the tf.variable_scope(),例如定义一个命名空间内的默认的初始化器(initializer)。可以参考作者上传的完整版代码:https://gist.github.com/danijar/8663d3bbfd586bffecf6a0094cd116f2