大家休息了一会儿,回到课堂上继续讨论如何优化这个卷积网络的性能。

一个同学率先发言:“是不是像上一课讲的归一化一样,即特征缩放方面的问题?”

“非常好!”咖哥回答,“你倒是记住了上一课的要点。放入神经网络中的数据的确需要归一化,然而这件事我们已经做了—图像数据张量的值已经压缩至[0,1]区间。”

“是激活函数的关系?”另一个同学发言。咖哥说:“使用ReLU函数进行卷积网络的激活,基本上是没有大问题的。当然,你们也可以试一试其他的激活函数,如eLU等。而后面的Softmax函数对于多分类问题的激活是标配。因此,也没有问题。”

小冰突然喊道:“那个Drop……解决过拟合的……”

“嗯,Dropout,”咖哥终于点头,“这倒是值得一试。实践是检验真理的唯一标准,我们来动手试几招。”

6.6.1 第一招:更新优化器并设置学习速率

从最简单的修改开始,暂时不改变网络结构,先考虑一下优化器的调整,并尝试使用不同的学习速率进行梯度下降。因为很多时候神经网络完全没有训练起来,是学习速率设定得 不好。

示例代码如下:

from keras import optimizers # 导入优化器

cnn = models.Sequential() # 贯序模型

cnn.add(layers.Conv2D(32, (3, 3), activation='relu', # 卷积层

input_shape=(150, 150, 3)))

cnn.add(layers.MaxPooling2D((2, 2))) # 最大池化层

cnn.add(layers.Conv2D(64, (3, 3), activation='relu')) # 卷积层

cnn.add(layers.MaxPooling2D((2, 2))) # 最大池化层

cnn.add(layers.Conv2D(128, (3, 3), activation='relu')) # 卷积层

cnn.add(layers.MaxPooling2D((2, 2))) # 最大池化层

cnn.add(layers.Conv2D(256, (3, 3), activation='relu')) # 卷积层

cnn.add(layers.MaxPooling2D((2, 2))) # 最大池化层

cnn.add(layers.Flatten()) # 展平层

cnn.add(layers.Dense(512, activation='relu')) # 全连接层

cnn.add(layers.Dense(10, activation='sigmoid')) # 分类输出

cnn.compile(loss='categorical_crossentropy', # 损失函数

optimizer=optimizers.Adam(lr=1e-4), # 更新优化器并设定学习速率

metrics=['acc']) # 评估指标

history = cnn.fit(X_train, y_train, # 指定训练集

epochs=50,   # 指定轮次

batch_size=256, # 指定批量大小

validation_data=(X_test, y_test)) # 指定验证集

输出结果如下:

Train on 1537 samples, validate on 385 samples

Epoch 1/50

1537/1537 [==============================] - 2s 1ms/step - loss: 2.2900 - acc: 0.1386

- val_loss: 2.2611 - val_acc: 0.1714

Epoch 2/50 1537/1537 [==============================] - 1s 876us/step - loss: 2.2068 - acc:

0.2088 - val_loss: 2.1376 - val_acc: 0.2753

… …

Epoch 50/50

1537/1537 [==============================] - 2s 1ms/step - loss: 0.0190 - acc: 0.9967

- val_loss: 4.4028 - val_acc: 0.3896

更换了优化器,并设定了学习速率之后,再次训练网络,发现准确率有了很大的提升,最后达到了40%左右,提高了近一倍。不过,从损失曲线上看(如下图所示),20轮之后,验证集的损失突然飙升了。这是比较典型的过拟合现象。

训练集和验证集上的损失曲线和准确率曲线(第一次调优)

6.6.2 第二招:添加Dropout层

“那么下一步怎么办呢?”咖哥说,“这时可以考虑一下小冰刚才说的Dropout层,降低过拟合风险。”

示例代码如下:

cnn = models.Sequential() # 序贯模型

cnn.add(layers.Conv2D(32, (3, 3), activation='relu', # 卷积层

input_shape=(150, 150, 3)))

cnn.add(layers.Max Pooling2D((2, 2))) # 最大池化层

cnn.add(layers.Conv2D(64, (3, 3), activation='relu')) # 卷积层

cnn.add(layers.Dropout(0.5)) # Dropout层

cnn.add(layers.Max Pooling2D((2, 2))) # 最大池化层

cnn.add(layers.Conv2D(128, (3, 3), activation='relu')) # 卷积层

cnn.add(layers.Dropout(0.5)) # Dropout层

cnn.add(layers.Max Pooling2D((2, 2))) # 最大池化层

cnn.add(layers.Conv2D(256, (3, 3), activation='relu')) # 卷积层

cnn.add(layers.Max Pooling2D((2, 2))) # 最大池化层

cnn.add(layers.Flatten()) # 展平层

cnn.add(layers.Dropout(0.5)) # Dropout

cnn.add(layers.Dense(512, activation='relu')) # 全连接层

cnn.add(layers.Dense(10, activation='sigmoid')) # 分类输出

cnn.compile(loss='categorical_crossentropy', # 损失函数

optimizer=optimizers.Adam(lr=1e-4), # 更新优化器并设定学习速率

metrics=['acc']) # 评估指标

history = cnn.fit(X_train, y_train, # 指定训练集

epochs=50,   # 指定轮次

batch_size=256, # 指定批量大小

validation_data=(X_test, y_test)) # 指定验证集

输出结果如下:

Train on 1537 samples, validate on 385 samples

Epoch 1/50

1537/1537 [==============================] - 2s 1ms/step - loss: 2.2998 - acc: 0.1496

- val_loss: 2.2810 - val_acc: 0.2416

Epoch 2/50

1537/1537 [==============================] - 2s 987us/step - loss: 2.1917 - acc:

0.2219 - val_loss: 2.2217 - val_acc: 0.2416

… …

Epoch 50/50

1537/1537 [==============================] - 2s 1ms/step - loss: 0.0190 - acc: 0.9967

- val_loss: 4.4028 - val_acc: 0.3896

添加了Dropout层防止过拟合之后,损失曲线显得更平滑了(如下图所示),不再出现在验证集上飙升的现象。但是准确率的提升不大,还是40%左右。而且训练集和验证集之间的准确率,仍然是天壤之别。

训练集和验证集上的损失曲线和准确率曲线(第二次调优)

6.6.3 “大杀器”:进行数据增强

大家看到各种调试还是取得了一些效果,于是继续献计。有的同学提议像搭积木一样再多加几层,看看增加网络的深度是否会进一步提高性能。

咖哥说:“先别试了,等会儿你们可以自己慢慢调试。现在我要给大家介绍一个提高卷积网络图像处理问题的性能的‘大杀器’,名字叫作数据增强(data augmentation)。这种方法肯定能够进一步提高计算机视觉问题的准确率,同时降低过拟合。”

同学们听到还有这么神奇的方法后,纷纷集中注意力。咖哥说:“机器学习,数据量是多多益善的。数据增强,能把一张图像当成7张、8张甚至10张、100张来用,也就是从现有的样本中生成更多的训练数据。”

怎么做到的?是通过对图像的平移、颠倒、倾斜、虚化、增加噪声等多种手段。这是利用能够生成可信图像的随机变换来增加样本数,如下图所示。这样,训练集就被大幅地增强了,无论是图像的数目,还是多样性。因此,模型在训练后能够观察到数据的更多内容,从而具有更好的准确率和泛化能力。

针对同一张狗狗图像的数据增强:一张变多张

在Keras中,可以用Image Data- Generator工具来定义一个数据增强器:

# 定义一个数据增强器, 并设定各种增强选项

from keras.preprocessing.image import ImageDataGenerator

augs_gen = ImageDataGenerator(

featurewise_center=False,

samplewise_center=False,

featurewise_std_normalization=False,

samplewise_std_normalization=False,

zca_whitening=False,

rotation_range=10,

zoom_range = 0.1,

width_shift_range=0.2,

height_shift_range=0.2,

horizontal_flip=True,

vertical_flip=False)

augs_gen.fit(X_train) # 针对训练集拟合数据增强器

网络还是用回相同的网络,唯一的区别是在训练时,需要通过fit_generator方法动态生成被增强后的训练集:

history = cnn.fit_generator( # 使用fit_generator

augs_gen.flow(X_train, y_train, batch_size=16), # 增强后的训练集

validation_data = (X_test, y_test), # 指定验证集

validation_steps = 100, # 指定验证步长

steps_per_epoch = 100, # 指定每轮步长

epochs = 50, # 指定轮次

verbose = 1) # 指定是否显示训练过程中的信息

输出结果如下:

Epoch 1/50

100/100 [==============================] - 8s 76ms/step - loss: 2.3003 - acc: 0.1293

- val_loss: 2.2951 - val_acc: 0.1532

Epoch 2/50

100/100 [==============================] - 7s 71ms/step - loss: 2.2571 - acc: 0.1735

- val_loss: 2.2648 - val_acc: 0.1662

… …

Epoch 50/50

100/100 [==============================] - 7s 73ms/step - loss: 1.3982 - acc: 0.5091

- val_loss: 1.7499 - val_acc: 0.5065

训练集和验证集上的损失曲线和准确率曲线如下图所示。

训练集和验证集上的损失曲线和准确率曲线(数据增强后)

这次训练的速度似乎变慢了很多(因为数据增强需要时间),但是训练结果更令人满意。而且,训练集和验证集的准确率最终呈现出在相同区间内同步上升的状态,这是很好的现象。从损失曲线上看,过拟合的问题基本解决了。而验证集准确率也上升至50%左右。对于这个多种狗狗的分类问题来说,这已经是一个相当不错的成绩了。

下面的代码可以将神经网络模型(包括训练好的权重等所有参数)保存到一个文件中,并随时可以读取。

from keras.models import load_model # 导入模型保存工具

cnn.save('../my_dog_cnn.h5') # 创建一个HDF5格式的文件'my_dog_cnn.h5'

del cnn # 删除当前模型

cnn = load_model('../my_dog_cnn.h5') # 重新载入已经保存的模型

总结一下,深度神经网络的性能优化是一个很大的课题。希望上一课和本课两次的尝试能带给大家一个基本的思路。此外,其他可以考虑的方向还包括以下几种。

■增加或减少网络层数。

■尝试不同的优化器和正则化方法。

■尝试不同的激活函数和损失函数。