卷积神经网络(LeNet)

“多层感知机的从零开始实现”一节里我们构造了一个含单隐藏层的多层感知机模型来对 Fashion-MNIST 数据集中的图像进行分类。每张图像高和宽均是 28 像素。我们将图像中的像素逐行展开,得到长度为 784 的向量,并输入进全连接层中。然而,这种分类方法有一定的局限性:

  1. 图像在同一列邻近的像素在这个向量中可能相距较远。它们构成的模式可能难以被模型识别。
  2. 对于大尺寸的输入图像,使用全连接层容易造成模型过大。假设输入是高和宽均为 1000 像素的彩色照片(含三个通道)。即使全连接层输出个数仍是 256,该层权重参数的形状是 \(3,000,000\times 256\):它占用了大约 3GB 的内存。这带来过复杂的模型和过高的存储开销。

卷积层尝试解决这两个问题。一方面,卷积层保留输入形状,使得图像的像素在高和宽两个方向上的相关性均可能被有效识别。另一方面,卷积层通过滑动窗口将同一卷积核与不同位置的输入重复计算,从而避免参数尺寸过大。

卷积神经网络就是含卷积层的网络。本节里我们将介绍一个早期用来识别手写数字图像的卷积神经网络:LeNet [1]。这个名字来源于 LeNet 论文的第一作者 Yann LeCun。LeNet 展示了通过梯度下降训练卷积神经网络可以达到手写数字识别在当时最先进的结果。这个奠基性的工作第一次将卷积神经网络推上舞台,为世人所知。

LeNet 模型

LeNet 分为卷积层块和全连接层块两个部分。下面我们分别介绍这两个模块。

卷积层块里的基本单位是卷积层后接最大池化层:卷积层用来识别图像里的空间模式,例如线条和物体局部,之后的最大池化层则用来降低卷积层对位置的敏感性。卷积层块由两个这样的基本单位重复堆叠构成。在卷积层块中,每个卷积层都使用 \(5\times 5\) 的窗口,并在输出上使用 sigmoid 激活函数。第一个卷积层输出通道数为 6,第二个卷积层输出通道数则增加到 16。这是因为第二个卷积层比第一个卷积层的输入的高和宽要小,所以增加输出通道使两个卷积层的参数尺寸类似。卷积层块的两个最大池化层的窗口形状均为 \(2\times 2\),且步幅为 2。由于池化窗口与步幅形状相同,池化窗口在输入上每次滑动所覆盖的区域互不重叠。

卷积层块的输出形状为(批量大小,通道,高,宽)。当卷积层块的输出传入全连接层块时,全连接层块会将小批量中每个样本变平(flatten)。也就是说,全连接层的输入形状将变成二维,其中第一维为小批量中的样本,第二维为每个样本变平后的向量表示,且向量长度为通道、高和宽的乘积。全连接层块含三个全连接层。它们的输出个数分别是 120、84 和 10。其中 10 为输出的类别个数。

下面我们通过 Sequential 类来实现 LeNet 模型。

In [1]:
import sys
sys.path.insert(0, '..')

import gluonbook as gb
import mxnet as mx
from mxnet import autograd, gluon, init, nd
from mxnet.gluon import loss as gloss, nn
import time

net = nn.Sequential()
net.add(nn.Conv2D(channels=6, kernel_size=5, activation='sigmoid'),
        nn.MaxPool2D(pool_size=2, strides=2),
        nn.Conv2D(channels=16, kernel_size=5, activation='sigmoid'),
        nn.MaxPool2D(pool_size=2, strides=2),
        # Dense 会默认将(批量大小,通道,高,宽)形状的输入转换成
        # (批量大小,通道 * 高 * 宽)形状的输入。
        nn.Dense(120, activation='sigmoid'),
        nn.Dense(84, activation='sigmoid'),
        nn.Dense(10))

接下来我们构造一个高和宽均为 28 的单通道数据样本,并逐层进行前向计算来查看每个层的输出形状。

In [2]:
X = nd.random.uniform(shape=(1, 1, 28, 28))
net.initialize()
for layer in net:
    X = layer(X)
    print(layer.name, 'output shape:\t', X.shape)
conv0 output shape:      (1, 6, 24, 24)
pool0 output shape:      (1, 6, 12, 12)
conv1 output shape:      (1, 16, 8, 8)
pool1 output shape:      (1, 16, 4, 4)
dense0 output shape:     (1, 120)
dense1 output shape:     (1, 84)
dense2 output shape:     (1, 10)

可以看到在卷积层块中输入的高和宽在逐层减小。卷积层由于使用高和宽均为 5 的卷积核,从而将高和宽分别减小 4,而池化层则将高和宽减半,但通道数则从 1 增加到 16。全连接层则逐层减少输出个数,直到变成图像的类别数 10。

获取数据和训练

下面我们来实验 LeNet 模型。实验中,我们仍然使用 Fashion-MNIST 作为训练数据集。

In [3]:
batch_size = 256
train_iter, test_iter = gb.load_data_fashion_mnist(batch_size=batch_size)

因为卷积神经网络计算比多层感知机要复杂,建议使用 GPU 来加速计算。我们尝试在gpu(0)上创建 NDArray,如果成功则使用gpu(0),否则仍然使用 CPU。

In [4]:
def try_gpu():  # 本函数已保存在 gluonbook 包中方便以后使用。
    try:
        ctx = mx.gpu()
        _ = nd.zeros((1,), ctx=ctx)
    except:
        ctx = mx.cpu()
    return ctx

ctx = try_gpu()
ctx
Out[4]:
gpu(0)

相应地,我们对“Softmax 回归的从零开始实现”一节中描述的evaluate_accuracy函数略作修改。由于数据刚开始存在 CPU 的内存上,当ctx变量为 GPU 时,我们通过“GPU 计算”一节中介绍的as_in_context函数将数据复制到 GPU 上,例如gpu(0)

In [5]:
# 本函数已保存在 gluonbook 包中方便以后使用。该函数将被逐步改进:它的完整实现将在“图像增
# 广”一节中描述。
def evaluate_accuracy(data_iter, net, ctx):
    acc = nd.array([0], ctx=ctx)
    for X, y in data_iter:
        # 如果 ctx 是 GPU,将数据复制到 GPU 上。
        X, y = X.as_in_context(ctx), y.as_in_context(ctx)
        acc += gb.accuracy(net(X), y)
    return acc.asscalar() / len(data_iter)

我们同样对“Softmax 回归的从零开始实现”一节中定义的train_ch3函数略作修改,确保计算使用的数据和模型同在 CPU 或 GPU 的内存上。

In [6]:
# 本函数已保存在 gluonbook 包中方便以后使用。
def train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx,
              num_epochs):
    print('training on', ctx)
    loss = gloss.SoftmaxCrossEntropyLoss()
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, start = 0, 0, time.time()
        for X, y in train_iter:
            X, y = X.as_in_context(ctx), y.as_in_context(ctx)
            with autograd.record():
                y_hat = net(X)
                l = loss(y_hat, y)
            l.backward()
            trainer.step(batch_size)
            train_l_sum += l.mean().asscalar()
            train_acc_sum += gb.accuracy(y_hat, y)
        test_acc = evaluate_accuracy(test_iter, net, ctx)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f, '
              'time %.1f sec' % (epoch + 1, train_l_sum / len(train_iter),
                                 train_acc_sum / len(train_iter),
                                 test_acc, time.time() - start))

我们重新将模型参数初始化到设备变量ctx之上,并使用 Xavier 随机初始化。损失函数和训练算法则依然使用交叉熵损失函数和小批量随机梯度下降。

In [7]:
lr, num_epochs = 0.8, 5
net.initialize(force_reinit=True, ctx=ctx, init=init.Xavier())
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr})
train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx, num_epochs)
training on gpu(0)
epoch 1, loss 2.3196, train acc 0.101, test acc 0.101, time 2.0 sec
epoch 2, loss 1.9714, train acc 0.242, test acc 0.543, time 1.7 sec
epoch 3, loss 0.9819, train acc 0.610, test acc 0.686, time 1.7 sec
epoch 4, loss 0.7753, train acc 0.698, test acc 0.714, time 1.7 sec
epoch 5, loss 0.6855, train acc 0.730, test acc 0.746, time 1.7 sec

小结

  • 卷积神经网络就是含卷积层的网络。
  • LeNet 交替使用卷积层和最大池化层后接全连接层来进行图像分类。

练习

  • 尝试基于 LeNet 构造更复杂的网络来改善精度。例如,调整卷积窗口大小、输出通道数、激活函数和全连接层输出个数。在优化方面,可以尝试使用不同的学习率、初始化方法以及增加迭代周期。

扫码直达讨论区

参考文献

[1] LeCun, Y., Bottou, L., Bengio, Y., & Haffner, P. (1998). Gradient-based learning applied to document recognition. Proceedings of the IEEE, 86(11), 2278-2324.