Sonnet是基于TensorFlow的一个库,可用于方便地构建复杂的神经网络,git地址为:https://github.com/deepmind/sonnet

1.Sonnet简介

sonnet采用了面向对象,中心思想是首先构造神经网络局部的python对象,然后将这些对象独立地连接到TensorFlow的计算图中。这里的python对象就是“模块”(Module),sonnet可以用输入张量为参数构造“模块”,“模块”可以连接到计算图中并返回输出张量,同样的“模块”可以多次连接到途中且自动重用变量来透明地实现变量共享。sonnet中的“模型”(Model)可以看作一个层级结构,比如一个神经算子包含了一个控制器,这个控制器可能是一个LSTM,而这个LSTM则可以通过包含一个标准线性层的方式来实现。“模块”可以嵌套,即将其他的“模块”作为参数传入(作为子模块)来构造一个“模块”。将“模块”表示为python对象可以支持在需要的时候自定义方法。sonnet支持直接操作某个“模块”的张量嵌套群组,这样就可以方便地替换网络类型而不用大量修改代码。

使用sonnet时仍然可以直接操作原生的Tensorflow细节,比如张量和变量域(variable_scopes),用sonnet编写的模型可以和原生的TF代码或其他的高级库混合在一起。

2.Sonnet的使用

import sonnet as snt

# 输入
train_data = get_training_data()
test_data = get_test_data()

# 定义两个线性模块来构造一个三层网络
lin_to_hidden = snt.Linear(output_size=FLAGS.hidden_size, name='inp_to_hidden')
hidden_to_out = snt.Linear(output_size=FLAGS.output_size, name='hidden_to_out')

# Sequential是一个将内部模块或操作序列化地应用到提供的数据的模块,对于不包含变量的原生TF操作如tanh可以在构造的模块中替换
mlp = snt.Sequential([lin_to_hidden, tf.sigmoid, hidden_to_out])

# 将操作序列连接到计算图中(可以进行多次)
train_predictions = mlp(train_data)
test_predictions = mlp(test_data)

3.自定义模块

sonnet中的模块都继承自snt.AbstractModule,自定义的类必须能够接受任何继承下来的定义模块操作的配置并将其存储到一个前缀为“_”(下划线)的私有成员变量中。

class MyMLP(snt.AbstractModule):
	# 构造方法,模块名称必须作为最后一个参数name传入
	def __init__(self, hidden_size, output_size,nonlinearity=tf.tanh, name="my_mlp"):
		# 调用超类的构造方法,传入模块名称
		super(MyMLP, self).__init__(name=name)
		self._hidden_size = hidden_size
		self._output_size = output_size
		self._nonlinearity = nonlinearity

	# 实现_build方法,这个方法将在模块连接到计算图tf.Graph时被调
	# 输入可以是空或者一个简单的张量或者某个包含了多个张量的结构,多个张量可以作为一个元组或者命名元组来提供,并且可以嵌套。不建议采用列表或字典
	# 多数情况下输入的张量为一个批处理维度,颜色通道必须作为最后一个维度
	# _build方法包含的操作可以是:构造或使用内部模块,使用传入到构造函数中的已存在的模块,直接创建变量
	# 创建变量时使用tf.get_variable,如果直接使用tf.Variable构造函数将只在模块第一次连接时有效
	def _build(self, inputs):
		# 从输入张量计算输出张量
		lin_x_to_h = snt.Linear(output_size=self._hidden_size, name="x_to_h")
		lin_h_to_o = snt.Linear(output_size=self._output_size, name="h_to_o")
		# 这里模块分开创建,最后连接到计算图中,这里的nonlinearity可以用原生的TF操作如tf.tanh、tf.sigmoid或者一个sonnet模块实例来替换,这里不做检查,可以在初始化时自行检查或添加约束
		return lin_h_to_o(self._nonlinearity(lin_x_to_h(inputs)))

4.递归模块

sonnet包含了递归的核心模块,他们执行一个时间步计算,可以使用TensorFlow的展开操作在时间上展开,比如LSTM。

hidden_size = 5
batch_size = 20
# 输入序列应为大小的张量
lstm = snt.LSTM(hidden_size)
initial_state = lstm.initial_state(batch_size)
output_sequence, final_state = tf.nn.dynamic_rnn(
    lstm, input_sequence, initial_state=initial_state, time_major=True)

5.自定义递归模块

递归模块是snt.RNNCore的子类,具有与tf.nn.rnn_cell.RNNCell兼容的接口。除了_build方法,递归模块还需要实现state_size和output_size属性,用于提供递归状态的预期大小,

class Add1RNN(snt.RNNCore):
  # 功能为状态加1,输出为0的结果
  # 过程:(`input`, (`state1`, `state2`)) -> (`output`, (`next_state1`, `next_state2`))
  # 这里所有的元素都是张量,next_statei` = `statei` + 1,`output` = 0
  # 所有的输出(`state` and `output`)都是大小为(`batch_size`, `hidden_size`),`hidden_size`是在构造器中指定的大小
  def __init__(self, hidden_size, name="add1_rnn"):
    # 模块构造器
    super(Add1RNN, self).__init__(name=name)
    self._hidden_size = hidden_size

  def _build(self, inputs, state):
    # 构建一个TF子图,执行计算中的一个时间步
    batch_size = tf.TensorShape([inputs.get_shape()[0]])
    outputs = tf.zeros(shape=batch_size.concatenate(self.output_size))
    state1, state2 = state
    next_state = (state1 + 1, state2 + 1)
    return outputs, next_state

  @property
  def state_size(self):
    # 返回一个状态大小的描述,不包括批处理维度
    return (tf.TensorShape([self._hidden_size]),
            tf.TensorShape([self._hidden_size]))

  @property
  def output_size(self):
    # 返回一个输出大小的描述,不包括批处理维度
    return tf.TensorShape([self._hidden_size])

  def initial_state(self, batch_size, dtype):
    # 返回一个批处理大小和类型的非零初始状态(这里只是用于解释,在超类中有相关的方法处理)
    sz1, sz2 = self.state_size
    # 将批处理大小加到状态shape中,并创建零值
    return (tf.zeros([batch_size] + sz1.as_list(), dtype=dtype),
            tf.zeros([batch_size] + sz2.as_list(), dtype=dtype))

6.Transposable接口

snt.Transposable是一个支持转移的接口。“转移”意味着一个新的模块的属性可以在不严格实现变量共享的情况下关联原始模块,比如有一个snt.Linear模块将大小为A的输入映射到大小为B的输出,通过转移可以得到一个输入大小为B输出大小为A的snt.Linear,他们的权重矩阵是转置的关系。

可转移的模块需要实现一个方法transpose,返回一个这个模块的转移版本。实现一个可转移模块时需要注意确保在实例化模块时值延迟到在计算图构建时传入的参数有函数来设置。

7.变量重用

有时需要一个tf.VariableScope在多个方法中共享,这因snt.AbstractModule是无法实现的。为方法添加@snt.reuse_variable装饰器可以使得变量以类似_build()的方式可重用。一个简单的tf.VariableScope可以跨不同的装饰方法使用,每个装饰方法都有自己的reuse标识用于进入变量域内部。

class Reusable(object):

  def __init__(self, name):
    with tf.variable_scope(None, default_name=name) as vs:
      self.variable_scope = vs

  @snt.reuse_variables
  def reusable_var(self):
    return tf.get_variable("a", shape=[1])

obj = Reusable("reusable")
a1 = obj.reusable_var()
a2 = obj.reusable_var()
# a1 == a2


class NaiveAutoEncoder(snt.AbstractModule):
  def __init__(self, n_latent, n_out, name="naive_auto_encoder"):
    super(NaiveAutoEncoder, self).__init__(name=name)
    self._n_latent = n_latent
    self._n_out = n_out

  @snt.reuse_variables
  def encode(self, input):
    # 构建AutoEncoder的前半部分,inputs -> latents
    w_enc = tf.get_variable("w_enc", shape=[self._n_out, self._n_latent])
    b_enc = tf.get_variable("b_enc", shape=[self._n_latent])
    return tf.sigmoid(tf.matmul(input, w_enc) + b_enc)

  @snt.reuse_variables
  def decode(self, latents):
    # 构建AutoEncoder的后半部分,latents -> reconstruction
    w_rec = tf.get_variable("w_dec", shape=[self._n_latent, self._n_out])
    b_rec = tf.get_variable("b_dec", shape=[self._n_out])
    return tf.sigmoid(tf.matmul(latents, w_rec) + b_rec)

  def _build(self, input):
    # 构建整个AutoEncoder,input -> latents -> reconstruction
    latents = self.encode(input)
    return self.decode(latents)


batch_size = 5
n_in = 10
n_out = n_in
n_latent = 2
nae = NaiveAutoEncoder(n_latent=n_latent, n_out=n_out)
inputs = tf.placeholder(tf.float32, shape=[batch_size, n_in])
latents = tf.placeholder(tf.float32, shape=[batch_size, n_latent])

# 默认方式调用build(),产生完整的AutoEncoder
reconstructed_from_input = nae(inputs)

# 连接其他的方法可能只需要变量的某个子集,但是共享仍然有效
reconstructed_from_latent = nae.decode(latents)

这里变量使用nae.encode()、nae.decode()或nae()来创建的变量存在于同一个tf.VariableScope中。注意,在NativeAutoEncoder.encode中使用tf.get_variable("w_dec",...)是不会共享变量的,TF会将NativeAutoEncoder.encode()和NativeAutoEncoder.decode()中的看作不同的变量,在第二次使用该变量时会报错。

8.使用snt.Module向Sonnet模块包装函数

snt.Module类的构造器可调用并返回一个Sonnet模块,当一个模块被调用时,对应的函数就会调用,指定新的节点如何连接到计算图中,如何从输入张量计算输出张量。