Keras深度学习实践3—计算机视觉问题:猫vs狗
内容参考以及代码整理自“深度学习四大名“著之一《Python深度学习》
一、卷积神经网络
卷积神经网络,也叫convnet,它是计算机视觉应用几乎都在使用的一种深度学习模型。
我们先来看一个简单的卷积神经网络例示,使用卷积神经网络对MNIST数字进行分类。
"""
MNIST手写数字问题的卷积神经网络解决方案
"""
from keras import layers
from keras import models
from keras.datasets import mnist
from keras.utils import to_categorical
import matplotlib.pyplot as plt
"""
构建网络
"""
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
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(64, (3, 3), activation='relu'))
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
"""
训练数据
"""
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float32') / 255
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
history = model.fit(train_images, train_labels, epochs=5, batch_size=64, validation_data=(test_images, test_labels))
"""
计算精度
"""
test_loss, test_acc = model.evaluate(test_images, test_labels)
print(test_acc)
上面这段代码中,网络中包含Conv2D和Maxpooling2D层也就是卷积运算和最大池化运算。使用简单卷积神经网络测试精度可以达到99%以上,要比密集连接效果好。
1.卷积运算
与全连接层相比卷积层絮叨的是局部模式。对图像来说,学到的就是图像二位小窗口发现的模式。
卷积神经网络具有以下两个有趣的性质。
- 平移不变性:卷积神经网络在图像中某个位置学到的特征,如果在其他位置出现也可以被识别。这使得卷积神经网络在处理图像的时候可以高效利用数据,它只需要更少的训练样本就可以学到具有泛化能力的数据表示。
- 可以学到模式的空间层次结构。例如,第一个卷积层讲学习到较小的局部模型,第二个卷积层将学习由第一层特征组成的更大模式。这使得卷积神经网络可以有效的学习越来越复杂、越来越抽象的视觉概念。
卷积的两个关键参数:
- 从输入中提取图块的尺寸:这些图块的大小通常是33或者55.
- 输出特征图的深度:卷积所计算的过滤器的数量,也可以理解为特征的数量。
卷积的工作原理:在3D输入特征图上滑动3*3的窗口,在每个可能位置提取特征的3D图块。将3D图块与一个权重矩阵即卷积核做张量积,转换成形状为(output,)的1D向量。然后对这些向量进行重组,将其形状转换为(height, width, output_depth)的3D输出特征图。输出特征图中每个空间位置都对应与输入特征图中的相同位置。
卷积原理2.最大池化运算
最大池化是从输入特征图中提取窗口,并输出每个通道的最大值。它的概念和卷积类似,但是最大池化使用的是硬编码的max张量运算对局部图块进行变化。最大池化通常使用的是2*2窗口和步幅2,其目的是将特征图下采样2倍。
使用最大池化的目的是:
- 如果不适用最大池化,不利于学习特征的空间层级结构。后面的卷积层的窗口还是只能学习局部的特征,无法学习更大范围的特征,可能不足以对图像进行分类。
- 如果不使用最大池化的话,最后一层的特征图对每个样本包含的元素就太多了,如果最后要展平并添加一个全连接层,那么最后一层的参数会非常巨大。可能会导致严重的过拟合。
最大池化不是实现这种下采样的唯一方法。还可以在卷积层使用步幅来实现,或者使用平均池化来实现。平均池化是将每个局部输入变换为图块各通道的平均值。
二、“猫狗大战”——猫狗图片分类问题实践
这里我们将使用一个较小的数据集来解决猫狗图片分类的问题。给出一张猫或者狗的图片,让程序告诉我们是狗还是猫。
1.数据准备
数据来源是从kaggle上下载,网址: www.kaggle.com/c/dogs-vs-cats/data
这个数据集包含25000张猫狗图像,我们只使用其中一小部分来完成实践:猫和狗各1000个样本的训练集、500个样本的验证集和测试集。所以我们要先构造这个规模较小的数据集。下面这个函数,我们来完成训练、验证、测试目录的生成。
def create_fold():
"""
将原始数据中的图片分成训练集、验证集、测试集,并分文件夹存放。
"""
original_dataset_dir = 'D:\\git_code\\data\\dogs-vs-cats\\train' # 原始数据的目录
base_dir = 'D:\\git_code\\data\\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') # 创建cats的训练集目录
if not os.path.exists(train_cats_dir):
os.mkdir(train_cats_dir)
train_dogs_dir = os.path.join(train_dir, 'dogs') # 创建dogs的训练集目录
if not os.path.exists(train_dogs_dir):
os.mkdir(train_dogs_dir)
validation_cats_dir = os.path.join(validation_dir, 'cats') # 创建cats的验证集目录
if not os.path.exists(validation_cats_dir):
os.mkdir(validation_cats_dir)
validation_dogs_dir = os.path.join(validation_dir, 'dogs') # 创建dogs的验证集目录
if not os.path.exists(validation_dogs_dir):
os.mkdir(validation_dogs_dir)
test_cats_dir = os.path.join(test_dir, 'cats') # 创建cats的测试集目录
if not os.path.exists(test_cats_dir):
os.mkdir(test_cats_dir)
test_dogs_dir = os.path.join(test_dir, 'dogs') # 创建dogs的测试集目录
if not os.path.exists(test_dogs_dir):
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) 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)
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)
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)
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)
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)
return train_dir, validation_dir, test_dir
2.构建网络
网络构建还是以Conv2D+MaxPooling的组合。对于这个二分类问题,优化器使用RMSprop,激活函数使用sigmod,损失函数使用二元交叉熵。
def bulid_model():
"""
构建网络:使用Conv2D和MaxPooling2D层交叠构成。
Flatten层将3D输出展平到1D
二分类问题最终使用sigmod激活
"""
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(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])
3.数据预处理
我们数据源图片是JPEG形式,我们需要对数据进行预处理以便网络更好的处理。预处理主要做以下几个步骤:
- 读取图像文件
- 将JPEG文件解码为RGB像素网格
- 将这些像素转换为浮点数张量
- 将像素值(0~255范围内)缩放到[0, 1]区间
keras提供完成这些的工具,位于keras.preprocessing,image。它有一个ImageDataGenerator,能够批量的将图像文件预处理。
def create_generator(dir):
"""
创建数据生成器,这个生成器的作用是,将JPEG解码为RGB像素网格,然后将这些像素网格转换为浮点数向量,
然后将像素值(0~255范围内)缩放到[0,1]区间。
:param dir: 数据所在的目录
:return: 返回一个生成器
"""
dir_datagen = ImageDataGenerator(rescale=1. / 255) # 将所有图像乘以1/255缩放
generator = dir_datagen.flow_from_directory(dir, target_size=(150, 150), # 图片大小调整为150 * 150
batch_size=20,
class_mode='binary') # 使用二进制
return generator
上面这段代码会生成: 150*150的RGB图像[shape=(20, 150, 150, 3)]与二进制标签[shape=(20,)]组成的批量,每个批量包含20个样本。
因为我们使用生成器来处理图像,我们要利用生成器来训练数据,keras提供了fit_generator来完成这个训练,fit_generator允许生成器作为训练集参数和验证集参数。
""" 生成器方式训练网网络 """
history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=30, validation_data=validation_generator,
validation_steps=50
训练结果如下:
精度曲线 损失曲线从训练结果曲线可以看出,训练精度随着时间逐渐增加,知道接近100%,而验证精度则停留在70%~72%。这都是典型的过拟合现象。针对计算机视觉领域,用深度学习处理图像时几乎都会用到的方法就是数据增强。
4.使用数据增强
过拟合的原因是学习样本太少,导致无法训练出能够泛化到新数据的模型。数据增强是从现有的训练样本中生成更多的训练数据,其方法是利用多种能够生成可信图像的随机变换来增加样本。其目标是,模型在训练时不会两次查看完全相同的图像,这让模型能够观察到数据的更多内容,从而具有更好的泛化能力。
keras中我们还是使用图像生成器的方法来进行数据增强,在初始化对象的时候使用更多的参数来增强数据:
datagen = ImageDataGenerator(rotation_range=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')
- rotation_range是角度值,表示图像堆积旋转的角度范围。
- width_shift和height_shift是图像在水平或垂直方向上平移的范围
- shear_range是随机错切变换的角度
- zoom_range是图像随机缩放的范围
- horizontal_flip是随机将一半图像水平翻转。如果没有水平不对称的假设,这种做法是有意义的。
- fill_mode是用于填充创建像素的方法,这些新像素可能来自于旋转或宽度/高度平移。
为了进一步降低拟合,在网络中添加了一个Dropout层,添加在密集连接分类器之前。
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.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
训练结果如下:
精度曲线 损失曲线
可以看出,经过数据增强的结果准确率大约在80%左右,比未增强的提高了不少。
下一次,我们将继续提升训练的精度,请持续关注!
查看完整代码,请看: https://github.com/ubwshook/MachineLearning