译自http://danijar.com/structuring-your-tensorflow-models/
使用TensorFlow构建神经网络模型很容易导致较大的代码量,那么如何以可读和可复用的方式构建代码?(没耐心的可直接参考可直接参考源代码https://gist.github.com/danijar/8663d3bbfd586bffecf6a0094cd116f2)
定义计算图
在每一个模型里面定义一个类是一个较好的选择。 那么,如何定义该类的接口呢? 通常,每个模型会连接到一些输入数据和占位符,并提供training,evaluation和inference的操作。
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
以上代码是如何在Tensorflow中定义模型的基本示例。但是,它有一些问题。 其中最值得注意的是,整个graph是在单个函数(即构造函数__init__)中定义的, 这既不是可读也不可复用。
使用 Properties
只需将代码拆分为函数就不起作用,因为每次调用函数时,图形中就会添加其他代码。 因此,我们必须确保只有当第一次调用函数时,操作才会添加到图形中,即使用 lazy-loading。
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
比第一个例子要好很多, 代码现在被组织为多个函数。 然而,由于lazy-loading逻辑,代码仍然有点膨胀。 让我们看看可以如何进一步改进。
Lazy Property Decorator
Python是一种非常灵活的语言, 如何从最后一个例子中删除冗余代码呢? 可以使用一个类似于@property的装饰器,但只能对该函数进行一次评估。 它将结果存储在以装饰器函数命名的成员中(加一个前缀),并在后续的任意调用中返回此值。 如果您尚未使用自定义装饰器,可以参考下这个教程:http://blog.apcelent.com/python-decorator-tutorial-with-example.html
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))
请注意,我们在构造函数中的添加了properties。 这样,可以保证在我们运行tf.initialize_variables()时,整个graph会被创建。
使用Scope组织计算图
现在,我们有一个简洁明了的方法定义了代码中的模型,但是所得的计算图仍然十分繁缛。 如果你可视化计算图,可以发现大量互连的小型节点。 解决方案是用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
给装饰器一个新名称,因为除了惰性缓存外它还具有特定于TensorFlow的功能。 除此之外,该模型看起来与前一个模型相同。
我们可以更进一步地,使用@define_scope装饰器将参数转发给tf.variable_scope(),例如为作用域定义默认的initializer。 有兴趣的话可查看完整示例:https://gist.github.com/danijar/8663d3bbfd586bffecf6a0094cd116f2
现在我们可以以结构化和紧凑的方式定义模型,从而构造结构清晰的计算图。