3.3 使用TensorFlow Slim微调模型

TensorFlow Slim是Google公司公布的一个图像分类工具包,它不仅定义了一些方便的接口,还提供了很多ImageNet数据集上常用的网络结构和预训练模型。截至2017年7月,Slim提供包括VGG16、VGG19、Inception V1~V4、ResNet 50、ResNet 101、MobileNet在内大多数常用模型的结构以及预训练模型,更多的模型还会被持续添加进来。

在本节中,先介绍如何下载Slim的源代码,再介绍如何在Slim中定义新的数据库,最后介绍如何使用新的数据库训练以及如何进行参数调整。

3.3.1 下载TensorFlow Slim的源代码

如果需要使用Slim微调模型,首先要下载Slim的源代码。Slim的源代码保存在tensorflow/models项目中,可以使用下面的git命令下载tensorflow/models:

    git clone https://github.com/tensorflow/models.git

找到models/research/目录中的slim文件夹,这就是要用到的TensorFlow Slim的源代码。在chapter3/slim/中也提供了这份代码。

这里简单介绍TensorFlow Slim的代码结构,见表3-2。

表3-2 TensorFlow Slim的代码结构及用途

表3-2中只列出了TensorFlow Slim中最重要的几个文件以及文件夹的作用。其他还有少量文件和文件夹,如果读者对它们的作用感兴趣,可以自行参阅其文档。

3.3.2 定义新的datasets文件

在slim/datasets中,定义了所有可以使用的数据库,为了使用在第3.2节中创建的tfrecord数据进行训练,必须要在datasets中定义新的数据库。

首先,在datasets/目录下新建一个文件satellite.py,并将flowers.py文件中的内容复制到satellite.py中。接下来,需要修改以下几处内容。

第一处是_FILE_PATTERN、SPLITS_TO_SIZES、_NUM_CLASSES,将其进行以下修改:

   _FILE_PATTERN='satellite_%s_*.tfrecord'

    SPLITS_TO_SIZES={'train': 4800, 'validation': 1200}
   _NUM_CLASSES=6

_FILE_PATTERN变量定义了数据的文件名的格式和训练集、验证集的数量。这里定义_FILE_PATTERN='satellite_%s_*.tfrecord’和SPLITS_TO_SIZES={'train': 4800, 'validation': 1200},就表明数据集中,训练集的文件名格式为satellite_train_*.tfrecord,共包含4800张图片,验证集文件名格式为satellite_validation_*.tfrecord,共包含1200张图片。

_NUM_CLASSES变量定义了数据集中图片的类别数目。

第二处修改为image/format部分,将之修改为:

    'image/format': tf.FixedLenFeature((), tf.string, default_value='jpg'),

此处定义了图片的默认格式。收集的卫星图片的格式为jpg图片,因此修改为jpg。

最后,读者也可以对文件中的注释内容进行合适的修改。

修改完satellite.py后,还需要在同目录的dataset_factory.py文件中注册satellite数据库。未修改的dataset_factory.py中注册数据库的对应代码为:

    from datasets import cifar10
    from datasets import flowers
    from datasets import imagenet
    from datasets import mnist

    datasets_map={
      'cifar10': cifar10,
      'flowers': flowers,
      'imagenet': imagenet,
      'mnist': mnist,
    }

很显然,此时只注册了4个数据库,对这部分进行修改,将satellite模块也添加进来就可以了:

    from datasets import cifar10
    from datasets import flowers
    from datasets import imagenet
    from datasets import mnist
    from datasets import satellite

    datasets_map={
      'cifar10': cifar10,
      'flowers': flowers,
      'imagenet': imagenet,
      'mnist': mnist,
      'satellite': satellite,
    }

3.3.3 准备训练文件夹

定义完数据集后,在slim文件夹下再新建一个satellite目录,在这个目录中,完成最后的几项准备工作:

· 新建一个data目录,并将第3.2节中准备好的5个转换好格式的训练数据复制进去。

· 新建一个空的train_dir目录,用来保存训练过程中的日志和模型。

· 新建一个pretrained目录,在slim的GitHub页面找到Inception V3模型的下载地址http://download.tensorflow.org/models/inception_v3_2016_08_28.tar.gz,下载并解压后,会得到一个inception_v3.ckpt文件在随书提供的数据中同样包含这个文件,对应路径是chapter_3_data/inception_v3.ckpt。,将该文件复制到pretrained目录下。

最后形成的目录结构为:

    slim/
      satellite/
          data/
            satellite_train_00000-of-00002.tfrecord
            satellite_train_00001-of-00002.tfrecord
            satellite_validation_00000-of-00002.tfrecord
            satellite_validation_00001-of-00002.tfrecord
            label.txt
          pretrained/
            inception_v3.ckpt
          train_dir/

3.3.4 开始训练

在slim文件夹下,运行以下命令就可以开始训练了:

    python train_image_classifier.py \
     --train_dir=satellite/train_dir \
     --dataset_name=satellite \
     --dataset_split_name=train \
     --dataset_dir=satellite/data \
     --model_name=inception_v3 \
     --checkpoint_path=satellite/pretrained/inception_v3.ckpt \
     --checkpoint_exclude_scopes=InceptionV3/Logits, InceptionV3/AuxLogits \
     --trainable_scopes=InceptionV3/Logits, InceptionV3/AuxLogits \
     --max_number_of_steps=100000 \
     --batch_size=32 \
     --learning_rate=0.001 \
     --learning_rate_decay_type=fixed \
     --save_interval_secs=300 \
     --save_summaries_secs=2 \
     --log_every_n_steps=10 \
     --optimizer=rmsprop \
     --weight_decay=0.00004

这里的参数比较多,下面一一进行介绍:

·--trainable_scopes=InceptionV3/Logits, InceptionV3/AuxLogits:首先来解释参数trainable_scopes的作用,因为它非常重要。trainable_scopes规定了在模型中微调变量的范围。这里的设定表示只对InceptionV3/Logits, InceptionV3/AuxLogits两个变量进行微调,其他变量都保持不动。InceptionV3/Logits, InceptionV3/AuxLogits就相当于在第3.1节中所讲的fc8,它们是Inception V3的“末端层”。如果不设定trainable_scopes,就会对模型中所有的参数进行训练。

·--train_dir=satellite/train_dir:表明会在satellite/train_dir目录下保存日志和checkpoint。

·--dataset_name=satellite、--dataset_split_name=train:指定训练的数据集。在第3.3.2节中定义的新的dataset就是在这里发挥用处的。

·--dataset_dir=satellite/data:指定训练数据集保存的位置。

·--model_name=inception_v3:使用的模型名称。

·--checkpoint_path=satellite/pretrained/inception_v3.ckpt:预训练模型的保存位置。

·--checkpoint_exclude_scopes=InceptionV3/Logits, InceptionV3/AuxLogits:在恢复预训练模型时,不恢复这两层。正如之前所说,这两层是Inception V3模型的末端层,对应着ImageNet数据集的1000类,和当前的数据集不符,因此不要去恢复它。

·--max_number_of_steps 100000:最大的执行步数。

·--batch_size=32:每步使用的batch数量。

·--learning_rate=0.001:学习率。

·--learning_rate_decay_type=fixed:学习率是否自动下降,此处使用固定的学习率。

·--save_interval_secs=300:每隔300s,程序会把当前模型保存到train_dir中。此处就是目录satellite/train_dir。

·--save_summaries_secs=2:每隔2s,就会将日志写入到train_dir中。可以用TensorBoard查看该日志。此处为了方便观察,设定的时间间隔较多,实际训练时,为了性能考虑,可以设定较长的时间间隔。

·--log_every_n_steps=10:每隔10步,就会在屏幕上打出训练信息。

·--optimizer=rmsprop:表示选定的优化器。

·--weight_decay=0.00004:选定的weight_decay值。即模型中所有参数的二次正则化超参数。

以上命令是只训练末端层InceptionV3/Logits, InceptionV3/AuxLogits,还可以使用以下命令对所有层进行训练:

    python train_image_classifier.py \
     --train_dir=satellite/train_dir \
     --dataset_name=satellite \
     --dataset_split_name=train \
     --dataset_dir=satellite/data \
     --model_name=inception_v3 \
   --checkpoint_path=satellite/pretrained/inception_v3.ckpt \
   --checkpoint_exclude_scopes=InceptionV3/Logits, InceptionV3/AuxLogits \
   --max_number_of_steps=100000 \
   --batch_size=32 \
   --learning_rate=0.001 \
   --learning_rate_decay_type=fixed \
   --save_interval_secs=300 \
   --save_summaries_secs=10 \
   --log_every_n_steps=1 \
   --optimizer=rmsprop \
   --weight_decay=0.00004

对比只训练末端层的命令,只有一处发生了变化,即去掉了--trainable_scopes参数。原先的--trainable_scopes=InceptionV3/Logits, InceptionV3/AuxLogits表示只对末端层InceptionV3/Logits和InceptionV3/AuxLogits进行训练,去掉后就可以训练模型中的所有参数了。我们会在下面比较这两种训练方式的效果。

3.3.5 训练程序行为

当train_image_classifier.py程序启动后,如果训练文件夹(即satellite/train_dir)里没有已经保存的模型,就会加载checkpoint_path中的预训练模型,紧接着,程序会把初始模型保存到train_dir中,命名为model.ckpt-0, 0表示第0步。这之后,每隔5min(参数--save_interval_secs=300指定了每隔300s保存一次,即5min)。程序还会把当前模型保存到同样的文件夹中,命名格式和第一次保存的格式一样。因为模型比较大,程序只会保留最新的5个模型。

此外,如果中断了程序并再次运行,程序会首先检查train_dir中有无已经保存的模型,如果有,就不会去加载checkpoint_path中的预训练模型,而是直接加载train_dir中已经训练好的模型,并以此为起点进行训练。Slim之所以这样设计,是为了在微调网络的时候,可以方便地按阶段手动调整学习率等参数。

3.3.6 验证模型准确率

如何查看保存的模型在验证数据集上的准确率呢?可以用eval_image_classifier.py程序进行验证,即执行下列命令:

    python eval_image_classifier.py \
     --checkpoint_path=satellite/train_dir \
     --eval_dir=satellite/eval_dir \
     --dataset_name=satellite \
     --dataset_split_name=validation \
     --dataset_dir=satellite/data \
     --model_name=inception_v3

这里参数的含义为:

·--checkpoint_path=satellite/train_dir:这个参数既可以接收一个目录的路径,也可以接收一个文件的路径。如果接收的是一个目录的路径,如这里的satellite/train_dir,就会在这个目录中寻找最新保存的模型文件,执行验证。也可以指定一个模型进行验证,以第300步的模型为例,在satellite/train_dir文件夹下它被保存为model.ckpt-300.meta、model.ckpt-300.index、model.ckpt-300.data-00000-of-00001三个文件。此时,如果要对它执行验证,给checkpoint_path传递的参数应该为satellite/train_dir/model.ckpt-300。

·--eval_dir=satellite/eval_dir:执行结果的日志就保存在eval_dir中,同样可以通过TensorBoard查看。

·--dataset_name=satellite、--dataset_split_name=validation指定需要执行的数据集。注意此处是使用验证集(validation)执行验证。

·--dataset_dir=satellite/data:数据集保存的位置。

·--model_name=inception_v3:使用的模型。

执行后,应该会出现类似下面的结果:

    eval/Accuracy[0.51]
    eval/Recall_5[0.97333336]

Accuracy表示模型的分类准确率,而Recall_5表示Top 5的准确率,即在输出的各类别概率中,正确的类别只要落在前5个就算对。由于此处的类别数比较少,因此可以不执行Top 5的准确率,换而执行Top 2或者Top 3的准确率,只要在eval_image_classifier.py中修改下面的部分就可以了:

      names_to_values, names_to_updates=slim.metrics.aggregate_
    metric_map({
        'Accuracy': slim.metrics.streaming_accuracy(predictions, labels),
        'Recall_5': slim.metrics.streaming_recall_at_k(
            logits, labels, 5),
      })

3.3.7 TensorBoard可视化与超参数选择

在训练时,可以使用TensorBoard对训练过程进行可视化,这也有助于设定训练模型的方式及超参数。

使用下列命令可以打开TensorBoard(其实就是指定训练文件夹):

    tensorboard--logdir satellite/train_dir

在TensorBoard中,可以看到损失的变化曲线,如图3-1所示。观察损失曲线有助于调整参数。当损失曲线比较平缓,收敛较慢时,可以考虑增大学习率,以加快收敛速度;如果损失曲线波动较大,无法收敛,就可能是因为学习率过大,此时就可以尝试适当减小学习率。

图3-1 训练损失的变化情况

此外,使用TensorBoard,还可以对比不同模型的损失变化曲线。如在第3.3.4节中给出了两条命令,一条命令是只微调Inception V3末端层的,另外一条命令是微调整个网络的。可以在train_dir中建立两个文件夹,训练这两个模型时,通过调整train_dir参数,将它们的日志分别写到新建的文件夹中,此时再使用命令tensorboard--logdir satellite/train_dir打开TensorBoard,就可以比较这两个模型的变化曲线了。如图3-2所示,上方的曲线为只训练末端层的损失,下方的曲线为训练所有层的损失。仅看损失,训练所有层的效果应该比只训练末端层要好。事实也是如此,只训练末端层最后达到的分类准确率在76%左右,而训练所有层的分类准确率在82%左右。读者还可以进一步调整训练变量、学习率等参数,以达到更好的效果。

图3-2 在TensorBoard中对比两种训练方式的损失

3.3.8 导出模型并对单张图片进行识别

训练完模型后,常见的应用场景是:部署训练好的模型并对单张图片做识别。这里提供了两个代码文件:freeze_graph.py和classify_image_inception_v3.py。前者可以导出一个用于识别的模型,后者则是使用inception_v3模型对单张图片做识别的脚本。

TensorFlow Slim为提供了导出网络结构的脚本export_inference_graph.py。首先在slim文件夹下运行:

    python export_inference_graph.py \
   --alsologtostderr \
   --model_name=inception_v3 \
   --output_file=satellite/inception_v3_inf_graph.pb \
   --dataset_name satellite

这个命令会在satellite文件夹中生成一个inception_v3_inf_graph.pb文件。注意:inception_v3_inf_graph.pb文件中只保存了Inception V3的网络结构,并不包含训练得到的模型参数,需要将checkpoint中的模型参数保存进来。方法是使用freeze_graph.py脚本(在chapter_3文件夹下运行):

    python freeze_graph.py \
   --input_graph slim/satellite/inception_v3_inf_graph.pb \
   --input_checkpoint slim/satellite/train_dir/model.ckpt-5271 \
   --input_binary true \
   --output_node_names InceptionV3/Predictions/Reshape_1 \
   --output_graph slim/satellite/frozen_graph.pb

这里参数的含义为:

·--input_graph slim/satellite/inception_v3_inf_graph.pb。这个参数很好理解,它表示使用的网络结构文件,即之前已经导出的inception_v3_inf_graph.pb。

·--input_checkpoint slim/satellite/train_dir/model.ckpt-5271。具体将哪一个checkpoint的参数载入到网络结构中。这里使用的是训练文件夹train_dir中的第5271步模型文件。读者需要根据训练文件夹下checkpoint的实际步数,将5271修改成对应的数值。

·--input_binary true。导入的inception_v3_inf_graph.pb实际是一个protobuf文件。而protobuf文件有两种保存格式,一种是文本形式,一种是二进制形式。inception_v3_inf_graph.pb是二进制形式,所以对应的参数是--input_binary true。初学的话对此可以不用深究,若有兴趣的话可以参考资料https://www.tensorflow.org/extend/tool_developers/。

·--output_node_names InceptionV3/Predictions/Reshape_1。在导出的模型中,指定一个输出结点,InceptionV3/Predictions/Reshape_1是Inception V3最后的输出层。

·--output_graph slim/satellite/frozen_graph.pb。最后导出的模型保存为slim/satellite/ frozen_graph.pb文件。

如何使用导出的frozen_graph.pb来对单张图片进行预测?编写了一个classify_image_inception_v3.py脚本来完成这件事。先来看这个脚本的使用方法:

    python classify_image_inception_v3.py \
   --model_path slim/satellite/frozen_graph.pb \
   --label_path data_prepare/pic/label.txt \
   --image_file test_image.jpg

--model_path很好理解,就是之前导出的模型frozen_graph.pb。模型的输出实际是“第0类”、“第1类”……所以用--label_path指定了一个label文件,label文件中按顺序存储了各个类别的名称,这样脚本就可以把类别的id号转换为实际的类别名。--image_file是需要测试的单张图片。脚本的运行结果应该类似于:

    water (score=5.46853)
    wetland (score=5.18641)
    urban (score=1.57151)
    wood (score=-1.80627)
    glacier (score=-3.88450)

这就表示模型预测图片对应的最可能的类别是water,接着是wetland、urban、wood等。score是各个类别对应的Logit。

最后来看classify_image_inception_v3.py的实现方式。代码中包含一个preprocess_for_eval函数,它实际上是从slim/preprocessing/inception_preprocessing.py里复制而来的,用途是对输入的图片做预处理。classify_image_inception_v3.py的主要逻辑在run_inference_on_image函数中,第一步就是读取图片,并用preprocess_for_eval做预处理:

    with tf.Graph().as_default():
      image_data=tf.gfile.FastGFile(image, 'rb').read()
      image_data=tf.image.decode_jpeg(image_data)
      image_data=preprocess_for_eval(image_data, 299, 299)
      image_data=tf.expand_dims(image_data, 0)
      with tf.Session() as sess:
        image_data=sess.run(image_data)

Inception V3的默认输入为299 * 299,所以调用preprocess_for_eval时指定了宽和高都是299。接着调用create_graph()将模型载入到默认的计算图中:

    def create_graph():
      with tf.gfile.FastGFile(FLAGS.model_path, 'rb') as f:
      graph_def=tf.GraphDef()
      graph_def.ParseFromString(f.read())
     _=tf.import_graph_def(graph_def, name='')

FLAGS.model_path就是保存的slim/satellite/frozen_graph.pb。将之读入后先转换为graph_def,然后用tf.import_graph_def()函数导入。导入后,就可以创建Session并测试图片了,对应的代码为:

    with tf.Session() as sess:
      softmax_tensor=\
    sess.graph.get_tensor_by_name('InceptionV3/Logits/SpatialSqueeze: 0')
      predictions=sess.run(softmax_tensor,
                      {'input:0': image_data})
      predictions=np.squeeze(predictions)

      # Creates node ID--> English string lookup.
      node_lookup=NodeLookup(FLAGS.label_path)

      top_k=predictions.argsort()[-FLAGS.num_top_predictions:][::-1]
      for node_id in top_k:
      human_string=node_lookup.id_to_string(node_id)
      score=predictions[node_id]
      print('%s (score=%.5f)' % (human_string, score))

InceptionV3/Logits/SpatialSqueeze:0是各个类别Logit值对应的结点。输入预处理后的图片image_data,使用sess.run()函数取出各个类别预测Logit。默认只取最有可能的FLAGS.num_top_predictions个类别输出,这个值默认是5。可以在运行脚本时用--num_top_predictions参数来改变此默认值。node_lookup定义了一个NodeLookup类,它会读取label文件,并将模型输出的类别id转换成实际类别名,实现代码比较简单,就不再详细介绍了。