跳至主要内容

使用 Keras 在 Python 中实现卷积神经网络

在本教程中,您将学习如何使用 Keras 在 Python 中实现卷积神经网络(CNN),以及如何通过 dropout 缓解过拟合。
· 15分钟 阅读

使用 AI 探索

您可能已经听说过图像或人脸识别,或自动驾驶汽车。这些都是真实场景下卷积神经网络(CNN)的应用。在这篇博文中,您将学习并理解如何在 Keras 中实现这种深层、前馈的人工神经网络,同时学习如何使用名为 "dropout" 的正则化技术来克服过拟合。

更具体地说,今天的教程将涵盖以下主题:

  • 首先介绍卷积神经网络;
  • 接着,先理解数据。您将使用 Python 及其库来加载、探索和分析数据,
  • 然后,您将进行数据预处理:学习如何调整尺寸、缩放、将标签转换为独热编码向量,并将数据拆分为训练集和验证集;
  • 完成以上步骤后,您就可以构建神经网络模型:学习如何对数据建模并搭建网络。随后,编译、训练并评估模型,并可视化准确率与损失曲线;
  • 接下来,您将了解过拟合的概念,以及如何通过添加 dropout 层来缓解它;
  • 基于这些信息,您可以回到最初的模型并重新训练。您还将重新评估新模型,并比较两个模型的结果;
  • 然后,您将对测试数据进行预测,将概率转换为类别标签,并绘制一些模型分类正确和错误的测试样本;
  • 最后,您将可视化分类报告,从中更深入地理解模型在哪些类别上(不)准确。

想在 Python 中系统学习 Keras 与深度学习吗?不妨考虑 DataCamp 的Python 深度学习入门课程!

另外别错过我们的Keras 速查表,它通过代码示例展示用 Python 构建神经网络所需经历的六个步骤!

卷积神经网络:简介

此时,您或许已经了解了机器学习和深度学习——一门研究能学习的算法设计的计算机科学分支。深度学习是机器学习的一个子领域,受人工神经网络启发,而人工神经网络又受生物神经网络启发。

这类深度神经网络中的一种就是卷积网络,通常称为 CNN 或 ConvNet。它是一种深层、前馈的人工神经网络。请记住,前馈神经网络也称为多层感知机(MLP),是典型的深度学习模型。之所以称为 "前馈",是因为信息自左向右流经模型,没有将模型输出反馈回自身的反馈连接。

CNN 特别受生物视觉皮层启发。视觉皮层有对视觉场中特定区域敏感的小细胞群。这一思想在 Hubel 和 Wiesel 于 1962 年进行的一项引人入胜的实验中得到扩展(若想了解更多,请看这个视频)。在实验中,研究者发现大脑中的一些单个神经元只在出现某种取向的边缘(如垂直或水平边缘)时才会被激活。例如,有些神经元在呈现垂直边时会触发,有些在呈现水平边时会触发。Hubel 和 Wiesel 发现所有这些神经元按柱状方式有序排列,并能共同产生视觉感知。这个关于系统内部的专门化组件各司其职的理念也被机器所借鉴,并能在 CNN 中找到体现。

卷积神经网络是计算机视觉领域最具影响力的创新之一。它们的表现远超传统计算机视觉方法,并产出了最先进的结果。这些网络在许多真实案例与应用中证明了成功,例如:

  • 图像分类、目标检测、分割、人脸识别;
  • 使用基于 CNN 的视觉系统的自动驾驶汽车;
  • 利用卷积神经网络进行晶体结构分类;
  • 当然,还有更多!

要理解这种成功,您需要回到 2012 年,那一年 Alex Krizhevsky 使用卷积神经网络赢得了当年的 ImageNet 比赛,将分类错误率从 26% 降至 15%。

注意,ImageNet 大规模视觉识别挑战赛(ILSVRC)始于 2010 年,是一项年度竞赛,研究团队在给定数据集上评估其算法,并在多个视觉识别任务上竞争更高的准确率。

也是在那个时期,神经网络在一段时间后再次崛起。这常被称为“神经网络的第三波”。前两波分别发生在 20 世纪 40 至 60 年代以及 70 至 80 年代。

好的,您知道我们将使用受生物视觉皮层启发的前馈网络,但这究竟意味着什么?

看看下面的图片:

Convolutional neural network
图:来自 Wikimedia 的卷积神经网络

图中显示,您将图像作为输入喂入网络,图像经过多次卷积、下采样、全连接层,最后输出结果。

但这些概念都是什么?

  1. 卷积层计算与输入中的局部区域(感受野)相连接的神经元输出,每个输出是其权重与输入体中小感受野的点积。每次计算都会从输入图像中提取一个特征图。换言之,想象您有一幅用 5x5 数值矩阵表示的图像,取一个 3x3 的矩阵,将这个 3x3 的窗口(或称核)在图像上滑动。在该矩阵的每个位置,您将 3x3 窗口的数值与当前窗口覆盖的图像数值逐一相乘。结果得到一个数字,代表该图像窗口中的所有数值。您用这一层来做滤波:窗口在图像上移动时,您在该区域内检查某些模式。这得益于滤波器,它与卷积输出的数值相乘。
  2. 下采样的目标是通过降低维度来获得输入的表示,有助于减少过拟合。下采样的一种技术是最大池化。该技术会根据区域大小选择其中的最高像素值。换言之,最大池化从当前由核覆盖的图像窗口中取最大值。例如,大小为 2 x 2 的最大池化层会从 2 x 2 区域中选取像素强度的最大值。您没错,池化层的工作方式与卷积层很像!同样取一个核或窗口并在图像上移动;唯一的区别在于应用于核与图像窗口的函数不是线性的。

    Max pooling
    图:来自 维基百科 的最大池化
  3. 全连接层的目标是将卷积层学到的高级特征展平并加以组合。它将展平的输出传递到输出层,在那里您使用 softmax 分类器或 sigmoid 来预测输入的类别标签。

想了解更多信息,请前往这里。

Fashion-MNIST 数据集

在继续加载数据之前,先看看您将处理的究竟是什么!Fashion-MNIST 数据集由 Zalando 的商品图片组成,包含 10 个类别共 70,000 张 28x28 的灰度图像,每类 7,000 张。训练集包含 60,000 张,测试集包含 10,000 张。稍后加载数据后,您可以再次核对这些信息!;)

Fashion-MNIST 与您可能已熟悉的 MNIST 数据集相似,后者用于手写数字分类。这意味着图像尺寸以及训练/测试划分与 MNIST 类似。提示:如果您想学习如何用后者数据集实现用于分类任务的多层感知机(MLP),请查看这篇 Keras 深度学习教程。

您可以在这里找到 Fashion-MNIST 数据集,但也可以借助特定的 TensorFlow 和 Keras 模块直接加载。下一节将展示其工作方式!

加载数据

Keras 自带一个名为 datasets 的库,可开箱即用地加载数据集:您从服务器下载数据,从而加速流程,因为无需手动下载到本地。训练与测试图像及其标签分别加载并存储在 train_X、train_Y、test_X、test_Y 变量中。

from keras.datasets import fashion_mnist
(train_X,train_Y), (test_X,test_Y) = fashion_mnist.load_data()
Using TensorFlow backend.

太好了!是不是很简单?

到现在为止,您可能已经做过无数次了,但这始终是入手的关键一步。现在,您已经完全准备好开始分析、处理和建模数据!

分析数据

现在我们来看看数据集中图像的样子。尽管此时您已经知道图像的尺寸,但仍值得以编程方式分析它:您可能需要对像素进行缩放并调整图像尺寸。

import numpy as np
from keras.utils import to_categorical
import matplotlib.pyplot as plt
%matplotlib inline

print('Training data shape : ', train_X.shape, train_Y.shape)

print('Testing data shape : ', test_X.shape, test_Y.shape)
('Training data shape : ', (60000, 28, 28), (60000,))
('Testing data shape : ', (10000, 28, 28), (10000,))

从上述输出可见,训练数据形状为 60000 x 28 x 28,因为共有 60,000 个训练样本,每个样本尺寸为 28 x 28。类似地,测试数据形状为 10000 x 28 x 28,因为共有 10,000 个测试样本。

# Find the unique numbers from the train labels
classes = np.unique(train_Y)
nClasses = len(classes)
print('Total number of outputs : ', nClasses)
print('Output classes : ', classes)
('Total number of outputs : ', 10)
('Output classes : ', array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8))

总共有从 0 到 9 的十个输出类别。

另外,别忘了看看数据集中图像的样子:

plt.figure(figsize=[5,5])

# Display the first image in training data
plt.subplot(121)
plt.imshow(train_X[0,:,:], cmap='gray')
plt.title("Ground Truth : {}".format(train_Y[0]))

# Display the first image in testing data
plt.subplot(122)
plt.imshow(test_X[0,:,:], cmap='gray')
plt.title("Ground Truth : {}".format(test_Y[0]))
Text(0.5,1,u'Ground Truth : 9')

Blurry ankle boots

上面两幅图的输出看起来像是一双踝靴,其类别标签为 9。类似地,其他服饰会有不同标签,但相似的商品会有相同标签。这意味着所有 7,000 张踝靴图片的类别标签都为 9。

数据预处理

如上图所示,图像为灰度图,像素值范围为 0 到 255。此外,这些图像的尺寸为 28 x 28。因此,在将数据喂入模型前需要进行预处理。

  • 第一步,将训练集和测试集中的每张 28 x 28 图像转换为 28 x 28 x 1 的矩阵,以便输入网络。
train_X = train_X.reshape(-1, 28,28, 1)
test_X = test_X.reshape(-1, 28,28, 1)
train_X.shape, test_X.shape
((60000, 28, 28, 1), (10000, 28, 28, 1))
  • 当前数据类型为 int8,因此在输入网络前需要将其转换为 float32,并将像素值缩放到 0 - 1(含)范围。我们来处理一下!
train_X = train_X.astype('float32')
test_X = test_X.astype('float32')
train_X = train_X / 255.
test_X = test_X / 255.
  • 现在需要将类别标签转换为独热编码向量。

在独热编码中,您将分类数据转换为一个数值向量。之所以进行独热编码,是因为机器学习算法无法直接处理分类数据。您会为每个类别生成一个布尔列。对于每个样本,只有其中一列取值为 1。因此称为独热编码。

在您的问题中,独热编码为行向量,对每张图像,维度为 1 x 10。这里要注意的是,向量除代表其类别的位置为 1 外,其余均为 0。例如,上面绘制的踝靴图片标签为 9,因此所有踝靴图片的独热编码向量为 [0 0 0 0 0 0 0 0 1 0]。

我们将训练与测试标签转换为独热编码向量:

# Change the labels from categorical to one-hot encoding
train_Y_one_hot = to_categorical(train_Y)
test_Y_one_hot = to_categorical(test_Y)

# Display the change for category label using one-hot encoding
print('Original label:', train_Y[0])
print('After conversion to one-hot:', train_Y_one_hot[0])
('Original label:', 9)
('After conversion to one-hot:', array([ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.]))

很清楚,对吧?注意,您也可以打印 train_Y_one_hot,它将显示一个 60000 x 10 的矩阵,每行对应一张图像的独热编码。

  • 最后一步至关重要。在机器学习或任何与数据相关的任务中,您都应该正确划分数据。为使模型具有良好的泛化能力,您需要将训练数据分为两部分,一部分用于训练,另一部分用于验证。在本例中,您将在 80\% 的训练数据上训练模型,并在剩余 20\% 的训练数据上进行验证。这样也有助于减少过拟合,因为您将在训练阶段未见过的数据上验证模型,有利于提升测试性能。
from sklearn.model_selection import train_test_split
train_X,valid_X,train_label,valid_label = train_test_split(train_X, train_Y_one_hot, test_size=0.2, random_state=13)

最后再检查一下训练集和验证集的形状。

train_X.shape,valid_X.shape,train_label.shape,valid_label.shape
((48000, 28, 28, 1), (12000, 28, 28, 1), (48000, 10), (12000, 10))

网络结构

图像尺寸为 28 x 28。您将图像矩阵转换为数组,将其缩放到 0 到 1 之间,重塑为 28 x 28 x 1,并将其作为输入喂入网络。

您将使用三个卷积层:

  • 第一层含有 32 个 3 x 3 的滤波器,
  • 第二层含有 64 个 3 x 3 的滤波器,
  • 第三层含有 128 个 3 x 3 的滤波器。

此外,还有三个 2 x 2 的最大池化层。

Architecture of the model
图:模型架构

对数据建模

首先,导入训练模型所需的所有模块。

import keras
from keras.models import Sequential,Input,Model
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.layers.normalization import BatchNormalization
from keras.layers.advanced_activations import LeakyReLU

您将使用 64 的批大小。也可以选择更大的批大小如 128 或 256,具体取决于内存。这对学习参数的确定影响很大,并会影响预测精度。您将训练 20 个轮次(epochs)。

batch_size = 64
epochs = 20
num_classes = 10

神经网络架构

在 Keras 中,您可以通过逐个添加所需层来堆叠网络层。您将在这里这么做:首先用 Conv2D() 添加第一个卷积层。注意使用该函数是因为您在处理图像!接着添加 Leaky ReLU 激活函数,帮助网络学习非线性决策边界。由于您有十个不同的类别,需要一个能分隔这十个线性不可分类别的非线性决策边界。

更具体地说,您添加 Leaky ReLU 是为了解决“ReLU 失活”问题。ReLU 激活函数在神经网络架构中被广泛使用,尤其在卷积网络中,其效果优于曾经广泛使用的 logistic sigmoid 函数。截至 2017 年,它是深度神经网络中最流行的激活函数。ReLU 函数使激活在零处被阈值化。然而在训练过程中,ReLU 单元可能会“失活”。当一个较大的梯度通过 ReLU 神经元时,可能导致权重更新使得该神经元不再对任何数据点激活。一旦发生,从那以后流经该单元的梯度将永远为零。Leaky ReLU 试图解决这一点:函数不为零,而是具有一个很小的负斜率。

接下来,您将添加最大池化层 MaxPooling2D(),依此类推。最后一层是具有 10 个单元并带有 softmax 激活函数的 Dense 层,这是该多分类问题所需的。

fashion_model = Sequential()
fashion_model.add(Conv2D(32, kernel_size=(3, 3),activation='linear',input_shape=(28,28,1),padding='same'))
fashion_model.add(LeakyReLU(alpha=0.1))
fashion_model.add(MaxPooling2D((2, 2),padding='same'))
fashion_model.add(Conv2D(64, (3, 3), activation='linear',padding='same'))
fashion_model.add(LeakyReLU(alpha=0.1))
fashion_model.add(MaxPooling2D(pool_size=(2, 2),padding='same'))
fashion_model.add(Conv2D(128, (3, 3), activation='linear',padding='same'))
fashion_model.add(LeakyReLU(alpha=0.1))                  
fashion_model.add(MaxPooling2D(pool_size=(2, 2),padding='same'))
fashion_model.add(Flatten())
fashion_model.add(Dense(128, activation='linear'))
fashion_model.add(LeakyReLU(alpha=0.1))                  
fashion_model.add(Dense(num_classes, activation='softmax'))

编译模型

创建模型后,使用 Adam 优化器对其进行编译,这是最受欢迎的优化算法之一。您可以在这里了解更多。此外,您将损失类型指定为用于多分类的类别交叉熵(也可以使用二元交叉熵作为损失函数)。最后,指定需要在模型训练时分析的指标为准确率。

fashion_model.compile(loss=keras.losses.categorical_crossentropy, optimizer=keras.optimizers.Adam(),metrics=['accuracy'])

让我们用 summary 函数可视化您在上一步创建的各层。它会显示每一层中的一些参数(权重和偏置),以及模型的参数总数。

fashion_model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_51 (Conv2D)           (None, 28, 28, 32)        320       
_________________________________________________________________
leaky_re_lu_57 (LeakyReLU)   (None, 28, 28, 32)        0         
_________________________________________________________________
max_pooling2d_49 (MaxPooling (None, 14, 14, 32)        0         
_________________________________________________________________
conv2d_52 (Conv2D)           (None, 14, 14, 64)        18496     
_________________________________________________________________
leaky_re_lu_58 (LeakyReLU)   (None, 14, 14, 64)        0         
_________________________________________________________________
max_pooling2d_50 (MaxPooling (None, 7, 7, 64)          0         
_________________________________________________________________
conv2d_53 (Conv2D)           (None, 7, 7, 128)         73856     
_________________________________________________________________
leaky_re_lu_59 (LeakyReLU)   (None, 7, 7, 128)         0         
_________________________________________________________________
max_pooling2d_51 (MaxPooling (None, 4, 4, 128)         0         
_________________________________________________________________
flatten_17 (Flatten)         (None, 2048)              0         
_________________________________________________________________
dense_33 (Dense)             (None, 128)               262272    
_________________________________________________________________
leaky_re_lu_60 (LeakyReLU)   (None, 128)               0         
_________________________________________________________________
dense_34 (Dense)             (None, 10)                1290      
=================================================================
Total params: 356,234
Trainable params: 356,234
Non-trainable params: 0
_________________________________________________________________

训练模型

终于到了用 Keras 的 fit() 函数训练模型的时候!模型将训练 20 个 epoch。fit() 会返回一个 history 对象;将其结果存为 fashion_train,您随后可以用它来绘制训练与验证的准确率和损失曲线,从而直观分析模型表现。

fashion_train = fashion_model.fit(train_X, train_label, batch_size=batch_size,epochs=epochs,verbose=1,validation_data=(valid_X, valid_label))
Train on 48000 samples, validate on 12000 samples
Epoch 1/20
48000/48000 [==============================] - 60s 1ms/step - loss: 0.4661 - acc: 0.8311 - val_loss: 0.3320 - val_acc: 0.8809
Epoch 2/20
48000/48000 [==============================] - 60s 1ms/step - loss: 0.2874 - acc: 0.8951 - val_loss: 0.2781 - val_acc: 0.8963
Epoch 3/20
48000/48000 [==============================] - 60s 1ms/step - loss: 0.2420 - acc: 0.9111 - val_loss: 0.2501 - val_acc: 0.9077
Epoch 4/20
48000/48000 [==============================] - 59s 1ms/step - loss: 0.2088 - acc: 0.9226 - val_loss: 0.2369 - val_acc: 0.9147
Epoch 5/20
48000/48000 [==============================] - 59s 1ms/step - loss: 0.1838 - acc: 0.9324 - val_loss: 0.2602 - val_acc: 0.9070
Epoch 6/20
48000/48000 [==============================] - 59s 1ms/step - loss: 0.1605 - acc: 0.9396 - val_loss: 0.2264 - val_acc: 0.9193
Epoch 7/20
48000/48000 [==============================] - 59s 1ms/step - loss: 0.1356 - acc: 0.9488 - val_loss: 0.2566 - val_acc: 0.9180
Epoch 8/20
48000/48000 [==============================] - 59s 1ms/step - loss: 0.1186 - acc: 0.9553 - val_loss: 0.2556 - val_acc: 0.9149
Epoch 9/20
48000/48000 [==============================] - 59s 1ms/step - loss: 0.0985 - acc: 0.9634 - val_loss: 0.2681 - val_acc: 0.9204
Epoch 10/20
48000/48000 [==============================] - 59s 1ms/step - loss: 0.0873 - acc: 0.9670 - val_loss: 0.2712 - val_acc: 0.9221
Epoch 11/20
48000/48000 [==============================] - 59s 1ms/step - loss: 0.0739 - acc: 0.9721 - val_loss: 0.2757 - val_acc: 0.9202
Epoch 12/20
48000/48000 [==============================] - 60s 1ms/step - loss: 0.0628 - acc: 0.9767 - val_loss: 0.3126 - val_acc: 0.9132
Epoch 13/20
48000/48000 [==============================] - 61s 1ms/step - loss: 0.0569 - acc: 0.9789 - val_loss: 0.3556 - val_acc: 0.9081
Epoch 14/20
48000/48000 [==============================] - 60s 1ms/step - loss: 0.0452 - acc: 0.9833 - val_loss: 0.3441 - val_acc: 0.9189
Epoch 15/20
48000/48000 [==============================] - 60s 1ms/step - loss: 0.0421 - acc: 0.9847 - val_loss: 0.3400 - val_acc: 0.9165
Epoch 16/20
48000/48000 [==============================] - 60s 1ms/step - loss: 0.0379 - acc: 0.9861 - val_loss: 0.3876 - val_acc: 0.9195
Epoch 17/20
48000/48000 [==============================] - 60s 1ms/step - loss: 0.0405 - acc: 0.9855 - val_loss: 0.4112 - val_acc: 0.9164
Epoch 18/20
48000/48000 [==============================] - 60s 1ms/step - loss: 0.0285 - acc: 0.9897 - val_loss: 0.4150 - val_acc: 0.9181
Epoch 19/20
48000/48000 [==============================] - 61s 1ms/step - loss: 0.0322 - acc: 0.9877 - val_loss: 0.4584 - val_acc: 0.9196
Epoch 20/20
48000/48000 [==============================] - 61s 1ms/step - loss: 0.0262 - acc: 0.9906 - val_loss: 0.4396 - val_acc: 0.9205

终于!您在 Fashion-MNIST 上训练了 20 个 epoch。观察训练准确率与损失,可以说模型完成得不错:20 个 epoch 后训练准确率达 99%,训练损失也很低。

不过,模型似乎过拟合了,因为验证损失为 0.4396,验证准确率为 92%。过拟合表明网络很好地记住了训练数据,但并不能保证在未见过的数据上表现良好,这也是训练与验证准确率存在差异的原因。

您可能需要处理这个问题。接下来,您将学习如何在保持其他层不变的情况下,通过在网络中添加 Dropout 层让模型表现更好。

但在下结论之前,先在测试集上评估模型表现。

在测试集上评估模型

test_eval = fashion_model.evaluate(test_X, test_Y_one_hot, verbose=0)
print('Test loss:', test_eval[0])
print('Test accuracy:', test_eval[1])
('Test loss:', 0.46366268818555401)
('Test accuracy:', 0.91839999999999999)

测试准确率看起来相当不错。结果显示,您的分类器优于此处报告的基线(平均准确率 0.897 的 SVM 分类器)。同时,相比在 GitHub 上(Fashion-MNIST 数据集作者)提到的一些深度学习模型,您的模型表现也不差。

然而,您已经看到模型似乎过拟合了。这些结果真的就足够好吗?

让我们结合可视化来评估模型,绘制训练与验证数据之间的准确率和损失曲线:

accuracy = fashion_train.history['acc']
val_accuracy = fashion_train.history['val_acc']
loss = fashion_train.history['loss']
val_loss = fashion_train.history['val_loss']
epochs = range(len(accuracy))
plt.plot(epochs, accuracy, 'bo', label='Training accuracy')
plt.plot(epochs, val_accuracy, 'b', label='Validation accuracy')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()

Training and validation accuracy graph

Training and validation loss graph

从上述两幅图可见,验证准确率在第 4-5 个 epoch 后几乎停滞,只有在某些 epoch 稍有上升。起初验证准确率与损失线性上升,但随后增幅不大。

验证损失表明出现了过拟合的迹象,与验证准确率类似,它先线性下降,但在第 4-5 个 epoch 后开始上升。这意味着模型尝试记住数据并且成功了。

考虑到这一点,现在是时候在模型中引入一些 dropout,看看是否有助于减少过拟合。

在网络中加入 Dropout

您可以添加一个 dropout 层,在一定程度上克服过拟合问题。Dropout 会在训练过程中随机关闭一部分神经元,从而在一定程度上降低对训练集的依赖。关闭多少比例的神经元由一个可调的超参数决定。通过关闭部分神经元,网络将无法记住训练数据,因为并非所有神经元会同时处于激活状态,未激活的神经元也无法学习任何东西。

现在我们重新创建、编译并训练网络,这次加入 dropout。并以批大小 64 训练 20 个 epoch。

batch_size = 64
epochs = 20
num_classes = 10
fashion_model = Sequential()
fashion_model.add(Conv2D(32, kernel_size=(3, 3),activation='linear',padding='same',input_shape=(28,28,1)))
fashion_model.add(LeakyReLU(alpha=0.1))
fashion_model.add(MaxPooling2D((2, 2),padding='same'))
fashion_model.add(Dropout(0.25))
fashion_model.add(Conv2D(64, (3, 3), activation='linear',padding='same'))
fashion_model.add(LeakyReLU(alpha=0.1))
fashion_model.add(MaxPooling2D(pool_size=(2, 2),padding='same'))
fashion_model.add(Dropout(0.25))
fashion_model.add(Conv2D(128, (3, 3), activation='linear',padding='same'))
fashion_model.add(LeakyReLU(alpha=0.1))                  
fashion_model.add(MaxPooling2D(pool_size=(2, 2),padding='same'))
fashion_model.add(Dropout(0.4))
fashion_model.add(Flatten())
fashion_model.add(Dense(128, activation='linear'))
fashion_model.add(LeakyReLU(alpha=0.1))           
fashion_model.add(Dropout(0.3))
fashion_model.add(Dense(num_classes, activation='softmax'))
fashion_model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_54 (Conv2D)           (None, 28, 28, 32)        320       
_________________________________________________________________
leaky_re_lu_61 (LeakyReLU)   (None, 28, 28, 32)        0         
_________________________________________________________________
max_pooling2d_52 (MaxPooling (None, 14, 14, 32)        0         
_________________________________________________________________
dropout_29 (Dropout)         (None, 14, 14, 32)        0         
_________________________________________________________________
conv2d_55 (Conv2D)           (None, 14, 14, 64)        18496     
_________________________________________________________________
leaky_re_lu_62 (LeakyReLU)   (None, 14, 14, 64)        0         
_________________________________________________________________
max_pooling2d_53 (MaxPooling (None, 7, 7, 64)          0         
_________________________________________________________________
dropout_30 (Dropout)         (None, 7, 7, 64)          0         
_________________________________________________________________
conv2d_56 (Conv2D)           (None, 7, 7, 128)         73856     
_________________________________________________________________
leaky_re_lu_63 (LeakyReLU)   (None, 7, 7, 128)         0         
_________________________________________________________________
max_pooling2d_54 (MaxPooling (None, 4, 4, 128)         0         
_________________________________________________________________
dropout_31 (Dropout)         (None, 4, 4, 128)         0         
_________________________________________________________________
flatten_18 (Flatten)         (None, 2048)              0         
_________________________________________________________________
dense_35 (Dense)             (None, 128)               262272    
_________________________________________________________________
leaky_re_lu_64 (LeakyReLU)   (None, 128)               0         
_________________________________________________________________
dropout_32 (Dropout)         (None, 128)               0         
_________________________________________________________________
dense_36 (Dense)             (None, 10)                1290      
=================================================================
Total params: 356,234
Trainable params: 356,234
Non-trainable params: 0
_________________________________________________________________
fashion_model.compile(loss=keras.losses.categorical_crossentropy, optimizer=keras.optimizers.Adam(),metrics=['accuracy'])
fashion_train_dropout = fashion_model.fit(train_X, train_label, batch_size=batch_size,epochs=epochs,verbose=1,validation_data=(valid_X, valid_label))
Train on 48000 samples, validate on 12000 samples
Epoch 1/20
48000/48000 [==============================] - 66s 1ms/step - loss: 0.5954 - acc: 0.7789 - val_loss: 0.3788 - val_acc: 0.8586
Epoch 2/20
48000/48000 [==============================] - 64s 1ms/step - loss: 0.3797 - acc: 0.8591 - val_loss: 0.3150 - val_acc: 0.8832
Epoch 3/20
48000/48000 [==============================] - 64s 1ms/step - loss: 0.3302 - acc: 0.8787 - val_loss: 0.2836 - val_acc: 0.8961
Epoch 4/20
48000/48000 [==============================] - 64s 1ms/step - loss: 0.3034 - acc: 0.8868 - val_loss: 0.2663 - val_acc: 0.9002
Epoch 5/20
48000/48000 [==============================] - 64s 1ms/step - loss: 0.2843 - acc: 0.8936 - val_loss: 0.2481 - val_acc: 0.9083
Epoch 6/20
48000/48000 [==============================] - 64s 1ms/step - loss: 0.2699 - acc: 0.9002 - val_loss: 0.2469 - val_acc: 0.9032
Epoch 7/20
48000/48000 [==============================] - 65s 1ms/step - loss: 0.2561 - acc: 0.9049 - val_loss: 0.2422 - val_acc: 0.9095
Epoch 8/20
48000/48000 [==============================] - 65s 1ms/step - loss: 0.2503 - acc: 0.9068 - val_loss: 0.2429 - val_acc: 0.9098
Epoch 9/20
48000/48000 [==============================] - 65s 1ms/step - loss: 0.2437 - acc: 0.9096 - val_loss: 0.2230 - val_acc: 0.9173
Epoch 10/20
48000/48000 [==============================] - 65s 1ms/step - loss: 0.2307 - acc: 0.9126 - val_loss: 0.2170 - val_acc: 0.9187
Epoch 11/20
48000/48000 [==============================] - 65s 1ms/step - loss: 0.2307 - acc: 0.9135 - val_loss: 0.2265 - val_acc: 0.9193
Epoch 12/20
48000/48000 [==============================] - 65s 1ms/step - loss: 0.2229 - acc: 0.9160 - val_loss: 0.2136 - val_acc: 0.9229
Epoch 13/20
48000/48000 [==============================] - 65s 1ms/step - loss: 0.2202 - acc: 0.9162 - val_loss: 0.2173 - val_acc: 0.9187
Epoch 14/20
48000/48000 [==============================] - 64s 1ms/step - loss: 0.2161 - acc: 0.9188 - val_loss: 0.2142 - val_acc: 0.9211
Epoch 15/20
48000/48000 [==============================] - 65s 1ms/step - loss: 0.2119 - acc: 0.9196 - val_loss: 0.2133 - val_acc: 0.9233
Epoch 16/20
48000/48000 [==============================] - 65s 1ms/step - loss: 0.2073 - acc: 0.9222 - val_loss: 0.2159 - val_acc: 0.9213
Epoch 17/20
48000/48000 [==============================] - 65s 1ms/step - loss: 0.2050 - acc: 0.9231 - val_loss: 0.2123 - val_acc: 0.9233
Epoch 18/20
48000/48000 [==============================] - 64s 1ms/step - loss: 0.2016 - acc: 0.9238 - val_loss: 0.2191 - val_acc: 0.9235
Epoch 19/20
48000/48000 [==============================] - 65s 1ms/step - loss: 0.2001 - acc: 0.9244 - val_loss: 0.2110 - val_acc: 0.9258
Epoch 20/20
48000/48000 [==============================] - 64s 1ms/step - loss: 0.1972 - acc: 0.9255 - val_loss: 0.2092 - val_acc: 0.9269

我们将模型保存下来,以便您可以直接加载而不必再次训练 20 个 epoch。这样,若之后需要,您可以加载模型并修改架构;或者基于此已保存模型重新开始训练。保存模型(以及模型权重!)总是个好主意——它能节省时间。注意,您也可以在每个 epoch 之后保存模型,这样一旦某个 epoch 出现问题导致训练中断,您不必从头开始。

fashion_model.save("fashion_model_dropout.h5py")

在测试集上评估模型

最后,也来评估一下新模型的表现!

test_eval = fashion_model.evaluate(test_X, test_Y_one_hot, verbose=1)
10000/10000 [==============================] - 5s 461us/step
print('Test loss:', test_eval[0])
print('Test accuracy:', test_eval[1])
('Test loss:', 0.21460009642243386)
('Test accuracy:', 0.92300000000000004)

哇!看起来在模型中加入 Dropout 的确起作用了,尽管测试准确率提升不大,但与之前的结果相比测试损失降低了。

现在,最后一次绘制训练与验证数据之间的准确率和损失曲线。

accuracy = fashion_train_dropout.history['acc']
val_accuracy = fashion_train_dropout.history['val_acc']
loss = fashion_train_dropout.history['loss']
val_loss = fashion_train_dropout.history['val_loss']
epochs = range(len(accuracy))
plt.plot(epochs, accuracy, 'bo', label='Training accuracy')
plt.plot(epochs, val_accuracy, 'b', label='Validation accuracy')
plt.title('Training and validation accuracy')
plt.legend()
plt.figure()
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()

Training and validation accuracy graph

Training and validation loss graph

最终您可以看到,验证损失与验证准确率与训练损失和训练准确率基本同步。尽管验证损失与准确率的曲线并非线性,但它显示出模型没有过拟合:验证损失在下降而不是上升,且训练与验证准确率之间的差距不大。

因此可以说,模型的泛化能力显著提升,因为测试集与验证集上的损失仅略高于训练损失。

预测标签

predicted_classes = fashion_model.predict(test_X)

由于得到的预测是浮点数值,直接与真实测试标签比较并不可行。因此,您将对输出进行四舍五入,将浮点数转换为整数。进一步地,您将使用 np.argmax() 来选择每一行中取值更高的索引号。

例如,假设某个测试图像的预测为 0 1 0 0 0 0 0 0 0 0,则输出应为类别标签 1。

predicted_classes = np.argmax(np.round(predicted_classes),axis=1)
predicted_classes.shape, test_Y.shape
((10000,), (10000,))
correct = np.where(predicted_classes==test_Y)[0]
print "Found %d correct labels" % len(correct)
for i, correct in enumerate(correct[:9]):
    plt.subplot(3,3,i+1)
    plt.imshow(test_X[correct].reshape(28,28), cmap='gray', interpolation='none')
    plt.title("Predicted {}, Class {}".format(predicted_classes[correct], test_Y[correct]))
    plt.tight_layout()
Found 9188 correct labels

Correct garment labels

incorrect = np.where(predicted_classes!=test_Y)[0]
print "Found %d incorrect labels" % len(incorrect)
for i, incorrect in enumerate(incorrect[:9]):
    plt.subplot(3,3,i+1)
    plt.imshow(test_X[incorrect].reshape(28,28), cmap='gray', interpolation='none')
    plt.title("Predicted {}, Class {}".format(predicted_classes[incorrect], test_Y[incorrect]))
    plt.tight_layout()
Found 812 incorrect labels

仅凭观察几张图片,您很难确定模型为何无法正确分类上述图像,但似乎多个类别中存在的相似模式会影响分类器的表现,尽管 CNN 是一种健壮的架构。例如,图 5 与图 6 属于不同类别,但看起来有点相似,可能是夹克,也可能是长袖衬衫。

分类报告

分类报告将帮助我们更详细地识别被误分类的类别。您将能观察到在这十个给定类别中,模型在哪些类别上的表现较差。

from sklearn.metrics import classification_report
target_names = ["Class {}".format(i) for i in range(num_classes)]
print(classification_report(test_Y, predicted_classes, target_names=target_names))
             precision    recall  f1-score   support

    Class 0       0.77      0.90      0.83      1000
    Class 1       0.99      0.98      0.99      1000
    Class 2       0.88      0.88      0.88      1000
    Class 3       0.94      0.92      0.93      1000
    Class 4       0.88      0.87      0.88      1000
    Class 5       0.99      0.98      0.98      1000
    Class 6       0.82      0.72      0.77      1000
    Class 7       0.94      0.99      0.97      1000
    Class 8       0.99      0.98      0.99      1000
    Class 9       0.98      0.96      0.97      1000

avg / total       0.92      0.92      0.92     10000

您可以看到,分类器在类别 6 上的精确率与召回率均表现欠佳。在类别 0 与类别 2 上,分类器的精确率不足。此外,在类别 4 上,分类器的精确率与召回率都略显不足。

继续探索!

本教程是用 Keras 在 Python 中入门卷积神经网络的一个良好起点。如果您能轻松跟上,或稍加努力就能完成,干得漂亮!试着做些实验,比如用相同的模型架构,配合不同的公开数据集。

仍有很多内容值得学习,何不试试 DataCamp 的Python 深度学习入门课程?同时,务必查看 Keras 文档,如果您还没看过的话。您会在其中找到更多示例及所有函数、参数、更多层等信息。当您学习如何在 Python 中使用神经网络时,这无疑将是不可或缺的资源!

如果您更愿意读一本讲解深度学习(配合 Keras)基础并介绍其实践应用的书,那么一定要读一读 François Chollet 的Deep Learning in Python。

主题