本文译自Danijar Hafner的博客Structuring Your TensorFlow Models。
构建计算图
一般来说会对每个模型建立一个class,这个class的接口是什么呢?通常模型会连接一些输入数据和目标的placeholders以及提供一些训练、评估和前向传播的操作(operation),下面是一个例子,展示了一个全连接神经网络:
class Model:
def __init__(self, data, target):
data_size = int(data.get_shape()[1])
target_size = int(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(data, weight) + bias
self._prediction = tf.nn.softmax(incoming)
cross_entropy = -tf.reduce_sum(target, tf.log(self._prediction))
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
这是一个基本结构。然而这里存在一些问题,最显著的问题是整个计算图是用单个函数定义的,这减少了可读性和可重用性。
使用Property装饰器
仅仅将代码分割为不同的函数不管用,因为一旦函数被调用,计算图就会增加(这点译者深有体会,Tensorflow中的代码复用和传统代码复用不一致,因为它会为每一行代码构建计算节点,即使该节点所使用的参数是同一套)。因此,我们需要确保操作(operation)仅在函数第一次被调用的时候加入计算图,这是基本的惰性编程(lazy-coding)思想。
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
这个例子比第一个例子好多了,现在代码被划分成了不同的函数。然而这个代码还是有点冗余(因为每个函数都用了相同的逻辑:if not ……,这个部分让代码看上去嵌套而不扁平,所以这个部分可用装饰器重用)。
惰性属性装饰器(Lazy Property Decorator)
上面的例子使用了property装饰器,它将函数的返回结构存储到一个以函数名为名字的对象属性中。现在我们还可以将惰性编程的部分加入装饰器。
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
现在我们的代码就可以更佳简化了,如下所示:
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()
前需要定义好。
用Scopes组织计算图
使用上面的例子产生的计算图依旧非常拥挤,如果你可视化整个计算图,那么它会包含很多内部的小节点,一个解决方式是使用tf.name_scope('name')
或者tf.variable_scope('name')
。这样节点会被分组,可视化非常直观。我们可以通过调整之前的装饰器,将一个函数的名字作为其命名空间:
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
这样我们就定义了一个紧凑、可读性强的代码。
网友评论