文章目录
- 1 原始API
- 1.1 卷积层
- 1.1.1 命名空间与变量名
- 1.1.2 权重变量的定义
- 1.1.3 偏置项变量的定义
- 1.1.4 卷积操作的定义
- 1.1.5 加偏置操作的定义
- 1.1.6 激活操作的定义
- 1.2 池化层
- 1.3 完整样例
- 1.3.1 完整样例1
- 1.3.2 完整样例2
- 2 TensorFlow-Slim API(推荐使用)
- 2.1 slim.conv2d()
- 2.2 slim.max_pool2d()
- 2.3 slim.fully_connected()
- 2.4 slim.arg_scope()
- 2.5 slim.repeat()
- 2.6 slim.stack()
1 原始API
1.1 卷积层
搭建一个卷积层的完整结构为:
with tf.variable_scope('layer1_conv1'):
# 定义卷积层参数
conv1_weights = tf.get_variable("weights", [5, 5, 3, 32], initializer=tf.truncated_normal_initializer(stddev=0.1))
conv1_biases = tf.get_variable("bias", [32], initializer=tf.constant_initializer(0.0))
# 卷积层前向传播
# input_tensor 是当前卷积层的输入张量
conv1 = tf.nn.conv2d(input_tensor, conv1_weights, strides=[1, 2, 2, 1], padding='SAME')
relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))
1.1.1 命名空间与变量名
-
tf.variable_scope()
用来提供变量的命名空间 -
tf.get_variable()
用来创建卷积核的权重变量和偏置项变量
这两个函数搭配使用,有一个很重要的好处:通过使用不同的命名空间来隔离不同层的变量,这可以让每一层中的变量命名只需要考虑在当前层的作用,而不需要担心重名的问题。
直接看例子:
import tensorflow as tf
with tf.variable_scope("layer1"):
layer1_weights = tf.get_variable("weights", [2, 3], initializer=tf.truncated_normal_initializer(stddev=0.1))
with tf.variable_scope("layer2"):
layer2_weights = tf.get_variable("weights", [2, 3], initializer=tf.truncated_normal_initializer(stddev=0.1))
print(layer1_weights.name)
print(layer2_weights.name)
输出结果:
layer1/weights:0
layer2/weights:0
分析:虽然在定义layer1_weights和layer2_weights时,给两者都赋予名字"weights",但它们属于不同的命名空间,所以最终的名字是:“命名空间/weights:0”。
1.1.2 权重变量的定义
权重变量的定义代码如下:
conv1_weights = tf.get_variable("weights", [5, 5, 3, 32], initializer=tf.truncated_normal_initializer(stddev=0.1))
第一个参数是名字,第三个参数是权重变量的初始化方法,最重要的是第二个参数,即权重变量的shape。
卷积层的权重变量是一个四维张量:
- 前两个维度是卷积核的尺寸(上述代码的卷积核尺寸为5*5)
- 第三个维度是当前层的深度,这个维度取决于当前卷积层输入张量的深度(上述代码的输入张量深度为3)
- 第四个维度是卷积核的深度,这个维度决定了当前卷积层输出张量的深度(上述代码的输出张量深度为32),一般来说就是下一层输入张量的深度。
1.1.3 偏置项变量的定义
偏置项变量的定义代码如下:
conv1_biases = tf.get_variable("bias", [32], initializer=tf.constant_initializer(0.0))
第一个参数是名字,第三个参数是 偏置项变量的初始化方法,最重要的是第二个参数,即 偏置项变量的shape。
卷积层的偏置项变量是一个一维张量,该维度表示卷积核的深度(与权重变量的第四个维度相同,因为卷积层不同位置的偏置项是共享的)
1.1.4 卷积操作的定义
卷积操作的定义代码如下:
conv1 = tf.nn.conv2d(input_tensor, conv1_weights, strides=[1, 2, 2, 1], padding='SAME')
- input_tensor 是当前卷积层的输入张量,它是一个四维张量,第一个维度对应一个输入batch,其它三个维度对应一个节点矩阵。比如在输入层,input_tensor [0, :, :, :]表示第一张图片;input_tensor [1, :, :, :]表示第二张图片。
- conv1_weights 是当前卷积层的权重变量
- strides 在不同维度上的卷积步长,虽然该参数提供的是一个四维数组,但是第一个维度和第四个维度要求一定是1。这是因为卷积层的步长只对矩阵的长和宽有效(只对input_tensor的第二个维度和第三个维度有效)
- padding 填充方法。'SAME’为全0填充,'VALID’表示不填充。
1.1.5 加偏置操作的定义
加偏置操作的定义代码如下:
tf.nn.bias_add(conv1, conv1_biases)
注意:卷积层的加偏置操作与全连接层的加偏置操作是不同的。
- 全连接层的偏置项数目与当前层的节点数是相同的,所以可以直接相加(+)。
- 卷积层的矩阵是多维的,矩阵上不同位置上的节点都需要加上同样的偏置,而偏置项是一维的,所以不能直接相加,只能使用函数
tf.nn.bias_add()
1.1.6 激活操作的定义
使用ReLU激活函数进行激活操作的定义代码如下:
relu1 = tf.nn.relu()
1.2 池化层
搭建一个最大池化层的完整结构为:
with tf.name_scope('layer2_pool1'):
pool1 = tf.nn.max_pool(relu1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
由于池化层没有用于训练的参数变量(只有超参数,如池化核尺寸、池化步长、是否全0填充)tf.nn.max_pool()
的第一个参数是池化层输入张量,其格式与tf.nn.conv2d()
的第一个参数相同。
- ksize 是池化核尺寸,第一维和第四维要求一定为1,第二维和第三维才是池化核真正的尺寸。
- strides 是池化步长,第一维和第四维要求一定为1,第二维和第三维是池化核真正的步长。
1.3 完整样例
一般的网络结构由若干个卷积-池化层后接若干个全连接层组成,所以典型的网络结构完整样例为:
1.3.1 完整样例1
import tensorflow as tf
def inference(input_tensor, train, regularizer):
"""
定义神经网络参数、结构和前向传播过程
"""
with tf.variable_scope('layer1_conv1'):
conv1_weights = tf.get_variable("weights", [5, 5, 1, 32], initializer=tf.truncated_normal_initializer(stddev=0.1))
conv1_biases = tf.get_variable("bias", [32], initializer=tf.constant_initializer(0.0))
# 使用边长为5,深度为32的过滤器,移动步长为1,使用全0填充
conv1 = tf.nn.conv2d(input_tensor, conv1_weights, strides=[1, 1, 1, 1], padding='SAME')
relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))
with tf.name_scope('layer2_pool1'):
# 使用2*2过滤器,步长为2
pool1 = tf.nn.max_pool(relu1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
with tf.variable_scope('layer3_conv2'):
conv2_weights = tf.get_variable(
"weights", [5, 5, 32, 64],
initializer=tf.truncated_normal_initializer(stddev=0.1))
conv2_biases = tf.get_variable("bias", [64], initializer=tf.constant_initializer(0.0))
# 使用边长为5,深度为64的过滤器,移动步长为1,使用全0填充
conv2 = tf.nn.conv2d(pool1, conv2_weights, strides=[1, 1, 1, 1], padding='SAME')
relu2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_biases))
with tf.name_scope('layer4_pool2'):
# 使用2*2过滤器,步长为2
pool2 = tf.nn.max_pool(relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# 数据维度转换
pool_shape = pool2.get_shape().as_list()
nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
reshaped = tf.reshape(pool2, [pool_shape[0], nodes])
with tf.variable_scope('layer5_fc1'):
fc1_weights = tf.get_variable("weights", [nodes, 512],
initializer=tf.truncated_normal_initializer(stddev=0.1))
if regularizer:
tf.add_to_collection('losses', regularizer(fc1_weights))
fc1_biases = tf.get_variable("bias", [512], initializer=tf.constant_initializer(0.0))
fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_weights)+fc1_biases)
if train:
fc1 = tf.nn.dropout(fc1, 0.5)
with tf.variable_scope('layer6_fc2'):
fc2_weights = tf.get_variable("weights", [512, 10],
initializer=tf.truncated_normal_initializer(stddev=0.1))
if regularizer:
tf.add_to_collection('losses', regularizer(fc2_weights))
fc2_biases = tf.get_variable("bias", [10], initializer=tf.constant_initializer(0.0))
logit = tf.matmul(fc1, fc2_weights)+fc2_biases
return logit
1.3.2 完整样例2
完整样例1存在一个问题:当卷积层、池化层和全连接层较多时,代码会显得非常冗余。解决方法是定义卷积层函数、池化层函数、全连接层函数,然后调用这些函数来搭建神经网络,这样就得到了完整样例2。
import tensorflow as tf
def create_conv(input_tensor, conv_size, conv_stride, padding, layer_name):
"""
搭建卷积层
:param input_tensor: 输入张量,shape = [batch_size, height, width, channels]
:param conv_size: 卷积核尺寸和深度,eg: conv_size = [3, 3,64]
:param conv_stride: 卷积步长,eg: conv_stride = [2, 2]
:param padding: 填充方式,只有两种填充方式: padding = 'SAME' or padding = 'VALID'
:param layer_name: 卷积层命名空间的名字,str
:return: layer_output: 卷积层输出张量
"""
with tf.variable_scope(layer_name):
input_shape = input_tensor.get_shape().as_list()
weights = tf.get_variable("weights", [conv_size[0], conv_size[1], input_shape[3], conv_size[2]],
initializer=tf.truncated_normal_initializer(stddev=0.1))
biases = tf.get_variable("bias", [conv_size[2]], initializer=tf.constant_initializer(0.0))
conv = tf.nn.conv2d(input_tensor, weights, strides=[1, conv_stride[0], conv_stride[1], 1], padding=padding)
layer_output = tf.nn.relu(tf.nn.bias_add(conv, biases))
return layer_output
def create_pool(input_tensor, pool_size, pool_stride, pool_type, padding, layer_name):
"""
搭建池化层
:param input_tensor: 输入张量,shape = [batch_size, height, width, channels]
:param pool_size: 池化核尺寸,eg: pool_size = [2, 2]
:param pool_stride: 池化步长,eg: pool_stride = [2, 2]
:param pool_stride: 池化步长,eg: pool_stride = [2, 2]
:param pool_type: 池化类型,只有两种池化方式: padding = 'MAX' or padding = 'VALID'
:param padding: 填充方式,只有两种填充方式: padding = 'SAME' or padding = 'VALID'
:param layer_name: 池化层命名空间的名字,str
:return: layer_output: 池化层输出张量
"""
with tf.name_scope(layer_name):
if pool_type == 'MAX':
layer_output = tf.nn.max_pool(input_tensor, ksize=[1, pool_size[0], pool_size[1], 1],
strides=[1, pool_stride[0], pool_stride[1], 1], padding=padding)
else:
layer_output = tf.nn.avg_pool(input_tensor, ksize=[1, pool_size[0], pool_size[1], 1],
strides=[1, pool_stride[0], pool_stride[1], 1], padding=padding)
return layer_output
def create_fc(input_tensor, num_output, train, activate_function, regularizer, layer_name):
"""
搭建全连接层
:param input_tensor: 输入张量,shape = [batch_size, num_input]
:param num_output: 当前全连接层的节点数
:param train: 是否用于训练,bool
:param activate_function: 激活函数
:param regularizer: 正则化
:param layer_name: 池化层命名空间的名字,str
:return: layer_output: 池化层输出张量
"""
with tf.variable_scope(layer_name):
input_shape = input_tensor.get_shape().as_list()
weights = tf.get_variable("weights", [input_shape[1], num_output],
initializer=tf.truncated_normal_initializer(stddev=0.1))
if regularizer:
tf.add_to_collection('losses', regularizer(weights))
biases = tf.get_variable("bias", [num_output], initializer=tf.constant_initializer(0.0))
if activate_function:
fc = tf.nn.relu(tf.matmul(input_tensor, weights)+biases)
else:
fc = tf.matmul(input_tensor, weights) + biases
if train:
fc = tf.nn.dropout(fc, 0.5)
layer_output = fc
return layer_output
def inference(input_tensor, train, regularizer):
"""
定义神经网络参数、结构和前向传播过程
"""
conv1 = create_conv(input_tensor, [5, 5, 32], [1, 1], 'SAME', 'layer1_conv1')
pool1 = create_pool(conv1, [2, 2], [2, 2], 'MAX', 'SAME', 'layer2_pool1')
conv2 = create_conv(pool1, [5, 5, 64], [1, 1], 'SAME', 'layer3_conv2')
pool2 = create_pool(conv2, [2, 2], [2, 2], 'MAX', 'SAME', 'layer4_pool2')
# 数据维度转换
reshaped_pool2 = tf.layers.flatten(pool2)
fc1 = create_fc(reshaped_pool2, 512, train, "relu", regularizer, 'layer5_fc1')
logit = create_fc(fc1, 10, train, None, regularizer, 'layer6_fc2')
return logit
实际上,该封装方式与下面要讲的slim库是非常相似的。
2 TensorFlow-Slim API(推荐使用)
关于TensorFlow-Slim API,有一篇博客写得很详细: TensorFlow-Slim API 官方教程 * * * * *.
我们这里主要讲解使用TensorFlow-Slim来搭建神经网络的结构。
首先给出一个VGG16的TF-Slim实现版本,让大家感受一下使用TF-Slim库来搭建神经网络结构的便捷性:
def vgg16(inputs):
with slim.arg_scope([slim.conv2d, slim.fully_connected],
activation_fn=tf.nn.relu,
weights_initializer=tf.truncated_normal_initializer(0.0, 0.01),
weights_regularizer=slim.l2_regularizer(0.0005)):
net = slim.repeat(inputs, 2, slim.conv2d, 64, [3, 3], scope='conv1')
net = slim.max_pool2d(net, [2, 2], scope='pool1')
net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3], scope='conv2')
net = slim.max_pool2d(net, [2, 2], scope='pool2')
net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], scope='conv3')
net = slim.max_pool2d(net, [2, 2], scope='pool3')
net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv4')
net = slim.max_pool2d(net, [2, 2], scope='pool4')
net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv5')
net = slim.max_pool2d(net, [2, 2], scope='pool5')
net = slim.fully_connected(net, 4096, scope='fc6')
net = slim.dropout(net, 0.5, scope='dropout6')
net = slim.fully_connected(net, 4096, scope='fc7')
net = slim.dropout(net, 0.5, scope='dropout7')
net = slim.fully_connected(net, 1000, activation_fn=None, scope='fc8')
return net
2.1 slim.conv2d()
slim.conv2d()
用于搭建卷积层,其函数定义如下:
slim.conv2d(inputs,
num_outputs,
kernel_size,
stride=1,
padding='SAME',
data_format=None,
rate=1,
activation_fn=nn.relu,
normalizer_fn=None,
normalizer_params=None,
weights_initializer=initializers.xavier_initializer(),
weights_regularizer=None,
biases_initializer=init_ops.zeros_initializer(),
biases_regularizer=None,
reuse=None,
variables_collections=None,
outputs_collections=None,
trainable=True,
scope=None)
- inputs : 指需要做卷积的输入图像
- num_outputs : 指定卷积核的个数(就是filter的- 个数)
- kernel_size : 用于指定卷积核的维度(卷积核的宽度,卷积核的高度)
- stride : 为卷积时在图像每一维的步长
- padding : 为padding的方式选择,VALID或者SAME
- data_format : 是用于指定输入输出张量的形状,默认为None,即“NHWC”,即[batch_size, height, width, channels];还有另一种为“NCHW”,即[batch_size, channels, height, width]。
- rate : 对于使用空洞卷积的膨胀率,rate等于1为普通卷积,rate=n代表卷积核中两两数之间插入了n-1个0
- activation_fn : 用于激活函数的指定,默认的为ReLU函数
- normalizer_fn : 用于指定正则化函数
- normalizer_params : 用于指定正则化函数的参数
- weights_initializer : 用于指定权重的初始化程序
- weights_regularizer : 为权重可选的正则化程序
- biases_initializer : 用于指定biase的初始化程序
- biases_regularizer : biases可选的正则化程序
- reuse : 指定是否共享层或者和变量
- variable_collections 指定所有变量的集合列表或者字典
- outputs_collections : 指定输出被添加的集合
- trainable : 卷积层的参数是否可被训练
- scope : 共享变量所指的variable_scop
2.2 slim.max_pool2d()
slim.max_pool2d()
用于搭建最大池化层,其函数定义如下:
slim.max_pool2d(inputs,
kernel_size,
stride=2,
padding='VALID',
data_format=DATA_FORMAT_NHWC,
outputs_collections=None,
scope=None)
slim.max_pool2d()
各参数与slim.conv2d()
均相同。
2.3 slim.fully_connected()
slim.fully_connected()
用于搭建全连接层,其函数定义如下:
slim.fully_connected(inputs,
num_outputs,
activation_fn=nn.relu,
normalizer_fn=None,
normalizer_params=None,
weights_initializer=initializers.xavier_initializer(),
weights_regularizer=None,
biases_initializer=init_ops.zeros_initializer(),
biases_regularizer=None,
reuse=None,
variables_collections=None,
outputs_collections=None,
trainable=True,
scope=None):
- inputs: A tensor of at least rank 2 and static value for the last dimension;
i.e.[batch_size, depth]
,[None, None, None, channels]
. - num_outputs: Integer or long, the number of output units in the layer.
- 其他参数与
slim.conv2d()
一样。
Note: that if inputs
have a rank greater than 2, then inputs
is flattened prior to the initial matrix multiply by weights
.
所以在第2节最开始的VGG16结构中,pool5的输出直接作为fc6的输入,因为slim.fully_connected()
会自动flatten。
2.4 slim.arg_scope()
有了以上3个函数,你完全可以搭建任何你想要的神经网络,只是有一个问题:这3个函数的参数非常之多,并且这3个函数有很多相同的参数,这会造成代码冗余、代码的可读性和维护性差,不信你看:
net = slim.conv2d(inputs, 64, [11, 11], 4, padding='SAME',
weights_initializer=tf.truncated_normal_initializer(stddev=0.01),
weights_regularizer=slim.l2_regularizer(0.0005), scope='conv1')
net = slim.conv2d(net, 128, [11, 11], padding='VALID',
weights_initializer=tf.truncated_normal_initializer(stddev=0.01),
weights_regularizer=slim.l2_regularizer(0.0005), scope='conv2')
net = slim.conv2d(net, 256, [11, 11], padding='SAME',
weights_initializer=tf.truncated_normal_initializer(stddev=0.01),
weights_regularizer=slim.l2_regularizer(0.0005), scope='conv3')
很明显,这三个卷积层共享很多相同的超参数。两个有相同的 padding
,三个都有相同的 weights_initializer
和 weight_regularizer
。这段代码很难读,并且包含了很多重复的值。一个解决方案是使用变量指定默认值:
padding = 'SAME'
initializer = tf.truncated_normal_initializer(stddev=0.01)
regularizer = slim.l2_regularizer(0.0005)
net = slim.conv2d(inputs, 64, [11, 11], 4,
padding=padding,
weights_initializer=initializer,
weights_regularizer=regularizer,
scope='conv1')
net = slim.conv2d(net, 128, [11, 11],
padding='VALID',
weights_initializer=initializer,
weights_regularizer=regularizer,
scope='conv2')
net = slim.conv2d(net, 256, [11, 11],
padding=padding,
weights_initializer=initializer,
weights_regularizer=regularizer,
scope='conv3')
这个解决方案保证了三个卷积层拥有相同的参数值,但代码仍不够清晰。通过使用一个arg_scope
,我们能够在保证每一层使用相同参数值的同时,简化代码:
with slim.arg_scope([slim.conv2d], padding='SAME',
weights_initializer=tf.truncated_normal_initializer(stddev=0.01)
weights_regularizer=slim.l2_regularizer(0.0005)):
net = slim.conv2d(inputs, 64, [11, 11], scope='conv1')
net = slim.conv2d(net, 128, [11, 11], padding='VALID', scope='conv2')
net = slim.conv2d(net, 256, [11, 11], scope='conv3')
如上例所示,使用arg_scope
使代码更清晰、简单并且容易去维护。arg_scope
允许用户给一个或多个 op 指定一套默认参数,这些默认参数将被传给arg_scope
里使用的的每一个 op。
注意,在arg_scope
内部指定op的参数值时,指定的参数将取代默认参数。具体来讲,上例中,当 padding 参数的默认值被设置为 ‘SAME’ 时,第二个卷积的 padding 参数被指定为 ‘VALID’。
2.5 slim.repeat()
允许用户重复地进行(perform)相同的操作(operation)。例如,考虑下面的代码段(来自 VGG 网络,它的 layers 在两个 pooling 层之间进行了很多 conv):
net = ...
net = slim.conv2d(net, 256, [3, 3], scope='conv3_1')
net = slim.conv2d(net, 256, [3, 3], scope='conv3_2')
net = slim.conv2d(net, 256, [3, 3], scope='conv3_3')
net = slim.max_pool2d(net, [2, 2], scope='pool2')
一个减少代码重复的方法是使用 for 循环:
net = ...
for i in range(3):
net = slim.conv2d(net, 256, [3, 3], scope='conv3_%d' % (i+1))
net = slim.max_pool2d(net, [2, 2], scope='pool2')
使用slim.repeat()
可以使上面的代码变得更清晰明了:
net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], scope='conv3')
net = slim.max_pool2d(net, [2, 2], scope='pool2')
注意:slim.repeat 不仅对 repeated 单元采用相同的参数,而且它对 repeated 单元的 scope 采用更好的命名方式(加下划线,再加迭代序号)。具体来说,上面例子中的 scopes 将会命名为 ‘conv3/conv3_1’,‘conv3/conv3_2’,‘conv3/conv3_3’
2.6 slim.stack()
更进一步,Slim 的slim.stack
允许去重复多个操作 with 不同的参数,从而创建一个多层的堆叠结构。slim.stack
也为每一个创建的 op 创造了一个新的 tf.variable_scope
。例如,创建一个多层感知器(Multi-Layer Perceptron (MLP))的一个简单方式:
# Verbose way: 冗长的方式
x = slim.fully_connected(x, 32, scope='fc/fc_1')
x = slim.fully_connected(x, 64, scope='fc/fc_2')
x = slim.fully_connected(x, 128, scope='fc/fc_3')
# Equivalent, TF-Slim way using slim.stack:
x = slim.stack(x, slim.fully_connected, [32, 64, 128], scope='fc')
在这个例子中,slim.stack
调用slim.fully_connected
三次,并将函数上一次调用的输出传递给下一次调用。但是,在每个调用中,隐形单元(hidden units)的数量分别为 32,64,128。相似地,我们可以使用 stack 去简化多层卷积的堆叠:
# Verbose way: 冗长的方式
x = slim.conv2d(x, 32, [3, 3], scope='core/core_1')
x = slim.conv2d(x, 32, [1, 1], scope='core/core_2')
x = slim.conv2d(x, 64, [3, 3], scope='core/core_3')
x = slim.conv2d(x, 64, [1, 1], scope='core/core_4')
# Using stack:
x = slim.stack(x, slim.conv2d, [(32, [3, 3]), (32, [1, 1]), (64, [3, 3]), (64, [1, 1])], scope='core')
参考资料:
[1]: TensorFlow实战Google深度学习框架(第2版)
[2]: TensorFlow-Slim API 官方教程 * * * * *.