在深度学习实践中,当训练数据量少时,可能会出现过拟合问题。根据Goodfellow等人的观点,我们对学习算法的任何修改的目的都是为了减小泛化误差,而不是训练误差。
我们已经在sb[后续补充]中提到了不同类型的正则化手段来防止模型的过拟合,然而,这些都是针对参数的正则化形式,往往要求我们修改loss函数。事实上,还有其他方式防止模型过拟合,比如:
Dropout是通过修改网络本身结构以达到正则化效果的技术。Dropout是指在深度学习网络的训练过程中,对于神经网络单元,按照一定的概率将其暂时从网络中丢弃。注意是暂时,对于随机梯度下降来说,由于是随机丢弃,故而每一个mini-batch都在训练不同的网络。
在这节,我们讨论另外一种防止过拟合方法,叫做数据增强(data augmentation),该方法主要对训练数据集进行一定的变换,改变图片的外观,然后将得到的‘新’图片进行模型训练。使用数据增强技术可以得到更多的数据。
备注:预测时候是不是也可以利用data augmentation,结果使用投票方法得到呢?
数据增强主要是运用各种技术生成新的训练样本,比如随机旋转或平移等,虽然,‘新’的样本在一定程度上改变了外观,但是样本的标签保持不变。使用数据增强技术可以增加模型训练的样本数据,一般而言,当模型不断地学习新的数据时,模型的泛化性会得到一定的提高。但是有一点需要注意,大多数情况下,数据增强会降低模型训练的准确度,而会提高模型测试的准确度,因此,我们一般不参考数据增强下的训练数据集的评估结果,而是参考测试集上的评估结果。
图2.1左,我们从标准的正态分布中产生一定量的数据,如果对该数据训练一个模型,可以得到一个很好的结果,但是在实际中,数据是很难严格满足标准的正态分布。相反,为了提高模型的通用性。我们对该分布进行微小的随机干扰,使得数据仍然满足一个近似正态分布,如图2.1右,在此数据训练得到的模型有更好的泛化性。
在计算机视觉中,使用数据增强是非常合理的,由于图像数据的特殊性,可以通过简单的几何变换从原始图像中获额外的训练数据,而且不改变图像的标签,常见的变换有:
最直观的明白数据增强的效果的最佳方法就是可视化,可以很直观地看到图像的外观变化。为了实现这个可视化,我们基于keras构建一个数据增强的python脚本,名为augmentation_demo.py,并写入以下代码:
#encoding:utf-8
from keras.preprocessing.image import ImageDataGenerator
from keras.preprocessing.image import img_to_array
from keras.preprocessing.image import load_img
import numpy as np
import argparse
其中ImageDataGenerator是keras中数据增强类,包含了各种变换方法。
接下来,我们解析命令行参数:
# 构造参数解析和解析参数
ap = argparse.ArgumentParser()
ap.add_argument('-i','--image',required=True,help = 'path to the input image')
ap.add_argument('-o','--ouput',required=True,help ='path to ouput directory to store augmentation examples')
ap.add_argument('-p','--prefix',type=str,default='image',help='output fielname prefix')
args = vars(ap.parse_args())
其中每个参数详细如下:
接下来,加载输入图像,将其转换为keras支持的array形式数组,并对图像增加一个额外维度。
# 加载图像,并转化为numpy的array
print('[INFO] loading example image...')
image = load_img(args['image'])
image = img_to_array(image)
#增加一个维度
image = np.expand_dims(image,axis = 0) #在0位置增加数据,主要是batch size
初始化ImageDataGenerator:
aug = ImageDataGenerator(
rotation_range=30, # 旋转角度
width_shift_range=0.1, # 水平平移幅度
height_shift_range= 0.1, # 上下平移幅度
shear_range=0.2, # 逆时针方向的剪切变黄角度
zoom_range=0.2, # 随机缩放的角度
horizontal_flip=True, # 水平翻转
fill_mode='nearest' # 变换超出边界的处理
)
# 初始化目前为止的图片产生数量
total = 0
ImageDataGenerator类有很多参数,这里无法一一列举。想了解详细的参数意义,请参阅官方的Keras文档. 相反,我们列出几个最有可能实际中用到的:
备注:无论使用哪种变换方法,需要注意我们想要的是增加数据集,只改变图像表面的外观,而不改变原始的图像语义信息。
一旦ImageDataGenerato初始化后,我们就可以产生新的训练数据集:
print("[INFO] generating images...")
imageGen = aug.flow(image,batch_size=1,save_to_dir=args['output'],save_prefix=args['prefix'],save_format='jpg')
for image in imageGen:
total += 1
# 只输出10个案例样本
if total == 10:
break
首先,初始化构造图像数据增强的生成器,并传入参数——输入图像,batch_size设置为1(因为我们这里只增加一个图像),输出的路径等。然后,对imageGen生成器进行遍历,imageGen每次被请求时都会自动生成一个新的训练样本。当新增10张图时,就停止。
执行下列命令,产生数据增强结果:
$ python augmentation_demo.py --image jemma.png --output output
当脚本执行完之后,你可以看到:
ls output/
image_0_1227.jpg image_0_2358.jpg image_0_4205.jpg image_0_4770.jpg ...
如图2.2右所示,每张图像都是经过随机旋转、剪切、缩放或水平翻转得到的。对原始图像微小变换之后,可以看到‘新’图像都保留了原始的标签:dog,从而使我们的神经网络在训练时可以学习新的模式。
数据增强技术虽然有可能降低模型训练时的准确度,但是,数据增强可以一定程度上降低过拟合,确保我们的模型可以更好地推广到新的输入样本。更重要的是,当我们只有少量数据集时——往往是无法进行深度学习实践的,那么可以利用数据增强生成额外的训练数据,从而减少训练深度学习网络需要手工标记的数据。
在本节的第一部分中,我们将讨论Flowers-17数据集,这是一个非常小的数据集(对计算机视觉任务的而言),以及数据增强如何帮助我们生成额外的训练样本。主要做2个实验:
在Flowers-17上训练MiniVGGNet,不使用数据增强。
在flower -17上使用数据增强技术训练MIniVGGNet。
Flowers-17数据集是一个细粒度分类数据,我们的任务是识别17种不同的花。图像数据集非常小,每个类只有80张图像,总共1360张图片。在计算机视觉任务中,应用深度学习算法一般要求每个类别差不多有1000 - 5000个数据,因此,Flowers-17数据集是严重不满足。
我们称Flowers-17为细粒度分类任务,因为所有类别都非常相似(主要花的物种)。事实上,我们可以把这些类别都看作子类别。虽然类别之间是不同的,但是存在相同的结构,比如花瓣,雄蕊、雌蕊等。
细粒度分类任务对于深度学习实践者来说是最具挑战性的,因为这意味着我们的机器学习模型需要学习极端的细粒度特征来区分非常相似的各个类。考虑到我们有限的train数据,这种细粒度的分类任务变得更加困难。
Flowers-17数据可以从地址 下载。
在此之前,我们处理图像时,一般是将它们调整为固定大小,忽略了纵横比。对于一般的数据集,这样做是可以接受的。但是,对于更具挑战性的数据集,我们将图像大小调整到一个固定的大小时,需要保持长宽比。比如图2.4:
左图是输入的原始图像,我们调整图像的大小为256×256(中间),忽略纵横比,从中间图可以看到图像发生了一定的扭曲。再对比右图,考虑图像的纵横比下,我们首先沿着较短的部分调整大小,比如这里宽度调整为256,然后沿高度裁剪图像,使其高度为256。虽然我们在剪辑过程中丢弃了部分图像,但我们也保留了部分图像原始长宽比。保持一致的纵横比可以使卷积神经网络学习到更有辨别力,一致性的特征。这是我们处理更高级的数据集比如ImageNet用到的一种常见的技术。
为了了解预处理,让我们对pyimagesearch项目结构增加一个AspectAwarePreprocessor类:
--- pyimagesearch
| |--- __init__.py
| |--- callbacks
| |--- nn
| |--- preprocessing
| |--- __init__.py
| |--- aspectawarepreprocessor.py
| |--- imagetoarraypreprocessor.py
| |--- simplepreprocessor.py
| |--- utils
在preprocessing子模块下,新建一个文件,名为aspectawarepreprocessor.py,并写入以下代码;
#encoding:utf-8
# 加载所需要模块
import cv2 #安装:pip install opencv-python
import imutils # 安装:pip install imutils
class AspectAwarePreprocessor:
def __init__(self,width,height,inter = cv2.INTER_AREA):
# 定义所需要的变量
self.width = width
self.heigth = height
self.inter = inter
就像在SimplePreprocessor中一样,函数需要两个参数(目标输出图像的宽度和高度)以及调整图像所使用的插值方法。定义预处理函数如下:
def preprocess(self,image):
# 获取image的维度
(h,w) = image.shape[:2]
# 修剪时用到的增量
dw = 0
dh = 0
预处理函数传入的是需要处理的图像。预处理主要包含两步:
代码如下:
if w < h:
image = imutils.resize(image,width = self.width, inter = self.inter)
dh = int((image.shape[0] - self.height) / 2.0)
else:
image = imutils.resize(image, height=self.height, inter=self.inter)
dw = int((image.shape[1] - self.width) / 2.0)
我们需要重新抓取宽度和高度,并使用delta裁剪图像的中心:
(h,w) = image.shape[:2]
image = image[dh:h - dh,dw:w-dw]
return cv2.resize(image,(self.width,self.height), interpolation=self.inter)
在裁剪过程中(由于舍入误差),目标图像尺寸可能增加或者减小一个像素,因此,我们调用cv2模块调整大小以确保我们的输出图像满足一定的宽度和高度。然后将预处理后的图像返回给调用函数。
数据预处理完之后,接下来主要构建模型以及对模型进行训练和评估。
首先,我们不对Flower-17数据进行数据增强,直接对原始数据训练MiniVGGNe模型,新建一个脚本文件,名为minivggnet_flowers17.py,并写入以下代码:
#encoding:utf-8
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from pyimagesearch.preprocessing import ImageToArrayPreprocessor as IAP
from pyimagesearch.preprocessing import AspectAwarePreprocessor as AAP
from pyimagesearch.preprocessing import ImageMove as IM #新增模块
from pyimagesearch.datasets import SimpleDatasetLoader as SDL
from pyimagesearch.nn.conv import MiniVGGNet as MVN
from keras.optimizers import SGD
from imutils import paths
import matplotlib.pyplot as plt
import numpy as np
import argparse
import os
说明: 由于从官方下载的Flower-17数据是没有标签的,因此,我增加了一个模块ImageMove,主要是将图片进行分类,对应到每一个标签文件夹中,并使用0-16代表类别。
定义命令行参数,这里只需要传入图片的数据路径
ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", required=True,help="path to input dataset")args = vars(ap.parse_args())
这里新增一个分类图片模块,使用0-16代表不同的类别
print('[INFO] moving image to label folder.....')
im = IM.MoveImageToLabel(dataPath=args['dataset'])
im.makeFolder()
im.move()
数据目录结构:
原始目录
flowers17/jpg/{image}
处理完之后的目录
flowers17/{species}/{image}
flowers17/3/image_0241.jpg
处理完图片目录,接下来从数据中提取数据标签
print("[INFO] loading images...")
imagePaths = list(paths.list_images(args["dataset"]))
classNames = [pt.split(os.path.sep)[-2] for pt in imagePaths]
classNames = [str(x) for x in np.unique(classNames)]
加载数据集,并对数据进行标准化处理
# 预处理模块
aap = AspectAwarePreprocessor(64, 64)
iap = ImageToArrayPreprocessor()
sdl = SimpleDatasetLoader(preprocessors=[aap, iap])
(data, labels) = sdl.load(imagePaths, verbose=500)
# 标准化
data = data.astype("float") / 255.0
将数据集分成train数据和test数据,并对标签one-hot编码化
(trainX, testX, trainY, testY) = train_test_split(data, labels,test_size=0.25, random_state=42)
trainY = LabelBinarizer().fit_transform(trainY)
testY = LabelBinarizer().fit_transform(testY)
初始化模型,并进行训练
# 初始化模型和优化器
print("[INFO] compiling model...")
opt = SGD(lr=0.05)
model = MiniVGGNet.build(width=64, height=64, depth=3,
classes=len(classNames))
model.compile(loss="categorical_crossentropy", optimizer=opt,
metrics=["accuracy"])
# 训练网络
print("[INFO] training network...")
H = model.fit(trainX, trainY, validation_data=(testX, testY),
batch_size=32, epochs=100, verbose=1)
在数据预处理部分,我们将图片大小调整为64x64,因此,MiniVGGNet网络主要输入shape为64x64x3(像素宽,像素高,通道数)的数据,类别个数是len(classNames),在本例中,它等于17。
使用SGD对模型进行训练,训练次数为100次,并对训练过程进行可视化。
# 评估模型性能
print("[INFO] evaluating network...")
predictions = model.predict(testX, batch_size=32)
print(classification_report(testY.argmax(axis=1),
predictions.argmax(axis=1), target_names=classNames))
# loss和精度可视化
plt.style.use("ggplot")
plt.figure()
plt.plot(np.arange(0, 100), H.history["loss"], label="train_loss")
plt.plot(np.arange(0, 100), H.history["val_loss"], label="val_loss")
plt.plot(np.arange(0, 100), H.history["acc"], label="train_acc")
plt.plot(np.arange(0, 100), H.history["val_acc"], label="val_acc")
plt.title("Training Loss and Accuracy")
plt.xlabel("Epoch #")
plt.ylabel("Loss/Accuracy")
plt.legend()
plt.show()
完成上述代码之后,执行下面命令,将得到MiniVGGNet模型训练的结果
$ python minivggnet_flowers17.py --dataset yourpath/flowers17
结果如下
precision recall f1-score support
0 0.36 0.50 0.42 16
1 0.52 0.57 0.55 21
10 0.83 0.62 0.71 16
11 0.60 0.43 0.50 21
12 0.62 0.69 0.65 26
13 0.53 0.39 0.45 23
14 0.74 0.58 0.65 24
15 0.70 0.73 0.71 22
16 0.82 0.82 0.82 17
2 0.62 0.71 0.67 14
3 0.79 0.65 0.71 23
4 0.44 0.50 0.47 22
5 0.76 0.64 0.70 25
6 0.78 0.74 0.76 19
7 0.26 0.36 0.30 14
8 0.58 0.71 0.64 21
9 0.79 0.94 0.86 16
avg / total 0.64 0.62 0.62 340
从输出结果中可以看出,模型的准确率为64%,由于有限的数据集,该结果还可以接受的。但是从图2.5中,我们可以看出模型出现了过拟合现象,训练不到20次,训练loss已经变得相当小。主要原因是数据量太少了,训练数据集只有1020个样本且每个类别只有60个样本。
此外,从图2.5中可以看到训练精度在不到20次的迭代中已经超过了95%,在最后一次迭代中获得了100%的准确性——很明显地发生了过拟合。由于缺乏大量的训练数据,MiniVGGNet对训练数据的样本学到了过于细微的特征,无法推广到测试数据。为了避免过拟合,我们可以应用正则化技术——在本章的上下文中,我们的正则化方法主要是数据增强。在实践中,您还将包括其他形式的正则化(权值衰减、Dropout等),以进一步减少过拟合的影响。
这一部分,我们将与上节相同的训练过程,唯一不同的是对原始数据进行了数据增强。新建一个脚本,名为minivggnet_flowers17_data_aug.py,并写入以下代码:
#encoding:utf-8
from sklearn.preprocessing import LabelBinarizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from pyimagesearch.preprocessing import ImageToArrayPreprocessor as IAP
from pyimagesearch.preprocessing import AspectAwarePreprocessor as AAP
from pyimagesearch.preprocessing import ImageMove as IM
from pyimagesearch.datasets import SimpleDatasetLoader as SDL
from pyimagesearch.nn.conv import MiniVGGNet as MVN
from keras.preprocessing.image import ImageDataGenerator # 数据增强类
from keras.optimizers import SGD
from imutils import paths
import matplotlib.pyplot as plt
import numpy as np
import argparse
import os
与minivggnet_flowers17.py一样,不同的是第10行新增了数据增强模块。
接下来同样进行数据预处理
ap = argparse.ArgumentParser()
ap.add_argument('-d','--dataset',required=True,help='path to input dataset')
args = vars(ap.parse_args())
print('[INFO] moving image to label folder.....')
im = IM.MoveImageToLabel(dataPath=args['dataset'])
im.makeFolder()
im.move()
print("[INFO] loading images...")
imagePaths = [ x for x in list(paths.list_images(args['dataset'])) if x.split(os.path.sep)[-2] !='jpg']
classNames = [pt.split(os.path.sep)[-2] for pt in imagePaths ]
classNames = [str(x) for x in np.unique(classNames)]
aap = AAP.AspectAwarePreprocesser(64,64)
iap = IAP.ImageToArrayPreprocess()
sdl = SDL.SimpleDatasetLoader(preprocessors = [aap,iap])
(data,labels) = sdl.load(imagePaths,verbose = 500)
data = data.astype('float') / 255.0
(trainX,testX,trainY,testY) = train_test_split(data,labels,test_size=0.25,random_state =43)
trainY = LabelBinarizer().fit_transform(trainY)
testY = LabelBinarizer().fit_transform(testY)
数据预处理完之后,在模型训练之前,我们对train数据进行数据增强处理。
aug = ImageDataGenerator(rotation_range=30, width_shift_range=0.1,height_shift_range=0.1, shear_range=0.2, zoom_range=0.2,horizontal_flip=True, fill_mode="nearest")
主要的变换有:
通常而言,这些调整的参数值需要根据你的具体数据进行设置,一般而言,旋转幅度控制在[0,30]之间,水平和垂直平移控制在[0.1,0.2](缩放也是一样的),如果水平翻转没有改变图片的语义信息,标签没有发生变化,则应该也使用水平翻转。
接下来。对模型进行初始化
print("[INFO] compiling model...")
opt = SGD(lr=0.05)
model = MVN.MiniVGGNet.build(width=64, height=64, depth=3,classes=len(classNames))
model.compile(loss="categorical_crossentropy", optimizer=opt,metrics=["accuracy"])
由于我们使用了数据增强处理,因此,训练模型部分,需要简单进行调整
# 训练模型
print("[INFO] training network...")
# 这里使用fit_generator
H = model.fit_generator(aug.flow(trainX, trainY, batch_size=32),validation_data=(testX, testY), steps_per_epoch=len(trainX) // 32,epochs=100, verbose=1)
需要注意的是,这里使用的是.fit_generator而不是.fit,第一个参数为aug.flow,生成经过数据增强或标准化后的batch数据。flow输入的是对应的训练数据和标签。
steps_per_epoch的含义是一个epoch分成多少个batch_size, 每个epoch以经过模型的样本数达到samples_per_epoch时,记一个epoch结束。如果说训练样本树N=1000,steps_per_epoch = 10,那么相当于一个batch_size=100。
接下来,开始训练模型,并对结果进行可视化。
# 评估网络
print("[INFO] evaluating network...")
predictions = model.predict(testX, batch_size=32)
print(classification_report(testY.argmax(axis=1),
predictions.argmax(axis=1), target_names=classNames))
# loss和精度可视化
plt.style.use("ggplot")
plt.figure()
plt.plot(np.arange(0, 100), H.history["loss"], label="train_loss")
plt.plot(np.arange(0, 100), H.history["val_loss"], label="val_loss")
plt.plot(np.arange(0, 100), H.history["acc"], label="train_acc")
plt.plot(np.arange(0, 100), H.history["val_acc"], label="val_acc")
plt.title("Training Loss and Accuracy")
plt.xlabel("Epoch #")
plt.ylabel("Loss/Accuracy")
plt.legend()
注意:上面我们只对train数据进行增强处理,而对于test数据不做任何处理。
完成上述代码之后,执行下面命令,得到结果
$ python minivggnet_flowers17_data_aug.py --dataset yourpath/flowers17
precision recall f1-score support
0 0.64 0.56 0.60 16
1 0.59 0.76 0.67 21
10 1.00 0.81 0.90 16
11 0.80 0.57 0.67 21
12 0.76 0.62 0.68 26
13 0.52 0.61 0.56 23
14 0.90 0.75 0.82 24
15 0.90 0.86 0.88 22
16 0.93 0.76 0.84 17
2 0.79 0.79 0.79 14
3 0.72 0.78 0.75 23
4 0.67 0.82 0.73 22
5 0.95 0.76 0.84 25
6 0.61 0.89 0.72 19
7 0.33 0.36 0.34 14
8 0.74 0.81 0.77 21
9 0.94 0.94 0.94 16
avg / total 0.76 0.74 0.74 340
从结果中,我们可以看到,模型准确率从64%提高到76%,比上次提高了12%左右。相比准确率的提高,我们关心的是数据增强是否有助于防止过拟合。从图2.6中,虽然仍然存在过拟合现象,但是相比上次,很明显的发现使用数据增强后,降低了过拟合。首先这两个实验是相同的——我们所做的唯一改变是是否应用了数据增强。两次结果对比,可以看到数据增强可以一定程度上降低过拟合。尽管降低了训练的准确率,但是提高了测试集的准确率,从而提高模型的泛化性。
数据增强主要对训练数据进行操作的一种正则化技术。顾名思义,数据增强通过应用一系列方法随机地改变训练数据,比如平移,旋转,剪切和翻转等。数据增强的详细变换幅度需要根据具体的应用数据而设计,只要注意一点:应用这些简单的转换不能改变输入图像的标签。每个通过增强得到的图像都可以被认为是一个“新”图像。这样我们可以不断的给模型提供新的训练样本,使模型能够学习到更加具有辨别力,更具泛化性的特征。
从上述的实验结果表明,应用数据增强技术可以提高模型的准确率,同时有助于减轻过拟合。此外,数据增强也可以增加数据量,降低深度学习需要的人工标记的大量数据集。尽管收集“自然”的训练样本越多越好,但是在无法增加真实的训练样本时,数据增强可以用来克服小数据集的局限性。
详细代码:github