出品 | 磐创AI团队
CIFAR-10项目
Alex Krizhevsky,Vinod Nair和Geoffrey Hinton收集了8000万个小尺寸图像数据集,CIFAR-10和CIFAR-100分别是这个数据集的一个子集(http://www.cs.toronto.edu/~kriz/cifar.html)。CIFAR-10数据集由10个类别共60000张彩色图片组成,其中每张图片的大小为32X32,每个类别分别6000张。
图1 cifar-10数据集中部分样本可视化
我们首先下载CIFAR-10数据集(http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz),解压之后如图2所示。其中“data_batch_1”至“data_batch_5”是训练文件,每个文件分别有10000个训练样本,共计50000个训练样本,“test_batch”是测试文件,包含了10000个测试样本。
图2 CIFAR-10数据集文件
- 数据预处理
我们先导入需要用到的包:
代码语言:javascript复制1 import tensorflow as tf
2 import numpy as np
3 import pickle
4 import os
由于这些数据文件是使用“cPickle”进行存储的,因此我们需要定义一个函数来加载这些数据文件:
代码语言:javascript复制5 def get_pickled_data(data_path):
6 data_x = []
7 data_y = []
8 with open(data_path, mode='rb') as file:
9 data = pickle.load(file, encoding='bytes')
10 x = data[b'data']
11 y = data[b'labels']
12 # 将3*32*32的数组变换为32*32*3
13 x = x.reshape(10000, 3, 32, 32)
14 .transpose(0, 2, 3, 1).astype('float')
15 y = np.array(y)
16 data_x.extend(x)
17 data_y.extend(y)
18 return data_x, data_y
接下来我们定义一个“prepare_data”函数用来获取训练和测试数据:
代码语言:javascript复制19 def prepare_data(path):
20 x_train = []
21 y_train = []
22 x_test = []
23 y_test = []
24 for i in range(5):
25 # train_data_path为训练数据的路径
26 train_data_path = os.path.join(path, ('data_batch_' str(i 1)))
27 data_x, data_y = get_pickled_data(train_data_path)
28 x_train = data_x
29 y_train = data_y
30 # 将50000个list型的数据样本转换为ndarray型
31 x_train = np.array(x_train)
32
33 # test_data_path为测试文件的路径
34 test_data_path = os.path.join(path, 'test_batch')
35 x_test, y_test = get_pickled_data(test_data_path)
36 x_test = np.array(x_test)
37
38 return x_train, y_train, x_test, y_test
- 模型搭建
在这个项目里我们将使用RasNet模型,RasNet我们简单的介绍过,它是一个残差网络,一定程度上解决了网络过深后出现的退化问题(论文地址:https://arxiv.org/abs/1512.03385)。ResNet的基本结构是如图3所示的“残差块(residual block)”,右侧是针对50层以上网络的优化结构。
图3 残差块(residual block)
图4所示是一个34层的ResNet的网络结构,ResNet的提出者以VGG-19模型(图4左)为参考,设计了一个34层的网络(图4中),并进一步构造了34层的ResNet(图4右),34层是按有参数更新的层来计算的,图4所示的34层ResNet中有参数更新的层包括第1层卷积层,中间残差部分的32个卷积层,以及最后的一个全连接层。
如图4所示,ResNet中主要使用的是3X3的卷积核,并遵守着两个简单的设计原则:(1)对于每一层卷积层,如果输出的特征图尺寸相同,那么这些层就使用相同数量的滤波器;(2)如果输出的特征图尺寸减半了,那么卷积核的数量加增加一倍,以便保持每一层的时间复杂度。
ResNet的第一层是66个7X7的卷积核,滑动步长为2;接着是一个步长为2的池化层;再接着是16个残差块,共32个卷积层,根据卷积层中卷积核数量的不同可以分为4个部分,每个部分的衔接处特征图的尺寸都缩小了一半,因此卷积核的数量也相应地增加了一倍;残差部分之后是一个池化层,采用平均池化;最后是一个全连接层,并用softmax作为激活函数,得到分类结果。
图4 ResNet34的网络结构
接下来我们先定义残差块:
代码语言:javascript复制1 class residual_lock(tf.keras.layers.Layer):
2 def __init__(self, filters, strides=1):
3 super(residual_lock, self).__init__()
4 self.conv1 = tf.keras.layers.Conv2D(filters=filters,
5 kernel_size=(3, 3),
6 strides=strides,
7 padding="same")
8 # 规范化层:加速收敛,控制过拟合
9 self.bn1 = tf.keras.layers.BatchNormalization()
10 self.conv2 = tf.keras.layers.Conv2D(filters=filters,
11 kernel_size=(3, 3),
12 strides=1,
13 padding="same")
14 # 规范化层:加速收敛,控制过拟合
15 self.bn2 = tf.keras.layers.BatchNormalization()
16 # 残差块的第一个卷积层中,卷积核的滑动步长为2时,输出特征图大小减半,
17 # 需要对残差块的输入使用步长为2的卷积来进行下采样,从而匹配维度
18 if strides != 1:
19 self.downsample = tf.keras.Sequential()
20 self.downsample.add(tf.keras.layers.Conv2D(filters=filters, kernel_size=(1, 1), strides=strides))
21 self.downsample.add(tf.keras.layers.BatchNormalization())
22 else:
23 self.downsample = lambda x: x
24
25 def call(self, inputs, training=None):
26 # 匹配维度
27 identity = self.downsample(inputs)
28
29 conv1 = self.conv1(inputs)
30 bn1 = self.bn1(conv1)
31 relu = tf.nn.relu(bn1)
32 conv2 = self.conv2(relu)
33 bn2 = self.bn2(conv2)
34
35 output = tf.nn.relu(tf.keras.layers.add([identity, bn2]))
36
37 return output
接着我们定义一个函数用来组合残差块:
代码语言:javascript复制38 def build_blocks(filters, blocks, strides=1):
39 """组合相同特征图大小的残差块"""
40 res_block = tf.keras.Sequential()
41 # 添加第一个残差块,每部分的第一个残差块的第一个卷积层,其滑动步长为2
42 res_block.add(residual_lock(filters, strides=strides))
43
44 # 添加后续残差块
45 for _ in range(1, blocks):
46 res_block.add(residual_lock(filters, strides=1))
47
48 return res_block
定义好残差块和组合组合残差块的函数后,我们就可以实现具体的ResNet模型了:
代码语言:javascript复制49 class ResNet(tf.keras.Model):
50 """ResNet模型"""
51 def __init__(self, num_classes=10):
52 super(ResNet, self).__init__()
53
54 self.preprocess = tf.keras.Sequential([
55 tf.keras.layers.Conv2D(filters=64,
56 kernel_size=(7, 7),
57 strides=2,
58 padding='same'),
59 # 规范化层:加速收敛,控制过拟合
60 tf.keras.layers.BatchNormalization(),
61 tf.keras.layers.Activation(tf.keras.activations.relu),
62 # 最大池化:池化操作后,特征图大小减半
63 tf.keras.layers.MaxPool2D(pool_size=(3, 3),strides=2)
64 ])
65
66 # 组合四个部分的残差块
67 self.blocks_1 = build_blocks(filters=64, blocks=3)
68 self.blocks_2 = build_blocks(filters=128, blocks=4,strides=2)
69 self.blocks_3 = build_blocks(filters=256, blocks=6, strides=2)
70 self.blocks_4 = build_blocks(filters=512, blocks=3, strides=2)
71
72 # 平均池化
73 self.avg_pool = tf.keras.layers.GlobalAveragePooling2D()
74 # 最后的全连接层,使用softmax作为激活函数
75 self.fc=tf.keras.layers.Dense(units=num_classes,activation=tf.keras.activations.softmax)
76
77 def call(self, inputs, training=None):
78 preprocess = self.preprocess(inputs)
79 blocks_1 = self.blocks_1(preprocess)
80 blocks2 = self.blocks_2(blocks_1)
81 blocks3 = self.blocks_3(blocks2)
82 blocks4 = self.blocks_4(blocks3)
83 avg_pool = self.avg_pool(blocks4)
84 out = self.fc(avg_pool)
85
86 return out
这里ResNet模型的实现完全依照图4中34层的ResNet模型结构。
- 模型训练
最后我们实现模型的训练部分:
代码语言:javascript复制87 if __name__ == '__main__':
88 model = ResNet()
89 model.build(input_shape=(None, 32, 32, 3))
90 model.summary()
91
92 # 数据集路径
93 path = "./cifar-10-batches-py"
94
95 # 数据载入
96 x_train, y_train, x_test, y_test = prepare_data(path)
97 # 将类标进行one-hot编码
98 y_train = tf.keras.utils.to_categorical(y_train, 10)
99 y_test = tf.keras.utils.to_categorical(y_test, 10)
100
101 model.compile(loss='categorical_crossentropy',
102 optimizer=tf.keras.optimizers.Adam(),
103 metrics=['accuracy'])
104
105 # 动态设置学习率
106 lr_reducer = tf.keras.callbacks.ReduceLROnPlateau(
107 monitor='val_accuracy',
108 factor=0.2, patience=5,
109 min_lr=0.5e-6)
110 callbacks = [lr_reducer]
111
112 # 训练模型
113 model.fit(x_train, y_train,
114 batch_size=50, epochs=20,
115 verbose=1, callbacks=callbacks,
116 validation_data=(x_test, y_test),
117 shuffle=True)
在第106行代码中我们设置了动态学习率,并通过“callbacks”传递给模型。“tf.keras.callbacks.ReduceLROnPlateau”函数可以用来动态调整学习率,参数“monitor”是我们要监测的指标,“factor”是调整学习率时的参数(新的学习率=旧的学习率*factor),“patience”个回合后如果“monitor”指定的指标没有变化,则对学习率进行调整,“min_lr”限定了学习率的下限。
训练过程的Accuracy和Loss的变化如下:
图5 ResNet34训练过程中Accuracy和Loss的变化
(橙色为训练集,蓝色为验证集)
最终在验证集上的准确率为76.12%,有过拟合的现象,准确率还有提升的空间。有兴趣进一步提升分类效果的读者可以尝试如下方法:
1) 数据集增强:通过旋转、平移等操作来扩充数据集;
2) 参数微调:包括训练的回合数、学习率等;
3) 修改模型:可以尝试在ResNet32的基础上修改模型的结构,或者替换其它网络模型;