线性回归与 softmax
线性回归与 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
)
=
[
x
1
(
i
)
,
x
2
(
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
p
r
i
c
e
=
w
a
r
e
a
⋅
a
r
e
a
+
w
a
g
e
⋅
a
g
e
+
b
w
a
r
e
a
w_{\mathrm{area}}
w
a
r
e
a
和
w
a
g
e
w_{\mathrm{age}}
w
a
g
e
称为
权重
(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
^
=
w
1
x
1
+
.
.
.
+
w
d
x
d
+
b
.
将所有特征放到向量
x
∈
R
d
\mathbf{x} \in \mathbb{R}^d
x
∈
R
d
中,并将所有权重放到向量
w
∈
R
d
\mathbf{w} \in \mathbb{R}^d
w
∈
R
d
中,我们可以用点积形式来简洁地表达模型:
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
∈
R
n
×
d
可以很方便地引用我们整个数据集的
n
n
n
个样本。其中,
X
\mathbf{X}
X
的每一行是一个样本,每一列是一种特征。
对于特征集合
X
\mathbf{X}
X
,预测值
y
^
∈
R
n
\hat{\mathbf{y}} \in \mathbb{R}^n
y
^
∈
R
n
可以通过矩阵-向量乘法表示为:
y
^
=
X
w
+
b
{\hat{\mathbf{y}}} = \mathbf{X} \mathbf{w} + b
y
^
=
X
w
+
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
)
=
2
1
(
y
^
(
i
)
−
y
(
i
)
)
2
.
常数
1
2
\frac{1}{2}
2
1
不会带来本质的差别,但这样在形式上稍微简单一些,表现为当我们对损失函数求导后常数系数为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
)
=
n
1
i
=
1
∑
n
l
(
i
)
(
w
,
b
)
=
n
1
i
=
1
∑
n
2
1
(
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
∗
=
*
a
r
g
m
i
n
w
,
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
−
X
w
∥
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
)
−
1
X
⊤
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}
w
b
←
w
−
∣
B
∣
η
i
∈
B
∑
∂
w
l
(
i
)
(
w
,
b
)
=
w
−
∣
B
∣
η
i
∈
B
∑
x
(
i
)
(
w
⊤
x
(
i
)
+
b
−
y
(
i
)
)
,
←
b
−
∣
B
∣
η
i
∈
B
∑
∂
b
l
(
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
w
1
,
w
2
,
…
,
w
d
)更具可读性。
∣
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
x
1
和房龄
x
2
x_2
x
2
来估计一个未包含在训练数据中的新房屋价格。给定特征估计目标的过程通常称为
预测
(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
π
σ
2
1
exp
(
−
2
σ
2
1
(
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
π
σ
2
1
exp
(
−
2
σ
2
1
(
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
∏
n
p
(
y
(
i
)
∣
x
(
i
)
)
根据最大似然估计法选择的估计量称为
最大似然估计量
。
虽然使许多指数函数的乘积最大化看起来很困难,但是我们可以在不改变目标的前提下,通过最大化似然对数来简化。
由于历史原因,优化通常是说最小化而不是最大化。我们可以改为
最小化负对数似然
−
log
P
(
y
∣
X
)
-\log P(\mathbf y \mid \mathbf X)
−
lo
g
P
(
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
−
lo
g
P
(
y
∣
X
)
=
i
=
1
∑
n
2
1
lo
g
(
2
π
σ
2
)
+
2
σ
2
1
(
y
(
i
)
−
w
⊤
x
(
i
)
−
b
)
2
现在我们只需要假设
σ
\sigma
σ
是某个固定常数就可以忽略第一项,因为第一项不依赖于
w
\mathbf{w}
w
和
b
b
b
。现在第二项除了常数
1
σ
2
\frac{1}{\sigma^2}
σ
2
1
外,其余部分和前面介绍的平方误差损失是一样的。
幸运的是,上面式子的解并不依赖于
σ
\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
∈
R
1
0
0
0
×
2
。
你可以将
ϵ
\epsilon
ϵ
视为捕获特征和标签时的潜在观测误差。在这里我们认为标准假设成立,即
ϵ
\epsilon
ϵ
服从均值为
0
0
0
的正态分布。
为了简化问题,我们将标准差设为
0.01
0.01
0
.
0
1
。下面的代码生成合成数据集。
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)
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 = 10
for 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}
X
w
是一个向量,而
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_size
param.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
∈
B
l
(
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_loss
for 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 d2l
true_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 d2l
true_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
L
2
范数。默认情况下,它返回所有样本损失的平均值。
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
o
1
、
o
2
o_2
o
2
和
o
3
o_3
o
3
。
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}
o
1
o
2
o
3
=
x
1
w
1
1
+
x
2
w
1
2
+
x
3
w
1
3
+
x
4
w
1
4
+
b
1
,
=
x
1
w
2
1
+
x
2
w
2
2
+
x
3
w
2
3
+
x
4
w
2
4
+
b
2
,
=
x
1
w
3
1
+
x
2
w
3
2
+
x
3
w
3
3
+
x
4
w
3
4
+
b
3
.
与线性回归一样,softmax回归也是一个单层神经网络。由于计算每个输出
o
1
o_1
o
1
、
o
2
o_2
o
2
和
o
3
o_3
o
3
取决于所有输入
x
1
x_1
x
1
、
x
2
x_2
x
2
、
x
3
x_3
x
3
和
x
4
x_4
x
4
,所以softmax回归的输出层也是全连接层。
为了更简洁地表达模型,我们仍然使用线性代数符号。
通过向量形式表达为
o
=
W
x
+
b
\mathbf{o} = \mathbf{W} \mathbf{x} + \mathbf{b}
o
=
W
x
+
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
(
d
q
)
,在实践中可能高得令人望而却步。
幸运的是,将
d
d
d
个输入转换为
q
q
q
个输出的成本可以减少到
O
(
d
q
n
)
\mathcal{O}(\frac{dq}{n})
O
(
n
d
q
)
,其中超参数
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
*
a
r
g
m
a
x
j
y
j
作为我们的预测。例如,如果
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
^
=
s
o
f
t
m
a
x
(
o
)
其中
y
^
j
=
∑
k
exp
(
o
k
)
exp
(
o
j
)
容易看出对于所有的
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
*
a
r
g
m
a
x
j
y
^
j
=
*
a
r
g
m
a
x
j
o
j
尽管 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
∈
R
n
×
d
,权重为
W
∈
R
d
×
q
\mathbf{W} \in \mathbb{R}^{d \times q}
W
∈
R
d
×
q
,偏置为
b
∈
R
1
×
q
\mathbf{b} \in \mathbb{R}^{1\times q}
b
∈
R
1
×
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}
O
Y
^
=
X
W
+
b
,
=
s
o
f
t
m
a
x
(
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}
X
W
+
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
∏
n
P
(
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)}),
−
lo
g
P
(
Y
∣
X
)
=
i
=
1
∑
n
−
lo
g
P
(
y
(
i
)
∣
x
(
i
)
)
=
i
=
1
∑
n
l
(
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
∑
q
y
j
lo
g
y
^
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
∑
q
y
j
lo
g
∑
k
=
1
q
exp
(
o
k
)
exp
(
o
j
)
=
j
=
1
∑
q
y
j
lo
g
k
=
1
∑
q
exp
(
o
k
)
−
j
=
1
∑
q
y
j
o
j
=
lo
g
k
=
1
∑
q
exp
(
o
k
)
−
j
=
1
∑
q
y
j
o
j
.
为了更好地理解发生了什么,考虑相对于任何未归一化的预测
o
j
o_j
o
j
的导数。我们得到:
∂
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.
∂
o
j
l
(
y
,
y
^
)
=
∑
k
=
1
q
exp
(
o
k
)
exp
(
o
j
)
−
y
j
=
s
o
f
t
m
a
x
(
o
)
j
−
y
j
.
换句话说,导数是我们模型分配的概率(由softmax得到)与实际发生的情况(由独热标签向量表示)之间的差异。从这个意义上讲,与我们在回归中看到的非常相似,其中梯度是观测值
y
y
y
和估计值
y
^
\hat{y}
y
^
之间的差异。
模型预测和评估
在训练softmax回归模型后,给出任何样本特征,我们可以预测每个输出类别的概率。通常我们使用预测概率最高的类别作为输出类别。如果预测与实际类别(标签)一致,则预测是正确的。在接下来的实验中,我们将使用
准确率
来评估模型的性能。准确率等于正确预测数与预测的总数之间的比率。
- softmax运算获取一个向量并将其映射为概率。
- softmax回归适用于分类问题。它使用了softmax运算中输出类别的概率分布。
- 交叉熵是一个衡量两个概率分布之间差异的很好的度量。它测量给定模型编码数据所需的比特数。
softmax回归的从零开始实现
我们使用刚刚在
sec_fashion_mnist
中引入的 Fashion-MNIST 数据集,并设置数据迭代器的批量大小为
256
256
2
5
6
。
import torch
from IPython import display
from d2l import torch as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
初始化模型参数
和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。原始数据集中的每个样本都是
28
×
28
28 \times 28
2
8
×
2
8
的图像。在本节中,我们将展平每个图像,把它们看作长度为784的向量。
在softmax回归中,我们的输出与类别一样多。。因此,权重将构成一个
784
×
10
784 \times 10
7
8
4
×
1
0
的矩阵,偏置将构成一个
1
×
10
1 \times 10
1
×
1
0
的行向量。与线性回归一样,我们将使用正态分布初始化我们的权重
W
,偏置初始化为0。
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回归模型之前,让我们简要地回顾一下
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})}.
s
o
f
t
m
a
x
(
X
)
i
j
=
∑
k
exp
(
X
i
k
)
exp
(
X
i
j
)
.
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) == y
return 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] * n
def 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_metrics
assert train_loss < 0.5, train_loss
assert train_acc <= 1 and train_acc > 0.7, train_acc
assert 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:
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)
小结
- 借助 softmax 回归,我们可以训练多分类的模型。
- softmax 回归的训练循环与线性回归中的训练循环非常相似:读取数据、定义模型和损失函数,然后使用优化算法训练模型。正如你很快就会发现的那样,大多数常见的深度学习模型都有类似的训练过程。
softmax 回归的简洁实现
通过深度学习框架的高级API也能更方便地实现分类模型。
import torch
from torch import nn
from d2l import torch as d2l
batch_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
=
∑
k
exp
(
o
k
)
exp
(
o
j
)
,其中
y
^
j
\hat y_j
y
^
j
是预测的概率分布。
o
j
o_j
o
j
是未归一化的预测
o
\mathbf{o}
o
的第
j
j
j
个元素。如果
o
k
o_k
o
k
中的一些数值非常大,那么
exp
(
o
k
)
\exp(o_k)
exp
(
o
k
)
可能大于数据类型容许的最大数字(即
上溢
(overflow))。这将使分母或分子变为
inf
(无穷大),我们最后遇到的是0、
inf
或
nan
(不是数字)的
y
^
j
\hat y_j
y
^
j
。在这些情况下,我们不能得到一个明确定义的交叉熵的返回值。
解决这个问题的一个技巧是,在继续softmax计算之前,先从所有
o
k
o_k
o
k
中减去
max
(
o
k
)
\max(o_k)
max
(
o
k
)
。你可以证明每个
o
k
o_k
o
k
按常数进行的移动不会改变softmax的返回值。在减法和归一化步骤之后,可能有些
o
j
o_j
o
j
具有较大的负值。由于精度受限,
exp
(
o
j
)
\exp(o_j)
exp
(
o
j
)
将有接近零的值,即
下溢
(underflow)。这些值可能会四舍五入为零,使
y
^
j
\hat y_j
y
^
j
为零,并且使得
log
(
y
^
j
)
\log(\hat y_j)
lo
g
(
y
^
j
)
的值为
-inf
。反向传播几步后,我们可能会发现自己面对一屏幕可怕的
nan
结果。
尽管我们要计算指数函数,但我们最终在计算交叉熵损失时会取它们的对数。
通过将softmax和交叉熵结合在一起,可以避免反向传播过程中可能会困扰我们的数值稳定性问题。如下面的等式所示,我们避免计算
exp
(
o
j
)
\exp(o_j)
exp
(
o
j
)
,而可以直接使用
o
j
o_j
o
j
。因为
log
(
exp
(
⋅
)
)
\log(\exp(\cdot))
lo
g
(
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}
lo
g
(
y
^
j
)
=
lo
g
(
∑
k
exp
(
o
k
)
exp
(
o
j
)
)
=
lo
g
(
exp
(
o
j
)
)
−
lo
g
(
k
∑
exp
(
o
k
)
)
=
o
j
−
lo
g
(
k
∑
exp
(
o
k
)
)
.
我们也希望保留传统的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回归比较复杂。在许多情况下,深度学习框架在这些著名的技巧之外采取了额外的预防措施,来确保数值的稳定性。这使我们避免了在实践中从零开始编写模型时可能遇到的陷阱。