跳至主要内容

使用 Fashion-MNIST 数据集的自编码器分类器教程

在本教程中,您将学习并理解如何在 Python 中结合 Keras 将自编码器用作分类器。示例使用 Fashion-MNIST 数据集。
· 15分钟 阅读

使用 AI 探索

注意:本教程主要讲解如何实际使用卷积神经网络与卷积自编码器进行分类。因此,如果您尚不了解卷积神经网络(CNN)和自编码器,建议先阅读 CNN 和 自编码器 教程。

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

  • 首先简要介绍 Fashion-MNIST 数据。您将使用 Python 的各类库来加载、探索并分析数据,
  • 随后预处理数据:学习如何调整尺寸、重缩放像素值、验证图像数据类型,并将数据划分为训练集与验证集。
  • 完成以上步骤后,您可以构建卷积自编码器模型:学习如何建模并搭建网络,接着编译并训练模型、可视化准确率与损失曲线,最后保存模型。
  • 接下来,您将对 fashion-mnist 数据进行分段:先将标签转换为独热(one-hot)向量,按照标签划分训练与验证图像。然后定义与自编码器结构中相同的编码器函数,并在其后连接全连接层。
  • 您将学习如何把已训练模型的权重加载到新模型的部分层中,验证新旧模型的权重矩阵,使新模型的若干层不可训练,然后编译新的分类模型、训练并保存其权重。
  • 您会将模型重新训练为所有层均可训练,评估模型、可视化准确率与损失曲线,在测试数据上进行预测、将概率转换为类别标签,并绘制部分被正确与错误分类的测试样本。
  • 最后,您将可视化分类报告,以更深入地了解模型对各类别的(不)正确分类情况。

Fashion-MNIST 数据集

在开始加载并处理数据集之前,最好先对数据集的类型、数据维度以及包含的类别数有一个大致了解。

Fashion-MNIST 数据集包含 10 个品类、共 70,000 张 28x28 的灰度图像,每个品类 7,000 张。训练集包含 60,000 张图像,测试集包含 10,000 张。为获得更好的效果,Fashion-MNIST 被设计为原始 MNIST 数据集的替代品;其图像尺寸、训练/测试划分与原始 MNIST 类似。该数据集可在此 URL 免费获取,并可通过 tensorflow 或 keras 直接加载,无需先下载到本地计算机。

与 MNIST 相似,Fashion-MNIST 也包含 10 个类别,但其对象不是手写数字,而是 10 种不同的时尚品类,如凉鞋、衬衫、长裤等。

当前任务是训练一个卷积自编码器,并使用自编码器的编码器部分,结合全连接层,来正确识别测试集中新的样本。

提示:如果您想学习如何使用 MNIST 数据集实现用于分类任务的多层感知机(MLP),请查看本教程。

在下面的代码中,您将通过 os.environ 在 notebook 中设置环境变量。最好在初始化 Keras 之前进行如下设置,以限制 Keras 的后端 TensorFlow 仅使用第一块 GPU。如果训练机器的可用 GPU 为 0,请确保使用 0 而不是 1。您可以在终端中运行一个简单命令来检查,例如 nvidia-smi

import os
os.environ["CUDA_DEVICE_ORDER"]="PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"]="0" #model will be trained on GPU 0

加载数据

接下来,导入所有所需模块,例如 numpy、matplotlib,以及最重要的 Keras,因为今天的教程将使用 Keras 作为框架!

import keras
from matplotlib import pyplot as plt
import numpy as np
import gzip
%matplotlib inline
from keras.models import Model
from keras.optimizers import RMSprop
from keras.layers import Input,Dense,Flatten,Dropout,merge,Reshape,Conv2D,MaxPooling2D,UpSampling2D,Conv2DTranspose
from keras.layers.normalization import BatchNormalization
from keras.models import Model,Sequential
from keras.callbacks import ModelCheckpoint
from keras.optimizers import Adadelta, RMSprop,SGD,Adam
from keras import regularizers
from keras import backend as K
from keras.utils import to_categorical
Using TensorFlow backend.
/usr/local/lib/python2.7/dist-packages/h5py/__init__.py:34: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.
  from ._conv import register_converters as _register_converters

这里定义了一个函数来打开 gzip 文件,并通过 bytestream.read() 读取数据。将图像尺寸与图像总数作为参数传入该函数。随后使用 np.frombuffer() 将变量 buf 中的字节串转换为 float32 类型的 NumPy 数组。

转换为 NumPy 数组后,将其重塑为三维数组或张量:第一维是图像数量,第二、三维是图像的高与宽。最后返回 NumPy 数组 data。

def extract_data(filename, num_images):
    with gzip.open(filename) as bytestream:
        bytestream.read(16)
        buf = bytestream.read(28 * 28 * num_images)
        data = np.frombuffer(buf, dtype=np.uint8).astype(np.float32)
        data = data.reshape(num_images, 28,28)
        return data

现在通过传入训练与测试文件及其各自的图像数量来调用 extract_data() 函数。

train_data = extract_data('train-images-idx3-ubyte.gz', 60000)
test_data = extract_data('t10k-images-idx3-ubyte.gz', 10000)

同样地,定义一个提取标签的函数,打开 gzip 文件,通过 bytestream.read() 读取数据,传入标签维度(1)与图像总数。随后使用 np.frombuffer() 将变量 buf 中的字节串转换为 int64 类型的 NumPy 数组。

这一次无需重塑数组,因为变量 labels 将返回一个 60,000 x 1 的列向量。最后返回 NumPy 数组 labels。

def extract_labels(filename, num_images):
    with gzip.open(filename) as bytestream:
        bytestream.read(8)
        buf = bytestream.read(1 * num_images)
        labels = np.frombuffer(buf, dtype=np.uint8).astype(np.int64)
        return labels

现在通过传入训练与测试的标签文件及其各自的图像数量来调用该函数。

train_labels = extract_labels('train-labels-idx1-ubyte.gz',60000)
test_labels = extract_labels('t10k-labels-idx1-ubyte.gz',10000)

加载完训练与测试数据后,就可以开始分析数据,从而对今天要处理的数据集建立一定的直觉认识!

数据探索

现在我们来看看数据集中图像的样子,并借助 NumPy 数组的 .shape 属性查看图像的维度:

# Shapes of training set
print("Training set (images) shape: {shape}".format(shape=train_data.shape))

# Shapes of test set
print("Test set (images) shape: {shape}".format(shape=test_data.shape))
Training set (images) shape: (60000, 28, 28)
Test set (images) shape: (10000, 28, 28)

从上述输出可见,训练数据的形状为 60000 x 28 x 28,因为共有 60,000 个训练样本,每个样本是 28 x 28 的矩阵。同理,测试数据形状为 10000 x 28 x 28,因为有 10,000 个测试样本。

注意:在使用卷积自编码器进行重构的任务中不需要训练与测试标签。训练图像既作为输入,也作为“真值”(类似于分类任务中的标签)。

但对于分类任务,您还需要标签(后续会用到)。尽管当前任务只涉及训练与测试图像,但为了更好地理解数据,在探索阶段我们会使用标签。

让我们创建一个字典,用类别名称与其对应的类别标签进行映射:

# Create dictionary of target classes
label_dict = {
 0: 'A',
 1: 'B',
 2: 'C',
 3: 'D',
 4: 'E',
 5: 'F',
 6: 'G',
 7: 'H',
 8: 'I',
 9: 'J',
}

现在我们来查看数据集中几张图像:

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

# Display the first image in training data
plt.subplot(121)
curr_img = np.reshape(train_data[10], (28,28))
curr_lbl = train_labels[10]
plt.imshow(curr_img, cmap='gray')
plt.title("(Label: " + str(label_dict[curr_lbl]) + ")")

# Display the first image in testing data
plt.subplot(122)
curr_img = np.reshape(test_data[10], (28,28))
curr_lbl = test_labels[10]
plt.imshow(curr_img, cmap='gray')
plt.title("(Label: " + str(label_dict[curr_lbl]) + ")")
Text(0.5,1,'(Label: E)')
示例图像

以上两个图的输出分别来自训练与测试数据中的样本图像,这些图像被分配了 0(或 A)与 4(或 E)的类别标签。类似地,不同字母对应不同标签,相同字母对应相同标签。这意味着所有 6,000 张类别为 E 的图像,其类别标签均为 4。

数据预处理

该数据集的图像是 28 x 28 的灰度图,像素值范围为 0 到 255,因此在将数据喂入模型之前进行预处理非常重要。首先将训练集与测试集中的每张 28 x 28 图像转换为 28 x 28 x 1 的矩阵,以便输入网络:

train_data = train_data.reshape(-1, 28,28, 1)
test_data = test_data.reshape(-1, 28,28, 1)
train_data.shape, test_data.shape
((60000, 28, 28, 1), (10000, 28, 28, 1))

接着,确保训练与测试的 NumPy 数组为 float32 类型;若不是,需要进行类型转换。不过由于读取数据时我们已做了转换,因此无需重复。还需要将像素值重缩放至 0–1(含)范围。动手试试吧!

别忘了验证训练与测试数据的类型:

train_data.dtype, test_data.dtype
(dtype('float32'), dtype('float32'))

接下来,以训练与测试数据的最大像素值为基准进行重缩放:

np.max(train_data), np.max(test_data)
(255.0, 255.0)
train_data = train_data / np.max(train_data)
test_data = test_data / np.max(test_data)

验证训练与测试数据的最大值是否在重缩放后为 1.0:

np.max(train_data), np.max(test_data)
(1.0, 1.0)

完成以上步骤后,需要对数据进行划分。为了使模型具备良好的泛化能力,将训练数据拆分为训练集与验证集:用 80% 的数据进行训练,用剩余 20% 的训练数据进行验证。

这也能帮助降低过拟合的可能性,因为您会在训练阶段未见过的数据上验证模型。

可以使用 scikit-learn 的 train_test_split 模块进行合理划分:

from sklearn.model_selection import train_test_split
train_X,valid_X,train_ground,valid_ground = train_test_split(train_data,
                                                             train_data,
                                                             test_size=0.2,
                                                             random_state=13)

注意:上述数据划分将使用两次:一次用于卷积自编码器的重构任务(不需要训练与测试标签,因此训练图像会被作为输入与真值各传入一次)。

而在分类任务中,您还需要连同图像一起传入标签,这将在后文进行。

现在可以定义网络并将数据送入网络了。事不宜迟,进入下一步!

卷积自编码器

图像尺寸为 28 x 28 x 1,或展平成 30976 维向量。我们将图像矩阵转换为数组、重缩放到 0 到 1、再重塑为 28 x 28 x 1 的形状,并作为输入馈入网络。

此外,批大小使用 128(更大的 256 或 512 也可以,取决于训练设备)。批大小对学习参数与预测精度影响很大。

batch_size = 64
epochs = 200
inChannel = 1
x, y = 28, 28
input_img = Input(shape = (x, y, inChannel))
num_classes = 10

如您所知,自编码器分为两部分:编码器与解码器。

编码器:包含 4 个卷积模块,每个模块由一个卷积层与一个批归一化层组成。第 1、2 个卷积模块后使用最大池化层。

  • 第一个卷积模块包含 32 个 3 x 3 卷积核,随后是下采样(最大池化)层,
  • 第二个模块包含 64 个 3 x 3 卷积核,随后是另一个下采样层,
  • 第三个编码器模块包含 128 个 3 x 3 卷积核,
  • 第四个编码器模块包含 256 个 3 x 3 卷积核。

解码器:包含 3 个卷积模块,每个模块由一个卷积层与一个批归一化层组成。第 2、3 个卷积模块后使用上采样层。

  • 第一个模块包含 128 个 3 x 3 卷积核,
  • 第二个模块包含 64 个 3 x 3 卷积核,并接上采样层,
  • 第三个模块包含 32 个 3 x 3 卷积核,并接上采样层,
  • 最终层包含 1 个 3 x 3 卷积核,将重构回单通道输入。

最大池化层每次使用会将输入下采样为原来的二分之一,而上采样层每次使用会将输入上采样为原来的两倍。

注意:卷积核数量、卷积核大小、层数、训练轮数等均为超参数,应根据您的直觉进行设置;您可自由尝试调整这些超参数并比较模型表现。这正是逐步掌握深度学习“技艺”的方式!

我们将编码器与解码器分别实现为函数,因为稍后会在分类任务中复用编码器的权重!

def encoder(input_img):
    #encoder
    #input = 28 x 28 x 1 (wide and thin)
    conv1 = Conv2D(32, (3, 3), activation='relu', padding='same')(input_img) #28 x 28 x 32
    conv1 = BatchNormalization()(conv1)
    conv1 = Conv2D(32, (3, 3), activation='relu', padding='same')(conv1)
    conv1 = BatchNormalization()(conv1)
    pool1 = MaxPooling2D(pool_size=(2, 2))(conv1) #14 x 14 x 32
    conv2 = Conv2D(64, (3, 3), activation='relu', padding='same')(pool1) #14 x 14 x 64
    conv2 = BatchNormalization()(conv2)
    conv2 = Conv2D(64, (3, 3), activation='relu', padding='same')(conv2)
    conv2 = BatchNormalization()(conv2)
    pool2 = MaxPooling2D(pool_size=(2, 2))(conv2) #7 x 7 x 64
    conv3 = Conv2D(128, (3, 3), activation='relu', padding='same')(pool2) #7 x 7 x 128 (small and thick)
    conv3 = BatchNormalization()(conv3)
    conv3 = Conv2D(128, (3, 3), activation='relu', padding='same')(conv3)
    conv3 = BatchNormalization()(conv3)
    conv4 = Conv2D(256, (3, 3), activation='relu', padding='same')(conv3) #7 x 7 x 256 (small and thick)
    conv4 = BatchNormalization()(conv4)
    conv4 = Conv2D(256, (3, 3), activation='relu', padding='same')(conv4)
    conv4 = BatchNormalization()(conv4)
    return conv4

def decoder(conv4):    
    #decoder
    conv5 = Conv2D(128, (3, 3), activation='relu', padding='same')(conv4) #7 x 7 x 128
    conv5 = BatchNormalization()(conv5)
    conv5 = Conv2D(128, (3, 3), activation='relu', padding='same')(conv5)
    conv5 = BatchNormalization()(conv5)
    conv6 = Conv2D(64, (3, 3), activation='relu', padding='same')(conv5) #7 x 7 x 64
    conv6 = BatchNormalization()(conv6)
    conv6 = Conv2D(64, (3, 3), activation='relu', padding='same')(conv6)
    conv6 = BatchNormalization()(conv6)
    up1 = UpSampling2D((2,2))(conv6) #14 x 14 x 64
    conv7 = Conv2D(32, (3, 3), activation='relu', padding='same')(up1) # 14 x 14 x 32
    conv7 = BatchNormalization()(conv7)
    conv7 = Conv2D(32, (3, 3), activation='relu', padding='same')(conv7)
    conv7 = BatchNormalization()(conv7)
    up2 = UpSampling2D((2,2))(conv7) # 28 x 28 x 32
    decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(up2) # 28 x 28 x 1
    return decoded

创建好模型后,需要使用 RMSProp 优化器进行编译。

同时需要通过 loss 参数指定损失类型。本例中使用均方误差;每个批次的损失将按像素逐个比较预测输出与真值的均方误差计算:

autoencoder = Model(input_img, decoder(encoder(input_img)))
autoencoder.compile(loss='mean_squared_error', optimizer = RMSprop())

使用 summary 函数可视化上面创建的各层。这会显示每层的参数数量(权重与偏置)以及模型的总参数量。

autoencoder.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_2 (InputLayer)         (None, 28, 28, 1)         0         
_________________________________________________________________
conv2d_16 (Conv2D)           (None, 28, 28, 32)        320       
_________________________________________________________________
batch_normalization_15 (Batc (None, 28, 28, 32)        128       
_________________________________________________________________
...

batch_normalization_28 (Batc (None, 14, 14, 32)        128       
_________________________________________________________________
up_sampling2d_4 (UpSampling2 (None, 28, 28, 32)        0         
_________________________________________________________________
conv2d_30 (Conv2D)           (None, 28, 28, 1)         289       
=================================================================
Total params: 1,758,657
Trainable params: 1,755,841
Non-trainable params: 2,816
_________________________________________________________________

终于可以使用 Keras 的 fit() 来训练模型了!模型训练 200 个 epoch。fit() 会返回一个历史对象;将结果保存到 autoencoder_train 后,您可以据此绘制训练与验证损失曲线,以便直观分析模型表现。

训练模型

autoencoder_train = autoencoder.fit(train_X, train_ground, batch_size=batch_size,epochs=epochs,verbose=1,validation_data=(valid_X, valid_ground))
Train on 48000 samples, validate on 12000 samples
Epoch 1/200
48000/48000 [==============================] - 19s - loss: 0.0202 - val_loss: 0.0114s: 0.020
Epoch 2/200
48000/48000 [==============================] - 17s - loss: 0.0087 - val_loss: 0.0071  
...  
Epoch 199/200
48000/48000 [==============================] - 18s - loss: 7.0886e-04 - val_loss: 8.9876e-04
Epoch 200/200
48000/48000 [==============================] - 18s - loss: 7.0929e-04 - val_loss: 0.0010

完成!您已在 fashion-mnist 数据集上训练了 100 个 epoch。现在让我们绘制训练与验证集之间的损失曲线,以可视化模型表现。

loss = autoencoder_train.history['loss']
val_loss = autoencoder_train.history['val_loss']
epochs = range(200)
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()
训练与验证损失曲线

可以看到,验证损失与训练损失走势一致,说明模型没有过拟合:验证损失在降低且未上升,训练与验证损失在整个训练阶段几乎没有明显间隙。

因此可以认为模型的泛化能力良好。

但请记住,我们的任务是使用上述已训练模型的编码器部分来对 fashion mnist 图像进行分类。让我们继续!

保存模型

由于分类任务需要用到编码器权重,先保存完整的自编码器权重。稍后您将学习如何提取编码器权重。

autoencoder.save_weights('autoencoder.h5')

分段 Fashion-MNIST 图像

现在您将使用已训练自编码器的“头部”,即编码器部分,并仅在模型的编码器部分加载刚刚训练好的自编码器权重。

随后在编码器后添加若干全连接层来对 fashion mnist 图像进行分类。

  • 首先将标签转换为独热(one-hot)向量。

    如您尚不了解独热编码:

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

在本问题中,独热编码为行向量,对于每张图像,其维度为 1 x 10。需要注意的是,该向量除所表示类别对应位置为 1 外,其余位置均为 0。

我们将标签转换为独热编码向量:

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

# Display the change for category label using one-hot encoding
print('Original label:', train_labels[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_X,valid_X,train_label,valid_label = train_test_split(train_data,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))

现在来定义分类模型。请记住,编码器部分应与自编码器结构中的编码器完全一致。

def encoder(input_img):
    #encoder
    #input = 28 x 28 x 1 (wide and thin)
    conv1 = Conv2D(32, (3, 3), activation='relu', padding='same')(input_img) #28 x 28 x 32
    conv1 = BatchNormalization()(conv1)
    conv1 = Conv2D(32, (3, 3), activation='relu', padding='same')(conv1)
    conv1 = BatchNormalization()(conv1)
    pool1 = MaxPooling2D(pool_size=(2, 2))(conv1) #14 x 14 x 32
    conv2 = Conv2D(64, (3, 3), activation='relu', padding='same')(pool1) #14 x 14 x 64
    conv2 = BatchNormalization()(conv2)
    conv2 = Conv2D(64, (3, 3), activation='relu', padding='same')(conv2)
    conv2 = BatchNormalization()(conv2)
    pool2 = MaxPooling2D(pool_size=(2, 2))(conv2) #7 x 7 x 64
    conv3 = Conv2D(128, (3, 3), activation='relu', padding='same')(pool2) #7 x 7 x 128 (small and thick)
    conv3 = BatchNormalization()(conv3)
    conv3 = Conv2D(128, (3, 3), activation='relu', padding='same')(conv3)
    conv3 = BatchNormalization()(conv3)
    conv4 = Conv2D(256, (3, 3), activation='relu', padding='same')(conv3) #7 x 7 x 256 (small and thick)
    conv4 = BatchNormalization()(conv4)
    conv4 = Conv2D(256, (3, 3), activation='relu', padding='same')(conv4)
    conv4 = BatchNormalization()(conv4)
    return conv4

定义将与编码器堆叠的全连接层。

def fc(enco):
    flat = Flatten()(enco)
    den = Dense(128, activation='relu')(flat)
    out = Dense(num_classes, activation='softmax')(den)
    return out
encode = encoder(input_img)
full_model = Model(input_img,fc(encode))
for l1,l2 in zip(full_model.layers[:19],autoencoder.layers[0:19]):
    l1.set_weights(l2.get_weights())

注意:下一步非常重要。为确保加载到分类模型编码器部分的权重与自编码器的编码器权重相同,最好打印两个模型中任一对应层的权重进行对比。如果不一致,那么采用自编码器分类策略就失去了意义。

我们来打印两个模型第一层的权重。

autoencoder.get_weights()[0][1]
array([[[ 0.22935028, -0.800786  ,  0.42421195, -0.6509941 ,
         -0.82958347, -0.44448015,  0.04182598, -0.05483926,
          0.44611776,  0.7123421 , -0.4499234 ,  0.16125064,
          0.1174996 ,  0.12156075,  0.8391102 , -0.44067   ,
          0.02915774, -0.7223025 ,  0.33398604, -0.69252896,
          0.04369332, -0.3793029 ,  0.37535954,  0.34269437,
          0.8863593 , -0.2114254 ,  0.21323568, -0.4076597 ,
          0.2965019 ,  0.11617199, -0.22282824, -0.9501956 ]],

       [[ 0.23096658,  0.3701021 ,  0.78717273, -0.5014979 ,
         -1.3326751 , -0.73818666,  2.6434395 , -0.7560537 ,
         -0.52561104, -0.67917436,  2.0205429 ,  0.14013338,
         -0.9140436 ,  0.169709  ,  0.09063474, -0.20975377,
         -0.11247484, -0.09702996,  0.17846109,  0.40699893,
         -0.5722246 , -1.0119121 ,  0.30877167,  0.6645408 ,
         -0.68007207, -0.57144946, -0.68339616,  0.45407826,
          1.0148963 ,  0.88867754, -0.57179326,  0.01268557]],

       [[ 0.23020297,  0.14018346, -0.37600747, -0.6213855 ,
         -0.4104492 , -0.2036299 ,  0.12469969,  0.08351921,
          0.20644444, -0.01170571, -0.07618313,  0.23164392,
         -0.38417578,  0.3481844 , -0.8055927 ,  0.76824665,
          0.06819476,  0.93830526,  0.31898668,  0.51119566,
          0.4445658 , -0.4568496 ,  0.1269397 , -0.34482956,
         -1.3285302 , -0.20479   , -0.17618039, -0.22546193,
         -0.35588196,  0.9971566 , -0.03546353, -0.7294457 ]]],
      dtype=float32)
full_model.get_weights()[0][1]
array([[[ 0.22935028, -0.800786  ,  0.42421195, -0.6509941 ,
         -0.82958347, -0.44448015,  0.04182598, -0.05483926,
          0.44611776,  0.7123421 , -0.4499234 ,  0.16125064,
          0.1174996 ,  0.12156075,  0.8391102 , -0.44067   ,
          0.02915774, -0.7223025 ,  0.33398604, -0.69252896,
          0.04369332, -0.3793029 ,  0.37535954,  0.34269437,
          0.8863593 , -0.2114254 ,  0.21323568, -0.4076597 ,
          0.2965019 ,  0.11617199, -0.22282824, -0.9501956 ]],

       [[ 0.23096658,  0.3701021 ,  0.78717273, -0.5014979 ,
         -1.3326751 , -0.73818666,  2.6434395 , -0.7560537 ,
         -0.52561104, -0.67917436,  2.0205429 ,  0.14013338,
         -0.9140436 ,  0.169709  ,  0.09063474, -0.20975377,
         -0.11247484, -0.09702996,  0.17846109,  0.40699893,
         -0.5722246 , -1.0119121 ,  0.30877167,  0.6645408 ,
         -0.68007207, -0.57144946, -0.68339616,  0.45407826,
          1.0148963 ,  0.88867754, -0.57179326,  0.01268557]],

       [[ 0.23020297,  0.14018346, -0.37600747, -0.6213855 ,
         -0.4104492 , -0.2036299 ,  0.12469969,  0.08351921,
          0.20644444, -0.01170571, -0.07618313,  0.23164392,
         -0.38417578,  0.3481844 , -0.8055927 ,  0.76824665,
          0.06819476,  0.93830526,  0.31898668,  0.51119566,
          0.4445658 , -0.4568496 ,  0.1269397 , -0.34482956,
         -1.3285302 , -0.20479   , -0.17618039, -0.22546193,
         -0.35588196,  0.9971566 , -0.03546353, -0.7294457 ]]],
      dtype=float32)

好极了!两组权重完全一致。马上编译模型并开始训练吧。

接下来将编码器部分(即模型的前 19 层)设置为不可训练。由于编码器已训练完成,无需再训练;仅训练全连接部分即可。

for layer in full_model.layers[0:19]:
    layer.trainable = False

编译模型!

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

也打印一下模型摘要。应当能看到不可训练参数,因为我们使前十五层不可训练。

full_model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_2 (InputLayer)         (None, 28, 28, 1)         0         
_________________________________________________________________
conv2d_55 (Conv2D)           (None, 28, 28, 32)        320       
_________________________________________________________________
batch_normalization_53 (Batc (None, 28, 28, 32)        128       
_________________________________________________________________
conv2d_56 (Conv2D)           (None, 28, 28, 32)        9248      
_________________________________________________________________
batch_normalization_54 (Batc (None, 28, 28, 32)        128       
_________________________________________________________________
max_pooling2d_11 (MaxPooling (None, 14, 14, 32)        0         
_________________________________________________________________
conv2d_57 (Conv2D)           (None, 14, 14, 64)        18496     
_________________________________________________________________
batch_normalization_55 (Batc (None, 14, 14, 64)        256       
_________________________________________________________________
conv2d_58 (Conv2D)           (None, 14, 14, 64)        36928     
_________________________________________________________________
batch_normalization_56 (Batc (None, 14, 14, 64)        256       
_________________________________________________________________
max_pooling2d_12 (MaxPooling (None, 7, 7, 64)          0         
_________________________________________________________________
conv2d_59 (Conv2D)           (None, 7, 7, 128)         73856     
_________________________________________________________________
batch_normalization_57 (Batc (None, 7, 7, 128)         512       
_________________________________________________________________
conv2d_60 (Conv2D)           (None, 7, 7, 128)         147584    
_________________________________________________________________
batch_normalization_58 (Batc (None, 7, 7, 128)         512       
_________________________________________________________________
conv2d_61 (Conv2D)           (None, 7, 7, 256)         295168    
_________________________________________________________________
batch_normalization_59 (Batc (None, 7, 7, 256)         1024      
_________________________________________________________________
conv2d_62 (Conv2D)           (None, 7, 7, 256)         590080    
_________________________________________________________________
batch_normalization_60 (Batc (None, 7, 7, 256)         1024      
_________________________________________________________________
flatten_4 (Flatten)          (None, 12544)             0         
_________________________________________________________________
dense_7 (Dense)              (None, 128)               1605760   
_________________________________________________________________
dense_8 (Dense)              (None, 10)                1290      
=================================================================
Total params: 2,782,570
Trainable params: 1,607,050
Non-trainable params: 1,175,520
_________________________________________________________________

训练模型

现在用 Keras 的 fit() 开始训练吧!模型训练 10 个 epoch。fit() 会返回一个历史对象;将其结果保存到 fashion_train 后,您可以据此绘制训练与验证的准确率与损失曲线,以便直观分析模型表现。

classify_train = full_model.fit(train_X, train_label, batch_size=64,epochs=100,verbose=1,validation_data=(valid_X, valid_label))
Train on 48000 samples, validate on 12000 samples
Epoch 1/100
48000/48000 [==============================] - 6s - loss: 0.3747 - acc: 0.8732 - val_loss: 0.2888 - val_acc: 0.8935
Epoch 2/100
48000/48000 [==============================] - 6s - loss: 0.2216 - acc: 0.9178 - val_loss: 0.2942 - val_acc: 0.9010
Epoch 3/100
48000/48000 [==============================] - 5s - loss: 0.1762 - acc: 0.9340 - val_loss: 0.2868 - val_acc: 0.9078
...
Epoch 74/100
48000/48000 [==============================] - 6s - loss: 0.0182 - acc: 0.9953 - val_loss: 0.8069 - val_acc: 0.9109
Epoch 75/100
48000/48000 [==============================] - 6s - loss: 0.0102 - acc: 0.9971 - val_loss: 0.7872 - val_acc: 0.9125
Epoch 76/100
11776/48000 [======>.......................] - ETA: 3s - loss: 0.0084 - acc: 0.9977

完成!您在 fashion-MNIST 上仅训练了 10 个 epoch,从训练准确率与损失来看,模型表现相当不错:10 个 epoch 后训练准确率达 99%,验证准确率为 98%。

保存分类模型!

full_model.save_weights('autoencoder_classification.h5')

接下来,将前 19 层的 trainable 从 False 改为 True,重新训练模型!马上动手。

for layer in full_model.layers[0:19]:
    layer.trainable = True
full_model.compile(loss=keras.losses.categorical_crossentropy, optimizer=keras.optimizers.Adam(),metrics=['accuracy'])

现在对整个模型进行最后一轮训练!

classify_train = full_model.fit(train_X, train_label, batch_size=64,epochs=100,verbose=1,validation_data=(valid_X, valid_label))
Train on 48000 samples, validate on 12000 samples
Epoch 1/100
48000/48000 [==============================] - 13s - loss: 0.1584 - acc: 0.9718 - val_loss: 0.7902 - val_acc: 0.8960
Epoch 2/100
48000/48000 [==============================] - 12s - loss: 0.1049 - acc: 0.9759 - val_loss: 0.8327 - val_acc: 0.8893
Epoch 3/100
48000/48000 [==============================] - 12s - loss: 0.0792 - acc: 0.9804 - val_loss: 0.6947 - val_acc: 0.9099
...
loss: 0.0123 - acc: 0.9971 - val_loss: 0.6827 - val_acc: 0.9217
Epoch 98/100
48000/48000 [==============================] - 13s - loss: 0.0097 - acc: 0.9975 - val_loss: 0.7074 - val_acc: 0.9211
Epoch 99/100
48000/48000 [==============================] - 13s - loss: 0.0081 - acc: 0.9984 - val_loss: 0.6846 - val_acc: 0.9205
Epoch 100/100
48000/48000 [==============================] - 13s - loss: 0.0090 - acc: 0.9977 - val_loss: 0.6739 - val_acc: 0.9226

最后再保存一次模型。

full_model.save_weights('classification_complete.h5')

将模型评估结果可视化,绘制训练与验证的准确率和损失曲线:

accuracy = classify_train.history['acc']
val_accuracy = classify_train.history['val_acc']
loss = classify_train.history['loss']
val_loss = classify_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()
图表
图表

从上面两张图可见,模型存在过拟合,因为训练与验证损失之间存在较大差距。为缓解过拟合,您可能需要使用如 Dropout 等正则化技术。您可以参考这篇关于使用 Keras 的 Python CNN 教程。

在测试集上的模型评估

最后,也来评估一下模型在测试数据上的表现!

test_eval = full_model.evaluate(test_data, test_Y_one_hot, verbose=0)
print('Test loss:', test_eval[0])
print('Test accuracy:', test_eval[1])
('Test loss:', 0.7068972043234281)
('Test accuracy:', 0.9205)

预测标签

predicted_classes = full_model.predict(test_data)

由于预测结果为浮点数,直接与真实测试标签比较不太方便,因此需要对输出进行四舍五入,将浮点值转换为整数。接着使用 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_labels.shape
((10000,), (10000,))
correct = np.where(predicted_classes==test_labels)[0]
print "Found %d correct labels" % len(correct)
for i, correct in enumerate(correct[:9]):
    plt.subplot(3,3,i+1)
    plt.imshow(test_data[correct].reshape(28,28), cmap='gray', interpolation='none')
    plt.title("Predicted {}, Class {}".format(predicted_classes[correct], test_labels[correct]))
    plt.tight_layout()
Found 9204 correct labels
图像
incorrect = np.where(predicted_classes!=test_labels)[0]
print "Found %d incorrect labels" % len(incorrect)
for i, incorrect in enumerate(incorrect[:9]):
    plt.subplot(3,3,i+1)
    plt.imshow(test_data[incorrect].reshape(28,28), cmap='gray', interpolation='none')
    plt.title("Predicted {}, Class {}".format(predicted_classes[incorrect], test_labels[incorrect]))
    plt.tight_layout()
Found 796 incorrect labels
图像

分类报告

分类报告有助于更详细地识别被错误分类的类别。您将看到在十个类别中,模型在哪些类别上的表现较差。

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

    Class 0       0.83      0.89      0.86      1000
    Class 1       0.99      0.99      0.99      1000
    Class 2       0.89      0.87      0.88      1000
    Class 3       0.92      0.93      0.92      1000
    Class 4       0.85      0.90      0.88      1000
    Class 5       0.99      0.99      0.99      1000
    Class 6       0.81      0.72      0.76      1000
    Class 7       0.96      0.98      0.97      1000
    Class 8       0.98      0.98      0.98      1000
    Class 9       0.98      0.96      0.97      1000

avg / total       0.92      0.92      0.92     10000

想更深入学习吗?

本教程是使用自编码器与全连接卷积神经网络(结合 Python 与 Keras)的良好起点。如果您能顺利跟进,或稍加努力便能完成,做得很好!尝试用相同的模型架构在更多公开数据集上进行实验吧。

仍有大量内容值得学习,不妨试试 DataCamp 的 Deep Learning in Python 课程?同时,务必查看 Keras 文档(如果您还没看过)。其中包含更多示例、函数与参数说明、更多层类型等。在学习如何在 Python 中使用神经网络的过程中,它无疑会成为不可或缺的参考资料!

如果您更想通过一本系统讲解深度学习(基于 Keras)基础并结合实践应用的书籍来学习,那么强烈推荐阅读 François Chollet 的 Deep Learning in Python 一书。

主题