《动手学深度学习》
Table Of Contents
《动手学深度学习》
Table Of Contents

Softmax 回归的从零开始实现

这一节我们来动手实现 Softmax 回归。首先导入本节实现所需的包或模块。

In [1]:
%matplotlib inline
import gluonbook as gb
from mxnet import autograd, nd

获取和读取数据

我们将使用 Fashion-MNIST 数据集,并设置批量大小为 256。

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

初始化模型参数

跟线性回归中的例子一样,我们将使用向量表示每个样本。已知每个样本输入是高和宽均为 28 像素的图像。模型的输入向量的长度是 \(28 \times 28 = 784\):该向量的每个元素对应图像中每个像素。由于图像有 10 个类别,单层神经网络输出层的输出个数为 10。所以 Softmax 回归的权重和偏差参数分别为 \(784 \times 10\)\(1 \times 10\) 的矩阵。

In [3]:
num_inputs = 784
num_outputs = 10

W = nd.random.normal(scale=0.01, shape=(num_inputs, num_outputs))
b = nd.zeros(num_outputs)

同之前一样,我们要对模型参数附上梯度。

In [4]:
W.attach_grad()
b.attach_grad()

实现 Softmax 运算

在介绍如何定义 Softmax 回归之前,我们先描述一下对如何对多维 NDArray 按维度操作。在下面例子中,给定一个 NDArray 矩阵X。我们可以只对其中同一列(axis=0)或同一行(axis=1)的元素求和,并在结果中保留行和列这两个维度(keepdims=True)。

In [5]:
X = nd.array([[1, 2, 3], [4, 5, 6]])
X.sum(axis=0, keepdims=True), X.sum(axis=1, keepdims=True)
Out[5]:
(
 [[5. 7. 9.]]
 <NDArray 1x3 @cpu(0)>,
 [[ 6.]
  [15.]]
 <NDArray 2x1 @cpu(0)>)

下面我们就可以定义前面小节里介绍的 softmax 运算了。在下面的函数中,矩阵X的行数是样本数,列数是输出个数。为了表达样本预测各个输出的概率,softmax 运算会先通过exp函数对每个元素做指数运算,再对exp矩阵同行元素求和,最后令矩阵每行各元素与该行元素之和相除。这样一来,最终得到的矩阵每行元素和为 1 且非负。因此,该矩阵每行都是合法的概率分布。Softmax 运算的输出矩阵中的任意一行元素代表了一个样本在各个输出类别上的预测概率。

In [6]:
def softmax(X):
    X_exp = X.exp()
    partition = X_exp.sum(axis=1, keepdims=True)
    return X_exp / partition  # 这里应用了广播机制。

可以看到,对于随机输入,我们将每个元素变成了非负数,且每一行和为 1。

In [7]:
X = nd.random.normal(shape=(2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(axis=1)
Out[7]:
(
 [[0.21324193 0.33961776 0.1239742  0.27106097 0.05210521]
  [0.11462264 0.3461234  0.19401033 0.29583326 0.04941036]]
 <NDArray 2x5 @cpu(0)>,
 [1.0000001 1.       ]
 <NDArray 2 @cpu(0)>)

定义模型

有了 softmax 运算,我们可以定义上节描述的 softmax 回归模型了。这里通过reshape函数将每张原始图像改成长度为num_inputs的向量。

In [8]:
def net(X):
    return softmax(nd.dot(X.reshape((-1, num_inputs)), W) + b)

定义损失函数

上一节中,我们介绍了 softmax 回归使用的交叉熵损失函数。为了得到标签的预测概率,我们可以使用pick函数。在下面例子中,变量y_hat是 2 个样本在 3 个类别的预测概率,变量y是这 2 个样本的标签类别。通过使用pick函数,我们得到了 2 个样本的标签的预测概率。与“Softmax 回归”一节数学表述中标签类别离散值从 1 开始逐一递增不同,在代码中,标签类别的离散值是从 0 开始逐一递增的。

In [9]:
y_hat = nd.array([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = nd.array([0, 2])
nd.pick(y_hat, y)
Out[9]:

[0.1 0.5]
<NDArray 2 @cpu(0)>

以下实现了“Softmax 回归”一节中介绍的交叉熵损失函数。

In [10]:
def cross_entropy(y_hat, y):
    return - nd.pick(y_hat, y).log()

计算分类准确率

给定一个类别的预测概率分布y_hat,我们把预测概率最大的类别作为输出类别。如果它与真实类别y一致,说明这次预测是正确的。分类准确率即正确预测数量与总预测数量之比。

下面定义准确率accuracy函数。其中y_hat.argmax(axis=1)返回矩阵y_hat每行中最大元素的索引,且返回结果与变量y形状相同。我们在“数据操作”一节介绍过,相等条件判断式(y_hat.argmax(axis=1) == y)是一个值为 0(相等为假)或 1(相等为真)的 NDArray。由于标签类型为整数,我们先将变量y变换为浮点数再进行相等条件判断。

In [11]:
# 本函数已保存在 gluonbook 包中方便以后使用。
def accuracy(y_hat, y):
    return (y_hat.argmax(axis=1) == y.astype('float32')).mean().asscalar()

让我们继续使用在演示pick函数时定义的变量y_haty,并将它们分别作为预测概率分布和标签。可以看到,第一个样本预测类别为 2(该行最大元素 0.6 在本行的索引为 2),与真实标签 0 不一致;第二个样本预测类别为 2(该行最大元素 0.5 在本行的索引为 2),与真实标签 2 一致。因此,这两个样本上的分类准确率为 0.5。

In [12]:
accuracy(y_hat, y)
Out[12]:
0.5

类似地,我们可以评价模型net在数据集data_iter上的准确率。

In [13]:
# 本函数已保存在 gluonbook 包中方便以后使用。该函数将被逐步改进:它的完整实现将在“图像增
# 广”一节中描述。
def evaluate_accuracy(data_iter, net):
    acc = 0
    for X, y in data_iter:
        acc += accuracy(net(X), y)
    return acc / len(data_iter)

因为我们随机初始化了模型net,所以这个随机模型的准确率应该接近于类别个数 10 的倒数 0.1。

In [14]:
evaluate_accuracy(test_iter, net)
Out[14]:
0.0947265625

训练模型

训练 softmax 回归的实现跟前面介绍的线性回归中的实现非常相似。我们同样使用小批量随机梯度下降来优化模型的损失函数。在训练模型时,迭代周期数num_epochs和学习率lr都是可以调的超参数。改变它们的值可能会得到分类更准确的模型。

In [15]:
num_epochs, lr = 5, 0.1

# 本函数已保存在 gluonbook 包中方便以后使用。
def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
              params=None, lr=None, trainer=None):
    for epoch in range(num_epochs):
        train_l_sum = 0
        train_acc_sum = 0
        for X, y in train_iter:
            with autograd.record():
                y_hat = net(X)
                l = loss(y_hat, y)
            l.backward()
            if trainer is None:
                gb.sgd(params, lr, batch_size)
            else:
                trainer.step(batch_size)  # 下一节将用到。
            train_l_sum += l.mean().asscalar()
            train_acc_sum += accuracy(y_hat, y)
        test_acc = evaluate_accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
              % (epoch + 1, train_l_sum / len(train_iter),
                 train_acc_sum / len(train_iter), test_acc))

train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs,
          batch_size, [W, b], lr)
epoch 1, loss 0.7895, train acc 0.746, test acc 0.802
epoch 2, loss 0.5734, train acc 0.812, test acc 0.819
epoch 3, loss 0.5290, train acc 0.824, test acc 0.828
epoch 4, loss 0.5050, train acc 0.829, test acc 0.833
epoch 5, loss 0.4900, train acc 0.833, test acc 0.838

预测

训练完成后,现在我们可以演示如何对图像进行分类。给定一系列图像(第三行图像输出),我们比较一下它们的真实标签(第一行文本输出)和模型预测结果(第二行文本输出)。

In [16]:
for X, y in test_iter:
    break

true_labels = gb.get_fashion_mnist_labels(y.asnumpy())
pred_labels = gb.get_fashion_mnist_labels(net(X).argmax(axis=1).asnumpy())
titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]

gb.show_fashion_mnist(X[0:9], titles[0:9])
../_images/chapter_deep-learning-basics_softmax-regression-scratch_31_0.svg

小结

  • 我们可以使用 softmax 回归做多类别分类。与训练线性回归相比,你会发现训练 softmax 回归的步骤和它非常相似:获取并读取数据、定义模型和损失函数并使用优化算法训练模型。事实上,绝大多数深度学习模型的训练都有着类似的步骤。

练习

  • 本节中,我们直接按照 softmax 运算的数学定义来实现 softmax 函数。这可能会造成什么问题?(提示:试一试计算 \(\exp(50)\) 的大小。)
  • 本节中的cross_entropy函数是按照交叉熵损失函数的数学定义实现的。这样的实现方式可能有什么问题?(提示:思考一下对数函数的定义域。)
  • 你能想到哪些办法来解决上面的两个问题?

扫码直达讨论区

image0