利用Python装饰器来组织Tensorflow代码的结构
装饰器
定义Python装饰器
装饰器是一种设计模式, 可以使用OOP中的继承和组合实现, 而Python还直接从语法层面支持了装饰器.
装饰器可以在不改变函数定义的前提下, 在代码运行期间动态增加函数的功能, 本质上就是将原来的函数与新加的功能包装成一个新的函数wrapper, 并让原函数的名字指向wrapper.
Python中实现decorator有两种方式: 函数方式 和 类方式
函数方式
可以用一个返回函数的高阶函数来实现装饰器
简单的无参数装饰器
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
@log
def now():
print('NOW')
在函数fun的定义前面放入@decorator实现的功能相当于fun=decorator(fun)
,
从而现在调用now()将打印前面的调用信息.
实现带参数的装饰器
只要给装饰器提供参数后,返回的object具备一个无参数装饰器的功能即可.
可以用返回无参数装饰器函数的高阶函数来实现.
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator
@log('execute')
def now():
print("parametric NOW")
该语法糖相当于now=log('execute')(now)
.
如果要保存原函数的__name__
属性, 使用python的functools
模块中的wraps()
装饰器, 只需要将@functools.wraps(func)
放在def wrapper()
前面即可.该装饰器实现的功能就相当于添加了wrapper.__name__ = func.__name__
语句.
类方式
Python中的类和函数差别不大, 实现类的__call__
method就可以把类当成一个函数来使用了.
实现以上带参数装饰器同样功能的装饰器类的代码如下:
class log():
def __init__(self, text):
self.text = text
def __call__(self,func):
@functools.wraps(func)
def wrapper(*args, **kw):
print("%s %s" % (self.text, func.__name__))
return func(*args, **kw)
return wrapper
@log("I love Python")
def now():
print("class decorator NOW")
使用类的好处是可以继承
使用场景
装饰器最巧妙的使用场景在Flask和Django Web框架中,它可以用来检查某人是否被授权使用Web应用的某个endpoint(假设是f函数), 下面是一个检查授权的示意性代码片段.
from functools import wraps
def require_auth(f):
@wraps(f)
def decorated(*args, **kw):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
authenticate()
return f(*args, **kw)
return decorated
另一个常见的用处是用于日志记录
from functools import wraps
def logit(func):
@wraps(func)
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
@logit
def addition_func(x):
"""Do some math."""
return x + x
result = addition_func(4)
是不是超级灵活呢? 虽然装饰器有点难定义, 但是一旦掌握, 它就像不可思议的魔法. Σ(*゚д゚ノ)ノ
利用装饰器改善你的Tensorflow代码结构
重头戏终于来了! 当你在写Tensorflow代码时, 定义模型的代码和动态运行的代码经常会混乱不清. 一方面, 我们希望定义compute graph的"静态"Python代码只执行一次, 而相反, 我们希望调用session来运行的代码可以运行多次取得不同状态的数据信息, 而两类代码一旦杂糅在一起, 很容易造成Graph中有冗余的nodes被定义了多次, 感觉十分不爽, 写过那种丑代码的你们都懂.
那么,如何以一种可读又可复用的方式来组织你的TF代码结构呢?
版本1
我们都希望用一个类来抽象一个模型, 这无疑是明智的. 但是如何定义类的接口呢?
我们的模型需要接受input的feature data和target value, 需要进行 training, evaluation 和 inference 操作.
class Model:
def __init__(self, data, target):
data_size = int(data.get_shape()[1]) # 假设data的shape为[N,D] N为Batch Size D是输入维度
target_size = int(target.get_shape()[1]) # 假设target的shape为[N,K] K是one-hot的label深度, 即要分类的类的数量
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_mean(-tf.reduce_sum(target * tf.log(self._prediction), reduction_indices=[1]))
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
这是最基本的形式, 但是它存在很多问题. 最严重的问题是整个图都被定义在init构造函数中, 这既不可读又不可复用.
版本2
直接将代码分离开来,放在多个函数中是不行的, 因为每次函数调用时都会向Graph中添加nodes, 所以我们必须确保这些Node Operations只在函数第一次调用的时候才添加到Graph中, 这有点类似于singleton模式, 或者叫做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_mean(-tf.reduce_sum(self.target * tf.log(self._prediction), reduction_indices=[1]))
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判断还是有点太臃肿, 利用装饰器, 我们可以做的更好!
版本3
实现一个自定义装饰器lazy_property, 它的功能和property类似,但是只运行function一次, 然后将返回结果存在一个属性中, 该属性的名字是 "_cache_" + function.__name__, 后续函数调用将直接返回缓存好的属性.
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_mean(-tf.reduce_sum(self.target * tf.log(self.prediction), reduction_indices=[1]))
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))
注意, 在init构造函数中调用了属性prediction,optimize和error, 这会让其第一次执行, 因此构造函数完成后Compute Graph也就构建完毕了.
有时我们使用TensorBoard来可视化Graph时, 希望将相关的Node分组到一起, 这样看起来更为清楚直观, 我们只需要修改之前的lazy_property
装饰器, 在其中加上with tf.name_scope("name")
或者 with 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的模型了, 这归功于Python的强大的decorator语法糖.
References:
- https://danijar.com/structuring-your-tensorflow-models/
- https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014318435599930270c0381a3b44db991cd6d858064ac0000
- https://eastlakeside.gitbooks.io/interpy-zh/content/decorators/deco_class.html
- https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386820062641f3bcc60a4b164f8d91df476445697b9e000
- https://www.tensorflow.org/get_started/mnist/beginners?hl=zh-cn#training
- https://mozillazg.github.io/2016/12/python-super-is-not-as-simple-as-you-thought.html