keras的图像预处理全攻略(五)—— ImageDataGenerator 类结合神经网络的实践

时间:2024-05-23 10:51:07

keras的图像预处理全攻略(五)—— ImageDataGenerator 类结合神经网络的实践

keras的图像预处理全攻略(五)—— ImageDataGenerator 类结合神经网络的实践

前言:前面的系列文章已经系统的说明了keras的图像预处理操作,有原理部分、也有少量实践部分,可以参考下面的文章:

keras的图像预处理全攻略(四)—— ImageDataGenerator 类实践案例

keras的图像预处理全攻略(三)—— ImageDataGenerator 类的辅助类

keras的图像预处理全攻略(二)—— ImageDataGenerator 类

keras的图像预处理全攻略(一)——基本的图像变换(Image Transform)方法

但是数据增强操作对于一个神经网络最终的训练效果真的有多少影响呢?我们还得通过试验来加以证实,本文也是系列文章的最后一篇,将详细说明data augmentation对于神经网络的重要性。一些重复的东西这里将不再赘述。

一、ImageDataGenerator对于单张图片的增强变换

这里就直接上完全的代码了,不再像前面一样分步骤书写,如下:

#from keras.preprocessing.image import ImageDataGenerator
from keras.preprocessing import image  #导入image.py模块
import numpy as np
import matplotlib.pyplot as plt

#第一步:构造ImageDataGenerator类的对象
img_generator = image.ImageDataGenerator(
    rotation_range = 90,       # rotation_range 旋转
    width_shift_range = 0.2,   # width_shift_range 左右平移
    height_shift_range = 0.2,  # height_shift_range 上下平移
    zoom_range = 0.3           # zoom_range 随机放大或缩小
    )

# 导入并显示图片
img_path = './images/1001.jpg'
img = image.load_img(img_path)  #返回的是一个 PIL.Image类的对象,当然我也可以自己使用pillow库也一样
plt.imshow(img)
plt.show()

# 将图片转为数组
x = image.img_to_array(img)
# 扩充一个维度
x = np.expand_dims(x, axis=0)  #本身的维度是(width,height,channel),现在拓展为(batch,width,height,channel)

#第二步:使用flow方法产生一个迭代器
# 生成图片,这里只有一张图片,所以batch_size=1
gen = img_generator.flow(x, batch_size=1)

#第三步:对迭代器进行迭代
#注意:这里的迭代是一直一直一直无限循环的生成,前面的系列文章也已经说明了
# 显示生成的图片
plt.figure()
for i in range(3):
    for j in range(3):
        x_batch = next(gen)
        idx = (3*i) + j
        plt.subplot(3, 3, idx+1)
        plt.imshow(x_batch[0]/256)

plt.show()

运行结果为:

keras的图像预处理全攻略(五)—— ImageDataGenerator 类结合神经网络的实践keras的图像预处理全攻略(五)—— ImageDataGenerator 类结合神经网络的实践

需要注意的是:

(1)flow()将会返回一个生成器。这个生成器用来扩充数据,每次都会产生batch_size个样本。 因为目前我们只导入了一张图片,因此每次生成的图片都是基于这张图片而产生的,可以看到结果,旋转、位移、放大缩小,统统都有。

(2)flow()有很多的可选参数实现不同的功能。可以将产生的图片进行保存,它还有很多其它的参数,前面的系列文章都有介绍,请参见前文。

(3)生成器的迭代过程是无限的。生成图片的过程大概是这样的,并且可以一直一直一直无限循环的生成 

keras的图像预处理全攻略(五)—— ImageDataGenerator 类结合神经网络的实践
 

二、不使用ImageDataGenerator结合神经网络的案例

在我们的实际生活中,通常情况下,我们应该是有一个不太大的训练集,当数据量有限的时候,这就需要Data Aumentation了,或者为了防止过拟合有时候也需要使用Data Aumentation技术,总之,就是对一组数据进行Data Aumentation。这里我们以cifar-10数据集做一个演示。

注意:完整的cifar-10数据集的基本信息如下:

(50000, 32, 32, 3) (50000, 1) (10000, 32, 32, 3) (10000, 1),即训练集有50000张,测试集有10000张,我们为了验证data augmentation的作用,现在不需要这么多的训练数据集,假设我的训练数据集也就10000张,所以我需要从50000张训练数据中取出10000张训练数据。

2.1 数据的准备工作——只选取10000张训练图片(cifar10_data.py)

import numpy as np
import matplotlib.pyplot as plt

from keras.datasets import cifar10
from keras.utils import np_utils

#像素数据预处理
def preprocess_data(x):
    x /= 255
    x -= 0.5
    x *= 2
    return x

#选择10000组数据,即20%的cifar数据,并进行保存,保存为npz文件
def select_save_data(x_train,y_train,x_test,y_test):
    # 预处理
    x_train = x_train.astype(np.float32)
    x_test = x_test.astype(np.float32)

    x_train = preprocess_data(x_train)
    x_test = preprocess_data(x_test)

    # 对类别进行编码
    n_classes = 10
    y_train = np_utils.to_categorical(y_train, n_classes)
    y_test = np_utils.to_categorical(y_test, n_classes)


    # 取 20% 的训练数据,由于是为了验证data augmentation的作用,我们不是用完整的cifar-10数据集,只是用其中的20%
    x_train_part = x_train[:10000]
    y_train_part = y_train[:10000]
    print(x_train_part.shape, y_train_part.shape)    #(10000, 32, 32, 3) (10000, 10)

    np.save("cifar10/x_train.npy",x_train_part)
    np.save("cifar10/x_test.npy",x_test)
    np.save("cifar10/y_train.npy",y_train_part)
    np.save("cifar10/y_test.npy",y_test)
    print("选择并保存完毕!")

#载入保存的训练数据
def load_cifar_data(directory:str):
    x_train=np.load(directory+"/x_train.npy")
    y_train=np.load(directory+"/y_train.npy")
    x_test=np.load(directory+"/x_test.npy")
    y_test=np.load(directory+"/y_test.npy")
    return x_train,y_train,x_test,y_test

if __name__=='__main__':
    (x_train, y_train),(x_test, y_test) = cifar10.load_data()  #我是自己已经下载了cifar数据集,并保存在用户目录之下的.keras/datasets/cifar-10-batches-py.tar.gz
                                                           #如果是要自己在线下载可能会比较麻烦,很慢而且很容易失败
    print(x_train.shape, y_train.shape, x_test.shape, y_test.shape) #(50000, 32, 32, 3) (50000, 1) (10000, 32, 32, 3) (10000, 1)
    select_save_data(x_train,y_train,x_test,y_test)

2.2 搭建网络模型进行训练并且进行可视化(cifar10_train.py)

import numpy as np
import matplotlib.pyplot as plt

from keras.layers.core import Dense, Flatten, Activation, Dropout
from keras.layers.convolutional import Conv2D
from keras.layers.pooling import MaxPooling2D
from keras.layers.normalization import BatchNormalization
from keras.models import Sequential
from keras.utils import plot_model

from cifar10_data import load_cifar_data  #导入自己写的数据模块

#================================================================================================
# 建立一个简单的卷积神经网络
def cifar10_model():
    model = Sequential()

    model.add(Conv2D(64, (3,3), input_shape=(32,32,3)))
    model.add(Activation('relu'))
    model.add(BatchNormalization(scale=False, center=False))

    model.add(Conv2D(32, (3,3)))
    model.add(Activation('relu'))
    model.add(MaxPooling2D((2,2)))
    model.add(Dropout(0.2))
    model.add(BatchNormalization(scale=False, center=False))

    model.add(Flatten())
    model.add(Dense(256))
    model.add(Activation('relu'))
    model.add(Dropout(0.2))
    model.add(BatchNormalization())

    model.add(Dense(10))
    model.add(Activation('softmax'))
    
    model.summary()
    return model
    
#训练模型
def train_model(model,x_train,y_train,batch_size=128,epochs=30):
    model.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy'])
    history=model.fit(x_train, y_train, epochs=epochs, batch_size=batch_size, verbose=1, validation_split=0.1)  #0.1的比例作为验证集
    return history,model

#保存模型
def save_model(model,filename):
    model.save(filename)
    plot_model(model,to_file="cifar10_model.png",show_shapes=True)

def tensorboard_model(model):
    ...

#可视化训练过程
def visual_train(history,epochs):
    plt.figure()
    plt.subplot(2,2,1)
    plt.plot(np.arange(epochs),history.history['loss'],label="train-loss")
    plt.legend()
    plt.subplot(2,2,2)
    plt.plot(np.arange(epochs),history.history['acc'],label="train-acc")
    plt.legend()
    plt.subplot(2,2,3)
    plt.plot(np.arange(epochs),history.history['val_loss'],label="validation-loss")
    plt.legend()
    plt.subplot(2,2,4)
    plt.plot(np.arange(epochs),history.history['val_acc'],label="validation-acc")
    plt.legend()
    #plt.show()
    plt.savefig("cifar10_model/visual_train.png",format="png")


if __name__=='__main__':
    x_train,y_train,x_test,y_test=load_cifar_data("cifar10")  #导入数据
    model=cifar10_model()                                     #构建模型
    history,model=train_model(model,x_train,y_train)          #训练模型
    save_model(model,"cifar10_model/model.h5")                #保存模型
    visual_train(history,30)                                  #训练过程可视化

最后训练的结果如下所示:

(1)最后几个epochs的损失以及准确率

Epoch 26/30
9000/9000 [==============================] - 1s 125us/step - loss: 0.0525 - acc: 0.9852 - val_loss: 1.5960 - val_acc: 0.6140
Epoch 27/30
9000/9000 [==============================] - 1s 123us/step - loss: 0.0510 - acc: 0.9853 - val_loss: 1.6012 - val_acc: 0.6130
Epoch 28/30
9000/9000 [==============================] - 1s 123us/step - loss: 0.0510 - acc: 0.9856 - val_loss: 1.7113 - val_acc: 0.6100
Epoch 29/30
9000/9000 [==============================] - 1s 124us/step - loss: 0.0449 - acc: 0.9883 - val_loss: 1.6147 - val_acc: 0.6100
Epoch 30/30
9000/9000 [==============================] - 1s 122us/step - loss: 0.0399 - acc: 0.9894 - val_loss: 1.6564 - val_acc: 0.6250

(2)误差曲线

keras的图像预处理全攻略(五)—— ImageDataGenerator 类结合神经网络的实践

(3)模型结构

keras的图像预处理全攻略(五)—— ImageDataGenerator 类结合神经网络的实践

2.3 模型的测试(cifar10_test.py)

import keras

from cifar10_data import load_cifar_data  #导入自己写的数据模块

#================================================================================================
def load_model(filename,x_test,y_test):
    model=keras.models.load_model(filename)
    loss, acc = model.evaluate(x_test, y_test, batch_size=32)
    print('Loss: ', loss)
    print('Accuracy: ', acc)


if __name__=='__main__':
    x_train,y_train,x_test,y_test=load_cifar_data("cifar10")  #导入数据
    load_model("cifar10_model/model.h5",x_test,y_test)        #测试模型
    

测试的结果如下:

10000/10000 [==============================] - 2s 177us/step
Loss:  1.8028176406860352
Accuracy:  0.6101

经过20轮的训练之后,在训练集上已经有98%的准确率,但是在验证集上的准确率只有62%,测试集上只有61%左右的准确率,可以说是过拟合了,而且从训练集和验证集上的训练曲线也可以看出,模型存在过拟合现象,主要原因就是训练集太小了,无法达到很好的效果。

那怎么办呢?那么接下来我们试试经过Data Augmentation之后的准确率如何。

三、ImageDataGenerator结合神经网络的案例

由于上面的模型因为训练样本太少9000组,比测试数据还少1000组,我们现在来进行数据增强,

新增一个data_augmentation_cifar.py,添加代码如下:

import numpy as np
import matplotlib.pyplot as plt

from keras.preprocessing.image import ImageDataGenerator

from cifar10_data import load_cifar_data  #导入自己写的数据模块
import cifar10_train as farmodel          #导入自己写的数据模块

# 设置生成参数
img_generator = ImageDataGenerator(
    rotation_range = 20,
    width_shift_range = 0.2,
    height_shift_range = 0.2,
    zoom_range = 0.2
    )

x_train,y_train,x_test,y_test=load_cifar_data("cifar10")  #导入数据
print(np.shape(x_train),np.shape(y_train),np.shape(x_test),np.shape(y_test))

model=farmodel.cifar10_model()  #导入模型
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])

history=model.fit_generator(img_generator.flow(x_train,y_train,batch_size=128,shuffle
                   =True),
                   epochs=100,
                   verbose=1,
                    )


epochs=100

plt.figure()
plt.subplot(1,2,1)
plt.plot(np.arange(epochs),history.history['loss'],label="train-loss")
plt.legend()
plt.subplot(1,2,2)
plt.plot(np.arange(epochs),history.history['acc'],label="train-acc")
plt.legend()
plt.show()
plt.savefig("cifar10_model/augmentation_visual_train.png",format="png")

loss, acc = model.evaluate(x_test, y_test, batch_size=32)
print('Loss: ', loss)
print('Accuracy: ', acc)

程序的运行结果如下:

由于Data Augmentation后,数据变多了,因此我们需要更的训练次数,这里选择epochs=100

(1)最后几个epochs的训练结果

Epoch 96/100
79/79 [==============================] - 3s 34ms/step - loss: 0.8282 - acc: 0.7114
Epoch 97/100
79/79 [==============================] - 3s 33ms/step - loss: 0.8142 - acc: 0.7154
Epoch 98/100
79/79 [==============================] - 3s 33ms/step - loss: 0.8074 - acc: 0.7178
Epoch 99/100
79/79 [==============================] - 3s 34ms/step - loss: 0.8104 - acc: 0.7162
Epoch 100/100
79/79 [==============================] - 3s 33ms/step - loss: 0.8147 - acc: 0.7126

注意:前面是训练9000组数据,所以在[===================================]这个进度条前面显示的是9000/9000,我们在训练的时候可以看到前面的那个数在不断增加,那这个地方为什么是 79/79 呢?

其实这是model.fit_generator()函数导致的,因为该函数接受的是由flow方法所产生的生成器进行迭代的数据,每一次迭代128组数据,共有10000组样本,则10000除以128=78.125,则需要迭代79个轮次,这就是这个79的由来了。

(2)测试结果

10000/10000 [==============================] - 1s 78us/step
Loss:  1.0293259397506713
Accuracy:  0.6819

总结:从上面的结果可以看出,在训练集上准确率为71%,在测试集上面准确率为68%,这基本上解决了“过拟合问题”,很多人可能认为,在训练集上才71%的准确率,这不是欠拟合吗,是的,但是通过误差曲线分析,由于是对原始数据进行了数据增强,每一次对增强的数据进行训练,而且每一次增强的数据有不是一样的,实际上训练数据量是大大增加了的,这也就是为什么这个地方要增加epochs,实际上通过误差曲线反映出,训练100个epoch依然还没有达到收敛,所以会表现出欠拟合的状态,但是前面由于训练样本欠缺导致的过拟合现象已经解决了。

思考:为什么使用data augmentation相当于大大增加了样本数量?不还是那10000组原始图片吗?

首先如果不使用data augmentation,那么每一个epoch都是使用这10000组数据,这10000组数据始终都是没有变化的。

但是如果使用了data augmentation,因为没一个epoch会对10000组样本数据产生data augmentation,而data augmentation之后的图片和之前的呐10000张图片是不一样的,所以实际上相当于是在没一个epoch都训练不一样的样本数据,这就是为什么说data augmentation技术大大增加了样本数量,在上面的例子中,计时训练了100个epoch依然没有收敛。

四、如何对那些被ImageDataGenerator处理过的数据进行训练

4.1 直接使用model.fit_generator()函数

如上面的例子所示,上面的例子里面没有使用验证数据集,那如果要使用验证数据该怎么办呢?实际上一样的道理,都对需要验证的数据也构造一个ImageDataGenerator对象,然后像下面那样:

# 训练数据集的data augmentation
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
)
# 验证数据集的data augmentation
validation_datagen = ImageDataGenerator(rescale=1./255) # 验证集不用增强

# 通过flow、flow_from_directory、flow_from_dateframe方法构造迭代器
train_generator = train_datagen.flow_from_directory(
    train_dir,
    target_size=(150, 150),
    batch_size=32,
    class_mode='binary'
)
validation_generator = validation_datagen.flow_from_directory(
    validation_dir,
    target_size=(150, 150),
    batch_size=32,
    class_mode='binary'
)

#使用model.fit_generator进行训练
history = model.fit_generator(
    train_generator,
    steps_per_epoch=100,
    epochs=100,
    validation_data=validation_generator,  #验证集其实就是多了这一个参数而已,
    validation_steps=50
)

4.2 手动进行迭代过程然后进行训练

# 训练100个epochs
for e in range(100):
    print(f'the {e} epoch is training...')
    progbar = generic_utils.Progbar(x_train_part.shape[0])  #这是一个产生进度条的类
    batches = 0

    for x_batch, y_batch in img_generator.flow(x_train, y_train, batch_size=128,shuffle=True):
        loss,acc = model.train_on_batch(x_batch, y_batch)
        batches += x_batch.shape[0]
        if batches > x_train_part.shape[0]:
            break
        progbar.add(x_batch.shape[0], values=[('train loss', loss),('train acc', train_acc)])

如果是不需要进度条这些,则进一步简化为下面的代码:

# 训练100个epochs
for e in range(100):
    print(f'the {e} epoch is training...')
   
    batches = 0 #这里的batch就是迭代的次数,在本例中,从0开始一直到79,第79次迭代就把10000组数据迭代完了
    for x_batch, y_batch in img_generator.flow(x_train, y_train, batch_size=128,shuffle=True):
        loss,acc = model.train_on_batch(x_batch, y_batch)
        batches += x_batch.shape[0]
        if batches > x_train_part.shape[0]:
            break