线性回归

我们希望根据房屋的面积(平方英尺)和房龄(年)来估算房屋价格(美元)。 为了开发一个能预测房价的模型,我们需要收集一个真实的数据集。 这个数据集包括了房屋的销售价格、面积和房龄。 在机器学习的术语中,该数据集称为训练数据集(training data set) 或训练集(training set)。 每行数据(比如一次房屋交易相对应的数据)称为样本(sample), 也可以称为数据点(data point)或数据样本(data instance)。 我们把试图预测的目标(比如预测房屋价格)称为标签(label)或目标(target)。 预测所依据的自变量(面积和房龄)称为特征(feature)或协变量(covariate)。

线性模型

线性假设是指目标(房屋价格)可以表示为特征(面积和房龄)的加权和。$$PRICE=W_{area}*area+W_{age}*age+b$$w是权重,b是偏移量。给定一个数据集,我们的目标是寻找模型的权重w和偏置b, 使得根据模型做出的预测大体符合数据里的真实价格。

损失函数

在我们开始考虑如何用模型拟合(fit)数据之前,我们需要确定一个拟合程度的度量。 损失函数(loss function)能够量化目标的实际值与预测值之间的差距。 通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。

回归问题中最常用的损失函数是平方误差函数。当样本$$i$$的预测值为$$\hat{y}^{(i)}$$, 其相应的真实标签为$$y{(i)}$$时,平方误差为$$l{(i)}(\mathbf{w}, b) = \frac{1}{2} \left(\hat{y}^{(i)} - y{(i)}\right)2$$。

由于平方误差函数中的二次方项, 估计值和观测值之间较大的差异将导致更大的损失。 为了度量模型在整个数据集上的质量,我们需计算在训练集n个样本上的损失均值(也等价于求和)。

L(w,b)=1ni=1nl(i)(w,b)=1ni=1n12(wx(i)+by(i))2.L(\mathbf{w}, b) =\frac{1}{n}\sum_{i=1}^n l^{(i)}(\mathbf{w}, b) =\frac{1}{n} \sum_{i=1}^n \frac{1}{2}\left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right)^2.

解析解

在线性回归中,解析解是一种计算最优解的方法,也称为闭式解或正规方程法。其基本思想是通过对数据集进行矩阵运算,求出使得误差最小的权重矩阵(即最优参数),从而得到一个全局最优解。令 w\boldsymbol{w} 表示回归系数矩阵,X\boldsymbol{X} 表示全部特征向量组合的矩阵,y\boldsymbol{y} 表示标记列向量组成的矩阵。则线性回归的解析解可表示为:

w=(XTX)1XTy\boldsymbol{w}=(\boldsymbol{X}^\mathrm{T}\boldsymbol{X})^{-1}\boldsymbol{X}^\mathrm{T}\boldsymbol{y}

公式 w=(XTX)1XTy\boldsymbol{w}=(\boldsymbol{X}^\mathrm{T}\boldsymbol{X})^{-1}\boldsymbol{X}^\mathrm{T}\boldsymbol{y} 可以通过求导和极值原理来推导得到。下面假设输入样本矩阵 X\boldsymbol{X} 的每一个实例均为 mm 维向量,输出结果为yy

假设我们想要通过矩阵 X\boldsymbol{X} 来预测输出结果 y\boldsymbol{y},我们假定预测值为 y^=Xw\hat{\boldsymbol{y}}=\boldsymbol{X}\boldsymbol{w}。为了最小化预测值 y^\hat{\boldsymbol{y}} 和实际值 y\boldsymbol{y} 之间的差距,我们可以通过最小二乘法来确定最佳的权重向量 w\boldsymbol{w}。最小二乘法就是通过最小化误差平方和的方法得到 w\boldsymbol{w}

定义误差 eie_i 为第 ii 个样本预测值 y^i\hat{y}_i 与 实际值 yiy_i 之间的差距,即 ei=yiy^ie_i = y_i-\hat{y}_i。则所有样本的误差平方和可以表示为:

J(w)=i=1nei2=i=1n(yixiTw)2=(yXw)T(yXw)J(\boldsymbol{w})=\sum_{i=1}^{n}e_i^{2}=\sum_{i=1}^{n}(y_i - \boldsymbol{x}_i^\mathrm{T}\boldsymbol{w})^2=(\boldsymbol{y}-\boldsymbol{Xw})^\mathrm{T}(\boldsymbol{y}-\boldsymbol{Xw})

其中,xi\boldsymbol{x}_i 表示第 ii 个输入样本的特征向量,X\boldsymbol{X} 表示整个训练集的特征矩阵,w\boldsymbol{w} 表示我们要求解的权重向量,y\boldsymbol{y} 表示整个训练集的目标向量。

为了找到使误差平方和最小的权重向量,我们需要对 J(w)J(\boldsymbol{w}) 求导。为了简化求导的过程,我们可以将 J(w)J(\boldsymbol{w}) 写成如下形式:

J(w)=wTXTXw2wTXTy+yTyJ(\boldsymbol{w})=\boldsymbol{w}^\mathrm{T}\boldsymbol{X}^\mathrm{T}\boldsymbol{X}\boldsymbol{w}-2\boldsymbol{w}^\mathrm{T}\boldsymbol{X}^\mathrm{T}\boldsymbol{y}+\boldsymbol{y}^T\boldsymbol{y}

其中,我们省略了常数项,并利用了 (yXw)T(yXw)=yTy2wTXTy+wTXTXw(\boldsymbol{y}-\boldsymbol{X}\boldsymbol{w})^T(\boldsymbol{y}-\boldsymbol{X}\boldsymbol{w})=\boldsymbol{y}^T\boldsymbol{y}-2\boldsymbol{w}^\mathrm{T}\boldsymbol{X}^\mathrm{T}\boldsymbol{y}+\boldsymbol{w}^\mathrm{T}\boldsymbol{X}^\mathrm{T}\boldsymbol{X}\boldsymbol{w} 的关系式。

J(w)J(\boldsymbol{w}) 求导可以得到:

J(w)w=2XTXw2XTy\frac{\partial J(\boldsymbol{w})}{\partial \boldsymbol{w}}=2\boldsymbol{X}^\mathrm{T}\boldsymbol{X}\boldsymbol{w}-2\boldsymbol{X}^\mathrm{T}\boldsymbol{y}

令导数为 0,则可以得到:

w=(XTX)1XTy\boldsymbol{w}=(\boldsymbol{X}^\mathrm{T}\boldsymbol{X})^{-1}\boldsymbol{X}^\mathrm{T}\boldsymbol{y}

因此,w\boldsymbol{w} 的最优解可由矩阵 XTX\boldsymbol{X}^\mathrm{T}\boldsymbol{X} 的逆矩阵和 XTy\boldsymbol{X}^\mathrm{T}\boldsymbol{y} 相乘而得。这就是线性回归模型的闭式解(即解析解)

随机梯度下降

即使在我们无法得到解析解的情况下,我们仍然可以有效地训练模型。 随机梯度下降这种方法几乎可以优化所有深度学习模型。 它通过不断地在损失函数递减的方向上更新参数来降低误差。

梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值) 关于模型参数的导数(在这里也可以称为梯度)。 但实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。 因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本, 这种变体叫做小批量随机梯度下降(minibatch stochastic gradient descent)。

在每次迭代中,我们首先随机抽样一个小批量$$\mathcal{B}$$, 它是由固定数量的训练样本组成的。 然后,我们计算小批量的平均损失关于模型参数的导数(也可以称为梯度)。 最后,我们将梯度乘以一个预先确定的正数$$\eta,$$并从当前参数的值中减掉。

(w,b)(w,b)ηBiB(w,b)l(i)(w,b)(\mathbf{w},b) \leftarrow (\mathbf{w},b) - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{(\mathbf{w},b)} l^{(i)}(\mathbf{w},b)

总结一下,算法的步骤如下: (1)初始化模型参数的值,如随机初始化; (2)从数据集中随机抽取小批量样本且在负梯度的方向上更新参数,并不断迭代这一步骤。 对于平方损失和仿射变换,我们可以明确地写成如下形式:

\begin{split}\begin{aligned} \mathbf{w} &\leftarrow \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{\mathbf{w}} l^{(i)}(\mathbf{w}, b) = \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right),\\ b &\leftarrow b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_b l^{(i)}(\mathbf{w}, b) = b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right). \end{aligned}\end{split}

|\mathcal{B}|$$表示每个小批量中的样本数,这也称为*批量大小*(batch size)。$$\eta$$表示*学习率*(learning rate)。 ### 预测 给定“已学习”的线性回归模型, 现在我们可以通过房屋面积x1和房龄x2来估计一个(未包含在训练数据中的)新房屋价格。 给定特征估计目标的过程通常称为*预测*(prediction)或*推断*(inference)。 #### 加速 矢量化代码通常会带来数量级的加速。 另外,我们将更多的数学运算放到库中,而无须自己编写那么多的计算,从而减少了出错的可能性。 #### 正态分布 若随机变量$$ X$$具有均值$$\mu$$和方差$$\sigma^2$$(标准差$$\sigma$$),其正态分布概率密度函数如下: $$p(x) = \frac{1}{\sqrt{2 \pi \sigma^2}} \exp\left(-\frac{1}{2 \sigma^2} (x - \mu)^2\right)

../_images/output_linear-regression_216540_70_0.svg

改变均值会产生沿x轴的偏移,增加方差将会分散分布、降低其峰值。

从线性回归到深度网络

该图只显示连接模式,即只显示每个输入如何连接到输出,隐去了权重和偏置的值。

../_images/singleneuron.svg

输入数为d,神经网络层数为1。我们可以将线性回归模型视为仅由单个人工神经元组成的神经网络,或称为单层神经网络。对于线性回归,每个输入都与每个输出(在本例中只有一个输出)相连, 我们将这种变换称为全连接层(fully-connected layer)或称为稠密层(dense layer)。

线性回归实现过程

生成数据集

生成一个包含1000个样本的数据集, 每个样本包含从标准正态分布中采样的2个特征。

我们使用线性模型参数$$\mathbf{w} = [2, -3.4]^\top$$、b = 4.2和噪声项$$\epsilon$$生成数据集及其标签:

y=Xw+b+ϵ\mathbf{y}= \mathbf{X} \mathbf{w} + b + \mathbf\epsilon

def synthetic_data(w, b, num_examples):  #@save
    """生成y=Xw+b+噪声"""
    X = torch.normal(0, 1, (num_examples, len(w)))
    y = torch.matmul(X, w) + b
    y += torch.normal(0, 0.01, y.shape)
    return X, y.reshape((-1, 1))

true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
features: tensor([-0.3679, -1.8471])
label: tensor([9.7361])

注意,features中的每一行都包含一个二维数据样本, labels中的每一行都包含一维标签值(一个标量)。

通过生成第二个特征features[:, 1]labels的散点图, 可以直观观察到两者之间的线性关系。

读取数据集

回想一下,训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数, 该函数能打乱数据集中的样本并以小批量方式获取数据。

def data_iter(batch_size, features, labels):
    num_examples = len(features)
    indices = list(range(num_examples))
    # 这些样本是随机读取的,没有特定的顺序
    random.shuffle(indices)
    for i in range(0, num_examples, batch_size):
        batch_indices = torch.tensor(
            indices[i: min(i + batch_size, num_examples)])
        yield features[batch_indices], labels[batch_indices]
  • 函数有三个输入参数:batch_size 表示每次迭代所选取的样本数量,features 和 labels 分别表示输入特征和对应的标签。
  • 通过 features 的长度获取数据集的样本数,在 indices 列表中记录所有样本的下标。
  • 对 indices 进行随机重排,将样本打乱,以保证每个 batch 中的样本具有随机性。
  • 在 for 循环内,从 0 到 num_examples 循环,每次取出 batch_size 个样本,并将这些样本的下标通过 torch.tensor 转换为 Tensor 类型。
  • 使用 yield 关键字将每次迭代生成的小批量样本(features 和 labels)返回给调用者。这里使用了 Python 的迭代器来实现,与常规的列表返回方法不同,它可以逐步生成数据,在大数据量的情况下节省了内存空间

初始化模型参数

从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。 每次更新都需要计算损失函数关于模型参数的梯度。 有了这个梯度,我们就可以向减小损失的方向更新每个参数。

定义模型

当我们用一个向量加一个标量时,标量会被加到向量的每个分量上。

定义损失函数

平方损失函数

定义优化算法

小批量随机梯度下降

在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。 接下来,朝着减少损失的方向更新我们的参数。 下面的函数实现小批量随机梯度下降更新。 该函数接受模型参数集合、学习速率和批量大小作为输入。每 一步更新的大小由学习速率lr决定。 因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size) 来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。

def sgd(params, lr, batch_size):  #@save
    """小批量随机梯度下降"""
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

更新其值为减去学习率乘以参数的梯度均值。

训练

  • 初始化参数
  • 重复以下训练,直到完成
    • 计算梯度$$\mathbf{g} \leftarrow \partial_{(\mathbf{w},b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l(\mathbf{x}^{(i)}, y^{(i)}, \mathbf{w}, b)$$
    • 更新参数$$(\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g}$$

在每个迭代周期(epoch)中,我们使用data_iter函数遍历整个数据集, 并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。 这里的迭代周期个数num_epochs和学习率lr都是超参数,分别设为3和0.03。

lr = 0.03            # 学习率
num_epochs = 3       # 迭代周期数
net = linreg         # 线性回归模型函数
loss = squared_loss  # 损失函数
for epoch in range(num_epochs):           # 迭代 num_epochs 次
    for X, y in data_iter(batch_size, features, labels):  # 遍历数据集,每次随机读取 batch_size 个样本
        l = loss(net(X, w, b), y)  # 计算模型预测值与真实值之间的损失,使用的损失函数是二次损失
        l.sum().backward()          # 自动求梯度,并将结果求和作为一个标量
        sgd([w, b], lr, batch_size)  # 使用小批量的梯度,按学习率更新模型参数

    with torch.no_grad():            # 关闭自动求梯度
        train_l = loss(net(features, w, b), labels)  # 在该迭代结束时计算模型在整个训练集上的损失
        print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}') # 打印输出模型在整个训练集上的损失
epoch 1, loss 0.043705
epoch 2, loss 0.000172
epoch 3, loss 0.000047

用深度学习框架实现线性回归

读取数据集

featureslabels作为API的参数传递,并通过数据迭代器指定batch_size。 此外,布尔值is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据。

def load_array(data_arrays, batch_size, is_train=True):  #@save
    """构造一个PyTorch数据迭代器"""
    dataset = data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset, batch_size, shuffle=is_train)

batch_size = 10
data_iter = load_array((features, labels), batch_size)

这是一段用于构造 PyTorch 数据迭代器的代码。下面是对每一行代码的解释:

def load_array(data_arrays, batch_size, is_train=True):
    """构造一个PyTorch数据迭代器"""
    dataset = data.TensorDataset(*data_arrays)  # 使用TensorDataset封装输入数据
    return data.DataLoader(dataset, batch_size, shuffle=is_train)  # 使用DataLoader加载数据集并返回迭代器

batch_size = 10                     # 批量大小
data_iter = load_array((features, labels), batch_size)  # 构造一个PyTorch数据迭代器

这段代码主要用于将数据集加载到 PyTorch 数据迭代器中。load_array() 函数将 PyTorch 中的 Tensor 数据类型封装到 TensorDataset 中,并将其作为数据集传递到 PyTorch 的 DataLoader 中,从而能够使用 PyTorch 的支持便捷的数据加载方式。

在这段代码中,data_arrays 是由特征数据和标签数据构成的元组,batch_size 是批量大小,is_train 表示是否为训练集。DataLoader 对象将数据集分批加载,然后返回一个 Python iterable,通过 __next__ 方法可以依次得到批量数据。

通过这段代码可以方便地将数据集转换为 PyTorch 数据迭代器,从而提高模型训练的效率。

定义模型

在PyTorch中,全连接层在Linear类中定义。 值得注意的是,我们将两个参数传递到nn.Linear中。 第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。

# nn是神经网络的缩写
from torch import nn

net = nn.Sequential(nn.Linear(2, 1))

初始化模型参数

net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)

定义损失函数

计算均方误差使用的是MSELoss类,也称为平方$$L_2$$范数。 默认情况下,它返回所有样本损失的平均值。

$ MSE = \frac{1}{n}\sum_{i=1}^{n} (y_{i}-\hat{y_{i}})^{2} $

loss = nn.MSELoss()

优化算法

小批量随机梯度下降算法是一种优化神经网络的标准工具, PyTorch在optim模块中实现了该算法的许多变种。 当我们实例化一个SGD实例时,我们要指定优化的参数 (可通过net.parameters()从我们的模型中获得)以及优化算法所需的超参数字典。

trainer = torch.optim.SGD(net.parameters(), lr=0.03)

训练

回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(train_data), 不停地从中获取一个小批量的输入和相应的标签。 对于每一个小批量,我们会进行以下步骤:

  • 通过调用net(X)生成预测并计算损失l(前向传播)。
  • 通过进行反向传播来计算梯度。
  • 通过调用优化器来更新模型参数。
num_epochs = 3
for epoch in range(num_epochs):
    for X, y in data_iter:
        l = loss(net(X) ,y)
        trainer.zero_grad()
        l.backward()
        trainer.step()
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')

softmax回归

独热编码是一个向量,它的分量和类别一样多。 类别对应的分量设置为1,其他所有分量设置为0。

为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。

为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数(affine function)。 每个输出对应于它自己的仿射函数。 在我们的例子中,由于我们有4个特征和3个可能的输出类别, 我们将需要12个标量来表示权重(带下标的w), 3个标量来表示偏置(带下标的b)。 下面我们为每个输入计算三个未规范化的预测(logit):$$O_1、O_2$$和$$O_3$$。

\begin{split}\begin{aligned} o_1 &= x_1 w_{11} + x_2 w_{12} + x_3 w_{13} + x_4 w_{14} + b_1,\\ o_2 &= x_1 w_{21} + x_2 w_{22} + x_3 w_{23} + x_4 w_{24} + b_2,\\ o_3 &= x_1 w_{31} + x_2 w_{32} + x_3 w_{33} + x_4 w_{34} + b_3. \end{aligned}\end{split}

与线性回归一样,softmax回归也是一个单层神经网络。每个输出取决于所有输入,所以也是全连接层。

../_images/softmaxreg.svg

现在我们将优化参数以最大化观测数据的概率。 为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。

我们希望模型的输出$$\hat{y}_j$$可以视为属于类j的概率, 然后选择具有最大输出值的类别$$\operatorname*{argmax}_j y_j$$作为我们的预测。 例如,如果$$\hat{y}_1、\hat{y}_2$$和$$\hat{y}_3$$分别为0.1、0.8和0.1, 那么我们预测的类别是2,在我们的例子中代表“鸡”。

要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。->softmax函数

y^=softmax(o)其中y^j=exp(oj)kexp(ok)\hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{其中}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}

*argmaxjy^j=*argmaxjoj\operatorname*{argmax}_j \hat y_j = \operatorname*{argmax}_j o_j

尽管softmax是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。 因此,softmax回归是一个线性模型(linear model)。

接下来,我们需要一个损失函数来度量预测的效果。 我们将使用最大似然估计。

损失函数(对数似然)

P(YX)=i=1nP(y(i)x(i))P(\mathbf{Y} \mid \mathbf{X}) = \prod_{i=1}^n P(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)})

logP(YX)=i=1nlogP(y(i)x(i))=i=1nl(y(i),y^(i))-\log P(\mathbf{Y} \mid \mathbf{X}) = \sum_{i=1}^n -\log P(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}) = \sum_{i=1}^n l(\mathbf{y}^{(i)}, \hat{\mathbf{y}}^{(i)})

l(y,y^)=j=1qyjlogy^jl(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j

SOFTMAX及其导数

\begin{split}\begin{aligned} l(\mathbf{y}, \hat{\mathbf{y}}) &= - \sum_{j=1}^q y_j \log \frac{\exp(o_j)}{\sum_{k=1}^q \exp(o_k)} \\ &= \sum_{j=1}^q y_j \log \sum_{k=1}^q \exp(o_k) - \sum_{j=1}^q y_j o_j\\ &= \log \sum_{k=1}^q \exp(o_k) - \sum_{j=1}^q y_j o_j. \end{aligned}\end{split}

ojl(y,y^)=exp(oj)k=1qexp(ok)yj=softmax(o)jyj\partial_{o_j} l(\mathbf{y}, \hat{\mathbf{y}}) = \frac{\exp(o_j)}{\sum_{k=1}^q \exp(o_k)} - y_j = \mathrm{softmax}(\mathbf{o})_j - y_j

换句话说,导数是我们softmax模型分配的概率与实际发生的情况(由独热标签向量表示)之间的差异。

SOFTMAX从零实现

初始化

num_inputs = 784
num_outputs = 10

W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)

定义softmax

回想一下,实现softmax由三个步骤组成:

  1. 对每个项求幂(使用exp);
  2. 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;
  3. 将每一行除以其规范化常数,确保结果的和为1。

定义模型

def net(X):
    return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)

定义损失函数

训练

定义一个函数来训练一个迭代周期。

def train_epoch_ch3(net, train_iter, loss, updater):  #@save
    """训练模型一个迭代周期(定义见第3章)"""
    # 将模型设置为训练模式
    if isinstance(net, torch.nn.Module):
        net.train()
    # 训练损失总和、训练准确度总和、样本数
    metric = Accumulator(3)
    for X, y in train_iter:
        # 计算梯度并更新参数
        y_hat = net(X)
        l = loss(y_hat, y)
        if isinstance(updater, torch.optim.Optimizer):
            # 使用PyTorch内置的优化器和损失函数
            updater.zero_grad()
            l.mean().backward()
            updater.step()
        else:
            # 使用定制的优化器和损失函数
            l.sum().backward()
            updater(X.shape[0])
        metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
    # 返回训练损失和训练精度
    return metric[0] / metric[2], metric[1] / metric[2]

预测

def predict_ch3(net, test_iter, n=6):  #@save
    """预测标签(定义见第3章)"""
    for X, y in test_iter:
        break
    trues = d2l.get_fashion_mnist_labels(y)
    preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
    titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
    d2l.show_images(
        X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])

predict_ch3(net, test_iter)

线性回归

我们希望根据房屋的面积(平方英尺)和房龄(年)来估算房屋价格(美元)。 为了开发一个能预测房价的模型,我们需要收集一个真实的数据集。 这个数据集包括了房屋的销售价格、面积和房龄。 在机器学习的术语中,该数据集称为训练数据集(training data set) 或训练集(training set)。 每行数据(比如一次房屋交易相对应的数据)称为样本(sample), 也可以称为数据点(data point)或数据样本(data instance)。 我们把试图预测的目标(比如预测房屋价格)称为标签(label)或目标(target)。 预测所依据的自变量(面积和房龄)称为特征(feature)或协变量(covariate)。

线性模型

线性假设是指目标(房屋价格)可以表示为特征(面积和房龄)的加权和。$$PRICE=W_{area}*area+W_{age}*age+b$$w是权重,b是偏移量。给定一个数据集,我们的目标是寻找模型的权重w和偏置b, 使得根据模型做出的预测大体符合数据里的真实价格。

损失函数

在我们开始考虑如何用模型拟合(fit)数据之前,我们需要确定一个拟合程度的度量。 损失函数(loss function)能够量化目标的实际值与预测值之间的差距。 通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。

回归问题中最常用的损失函数是平方误差函数。当样本$$i$$的预测值为$$\hat{y}^{(i)}$$, 其相应的真实标签为$$y{(i)}$$时,平方误差为$$l{(i)}(\mathbf{w}, b) = \frac{1}{2} \left(\hat{y}^{(i)} - y{(i)}\right)2$$。

由于平方误差函数中的二次方项, 估计值和观测值之间较大的差异将导致更大的损失。 为了度量模型在整个数据集上的质量,我们需计算在训练集n个样本上的损失均值(也等价于求和)。

L(w,b)=1ni=1nl(i)(w,b)=1ni=1n12(wx(i)+by(i))2.L(\mathbf{w}, b) =\frac{1}{n}\sum_{i=1}^n l^{(i)}(\mathbf{w}, b) =\frac{1}{n} \sum_{i=1}^n \frac{1}{2}\left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right)^2.

解析解

在线性回归中,解析解是一种计算最优解的方法,也称为闭式解或正规方程法。其基本思想是通过对数据集进行矩阵运算,求出使得误差最小的权重矩阵(即最优参数),从而得到一个全局最优解。令 w\boldsymbol{w} 表示回归系数矩阵,X\boldsymbol{X} 表示全部特征向量组合的矩阵,y\boldsymbol{y} 表示标记列向量组成的矩阵。则线性回归的解析解可表示为:

w=(XTX)1XTy\boldsymbol{w}=(\boldsymbol{X}^\mathrm{T}\boldsymbol{X})^{-1}\boldsymbol{X}^\mathrm{T}\boldsymbol{y}

公式 w=(XTX)1XTy\boldsymbol{w}=(\boldsymbol{X}^\mathrm{T}\boldsymbol{X})^{-1}\boldsymbol{X}^\mathrm{T}\boldsymbol{y} 可以通过求导和极值原理来推导得到。下面假设输入样本矩阵 X\boldsymbol{X} 的每一个实例均为 mm 维向量,输出结果为yy

假设我们想要通过矩阵 X\boldsymbol{X} 来预测输出结果 y\boldsymbol{y},我们假定预测值为 y^=Xw\hat{\boldsymbol{y}}=\boldsymbol{X}\boldsymbol{w}。为了最小化预测值 y^\hat{\boldsymbol{y}} 和实际值 y\boldsymbol{y} 之间的差距,我们可以通过最小二乘法来确定最佳的权重向量 w\boldsymbol{w}。最小二乘法就是通过最小化误差平方和的方法得到 w\boldsymbol{w}

定义误差 eie_i 为第 ii 个样本预测值 y^i\hat{y}_i 与 实际值 yiy_i 之间的差距,即 ei=yiy^ie_i = y_i-\hat{y}_i。则所有样本的误差平方和可以表示为:

J(w)=i=1nei2=i=1n(yixiTw)2=(yXw)T(yXw)J(\boldsymbol{w})=\sum_{i=1}^{n}e_i^{2}=\sum_{i=1}^{n}(y_i - \boldsymbol{x}_i^\mathrm{T}\boldsymbol{w})^2=(\boldsymbol{y}-\boldsymbol{Xw})^\mathrm{T}(\boldsymbol{y}-\boldsymbol{Xw})

其中,xi\boldsymbol{x}_i 表示第 ii 个输入样本的特征向量,X\boldsymbol{X} 表示整个训练集的特征矩阵,w\boldsymbol{w} 表示我们要求解的权重向量,y\boldsymbol{y} 表示整个训练集的目标向量。

为了找到使误差平方和最小的权重向量,我们需要对 J(w)J(\boldsymbol{w}) 求导。为了简化求导的过程,我们可以将 J(w)J(\boldsymbol{w}) 写成如下形式:

J(w)=wTXTXw2wTXTy+yTyJ(\boldsymbol{w})=\boldsymbol{w}^\mathrm{T}\boldsymbol{X}^\mathrm{T}\boldsymbol{X}\boldsymbol{w}-2\boldsymbol{w}^\mathrm{T}\boldsymbol{X}^\mathrm{T}\boldsymbol{y}+\boldsymbol{y}^T\boldsymbol{y}

其中,我们省略了常数项,并利用了 (yXw)T(yXw)=yTy2wTXTy+wTXTXw(\boldsymbol{y}-\boldsymbol{X}\boldsymbol{w})^T(\boldsymbol{y}-\boldsymbol{X}\boldsymbol{w})=\boldsymbol{y}^T\boldsymbol{y}-2\boldsymbol{w}^\mathrm{T}\boldsymbol{X}^\mathrm{T}\boldsymbol{y}+\boldsymbol{w}^\mathrm{T}\boldsymbol{X}^\mathrm{T}\boldsymbol{X}\boldsymbol{w} 的关系式。

J(w)J(\boldsymbol{w}) 求导可以得到:

J(w)w=2XTXw2XTy\frac{\partial J(\boldsymbol{w})}{\partial \boldsymbol{w}}=2\boldsymbol{X}^\mathrm{T}\boldsymbol{X}\boldsymbol{w}-2\boldsymbol{X}^\mathrm{T}\boldsymbol{y}

令导数为 0,则可以得到:

w=(XTX)1XTy\boldsymbol{w}=(\boldsymbol{X}^\mathrm{T}\boldsymbol{X})^{-1}\boldsymbol{X}^\mathrm{T}\boldsymbol{y}

因此,w\boldsymbol{w} 的最优解可由矩阵 XTX\boldsymbol{X}^\mathrm{T}\boldsymbol{X} 的逆矩阵和 XTy\boldsymbol{X}^\mathrm{T}\boldsymbol{y} 相乘而得。这就是线性回归模型的闭式解(即解析解)

随机梯度下降

即使在我们无法得到解析解的情况下,我们仍然可以有效地训练模型。 随机梯度下降这种方法几乎可以优化所有深度学习模型。 它通过不断地在损失函数递减的方向上更新参数来降低误差。

梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值) 关于模型参数的导数(在这里也可以称为梯度)。 但实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。 因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本, 这种变体叫做小批量随机梯度下降(minibatch stochastic gradient descent)。

在每次迭代中,我们首先随机抽样一个小批量$$\mathcal{B}$$, 它是由固定数量的训练样本组成的。 然后,我们计算小批量的平均损失关于模型参数的导数(也可以称为梯度)。 最后,我们将梯度乘以一个预先确定的正数$$\eta,$$并从当前参数的值中减掉。

(w,b)(w,b)ηBiB(w,b)l(i)(w,b)(\mathbf{w},b) \leftarrow (\mathbf{w},b) - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{(\mathbf{w},b)} l^{(i)}(\mathbf{w},b)

总结一下,算法的步骤如下: (1)初始化模型参数的值,如随机初始化; (2)从数据集中随机抽取小批量样本且在负梯度的方向上更新参数,并不断迭代这一步骤。 对于平方损失和仿射变换,我们可以明确地写成如下形式:

\begin{split}\begin{aligned} \mathbf{w} &\leftarrow \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{\mathbf{w}} l^{(i)}(\mathbf{w}, b) = \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right),\\ b &\leftarrow b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_b l^{(i)}(\mathbf{w}, b) = b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right). \end{aligned}\end{split}

|\mathcal{B}|$$表示每个小批量中的样本数,这也称为*批量大小*(batch size)。$$\eta$$表示*学习率*(learning rate)。 ### 预测 给定“已学习”的线性回归模型, 现在我们可以通过房屋面积x1和房龄x2来估计一个(未包含在训练数据中的)新房屋价格。 给定特征估计目标的过程通常称为*预测*(prediction)或*推断*(inference)。 #### 加速 矢量化代码通常会带来数量级的加速。 另外,我们将更多的数学运算放到库中,而无须自己编写那么多的计算,从而减少了出错的可能性。 #### 正态分布 若随机变量$$ X$$具有均值$$\mu$$和方差$$\sigma^2$$(标准差$$\sigma$$),其正态分布概率密度函数如下: $$p(x) = \frac{1}{\sqrt{2 \pi \sigma^2}} \exp\left(-\frac{1}{2 \sigma^2} (x - \mu)^2\right)

../_images/output_linear-regression_216540_70_0.svg

改变均值会产生沿x轴的偏移,增加方差将会分散分布、降低其峰值。

从线性回归到深度网络

该图只显示连接模式,即只显示每个输入如何连接到输出,隐去了权重和偏置的值。

../_images/singleneuron.svg

输入数为d,神经网络层数为1。我们可以将线性回归模型视为仅由单个人工神经元组成的神经网络,或称为单层神经网络。对于线性回归,每个输入都与每个输出(在本例中只有一个输出)相连, 我们将这种变换称为全连接层(fully-connected layer)或称为稠密层(dense layer)。

线性回归实现过程

生成数据集

生成一个包含1000个样本的数据集, 每个样本包含从标准正态分布中采样的2个特征。

我们使用线性模型参数$$\mathbf{w} = [2, -3.4]^\top$$、b = 4.2和噪声项$$\epsilon$$生成数据集及其标签:

y=Xw+b+ϵ\mathbf{y}= \mathbf{X} \mathbf{w} + b + \mathbf\epsilon

def synthetic_data(w, b, num_examples):  #@save
    """生成y=Xw+b+噪声"""
    X = torch.normal(0, 1, (num_examples, len(w)))
    y = torch.matmul(X, w) + b
    y += torch.normal(0, 0.01, y.shape)
    return X, y.reshape((-1, 1))

true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
features: tensor([-0.3679, -1.8471])
label: tensor([9.7361])

注意,features中的每一行都包含一个二维数据样本, labels中的每一行都包含一维标签值(一个标量)。

通过生成第二个特征features[:, 1]labels的散点图, 可以直观观察到两者之间的线性关系。

读取数据集

回想一下,训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数, 该函数能打乱数据集中的样本并以小批量方式获取数据。

def data_iter(batch_size, features, labels):
    num_examples = len(features)
    indices = list(range(num_examples))
    # 这些样本是随机读取的,没有特定的顺序
    random.shuffle(indices)
    for i in range(0, num_examples, batch_size):
        batch_indices = torch.tensor(
            indices[i: min(i + batch_size, num_examples)])
        yield features[batch_indices], labels[batch_indices]
  • 函数有三个输入参数:batch_size 表示每次迭代所选取的样本数量,features 和 labels 分别表示输入特征和对应的标签。
  • 通过 features 的长度获取数据集的样本数,在 indices 列表中记录所有样本的下标。
  • 对 indices 进行随机重排,将样本打乱,以保证每个 batch 中的样本具有随机性。
  • 在 for 循环内,从 0 到 num_examples 循环,每次取出 batch_size 个样本,并将这些样本的下标通过 torch.tensor 转换为 Tensor 类型。
  • 使用 yield 关键字将每次迭代生成的小批量样本(features 和 labels)返回给调用者。这里使用了 Python 的迭代器来实现,与常规的列表返回方法不同,它可以逐步生成数据,在大数据量的情况下节省了内存空间

初始化模型参数

从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。 每次更新都需要计算损失函数关于模型参数的梯度。 有了这个梯度,我们就可以向减小损失的方向更新每个参数。

定义模型

当我们用一个向量加一个标量时,标量会被加到向量的每个分量上。

定义损失函数

平方损失函数

定义优化算法

小批量随机梯度下降

在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。 接下来,朝着减少损失的方向更新我们的参数。 下面的函数实现小批量随机梯度下降更新。 该函数接受模型参数集合、学习速率和批量大小作为输入。每 一步更新的大小由学习速率lr决定。 因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size) 来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。

def sgd(params, lr, batch_size):  #@save
    """小批量随机梯度下降"""
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

更新其值为减去学习率乘以参数的梯度均值。

训练

  • 初始化参数
  • 重复以下训练,直到完成
    • 计算梯度$$\mathbf{g} \leftarrow \partial_{(\mathbf{w},b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l(\mathbf{x}^{(i)}, y^{(i)}, \mathbf{w}, b)$$
    • 更新参数$$(\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g}$$

在每个迭代周期(epoch)中,我们使用data_iter函数遍历整个数据集, 并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。 这里的迭代周期个数num_epochs和学习率lr都是超参数,分别设为3和0.03。

lr = 0.03            # 学习率
num_epochs = 3       # 迭代周期数
net = linreg         # 线性回归模型函数
loss = squared_loss  # 损失函数
for epoch in range(num_epochs):           # 迭代 num_epochs 次
    for X, y in data_iter(batch_size, features, labels):  # 遍历数据集,每次随机读取 batch_size 个样本
        l = loss(net(X, w, b), y)  # 计算模型预测值与真实值之间的损失,使用的损失函数是二次损失
        l.sum().backward()          # 自动求梯度,并将结果求和作为一个标量
        sgd([w, b], lr, batch_size)  # 使用小批量的梯度,按学习率更新模型参数

    with torch.no_grad():            # 关闭自动求梯度
        train_l = loss(net(features, w, b), labels)  # 在该迭代结束时计算模型在整个训练集上的损失
        print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}') # 打印输出模型在整个训练集上的损失
epoch 1, loss 0.043705
epoch 2, loss 0.000172
epoch 3, loss 0.000047

用深度学习框架实现线性回归

读取数据集

featureslabels作为API的参数传递,并通过数据迭代器指定batch_size。 此外,布尔值is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据。

def load_array(data_arrays, batch_size, is_train=True):  #@save
    """构造一个PyTorch数据迭代器"""
    dataset = data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset, batch_size, shuffle=is_train)

batch_size = 10
data_iter = load_array((features, labels), batch_size)

这是一段用于构造 PyTorch 数据迭代器的代码。下面是对每一行代码的解释:

def load_array(data_arrays, batch_size, is_train=True):
    """构造一个PyTorch数据迭代器"""
    dataset = data.TensorDataset(*data_arrays)  # 使用TensorDataset封装输入数据
    return data.DataLoader(dataset, batch_size, shuffle=is_train)  # 使用DataLoader加载数据集并返回迭代器

batch_size = 10                     # 批量大小
data_iter = load_array((features, labels), batch_size)  # 构造一个PyTorch数据迭代器

这段代码主要用于将数据集加载到 PyTorch 数据迭代器中。load_array() 函数将 PyTorch 中的 Tensor 数据类型封装到 TensorDataset 中,并将其作为数据集传递到 PyTorch 的 DataLoader 中,从而能够使用 PyTorch 的支持便捷的数据加载方式。

在这段代码中,data_arrays 是由特征数据和标签数据构成的元组,batch_size 是批量大小,is_train 表示是否为训练集。DataLoader 对象将数据集分批加载,然后返回一个 Python iterable,通过 __next__ 方法可以依次得到批量数据。

通过这段代码可以方便地将数据集转换为 PyTorch 数据迭代器,从而提高模型训练的效率。

定义模型

在PyTorch中,全连接层在Linear类中定义。 值得注意的是,我们将两个参数传递到nn.Linear中。 第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。

# nn是神经网络的缩写
from torch import nn

net = nn.Sequential(nn.Linear(2, 1))

初始化模型参数

net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)

定义损失函数

计算均方误差使用的是MSELoss类,也称为平方$$L_2$$范数。 默认情况下,它返回所有样本损失的平均值。

$ MSE = \frac{1}{n}\sum_{i=1}^{n} (y_{i}-\hat{y_{i}})^{2} $

loss = nn.MSELoss()

优化算法

小批量随机梯度下降算法是一种优化神经网络的标准工具, PyTorch在optim模块中实现了该算法的许多变种。 当我们实例化一个SGD实例时,我们要指定优化的参数 (可通过net.parameters()从我们的模型中获得)以及优化算法所需的超参数字典。

trainer = torch.optim.SGD(net.parameters(), lr=0.03)

训练

回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(train_data), 不停地从中获取一个小批量的输入和相应的标签。 对于每一个小批量,我们会进行以下步骤:

  • 通过调用net(X)生成预测并计算损失l(前向传播)。
  • 通过进行反向传播来计算梯度。
  • 通过调用优化器来更新模型参数。
num_epochs = 3
for epoch in range(num_epochs):
    for X, y in data_iter:
        l = loss(net(X) ,y)
        trainer.zero_grad()
        l.backward()
        trainer.step()
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')

softmax回归

独热编码是一个向量,它的分量和类别一样多。 类别对应的分量设置为1,其他所有分量设置为0。

为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。

为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数(affine function)。 每个输出对应于它自己的仿射函数。 在我们的例子中,由于我们有4个特征和3个可能的输出类别, 我们将需要12个标量来表示权重(带下标的w), 3个标量来表示偏置(带下标的b)。 下面我们为每个输入计算三个未规范化的预测(logit):$$O_1、O_2$$和$$O_3$$。

\begin{split}\begin{aligned} o_1 &= x_1 w_{11} + x_2 w_{12} + x_3 w_{13} + x_4 w_{14} + b_1,\\ o_2 &= x_1 w_{21} + x_2 w_{22} + x_3 w_{23} + x_4 w_{24} + b_2,\\ o_3 &= x_1 w_{31} + x_2 w_{32} + x_3 w_{33} + x_4 w_{34} + b_3. \end{aligned}\end{split}

与线性回归一样,softmax回归也是一个单层神经网络。每个输出取决于所有输入,所以也是全连接层。

../_images/softmaxreg.svg

现在我们将优化参数以最大化观测数据的概率。 为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。

我们希望模型的输出$$\hat{y}_j$$可以视为属于类j的概率, 然后选择具有最大输出值的类别$$\operatorname*{argmax}_j y_j$$作为我们的预测。 例如,如果$$\hat{y}_1、\hat{y}_2$$和$$\hat{y}_3$$分别为0.1、0.8和0.1, 那么我们预测的类别是2,在我们的例子中代表“鸡”。

要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。->softmax函数

y^=softmax(o)其中y^j=exp(oj)kexp(ok)\hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{其中}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}

*argmaxjy^j=*argmaxjoj\operatorname*{argmax}_j \hat y_j = \operatorname*{argmax}_j o_j

尽管softmax是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。 因此,softmax回归是一个线性模型(linear model)。

接下来,我们需要一个损失函数来度量预测的效果。 我们将使用最大似然估计。

损失函数(对数似然)

P(YX)=i=1nP(y(i)x(i))P(\mathbf{Y} \mid \mathbf{X}) = \prod_{i=1}^n P(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)})

logP(YX)=i=1nlogP(y(i)x(i))=i=1nl(y(i),y^(i))-\log P(\mathbf{Y} \mid \mathbf{X}) = \sum_{i=1}^n -\log P(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}) = \sum_{i=1}^n l(\mathbf{y}^{(i)}, \hat{\mathbf{y}}^{(i)})

l(y,y^)=j=1qyjlogy^jl(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j

SOFTMAX及其导数

\begin{split}\begin{aligned} l(\mathbf{y}, \hat{\mathbf{y}}) &= - \sum_{j=1}^q y_j \log \frac{\exp(o_j)}{\sum_{k=1}^q \exp(o_k)} \\ &= \sum_{j=1}^q y_j \log \sum_{k=1}^q \exp(o_k) - \sum_{j=1}^q y_j o_j\\ &= \log \sum_{k=1}^q \exp(o_k) - \sum_{j=1}^q y_j o_j. \end{aligned}\end{split}

ojl(y,y^)=exp(oj)k=1qexp(ok)yj=softmax(o)jyj\partial_{o_j} l(\mathbf{y}, \hat{\mathbf{y}}) = \frac{\exp(o_j)}{\sum_{k=1}^q \exp(o_k)} - y_j = \mathrm{softmax}(\mathbf{o})_j - y_j

换句话说,导数是我们softmax模型分配的概率与实际发生的情况(由独热标签向量表示)之间的差异。

SOFTMAX从零实现

初始化

num_inputs = 784
num_outputs = 10

W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)

定义softmax

回想一下,实现softmax由三个步骤组成:

  1. 对每个项求幂(使用exp);
  2. 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;
  3. 将每一行除以其规范化常数,确保结果的和为1。

定义模型

def net(X):
    return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)

定义损失函数

训练

定义一个函数来训练一个迭代周期。

def train_epoch_ch3(net, train_iter, loss, updater):  #@save
    """训练模型一个迭代周期(定义见第3章)"""
    # 将模型设置为训练模式
    if isinstance(net, torch.nn.Module):
        net.train()
    # 训练损失总和、训练准确度总和、样本数
    metric = Accumulator(3)
    for X, y in train_iter:
        # 计算梯度并更新参数
        y_hat = net(X)
        l = loss(y_hat, y)
        if isinstance(updater, torch.optim.Optimizer):
            # 使用PyTorch内置的优化器和损失函数
            updater.zero_grad()
            l.mean().backward()
            updater.step()
        else:
            # 使用定制的优化器和损失函数
            l.sum().backward()
            updater(X.shape[0])
        metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
    # 返回训练损失和训练精度
    return metric[0] / metric[2], metric[1] / metric[2]

预测

def predict_ch3(net, test_iter, n=6):  #@save
    """预测标签(定义见第3章)"""
    for X, y in test_iter:
        break
    trues = d2l.get_fashion_mnist_labels(y)
    preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
    titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
    d2l.show_images(
        X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])

predict_ch3(net, test_iter)