• TensorFlow slim(一) 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分钟保存一次
  • 相关阅读:
    SpringMvc 大概流程分析
    HandlerMethodArgumentResolver 参数解析器
    linux 技巧:使用 screen 管理你的远程会话
    CentOS Linux解决Device eth0 does not seem to be present
    php连接oracle oracle开启扩展
    关于linux一些备份、还原,压缩,归档的命令
    Sphinx学习之sphinx的安装篇
    linux wget 命令用法详解(附实例说明)
    Linux的bg和fg命令
    linux中ctrl+z和ctrl+c的区别
  • 原文地址:https://www.cnblogs.com/monologuesmw/p/12627697.html
Copyright © 2020-2023  润新知