线性回归与 softmax
线性回归
线性回归概要
线性回归 (linear regression)在回归的各种标准工具中最简单而且最流行。它可以追溯到19世纪初。线性回归基于几个简单的假设:首先,假设自变量 x \mathbf{x} x 和因变量 y y y 之间的关系是线性的,即 y y y可以表示为 x \mathbf{x} x 中元素的加权和,这里通常允许包含观测值的一些噪声;其次,我们假设任何噪声都比较正常,如噪声遵循正态分布。
通常,我们使用 n n n 来表示数据集中的样本数。对索引为 i i i 的样本,其输入表示为 x ( i ) = [ x 1 ( i ) , x 2 ( i ) ] ⊤ \mathbf{x}^{(i)} = [x_1^{(i)}, x_2^{(i)}]^\top x(i)=[x1(i),x2(i)]⊤,其对应的标签是 y ( i ) y^{(i)} y(i)。
线性模型
线性假设是指目标可以表示为特征的加权和,例如:
p r i c e = w a r e a ⋅ a r e a + w a g e ⋅ a g e + b \mathrm{price} = w_{\mathrm{area}} \cdot \mathrm{area} + w_{\mathrm{age}} \cdot \mathrm{age} + b price=warea⋅area+wage⋅age+b
w a r e a w_{\mathrm{area}} warea 和 w a g e w_{\mathrm{age}} wage 称为 权重(weight), b b b 称为 偏置(bias),或称为偏移量(offset)、截距(intercept)。权重决定了每个特征对我们预测值的影响。偏置是指当所有特征都取值为0时,预测值应该为多少。
给定一个数据集,我们的目标是寻找模型的权重 w \mathbf{w} w 和偏置 b b b,使得根据模型做出的预测大体符合数据里的真实价格。输出的预测值由输入特征通过线性模型的仿射变换决定,仿射变换由所选权重和偏置确定。在机器学习领域,我们通常使用的是高维数据集,建模时采用线性代数表示法会比较方便。当我们的输入包含 d d d 个特征时,我们将预测结果 y ^ \hat{y} y^(通常使用 “尖角” 符号表示估计值)表示为:
y ^ = w 1 x 1 + . . . + w d x d + b . \hat{y} = w_1 x_1 + ... + w_d x_d + b. y^=w1x1+...+wdxd+b.
将所有特征放到向量 x ∈ R d \mathbf{x} \in \mathbb{R}^d x∈Rd 中,并将所有权重放到向量 w ∈ R d \mathbf{w} \in \mathbb{R}^d w∈Rd 中,我们可以用点积形式来简洁地表达模型:
y ^ = w ⊤ x + b . \hat{y} = \mathbf{w}^\top \mathbf{x} + b. y^=w⊤x+b.
向量 x \mathbf{x} x 对应于单个数据样本的特征。用符号表示的矩阵 X ∈ R n × d \mathbf{X} \in \mathbb{R}^{n \times d} X∈Rn×d 可以很方便地引用我们整个数据集的 n n n 个样本。其中, X \mathbf{X} X 的每一行是一个样本,每一列是一种特征。
对于特征集合 X \mathbf{X} X ,预测值 y ^ ∈ R n \hat{\mathbf{y}} \in \mathbb{R}^n y^∈Rn 可以通过矩阵-向量乘法表示为:
y ^ = X w + b {\hat{\mathbf{y}}} = \mathbf{X} \mathbf{w} + b y^=Xw+b
这个过程中的求和将使用广播机制,给定训练数据特征 X \mathbf{X} X 和对应的已知标签 y \mathbf{y} y ,线性回归的目标是找到一组权重向量 w \mathbf{w} w 和偏置 b b b。当给定从 X \mathbf{X} X的同分布中取样的新样本特征时,找到的权重向量和偏置能够使得新样本预测标签的误差尽可能小。
虽然我们相信给定 x \mathbf{x} x 预测 y y y 的最佳模型会是线性的,但我们很难找到一个有 n n n个样本的真实数据集,其中对于所有的 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n, y ( i ) y^{(i)} y(i) 完全等于 w ⊤ x ( i ) + b \mathbf{w}^\top \mathbf{x}^{(i)}+b w⊤x(i)+b。无论我们使用什么手段来观察特征 X \mathbf{X} X 和标签 y \mathbf{y} y ,都可能会出现少量的观测误差。因此,即使确信特征与标签的潜在关系是线性的,我们也会加入一个噪声项来考虑观测误差带来的影响。
损失函数
在我们开始考虑如何用模型拟合(fit)数据之前,我们需要确定一个拟合程度的度量。损失函数 能够量化目标的实际值与预测 值之间的差距。通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。回归问题中最常用的损失函数是平方误差函数。当样本 i i i 的预测值为 y ^ ( i ) \hat{y}^{(i)} y^(i),其相应的真实标签为 y ( i ) y^{(i)} y(i) 时,平方误差可以定义为以下公式:
l ( i ) ( w , b ) = 1 2 ( y ^ ( i ) − y ( i ) ) 2 . l^{(i)}(\mathbf{w}, b) = \frac{1}{2} \left(\hat{y}^{(i)} - y^{(i)}\right)^2. l(i)(w,b)=21(y^(i)−y(i))2.
常数 1 2 \frac{1}{2} 21不会带来本质的差别,但这样在形式上稍微简单一些,表现为当我们对损失函数求导后常数系数为1。由于训练数据集并不受我们控制,所以经验误差只是关于模型参数的函数。
由于平方误差函数中的二次方项,估计值 y ^ ( i ) \hat{y}^{(i)} y^(i) 和观测值 y ( i ) y^{(i)} y(i) 之间较大的差异将贡献更大的损失。为了度量模型在整个数据集上的质量,我们需计算在训练集 n n n个样本上的损失均值(也等价于求和)。
L ( w , b ) = 1 n ∑ i = 1 n l ( i ) ( w , b ) = 1 n ∑ i = 1 n 1 2 ( w ⊤ x ( i ) + b − y ( 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. L(w,b)=n1i=1∑nl(i)(w,b)=n1i=1∑n21(w⊤x(i)+b−y(i))2.
在训练模型时,我们希望寻找一组参数 ( w ∗ , b ∗ \mathbf{w}^*, b^* w∗,b∗),这组参数能最小化在所有训练样本上的总损失。如下式:
w ∗ , b ∗ = * a r g m i n w , b L ( w , b ) . \mathbf{w}^*, b^* = \operatorname*{argmin}_{\mathbf{w}, b}\ L(\mathbf{w}, b). w∗,b∗=*argminw,b L(w,b).
解析解
线性回归刚好是一个很简单的优化问题。与我们将在本书中所讲到的其他大部分模型不同,线性回归的解可以用一个公式简单地表达出来,这类解叫作解析解(analytical solution)。首先,我们将偏置 b b b 合并到参数 w \mathbf{w} w 中。合并方法是在包含所有参数的矩阵中附加一列。我们的预测问题是最小化 ∥ y − X w ∥ 2 \|\mathbf{y} - \mathbf{X}\mathbf{w}\|^2 ∥y−Xw∥2。这在损失平面上只有一个临界点,这个临界点对应于整个区域的损失最小值。将损失关于 w \mathbf{w} w的导数设为0,得到解析解(闭合形式):
w ∗ = ( X ⊤ X ) − 1 X ⊤ y . \mathbf{w}^* = (\mathbf X^\top \mathbf X)^{-1}\mathbf X^\top \mathbf{y}. w∗=(X⊤X)−1X⊤y.
像线性回归这样的简单问题存在解析解,但并不是所有的问题都存在解析解。解析解可以进行很好的数学分析,但解析解的限制很严格,导致它无法应用在深度学习里。
小批量随机梯度下降
本书中我们用到一种名为梯度下降(gradient descent)的方法,这种方法几乎可以优化所有深度学习模型。它通过不断地在损失函数递减的方向上更新参数来降低误差。
梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值)关于模型参数的导数(在这里也可以称为梯度)。但实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本,这种变体叫做小批量随机梯度下降(minibatch stochastic gradient descent)。
在每次迭代中,我们首先随机抽样一个小批量 B \mathcal{B} B,它是由固定数量的训练样本组成的。然后,我们计算小批量的平均损失关于模型参数的导数(也可以称为梯度)。最后,我们将梯度乘以一个预先确定的正数 η \eta η,并从当前参数的值中减掉。
我们用下面的数学公式来表示这一更新过程( ∂ \partial ∂ 表示偏导数):
( w , b ) ← ( w , b ) − η ∣ B ∣ ∑ i ∈ B ∂ ( 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). (w,b)←(w,b)−∣B∣ηi∈B∑∂(w,b)l(i)(w,b).
总结一下,算法的步骤如下:
- 初始化模型参数的值,如随机初始化;
- 从数据集中随机抽取小批量样本且在负梯度的方向上更新参数,并不断迭代这一步骤。对于平方损失和仿射变换,我们可以明确地写成如下形式:
w ← w − η ∣ B ∣ ∑ i ∈ B ∂ w l ( i ) ( w , b ) = w − η ∣ B ∣ ∑ i ∈ B x ( i ) ( w ⊤ x ( i ) + b − y ( i ) ) , b ← b − η ∣ B ∣ ∑ i ∈ B ∂ b l ( i ) ( w , b ) = b − η ∣ B ∣ ∑ i ∈ B ( w ⊤ x ( i ) + b − y ( i ) ) . \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} wb←w−∣B∣ηi∈B∑∂wl(i)(w,b)=w−∣B∣ηi∈B∑x(i)(w⊤x(i)+b−y(i)),←b−∣B∣ηi∈B∑∂bl(i)(w,b)=b−∣B∣ηi∈B∑(w⊤x(i)+b−y(i)).
公式中的 w \mathbf{w} w 和 x \mathbf{x} x 都是向量。在这里,更优雅的向量表示法比系数表示法(如 w 1 , w 2 , … , w d w_1, w_2, \ldots, w_d w1,w2,…,wd)更具可读性。 ∣ B ∣ |\mathcal{B}| ∣B∣ 表示每个小批量中的样本数,这也称为批量大小(batch size)。 η \eta η 表示 学习率(learning rate)。在训练了预先确定的若干迭代次数后(或者直到满足某些其他停止条件后),我们记录下模型参数的估计值,表示为 w ^ , b ^ \hat{\mathbf{w}}, \hat{b} w^,b^。但是,即使我们的函数确实是线性的且无噪声,这些估计值也不会使损失函数真正地达到最小值。因为算法会使得损失向最小值缓慢收敛,但却不能在有限的步数内非常精确地达到最小值。
用学习到的模型进行预测
给定学习到的线性回归模型 w ^ ⊤ x + b ^ \hat{\mathbf{w}}^\top \mathbf{x} + \hat{b} w^⊤x+b^,现在我们可以通过给定的房屋面积 x 1 x_1 x1 和房龄 x 2 x_2 x2来估计一个未包含在训练数据中的新房屋价格。给定特征估计目标的过程通常称为预测(prediction)或推断(inference)。
矢量化加速
在训练我们的模型时,我们经常希望能够同时处理整个小批量的样本。为了实现这一点,需要(我们对计算进行矢量化,从而利用线性代数库,而不是在Python中编写开销高昂的for循环)。
import math
import time
import numpy as np
import torch
from d2l import torch as d2l
为了说明矢量化为什么如此重要,我们考虑(对向量相加的两种方法)。
我们实例化两个全1的1000维向量。在一种方法中,我们将使用Python的for循环遍历向量。在另一种方法中,我们将依赖对 + + + 的调用。
n = 10000
a = torch.ones(n)
b = torch.ones(n)
定义计时器,便于比较‘
class Timer: #@save"""记录多次运行时间。"""def __init__(self):self.times = []self.start()def start(self):"""启动计时器。"""self.tik = time.time()def stop(self):"""停止计时器并将时间记录在列表中。"""self.times.append(time.time() - self.tik)return self.times[-1]def avg(self):"""返回平均时间。"""return sum(self.times) / len(self.times)def sum(self):"""返回时间总和。"""return sum(self.times)def cumsum(self):"""返回累计时间。"""return np.array(self.times).cumsum().tolist()
基准测试
c = torch.zeros(n)
timer = Timer()
for i in range(n):c[i] = a[i] + b[i]
f'{timer.stop():.5f} sec'
或使用重载的 + + +
timer.start()
d = a + b
f'{timer.stop():.5f} sec'
方法二要快很多
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RXfFpsO6-1636466983091)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\1.jpg)]
正态分布与平方损失
正态分布和线性回归之间的关系很密切。
简单的说,若随机变量 x x x 具有均值 μ \mu μ 和方差 σ 2 \sigma^2 σ2(标准差 σ \sigma σ),其正态分布概率密度函数如下:
p ( x ) = 1 2 π σ 2 exp ( − 1 2 σ 2 ( x − μ ) 2 ) p(x) = \frac{1}{\sqrt{2 \pi \sigma^2}} \exp\left(-\frac{1}{2 \sigma^2} (x - \mu)^2\right) p(x)=2πσ21exp(−2σ21(x−μ)2)
下面进行计算
def normal(x, mu, sigma):p = 1 / math.sqrt(2 * math.pi * sigma**2)return p * np.exp(-0.5 / sigma**2 * (x - mu)**2)# 再次使用numpy进行可视化
x = np.arange(-7, 7, 0.01)# 均值和标准差对
params = [(0, 1), (0, 2), (3, 1)]
d2l.plot(x, [normal(x, mu, sigma) for mu, sigma in params], xlabel='x',ylabel='p(x)', figsize=(4.5, 2.5),legend=[f'mean {mu}, std {sigma}' for mu, sigma in params])
就像我们所看到的,改变均值会产生沿 x x x 轴的偏移,增加方差将会分散分布、降低其峰值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8nTkycvN-1636466983093)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\2.jpg)]
均方误差损失函数(简称均方损失)可以用于线性回归的一个原因是:我们假设了观测中包含噪声,其中噪声服从正态分布。噪声正态分布如下式:
y = w ⊤ x + b + ϵ where ϵ ∼ N ( 0 , σ 2 ) y = \mathbf{w}^\top \mathbf{x} + b + \epsilon \text{ where } \epsilon \sim \mathcal{N}(0, \sigma^2) y=w⊤x+b+ϵ where ϵ∼N(0,σ2)
因此,我们现在可以写出通过给定的 x \mathbf{x} x观测到特定 y y y的可能性(likelihood):
P ( y ∣ x ) = 1 2 π σ 2 exp ( − 1 2 σ 2 ( y − w ⊤ x − b ) 2 ) P(y \mid \mathbf{x}) = \frac{1}{\sqrt{2 \pi \sigma^2}} \exp\left(-\frac{1}{2 \sigma^2} (y - \mathbf{w}^\top \mathbf{x} - b)^2\right) P(y∣x)=2πσ21exp(−2σ21(y−w⊤x−b)2)
现在,根据最大似然估计法,参数 w \mathbf{w} w 和 b b b 的最优值是使整个数据集的可能性最大的值:
P ( y ∣ X ) = ∏ i = 1 n p ( y ( i ) ∣ x ( i ) ) P(\mathbf y \mid \mathbf X) = \prod_{i=1}^{n} p(y^{(i)}|\mathbf{x}^{(i)}) P(y∣X)=i=1∏np(y(i)∣x(i))
根据最大似然估计法选择的估计量称为最大似然估计量 。
虽然使许多指数函数的乘积最大化看起来很困难,但是我们可以在不改变目标的前提下,通过最大化似然对数来简化。
由于历史原因,优化通常是说最小化而不是最大化。我们可以改为 最小化负对数似然 − log P ( y ∣ X ) -\log P(\mathbf y \mid \mathbf X) −logP(y∣X)。由此可以得到的数学公式是:
− log P ( y ∣ X ) = ∑ i = 1 n 1 2 log ( 2 π σ 2 ) + 1 2 σ 2 ( y ( i ) − w ⊤ x ( i ) − b ) 2 -\log P(\mathbf y \mid \mathbf X) = \sum_{i=1}^n \frac{1}{2} \log(2 \pi \sigma^2) + \frac{1}{2 \sigma^2} \left(y^{(i)} - \mathbf{w}^\top \mathbf{x}^{(i)} - b\right)^2 −logP(y∣X)=i=1∑n21log(2πσ2)+2σ21(y(i)−w⊤x(i)−b)2
现在我们只需要假设 σ \sigma σ是某个固定常数就可以忽略第一项,因为第一项不依赖于 w \mathbf{w} w和 b b b。现在第二项除了常数 1 σ 2 \frac{1}{\sigma^2} σ21外,其余部分和前面介绍的平方误差损失是一样的。
幸运的是,上面式子的解并不依赖于 σ \sigma σ。因此,在高斯噪声的假设下,最小化均方误差等价于对线性模型的最大似然估计。
线性回归的从零开始实现
在这一节中,我们将只使用张量和自动求导。在之后的章节中,我们会充分利用深度学习框架的优势,介绍更简洁的实现方式。
import random
import torch
from d2l import torch as d2l
生成数据集
在下面的代码中,我们生成一个包含1000个样本的数据集,每个样本包含从标准正态分布中采样的2个特征。我们的合成数据集是一个矩阵 X ∈ R 1000 × 2 \mathbf{X}\in \mathbb{R}^{1000 \times 2} X∈R1000×2。
你可以将 ϵ \epsilon ϵ 视为捕获特征和标签时的潜在观测误差。在这里我们认为标准假设成立,即 ϵ \epsilon ϵ 服从均值为 0 0 0 的正态分布。
为了简化问题,我们将标准差设为 0.01 0.01 0.01 。下面的代码生成合成数据集。
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) + by += 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)print('features:', features[0], '\nlabel:', labels[0])
# 通过生成第二个特征 `features[:, 1]` 和 `labels` 的散点图,可以直观地观察到两者之间的线性关系。
d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(),labels.detach().numpy(), 1);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QR6WI788-1636466983095)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\3.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDvdR12m-1636466983097)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\4.jpg)]
读取数据集
回想一下,训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数,该函数能打乱数据集中的样本并以小批量方式获取数据。
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 = 10for X, y in data_iter(batch_size, features, labels):print(X, '\n', y)break
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aXE9GIwo-1636466983098)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\5.jpg)]
初始化模型参数
在我们开始用小批量随机梯度下降优化我们的模型参数之前,我们需要先有一些参数。
在下面的代码中,我们通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重,并将偏置初始化为0。
w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。
每次更新都需要计算损失函数关于模型参数的梯度。有了这个梯度,我们就可以向减小损失的方向更新每个参数。
因为手动计算梯度很枯燥而且容易出错,所以没有人会手动计算梯度。我们使用 :numref:sec_autograd
中引入的自动微分来计算梯度。
定义参数
接下来,我们必须定义模型,将模型的输入和参数同模型的输出关联起来。
回想一下,要计算线性模型的输出,我们只需计算输入特征 X \mathbf{X} X 和模型权重 w \mathbf{w} w的矩阵-向量乘法后加上偏置 b b b。注意,上面的 X w \mathbf{Xw} Xw 是一个向量,而 b b b是一个标量。广播机制使得当我们用一个向量加一个标量时,标量会被加到向量的每个分量上。
def linreg(X, w, b): #@save"""线性回归模型。"""return torch.matmul(X, w) + b
定义损失函数
因为要更新模型。需要计算损失函数的梯度,所以我们应该先定义损失函数。
def squared_loss(y_hat, y): #@save"""均方损失。"""return (y_hat - y.reshape(y_hat.shape))**2 / 2
定义优化算法
正如我们在 :numref:sec_linear_regression
中讨论的,线性回归有解析解。然而,这是一本关于深度学习的书,而不是一本关于线性回归的书。
由于这本书介绍的其他模型都没有解析解,下面我们将在这里介绍小批量随机梯度下降的工作示例。
在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。接下来,朝着减少损失的方向更新我们的参数。
下面的函数实现小批量随机梯度下降更新。该函数接受模型参数集合、学习速率和批量大小作为输入。每一步更新的大小由学习速率lr
决定。
因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size
)来归一化步长,这样步长大小就不会取决于我们对批量大小的选择。
def sgd(params, lr, batch_size): #@save"""小批量随机梯度下降。"""with torch.no_grad():for param in params:param -= lr * param.grad / batch_sizeparam.grad.zero_()
训练
概括一下,我们将执行以下循环:
-
初始化参数
-
重复,直到完成
计算梯度 g ← ∂ ( w , b ) 1 ∣ B ∣ ∑ i ∈ B l ( x ( i ) , y ( i ) , w , b ) \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) g←∂(w,b)∣B∣1∑i∈Bl(x(i),y(i),w,b)
更新参数 ( w , b ) ← ( w , b ) − η g (\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g} (w,b)←(w,b)−ηg
在每个迭代周期(epoch)中,我们使用 data_iter
函数遍历整个数据集,并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。这里的迭代周期个数num_epochs
和学习率lr
都是超参数,分别设为3和0.03。设置超参数很棘手,需要通过反复试验进行调整。
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_lossfor epoch in range(num_epochs):for X, y in data_iter(batch_size, features, labels):l = loss(net(X, w, b), y) # `X`和`y`的小批量损失# 因为`l`形状是(`batch_size`, 1),而不是一个标量。`l`中的所有元素被加到一起,# 并以此计算关于[`w`, `b`]的梯度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}')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LG204kOC-1636466983099)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\6.jpg)]
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rc3Paho4-1636466983100)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\7.jpg)]
线性回归的简洁实现
生成数据集
首先生成数据集
import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2ltrue_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2ltrue_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
读取数据集
我们可以调用框架中现有的API来读取数据。我们将 features
和 labels
作为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)
next(iter(data_iter))
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LNQzOfWY-1636466983100)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\8.jpg)]
这里我们使用 iter
构造Python迭代器,并使用 next
从迭代器中获取第一项。
定义模型
对于标准操作,我们可以使用框架的预定义好的层。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。我们首先定义一个模型变量net
,它是一个 Sequential
类的实例。 Sequential
类为串联在一起的多个层定义了一个容器。当给定输入数据, Sequential
实例将数据传入到第一层,然后将第一层的输出作为第二层的输入,依此类推。在下面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential
。但是由于以后几乎所有的模型都是多层的,在这里使用Sequential
会让你熟悉标准的流水线。这一单层被称为 全连接层(fully-connected layer),因为它的每一个输入都通过矩阵-向量乘法连接到它的每个输出。
在 PyTorch 中,全连接层在 Linear
类中定义。值得注意的是,我们将两个参数传递到 nn.Linear
中。第一个指定输入特征形状,第二个指定输出特征形状。
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
初始化模型参数
在使用net
之前,我们需要初始化模型参数。如在线性回归模型中的权重和偏置。
深度学习框架通常有预定义的方法来初始化参数。
在这里,我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样,偏置参数将初始化为零。
正如我们在构造 nn.Linear
时指定输入和输出尺寸一样。现在我们直接访问参数以设定初始值。我们通过 net[0]
选择网络中的第一个图层,然后使用 weight.data
和 bias.data
方法访问参数。然后使用替换方法 normal_
和 fill_
来重写参数值。
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
定义损失函数
计算均方误差使用的是MSELoss
类,也称为平方 L 2 L_2 L2 范数。默认情况下,它返回所有样本损失的平均值。
loss = nn.MSELoss()
定义优化算法
小批量随机梯度下降算法是一种优化神经网络的标准工具,PyTorch 在 optim
模块中实现了该算法的许多变种。当我们实例化 SGD
实例时,我们要指定优化的参数(可通过 net.parameters()
从我们的模型中获得)以及优化算法所需的超参数字典。小批量随机梯度下降只需要设置 lr
值,这里设置为 0.03。
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
训练
通过深度学习框架的高级 API 来实现我们的模型只需要相对较少的代码。我们不必单独分配参数、不必定义我们的损失函数,也不必手动实现小批量随机梯度下降。当我们需要更复杂的模型时,高级 API 的优势将大大增加。当我们有了所有的基本组件,训练过程代码与我们从零开始实现时所做的非常相似。
回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(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}')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CGJlgRQH-1636466983101)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\9.jpg)]
比较生成数据集的真实参数和通过有限数据训练获得的模型参数,要访问参数,我们首先从 net
访问所需的层,然后读取该层的权重和偏置。正如在从零开始实现中一样,我们估计得到的参数与生成数据的真实参数非常接近。
w = net[0].weight.data
print('w的估计误差:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
print('b的估计误差:', true_b - b)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-njOOEwi5-1636466983102)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\10.jpg)]
小结
- 我们可以使用 PyTorch 的高级 API更简洁地实现模型。
- 在 PyTorch 中,
data
模块提供了数据处理工具,nn
模块定义了大量的神经网络层和常见损失函数。 - 我们可以通过
_
结尾的方法将参数替换,从而初始化参数。
softmax回归
softmax 综述
事实上,我们经常对分类问题感兴趣:不是问“多少”,而是问“哪一个”:
通常,机器学习实践者用分类这个词来描述两个有微妙差别的问题:
- 我们只对样本的硬性类别感兴趣,即属于哪个类别;
- 我们希望得到软性类别,即得到属于每个类别的概率。这两者的界限往往很模糊。其中的一个原因是,即使我们只关心硬类别,我们仍然使用软类别的模型。
网络结构
为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。
为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数(affine function)。
每个输出对应于它自己的仿射函数。
在我们的例子中,由于我们有4个特征和3个可能的输出类别,我们将需要12个标量来表示权重(带下标的 w w w),3个标量来表示偏置(带下标的 b b b)。
下面我们为每个输入计算三个未归一化的预测(logits): o 1 o_1 o1、 o 2 o_2 o2和 o 3 o_3 o3。
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 . \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} o1o2o3=x1w11+x2w12+x3w13+x4w14+b1,=x1w21+x2w22+x3w23+x4w24+b2,=x1w31+x2w32+x3w33+x4w34+b3.
与线性回归一样,softmax回归也是一个单层神经网络。由于计算每个输出 o 1 o_1 o1、 o 2 o_2 o2和 o 3 o_3 o3取决于所有输入 x 1 x_1 x1、 x 2 x_2 x2、 x 3 x_3 x3和 x 4 x_4 x4,所以softmax回归的输出层也是全连接层。
为了更简洁地表达模型,我们仍然使用线性代数符号。
通过向量形式表达为 o = W x + b \mathbf{o} = \mathbf{W} \mathbf{x} + \mathbf{b} o=Wx+b,这是一种更适合数学和编写代码的形式。我们已经将所有权重放到一个 3 × 4 3 \times 4 3×4 矩阵中。对于给定数据样本的特征 x \mathbf{x} x,我们的输出是由权重与输入特征进行矩阵-向量乘法再加上偏置 b \mathbf{b} b得到的。
全连接层的参数开销
正如我们将在后续章节中看到的,在深度学习中,全连接层无处不在。
然而,顾名思义,全连接层是“完全”连接的,可能有很多可学习的参数。
具体来说,对于任何具有 d d d个输入和 q q q个输出的全连接层,参数开销为 O ( d q ) \mathcal{O}(dq) O(dq),在实践中可能高得令人望而却步。
幸运的是,将 d d d个输入转换为 q q q个输出的成本可以减少到 O ( d q n ) \mathcal{O}(\frac{dq}{n}) O(ndq),其中超参数 n n n可以由我们灵活指定,以在实际应用中平衡参数节约和模型。
softmax运算
在这里要采取的主要方法是将模型的输出视作为概率。我们将优化参数以最大化观测数据的概率。为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。
我们希望模型的输出 y ^ j \hat{y}_j y^j 可以视为属于类 j j j 的概率。然后我们可以选择具有最大输出值的类别 * a r g m a x j y j \operatorname*{argmax}_j y_j *argmaxjyj作为我们的预测。例如,如果 y ^ 1 \hat{y}_1 y^1、 y ^ 2 \hat{y}_2 y^2 和 y ^ 3 \hat{y}_3 y^3 分别为 0.1、0.8 和 0.1,那么我们预测的类别是2。
为了将未归一化的预测变换为非负并且总和为1,同时要求模型保持可导。我们首先对每个未归一化的预测求幂,这样可以确保输出非负。为了确保最终输出的总和为1,我们再对每个求幂后的结果除以它们的总和。如下式:
y ^ = s o f t m a x ( o ) 其中 y ^ j = exp ( o j ) ∑ k exp ( o k ) \hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{其中}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)} y^=softmax(o)其中y^j=∑kexp(ok)exp(oj)
容易看出对于所有的 j j j 总有 0 ≤ y ^ j ≤ 1 0 \leq \hat{y}_j \leq 1 0≤y^j≤1。因此, y ^ \hat{\mathbf{y}} y^ 可以视为一个正确的概率分布。softmax 运算不会改变未归一化的预测 o \mathbf{o} o 之间的顺序,只会确定分配给每个类别的概率。因此,在预测过程中,我们仍然可以用下式来选择最有可能的类别。
* a r g m a x j y ^ j = * a r g m a x j o j \operatorname*{argmax}_j \hat y_j = \operatorname*{argmax}_j o_j *argmaxjy^j=*argmaxjoj
尽管 softmax 是一个非线性函数,但 softmax 回归的输出仍然由输入特征的仿射变换决定。因此,softmax 回归是一个线性模型。
小批量样本的矢量化
为了提高计算效率并且充分利用GPU,我们通常会针对小批量数据执行矢量计算。假设我们读取了一个批量的样本 X \mathbf{X} X ,其中特征维度(输入数量)为 d d d,批量大小为 n n n。此外,假设我们在输出中有 q q q 个类别。那么小批量特征为 X ∈ R n × d \mathbf{X} \in \mathbb{R}^{n \times d} X∈Rn×d ,权重为 W ∈ R d × q \mathbf{W} \in \mathbb{R}^{d \times q} W∈Rd×q,偏置为 b ∈ R 1 × q \mathbf{b} \in \mathbb{R}^{1\times q} b∈R1×q。softmax回归的矢量计算表达式为:
O = X W + b , Y ^ = s o f t m a x ( O ) . \begin{aligned} \mathbf{O} &= \mathbf{X} \mathbf{W} + \mathbf{b}, \\ \hat{\mathbf{Y}} & = \mathrm{softmax}(\mathbf{O}). \end{aligned} OY^=XW+b,=softmax(O).
相对于一次处理一个样本,小批量样本的矢量化加快了 X 和 W \mathbf{X}和\mathbf{W} X和W 的矩阵-向量乘法。由于 X \mathbf{X} X 中的每一行代表一个数据样本,所以softmax运算可以按行(rowwise)执行:对于 O \mathbf{O} O的每一行,我们先对所有项进行幂运算,然后通过求和对它们进行标准化。
在 X W + b \mathbf{X} \mathbf{W} + \mathbf{b} XW+b 的求和会使用广播,小批量的未归一化预测 O \mathbf{O} O 和输出概率 Y ^ \hat{\mathbf{Y}} Y^ 都是形状为 n × q n \times q n×q 的矩阵。
损失函数
接下来,我们需要一个损失函数来度量预测概率的效果。我们将依赖最大似然估计,这与我们在为线性回归(中的均方误差目标提供概率证明时遇到的概念完全相同。
对数似然
softmax函数给出了一个向量 y ^ \hat{\mathbf{y}} y^,我们可以将其视为给定任意输入 x \mathbf{x} x的每个类的估计条件概率。例如, y ^ 1 \hat{y}_1 y^1 = P ( y = 猫 ∣ x ) P(y=\text{猫} \mid \mathbf{x}) P(y=猫∣x)。假设整个数据集 { X , Y } \{\mathbf{X}, \mathbf{Y}\} {X,Y} 具有 n n n 个样本,其中索引 i i i 的样本由特征向量 x ( i ) \mathbf{x}^{(i)} x(i) 和独热标签向量 y ( i ) \mathbf{y}^{(i)} y(i) 组成。我们可以将估计值与实际值进行比较:
P ( Y ∣ X ) = ∏ i = 1 n P ( y ( i ) ∣ x ( i ) ) . P(\mathbf{Y} \mid \mathbf{X}) = \prod_{i=1}^n P(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}). P(Y∣X)=i=1∏nP(y(i)∣x(i)).
根据最大似然估计,我们最大化 P ( Y ∣ X ) P(\mathbf{Y} \mid \mathbf{X}) P(Y∣X),相当于最小化负对数似然:
− log P ( Y ∣ X ) = ∑ i = 1 n − log P ( y ( i ) ∣ x ( i ) ) = ∑ i = 1 n l ( 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)}), −logP(Y∣X)=i=1∑n−logP(y(i)∣x(i))=i=1∑nl(y(i),y^(i)),
其中,对于任何标签 y \mathbf{y} y 和模型预测 y ^ \hat{\mathbf{y}} y^,损失函数为:
l ( y , y ^ ) = − ∑ j = 1 q y j log y ^ j . l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j. l(y,y^)=−j=1∑qyjlogy^j.
在本节稍后的内容会讲到, :eqref:eq_l_cross_entropy
中的损失函数通常被称为 交叉熵损失(cross-entropy loss)。由于 y \mathbf{y} y 是一个长度为 q q q 的独热编码向量,所以除了一个项以外的所有项 j j j 都消失了。由于所有 y ^ j \hat{y}_j y^j 都是预测的概率,所以它们的对数永远不会大于 0 0 0。
因此,如果正确地预测实际标签,即,如果实际标签 P ( y ∣ x ) = 1 P(\mathbf{y} \mid \mathbf{x})=1 P(y∣x)=1,则损失函数不能进一步最小化。
注意,这往往是不可能的。例如,数据集中可能存在标签噪声(某些样本可能被误标),或输入特征没有足够的信息来完美地对每一个样本分类。
softmax及其导数
由于softmax和相关的损失函数很常见,因此值得我们更好地理解它的计算方式。将eq_softmax_y_and_o
代入损失 :eqref:eq_l_cross_entropy
中。利用softmax的定义,我们得到:
l ( y , y ^ ) = − ∑ j = 1 q y j log exp ( o j ) ∑ k = 1 q exp ( o k ) = ∑ j = 1 q y j log ∑ k = 1 q exp ( o k ) − ∑ j = 1 q y j o j = log ∑ k = 1 q exp ( o k ) − ∑ j = 1 q y j o j . \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} l(y,y^)=−j=1∑qyjlog∑k=1qexp(ok)exp(oj)=j=1∑qyjlogk=1∑qexp(ok)−j=1∑qyjoj=logk=1∑qexp(ok)−j=1∑qyjoj.
为了更好地理解发生了什么,考虑相对于任何未归一化的预测 o j o_j oj 的导数。我们得到:
∂ o j l ( y , y ^ ) = exp ( o j ) ∑ k = 1 q exp ( o k ) − y j = s o f t m a x ( o ) j − y j . \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. ∂ojl(y,y^)=∑k=1qexp(ok)exp(oj)−yj=softmax(o)j−yj.
换句话说,导数是我们模型分配的概率(由softmax得到)与实际发生的情况(由独热标签向量表示)之间的差异。从这个意义上讲,与我们在回归中看到的非常相似,其中梯度是观测值 y y y和估计值 y ^ \hat{y} y^之间的差异。
模型预测和评估
在训练softmax回归模型后,给出任何样本特征,我们可以预测每个输出类别的概率。通常我们使用预测概率最高的类别作为输出类别。如果预测与实际类别(标签)一致,则预测是正确的。在接下来的实验中,我们将使用 准确率 来评估模型的性能。准确率等于正确预测数与预测的总数之间的比率。
- softmax运算获取一个向量并将其映射为概率。
- softmax回归适用于分类问题。它使用了softmax运算中输出类别的概率分布。
- 交叉熵是一个衡量两个概率分布之间差异的很好的度量。它测量给定模型编码数据所需的比特数。
softmax回归的从零开始实现
我们使用刚刚在 sec_fashion_mnist
中引入的 Fashion-MNIST 数据集,并设置数据迭代器的批量大小为 256 256 256。
import torch
from IPython import display
from d2l import torch as d2lbatch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
初始化模型参数
和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。原始数据集中的每个样本都是 28 × 28 28 \times 28 28×28 的图像。在本节中,我们将展平每个图像,把它们看作长度为784的向量。
在softmax回归中,我们的输出与类别一样多。。因此,权重将构成一个 784 × 10 784 \times 10 784×10 的矩阵,偏置将构成一个 1 × 10 1 \times 10 1×10 的行向量。与线性回归一样,我们将使用正态分布初始化我们的权重 W
,偏置初始化为0。
num_inputs = 784
num_outputs = 10W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
定义softmax操作
在实现softmax回归模型之前,让我们简要地回顾一下sum
运算符如何沿着张量中的特定维度工作。
X = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
X.sum(0, keepdim=True), X.sum(1, keepdim=True)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-44PlA1tN-1636466983103)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\11.jpg)]
softmax 由三个步骤组成:
- 对每个项求幂(使用
exp
); - 对每一行求和(小批量中每个样本是一行),得到每个样本的归一化常数;
- 将每一行除以其归一化常数,确保结果的和为1。
s o f t m a x ( X ) i j = exp ( X i j ) ∑ k exp ( X i k ) . \mathrm{softmax}(\mathbf{X})_{ij} = \frac{\exp(\mathbf{X}_{ij})}{\sum_k \exp(\mathbf{X}_{ik})}. softmax(X)ij=∑kexp(Xik)exp(Xij).
def softmax(X):X_exp = torch.exp(X)partition = X_exp.sum(1, keepdim=True)return X_exp / partition # 这里应用了广播机制
正如你所看到的,对于任何随机输入,我们将每个元素变成一个非负数。此外,依据概率原理,每行总和为1。
X = torch.normal(0, 1, (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vI06H2ho-1636466983103)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\12.jpg)]
定义模型
在将数据传递到我们的模型之前,我们使用 reshape
函数将每张原始图像展平为向量。
def net(X):return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
定义损失函数
接下来,我们需要实现 sec_softmax
中引入的交叉熵损失函数。这可能是深度学习中最常见的损失函数,因为目前分类问题的数量远远超过回归问题。
y = torch.tensor([0, 2])
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y_hat[[0, 1], y]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yv6bpASA-1636466983104)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\13.jpg)]
交叉熵损失函数
def cross_entropy(y_hat, y):return -torch.log(y_hat[range(len(y_hat)), y])cross_entropy(y_hat, y)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FFGnMb5V-1636466983104)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\14.jpg)]
分类准确率
给定预测概率分布 y_hat
,当我们必须输出硬预测(hard prediction)时,我们通常选择预测概率最高的类。当预测与标签分类 y
一致时,它们是正确的。分类准确率即正确预测数量与总预测数量之比。虽然直接优化准确率可能很困难(因为准确率的计算不可导),但准确率通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总是会报告它。
为了计算准确率,我们执行以下操作。首先,如果 y_hat
是矩阵,那么假定第二个维度存储每个类的预测分数。我们使用 argmax
获得每行中最大元素的索引来获得预测类别。然后我们将预测类别与真实 y
元素进行比较。由于等式运算符 ==
对数据类型很敏感,因此我们将 y_hat
的数据类型转换为与 y
的数据类型一致。结果是一个包含 0(错)和 1(对)的张量。进行求和会得到正确预测的数量。
def accuracy(y_hat, y): #@save"""计算预测正确的数量。"""if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:y_hat = y_hat.argmax(axis=1)cmp = y_hat.type(y.dtype) == yreturn float(cmp.type(y.dtype).sum())
我们将继续使用之前定义的变量 y_hat
和 y
分别作为预测的概率分布和标签。我们可以看到,第一个样本的预测类别是2(该行的最大元素为0.6,索引为2),这与实际标签0不一致。第二个样本的预测类别是2(该行的最大元素为0.5,索引为 2),这与实际标签2一致。因此,这两个样本的分类准确率率为0.5。
accuracy(y_hat, y) / len(y)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zPKmeb2c-1636466983105)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\15.jpg)]
def evaluate_accuracy(net, data_iter): #@save"""计算在指定数据集上模型的精度。"""if isinstance(net, torch.nn.Module):net.eval() # 将模型设置为评估模式metric = Accumulator(2) # 正确预测数、预测总数for X, y in data_iter:metric.add(accuracy(net(X), y), y.numel())return metric[0] / metric[1]
这里 Accumulator
是一个实用程序类,用于对多个变量进行累加。
在上面的 evaluate_accuracy
函数中,我们在 Accumulator
实例中创建了 2 个变量,用于分别存储正确预测的数量和预测的总数量。当我们遍历数据集时,两者都将随着时间的推移而累加。
class Accumulator: #@save"""在`n`个变量上累加。"""def __init__(self, n):self.data = [0.0] * ndef add(self, *args):self.data = [a + float(b) for a, b in zip(self.data, args)]def reset(self):self.data = [0.0] * len(self.data)def __getitem__(self, idx):return self.data[idx]
由于我们使用随机权重初始化 net
模型,因此该模型的准确率应接近于随机猜测。例如在有10个类别情况下的准确率为0.1。
evaluate_accuracy(net, test_iter)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uUXlEuK8-1636466983105)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\16.jpg)]
训练
如果你看过 sec_linear_scratch
中的线性回归实现,softmax回归的训练过程代码应该看起来非常熟悉。在这里,我们重构训练过程的实现以使其可重复使用。首先,我们定义一个函数来训练一个迭代周期。请注意,updater
是更新模型参数的常用函数,它接受批量大小作为参数。它可以是封装的d2l.sgd
函数,也可以是框架的内置优化函数。
def train_epoch_ch3(net, train_iter, loss, updater): #@save# 将模型设置为训练模式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.backward()updater.step()metric.add(float(l) * len(y), accuracy(y_hat, y),y.size().numel())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]
接下来我们实现一个训练函数,它会在train_iter
访问到的训练数据集上训练一个模型net
。该训练函数将会运行多个迭代周期(由num_epochs
指定)。在每个迭代周期结束时,利用 test_iter
访问到的测试数据集对模型进行评估。我们将利用 Animator
类来可视化训练进度。
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #@save"""训练模型(定义见第3章)。"""animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],legend=['train loss', 'train acc', 'test acc'])for epoch in range(num_epochs):train_metrics = train_epoch_ch3(net, train_iter, loss, updater)test_acc = evaluate_accuracy(net, test_iter)animator.add(epoch + 1, train_metrics + (test_acc,))train_loss, train_acc = train_metricsassert train_loss < 0.5, train_lossassert train_acc <= 1 and train_acc > 0.7, train_accassert test_acc <= 1 and test_acc > 0.7, test_acc
作为一个从零开始的实现,我们使用 :numref:sec_linear_scratch
中定义的小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1。
lr = 0.1
def updater(batch_size):return d2l.sgd([W, b], lr, batch_size)
现在,我们训练模型10个迭代周期。请注意,迭代周期(num_epochs
)和学习率(lr
)都是可调节的超参数。通过更改它们的值,我们可以提高模型的分类准确率。
num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n3zTaKxX-1636466983106)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\17.jpg)]
预测
def predict_ch3(net, test_iter, n=6): #@save"""预测标签(定义见第3章)。"""for X, y in test_iter:breaktrues = 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)
小结
- 借助 softmax 回归,我们可以训练多分类的模型。
- softmax 回归的训练循环与线性回归中的训练循环非常相似:读取数据、定义模型和损失函数,然后使用优化算法训练模型。正如你很快就会发现的那样,大多数常见的深度学习模型都有类似的训练过程。
softmax 回归的简洁实现
通过深度学习框架的高级API也能更方便地实现分类模型。
import torch
from torch import nn
from d2l import torch as d2lbatch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
初始化模型参数
如我们在 :numref:sec_softmax
所述,[softmax 回归的输出层是一个全连接层]。因此,为了实现我们的模型,我们只需在 Sequential
中添加一个带有10个输出的全连接层。同样,在这里,Sequential
并不是必要的,但我们可能会形成这种习惯。因为在实现深度模型时,Sequential
将无处不在。我们仍然以均值0和标准差0.01随机初始化权重。
PyTorch不会隐式地调整输入的形状。因此我们在线性层前定义了展平层(flatten),来调整网络输入的形状.
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))def init_weights(m):if type(m) == nn.Linear:nn.init.normal_(m.weight, std=0.01)net.apply(init_weights);
重新审视 Softmax 的实现
回想一下,softmax 函数 y ^ j = exp ( o j ) ∑ k exp ( o k ) \hat y_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)} y^j=∑kexp(ok)exp(oj),其中 y ^ j \hat y_j y^j是预测的概率分布。 o j o_j oj是未归一化的预测 o \mathbf{o} o的第 j j j个元素。如果 o k o_k ok中的一些数值非常大,那么 exp ( o k ) \exp(o_k) exp(ok) 可能大于数据类型容许的最大数字(即 上溢(overflow))。这将使分母或分子变为inf
(无穷大),我们最后遇到的是0、inf
或 nan
(不是数字)的 y ^ j \hat y_j y^j。在这些情况下,我们不能得到一个明确定义的交叉熵的返回值。
解决这个问题的一个技巧是,在继续softmax计算之前,先从所有 o k o_k ok中减去 max ( o k ) \max(o_k) max(ok)。你可以证明每个 o k o_k ok 按常数进行的移动不会改变softmax的返回值。在减法和归一化步骤之后,可能有些 o j o_j oj 具有较大的负值。由于精度受限, exp ( o j ) \exp(o_j) exp(oj) 将有接近零的值,即 下溢(underflow)。这些值可能会四舍五入为零,使 y ^ j \hat y_j y^j 为零,并且使得 log ( y ^ j ) \log(\hat y_j) log(y^j) 的值为 -inf
。反向传播几步后,我们可能会发现自己面对一屏幕可怕的nan
结果。
尽管我们要计算指数函数,但我们最终在计算交叉熵损失时会取它们的对数。
通过将softmax和交叉熵结合在一起,可以避免反向传播过程中可能会困扰我们的数值稳定性问题。如下面的等式所示,我们避免计算 exp ( o j ) \exp(o_j) exp(oj),而可以直接使用 o j o_j oj。因为 log ( exp ( ⋅ ) ) \log(\exp(\cdot)) log(exp(⋅))被抵消了。
log ( y ^ j ) = log ( exp ( o j ) ∑ k exp ( o k ) ) = log ( exp ( o j ) ) − log ( ∑ k exp ( o k ) ) = o j − log ( ∑ k exp ( o k ) ) . \begin{aligned} \log{(\hat y_j)} & = \log\left( \frac{\exp(o_j)}{\sum_k \exp(o_k)}\right) \\ & = \log{(\exp(o_j))}-\log{\left( \sum_k \exp(o_k) \right)} \\ & = o_j -\log{\left( \sum_k \exp(o_k) \right)}. \end{aligned} log(y^j)=log(∑kexp(ok)exp(oj))=log(exp(oj))−log(k∑exp(ok))=oj−log(k∑exp(ok)).
我们也希望保留传统的softmax函数,以备我们需要评估通过模型输出的概率。
loss = nn.CrossEntropyLoss()
优化算法
在这里,我们使用学习率为0.1的小批量随机梯度下降作为优化算法。这与我们在线性回归例子中的相同,这说明了优化器的普适性。
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
训练
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
和以前一样,这个算法收敛到一个相当高的精度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LB0Yb4np-1636466983106)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\18.jpg)]
小结
- 使用高级 API,我们可以更简洁地实现 softmax 回归。
- 从计算的角度来看,实现softmax回归比较复杂。在许多情况下,深度学习框架在这些著名的技巧之外采取了额外的预防措施,来确保数值的稳定性。这使我们避免了在实践中从零开始编写模型时可能遇到的陷阱。