深度学习:从零开始构造一个识别猫狗图片的卷积网络

在深度学习的项目实践中,往往会遇到两个非常难以克服的难题,一是算力,要得到精确的结果,你需要设计几千层,规模庞大的神经网络,然后使用几千个GPU,把神经网络布置到这些GPU上进行运算;第二个难以克服的困难就是数据量,要想得到足够精确的结果,必须依赖于足够量的数据来训练网络模型。本节我们先看看第二个问题如何解决。

我们将开放一个神经网络,用于识别猫狗照片,用于训练模型的照片数量不多,大概4000张左右,猫狗各有2000张,我们将用2000张图片训练模型,1000张用来校验模型,最后1000张对模型进行测试。基于这些有限的数据,我们从零开始构造一个卷积网络模型,在没有使用任何优化手段的情况下,先使得模型的识别准确率达到70%左右,这时如果继续加大模型的训练强度会引起过度拟合,此时我们引入数据扩展法,一种能有效应对视觉识别过程中出现过度拟合的技巧,使用该方法我们可以把网络的准确度提升到80%左右,接着我们再使用其他方法,例如特征抽取,模型预训练,再加上一些具备参数调优,最后让模型的准确率达到97%。

首先我们的训练数据来自于kaggle网站,我已经下载并上传到下面链接的对应课程页面里:
更详细的讲解和代码调试演示过程,请点击链接
把图片下载到本地解压后,我们再使用下面代码,将相关图片拷贝到不同的路径下:

import os, shutil
#数据包被解压的路径
original_dataset_dir = '/Users/chenyi/Documents/人工智能/all/train'
#构造一个专门用于存储图片的路径
base_dir = '/Users/chenyi/Documents/人工智能/all/cats_and_dogs_small'
os.makedirs(base_dir, exist_ok=True)
#构造路径存储训练数据,校验数据以及测试数据
train_dir = os.path.join(base_dir, 'train')
os.makedirs(train_dir, exist_ok = True)
test_dir = os.path.join(base_dir, 'test')
os.makedirs(test_dir, exist_ok = True)
validation_dir = os.path.join(base_dir, 'validation')
os.makedirs(validation_dir, exist_ok = True)

#构造专门存储猫图片的路径,用于训练网络
train_cats_dir = os.path.join(train_dir, 'cats')
os.makedirs(train_cats_dir, exist_ok = True)
#构造存储狗图片路径,用于训练网络
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.makedirs(train_dogs_dir, exist_ok = True)

#构造存储猫图片的路径,用于校验网络
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.makedirs(validation_cats_dir, exist_ok = True)
#构造存储狗图片的路径,用于校验网络
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.makedirs(validation_dogs_dir, exist_ok = True)

#构造存储猫图片路径,用于测试网络
test_cats_dir = os.path.join(test_dir, 'cats')
os.makedirs(test_cats_dir, exist_ok = True)
#构造存储狗图片路径,用于测试网络
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.makedirs(test_dogs_dir, exist_ok = True)


#把前1000张猫图片复制到训练路径
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)

#把接着的500张猫图片复制到校验路径
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)

#把接着的500张猫图片复制到测试路径
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张狗图片复制到训练路径
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)

#把接下500张狗图片复制到校验路径
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)

#把接下来500张狗图片复制到测试路径
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)

print('total trainning cat images: ', len(os.listdir(train_cats_dir)))

print('total training dog images', len(os.listdir(train_dogs_dir)))

print('total validation cat images', len(os.listdir(validation_cats_dir)))

print('total validation dogs images', len(os.listdir(validation_dogs_dir)))

print('total test cat images:', len(os.listdir(test_cats_dir)))

print('total test dog images:', len(os.listdir(test_dogs_dir)))

上面代码把图片分别放置到不同文件夹下,训练用的图片在一个文件夹,校验用的图片在一个文件夹,最后测试用的图片在一个文件夹,上面代码运行后,结果如下:

屏幕快照 2018-07-18 下午5.10.24.png

我们将向上一节例子那样,构造一个Conv2D和MaxPooling2D相互交替的卷积网络。由于我们现在读取的图片比上一节的手写数字图片要到,而且图片的颜色深度比上一节的灰度图要大,因此我们这次构造的网络规模也要相应变大。卷积网络模型的构建代码如下:

from keras import layers
from keras import models
from keras import optimizers

model = models.Sequential()
#输入图片大小是150*150 3表示图片像素用(R,G,B)表示
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'])

model.summary()

上面代码运行后结果如下:

屏幕快照 2018-07-18 下午5.43.55.png

我们看到网络在第六层时,已经有了三百万个参数!这是由于我们反复做卷积,对输入的矩阵做切片造成的。由于网络需要对数据进行二分,所以最后一层只有一个神经元。

接下来我们看看数据预处理,由于机器学习需要读取大量数据,因此keras框架提供了一些辅助机制,让我们能快速将数据以批量的方式读入内存,我们看相应代码:

from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(rescale = 1./ 255) #把像素点的值除以255,使之在0到1之间
test_datagen = ImageDataGenerator(rescale = 1. / 255)

#generator 实际上是将数据批量读入内存,使得代码能以for in 的方式去方便的访问
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')
#calss_mode 让每张读入的图片对应一个标签值,我们上面一下子读入20张图片,因此还附带着一个数组(20, )
#标签数组的具体值没有设定,由我们后面去使用
for data_batch, labels_batch in train_generator:
    print('data batch shape: ', data_batch.shape)
    print('labels batch shape: ', labels_batch.shape)
    break

Generator 是一种数据批量读取类,而且他们是可循环的,也就是可以对它们使用for in ,在上面我们构造了两个Generator用于读取训练图片和校验图片,同时把图片大小设置为150*150,同时它还能让我们在图片后面附带一个标签值,这就是参数class_mode的作用,由于我们只有猫狗两种图片,因此该标签值不是0就是1,由于train_dir路径下只有两个文件夹,它会为从这两个文件夹中读取的图片分别赋值0和1。

在用for in 遍历generator时,每次遍历都能读取20张图片,而且这个过程是无止境的,当所有图片读取完后,generator又会重头再次读入图片,因此我们必须自己使用break把循环中断掉。上面代码运行后结果如下:

屏幕快照 2018-07-19 下午5.45.42.png

我们看看如何通过generator把数据高效的传递给网络,代码如下:

history = model.fit_generator(train_generator, steps_per_epoch = 100,
                             epochs = 30, validation_data = validation_generator,
                             validation_steps = 50)

网络模型支持直接将generator作为参数输入,由于我们构造的generator一次批量读入20张图片,总共有2000张图片,所以我们将参数steps_per_epoch = 100,这样每次训练时,模型会用for…in… 在train_generator上循环100次,将所有2000张图片全部读取,后面设置校验数据参数时,逻辑也类似,我们指定循环训练模型30次,上面代码执行后,在普通单CPU机器上运行将会非常缓慢,在我的电脑上,大概执行了十几分钟。

训练结束后,我们模型的训练准确率和校验准确率绘制出来,看看模型对数据的处理情况,代码如下:

model.save('cats_and_dogs_small_1.h5')
import matplotlib.pyplot as plt

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 = 'trainning acc')
plt.plot(epochs, val_acc, 'b', label = 'validation acc')
plt.title('Trainning and validation accuary')
plt.legend()

plt.show()
plt.figure()

#绘制模型对训练数据和校验数据判断的错误率
plt.plot(epochs, loss, 'bo', label = 'Trainning loss')
plt.plot(epochs, val_loss, 'b', label = 'Validation loss')
plt.title('Trainning and validation loss')
plt.legend()

plt.show()

上面代码运行后,绘制的图形如下:

屏幕快照 2018-07-20 上午9.52.45.png

从第一个图可以看出,模型对训练数据的识别率不断提升,但是对校验数据的识别率基本停滞在一个水平,从第二个图看出,模型对训练数据识别的错误率极具下降,但对校验数据的识别错误率反而快速上升了,这表明模型出现了过度拟合的现象,这是在任何机器学习项目中都会遇到的问题。

在计算机视觉识别中,有一种技巧叫数据扩展,专门用于图像识别过程中出现的过度拟合现象。过度拟合出现的一个原因在于数据量太小,我们遇到的情况正是如此。数据扩展本质上是通过利用现有数据创造出新数据,从而增加数据量,我们通过对原有图片随机进行一些修改,在不改变图片本质的情况下,将一张图片修改成一张新的图片,当然这种修改不能将一只猫改成一只狗,只能将一只黑猫变成一只灰猫,我们要保证在训练中,模型不用多次运算同一张图片,在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是对图片进行放大缩小,horizaontal_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[3]
#加载图片并把它设置为150*150
img = image.load_img(img_path, target_size=(150, 150))
x = image.img_to_array(img)
x = x.reshape((1, ) + x.shape)

i = 0
'''
下面的flow函数能自动帮我们进行指定的各种图形变换,例如拉伸,缩放,裁剪等,它是一个死循环,我们需要自己通过break命令才能跳出来
'''
f, ax = plt.subplots(1,4)
for batch in datagen.flow(x, batch_size = 1):
    imgplot = ax[i].imshow(image.array_to_img(batch[0]))
    ax[i].axis('off')
    i += 1
    if i % 4 == 0:
        break

plt.show()

我们拿出一张猫的图片,然后使用上面代码对图片进行各种变换,上面代码运行后得到结果如下:

屏幕快照 2018-07-20 下午5.23.42.png

从上面可以看到,我们从一张图片就可以通过变换生成好几张不同图片,我们在把图片传入网络前,先通过上面办法扩张图片,那么网络一下子就能获得成倍增长的训练数据,然而这种做法使得新生成的图片与原有图片存在很强的关联性,因此它对改善过度拟合的作用比较有限,因此我们还得运用前面说过的对网络层输出结果随机清零的办法,我们把这两方法结合起来重新训练网络,代码如下:

model = models.Sequential()
...
model.add(layers.Flatten())
#把上层网络的输出结果中的一半数据随机清零
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
                                  ) #把像素点的值除以255,使之在0到1之间,增加图片变换
test_datagen = ImageDataGenerator(rescale = 1. / 255)

#generator 实际上是将数据批量读入内存,使得代码能以for in 的方式去方便的访问
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')

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_2.h5')

完了,我们再次运行前面的绘图代码,看看网络对训练数据和校验数据的识别度有何变化,运行后得到的绘制结果如下:

屏幕快照 2018-07-21 下午4.24.50.png

经过数据扩展和输出结果随机清零后,对过度拟合的处理非常有效,从上图看,网络对训练数据和校验数据的识别正确率最终完全一致,对两类数据的识别错误率都在有序下降。此时我们达到了80%左右的准确率。如果进一步使用数据正规化以及参数调优等手段,网络的识别率还能进一步提升,但是就如车没油跑不远一样,如果数据不足,无论我们使用什么深度去优化,识别率都很难再有明显的提升,进一步提升识别率的方法,我们将在下一节详细阐述。

更详细的讲解和代码调试演示过程,请点击链接

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
这里写图片描述

猜你喜欢

转载自blog.csdn.net/tyler_download/article/details/81170962