在小型数据集上训练卷积神经网络,本文用到的数据集来自于sklearn中的猫狗分类,但是只用了其中的4000数据,其中2000张用于训练,1000张用于验证,1000张用于测试。
想要在小型图像数据集上训练,一种常见的且非常高效的方法就是使用预训练网络,这种网络是一个保存好的,之前已经在大型数据集上训练好的网络。使用与训练网络一般有两种方法,特征提取和模型微调。
其中特征提取又有两种方法:一种是在本地数据集上运行卷积基,将输出保存成numpy数组,然后以这个数组数据作为输入,训练全连接层。这种方式运行速度快,但是不能做数据增强。
第二种方式就是在卷积基的顶部添加dense层来扩展已有模型,并在本地数据集上端到端的训练整个模型,这个好处是可以做数据增强,但是速度很慢。
特征提取的第一种实现方式的训练和验证精度如下所示:
训练和验证的损失如下所示:
由上面两张图可以看到出现了过拟合的问题,训练精度随着时间线性增加,直到接近100%,而验证精度则停留在70%左右,这是因为样本数较少导致的。解决过拟合的问题是数据增强、dropout层、L2正则化等。
正则化就是模型在训练过程中,调节模型允许存储的信息量,如果一个模型智能记住几个模式,那么优化过程会迫使模型记住最重要的模式,从而得到良好的泛化能力。
正则化实施的第一个方法是:降低网络大小,从而降低模型的记忆容量,但是问题就出现在最佳层数或者每层最佳大小的确定
正则化的第二个实施方法是:添加权重正则化。一般地,给定一些训练数据和一种网络架构,很多组权重值都可以解释这些数据,那么简单模型回避复杂模型更不容易过拟合。这里的简单模型是指熵值更小的模型。依照这个思路,一种常见的解决方法就是向网络中添加与较大权重值相关的成本,这个成本可以与权重值正比,叫做L1正则化,或者与权重值的平方成正比,叫做L2正则化。
dropout是在训练时随机将该层的一些输出特征舍弃掉。
'''
@author: liuyunsheng
@file: Cat_Dogs.py
@time: 2019/5/20 14:34
@desc: 本例是作猫狗的分类任务,使用的数据集中包含4000张猫和狗的图像,将其中2000张用于训练,1000张用于验证,1000张用于测试
'''
import os, shutil
from keras import layers
from keras import models
from keras import optimizers
from keras.preprocessing.image import ImageDataGenerator
from keras.preprocessing import image
import matplotlib.pyplot as plt
# 步骤1:划分数据集
original_dataset_dir = 'E:\\BaiduNetdiskDownload\\kaggle\\dogsvscats\\train'
base_dir = 'G:\\Document\\电子书\\5月重点看\\cats_and_dogs_small'
# 判断文件夹是否存在,不存在的话就新建文件夹
if not os.path.exists(base_dir):
os.mkdir(base_dir)
train_dir = os.path.join(base_dir, 'train')
if not os.path.exists(train_dir):
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation')
if not os.path.exists(validation_dir):
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test')
if not os.path.exists(test_dir):
os.mkdir(test_dir)
train_cats_dir = os.path.join(train_dir, 'cats')
if not os.path.exists(train_cats_dir):
os.mkdir(train_cats_dir)
train_dogs_dir = os.path.join(train_dir, 'dogs')
if not os.path.exists(train_dogs_dir):
os.mkdir(train_dogs_dir)
validation_cats_dir = os.path.join(validation_dir, 'cats')
if not os.path.exists(validation_cats_dir):
os.mkdir(validation_cats_dir)
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
if not os.path.exists(validation_dogs_dir):
os.mkdir(validation_dogs_dir)
test_cats_dir = os.path.join(test_dir, 'cats')
if not os.path.exists(test_cats_dir):
os.mkdir(test_cats_dir)
test_dogs_dir = os.path.join(test_dir, 'dogs')
if not os.path.exists(test_dogs_dir):
os.mkdir(test_dogs_dir)
# 将前1000张猫的图像复制到train_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_cats_dir, fname)
shutil.copyfile(src, dst)
# 将前1000张猫的图像复制到validation_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_cats_dir, fname)
shutil.copyfile(src, dst)
# 将前1000张猫的图像复制到test_cats_dir
fnames = ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_cats_dir, fname)
shutil.copyfile(src, dst)
# 将前1000张猫的图像复制到train_dogs_dir
fnames = ['dog.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_dogs_dir, fname)
shutil.copyfile(src, dst)
# 将前1000张猫的图像复制到validation_dogs_dir
fnames = ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_dogs_dir, fname)
shutil.copyfile(src, dst)
# 将前1000张猫的图像复制到test_dogs_dir
fnames = ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_dogs_dir, fname)
shutil.copyfile(src, dst)
# 步骤2:构建网络
# 2.1 构建网络
model = models.Sequential()
# 表示输出的是32维的张量,卷积核为3*3,激活函数采用relu函数,输入进来的图像长宽为150*150,通道数为3
model.add(layers.Conv2D(32, (3, 3), activation='relu',input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
# 输出为1维张量,即向量,激活函数为sigmoid
model.add(layers.Dense(1, activation='sigmoid'))
# 2.2 配置模型用于训练,并设置二元交叉熵作为损失函数
model.compile(loss='binary_crossentropy',optimizer=optimizers.RMSprop(lr=1e-4),metrics=['acc'])
# 步骤3:数据预处理
# 3.1 将所有图像乘以1/255缩放;ImageDataGenerator类能够将硬盘上的图像文件自动转换为预处理好的张量批量
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)
# 3.2 将所有图像调整为150*150,并设置二进制标签binary;每个批量20个样本
train_generator = train_datagen.flow_from_directory(train_dir,target_size=(150, 150),batch_size=20,class_mode='binary')
validation_generator = test_datagen.flow_from_directory(validation_dir,target_size=(150, 150),batch_size=20,class_mode='binary')
# 步骤4:利用批量生成器拟合模型;fit_generator方法的第一个参数是python生成器,可以不停地生成输入和目标组成的批量
# epochs表示在数据集上迭代的总数;step_per_epoch一般等于数据集的样本数除以每个批量的大小
history = model.fit_generator(train_generator,steps_per_epoch=100,epochs=30,validation_data=validation_generator,validation_steps=50)
# 步骤5:保存模型
model.save('cats_and_dogs_small_1.h5')
# 步骤6:绘制训练中的损失和精度曲线
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()
特征提取的第二种实现方式的结果如下图所示:
主要变更代码如下:
model.add(layers.Dropout(0.5))
train_datagen = ImageDataGenerator(
rescale=1./255,
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True)
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
train_dir,
target_size=(150, 150),
batch_size=32,
class_mode='binary')
validation_generator = test_datagen.flow_from_directory(
validation_dir,
target_size=(150, 150),
batch_size=32,
class_mode='binary')
history = model.fit_generator(
train_generator,
steps_per_epoch=100,
epochs=100,
validation_data=validation_generator,
validation_steps=50)
除了特征提取之外,另一种广泛使用的模型服用的方法就是模型微调,是在特征提取的基础上做的。对于用于特征提取的冻结的卷积基,微调是指将其顶部(即靠近全连接层)的几层解冻,并将这解冻的几层和新增加的部分联合训练,通过调整模型的抽象表示,以便更加契合自身的问题。其步骤一般包括:
1)在已经训练好的基网络上添加自定义网络
2)冻结基网络
3)训练所添加的部分
4)解冻基网络的一些层
5)联合训练解冻的这些层和添加的部分
其中特征提取已经完成了前三部分。所以可以直接进行第四部分。首先解冻基网络的一些层。本文选择解冻block_conv1/2/3;解冻那些层是有考虑的:1)卷积基中更靠近底部的层编码是更加通用的可复用特征,而更靠顶部的编码是更专业的特征。因此一般地,都是修改更加靠近顶部的编码;2)训练的采纳数越多,过拟合的风险就越大
conv_base.trainable=True
set_trainable=False
for layer in conv_base.layers:
if layer.name=='block5_conv1':
set_trainable=True
if set_trainable:
layer.trainable = True
else:
layer.trainable = False
然后开始微调网络,本文选择学习率非常小的RMSProp优化器来实现,之所以让学习率非常小,是因为对于微调的三层来讲,我们不希望其变化范围过大。
model.compile(
loss='binary_crossentropy',
optimizer=optimizers.
RMSprop(lr=1e-5),
metrics=['acc'])
history = model.fit_generator(
train_generator,
steps_per_epoch=100,
epochs=30,
validation_data=validation_generator,
validation_steps=50)
然后绘制图像,不一样的地方在于,这里采用指数移动平均值来代替每个损失和精度,以达到降噪的效果。
def smooth_curve(points, factor=0.8):
smoothed_points = []
for point in points:
if smoothed_points:
previous = smoothed_points[-1]
smoothed_points.append(previous * factor + point * (1 - factor))
else:
smoothed_points.append(point)
return smoothed_points
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(acc) + 1)
plt.plot(epochs,smooth_curve(acc), 'bo', label='Smoothed training acc')
plt.plot(epochs,smooth_curve(val_acc), 'b', label='Smoothed validation acc')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs,smooth_curve(loss), 'bo', label='Smoothed training loss')
plt.plot(epochs,smooth_curve(val_loss), 'b', label='Smoothed validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()
最终的结果如下图所示:
值得注意的是,验证精度提到到96%左右。但是有一个问题,损失没有降低的情况下,精度怎么保持稳定或提高呢?这是因为图中展示的是逐点损失值的平均值,但影响精度的是损失值的分布,而不是平均值,因为精度是模型预测的类别概率的二进制阈值。即从平均损失中无法看出,但是模型也仍然在改进。
另外值得注意的一点是,这个方法给长消耗时间。我一共是训练了30轮,每轮消耗的时间都在400秒以上。