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文件,将该文件复制到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。初学的话对此可以不用深究,若有兴趣的话可以参考资料。
·--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转换成实际类别名,实现代码比较简单,就不再详细介绍了。