TF-slim 模块是TensorFLow中比较实用的API之一,是一个用于模型构建、训练、评估复杂模型的轻量化库。 其中引入的比较实用的函数包含arg_scope、model_variables、repeat、stack。
slim 模块是在16年推出的,其主要功能是为了实现"代码瘦身"。
该模块已经成为很常用的模块之一,在github上大部分TensorFLow的代码中都会涉及到它,如果没有涉及到,其网络架构的实现可能会存在很多冗余,代码不够简练,可读性较低。
引言
首先,来看一下运用slim模块实现LeNet-5网络架构的代码:
1 def lenet_architecture(self, is_trained=True): 2 with slim.arg_scope([slim.conv2d], padding="valid", 3 weights_initializer=tf.truncated_normal_initializer(stddev=0.01), 4 weights_regularizer=slim.l2_regularizer(0.005)): 5 # 由于Lenet中是32*32的输入,而MNIST是28*28的图片,所以第一层卷积需要使用SAME卷积 6 net = slim.conv2d(self.input_image, 6, [5, 5], 1, padding="SAME", scope="conv1") # 28*28*5 7 net = slim.max_pool2d(net, [2, 2], 2, scope='pool_2') # 14*14*6 8 net = slim.conv2d(net, 16, [5, 5], 1, scope='conv3') # 10*10*16 9 net = slim.max_pool2d(net, [2, 2], 2, scope='pool_4') # 5*5*16 10 net = slim.conv2d(net, 120, [1, 1], 1, scope='conv5') # 通过1*1的方式代替全连接 11 net = slim.flatten(net, scope='flatten') # 展平 12 net = slim.fully_connected(net, 84, scope='fc6') 13 net = slim.dropout(net, self.dropout, is_training=is_trained, scope='dropout') 14 digits = slim.fully_connected(net, 10, scope='fc7') 15 return digits
在上述代码第6-14行,为LeNet网络在处理MNIST手写体识别时的网络实现。可以看出,每一行即为一层网络的实现。 尤其是每一层的卷积操作,并没有按照先生成卷积核,再进行卷积操作,再添加正则化的操作进行实现。 而是很干练,一行直接包含了所有的内容。这都是源于arg_scope函数内允许用户对scope内的操作定义默认参数,从而可以减少很多冗余的操作。
可以初步的感受到,slim模块可以使模型的构建、训练评估变得更简单。尤其是机器视觉领域的很多模型(LeNet-5, AlexNet, VGG等)。
闲言少絮不用讲,开始揭开slim神秘的面纱。
slim 模块的基本使用
Slim模块的导入
1 import tensorflow.contrib.slim as slim
本文使用的环境是Python3.6,TensorFlow 1.12.0
如果您的Python或者TF版本过高,可能会出现slim.没有联想输入 或者 会报 ModuleNotFound Error: No module named 'tensorflow.contrib'的错误。
使用slim构建模型详解
slim 变量(Variables)
模型的建立需要生成变量,首先来对比一下TensorFlow原生的变量生成方式和slim变量生成方式的区别。
原生的TensorFlow中创建变量的Variable函数中,需要设置预定义的值或者一个初始化的机制(随机生成之类),其使用如下:
1 W = tf.Variable(tf.truncated_normal([10, 4], 0, 1), trainable=True, 2 name="weight", dtype=tf.float32)
在slim中创建变量的variable函数中,提供了一系列wrapper函数。直观上看,slim的变量生成函数将参数的设置都扁平化了,而且更加容易理解,例如生成一个变量,名字是什么,大小如何,使用什么方式进行初始化,使用什么方式进行正则化,存放在哪里等等。 除了扁平化的使用方式外,其还添加了一些额外的功能,像正则化、存放的设备等。
1 w = slim.variable('weight', shape=[10, 10, 3, 3], 2 initializer=tf.truncated_normal_initializer(stddev=0.1), 3 regularizer=slim.l2_regularizer(0.5), 4 device='/CPU:0')
在slim中,同样也对变量进行了进一步的区分,将变量定义为局部变量和模型变量。顾名思义,模型变量是在训练过程中需要训练,进行微调的,并且在模型保存时会保存到.ckpt中,并用于推理过程的变量(Model variables are trained or fine-tuned during learning and are loaded from a checkpoint during evaluation or inference)。而局部变量只是训练过程所使用的一些参数,不需要微调,也不会保存到模型中,当然,推理的过程也不需要使用的变量(诸如迭代次数、学习率等参数)。具体使用时,如下所示:
1 # Model Variables 模型变量 使用model_variable() 2 weights = slim.model_variable('weights', 3 shape=[10, 10, 3 , 3], 4 initializer=tf.truncated_normal_initializer(stddev=0.1), 5 regularizer=slim.l2_regularizer(0.05), 6 device='/CPU:0') 7 model_variables = slim.get_model_variables() 8 9 # Regular variables # 局部变量,使用variable() 10 var = slim.variable('var', 11 shape=[20, 1], 12 initializer=tf.zeros_initializer()) 13 regular_variables_and_model_variables = slim.get_variables()
slim 层(Layers)
正如开篇LeNet示例所述,通过TensorFlow基础函数建立一个卷积层必不可少的op包括:
- 创建当前层卷积核和偏置变量
- 通过卷积核对输入进行卷积操作
- 卷积结果添加偏置
- 对结果添加激活函数
其每一层的建立将会冗余成如下模样:
1 # conv1 2 with tf.name_scope('conv1') as scope: 3 kernel = tf.Variable(tf.truncated_normal([11, 11, 3, 96], dtype=tf.float32, 4 stddev=1e-1), name='weights' 5 biases = tf.Variable(tf.constant(0.0, shape=[96], dtype=tf.float32), 6 trainable=True, name='biases') 7 conv = tf.nn.conv2d(x, kernel, [1, 4, 4, 1], padding='SAME') 8 bias = tf.nn.bias_add(conv, biases) 9 conv1 = tf.nn.relu(bias, name=scope)
其中,3-6行是卷积核和偏置的初始化,7、8、9分别是卷积、偏置、激活函数的操作。对于深度、宽度都比较少的模型网络(诸如LeNet),该操作还可行。但对于模型层数深或者宽度深的模型网络(诸如Inception、ResNet等), 如果采用上述编写方式,书写繁琐,也不便于维护。
为了避免代码的重复,slim提供了比较高级的Layers op,如下所示,slim版本的卷积操作。当然,该操作需要配合arg_scope()函数进行默认参数的设置,才会发挥其功效。
1 net = slim.conv2d(input, 128, [3, 3], scope='conv1_1')
slim.arg_scope(), 对指定的函数设置默认参数,当然,如果其中有一两个不符合默认参数的设置,可以在指定函数中使用关键字参数进行修改。将会在slim的作用域中详细进行介绍。
1 with slim.arg_scope([slim.conv2d], padding="valid", 2 weights_initializer=tf.truncated_normal_initializer(stddev=0.01), 3 weights_regularizer=slim.l2_regularizer(0.005)):
另外,slim还提供了两个meta-operations:repeat和stack,用于重复进行一些相同的操作。其应用场景为像VGG这种几个卷积操作的堆叠后进行一个池化的模型网络。如下述所示:
1 net = slim.conv2d(net, 256, [3, 3], scope='conv3_1') 2 net = slim.conv2d(net, 256, [3, 3], scope='conv3_2') 3 net = slim.conv2d(net, 256, [3, 3], scope='conv3_3') 4 net = slim.max_pool2d(net, [2, 2], scope='pool2')
其中,包含3个卷积操作。这3个卷积操作可以按照如上的方式进行编写。也可以通过循环的方式进行:
1 for i in range(3): 2 net = slim.conv2d(net, 256, [3, 3], scope='conv3_%d' % (i+1)) 3 net = slim.max_pool2d(net, [2, 2], scope='pool2')
还可以使用slim中提供的repeat方法,可以使代码更加简明:
1 net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], scope='conv3') 2 net = slim.max_pool2d(net, [2, 2], scope='pool2')
在repeat的过程中,会将scope的名称依次命名为conv3_1,conv3_2,conv3_3。repeat函数允许重复参数相同的操作。
另外,slim中的stack方法允许操作不同参数的重复操作,好比上述卷积操作为卷积核大小、通道数量不一样的卷积操作,或者是多个全连接网络(一般每层神经元节点的个数都是不一样的)。
1 # 全连接操作 之 冗长的方式 2 x = slim.fully_connected(x, 4096, scope='fc_1') 3 x = slim.fully_connected(x, 4096, scope='fc_2') 4 x = slim.fully_connected(x, 1000, scope='fc_3')
可以看出,全连接操作中神经元节点个数不相同。stack的方式如下所示:
1 x = slim.stack(x, slim.fully_connected, [32, 64, 128], scope='fc')
将不同的参数写成一个列表即可,stack操作不标注堆叠的次数,因为每次参数不一样。
除了全连接操作,实际上stack也可以处理卷积操作,对于下述3*3、 1*1的卷积操作:
1 x = slim.conv2d(x, 32, [3, 3], scope='conv_1') 2 x = slim.conv2d(x, 32, [1, 1], scope='conv_2') 3 x = slim.conv2d(x, 64, [3, 3], scope='conv_3') 4 x = slim.conv2d(x, 64, [1, 1], scope='conv_4')
由于卷积核的大小和通道数量不尽相同,不能使用repeat操作,但可以通过stack的方式:
1 x = slim.stack(x, slim.conv2d, [(32, [3, 3]), (32, [1, 1]), 2 (64, [3, 3]), (64, [1, 1])], scope='conv')
将不同的参数以元组的形式存在列表中。
slim作用域(scopes)
TensorFlow中scope机制的几种类型:
- name_scope:限制op的作用域
- variable_scope:变量的作用域
slim中还新增了arg_scope的scope机制。该机制可以给一个或者多个op指定默认参数。
还用开篇的LeNet来举例:如果没有arg_scope(),LeNet的三个卷积操作的slim层的使用应该是这样:
1 net = slim.conv2d(inputs, 6, [5, 5], 1, padding='SAME', 2 weights_initializer=tf.truncated_normal_initializer(stddev=0.01), 3 weights_regularizer=slim.l2_regularizer(0.005), scope='conv1') 4 net = slim.conv2d(net, 16, [5, 5], 1, padding='VALID', 5 weights_initializer=tf.truncated_normal_initializer(stddev=0.01), 6 weights_regularizer=slim.l2_regularizer(0.005), scope='conv2') 7 net = slim.conv2d(net, 120, [1, 1], 1, padding='SAME', 8 weights_initializer=tf.truncated_normal_initializer(stddev=0.01), 9 weights_regularizer=slim.l2_regularizer(0.005), scope='conv3')
看起来真的很繁琐,每个slim.conv2d()中有很多一样的参数。但如果给其设置默认参数,使用arg_scope(),代码将会得到简化。
1 with slim.arg_scope([slim.conv2d], padding='SAME', 2 weights_initializer=tf.truncated_normal_initializer(stddev=0.01) 3 weights_regularizer=slim.l2_regularizer(0.0005)): 4 net = slim.conv2d(inputs, 64, [11, 11], scope='conv1') 5 net = slim.conv2d(net, 128, [11, 11], padding='VALID', scope='conv2') 6 net = slim.conv2d(net, 256, [11, 11], scope='conv3')
在使用的时候,就是对各层找共性,共性越多,arg_scope()的使用便可以使代码越简洁。
但一般而言不同类型的层的共性不多,因此,可以使用嵌套的方式进行制定:
1 with slim.arg_scope([slim.conv2d, slim.fully_connected], 2 activation_fn=tf.nn.relu, 3 weights_initializer=tf.truncated_normal_initializer(stddev=0.01), 4 weights_regularizer=slim.l2_regularizer(0.0005)): 5 with slim.arg_scope([slim.conv2d], stride=1, padding='SAME'): 6 net = slim.conv2d(inputs, 64, [11, 11], 4, padding='VALID', scope='conv1') 7 net = slim.conv2d(net, 256, [5, 5], 8 weights_initializer=tf.truncated_normal_initializer(stddev=0.03), 9 scope='conv2') 10 net = slim.fully_connected(net, 1000, activation_fn=None, scope='fc')
在第一层arg_scope()中,对卷积层和全连接层的一些共性参数进行指定, 在第二层arg_scope()中,又对卷积层特有的参数进行指定。
使用slim训练模型
在模型建立后,模型的训练需要添加损失函数loss function,梯度计算gradient computation
Slim 损失函数Losses
据官方声明,slim.losses模块将被去除,请使用tf.losses模块,因为二者功能完全一致。
损失函数是教导机器分辨对错的量,也是要进行优化的参数。对于分类问题,通常采用交叉熵,对于回归问题,一般采用MSE/SSE。从下述对比中,可以看出,slim.losses模块和tf.losses模块的使用完全一致:
1 slim.losses.softmax_cross_entropy(predictions, input_label, scope='loss') 2 tf.losses.softmax_cross_entropy(predictions, input_label, scope='loss')
对于多任务需学习模型中,同一模型会存在多个损失函数,用于衡量不同功能的损失。例如,yolo v3中包含边框坐标的损失、分类的损失和置信度的损失。对于多任务的损失,通常会求取多个损失的和,或者是加权的和。 如下所示:
1 total_loss = classification_loss + sum_of_squares_loss
slim中也设计了相应函数get_total_loss(),会将通过slim生成的loss进行加和:
1 total_loss = slim.losses.get_total_loss(add_regularization_losses=False)
那如果有一些手动建立的loss,需要与slim建立的loss进行加和,手动建立的loss又该如何添加到slim当中呢?可以使用losses.add_loss()方法:
1 slim.losses.add_loss(my_loss)
之后,再进行加和的运算:
1 total_loss = slim.losses.get_total()
slim训练优化(Training Loop)
在tf中,当完成模型、损失的建立之后,接下来将会生成优化器:
1 optimizer = tf.train.GradientDescentOptimizer(lr).minimize(loss)
slim当中,训练op的功能包含两个操作,包含了:
- 计算损失;
- 进行梯度运算
其使用模式如下所示:
1 total_loss = slim.losses.get_total_loss() 2 optimizer = tf.train.GradientDescentOptimizer(learning_rate) 3 4 train_op = slim.learning.create_train_op(total_loss, optimizer) 5 logdir = ... # Where checkpoints are stored. 6 7 slim.learning.train( # actually runs training 8 train_op, 9 logdir, 10 number_of_steps=1000, 11 save_summaries_secs=300, 12 save_interval_secs=600)
- 损失
- 优化器,此时不需要.minimize(loss)
- 生成train_op , 损失和优化器一起; 个人感觉有些繁琐,不如普通TF的方式。
-
slim.learning.train() # 这个训练的机制倒是挺简洁,不用开session 也不用写循环
- checkpoint 和 event的保存目录
- 训练代数
- 每多10分钟保存一次