重要概念
首先,了解一下tensorflow中的一些基础的概念:
0 tensorflow是一种符号式编程,特点是,网络结构的构建与运行完全分离。
1 会话,它是定义图的基础,建立实例sess =tf.Session()后,会生成一张空图,为了实现特定的功能,需要在空图上定义节点与边。
2 图,它是tensorlfow的一大特色,实际使用过程中,可以在图中定义不同的节点与边,完成特定的逻辑。
3 节点,节点表示运算操作,一般是进行数学运算,读取数据,矩阵操作,状态操作(参数初始化/赋值),控制张量流动的操作
4 边,将不同的节点连接在一起,可控制数据的流向,数据运行的先后次序。因此,边的连接中有两种连接关系,一种是数据依赖(实线),另外一种是控制依赖(虚线)。
5 设备,用来进行运算,并且拥有自己地址空间的硬件(GPU,CUP),tensorflow中的接口
with tf.device(’/gpu:0’): 可以指定运算在哪个硬件设备进行。
从可视化开始
为了更加直观地理解上述的概念,先从tensorboard出发,tensorboard可用于显示网络结构图(节点,边一目了然),为了完整的显示网络结构,需要做哪些准备呢?
1 tensorboard是通过summary收集各种数据信息,因此需要创建保存这些信息的句柄:
writer = tf.summary.FileWriter()
2 指定可视化的信息,比如,loss,accuracy,不同的信息需要不同的方法显示,准确率,损失这种标量类型的使用scalar(一维的标量信息),但权重,偏置信息也是通过这个接口显示:
loss_summary=tf.summary.scalar('loss',loss)
而若是想观察神经元活动情况,使用histogram(可能直方图显示的更加明了吧):
var_summary=tf.summary.histogram('fc1',fc1)
某一层神经元的活动情况,是高维的信息,高维信息使用直方图统计数据分布情况,比如BN 缓解了网络内部协方差偏移的情况,而在之前,若是想观察到这种现象,可以通过统计某一层神经元激活的分布情况,并分析得到。
3 summary收集用于所有信息,准备进行可视化。
merge_summary=tf.summary.merge(loss_summary)
很多情况,我们可以直接将所有的信息都显示出来,为了减少麻烦,可以使用:
merge_summary = tf.summary.merge_all()
4 得到所有需要可视化的信息后,也仅仅是定义了哪些需要可视化,不要忘了tensorflow中,定义与运行是分离的。因此,为了得到每个batch的loss与accuracy,需要将merge_summary也run一下,简单来说就是:
_loss,_,summary_res=sess.run([lstm_model.loss,lstm_model.train_op,merge_summary],
feed_dict={
data_placeholder: images,
label_placeholder:labels
})
5 最后,很重要的一点就是,不要忘了将我们辛苦得到的信息保存到summary中:
writer.add_summary(summary_res)
注:为了确保上述的这些events都被保存到磁盘,可以使用:
writer.flush()
6 操作结束后,还应该关闭文件操作符(有始有终)
writer.close()
7 用一张图来总结:
不过,实际使用中,虽然我们将loss,accuracy之类的信息保存下来并进行可视化操作,但当点击Graphs面板那一栏时,结果可能并不是预期的那样,或许你都无法想象这是我们构建的网络,甚至怀疑,是不是网络搭建错了,如下图所示:
事实上,造成这种现象的原因,可能是我们忽略了一些细节:在于我们在定义节点的时候,没有给节点定义范围。通常给节点定义范围,我们会通过:
with tf.name_scope('name'):
在该范围内定义的所有节点操作都会被合并到一起,若是想查看细节信息,点击“+”即可。
除了name_scope,另外一种:
with tf.variable_scope('name'):
两者不同在于,下面一种是针对变量而言的,上面一种是针对节点而言的。并且即使将变量写在name_scope的作用范围内,也不会影响到变量的name,而variable_scope就不同了,它会直接在变量的名称之前将作用域的名称添加上,使得在同一个作用域下面的变量都有相同的前缀,从这个角度来看,不是就是对变量进行分类嘛。
值得一提的是,假如在同一个变量作用域范围内,我们写了很多个变量,为了不一一指定变量初始化的方式,可以将初始化方式写在作用域上,但通常对权重和偏置的初始化方式不一样,这时候,我们只需要在偏置的时候重新指定初始化方式即可,对于不指定的,默认是tf.variable_scope中定义的方式:
with tf.variable_scope('name',initializer = tf.constant_initializer(0.1)):
来个栗子
以手写数字识别为例,这里使用循环神经网络实现分类任务,网络结构非常简单,这里并非想要获得更高的识别率,仅仅是想通过一个简单的栗子,来说明tensorflow中的一些基础的概念。
为了更加直观地了解神经网络,还是先将利用tensorboard 可视化的网络拿出来(使用name_scope后):
从上图中可以很明显地看出节点与边的概念,节点与边构成了图。为了方便说明,我们标记出了两个红色矩形框,左边的是参数节点(param),右边的是网络节点(model),灰色的箭头表示张量流动的方向,也就是当前应该流向的目的节点,黄色箭头指向模型中应该要保存的参数。
现在来解释一下模型结构与参数:
本实验模型使用的是两层双向LSTM的结构,
模型代码为:
with tf.name_scope('lstm_layer'):
fw_cell = tf.nn.rnn_cell.DropoutWrapper(tf.nn.rnn_cell.MultiRNNCell(
[tf.nn.rnn_cell.BasicLSTMCell(self.hidden_size, dtype=tf.float32) for _ in range(self.layer_nums)]),
input_keep_prob=self.keep_prob, output_keep_prob=self.keep_prob)
bw_cell = tf.nn.rnn_cell.DropoutWrapper(tf.nn.rnn_cell.MultiRNNCell(
[tf.nn.rnn_cell.BasicLSTMCell(self.hidden_size, dtype=tf.float32) for _ in range(self.layer_nums)]),
input_keep_prob=self.keep_prob, output_keep_prob=self.keep_prob)
outputs, state = tf.nn.bidirectional_dynamic_rnn(
fw_cell, bw_cell,
inputs=input_x,
dtype=tf.float32
)
merged_output = outputs[0] + outputs[1] # [N,T,hidden_size] --> [N,28,20]
with tf.name_scope('fc_layer'):
with tf.variable_scope('fc', initializer=tf.contrib.layers.xavier_initializer()):
w1 = tf.get_variable('w1', shape=[self.hidden_size, args.class_nums])
b1 = tf.get_variable('b1', shape=[args.class_nums], initializer=tf.zeros_initializer())
output = tf.matmul(merged_output, w1) + b1 # [N,28,10]
output = tf.reduce_mean(output, axis=1) # [N,10]
self.logits = tf.nn.softmax(output)
由于MNIST数据集中每个样本的大小为:28*28 ,训练网络时,lstm的第t个时刻,输入图片的第t行数据。这里从参数地角度简单解释一下lstm的内部结构,为了更好地说明,可以我们将模型的参数都打印出来:
其中,fw_cell和bw_cell 的shape 都是 [48,80]。但是我们并没有显示地定义这个kernel啊,那这几个kernel从何而来呢?
在构建模型时,我们定义lstm 内部cell个数为20(当然也可以定义为其他),由于lstm内部包含了三个门(输入门,遗忘门,输出门)和一个状态(20个cell表示),网络默认定义4个权重,shape都是[48,20]。但是,每个时刻输入的不是28维的向量吗?为什么会变成48呢?这是因为lstm不仅包含了来自外界的输入,还包含了来自上一个时刻的输入(可以理解为旧记忆),在旧记忆的基础上,不断接受外界的信息,并更新为新的记忆(我们的记忆可能也是也是这样吧)。
最终,tf.nn.bidirectional_dynamic_rnn的输出包含两个部分:output以及state,shape为[batch_size,T,hidden_size]。由于使用的是双向lstm,outputs 是一个元组,outputs[0]是前向的输出,outputs[1]是后向的输出。而这里,我们将前向与后向的输出相加,并沿着时间维度加权平均,得到shape为[batch_size,hidden_size]的feature。OK,下面就是我们熟知的分类层了,这里就不介绍了。
总结
首先,本文引入tensorflow的基本概念:会话,图,节点,边。其次,以可视化工具tensorboard更加直观地了解tensorflow中基本概念。然后,使用了一个引入手写数字识别任务,但这里并没有采用DNN或是CNN的结构,而是采用了RNN,并以lstm为基础,总结了一些在使用lstm中可能遇到的一些问题,或者是难以理解的概念。
参考文献
tensorflow技术解析与实战