本文首发于个人博客https://blog.chyelang.ml/image_classification/,欢迎关注
利用TensorFlow(TF),本项目实现了一个基于full convolution stack、inception v2 module等模块的图片分类网络,纵向共包含10层包含参数的层,采用自己搭建的卷积层。对于dset1与dset2两个数据集采用同样的网络结构,分别进行训练。最终在dset1验证集(约含900张图,下同)上的Top1分类准确率约为0.52,在dset2验证集上的Top1准确率约为0.63。在K80显卡的单核上,该模型的训练速度约为205张图每秒,最终所得模型的checkpoint约为45MB。本报告将从模型搭建、模型训练、测试方法等方面对本项目的工作进行详细说明。
本项目代码仓库见于:https://github.com/chyelang/hw2_image_classification 。项目所有代码以及训练好的模型存放于hw2_image_classification_handed文件夹中。参考借鉴TensorFlow的利用CNN给CIFAR-10数据集分类的官方教程1,本项目代码的组织结构如下表所示:
代码文件 | 功能 |
---|---|
hw2_dataset.py | 数据集预处理:将给定的数据集随机切分成训练集和验证集 |
augmentation.py | 图片数据增广:包含缩放、旋转、镜像、偏移、加噪声等函数 |
hw2_input.py | 基于TF的QueueRunner,载入训练和测试所需的batch |
layers.py | 模型中子模块的实现。包括full conv stack, inception_v1_module, inception_v2_module, 以及卷积层的实现及测试函数。 |
hw2.py | 实现了模型图生成所用到的train、loss等函数 |
hw2_train.py | CPU或单GPU训练的主程序 |
hw2_multi_gpu_train.py | 多GPU训练的主程序 |
hw2_eval.py | 进行模型评估的主程序 |
utils.py | 新建模型参数的一些辅助函数 |
config.cfg | 模型配置文件。可以根据代码运行机器(本地或服务器)分别配置不同的运行参数 |
模型计算图的构建由hw2.inference()完成。参考VGG和Inception V2的设计,本项目模型设计的主要思路如下:
基于以上考虑,并经过不断的试验,最终设计了如下表所示的十层网络。
名称 | 输入维度 | 输出维度 | 描述 |
---|---|---|---|
conv_stack1/conv1 | 100x100x3 | 100x100x64 | 采用ReLU激活、带批正则化(BN)的卷积层 |
conv_stack1/conv2 | 100x100x64 | 50x50x128 | 采用ReLU激活、带批正则化(BN)的卷积层 |
pool1 | 50x50x128 | 25x25x128 | 最大池化层 |
inception2 | 25x25x128 | 25x25x320 | 输出带BN的inception v2 module |
pool2 | 25x25x320 | 13x13x320 | 最大池化层 |
dropout2 | 13x13x320 | 13x13x320 | spatial dropout |
inception3 | 13x13x320 | 13x13x192 | 输出带BN的inception v2 module |
pool3 | 13x13x192 | 7x7x192 | 最大池化层 |
dropout3 | 7x7x192 | 7x7x192 | spatial dropout |
dense1 | 7x7x192 | 256 | 采用ReLU激活、带L2正则化、含BN与dropout的全连接层 |
softmax_linear | 256 | 65 | softmax分类器 |
上表中所列举的conv_stack, inception v2 module均在layers.py中实现。其中inception v2 module具体结构如下图所示。由于采取了的padding方法为same,该模块不会改变输入的WxH,但可能改变其channel数。表中所用的inception层参数如下:
在layers.py中实现了conv2d_func(input, filter, strides, padding='SAME')及其测试函数conv2d_test()。conv2d_func()与tensorflow.python.ops.gen_nn_ops.conv2d()等价,通过测试可发现对于相同输入,两者所得运算结果相同。但在输入数据维度很大时存在一定小误差,该误差在可接受范围内。对于实际训练过程,发现conv2d_func的使用使得训练速度将为原来的一半左右,而训练精度则基本相同。
conv2d_func的实现借鉴了caffe中卷积层的实现。其主要分为两步,首先调用tf.extract_image_patches()得到卷积核在每一步卷积过程中需要做element-wise multiplication的对象,再把这些运算对象和卷积核都reshape成矩阵,调用tf.matmul()进行相乘,最后将结果reshape成相应的维度进行输出。
原始数据集被按照4:1的关系随机分成训练集与验证集。模型的数据读入由CPU完成。hw2.distorted_inputs()负责生成训练所需的batch,hw2. inputs()负责生成测试所需的batch。基本过程如下:程序首先从给定的数据集文件夹中读取所有图片的文件名称及其标签,利用tf.train.slice_input_producer()将其加入到TF的QueueRunner中,从该队列中读取图片,进行图片预处理后,将其加入到tf.train.shuffle_batch()(训练时)或者tf.train.batch()(测试时)所形成的QueueRunner中,训练过程中hw2.train()则可以按需从队列中读训练数据到GPU显存中。对于本次项目的可以一次性全部加载到内存中小数据量而言,这种惰性读取的方法稍显复杂,但对于一般意义上数据集动辄几十上百GB的图片任务来说,这个做法是非常有必要的。
图片数据预处理的过程即为图片数据增广的过程。通过试验可发现数据增广并不是越多越好,关键是要看测试集的数据分布选择合适的增广方法。本项目训练过程中,先将读入的图片保持比例地将短边缩放到260像素,再随机crop出200x200的方框图,经过augmentation.image_augmentation()的随机左右镜像、随机亮度调整、随机对比度调整后,将图片缩放至100x100(输入两倍大小的图片进行数据增广是为了减少某些增广操作带来的图像失真),再调用tf.image.per_image_standardization()进行归一化处理后得到最终待输入的图片。以下三张图从左之后分别展示了原始图片、待数据增广图片和待输入图片。在测试过程中,图片首先仍然保持比例地将短边缩放到260像素,但随后central crop出260x260的图像,再缩放成100x100输入到模型中。
可通过运行hw2_train.py或者hw2_multi_gpu_train.py以在单个GPU或者多个GPU上对模型进行训练,其中hw2_train.py也可用于借助CPU的训练。为实现多GPU训练,模型所有的参数都存放在CPU上,训练时数据batch会送到GPU中,由每个GPU分别计算该batch的梯度,送回到CPU计算平均梯度后进行参数更新,再将新参数送到GPU中进行下一轮计算。该训练方式与采用同步方式进行更新的参数服务器是一样的。实验表明,采用K80的双核心进行训练比采用单核心进行训练的速度要快一倍左右,这表明多GPU、分布式的训练方法在数据量庞大的情况下是极为有用的。
模型的训练采用tf.train.MonitoredTrainingSession(),定制化地实现了_LoggerHook、_EarlyStoppingHook、ckpt_hook等hook,挂到session中以对训练过程进行日志显示、提早结束、模型保存等操作。通过feed_dict传入模型中三个dropout层的keep_prob参数,从上游到下游分别为0.75,0.75,0.5。将上游的keep_prob设置得大一些有助于避免信息损失过多导致模型训练缓慢甚至完全不能收敛的问题。
模型每个batch的大小设置为128,采用tf.train.AdamOptimizer()进行优化,初始学习率lr设置为默认的0.001,通过_EarlyStoppingHook,如果检测到模型的验证集准确率在连续7*FLAGS.save_checkpoint_steps个batch内都没有得到提升,则AdamOptimizer的初始学习率会将为原来的一半。右图即展现了训练过程中初始学习率的变化过程。如果连续15*FLAGS.save_checkpoint_steps个batch内验证集准确率都没有提升,则模型会提前结束训练,然后取所保存的最好模型作为最终模型。模型参数的更新借助tf.train.ExponentialMovingAverage(),采用滑动平均的方式进行更新,这有助于提高模型的鲁棒性。
以下两图展示了对于dset1和dset2在训练过程中train Top1 acc和val Top1 acc的变化曲线。可以看出由于采用了BN、AdamOptimizer等技术,模型初始的收敛很快。由于dropout的加入,模型的训练集准确率在波动中缓慢上升,但后期验证集准确率提升程度已经比较有限。模型最终经过约10000个batch(即约10000*128/3200=400个epoch)仍然存在一定的过拟合问题。模型最终效果如下:
validation accurary (be done for about 900 samples in validation set):
具体说明如下:
The hw2_eval.py will fetch randomly --num_examples images in --test_data_path using tf.train.batch() for a single test and return the --top_k error, this procedure will repeat if --run_once is set 0 (and don’t repeat if it’s set to 1).
Attention: it’s recommended to set --num_examples as big as your test set to get a consistent test result!
举例来说,对于dset1的测试可运行以下命令:
python hw2_eval.py --section ecm --eval_dir ./eval_dir --checkpoint_dir ./saved_model/train_log_dset1_handed
–test_data_path /scratch/xzou/hw2_image_classification/modified_data/dset1/test --num_examples 900 --top_k 1 --run_once 0
经过一系列的模型结构优化、参数调试,所提模型在dset1上的Top1 acc由最初的0.16左右提高到了最终的0.53左右,模型的规模由最大的900MB左右降低到了最终的45MB,层数由最多时的22层降低到了最终的10层。现将相关经验总结如下: