本文主要介绍猫狗分类问题,原型取自2013年的kaggle计算机竞赛,你可以从https://www.kaggle.com/c/dogs_vs_cats/data获取必要的数据集,或者寻找其他的镜像文件。数据集包含25000张猫狗图像,这里我们选取2000张,其中,1000张训练集,500张验证集合500张测试集。
本文将采用2种方法;
(1)使用普通的CNN来训练模型;
(2)使用预训练的VGG16来训练模型。
import os, shutil
original_dataset_dir = '原始数据集解压路径'
base_dir = '保存较小数据集路径'
os.mkdir(base.dir)
#在较小数据集下分别划分训练集,验证集和测试集目录
train_dir = os.path.join(base_dir, 'train')
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation')
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test')
os.mkdir(test_dir)
#在上面三个目录下分别划分猫狗目录
train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir)
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)
validation_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_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)
fnames = ['cat.{}.jpg'.format(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)
fnames = ['cat.{}.jpg'.format(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)
fnames = ['dog.{}.jpg'.format(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)
fnames = ['dog.{}.jpg'.format(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)
fnames = ['dog.{}.jpg'.format(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)
需要注意的是,在目录创建完毕后,我们应该注释掉创建的命令,以免下次运行程序时报错。
此时我们可以用下面的代码来看看每个分组分别包含多少张图像:
print('The number of training cat images is:', len(os.listdir(train_cats_dir)))
下面代码不再赘述
结果应该是猫和狗训练集 / 验证集 / 测试集各分别有1000 / 500 / 500张图像。
from keras import models
from keras import layers
from keras import optimizers
model = models.Sequential()
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.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer=optimizers.RMSprop(lr=1e-4),
loss='binary_crossentropy',
metrics=['acc'])
卷积神经网络由Conv2D层和MaxPoolingD层交替堆叠而成,因为这里的问题较为复杂,所以适当增大网络,这样既能增加网络容量,还可以进一步缩小特征图的尺寸,使其在连接Flatten层时尺寸不会太大。本节中设置图形大小为150x150(随意),所以最后图形在Flatten层之前的尺寸为7x7。
编译网络时,我们使用RMSprop优化器。因为网络最后一层是单一sigmoid单元,所以我们将使用二元交叉熵作为损失函数。基本规则如下表所示:
问题类型 | 最后一层激活 | 损失函数 |
二分类问题 | sigmoid | binary_crossentropy |
多分类、单标签问题 | softmax | categorical_crossentropy |
多分类、多标签问题 | sigmoid | binary_crossentropy |
回归到任意值 | 无 | mse |
回归到0~1范围内的值 | sigmoid | mse或binary_crossentropy |
我们来看看特征图的维度如何随着每层变化:
model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 148, 148, 32) 896
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 74, 74, 32) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 72, 72, 64) 18496
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 36, 36, 64) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 34, 34, 128) 73856
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 17, 17, 128) 0
_________________________________________________________________
conv2d_4 (Conv2D) (None, 15, 15, 128) 147584
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 7, 7, 128) 0
_________________________________________________________________
conv2d_5 (Conv2D) (None, 5, 5, 128) 147584
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 2, 2, 128) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 512) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 512) 0
_________________________________________________________________
dense_1 (Dense) (None, 512) 262656
_________________________________________________________________
dense_2 (Dense) (None, 1) 513
=================================================================
Total params: 651,585
Trainable params: 651,585
Non-trainable params: 0
_________________________________________________________________
通常地,将数据输入到神经网络之前,应该将数据格式转化为经过预处理的浮点数张量。现在,数据以JEPG的形式保存在硬盘中,处理步骤大致如下:
(1)读取图像文件
(2)将JEPG文件解码为RGB像素风格
(3)将这些像素网格转化为浮点型张量
(4)将像素值从(0~255)缩小到[0-1]区间(神经网络喜欢处理较小的输入值)
Keras自带图像处理辅助工具的模块,位于keras.preprocessing.image。它包含ImageDataGenerator类,可以快速创建Python生成器,能够将硬盘上的图像文件自动转换为预处理好的张量批量。
#使用ImageDataGenerator从目录中读取图像
from keras.preprocessing.image import ImageGenerator
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(train_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
validation_datagen = test_datagen.flow_from_directory(test_dir,
target_size=(150, 150),
batch_size=20,
class_mode='binary')
#看一下生成器的输出
for data_batch, labels_batch in train_generator:
print('Data batch shape:', data_batch.shape)
print('Labels batch shape:', labels_batch.shape)
break
生成器输出如下:
('data batch shape:', (20, 150, 150, 3))
('labels batch shape:', (20,))
生成器生成了150x150的RGB图像 [ 形状为(20, 150, 150, 3) ] 与二进制标签 [ 形状为(20,) ] 组成的批量。每个批量包含20个样本(批量大小)。因为生成器会不停地生成批量,因此我们要在某个时刻让它停下来(break)。
#利用批量生成器拟合模型
history = model.fit_generator(train_generator,
steps_per_epoch=100,
epochs=30,
validation_data=validation_generator,
validation_steps=50)
#保存模型是一种良好的习惯
model.save('cats_and_dogs_small_1.h5')
此时,我们使用fit_generator的方法来拟合,它在数据生成器上的效果和fit一样。第一个参数应该为Python生成器:train_generator。因为数据不断生成,所以要知道每轮需要从生成器中抽取多少样本,这就是steps_per_epoch的作用:从生成器中抽取steps_per_epoch个批量后(即运行了steps_per_epochs次梯度下降),拟合过程将进入下一个轮次。本例中,每个批量包含20个样本,所以2000个样本需要100个批量。
由图中可以明显看出过拟合的特征,训练精度随着时间线性增加,直到接近%100。而验证精度则停留在%70~%72,。验证损失在5轮后就达到最小值,然后保持不变,而训练损失一直线性下降,直到接近0。
过拟合原因是因为学习样本太少,无法训练出能够泛化到新数据的模型。我们已经知道dropout和权重衰减(L2正则化),这里我们采用数据增强,它的原理是将图像随机变换一产生新的数据(对于模型而言)。
首先我们定义一个包含dropout的新卷积神经网络:
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3)))
model.add(layers.BatchNormalization())
model.add(layers.MaxPooling2D(2, 2))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.BatchNormalization())
model.add(layers.MaxPooling2D(2, 2))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.BatchNormalization())
model.add(layers.MaxPooling2D())
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.BatchNormalization())
model.add(layers.MaxPooling2D(2, 2))
model.add(layers.BatchNormalization())
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(optimizer=optimizersRMSprop(lr=1e-4),
loss='binary_crossentropy',
metrics=['acc'])
其中,我们在每个卷积层之间和Flatten层之前增加了BatchNormalization层,最后的准确率大概会提示%1左右。
下面,利用数据增强生成器来训练卷积神经网络:
train_datagen = ImageDataGenerator(rescale=1./255,
rotation=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom-range=0.2,
horizontal_flip=True,
fill_mode='nearest')
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(test_dir,
target_size=(150, 150),
batch_size=32,
class_mode='binary')
history = model.fit_generator(train_generator,
step_per_epoch=63,
epochs=100,
validation_data=validation_generator,
validation_steps=50)
#保存模型
model.save('cats_and_dogs_small_2.h5')
其中,rotation_range是角度值(0~180),表示图像随机旋转的角度;
width_shift和hieght_shift是图像在水平或垂直方向上平移的范围(相对于总的宽度或高度的比例);
shear_range是随机错切变换的角度;
zoom_range是图像随机缩放的范围;
horizontal_flip是随机讲一半图像水平翻转;
fill_mode是用于填充新创建像素的方法,这些新像素可能来自于旋转或宽度 / 高度平移。
随机选择图片,显示数据增强的结果:
from keras.preprocessing import image
fnames = [os.path.join(train_cats_dir, fname) for fname in os.listdir(train_cats_dir)]
#选一张图像
img_path = fnames[10]
#读取图像并调整大小
img = image.load_img(img_path, target_size=(150, 150))
#将其转换成形状为(150, 150, 3)的Numpy数组
x = image.img_to_array(img)
#将其形状改为(1, 150, 150, 3)
x = x.reshape((1,) + x.shape)
#循环是无限的,需要在某个时刻停止循环
i = 0
for batch in datagen.flow(x, batch_size=1):
plt.figure(i)
imgplot = plt.imshow(image.array_to_img(batch[0]))
i += 1
if i % 4 == 0:
break
plt.show()
得到的结果如下:
在使用了数据增强、dropout和normalization之后,模型不再过拟合,训练曲线紧紧跟着验证曲线,精度可达%83。如果再增加网络层数,精度将再提高差不多3个百分点。
目标达成