这章感觉没什么需要特别记住的东西,感觉忘了回来翻一翻代码就好。

3.1 线性回归

3.1.1 线性回归的基本元素

1. 线性模型

\(\boldsymbol{x}^{(i)}\) 是一个列向量,表示第 \(i\) 个样本。用符号标识的矩阵 \(\boldsymbol{X} \in \mathbb{R}^{n\times d}\) 可以很方便地引用整个数据集中的 \(n\) 个样本。其中 \(\boldsymbol{X}\) 的每一行是一个样本,每一列是一种特征。

对于特征集合 \(\boldsymbol{X}\),预测值 \(\hat{\boldsymbol{y}} \in \mathbb{R}^n\) 可以通过矩阵-向量乘法表示为

\[\hat{\boldsymbol{y}} = \boldsymbol{Xw} + b
\]

然后求和的过程使用广播机制。另外,即使确信特征与标签的潜在关系是线性的,也会加入一个噪声项以考虑观测误差带来的影响。

2. 损失函数

这里采用的损失函数为平方误差函数。当样本 \(i\) 的预测值为 \(\hat{y}^{(i)}\),其相应的真实标签为 \(y^{(i)}\) 时,平方误差可以定义为:

\[l^{(i)}(\boldsymbol{w}, b) = \frac{1}{2} (\hat{y}^{(i)}-y^{(i)})^2
\]

这里的系数 \(\frac{1}{2}\) 的目的是为了求导后常数为 \(1\)。

因此,整个数据集上的损失均值为:

\[L(\boldsymbol{w}, b) = \frac{1}{n} \sum_{i=1}^n l^{(i)}(\boldsymbol{w}, b) = \frac{1}{n} \sum_{i=1}^n \frac{1}{2} (\boldsymbol{w}^T \boldsymbol{x}^{(i)} + b - y^{(i)})^2
\]

最后在训练模型时要找一组参数 \((\boldsymbol{w}^*, b^*)\) ,最小化总损失,即如:

\[\boldsymbol{w}^*, b^* = {\arg \min}_{\boldsymbol{w}, b} L(\boldsymbol{w}, b)
\]

3. 解析解

这里原书写的不是很清楚。具体合并大概是

\[\boldsymbol{X} \leftarrow \begin{bmatrix}
x_{11} & x_{12} & \cdots & x_{1d} & 1\\ x_{21} & x_{22} & \cdots & x_{2d} & 1\\ \vdots & \vdots & \ddots & \vdots & \vdots \\ x_{n1} & x_{n2} & \cdots & x_{nd} & 1
\end{bmatrix}
, ~~
\boldsymbol{w} \leftarrow \begin{bmatrix}
w_1 \\ w_2 \\ \vdots \\ w_d \\ b
\end{bmatrix}
\]

是这样合并的,然后问题就转化为最小化 \(||\boldsymbol{y} - \boldsymbol{Xw}||^2\),然后令损失关于 \(\boldsymbol{w}\) 的导数设为 \(0\),那么有解析解:

\[\boldsymbol{w}^* = (\boldsymbol{X}^T\boldsymbol{X})^{-1} \boldsymbol{X}^T\boldsymbol{y}
\]

当然了,一般的深度学习问题也没有解析解能给你求出来(233)。

4. 随机梯度下降

在每次需要计算更新的时候随机抽取一小批样本,这种变体叫做小批量随机梯度下降(minibatch stochastic gradient descent)。

大致可以写作如下公式:

\[(\boldsymbol{w},b) \leftarrow (\boldsymbol{w}, b) - \frac{\eta}{|B|} \sum_{i \in B} \partial_{(\boldsymbol{w}, b)} l^{(i)} (\boldsymbol{w}, b)
\]

其中,\(|B|\) 是 batch size,\(\eta\) 是学习率。

5. 用模型进行预测

由于在统计学中,推断(inference)更多地表示基于数据集估计参数,所以请尽量将给定特征的情况下估计目标的过程称为预测

3.1.2 向量化加速

先定义一下 Timer 类,这个类可以丢进小本本里。

class Timer:
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()

然后用下面的 python 循环加法和 tensor 的向量加法比较。可以发现,尽量使用 PyTorch 向量化后的 tensor 进行运算。不得不感慨一下 python 是真的慢啊,即使是 tensor 加法也还要比 C++ 慢(tensor 的基础运算应该就是拿 C++ 实现的)。这也侧面证明向量加法在 CPU 环境下应该没有涉及到应用多核。

n = 10000
a = torch.ones(n)
b = torch.ones(n) c = torch.zeros(n)
timer = Timer()
for i in range(n):
c[i] = a[i] + b[i]
f'{timer.stop():.5f} sec'
# '0.09908 sec' timer.start()
d = a + b
f'{timer.stop():.5f} sec'
'0.00035 sec'

3.1.3 正态分布与平方损失

正态分布概率密度函数如下:

\[p(x) = \frac{1}{\sqrt{2\pi \sigma^2}} \exp\left(-\frac{1}{2\sigma^2}(x-\mu)^2\right)
\]

然后本书假设观测中包含的噪声服从正态分布。噪声正态分布如:\(y = \boldsymbol{w}^T\boldsymbol{x} + b + \epsilon\),其中,\(\epsilon \sim N(0, \sigma^2)\)。

通过给定 \(\boldsymbol{x}\) 观测到特定的 \(y\) 的似然为:

\[P(y|\boldsymbol{x}) = \frac{1}{\sqrt{2\pi\sigma^2}} \exp \left(- \frac{1}{2\sigma^2}(y-\boldsymbol{w}^T\boldsymbol{x}-b)^2\right)
\]

那么参数 \(\boldsymbol{w}\) 和 b 的最优值是使整个数据集的似然最大的值:

\[p(\boldsymbol{y} | \boldsymbol{X}) = \prod_{i=1}^n p(y^{(i)} | \boldsymbol{x}^{(i)})
\]

等价于最小化负对数似然:

\[-\log P(\boldsymbol{y} |\boldsymbol{X}) = \sum_{i=1}^n \frac{1}{2} \log (2\pi \sigma^2) + \frac{1}{2\sigma^2}(y^{(i)} - \boldsymbol{w}^T\boldsymbol{x}^{(i)} - b)^2
\]

要让上式最小,即让最后一项最小。因此在有高斯噪声的假设下,最小化均方误差等价于对线性模型的极大似然估计。

3.2 线性回归的从零开始实现

这节就不详细写了,有一些代码里感觉有意思的点写在这里好了。

  • 假如说有一个 numpy 数组或者 PyTorch 的向量 \(\boldsymbol{x}\),那么可以用 y = x[torch.tensr([1, 2, 5])] 来获得一个只包含 \(x_1, x_2, x_5\) 的 \(\boldsymbol{y}\)。

优化算法代码如下:

def sgd(params, lr, batch_size):
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()

也就是说本质上是让它在不计算梯度的情况下,更新 param,然后让它的梯度更新成零。

训练用的代码为:

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)
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}')

这里有一大堆没定义的东西在书中前文提到了,但是我这里仅提供训练代码仅供示意。

  • linreg(X, w, b) 表示用 \(\boldsymbol{X}, \boldsymbol{w}, b\) 计算 \(\hat{\boldsymbol{y}}\)
  • squared_loss(y_hat, y) 为均方损失
  • featureslabels 是数据。
  • data_iter 是一个生成器,负责迭代 batch_size 大小的数据。

练习中问了个很有趣的问题:

如果将权重初始化为零,会发生什么?算法仍然有效吗?

参考文章 谈谈神经网络权重为什么不能初始化为0,来回答这一问题。

  1. 如同书中只有一层线性层的时候:

    由于

    \[\hat{\boldsymbol{y}} = \boldsymbol{Xw} + b \\
    l^{(i)}(\boldsymbol{w}, b) = \frac{1}{2} (\hat{y}^{(i)}-y^{(i)})^2 \\
    L = \sum_{i=1}^n l^{(i)}(\boldsymbol{w}, b)
    \]

    代入,有

    \[L = \sum_{i=1}^n l^{(i)}(\boldsymbol{w}, b) = \sum_{i=1}^n \frac{1}{2} (\hat{y}^{(i)}-y^{(i)})^2 = \sum_{i=1}^n \frac{1}{2} (\boldsymbol{x}^{(i)^T} \boldsymbol{w} + b -y^{(i)})^2
    \]

    求导数,有

    \[\frac{\partial L}{\partial w_0} = \sum_{i=1}^n x_0^{(i)} (x_0^{(i)}w_0 + x_1^{(i)}w_1 + b - y^{(i)}) \\
    \frac{\partial L}{\partial w_1} = \sum_{i=1}^n x_1^{(i)} (x_0^{(i)}w_0 + x_1^{(i)}w_1 + b - y^{(i)}) \\
    \]

    那么在第一次求导的时候,得

    \[\frac{\partial L}{\partial w_0} = \sum_{i=1}^n x_0^{(i)} (x_0^{(i)}w_0 + x_1^{(i)}w_1 + b - y^{(i)}) = \sum_{i=1}^n x_0^{(i)} (b-y^{(i)}) \\
    \frac{\partial L}{\partial w_1} = \sum_{i=1}^n x_1^{(i)} (x_0^{(i)}w_0 + x_1^{(i)}w_1 + b - y^{(i)}) = \sum_{i=1}^n x_1^{(i)} (b-y^{(i)}) \\
    \]

    那么,导数不为 \(0\),就确实对于算法没有影响。

  2. 但是如果不是只有一层线性层的话:

    不妨假设此时有两层线性层。第一层 \(\boldsymbol{W^{(0)} \in \mathbb{R}^{2 \times 2}}\),第二层 \(\boldsymbol{W}^{(1)} \in \mathbb{R}^2\)。过第一个线性层后输出的值就与输入 \(\boldsymbol{X}\) 无关了,那么再过第二个线性层后得到的结果就仅与第一个线性层的偏置以及第二个线性层有关了。那么,第二个线性层的权重关于第一个线性层权重的 Jacobi 矩阵是什么样子的呢?由于 \(y_{i} = (\sum_{j=1}^n w_{ij} x_j) + b\),因此 \(\frac{\mathrm{d}y_i}{\mathrm{d}x_j} = w_{ij} = 0\)。所以该矩阵是 \(0\) 矩阵,因此无法更新第一个线性层。然后又由于过了第一个线性层就与输入 \(\boldsymbol{X}\) 无关,所以权重不可以初始化为 \(0\)。

3.3 线性回归的简洁实现

仍然不全抄,只写一些有趣的代码放在这里。

torch.utils.data.DataLoader 返回的是一个可迭代对象(Iterable)而不是一个迭代器(Iterator)。

可以用 iter() 函数构造 Python 迭代器,并使用 next() 函数从迭代器中获取第一项。如下所示:

next(iter(torch.utils.data.DataLoader(dataset, batch_size, shuffle=is_train)))

然后用 net = nn.Sequential(nn.Linear(2, 1)) 得到模型,那么,除了新写一个类初始化参数外,怎么初始化第一层的参数呢?通过 net[0] 选择网络中第一层,然后使用 weight.databias.data 方法来访问参数。

net[0].weighttorch.nn.parameter.Parameter 类的实例,这个类很有趣。一种被视为模块参数的张量。Parameter 是 Tensor 的子类,当与 Module 类一起使用时具有非常特殊的属性:当 Parameter 类被分配为 Module 类的属性时,它们会自动添加到其参数列表中,并将出现在例如在 parameters() 迭代器中。但是分配一个 Tensor 就没有这样的效果。调用 net[0].weight 返回:

net[0].weight
# Parameter containing:
# tensor([[-0.3418, -0.5904]], requires_grad=True)

调用 net[0].weight.data 返回 tensor,这说明它们仅仅是张量:

net[0].weight.data
# tensor([[-0.3418, -0.5904]])

还可以使用替换方法 normal_fill_ 来重写参数值。这两个方法是在 torch.tensor() 里面的方法。

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

损失函数 nn.MSELoss() 及优化算法 torch.optim.SGD(net.parameters(), lr=0.03)。这里 net.parameters() 是个生成器,同时也是一个特殊的迭代器。输出一下它:

loss = nn.MSELoss()
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
print(net.parameters())
# <generator object Module.parameters at 0x716c1cf61150>

下面是训练代码,这段代码先把梯度清零再做反向传播,证明把梯度清零不会把 Jacobi 矩阵之类的中间状态清理掉,实践中极其不推荐像书中这么写,最好还是在求 loss 之前就清理梯度:

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}')
"""
epoch 1, loss 0.000226
epoch 2, loss 0.000103
epoch 3, loss 0.000103
"""

3.4 softmax 回归

希望在对硬性类别分类的同时使用软性带有概率的模型。

3.4.1 模型

本章介绍了表示分类数据的简单方法:独热编码(one-hot encoding)。独热编码是一个向量,它的分量和类别一样多。类别对应的分量设置为 \(1\),其他所有分量设置为 \(0\)。

本节的网络架构仍为线性层,这里有和输出一样多的仿射函数,向量形式记作 \(\boldsymbol{o} = \boldsymbol{Wx} + \boldsymbol{b}\)。具有 \(d\) 个输入和 \(q\) 个输出的全连接层,参数开销为 \(O(dq)\),但是论文 Beyond Fully-Connected Layers with Quaternions: Parameterization of Hypercomplex Multiplications with \(\frac{1}{n}\) Parameters 提及可以将具有 \(d\) 个输入和 \(q\) 个输出的全连接层的成本减少到 \(O(dq/n)\),其中超参数 \(n\) 可以设定,以在实际应用中在参数节省和模型有效性之间进行平衡。(完全没读这论文说了啥233)

Softmax 函数可以表示为:

\[\hat{\boldsymbol{y}} = \text{softmax} (\boldsymbol{o})
\]

其中,

\[\hat{y_j} = \frac{\exp(o_j)}{\sum_k \exp(o_k)}
\]

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

3.4.2 损失函数

1. 对数似然

假设数据集 \(\{ \boldsymbol{X}, \boldsymbol{Y}\}\) 具有 \(n\) 个样本,其中索引 \(i\) 的样本由特征向量 \(\boldsymbol{x}^{(i)}\) 和独热标签向量 \(\boldsymbol{y}^{(i)}\) 组成。因此可以将估计值与实际值比较:

\[p(\boldsymbol{Y} | \boldsymbol{X}) = \prod_{i=1}^n p(\boldsymbol{y}^{(i)} | \boldsymbol{x}^{(i)})
\]

最大化似然仍然是等价于熟悉的最小化负对数似然:

\[-\log P(\boldsymbol{Y} |\boldsymbol{X}) = \sum_{i=1}^n -\log P(\boldsymbol{y}^{(i)} | \boldsymbol{x}^{(i)}) = \sum_{i=1}^n l(\boldsymbol{y}^{(i)}, \hat{\boldsymbol{y}}^{(i)})
\]

其中,对于任何标签 \(\boldsymbol{y}\) 和模型预测 \(\hat{\boldsymbol{y}}\),损失函数为:

\[l(\boldsymbol{y}, \hat{\boldsymbol{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j
\]

上式一般被称为交叉熵损失。

2. softmax 及其导数

利用 softmax 定义可得:

\[l(\boldsymbol{y}, \hat{\boldsymbol{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
\]

考虑相对于任何未规范化的预测 \(o_j\) 的导数,可以得到:

\[\partial_{o_j} l(\boldsymbol{y}, \hat{\boldsymbol{y}}) = \frac{\exp(o_j)}{\sum_{k=1}^q \exp(o_k)} - y_j = \text{softmax}(\boldsymbol{o})_j - y_j = \hat{y}_j - y_j
\]

不妨设 \(s_i = \text{softmax} (\boldsymbol{o})_i\),再求二阶导:

\[\frac{\partial l(\boldsymbol{y}, \hat{\boldsymbol{y}})}{\partial o_i \partial o_j} = \frac{\partial s_i}{\partial o_j} = \begin{cases}
s_i (1 - s_i),& i=j \\
-s_j s_i, & i \not = j
\end{cases}
\]

课后题还要求 \(\text{softmax} (\boldsymbol{o})\) 给出的分布方差,并和二阶导匹配起来,所以有

\[\begin{align}
var(\boldsymbol{o}) &= \frac{1}{q-1} \sum_{j=1}^q (s_j - \overline{s})^2 \\
&= \frac{1}{q-1} [s_1^2 + s_2^2 + \cdots + s_q^2 + \overline{s}^2 * q - 2\overline{s}(s_1+s_2+\cdots+s_q)] \\
&= \frac{1}{q-1} \left( \sum_{j=1}^q s_j^2 - 2 \overline{s} \sum_{j=1}^q s_j\right) + \frac{q}{q-1} \overline{s}^2 \\
&= \frac{1}{q-1} \left( \sum_{j=1}^q s_j(s_j - 1) + \sum_{j=1}^q s_j - 2 \overline{s} \sum_{j=1}^q s_j \right) + \frac{q}{q-1} \overline{s}^2 \\
&= - \frac{1}{q-1} \sum_{j=1}^q \frac{\partial^2 l}{\partial o_j^2} + \frac{q}{q-1} (\overline{s} - \overline{s}^2 ) \\
&\approx - \frac{1}{q} \sum_{j=1}^q \frac{\partial^2 l}{\partial o_j^2} + (\overline{s} - \overline{s}^2 )
\end{align}
\]

上面式子里除以 \(q-1\) 是符合统计学中无偏估计的做法。当然和除以 \(q\) 差别也不太大。

3. 信息论浅谈

信息论的基本想法是一个不太可能的事件居然发生了,要比一个非常可能的事件发生,能提供更多的信息。如果要通过这种基本想法来量化信息,可以遵循以下三个点:

  • 非常可能发生的事件信息量要比较少,并且极端情况下,确保能够发生的事件应该没有信息量。
  • 较不可能发生的事件具有更高的信息量。
  • 独立事件应具有增量的信息。例如,投掷的硬币两次正面朝上传递的信息量,应该是投掷一次硬币正面朝上的信息量的两倍。

为了满足上述 \(3\) 个性质,因此定义一个事件 \(x\) 的自信息(self-information)为

\[I(x) = -\log P(x)
\]

这里定义的 \(I(x)\) 单位是奈特(nat)。一奈特是以 \(\frac{1}{e}\) 的概率观测到一个事件时获得的信息量。

自信息只处理单个的输出。可以用香农熵对整个概率分布中不确定性总量进行量化:

\[H(x) = \mathbb{E}_{x\sim P} [I(x)] = -\mathbb{E}_{x\sim P} [\log P(x)]
\]

这个也可以记作 \(H(P)\)。一个分布的香农熵是指遵循这个分布的事件所产生的期望信息总量。

如果对同一个随机变量 \(x\) 有两个单独的概率分布 \(P(x)\) 和 \(Q(x)\),可以使用KL散度(Kullback-Leibler divergence)来衡量这两个分布的差异:

\[D_{KL}(P||Q) = \mathbb{E}_{x \sim P} \left[\log \frac{P(x)}{Q(x)} \right] = \mathbb{E}_{x \sim P} [\log P(x) - \log Q(x)]
\]

在离散型变量的情况下,KL 散度衡量的是,当使用一种被设计成能够使得概率分布 \(Q\) 产生的消息的长度最小的编码,发送包含由概率分布 \(P\) 产生的符号的消息时,所需要的额外信息量。

KL 散度有一些有用的性质如下:

  • 非负
  • KL 散度为 \(0\),当且仅当 \(P\) 和 \(Q\) 在离散性变量的情况下是相同的分布,或者在连续型变量的情况下是“几乎处处”相同的。

由于上述两个性质,因此它经常被用作分布之间的某种距离。然而,它并不满足交换性,即 \(D_{KL}(P||Q) \not = D_{KL}(Q||P)\)。

假设此时有一个分布 \(p(x)\),并且希望用另一个分布 \(q(x)\) 来近似它,那么就可以选择最小化 \(D_{KL}(p||q)\) 或者最小化 \(D_{KL}(q||p)\)。这其中选择哪一个 KL 散度是取决于问题的。选择 \(D_{KL}(p||q)\) 的目的是为了让近似分布 \(q\) 在真实分布 \(p\) 放置高概率的所有地方都放置高概率,而选择 \(D_{KL}(q||p)\) 的目的是为了让近似分布 \(q\) 在真实分布 \(p\) 放置低概率的所有地方都很少放置高概率。

一个和 KL 散度密切联系的量是交叉熵,即 \(H(P,Q) = H(P) + D_{KL}(P||Q) = -\mathbb{E}_{x \sim P} \log Q(x)\)。因此针对 \(Q\) 最小化交叉熵等价于最小化 KL 散度。

3.5 图像分类数据集

本章其实没啥亮点,有趣的内容稍微写一下:

一个用来展示图片以及标题的函数,有 num_rows 行 num_cols 列。

def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
figsize = (num_cols * scale, num_rows * scale)
_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)
axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
ax.imshow(img.numpy())
else:
ax.imshow(img)
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
if titles:
ax.set_title(titles[i])
return axes

此外,num_workers 这个参数表示了使用子进程读取数据的个数。如果调小 batch_size 的话即使是 CPU 运行的代码速度也会减慢,在 num_workers=4 的时候,测试时间长度如下表:

batch_size 时间
1 117.74
4 28
256 3.11

3.6 softmax 回归的从零开始实现

仍然是有趣的内容:

torch.normal() 能够返回一个其中所有值都符合正态分布的 tensor。

Accumulator 类对多个变量进行累加。

class Accumulator:
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]

还有一个可以在动画中绘制图表的实用程序类 Animator。此函数仅能在 notebook 中使用。

import torch
from IPython import display
from d2l import torch as d2l
class Animator: #@save
"""在动画中绘制数据"""
def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
figsize=(3.5, 2.5)):
# 增量地绘制多条线
if legend is None:
legend = []
d2l.use_svg_display()
self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
if nrows * ncols == 1:
self.axes = [self.axes, ]
# 使用lambda函数捕获参数
self.config_axes = lambda: d2l.set_axes(
self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
self.X, self.Y, self.fmts = None, None, fmts def add(self, x, y):
# 向图表中添加多个数据点
if not hasattr(y, "__len__"):
y = [y]
n = len(y)
if not hasattr(x, "__len__"):
x = [x] * n
if not self.X:
self.X = [[] for _ in range(n)]
if not self.Y:
self.Y = [[] for _ in range(n)]
for i, (a, b) in enumerate(zip(x, y)):
if a is not None and b is not None:
self.X[i].append(a)
self.Y[i].append(b)
self.axes[0].cla()
for x, y, fmt in zip(self.X, self.Y, self.fmts):
self.axes[0].plot(x, y, fmt)
self.config_axes()
display.display(self.fig)
display.clear_output(wait=True)

这个类应该怎么用呢?见下方代码

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_loss, train_acc)
animator.add(epoch + 1, train_metrics + (test_acc,))

3.7 softmax 回归的简洁实现

如何在类外给所有线性层初始化?可以使用 nn.Module.apply(fn) 可以做到。它的本来作用是递归地对所有子模块(包括自己)做相同的操作。如:

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)

即对 net 中所有 Linear 层初始化参数。

为防止由于指数函数导致的上溢出,因此再继续 softmax 运算之前,先从所有 \(o_k\) 中减去 \(\max (o_k)\),事实上这样不会改变 softmax 的返回值:

\[\begin{align}
\hat{y}_j &= \frac{\exp(o_j - max(o_k)) \exp(\max(o_k))}{\sum_k \exp(o_k - \max (o_k))\exp(\max(o_k))} \\
&= \frac{\exp(o_j - max(o_k))}{\sum_k \exp(o_k - \max (o_k))}
\end{align}
\]

又由于有些 \(\exp(o_j - max(o_k))\) 具有较大的负值,可能导致求完指数函数后直接下溢出归零,并使得 \(\log(\hat{y}_j)\) 的值变为负无穷大。反向传播几步之后,可能会发现满屏幕的 nan。因此将交叉熵和 softmax 操作结合在一起:

\[\begin{align}
\log (\hat{y}_j) &= \log \left( \frac{\exp(o_j - max(o_k))}{\sum_k \exp(o_k - \max (o_k))}\right) \\
&= \log (\exp (o_j - \max (o_k))) - \log \left( \sum_k \exp (o_k - \max (o_k))\right) \\
&= o_j - \max (o_k) - \log \left( \sum_k \exp(o_k - \max (o_k))\right)
\end{align}
\]

这些具体落实到代码上是模型过完最后一个线性层不要做 softmax 操作,直接往 PyTorch 的 CrossEntropyLoss 里面丢就行了,因为它已经结合好了。

【动手学深度学习】第三章笔记:线性回归、SoftMax 回归、交叉熵损失的更多相关文章

  1. 小白学习之pytorch框架(2)-动手学深度学习(begin-random.shuffle()、torch.index_select()、nn.Module、nn.Sequential())

    在这向大家推荐一本书-花书-动手学深度学习pytorch版,原书用的深度学习框架是MXNet,这个框架经过Gluon重新再封装,使用风格非常接近pytorch,但是由于pytorch越来越火,个人又比 ...

  2. 对比《动手学深度学习》 PDF代码+《神经网络与深度学习 》PDF

    随着AlphaGo与李世石大战的落幕,人工智能成为话题焦点.AlphaGo背后的工作原理"深度学习"也跳入大众的视野.什么是深度学习,什么是神经网络,为何一段程序在精密的围棋大赛中 ...

  3. 【动手学深度学习】Jupyter notebook中 import mxnet出错

    问题描述 打开d2l-zh目录,使用jupyter notebook打开文件运行,import mxnet 出现无法导入mxnet模块的问题, 但是命令行运行是可以导入mxnet模块的. 原因: 激活 ...

  4. 《动手学深度学习》系列笔记—— 1.2 Softmax回归与分类模型

    目录 softmax的基本概念 交叉熵损失函数 模型训练和预测 获取Fashion-MNIST训练集和读取数据 get dataset softmax从零开始的实现 获取训练集数据和测试集数据 模型参 ...

  5. 动手学深度学习14- pytorch Dropout 实现与原理

    方法 从零开始实现 定义模型参数 网络 评估函数 优化方法 定义损失函数 数据提取与训练评估 pytorch简洁实现 小结 针对深度学习中的过拟合问题,通常使用丢弃法(dropout),丢弃法有很多的 ...

  6. 动手学深度学习9-多层感知机pytorch

    多层感知机 隐藏层 激活函数 小结 多层感知机 之前已经介绍过了线性回归和softmax回归在内的单层神经网络,然后深度学习主要学习多层模型,后续将以多层感知机(multilayer percetro ...

  7. 动手学深度学习6-认识Fashion_MNIST图像数据集

    获取数据集 读取小批量样本 小结 本节将使用torchvision包,它是服务于pytorch深度学习框架的,主要用来构建计算机视觉模型. torchvision主要由以下几个部分构成: torchv ...

  8. 动手学深度学习1- pytorch初学

    pytorch 初学 Tensors 创建空的tensor 创建随机的一个随机数矩阵 创建0元素的矩阵 直接从已经数据创建tensor 创建新的矩阵 计算操作 加法操作 转化形状 tensor 与nu ...

  9. 动手学深度学习7-从零开始完成softmax分类

    获取和读取数据 初始化模型参数 实现softmax运算 定义模型 定义损失函数 计算分类准确率 训练模型 小结 import torch import torchvision import numpy ...

  10. 动手学深度学习5-softmax回归

    softmax回归 softmax 回归模型 单样本分类的矢量计算表达式 小批量样本分类的矢量计算表达式 交叉熵损失函数 模型预测以及评价 小结 softmax回归 前几节介绍的是线性回归模型适用于输 ...

随机推荐

  1. WPF 编写一个测试 WM_TOUCH 触摸消息延迟的应用

    我听说在 Win10 到 Win11 的系统版本左右,微软加上了一大波触摸性能优化,准确来说是 HID 性能优化.我想测试一下在这些系统下,采用从 Windows 消息接收到 WM_TOUCH 触摸消 ...

  2. WPF 使用 Skia 解析绘制 SVG 图片

    本文告诉大家如何在 WPF 里面,使用 Skia 解析绘制 SVG 图片.本文也适合控制台使用 SkiaSharp 解析绘制 SVG 图片,本文的 WPF 部分只是在 Skia 绘制完成之后,将 Sk ...

  3. Taurus.MVC 微服务框架 入门开发教程:项目集成:7、微服务间的调用方式(调整):引用程序集及代码下载(新增)

    前言: 今天补充一下 Taurus.MVC 微服务教程,介绍一下微服务中针对RPC方面调整或新增的功能. 之前系列文章目录: Taurus.MVC 微服务框架 入门开发教程:项目集成:1.服务端:注册 ...

  4. arduino uno+LCD12864(ST7735S)+蓝牙模块实现贪吃蛇

    1.前言: 1.1本实验实现的贪吃蛇能穿越边界,结束游戏的唯一条件是贪吃蛇到达指定长度 1.2本实验所用LCD可能不是LCD12864,LCD12864所用库为u8glib,笔者在词库中并没有找到型号 ...

  5. SimpleHTTPServer模块详解

    SimpleHTTPServer实现文件的展示和下载 可以用python2.7直接启动一个进程.以命令执行的当前目录为页面根目录,如果不存在index.html,默认展示当前目录的所有文件. pyth ...

  6. apisix~集成服务发现注册中心

    摘要 当业务量发生变化时,需要对上游服务进行扩缩容,或者因服务器硬件故障需要更换服务器.如果网关是通过配置来维护上游服务信息,在微服务架构模式下,其带来的维护成本可想而知.再者因不能及时更新这些信息, ...

  7. GPU简介

    摘自:https://zhidao.baidu.com/question/1765722944085349980.html 其发起者和主导者是baiNVIDIA(英伟达)公司. 1999年,duNVI ...

  8. redis三主三从集群快速搭建

    redis集群快速搭建 1 本文以redis6.2.6版本为例,快速搭建一套三主三从的redis集群,已经提前将脚本写好,只需要在节点上面进行执行即可安装 #!/bin/bash install_re ...

  9. 关于对于Java中Entity以及VO,以及DTO中Request对象序列化的学习

    关于 Serializable的探讨 前提引入 是由于软件测试上有同学提到说,什么该字段在程序刚运行时,导致jvm激增,所以吸引了我的注意 回顾代码 MybatisPlus Generator自动生成 ...

  10. 详解Spring循环依赖

    一. 什么是循环依赖 循环依赖,就是两个或则两个以上的bean互相依赖对方,最终形成闭环.比如"A对象依赖B对象,而B对象也依赖A对象",或者"A对象依赖B对象,B对象依 ...