从cifar10分类入门深度学习图像分类(Keras)

2021-11-23 13:54:07 浏览数 (1)

之前需要做一个图像分类模型,因为刚入门,拿cifar10数据集练了下手,试了几种优化方案和不同的模型效果,这里就统一总结一下这段学习经历。

对于新手来说,最方便的深度学习框架应该是Keras了,这是一个可以基于Tensorflow、PyTorch等多种深度学习框架的高级框架,用它来搭建和训练模型特别方便,很适合入门快速掌握。这里推荐下Keras之父写的深度学习入门书籍:《Python深度学习》,轻松易懂,而且直接教你一步步用Keras搭建模型,比看西瓜书《机器学习》和花书《深度学习》效率快很多。

cifar10是是一个图像数据集(官网),包含10种类别的32*32大小的图像共60000张。另外还有cifar100,包含100种类别的更多图像。因此,cifar10分类就是一个图像多分类任务。Keras另一个好处在于已经集成了很多常见的数据集和模型,在接口里可以直接调用,当然,为了减小安装包,会在你第一次调用的时候才进行下载,但因为某些因素可能直接下载会失败,因此也可以自己先下载好后再使用,至于下载的方法网上有很多。

因此,本文要说的就是使用Keras框架来开发多种模型和优化方法去训练一个基于cifar10数据集的图像多分类模型。

环境

本文代码运行的环境为Linux,Tensorflow 1.4,Keras 2.1.1,其他的库版本就不说啦,直接安装即可,当然,跑训练最重要的还是最好有一个性能强劲的GPU。

目录

  • 简单CNN训练
  • 简单CNN数据增强
  • 基于VGG16-imagenet进行特征提取
  • 基于VGG16-imagenet进行模型微调(fine-tuning)
  • 深层CNN训练
  • 简单Resnet训练

简单CNN分类

最简单的图像分类模型就是一个层数较少的CNN(卷积神经网络)啦,至于CNN是什么,这里不介绍了,总之就是一种适合处理图像数据的网络层。先给出简单CNN的网络结构代码:

代码语言:javascript复制
def quality_classify_model():
    model = Sequential()
    model.add(Conv2D(32, (3, 3), padding='same', input_shape=(32,32,3))) # 卷积层
    model.add(Activation('relu')) # 激活函数
    model.add(MaxPooling2D(pool_size=(2, 2))) # 最大池化
    model.add(Conv2D(32, (3, 3)))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Conv2D(64, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))

    model.add(Flatten()) # 全连接层
    model.add(Dense(64))
    model.add(Activation('relu'))
    model.add(Dropout(0.5)) # 随机抛弃一半
    model.add(Dense(classes_num))
    model.add(Activation('softmax'))

    # 编译模型
    opt = keras.optimizers.rmsprop(lr=0.0001, decay=1e-6)
    model.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy'])
    return model

这网络很简单,首先我们的模型(model)使用Keras的Sequential形式定义,也就是一层一层去定义和add,最后进行编译(compile)。我们首先循环了三次“卷积层-激活函数-最大池化”的过程,卷积层就是conv2D,第一个参数表示过滤器数量,第二个参数是卷积核的尺寸,第三个是padding的模式,第四个是输入尺寸,这里我们的输入就是一个32*32的图像,至于那个3,表示图像石油RGB三层颜色构成。Keras的方便又一次体现出来,除了第一层需要我们定义输入尺寸外,后面都不再需要定义了,框架会自行判断上一层的输出尺寸就是下一层的输入尺寸。在卷积层后是一个激活函数,我们使用relu。之后是一个池化层,我们使用最大池化,这些名称的含义可以自行搜索一下。我们连续了三次这样的层定义后,就对接上一个Flatten层,也就是平铺开到一个全连接层,然后就接上一般的神经层了,有64个神经元,然后同样使用relu激活函数,再接入一个Dropout函数,这个表示我们要将64个神经元那一层中的权重随机抛弃一半,这样可以降低过拟合。最后就是接上输出层了,这层的神经元数量我们用分类数来表示,在cifar10中就是10类,最后的激活函数我们用softmax,这个函数适合多分类任务,sigmoid适合二分类任务。

网络搭建完后,需要进行编译,这和代码写完后要编译才能运行的意思差不多,我们定义了损失函数为categorical_crossentropy,优化器是自定义的rmsprop,这是一种很好用的优化器,以及要观察的指标是accuracy,也就是分类准确率。

搭建完模型网络后,我们就可以开始设置训练数据并且开始训练了:

代码语言:javascript复制
def train():
    # 数据载入
    (x_train, y_train), (x_test, y_test) = cifar10.load_data()

    # 多分类标签生成
    y_train = keras.utils.to_categorical(y_train, classes_num)
    y_test = keras.utils.to_categorical(y_test, classes_num)
    # 生成训练数据
    x_train = x_train.astype('float32')
    x_test = x_test.astype('float32')
    x_train /= 255
    x_test /= 255
    
    # 开始训练
    model = quality_classify_model()
    hist = model.fit(x_train, y_train, batch_size=64, epochs=epochs_num, validation_data=(x_test, y_test), shuffle=True)

    # 保存模型
    model.save('./change_model/cifar10_model.hdf5') 
    model.save_weights('./change_model/cifar10_model_weight.hdf5')

    # 查看准确率
    hist_dict = hist.history
    print("train acc:")
    print(hist_dict['acc'])
    print("validation acc:")
    print(hist_dict['val_acc'])

训练部分的代码也很直观,首先我们要获取数据,使用cifar10.load_data()接口就可以了,如果没下载过它会自动开始下载数据,如果已经下载过,就会直接获取。然后要根据数据生成多分类的标签,也就是每张图像属于哪个类别,我们的训练集一定要有标签,也就是一定要知道他是什么,才可能训练。为了方便训练,我们将图像数据转成float32的形式(tensorflow处理的基本都是float32类型),并且进行标准化,也就是将原本用0255表示颜色值的方式改成01表示颜色值,这有助于训练。

接着我们调用之前定义的模型(已经编译过),然后用model.fit()开始训练,里面可以加入很多参数,比如训练数据x_train, y_train,批尺寸batch_size(表示每次处理多少张图像后再统一反向传播梯度进行权重优化),训练轮数epochs(每完整过一遍所有数据为一个epoch),validation_data(在训练过程中用于验证的数据,Keras会将cifar10的5W张图像作为训练集,1W张作为验证集),以及可有可无的shuffle(是否随机打乱数据),此外还有其他的参数可以自行查官网。这个函数会返回一个数据,里面包含了每一轮训练下来的准确率、损失,是一个词典格式,所以我们获取到后输出训练和验证的准确率。

但是直接看输出的一堆数字可能没什么直观感受,因此可以使用matplotlib库来绘制一个准确率的折线图:

代码语言:javascript复制
    # 绘图
    epochs = range(1, len(train_acc) 1)
    plt.plot(epochs, train_acc, 'bo', label = 'Training acc')
    plt.plot(epochs, val_acc, 'r', label = 'Validation acc')
    plt.title('Training and validation accuracy')
    plt.legend()
    plt.savefig("accuracy.png")
    plt.figure() # 新建一个图
    plt.plot(epochs, train_loss, 'bo', label = 'Training loss')
    plt.plot(epochs, val_loss, 'r', label = 'Validation loss')
    plt.title('Training and validation loss')
    plt.legend()
    plt.savefig("loss.png")

关于如何绘制图像可以参考我这篇博客:Python使用matplotlib库绘图保存

至此,我们就完成了一个模型的训练代码,很简单吧!这全都得益于Keras的方便易用,完整的代码可以看我的github,有帮助的话可以加star,多谢~

基于这个简单CNN的训练我们可以在验证集得到72%的准确率,这对于实用还是差的太远了,还需要继续优化,因此我们考虑第一个优化方案——数据增强。

简单CNN数据增强

由于深度学习的效果很大程度上依赖于数据量,因此如果固定模型不变,效果不佳时一个很重要的优化方案就是增加数据量,但有时候我们无法简单地获取到新的图像数据,比如这个cifar10数据集,就这么多数据,再找不太现实,那怎么办呢?有一种增加数据量的方法叫做数据增强。

Keras自带一种生成相似图像数据的方式,即使用ImageDataGenerator类。简单地说就是这个类可以对原始图像进行水平/竖直移动一定范围、水平/垂直翻转图像、放大图像一定范围等等,达到生成新的同类图像的目的,这种新生成的图像还是属于同样的类别,比如你把一张猫的图像平移15%的距离,它还是一只猫,但是与原图像又不是完全一样,因此也是提升了数据丰富程度的。具体的ImageDataGenerator类使用方法可以看我这篇博客:图像训练样本量少时的数据增强技术。

要使用数据增强,不用改太多东西,只需要在获取数据的时候做一点变动就可以了:

代码语言:javascript复制
def train():
    # 数据载入
    (x_train, y_train), (x_test, y_test) = cifar10.load_data()

    # 多分类标签生成
    y_train = keras.utils.to_categorical(y_train, classes_num)
    y_test = keras.utils.to_categorical(y_test, classes_num)
    # 生成训练数据
    x_train = x_train.astype('float32')
    x_test = x_test.astype('float32')
    # x_train /= 255
    x_test /= 255

    train_datagan = ImageDataGenerator(rescale=1./255, rotation_range=15, width_shift_range=0.15, height_shift_range=0.15, fill_mode='wrap')
    # test_datagen = ImageDataGenerator(rescale=1./255)

    model = quality_classify_model()
    hist = model.fit_generator(train_datagan.flow(x_train, y_train, batch_size=batch_size), steps_per_epoch = x_train.shape[0] // batch_size, epochs = epochs_num, validation_data=(x_test,y_test), shuffle=True)
    # hist = model.fit(x_train, y_train, batch_size=64, epochs=epochs_num, validation_data=(x_test, y_test), shuffle=True)

    model.save('./augment_model/cifar10_model.hdf5') 
    model.save_weights('./augment_model/cifar10_model_weight.hdf5')

对比一下上一节的train函数,就可以发现不同点,首先,对于训练数据,我用ImageDataGenerator类返回了一个train_datagan ,用来生成训练的数据,这个ImageDataGenerator中就包含了一些变化的方式,包括标准化(1./255这个即使是一般的训练也都需要)、旋转角度、水平移动、竖直移动。

需要注意的是,我们只只应该对训练数据进行数据增强,对于验证集,不要去变动,因此我们只需要同样做标准化即可。

在开始训练的时候,也从fit函数改成了fit_generator函数,这个函数才能接受ImageDataGenerator类返回的train_datagan作为输入,也就是train_datagan.flow()函数,其中设置了原始数据、批尺寸batch_size,这里同样是每一次处理的数据量,steps_per_epoch 是指每一轮训练需要多少步,epoch还是原来那个epoch,也就是要完整训练多少轮,因此这个steps_per_epoch 就是总数据量除以批尺寸,这是为了让所有图像都有机会被处理到。其他就没什么不同啦。

同样,完整的代码可以看我的github

基于VGG16-imagenet进行特征提取

训练效果不好还有一种可能性是因为我们的模型不好,毕竟我们就是很简单的一个浅层CNN模型,那怎样的模型才叫好呢?有句话叫站在巨人的肩膀上,而在深度学习方面也有这种做法,而且还有个专有名词,叫做迁移学习。

所谓迁移学习其实就是一种思想,意思就是把某个优秀模型的能力迁移到其他任务中去,在这里,我们要做的就是找某个已经预训练好的效果很好的图像分类模型,基于它来完成我们的图像分类任务。这里我们找的巨人就是在imagenet图像数据集(1000个类别的大数据集)上预训练好的VGG16网络模型。

还记得我们简单CNN的模型结构吧,几个卷积层池化层,然后输入到全连接网络去逐渐分类。后面的全连接网络部分实际上就是个分类器,而前面的卷积层池化层(卷积基)的工作目的就在于提取特征。我们想象一下预训练好的VGG16已经能够较好地完成imagenet数据集的分类任务了,那么它一定是在识别图像上有一定的过人之处,我们就把它识别图像的能力拿过来(卷积基),在这个基础上只去训练分类器部分(全连接层后续部分),这就叫基于预训练的网络进行特征提取。

要利用预训练好的模型,首先肯定得加载它,Keras也提供了一些常用的预训练好的模型,同cifar10数据集一样,在第一次调用时会下载,如果下载失败,可以参考我这篇博客:keras离线下载模型的存储位置,来自行下载和使用。

如前所说,我们要使用VGG16的卷积基,因此我们自己的模型网络只需要分类器部分就可以了:

代码语言:javascript复制
def quality_classify_model():
    model = Sequential()
    model.add(Flatten(input_shape=(4,4,512)))# 4*4*512
    model.add(Dense(256, activation='relu'))
    model.add(Dropout(0.5))
    model.add(Dense(classes_num, activation='softmax'))  # 多分类

    opt = keras.optimizers.rmsprop(lr=0.0001, decay=1e-6)
    model.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy'])
    return model

其实就是把前面的卷积基部分删掉了。

然后在训练过程中,我们要加载并调用VGG16模型,去处理我们的图像数据,得到它提取出来的特征,然后再把这个提取出来的特征输入我们的模型网络去训练我们的分类器部分:

代码语言:javascript复制
def train():
    # 数据载入
    (x_train, y_train), (x_test, y_test) = cifar10.load_data()

    # 多分类标签生成
    y_train = keras.utils.to_categorical(y_train, classes_num)
    y_test = keras.utils.to_categorical(y_test, classes_num)
    # 生成训练数据
    x_train = x_train.astype('float32')
    x_test = x_test.astype('float32')

    datagan = ImageDataGenerator(rescale=1./255)

    # 加载预训练好的卷积基
    conv_base = VGG16(include_top=False, weights='imagenet')

    # 用预训练好的卷积基处理训练集提取特征
    sample_count = len(y_train)
    train_features = np.zeros(shape=(sample_count, 4, 4, 512))
    train_labels = np.zeros(shape=(sample_count, classes_num))
    train_generator = datagan.flow(x_train, y_train, batch_size=batch_size)
    i = 0
    for inputs_batch, labels_batch in train_generator:
        features_batch = conv_base.predict(inputs_batch)
        train_features[i * batch_size : (i   1) * batch_size] = features_batch
        train_labels[i * batch_size : (i   1) * batch_size] = labels_batch
        i  = 1
        if i * batch_size >= sample_count:
            break
    # train_features = np.reshape(train_features, (sample_count, 4*4*512))

    # 用预训练好的卷积基处理验证集提取特征
    sample_count = len(y_test)
    test_generator = datagan.flow(x_test, y_test, batch_size=batch_size)
    test_features = np.zeros(shape=(sample_count, 4, 4, 512))
    test_labels = np.zeros(shape=(sample_count, classes_num))
    i = 0
    for inputs_batch, labels_batch in test_generator:
        features_batch = conv_base.predict(inputs_batch)
        test_features[i * batch_size : (i   1) * batch_size] = features_batch
        test_labels[i * batch_size : (i   1) * batch_size] = labels_batch
        i  = 1
        if i * batch_size >= sample_count:
            break
    # test_features = np.reshape(test_features, (sample_count, 4*4*512))

    model = quality_classify_model()

    # hist = model.fit_generator(train_datagan.flow(x_train, y_train, batch_size=batch_size), steps_per_epoch = 8000, epochs = epochs_num, validation_data=(x_test,y_test), shuffle=True)
    hist = model.fit(train_features, train_labels, batch_size=batch_size, epochs=epochs_num, validation_data=(test_features, test_labels))

    model.save('./extract_features/cifar10_model.hdf5') 
    model.save_weights('./extract_features/cifar10_model_weight.hdf5')

    hist_dict = hist.history
    print("train acc:")
    print(hist_dict['acc'])
    print("validation acc:")
    print(hist_dict['val_acc'])

看注释应该比较好理解,与之前不同的就在于我们先把训练和验证数据经过了一遍VGG模型的处理,得到特征向量,然后真正输入我们自己模型做训练的是这些特征向量。

完整的代码可以看我的github

基于VGG16-imagenet进行模型微调(fine-tuning)

迁移学习的另一种做法是不单单只重新训练分类器部分,而是还去训练卷积基的一部分。毕竟卷积基可能由很多个卷积层池化层组成,我们冻结接近输入端的一大部分卷积基,而同时训练靠后的一小部分卷积基和分类器,这就叫做微调(fine-tuning)。这个做法其实和上一节的特征提取差不多,只是我们训练的层多一点而已。Keras支持对部分层进行“冻结”,即不在训练时改变其既有的权重参数,只改变未被冻结的部分,这个做法在我开头推荐的《Python深度学习》书中有详细的介绍,相信Keras官网中也会有相应的例子,这里就不展开说明了(其实是因为我也没做)。

当然,特征提取和微调都可以结合数据增强去做,换句话说,任何的模型优化方案都可以结合数据增强去在数据部分进行优化,至于要使用那些增加数据的方式,得看你的需求上什么变换是合理的。比如如果你要判断图像中物体的完整性,就不能用横移来做变换,因为这可能会将图像中原本完整的物体给移动到不完整了。

深层CNN训练

实际上,如果你不想弄得那么复杂,又是找模型又是冻结微调的,还有一种方案可以简单粗暴地提升准确率,那就是加深你的网络层数。

实际上我们最开始写的网络是非常浅的,所以效果不好是理所当然的,这一节我们就粗暴地去直接增加网络深度,毕竟这是个深度学习的事儿。我们直接不断地循环增加卷积基部分,增加到40层的CNN:

代码语言:javascript复制
def quality_classify_model():
    model = Sequential()

    model.add(Conv2D(32, (3, 3), padding='same', input_shape=(32,32,3)))
    model.add(Activation('relu'))
    model.add(Conv2D(32, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(32, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(48, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(48, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))

    model.add(Conv2D(80, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(80, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(80, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(80, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(80, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))

    model.add(Conv2D(128, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(128, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(128, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(128, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(128, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(GlobalMaxPooling2D())
    model.add(Dropout(0.25))

    model.add(Dense(500))
    model.add(Activation('relu'))
    model.add(Dropout(0.25))
    model.add(Dense(classes_num))
    model.add(Activation('softmax'))
    # model.summary () # 输出网络结构信息

    opt = keras.optimizers.Adam(lr=0.0001)
    model.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy'])

    return model

代码中可以看到,就是简单粗暴的提升层数,但是注意需要不断地增加过滤器的数量,并且适当地dropout一些神经元比例既可以减少计算量,也可以增强训练效果。此外,我们把最后编译用的优化器改成了Adam,Adam的思想和之前用的rmsprop思想差不多,都是在用梯度下降算法找最优解的时候既给出前进方向又去避免震荡地太厉害,可以理解为更稳更快地找到最优解。

训练部分和使用简单CNN时都一样,完整的代码可以看我的github

使用深层CNN后效果拔群,能得到训练集99%、测试集83.5%的准确率,训练到接近200轮后开始出现过拟合。同样,我们还是可以使用数据增强,增强后测试集准确率增长到了89%。

努力了半天,还是没上90%,怎么办?

简单Resnet训练

想要准确率更高,继续加深CNN层数是一个方法,但更高的层数会带来训练时间的显著增加,并且可能会由于层数太高,发生退化的问题:随着网络深度的增加,准确率达到饱和(这可能并不奇怪)然后迅速下降。这个原因可能是因为网络太长,梯度在不断的反向传播过程中会越来越小,就像0.99的n次方会非常小一样,这叫做梯度消失。

为了解决这个问题,几位大神创造性地提出了Resnet,能够很好地解决深层网络的问题,Resnet的作者都是中国人,其中一位现在还是一家独角兽自动驾驶创业公司(Momenta,腾讯投资)的研发总监,厉害呀。关于Resnet本身这里不做讲解,有兴趣的可以看一看论文:https://arxiv.org/abs/1512.03385

我们使用Resnet构建网络,首先搭建一个Resnet的Block模块,长这样:

这个模块包含了一个卷积层,一个BN层,一个激活层。代码如下:

代码语言:javascript复制
def resnet_block(inputs, num_filters=16, kernel_size=3,strides=1, activation='relu'):
    x = Conv2D(num_filters, kernel_size=kernel_size, strides=strides, padding='same',
           kernel_initializer='he_normal', kernel_regularizer=l2(1e-4))(inputs)
    x = BatchNormalization()(x)
    if (activation):
        x = Activation('relu')(x)
    return x

首先对输入x进行卷积,然后每次都做一次BN(BatchNormalization),也就是批标准化,这个是为了克服神经网络层数加深,收敛速度变慢,常常导致梯度消失(vanishing gradient problem)或梯度爆炸(gradient explore),通过引入批标准化来规范某些层或者所有层的输入,从而固定每层输入信号的均值与方差。因为第二层后面没有直接跟relu激活函数,所以需要判断一下。

接着我们就可以用这个block来搭建网络了:

代码语言:javascript复制
# 建一个20层的ResNet网络 
def resnet(input_shape):
    inputs = Input(shape=input_shape)# Input层,用来当做占位使用
    
    #第一层
    x = resnet_block(inputs)
    print('layer1,xshape:',x.shape)
    # 第2~7层
    for i in range(6):
        a = resnet_block(inputs = x)
        b = resnet_block(inputs=a,activation=None)
        x = keras.layers.add([x,b])
        x = Activation('relu')(x)
    # out:32*32*16
    # 第8~13层
    for i in range(6):
        if i == 0:
            a = resnet_block(inputs = x,strides=2,num_filters=32)
        else:
            a = resnet_block(inputs = x,num_filters=32)
        b = resnet_block(inputs=a,activation=None,num_filters=32)
        if i==0:
            x = Conv2D(32,kernel_size=3,strides=2,padding='same',
                       kernel_initializer='he_normal',kernel_regularizer=l2(1e-4))(x)
        x = keras.layers.add([x,b])
        x = Activation('relu')(x)
    # out:16*16*32
    # 第14~19层
    for i in range(6):
        if i ==0 :
            a = resnet_block(inputs = x,strides=2,num_filters=64)
        else:
            a = resnet_block(inputs = x,num_filters=64)

        b = resnet_block(inputs=a,activation=None,num_filters=64)
        if i == 0:
            x = Conv2D(64,kernel_size=3,strides=2,padding='same',
                       kernel_initializer='he_normal',kernel_regularizer=l2(1e-4))(x)
        x = keras.layers.add([x,b])# 相加操作,要求x、b shape完全一致
        x = Activation('relu')(x)
    # out:8*8*64
    # 第20层   
    x = AveragePooling2D(pool_size=2)(x)
    # out:4*4*64
    y = Flatten()(x)
    # out:1024
    outputs = Dense(10,activation='softmax',
                    kernel_initializer='he_normal')(y)
    
    #初始化模型
    #之前的操作只是将多个神经网络层进行了相连,通过下面这一句的初始化操作,才算真正完成了一个模型的结构初始化
    model = Model(inputs=inputs,outputs=outputs)
    return model

代码注释很详尽了,我们一共构建了20层,其中每过6层我们将步长设为2一次,因此每过六层尺寸会缩小一半。而且这里我们构建模型不再是之前的Sequential形式,而是函数式编程的方式,可以参考官网文档:快速开始函数式(Functional)模型

在学习率方面,这里我们使用一个动态的学习率,也就是学习率会随着学习的过程不断减少,毕竟越接近最优解,我们步子就越要迈得小一点,以防错过。

代码语言:javascript复制
# 动态变化学习率
def lr_sch(epoch):
    #200 total
    if epoch <50:
        return 1e-3
    if 50<=epoch<100:
        return 1e-4
    if epoch>=100:
        return 1e-5

这里的意思就是每过一定轮次就开始减小学习率。此外,我们还使用ReduceLROnPlateau类,来在训练没有增加效果的时候主动降低学习率,这也可以在官方文档查看。因此训练部分的代码变成:

代码语言:javascript复制
def train():
    # 数据载入
    (x_train, y_train), (x_test, y_test) = cifar10.load_data()

    # 多分类标签生成
    y_train = keras.utils.to_categorical(y_train, classes_num)
    y_test = keras.utils.to_categorical(y_test, classes_num)
    # 生成训练数据
    x_train = x_train.astype('float32')
    x_test = x_test.astype('float32')
    x_train /= 255
    x_test /= 255

    # train_datagan = ImageDataGenerator(rescale=1./255, rotation_range=10, width_shift_range=0.1, height_shift_range=0.1, fill_mode='wrap')
    # test_datagen = ImageDataGenerator(rescale=1./255)

    model = resnet((32,32,3))
    model.compile(loss='categorical_crossentropy', optimizer=Adam(), metrics=['accuracy'])

    checkpoint = ModelCheckpoint(filepath='./resnet_model/cifar10_resnet_ckpt.h5', monitor='val_acc', verbose=1,save_best_only=True)
    lr_scheduler = LearningRateScheduler(lr_sch)
    lr_reducer = ReduceLROnPlateau(monitor='val_acc', factor=0.2, patience=5, mode='max', min_lr=1e-3)
    callbacks = [checkpoint, lr_scheduler, lr_reducer]
    hist = model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs_num, validation_data=(x_test,y_test), verbose=1,callbacks=callbacks)

    # hist = model.fit_generator(train_datagan.flow(x_train, y_train, batch_size=batch_size), steps_per_epoch = 8000, epochs = epochs_num, validation_data=(x_test,y_test), verbose=1, callbacks=callbacks)

    hist_dict = hist.history
    print("train acc:")
    print(hist_dict['acc'])
    print("validation acc:")
    print(hist_dict['val_acc'])

    train_acc = hist.history['acc']
    val_acc = hist.history['val_acc']
    train_loss = hist.history['loss']
    val_loss = hist.history['val_loss']

代码中我们使用了callbacks参数,用来动态降低学习率,以及随时在准确率增加是保存我们的模型,也就是ModelCheckpoint做的事情。这些都可以在官方文档查看。

注释部分的代码是使用数据增强的模式,和之前的一样,有兴趣可以试试。

实验结果为,使用Resnet网络训练cifar10分类,100轮以内准确率就基本不再提高,训练集达到100%,验证集达到82.7%,和深层CNN的效果差不多。使用数据增强后的Resnet网络,训练70轮就能达到验证89%的准确率。这些数据说明Resnet确实可以以更小的层数和更快的速度达到深层CNN的效果,如果想要更高的准确率,还可以加深Resnet的深度。

完整的代码可以看我的github

以上,就是用Keras实验各种模型和优化方法来训练cifar10图像分类了,我认为这是一个很好的入手深度学习图像分类的案例,而Keras也是一个很好上手的框架,在这段学习过程中我受益良多。如果你有更多的GPU,想要训练更大的网络或者针对更大的图像,那也可以使用Keras提供的简单多GPU方式,我在这篇博客中有介绍:Keras多GPU训练。写完收工,继续搬砖,谢谢观看。

整体工程:https://github.com/Cloudox/Cifar10-Classify

参考文章:

https://cloud.tencent.com/developer/article/1010851

https://blog.csdn.net/nima1994/article/details/79910597

https://blog.csdn.net/tsyccnh/article/details/78838005

https://blog.csdn.net/tsyccnh/article/details/78865167

https://blog.csdn.net/u010177286/article/details/79451590

0 人点赞