有多少人工,就有多少智能

TensorFlow slim API使用方法说明

 

  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分钟保存一次
posted @ 2021-08-14 21:18  lvdongjie-avatarx  阅读(113)  评论(0编辑  收藏  举报