利用Python装饰器(decorator)来改善Tensorflow代码的结构

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zongza/article/details/84951240

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

装饰器

定义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:

  1. property:廖雪峰python教程 相当于get_value(private的属性)
  2. https://eastlakeside.gitbooks.io/interpy-zh/content/decorators/deco_class.html
  3. 装饰器:廖雪峰python教程 tf中主要是autograph.convert()用来自动转成静态图代码
  4. super类:blog super()用来调用父类的方法

猜你喜欢

转载自blog.csdn.net/zongza/article/details/84951240