前面的教程都只在小模型、小数据库上进行了演示,这次来真正实战一个大型数据库ImageNet。教程会分为三部分:数据增强、模型加载与训练、模型测试,最终在ResNet50上可以达到77.72%的top-1准确率,复现出了ResNet原文的结果。
完整的代码可以在我的github上找到。https://github.com/Apm5/ImageNet_Tensorflow2.0
提供ResNet-18和ResNet-50的预训练模型,以供大家做迁移使用。
链接:https://pan.baidu.com/s/1nwvkt3Ei5Hp5Pis35cBSmA
提取码:y4wo
还提供百度云链接的ImageNet原始数据,但是这份资源只能创建临时链接以供下载,有需要的还请私信联系。下面开始正文。
数据增强
本文着重于数据增强的代码实现,内容的学习可以参考CVPR2019上的一篇文章Bag of Tricks for Image Classification with Convolutional Neural Networks。文章中提到在训练时图像依次进行:
- 随机选取一张图片并转为32位浮点数
- 随机选取一个长宽比在3:4到4:3之间的区域,区域面积占完整图像的8%到100%,然后将选取的区域插值为224*224大小
- 50%的概率进行左右翻转
- 图像在hsv空间随机抖动,抖动幅度采样于(0.6,1.4)的均匀分布
- 为图像添加PCA噪声,噪声系数采样于(0,0.1)的正态分布
- 将图像归一化到(0,1)分布
在代码的实现中,实际的处理顺序与参数选取与论文稍有不同,整体流程为:
image = random_aspect(image)
image = random_size(image)
image = random_crop(image)
image = random_flip(image)
image = random_hsv(image)
image = random_pca(image)
下面一一解析每个增强函数。
随机长宽比
def random_aspect(image):
height, width, _ = np.shape(image)
aspect_ratio = np.random.uniform(*c.aspect_ratio_scale)
if height < width:
resize_shape = (int(width * aspect_ratio), height)
else:
resize_shape = (width, int(height * aspect_ratio))
return cv2.resize(image, resize_shape)
裁剪随机长宽比的区域,等价于先对原始图像的长宽比进行调整,然后裁剪正方形的区域。长宽比设置为aspect_ratio_scale = (0.8, 1.25)
,即4:5到5:4之间。
随机尺度
def random_size(image, target_size=None):
height, width, _ = np.shape(image)
if target_size is None:
# for test
# target size is fixed
target_size = np.random.randint(*c.short_side_scale)
if height < width:
size_ratio = target_size / height
else:
size_ratio = target_size / width
resize_shape = (int(width * size_ratio), int(height * size_ratio)) # width and height in cv2 are opposite to np.shape()
return cv2.resize(image, resize_shape)
裁剪随机面积占比的区域再插值到224224大小,等价于先随机放缩原始图像,再裁剪224224大小的区域。实现上保持长宽比,让图像的短边长度在256到384之间,即short_side_scale = (256, 384)
。
随机裁剪
def random_crop(image):
height, width, _ = np.shape(image)
input_height, input_width, _ = c.input_shape
crop_x = np.random.randint(0, width - input_width)
crop_y = np.random.randint(0, height - input_height)
return image[crop_y: crop_y + input_height, crop_x: crop_x + input_width, :]
有了上述两个随机长宽比和随机尺度的函数,随机裁剪只需要随机选取224*224大小的区域位置即可。
随机翻转
def random_flip(image):
if np.random.rand() < 0.5:
image = cv2.flip(image, 1)
return image
对于ImageNet这类日常图像来说,上下翻转是没有意义的,因此只以50%的概率进行左右翻转
hsv空间随机抖动
def random_hsv(image):
random_h = np.random.uniform(*c.hue_delta)
random_s = np.random.uniform(*c.saturation_scale)
random_v = np.random.uniform(*c.brightness_scale)
image_hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
image_hsv[:, :, 0] = image_hsv[:, :, 0] + random_h % 360.0 # hue
image_hsv[:, :, 1] = np.minimum(image_hsv[:, :, 1] * random_s, 1.0) # saturation
image_hsv[:, :, 2] = np.minimum(image_hsv[:, :, 2] * random_v, 255.0) # brightness
return cv2.cvtColor(image_hsv, cv2.COLOR_HSV2BGR)
hsv空间即图像的色调(hue)、对比度(saturation)和明度(brightness),随机抖动时需要注意各维度取值上下界以避免转回RGB空间时发生溢出。
随机pca噪声
def random_pca(image):
alpha = np.random.normal(0, c.pca_std, size=(3,))
offset = np.dot(c.eigvec * alpha, c.eigval)
image = image + offset
return np.maximum(np.minimum(image, 255.0), 0.0)
需要事先对训练集数据进行主成分分析,然后再在训练时添加随机大小的主成分噪声。
至此,图像增强便已完成,最后再将图像归一化到标准正态分布,作为网络的输入。
def normalize(image):
for i in range(3):
image[..., i] = (image[..., i] - c.mean[i]) / c.std[i]
return image
在实际训练中,数据增强能在不改变模型本身的情况下大幅提高网络性能,但过多的增强(如极端的长宽比变化、尺度放缩)也会使网络展现出欠拟合的结果。
上述代码位于项目中的utils/aug_utils.py