tensorflow slim(TF-Slim) 高级api介绍

1. TF-Slim 介绍

tf-slim是一个用于定义,训练和评估tensorflow中的复杂模型的轻量级库。tf-slim的组件可以与原生tensorflow以及其他框架(如tf.contrib.learn)自由混合。

https://github.com/tensorflow/tensorflow/tree/master/tensorflow/contrib/slim

导入

import tensorflow.contrib.slim as slim

2. TF-Slim 优势

tf-slim是一个使构建,训练和评估神经网络简单的库:

  • 允许用户通过消除样板代码来更加紧凑地定义模型。这是通过使用参数范围和许多高层次和变量来完成的。这些工具提高了可读性和可维护性,通过复制和粘贴超参数值降低了出错的可能性,并简化了超参数调优。

  • 通过提供常用的正规化项使开发模型变得简单。

  • 几种广泛使用的计算机视觉模型(例如,vgg,alexanetnet)已经开发出来,并且可供用户使用。这些可以用作黑盒子,或者可以以各种方式扩展,例如通过向不同的内部层添加“multiple heads”。通过提供常用的模型使开发模型变得简单

  • slim使扩展复杂模型变得容易,并通过使用预先存在的模型检查点来热启动训练算法

3. TF-Slim组成结构

TF-Slim由设计独立存在的几个部分组成,允许用户通过消除来更加紧凑地定义模型样板代码。
这些包括以下主要部分(详细解释如下)。

  • arg_scope:
    提供了一个名为arg_scope的新范围,它允许用户为该范围内的特定操作定义默认参数。

  • datadatasetdata providers,parallel_reader,decoding
    包含tf-slim的数据集定义,数据提供者,并行编译器和解码工具。

  • evaluation:
    评估:包含评估模型的例程。

  • layers:图层:包含使用张量流构建模型的高级图层。

  • learning:学习:包含训练模型的例程。
    contains routines for training models.

  • losses:损失:包含常用损失函数。

  • metrics:指标:包含流行的评估指标。

  • nets:网络:包含流行的网络定义,如vgg和alexnet模型。VGGAlexNet

  • queues:队列:提供了一个上下文管理器,可以轻松安全地启动和关闭

  • regularizers:正规化:包含权重正规化。

  • variables: 变量:为变量创建和操作提供便利包装。

4. 定义模型

利用TF-Slim通过合并variables, layers and scopes,模型可以简洁地进行定义。各元素定义如下。

4.1. 变量Variables

想在原生tensorflow中创建变量,要么需要一个预定义值,要么需要一种初始化机制。此外,如果变量需要在特定的设备上创建,比如GPU上,则必要要显式指定。为了简化代码的变量创建,TF-Slim在variables.py中提供了一批轻量级的函数封装,从而是调用者可以更加容易地定义变量。

Creating
Variables
in native tensorflow requires either a predefined value or an initialization
mechanism (e.g. randomly sampled from a Gaussian). Furthermore, if a variable
needs to be created
on a specific device, such as a GPU, the specification must be
made explicit.
To alleviate the code required for variable creation, TF-Slim provides a set
of thin wrapper functions in
variables.py
which allow callers to easily define variables.

例如,创建一个权值变量,并且用truncated_normal初始化,用L2损失正则化,放置于CPU中,我们只需要定义如下:

weights = slim.variable('weights',
                             shape=[10, 10, 3 , 3],
                             initializer=tf.truncated_normal_initializer(stddev=0.1),
                             regularizer=slim.l2_regularizer(0.05),
                             device='/CPU:0')

常规变量和局部(临时)变量

在原生tensorflow中,有两种类型的变量:常规变量和局部(临时)变量。绝大部分都是常规变量,它们一旦创建,可以用Saver保存在磁盘上。局部变量则只在一个session期间存在,且不会保存在磁盘上。

4.2. model variables

TF-Slim通过定义model variables可以进一步区分变量,这种变量代表一个模型的参数。模型变量在学习阶段被训练或微调,在评估和预测阶段从checkpoint中加载。比如通过slim.fully_connected 或slim.conv2d进行创建的变量。非模型变量是在学习或评估阶段使用,但不会在预测阶段起作用的变量。例如global_step,它在学习和评估阶段使用,但不是模型的一部分。类似地,移动均值可以mirror模型参数,但是它们本身不是模型变量。

通过TF-Slim,模型变量和常规变量都可以很容易地创建和获取

# Model Variables
weights = slim.model_variable('weights',
                              shape=[10, 10, 3 , 3],
                              initializer=tf.truncated_normal_initializer(stddev=0.1),
                              regularizer=slim.l2_regularizer(0.05),
                              device='/CPU:0')
model_variables = slim.get_model_variables()

# Regular variables
my_var = slim.variable('my_var',
                       shape=[20, 1],
                       initializer=tf.zeros_initializer())
regular_variables_and_model_variables = slim.get_variables()

这玩意是怎么起作用的呢?当你通过TF-Slim的layers或者直接通过slim.model_variable函数创建一个模型变量,TF-Slim会把这个变量添加到tf.GraphKeys.MODEL_VARIABLES这个集合中。
那我们自己的网络层变量怎么让TF-Slim管理呢?TF-Slim提供了一个很方便的函数可以将模型的变量添加到集合中:

my_model_variable = CreateViaCustomCode()

# Letting TF-Slim know about the additional variable.
slim.add_model_variable(my_model_variable)

4.3. Layers

tensorflow的操作符集合是十分广泛的,神经网络开发者通常会以更高层的概念,比如”layers”, “losses”, “metrics”, and “networks”去考虑模型。

一个层,比如卷积层、全连接层或BatchNorm层,要比一个单独的tensorflow操作符更抽象,并且通常会包含若干操作符。此外,和原始操作符不同,一个层经常(不总是)有一些与自己相关的变量(可调参数)。

例如,在神经网络中,一个卷积层由许多底层操作符组成:

  1. 创建权重、偏置变量
  2. 将来自上一层的数据和权值进行卷积
  3. 在卷积结果上加上偏置
  4. 应用激活函数

    如果只用普通的tensorflow代码,干这个事是相当的费事:

input = ...
with tf.name_scope('conv1_1') as scope:
  kernel = tf.Variable(tf.truncated_normal([3, 3, 64, 128], dtype=tf.float32,
                                           stddev=1e-1), name='weights')
  conv = tf.nn.conv2d(input, kernel, [1, 1, 1, 1], padding='SAME')
  biases = tf.Variable(tf.constant(0.0, shape=[128], dtype=tf.float32),
                       trainable=True, name='biases')
  bias = tf.nn.bias_add(conv, biases)
  conv1 = tf.nn.relu(bias, name=scope)

为了缓解重复这些代码,TF-Slim在更抽象的神经网络层的层面上提供了大量方便使用的操作符

比如,将上面的代码和TF-Slim响应的代码调用进行比较:

input = ...
net = slim.conv2d(input, 128, [3, 3], scope='conv1_1')

TF-Slim提供了标准接口用于组建神经网络,包括:

Layer TF-Slim
BiasAdd slim.bias_add
BatchNorm slim.batch_norm
Conv2d slim.conv2d
Conv2dInPlane slim.conv2d_in_plane
Conv2dTranspose (Deconv) slim.conv2d_transpose
FullyConnected slim.fully_connected
AvgPool2D slim.avg_pool2d
Dropout slim.dropout
Flatten slim.flatten
MaxPool2D slim.max_pool2d
OneHotEncoding slim.one_hot_encoding
SeparableConv2 slim.separable_conv2d
UnitNorm slim.unit_norm

TF-Slim也提供了两个元运算符repeat和stack,允许用户可以重复地使用相同的运算符。

例如,VGG网络的一个片段,这个网络在两个池化层之间就有许多卷积层的堆叠:

VGG network whose layers
perform several convolutions in a row between pooling layers:

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')

若使用TF-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不但可以在一行中使用相同的参数,而且还能智能地展开scope,即每个后续的slim.conv2d调用所对应的scope都会追加下划线及迭代数字。更具体地讲,上面代码的scope分别为 ‘conv3/conv3_1’, ‘conv3/conv3_2’ and ‘conv3/conv3_3’.

  • 此外,tf-slim的slim.stack运算符允许调用者,重复应用具有不同参数的相同操作,来创建堆栈或图层塔。
    slim.stack也为每个创建的操作创建一个新的tf.variable_scope。例如,创建多层感知器(mlp)的简单方法:

Furthermore, TF-Slim’s slim.stack operator allows a caller to repeatedly apply
the same operation with different arguments to create a stack or tower of
layers. slim.stack also creates a new tf.variable_scope for each
operation created. For example, a simple way to create a 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:
slim.stack(x, slim.fully_connected, [32, 64, 128], scope='fc')

In this example, slim.stack calls slim.fully_connected three times passing
the output of one invocation of the function to the next. However, the number of
hidden units in each invocation changes from 32 to 64 to 128. Similarly, one
can use stack to simplify a tower of multiple convolutions:

在这个例子中,slim.stack三次调用slim.fully_connected,将函数调用的输出传递给下一个。
而每个网络层的输出通道数从32变到64,再到128。

  • 同样,可以使用堆栈来简化多卷积塔:
# 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:
slim.stack(x, slim.conv2d, [(32, [3, 3]), (32, [1, 1]), (64, [3, 3]), (64, [1, 1])], scope='core')

4.4. Scopes

除了tensorflow中自带的scope机制类型name_scope,
variable_scope外, TF-Slim添加了一种叫做arg_scope的scope机制。

这种scope允许用户在arg_scope中指定若干操作符以及一批参数,这些参数会传给前面所有的操作符中。

以下代码是原始笨办法

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')

此解决方案确保所有三个卷积共享完全相同的参数值,但不会完全减少代码混乱。
通过使用anarg_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中被指定的参数值,也可以在局部位置进行覆盖。
比如,padding参数设置为’SAME’, 而第二个卷积层仍然可以通过把它设为’VALID’而覆盖掉arg_scope中的默认设置。

我们可以嵌套arg_scope, 也可以在一个scope中指定多个操作符,例如

with slim.arg_scope([slim.conv2d, slim.fully_connected],
                      activation_fn=tf.nn.relu,
                      weights_initializer=tf.truncated_normal_initializer(stddev=0.01),
                      weights_regularizer=slim.l2_regularizer(0.0005)):
  with slim.arg_scope([slim.conv2d], stride=1, padding='SAME'):
    net = slim.conv2d(inputs, 64, [11, 11], 4, padding='VALID', scope='conv1')
    net = slim.conv2d(net, 256, [5, 5],
                      weights_initializer=tf.truncated_normal_initializer(stddev=0.03),
                      scope='conv2')
    net = slim.fully_connected(net, 1000, activation_fn=None, scope='fc')

在这个例子中,第一个arg_scope对处于它的scope中的conv2d和fully_connected操作层应用相同的weights_initializer andweights_regularizer参数。在第二个arg_scope中,默认参数只是在conv2d中指定。

4.5. 示例:构建vgg16图层

通过整合TF-Slim的变量、操作符和scope,我们可以用寥寥几行代码写一个通常非常复杂的网络。
例如,完整的VGG结构只需要用下面的一小段代码定义:
VGG官方的结构说明:https://arxiv.org/pdf/1409.1556.pdf

网络结构:

构建代码:

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

5. 训练模型

训练一个tensorflow模型,需要一个网络模型,一个损失函数,梯度计算方式和用于迭代计算模型权重的训练过程。TF-Slim提供了损失函数,同时也提供了一批运行训练和评估模型的帮助函数。

6. 损失函数

损失函数定义了我们想要最小化的数量。
对于分类问题,这通常是跨分类的真实分布和预测概率分布之间的交叉熵。
对于回归问题,这通常是预测值和真值之间的平方和差异。

某些模型(如多任务学习模型)需要同时使用多种损失函数。换句话说,最终被最小化的损失函数是各种其他损失函数的总和。例如,有一个模型既要预测图像中场景的类型,又要预测每个像素的深度。那这个模型的损失函数就是分类损失和深度预测损失的和。

TF-Slim通过losses模块,提供了一种易用的机制去定义和跟踪损失函数的足迹。

看一个简单的例子,我们想训练VGG网络:

import tensorflow as tf
import tensorflow.contrib.slim.nets as nets
vgg = nets.vgg

# Load the images and labels.
images, labels = ...

# Create the model.
predictions, _ = vgg.vgg_16(images)

# Define the loss functions and get the total loss.
loss = slim.losses.softmax_cross_entropy(predictions, labels)

在上面的例子中,我们首先创建了模型(用TF-Slim的VGG接口实现),并添加了标准的分类损失。

现在,我们再看一个产生多输出的多任务模型:

# Load the images and labels.
images, scene_labels, depth_labels = ...

# Create the model.
scene_predictions, depth_predictions = CreateMultiTaskModel(images)

# Define the loss functions and get the total loss.
classification_loss = slim.losses.softmax_cross_entropy(scene_predictions, scene_labels)
sum_of_squares_loss = slim.losses.sum_of_squares(depth_predictions, depth_labels)

# The following two lines have the same effect:
total_loss = classification_loss + sum_of_squares_loss
total_loss = slim.losses.get_total_loss(add_regularization_losses=False)

在这个例子中,我们有两个损失,分别是通过slim.losses.softmax_cross_entropy和 slim.losses.sum_of_squares得到的。
我们可以通过将它们加在一起(total_loss)或调用slim.losses.get_total_loss()来获得全部损失。这是如何工作的?当您通过tf-slim创建损失功能时,tf-slim会将损失添加到特殊的tensorflow损失功能集合中。这使您可以手动管理全部损失,或允许tf-slim为您管理它们。

如果我们有一个自定义的损失函数,现在也想托管给TF-Slim,该怎么做呢?

loss_ops.py也有一个函数可以将这个损失函数加入到TF-Slim集合中。例如

# Load the images and labels.
images, scene_labels, depth_labels, pose_labels = ...

# Create the model.
scene_predictions, depth_predictions, pose_predictions = CreateMultiTaskModel(images)

# Define the loss functions and get the total loss.
classification_loss = slim.losses.softmax_cross_entropy(scene_predictions, scene_labels)
sum_of_squares_loss = slim.losses.sum_of_squares(depth_predictions, depth_labels)
pose_loss = MyCustomLossFunction(pose_predictions, pose_labels)
slim.losses.add_loss(pose_loss) # Letting TF-Slim know about the additional loss.

# The following two ways to compute the total loss are equivalent:
regularization_loss = tf.add_n(slim.losses.get_regularization_losses())
total_loss1 = classification_loss + sum_of_squares_loss + pose_loss + regularization_loss

# (Regularization Loss is included in the total loss by default).
total_loss2 = slim.losses.get_total_loss()

这个例子中,我们同样既可以手动管理损失函数,也可以让TF-Slim知晓这个自定义损失函数,然后托管给TF-Slim。

7. 循环训练

在learning.py中,TF-Slim提供了简单却非常强大的训练模型的工具集。包括Train函数,可以重复地测量损失,计算梯度以及保存模型到磁盘中,还有一些方便的函数用于操作梯度。例如,当我们定义好了模型、损失函数以及优化方式,我们就可以调用slim.learning.create_train_op 和 slim.learning.train 去执行优化:

https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/slim/python/slim/learning.py

g = tf.Graph()

# Create the model and specify the losses...
...

total_loss = slim.losses.get_total_loss()
optimizer = tf.train.GradientDescentOptimizer(learning_rate)

# create_train_op ensures that each time we ask for the loss, the update_ops
# are run and the gradients being computed are applied too.
train_op = slim.learning.create_train_op(total_loss, optimizer)
logdir = ... # Where checkpoints are stored.

slim.learning.train(
    train_op,
    logdir,
    number_of_steps=1000,
    save_summaries_secs=300,
    save_interval_secs=600):

在该例中,slim.learning.train根据train_op计算损失、应用梯度step。logdir指定了checkpoints和event文件的存储路径。我们可以限制梯度step到任何数值。这里我们采用1000步。最后,save_summaries_secs=300表示每5分钟计算一次summaries,save_interval_secs=600表示每10分钟保存一次模型的checkpoint。

8. 例子: 训练VGG

import tensorflow as tf
import tensorflow.contrib.slim.nets as nets

slim = tf.contrib.slim
vgg = nets.vgg
...
train_log_dir = ...
if not tf.gfile.Exists(train_log_dir):
  tf.gfile.MakeDirs(train_log_dir)

with tf.Graph().as_default():
  # Set up the data loading:
  images, labels = ...

  # Define the model:
  predictions = vgg.vgg_16(images, is_training=True)

  # Specify the loss function:
  slim.losses.softmax_cross_entropy(predictions, labels)

  total_loss = slim.losses.get_total_loss()
  tf.summary.scalar('losses/total_loss', total_loss)

  # Specify the optimization scheme:
  optimizer = tf.train.GradientDescentOptimizer(learning_rate=.001)

  # create_train_op that ensures that when we evaluate it to get the loss,
  # the update_ops are done and the gradient updates are computed.
  train_tensor = slim.learning.create_train_op(total_loss, optimizer)

  # Actually runs training.
  slim.learning.train(train_tensor, train_log_dir)

9. 微调现有模型

10. 对从checkpoint加载variables的简略概括

在一个模型训练完成后,我们可以用tf.train.Saver()通过指定checkpoing加载variables的方式加载这个模型。对于很多情况,tf.train.Saver()提供了一种简单的机制去加载所有或一些varialbes变量。

# Create some variables.
v1 = tf.Variable(..., name="v1")
v2 = tf.Variable(..., name="v2")
...
# Add ops to restore all the variables.
restorer = tf.train.Saver()

# Add ops to restore some variables.
restorer = tf.train.Saver([v1, v2])

# Later, launch the model, use the saver to restore variables from disk, and
# do some work with the model.
with tf.Session() as sess:
  # Restore variables from disk.
  restorer.restore(sess, "/tmp/model.ckpt")
  print("Model restored.")
  # Do some work with the model
  ...

https://tensorflow.google.cn/programmers_guide/saved_model

参阅Variables章中Restoring Variables和Choosing which Variables to Save and Restore 相关部分,获取更多细节。

11. 部分恢复模型

有时我们希望在一个全新的数据集上或面对一个信息任务方向去微调预训练模型。在这些情况下,我们可以使用TF-Slim’s的帮助函数去加载模型中变量的一个子集:

# Create some variables.
v1 = slim.variable(name="v1", ...)
v2 = slim.variable(name="nested/v2", ...)
...

# Get list of variables to restore (which contains only 'v2'). These are all
# equivalent methods:
variables_to_restore = slim.get_variables_by_name("v2")
# or
variables_to_restore = slim.get_variables_by_suffix("2")
# or
variables_to_restore = slim.get_variables(scope="nested")
# or
variables_to_restore = slim.get_variables_to_restore(include=["nested"])
# or
variables_to_restore = slim.get_variables_to_restore(exclude=["v1"])

# Create the saver which will be used to restore the variables.
restorer = tf.train.Saver(variables_to_restore)

with tf.Session() as sess:
  # Restore variables from disk.
  restorer.restore(sess, "/tmp/model.ckpt")
  print("Model restored.")
  # Do some work with the model
  ...

12. 用不同的变量名加载模型

当从checkpoint加载变量时,Saver先在checkpoint中定位变量名,然后映射到当前图的变量中。
我们也可以通过向saver传递一个变量列表来创建saver。这时,在checkpoint文件中用于定位的变量名可以隐式地从各自的var.op.name中获得。

当checkpoint文件中的变量名与当前图中的变量名完全匹配时,这会运行得很好。但是,有时我们想从一个变量名与与当前图的变量名不同的checkpoint文件中装载一个模型。这时,我们必须提供一个saver字典,这个字典对checkpoint中的每个变量和每个图变量进行了一一映射。

请看下面这个例子,checkpoint的变量是通过一个简单的函数获得的:

# Assuming than 'conv1/weights' should be restored from 'vgg16/conv1/weights'
def name_in_checkpoint(var):
  return 'vgg16/' + var.op.name

# Assuming than 'conv1/weights' and 'conv1/bias' should be restored from 'conv1/params1' and 'conv1/params2'
def name_in_checkpoint(var):
  if "weights" in var.op.name:
    return var.op.name.replace("weights", "params1")
  if "bias" in var.op.name:
    return var.op.name.replace("bias", "params2")

variables_to_restore = slim.get_model_variables()
variables_to_restore = {name_in_checkpoint(var):var for var in variables_to_restore}
restorer = tf.train.Saver(variables_to_restore)

with tf.Session() as sess:
  # Restore variables from disk.
  restorer.restore(sess, "/tmp/model.ckpt")

13. 在不同的任务上微调模型

考虑我们有预先培训的vgg16模型的情况。该模型在imagenet数据集上进行了训练,该数据集有1000个类。然而,我们想将它应用于只有20个类的pascal voc数据集。为了能这样做,我们可以通过利用除最后一些全连接层的其他预训练模型值来初始化新模型的达到目的:

# Load the Pascal VOC data
image, label = MyPascalVocDataLoader(...)
images, labels = tf.train.batch([image, label], batch_size=32)

# Create the model
predictions = vgg.vgg_16(images)

train_op = slim.learning.create_train_op(...)

# Specify where the Model, trained on ImageNet, was saved.
model_path = '/path/to/pre_trained_on_imagenet.checkpoint'

# Specify where the new model will live:
log_dir = '/path/to/my_pascal_model_dir/'

# Restore only the convolutional layers:
variables_to_restore = slim.get_variables_to_restore(exclude=['fc6', 'fc7', 'fc8'])
init_fn = assign_from_checkpoint_fn(model_path, variables_to_restore)

# Start training.
slim.learning.train(train_op, log_dir, init_fn=init_fn)

14. 评估模型

一旦我们训练好了一个模型(或者模型还在训练中),我们想看一下模型在实际中性能如何。这可以通过获取一系列表征模型性能的评估指标来实现,评估代码一般会加载数据,执行前向传播,和ground truth进行比较并记录评估分数。这个步骤可能执行一次,也可能周期性地执行。

15. 指标

比如我们定义了一个不是损失函数的性能度量指标(损失在训练过程中进行直接优化),而这个指标出于评估模型的目的我们还非常感兴趣。比如说我们想最小化log损失,但是我们感兴趣的指标可能是F1 score(测试准确率),或者IoU分数(这个指标不可微,因此不能作为损失)。

TF-Slim提供了一系列指标操作符,它们可以使模型评估更简单。抽象来讲,计算一个指标值可以分为3步:

  1. 初始化:初始化用于计算指标的变量。

  2. 聚合:执行用于计算指标的运算流程(比如sum)。

  3. 收尾:(可选)执行其他用于计算指标值的操作。例如,计算mean、min、max等。

例如,为了计算绝对平均误差,一个count变量和一个total变量需要初始化为0. 在聚合阶段,我们可以观察到一系列预测值及标签,计算他们差的绝对值,并加到total中。每次循环,count变量自加1。最后,在收尾阶段,total除以count就得到了mean。

下面的例子演示了定义指标的API。因为指标通常是在测试集上计算,而不是训练集(训练集上是用于计算loss的),我们假设我们在使用测试集:

images, labels = LoadTestData(...)
predictions = MyModel(images)

mae_value_op, mae_update_op = slim.metrics.streaming_mean_absolute_error(predictions, labels)
mre_value_op, mre_update_op = slim.metrics.streaming_mean_relative_error(predictions, labels)
pl_value_op, pl_update_op = slim.metrics.percentage_less(mean_relative_errors, 0.3)

如上例所示,指标的创建会返回两个值,一个value_op和一个update_op。value_op表示和当前指标值幂等的操作。update_op是上文提到的执行聚合步骤并返回指标值的操作符。

跟踪每个value_op和update_op是非常费劲的。为了解决这个问题,TF-Slim提供了两个方便的函数:


# Aggregates the value and update ops in two lists:
value_ops, update_ops = slim.metrics.aggregate_metrics(
    slim.metrics.streaming_mean_absolute_error(predictions, labels),
    slim.metrics.streaming_mean_squared_error(predictions, labels))

# Aggregates the value and update ops in two dictionaries:
names_to_values, names_to_updates = slim.metrics.aggregate_metric_map({
    "eval/mean_absolute_error": slim.metrics.streaming_mean_absolute_error(predictions, labels),
    "eval/mean_squared_error": slim.metrics.streaming_mean_squared_error(predictions, labels),
})

16. 示例:跟踪多个指标

Putting it all together:

import tensorflow as tf
import tensorflow.contrib.slim.nets as nets

slim = tf.contrib.slim
vgg = nets.vgg


# Load the data
images, labels = load_data(...)

# Define the network
predictions = vgg.vgg_16(images)

# Choose the metrics to compute:
names_to_values, names_to_updates = slim.metrics.aggregate_metric_map({
    "eval/mean_absolute_error": slim.metrics.streaming_mean_absolute_error(predictions, labels),
    "eval/mean_squared_error": slim.metrics.streaming_mean_squared_error(predictions, labels),
})

# Evaluate the model using 1000 batches of data:
num_batches = 1000

with tf.Session() as sess:
  sess.run(tf.global_variables_initializer())
  sess.run(tf.local_variables_initializer())

  for batch_id in range(num_batches):
    sess.run(names_to_updates.values())

  metric_values = sess.run(names_to_values.values())
  for metric, value in zip(names_to_values.keys(), metric_values):
    print('Metric %s has value: %f' % (metric, value))

注意,metric_ops.py可以在没有layers.py和loss_ops.py的情况下独立使用。
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/metrics/python/ops/metric_ops.py

17. 循环评估

TF-Slim提供了一个评估模块(evaluation.py),这个模块包含了一些利用来自metric_ops.py模块的指标写模型评估脚本的帮助函数。其中包含一个可以周期运行评估,评估数据batch之间的指标、打印并总结指标结果的函数。
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/slim/python/slim/evaluation.py

import tensorflow as tf

slim = tf.contrib.slim

# Load the data
images, labels = load_data(...)

# Define the network
predictions = MyModel(images)

# Choose the metrics to compute:
names_to_values, names_to_updates = slim.metrics.aggregate_metric_map({
    'accuracy': slim.metrics.accuracy(predictions, labels),
    'precision': slim.metrics.precision(predictions, labels),
    'recall': slim.metrics.recall(mean_relative_errors, 0.3),
})

# Create the summary ops such that they also print out to std output:
summary_ops = []
for metric_name, metric_value in names_to_values.iteritems():
  op = tf.summary.scalar(metric_name, metric_value)
  op = tf.Print(op, [metric_value], metric_name)
  summary_ops.append(op)

num_examples = 10000
batch_size = 32
num_batches = math.ceil(num_examples / float(batch_size))

# Setup the global step.
slim.get_or_create_global_step()

output_dir = ... # Where the summaries are stored.
eval_interval_secs = ... # How often to run the evaluation.
slim.evaluation.evaluation_loop(
    'local',
    checkpoint_dir,
    log_dir,
    num_evals=num_batches,
    eval_op=names_to_updates.values(),
    summary_op=tf.summary.merge(summary_ops),
    eval_interval_secs=eval_interval_secs)

参考文献:

基于官方文档翻译总结
https://github.com/tensorflow/models/tree/master/research/slim


技术交流学习,请加QQ微信:631531977
目录