TensorFlow-Slim

TensorFlow-Slim

TF-Slim 是一个用于定义、训练、和评估复杂模型的TensorFlow高级库。tf-slim能与原生tensorflow和其他高层框架(如tf.contrib.learn等)混合使用。

使用Tf-Slim

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

为什么使用tf-slim?

TF-Slim是一个能够简化神经网络的构建、训练和评估流程的高级库 :

  • 通过消除模板化代码,使得用户定义模板的过程变得更加简洁。这是通过参数的作用域和大量高级网络层和变量的定义来实现的。这些工具能够增加代码的可读性、稳定性,减少超参复制粘贴过程中出错的可能性,同时简化了超参的调节过层。
  • 提供内置的正则化选项,使得模型开发更加简单
  • 已经实现好的几个常见图像模型(如VGG,AlexNet等)。一方面用户可以吧这些模型当作黑匣子来使用,另一方面,通过在不同的中间层加"multiple heads",这些模型能以多种方式进行扩展。
  • 通过调用预训练的参数,缩短训练时间,Slim能够轻松地实现复杂的模型。

Tf-slim包含那些组件?

Tf-Slim包含多个独立的组成部件,主要包括:

  • arg_scope:在该参数作用域内,用户可以为特定操作类型定义默认参数。
  • data:保含了tf-slim对于数据集的定义、数据提供者、并行读取和解码工具。
  • evaluation:包含了模型的评估模板
  • layers:包含了经过封装的高级网络层,用于构建tensorflow模型。
  • learning:包含了模型的训练模板
  • losses:包含了常用的损失函数
  • metrics:包含了常用的评估指标
  • nets:包含了常用的网络模型如VGG、AlexNet等等
  • quenes:包含一个上下文管理机制,用于安全地开启和关闭QueueRunners
  • regularizers:包含了正则化选项
  • variables:包含一些装饰器,用于构造和处理变量

定义模型

通过对变量、网络层和作用域进行组合,tf-slim能成功构建神经网络模型。不同组件的定义如下。

变量

在原生tensorflow代码中,创建一个变量需要一个初始化机制(如高斯分布、均匀分布等),或者一个已经定义过的变量。此外,变量使用的硬件(GPU或CPU)必须被明确指定。为了简化创建变量的代码,tf-slim在文件variables.py中提供了一系列的装饰函数。例如,创建一个权重变量,用truncated normal distribution来初始化,并添加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中,有两种变量:常规变量和局部(临时)变量。其中大部分变量都是常规变量:一旦创建,这些变量就能通过savar存储到磁盘中。局部变量则只存在于会话(session)期间,无法被保存。
tf-slim通过定义表示模型参数的模型变量(model variables),进一步对变量进行了区分。模型变量在训练过层中被调整,在评估和推断过层中通过加载点(checkpoint)来加载。模型变量包括由slim.fully_connected或slim.conv2d创建的变量等。非模型变量则只是在训练和评估过层中被使用,在实际推断过层中则不使用。例如,global_step就是一个非模型变量,只是用于对模型的训练,但它不属于最终模型的组成部分。同样地,moving average variables(滑动平均相关的变量)会影响模型变量的取值,但他们本身不属于模型变量。
这两种变量在都能在tf-slim中被创建和检索:

#model variables
model_weights = slim.model_variable('model_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中。如果你想自定义layers或者变量,但是仍然希望tf-slim管理这些模型变量,应该怎么做呢?tf.slim提供了简单的函数用于将自定义变量加入模型变量集合:

my_model_variable=CreateViaCustomCode()
#letting tf-slim konw about the additianal variable.
slim.add_model_variable(my_model_variable)

网络层(layers)

一方面,tensorflow的操作集合包含内容很多;另一方面,神经网络开发者往往倾向于考虑高层概如"layers","losses","metrics"和"networks"等等。一个网络层,如卷积层、全连接层、BatchNorm Layer等往往比一个单一的tensorflow操作(operation)更加抽象,并且一般涉及多个操作。此外,与原始的操作不同,一个layer通常包含与之相关的可调整变量。例如,一个卷积层一般包括如果几个基本操作:

  • 创建权重变量和偏置变量
  • 将来之前一网络层的输出与本层权重矩阵做卷积
  • 将卷积的结果与偏置矩阵相加
  • 添加激活函数
    如果使用原生tensorflow代码来实现,这会耗费大量的精力:
with tf.name_scope('conv1_1') as scope:
    kernel = tf.Variable(tf.truncated_normal([3,3,64,128],dtype=tf.float32,stddev=le-1),name='weights')
    conv = tf.nn.conv2d(input,kernel,[1,1,1,1],padding='SAME')
    biases = tf.Variable(tf.cosntant(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同时提供两种元操作(meta-operations),分别是repeat和stack,他们允许用户重复执行相同的操作。例如,考虑如下部分VGG网络,该部分由卷积层和池化层组成:

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_' % (i+1))
net = slim.max_maxpool2d(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不仅仅是重复执行该行代码中相同的参数,它也足够智能,能够根据迭代次数对slim.conv2d所创建的子网络层的作用域进行适当修改。具体来说,上述代码中,作用域将分别被命名为'conv3/conv3_1','conv3/conv3_2'和'conv3/conv3_3'。
此外,tf-slim的slim.stack操作允许调用者将同意一个操作赋予不同的参数值,进行迭代并最终创建一个包含多个网络层的栈。slim.stack同时会为每个操作创建一个新的tf.variable_scope。例如,一个创建多层感知器的方法如下:

#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='tc')

在这个例子中,slim.stack调用了三次slim.fully_connected,分别将输入进行处理并向后传递。然而,不同网络层的节点数是不一样的(分别为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:
slim.stack(x, slim.conv2d, [(32, [3, 3]), (32, [1, 1]), (64, [3, 3]), (64, [1, 1])], scope='core')

作用域

作为对tensorflow作用域机制(name_scope, variable_scope)的补充,tf-slim增加了一个新的作用域机制arg_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')

很明显,上述三个卷积层包含很多相同的超参。其中两个有相同的填充模式,三个都包含相同的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(seddev=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参数在arg_scope中被设定为'SAME',作为默认值,但是该值在第二个卷积中被改写了。
arg_scope可以被嵌套使用,并且同一arg_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为conv2d和fully_connected两个网络层设定了相同的weights_initializer和weights_regularizer;在第二个arg_scope中,进一步为conv2d网络层设定了其他参数的默认值。

实际案例:设定VGG16的网络层

通过组合tf-slim变量,操作和作用域,我们能够在少量代码内实现复杂的网络结构。例如,整个VGG架构可以用以下代码段实现:

def vgg16(inputs):
    with slim.arg_scope([slim.conv2d, slim.fully_connected],
                       activate_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

训练模型

训练tensorflow模型需要一个网络模型,一个损失函数,梯度计算和训练例程用于迭代计算模型参数关于损失函数的梯度并进行相应的更新。tf-slim同时提供了常用损失函数和一些辅助函数用于执行训练和评估过程。

损失函数

损失函数定义了一个我们希望最小化的实数。对于分类问题来说,典型的损失函数是标签和预测的概率分布之间的交叉熵。对于回归问题来说,损失函数一般使用预测值和实际值之间的误差平方和。
对于有些模型,比如多任务模型,可能需要同时使用多种损失函数的加权和。换句话说,我们需要最小化的是各项损失函数的加权和。例如,对于一个同时预测图像类型和像素深度的模型来说,其损失函数应该是分类损失函数和像素深度预测损失函数的加权和。
tf-slim通过损失函数模型提供了简单易用的机制用于定义和跟踪损失函数。以VGG模型的情况为例:

import tensorflow as tf
vgg = tf.contrib.slim.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。我们可以通过加法操作或者调用slim.losses.get_total_loss()得到总的损失函数项。这是如何运作的呢?每当你通过tf-slim创建一个损失项,tf-slim自动将损失项加入损失函数集合。这使得你即可以手动管理总的损失函数,也可以委托tf-slim代为管理。
如果你想自定义损失函数,并委托tf-slim代为管理,应该怎样做呢?loss_ops.py文件提供了相应的函数用于将自定义损失函数加入对应集合。例如:

# 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在文档learning.py中提供了一系列简单有效的工具用于训练模型。这些包括一个训练函数用于重复测量损失值、计算梯度和保存模型到磁盘,同时包括其它简便的函数用于处理梯度函数。例如,一旦确定了一个模型和对应的损失函数及优化策略,我们就能调用slim.learning.create_train_op 和slim.learning.train来处理优化过程:

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被用于计算损失函数和梯度并进行参数更新。logdir指定了加载点和事件文件的存储目录。我们可以将梯度算法的迭代步数限定为任意值,本例中,我们的迭代次数为1000。最后,save_summaries_secs=300表示每300s做一次总结,save_interval_secs=600表示每600s保存一次模型加载点。

实际案例:训练VGG16模型

为了说明问题,我们考察如下VGG网络的训练过程:

import tensorflow as tf

slim = tf.contrib.slim

import tensorflow as tf 

slim = tf.contrib.slim
vgg = tf.contrib.slim.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.scalar('losses/total_lose', total_loss)
    
    #specify the optimization scheme:
    optimizer = tf.train.GradientDescentOptimizer(learning_rate=0,.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)

微调(Fine-Tuning)已有模型

简要介绍从checkpoint加载变量

已经训练好的模型,可以通过tf.train.Saver()进行恢复,该函数能从给定的checkpoint中加载变量。在很多情况下,tf.train.Saver()为重新加载部分或全部变量提供了简单的机制。

#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 sth with the model
with tf.Session() as sess:
    #restore variables from disk.
    restorer.restore(sess, '/tmp/model.ckpt')
    print('model restored.')
    #do sth with the model
    ...

更多细节请查看文档变量中的加载变量、选择性保存和加载变量部分。

加载部分模型

针对一个新的数据集甚至一个新的任务,对一个预先训练好的模型进行微调是很有必要的。这种情况下,我们可以通过tf-slim的辅助函数来选择性加载部分变量:

# 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 

加载变量名不相同的模型

当我们从checkpoint加载变量时,Saver将checkpoint文件中的变量与当前计算图中的变量通过变量名进行一一映射。上文代码中,我们通过传递变量列表创建了一个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")

针对一个新的任务对模型进行微调

考虑已经训练好的VGG-16模型。这个模型是在ImageNet数据集上训练的,它包含一千个分类类别。然而,我们希望将该模型应用到只有20个类别的Pascal VOC数据集上。为了完成这个任务,我们可以除去最后的全连接层,用其它预训练好的参数来初始化我们的新模型。

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

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

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

#specify whte the Moder, 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)

评估模型

对于一个已经训练好(或者正在训练)的模型,我们希望知道其实际效果怎么样。这可以通过选择合适的评估指标、给模型打分来实现。相应的代码首先会下载一些测试数据,然后执行推断过程,并将估计结果与实际标签进行对比,获得并记录相应的评估分数。这个过程可能只执行一次,也可能周期性重复执行。

评估指标

我们定义一个评估指标作为对模型性能的度量,该指标不是损失函数(损失值在训练过层中被直接优化),但是为了观察模型的好坏,我们仍然对这个评估指标感兴趣。例如,我们在训练时可能想最小化对数损失函数,但是我们的感兴趣的评估指标可能是F1分数(测试精确度),或者IOU(Intersection Over Union,该指标是不可微的,因此不能被用作损失函数)。
TF-Slim提供了一些指标使得模型的评估过程变得更简单。概括来说,计算一个指标的值可以被分成三步:

  1. Initialization:初始化用于计算metrics的值。
  2. Aggregation: 执行用于计算metrics的运算步骤(如求和等)。
  3. Finalization: (可选)执行最后的运算步骤来计算metrics的值,如均值、取最小值、取最大值等。
    例如,为了计算mean_absolute_error,两个变量(一个count和一个total variable)被初始化为0。在aggregation过程中,我们观测一系列的估计值和标签,计算他们的绝对误差,与total variable相加。每当我们移到下一个观测样本,count值被加1。最后,在finalization过程中,total variable除以count得到误差均值。
    下面的例子演示了申明metrics的API。因为metrics的值一般在测试集(这是与训练集不同的)中计算,所以我们假定下面的数据来自测试集:
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)

如上例所示,创建一个metrics会返回两个值:一个value_op和一个update_op。value_op是一个幂等的操作,返回metric的当前值,update_op则执行上述的aggregation步骤,并返回相应的metric值。
跟踪每个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),
})

实际案例:跟踪多个评估指标

import tensorflow as tf

slim = tf.contrib.slim
vgg = tf.contrib.slim.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))

评估循环

TF-Slim提供了评估模型(evaluation.py),该模型包含用于编写模型评估脚本的辅助函数(metrics由metric_ops.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)

作者

Sergio Guadarrama和Nathan Silberman

你可能感兴趣的:(TensorFlow-Slim)