多层感知机

:label:sec_mlp

隐藏层

在网络中加入隐藏层

我们可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制,
使其能处理更普遍的函数关系类型。
要做到这一点,最简单的方法是将许多全连接层堆叠在一起。
每一层都输出到上面的层,直到生成最后的输出。
我们可以把前层看作表示,把最后一层看作线性预测器。
这种架构通常称为多层感知机(multilayer perceptron),通常缩写为MLP
下面,我们以图的方式描述了多层感知机( :numref:fig_mlp)。

:label:`fig_mlp`

这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。 输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算。 因此,这个多层感知机中的层数为2。 注意,这两个层都是全连接的。 每个输入都会影响隐藏层中的每个神经元, 而隐藏层中的每个神经元又会影响输出层中的每个神经元。

从线性到非线性

同之前的章节一样,
我们通过矩阵
来表示个样本的小批量,
其中每个样本具有个输入特征。
对于具有个隐藏单元的单隐藏层多层感知机,
表示隐藏层的输出,
称为隐藏表示(hidden representations)。
在数学或代码中,也被称为隐藏层变量(hidden-layer variable)
隐藏变量(hidden variable)。
因为隐藏层和输出层都是全连接的,
所以我们有隐藏层权重
和隐藏层偏置
以及输出层权重
和输出层偏置
形式上,我们按如下方式计算单隐藏层多层感知机的输出

注意在添加隐藏层之后,模型现在需要跟踪和更新额外的参数。
可我们能从中得到什么好处呢?在上面定义的模型里,我们没有好处!
原因很简单:上面的隐藏单元由输入的仿射函数给出,
而输出(softmax操作前)只是隐藏单元的仿射函数。
仿射函数的仿射函数本身就是仿射函数,
但是我们之前的线性模型已经能够表示任何仿射函数。

我们可以证明这一等价性,即对于任意权重值,
我们只需合并隐藏层,便可产生具有参数


的等价单层模型:

为了发挥多层架构的潜力,
我们还需要一个额外的关键要素:
在仿射变换之后对每个隐藏单元应用非线性的激活函数(activation function)
激活函数的输出(例如,)被称为活性值(activations)。
一般来说,有了激活函数,就不可能再将我们的多层感知机退化成线性模型

由于中的每一行对应于小批量中的一个样本,
出于记号习惯的考量,
我们定义非线性函数也以按行的方式作用于其输入,
即一次计算一个样本。
我们在 :numref:subsec_softmax_vectorization
以相同的方式使用了softmax符号来表示按行操作。
但是本节应用于隐藏层的激活函数通常不仅按行操作,也按元素操作。
这意味着在计算每一层的线性部分之后,我们可以计算每个活性值,
而不需要查看其他隐藏单元所取的值。对于大多数激活函数都是这样。

为了构建更通用的多层感知机,
我们可以继续堆叠这样的隐藏层,
例如
一层叠一层,从而产生更有表达能力的模型。

通用近似定理

多层感知机可以通过隐藏神经元,捕捉到输入之间复杂的相互作用,
这些神经元依赖于每个输入的值。
我们可以很容易地设计隐藏节点来执行任意计算。
例如,在一对输入上进行基本逻辑操作,多层感知机是通用近似器。
即使是网络只有一个隐藏层,给定足够的神经元和正确的权重,
我们可以对任意函数建模,尽管实际中学习该函数是很困难的。

而且,虽然一个单隐层网络能学习任何函数,
但并不意味着我们应该尝试使用单隐藏层网络来解决所有问题。
事实上,通过使用更深(而不是更广)的网络,我们可以更容易地逼近许多函数。
我们将在后面的章节中进行更细致的讨论。

激活函数

:label:subsec_activation_functions

激活函数(activation function)通过计算加权和并加上偏置来确定神经元是否应该被激活
它们将输入信号转换为输出的可微运算。
大多数激活函数都是非线性的

ReLU函数

最受欢迎的激活函数是修正线性单元(Rectified linear unit,ReLU),
因为它实现简单,同时在各种预测任务中表现良好。
[ReLU提供了一种非常简单的非线性变换]。
给定元素,ReLU函数被定义为该元素与的最大值:

()

通俗地说,ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。
为了直观感受一下,我们可以画出函数的曲线图。
正如从图中所看到,激活函数是分段线性的。

1
2
3
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)  
y = torch.relu(x)
d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))

当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。
注意,当输入值精确等于0时,ReLU函数不可导。
在此时,我们默认使用左侧的导数,即当输入为0时导数为0。
我们可以忽略这种情况,因为输入可能永远都不会是0。
这里引用一句古老的谚语,“如果微妙的边界条件很重要,我们很可能是在研究数学而非工程”, 这个观点正好适用于这里。
下面我们绘制ReLU函数的导数。

1
2
y.backward(torch.ones_like(x), retain_graph=True)  
d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))

使用ReLU的原因是,它求导表现得特别好:要么让参数消失,要么让参数通过
这使得优化表现得更好,并且ReLU减轻了困扰以往神经网络的梯度消失问题。

注意,ReLU函数有许多变体,包括参数化ReLU(Parameterized ReLU,pReLU
函数 :cite:He.Zhang.Ren.ea.2015
该变体为ReLU添加了一个线性项,因此即使参数是负的,某些信息仍然可以通过:

sigmoid函数

[*对于一个定义域在中的输入, sigmoid函数将输入变换为区间(0, 1)上的输出]。
因此,sigmoid通常称为
挤压函数*(squashing function):
它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值:

()

在最早的神经网络中,科学家们感兴趣的是对“激发”或“不激发”的生物神经元进行建模。
因此,这一领域的先驱可以一直追溯到人工神经元的发明者麦卡洛克和皮茨,他们专注于阈值单元。
阈值单元在其输入低于某个阈值时取值0,当输入超过阈值时取值1。

当人们逐渐关注到到基于梯度的学习时,
sigmoid函数是一个自然的选择,因为它是一个平滑的、可微的阈值单元近似
当我们想要将输出视作二元分类问题的概率时,
sigmoid仍然被广泛用作输出单元上的激活函数 (sigmoid可以视为softmax的特例)。
然而,sigmoid在隐藏层中已经较少使用
它在大部分时候被更简单、更容易训练的ReLU所取代。
在后面关于循环神经网络的章节中,我们将描述利用sigmoid单元来控制时序信息流的架构。

下面,我们绘制sigmoid函数。
注意,当输入接近0时,sigmoid函数接近线性变换。

1
2
y = torch.sigmoid(x)  
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))

sigmoid函数的导数为下面的公式:

sigmoid函数的导数图像如下所示。
注意,当输入为0时,sigmoid函数的导数达到最大值0.25;
而输入在任一方向上越远离0点时,导数越接近0。

1
2
3
4
# 清除以前的梯度  
x.grad.data.zero_()
y.backward(torch.ones_like(x),retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of sigmoid', figsize=(5, 2.5))

tanh函数

与sigmoid函数类似,
[tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上]。
tanh函数的公式如下:

()

下面我们绘制tanh函数。
注意,当输入在0附近时,tanh函数接近线性变换。
函数的形状类似于sigmoid函数,
不同的是tanh函数关于坐标系原点中心对称。

1
2
y = torch.tanh(x)  
d2l.plot(x.detach(), y.detach(), 'x', 'tanh(x)', figsize=(5, 2.5))

tanh函数的导数是:

tanh函数的导数图像如下所示。
当输入接近0时,tanh函数的导数接近最大值1。
与我们在sigmoid函数图像中看到的类似,
输入在任一方向上越远离0点,导数越接近0。

1
2
3
4
# 清除以前的梯度  
x.grad.data.zero_()
y.backward(torch.ones_like(x),retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of tanh', figsize=(5, 2.5))

小结

  • 多层感知机在输出层和输入层之间增加一个或多个全连接隐藏层,并通过激活函数转换隐藏层的输出。
  • 常用的激活函数包括ReLU函数、sigmoid函数和tanh函数。

多层感知机的从零开始实现


:label:sec_mlp_scratch

我们已经在 :numref:sec_mlp中描述了多层感知机(MLP),
现在让我们尝试自己实现一个多层感知机。
为了与之前softmax回归( :numref:sec_softmax_scratch
获得的结果进行比较,
我们将继续使用Fashion-MNIST图像分类数据集
( :numref:sec_fashion_mnist)。

1
2
3
import torch  
from torch import nn
from d2l import torch as d2l
1
2
batch_size = 256  
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

初始化模型参数

回想一下,Fashion-MNIST中的每个图像由
个灰度像素值组成。
所有图像共分为10个类别。
忽略像素之间的空间结构,
我们可以将每个图像视为具有784个输入特征
和10个类的简单分类数据集。
首先,我们将[实现一个具有单隐藏层的多层感知机,
它包含256个隐藏单元
]。
注意,我们可以将这两个变量都视为超参数。
通常,我们选择2的若干次幂作为层的宽度。
因为内存在硬件中的分配和寻址方式,这么做往往可以在计算上更高效。

我们用几个张量来表示我们的参数。
注意,对于每一层我们都要记录一个权重矩阵和一个偏置向量。
跟以前一样,我们要为损失关于这些参数的梯度分配内存。

1
2
3
4
5
6
7
8
9
10
num_inputs, num_outputs, num_hiddens = 784, 10, 256  

W1 = nn.Parameter(torch.randn(
num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(
num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))

params = [W1, b1, W2, b2]

激活函数

为了确保我们对模型的细节了如指掌,
我们将[实现ReLU激活函数],
而不是直接调用内置的relu函数。

1
2
3
def relu(X):  
a = torch.zeros_like(X)
return torch.max(X, a)

模型

因为我们忽略了空间结构,
所以我们使用reshape将每个二维图像转换为一个长度为num_inputs的向量。
只需几行代码就可以(实现我们的模型)。

1
2
3
4
def net(X):  
X = X.reshape((-1, num_inputs))
H = relu(X@W1 + b1) # 这里“@”代表矩阵乘法
return (H@W2 + b2)

损失函数

由于我们已经从零实现过softmax函数( :numref:sec_softmax_scratch),
因此在这里我们直接使用高级API中的内置函数来计算softmax和交叉熵损失。
回想一下我们之前在 :numref:subsec_softmax-implementation-revisited

1
loss = nn.CrossEntropyLoss(reduction='none')

训练

幸运的是,[多层感知机的训练过程与softmax回归的训练过程完全相同]。
可以直接调用d2l包的train_ch3函数(参见 :numref:sec_softmax_scratch ),
将迭代周期数设置为10,并将学习率设置为0.1.

1
2
3
num_epochs, lr = 10, 0.1  
updater = torch.optim.SGD(params, lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)
1
d2l.predict_ch3(net, test_iter)

多层感知机的简洁实现


:label:sec_mlp_concise

本节将介绍(通过高级API更简洁地实现多层感知机)。

1
2
3
import torch  
from torch import nn
from d2l import torch as d2l

模型

与softmax回归的简洁实现( :numref:sec_softmax_concise)相比,
唯一的区别是我们添加了2个全连接层(之前我们只添加了1个全连接层)。
第一层是[隐藏层],它(包含256个隐藏单元,并使用了ReLU激活函数)。
第二层是输出层。

1
2
3
4
5
6
7
8
9
10
net = nn.Sequential(nn.Flatten(),  
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10))

def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);

[训练过程]的实现与我们实现softmax回归时完全相同,
这种模块化设计使我们能够将与模型架构有关的内容独立出来。

1
2
3
batch_size, lr, num_epochs = 256, 0.1, 10  
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)
1
2
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)  
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
## 小结
  • 我们可以使用高级API更简洁地实现多层感知机。
  • 对于相同的分类问题,多层感知机的实现与softmax回归的实现相同,只是多层感知机的实现里增加了带有激活函数的隐藏层。

模型选择、欠拟合和过拟合


:label:sec_model_selection

将模型在训练数据上拟合的比在潜在分布中更接近的现象称为过拟合(overfitting),
用于对抗过拟合的技术称为正则化(regularization)。

在实验中调整模型架构或超参数时会发现:
如果有足够多的神经元、层数和训练迭代周期,
模型最终可以在训练集上达到完美的精度,此时测试集的准确性却下降了。

训练误差和泛化误差

为了进一步讨论这一现象,我们需要了解训练误差和泛化误差。

训练误差(training error)是指,模型在训练数据集上计算得到的误差。
泛化误差(generalization error)是指,模型应用在同样从原始样本的分布中抽取的无限多数据样本时,模型误差的期望。

问题是,我们永远不能准确地计算出泛化误差。
这是因为无限多的数据样本是一个虚构的对象。
在实际中,我们只能通过将模型应用于一个独立的测试集来估计泛化误差,
测试集由随机选取的、未曾在训练集中出现的数据样本构成

当我们训练模型时,我们试图找到一个能够尽可能拟合训练数据的函数。
但是如果它执行地“太好了”,而不能对看不见的数据做到很好泛化,就会导致过拟合。
这种情况正是我们想要避免或控制的。
深度学习中有许多启发式的技术旨在防止过拟合。

模型复杂性

当我们有简单的模型和大量的数据时,我们期望泛化误差与训练误差相近。
当我们有更复杂的模型和更少的样本时,我们预计训练误差会下降,但泛化误差会增大。
模型复杂性由什么构成是一个复杂的问题。
一个模型是否能很好地泛化取决于很多因素。
例如,具有更多参数的模型可能被认为更复杂,
参数有更大取值范围的模型可能更为复杂。
通常对于神经网络,我们认为需要更多训练迭代的模型比较复杂,
而需要早停(early stopping)的模型(即较少训练迭代周期)就不那么复杂。

我们很难比较本质上不同大类的模型之间(例如,决策树与神经网络)的复杂性。
就目前而言,一条简单的经验法则相当有用:
统计学家认为,能够轻松解释任意事实的模型是复杂的,
而表达能力有限但仍能很好地解释数据的模型可能更有现实用途。

本节为了给出一些直观的印象,我们将重点介绍几个倾向于影响模型泛化的因素。

  1. 可调整参数的数量。当可调整参数的数量(有时称为自由度)很大时,模型往往更容易过拟合。
  2. 参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。
  3. 训练样本的数量。即使模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。

模型选择

在机器学习中,我们通常在评估几个候选模型后选择最终的模型
这个过程叫做模型选择
有时,需要进行比较的模型在本质上是完全不同的(比如,决策树与线性模型)。
又有时,我们需要比较不同的超参数设置下的同一类模型。

例如,训练多层感知机模型时,我们可能希望比较具有
不同数量的隐藏层、不同数量的隐藏单元以及不同的激活函数组合的模型。
为了确定候选模型中的最佳模型,我们通常会使用验证集。

验证集

原则上,在我们确定所有的超参数之前,我们不希望用到测试集。
如果我们在模型选择过程中使用测试数据,可能会有过拟合测试数据的风险,那就麻烦大了。
如果我们过拟合了训练数据,还可以在测试数据上的评估来判断过拟合。
但是如果我们过拟合了测试数据,我们又该怎么知道呢?

因此,我们决不能依靠测试数据进行模型选择。
然而,我们也不能仅仅依靠训练数据来选择模型,因为我们无法估计训练数据的泛化误差。

在实际应用中,情况变得更加复杂。
虽然理想情况下我们只会使用测试数据一次
以评估最好的模型或比较一些模型效果,但现实是测试数据很少在使用一次后被丢弃
我们很少能有充足的数据来对每一轮实验采用全新测试集。

解决此问题的常见做法是将我们的数据分成三份,
除了训练测试数据集之外,还增加一个验证数据集(validation dataset),
也叫验证集(validation set)。
但现实是验证数据和测试数据之间的边界模糊得令人担忧
除非另有明确说明,否则在这本书的实验中,
我们实际上是在使用应该被正确地称为训练数据验证数据的数据集,
并没有真正的测试数据集。
因此,书中每次实验报告的准确度都是验证集准确度,而不是测试集准确度。

折交叉验证

训练数据稀缺时,我们甚至可能无法提供足够的数据来构成一个合适的验证集
这个问题的一个流行的解决方案是采用折交叉验证
这里,原始训练数据被分成个不重叠的子集。
然后执行次模型训练和验证,每次在个子集上进行训练,
并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。
最后,通过对次实验的结果取平均来估计训练和验证误差。

欠拟合还是过拟合?

当我们比较训练和验证误差时,我们要注意两种常见的情况。
首先,我们要注意这样的情况:训练误差和验证误差都很严重,
但它们之间仅有一点差距。
如果模型不能降低训练误差,这可能意味着模型过于简单(即表达能力不足),
无法捕获试图学习的模式。
此外,由于我们的训练和验证误差之间的泛化误差很小,
我们有理由相信可以用一个更复杂的模型降低训练误差。
这种现象被称为欠拟合(underfitting)。

另一方面,当我们的训练误差明显低于验证误差时要小心,
这表明严重的过拟合(overfitting)。
注意,过拟合并不总是一件坏事。
特别是在深度学习领域,众所周知,
最好的预测模型在训练数据上的表现往往比在保留(验证)数据上好得多。
最终,我们通常更关心验证误差,而不是训练误差和验证误差之间的差距。

是否过拟合或欠拟合可能取决于模型复杂性和可用训练数据集的大小,
这两个点将在下面进行讨论。

模型复杂性

为了说明一些关于过拟合和模型复杂性的经典直觉,
我们给出一个多项式的例子。
给定由单个特征和对应实数标签组成的训练数据,
我们试图找到下面的阶多项式来估计标签

这只是一个线性回归问题,我们的特征是的幂给出的,
模型的权重是给出的,偏置是给出的
(因为对于所有的都有)。
由于这只是一个线性回归问题,我们可以使用平方误差作为我们的损失函数。

高阶多项式函数比低阶多项式函数复杂得多。
高阶多项式的参数较多,模型函数的选择范围较广。
因此在固定训练数据集的情况下,
高阶多项式函数相对于低阶多项式的训练误差应该始终更低(最坏也是相等)。
事实上,当数据样本包含了的不同值时,
函数阶数等于数据样本数量的多项式函数可以完美拟合训练集。
在 :numref:fig_capacity_vs_error中,
我们直观地描述了多项式的阶数和欠拟合与过拟合之间的关系。

:label:`fig_capacity_vs_error`

数据集大小

另一个重要因素是数据集的大小。
训练数据集中的样本越少,我们就越有可能(且更严重地)过拟合。
随着训练数据量的增加,泛化误差通常会减小。
此外,一般来说,更多的数据不会有什么坏处。
对于固定的任务和数据分布,模型复杂性和数据集大小之间通常存在关系。
给出更多的数据,我们可能会尝试拟合一个更复杂的模型。
能够拟合更复杂的模型可能是有益的。
如果没有足够的数据,简单的模型可能更有用。
对于许多任务,深度学习只有在有数千个训练样本时才优于线性模型。
从一定程度上来说,深度学习目前的生机要归功于
廉价存储、互联设备以及数字化经济带来的海量数据集。

多项式回归

我们现在可以(通过多项式拟合来探索这些概念)。

1
2
3
4
5
import math  
import numpy as np
import torch
from torch import nn
from d2l import torch as d2l

生成数据集

给定,我们将[使用以下三阶多项式来生成训练和测试数据的标签:]

()

噪声项服从均值为0且标准差为0.1的正态分布。
在优化的过程中,我们通常希望避免非常大的梯度值或损失值。
这就是我们将特征从调整为的原因,
这样可以避免很大的带来的特别大的指数值。
我们将为训练集和测试集各生成100个样本。

1
2
3
4
5
6
7
8
9
10
11
12
13
max_degree = 20  # 多项式的最大阶数  
n_train, n_test = 100, 100 # 训练和测试数据集大小
true_w = np.zeros(max_degree) # 分配大量的空间
true_w[0:4] = np.array([5, 1.2, -3.4, 5.6])

features = np.random.normal(size=(n_train + n_test, 1))
np.random.shuffle(features)
poly_features = np.power(features, np.arange(max_degree).reshape(1, -1))
for i in range(max_degree):
poly_features[:, i] /= math.gamma(i + 1) # gamma(n)=(n-1)!
# labels的维度:(n_train+n_test,)
labels = np.dot(poly_features, true_w)
labels += np.random.normal(scale=0.1, size=labels.shape)

同样,存储在poly_features中的单项式由gamma函数重新缩放,
其中
从生成的数据集中[查看一下前2个样本],
第一个值是与偏置相对应的常量特征。

1
2
3
# NumPy ndarray转换为tensor  
true_w, features, poly_features, labels = [torch.tensor(x, dtype=
torch.float32) for x in [true_w, features, poly_features, labels]]
1
features[:2], poly_features[:2, :], labels[:2]
1
2
3
4
5
6
7
8
9
10
11
(tensor([[-0.5654],
[-0.8904]]),
tensor([[ 1.0000e+00, -5.6545e-01, 1.5987e-01, -3.0132e-02, 4.2595e-03,
-4.8171e-04, 4.5397e-05, -3.6671e-06, 2.5919e-07, -1.6284e-08,
9.2080e-10, -4.7333e-11, 2.2304e-12, -9.7012e-14, 3.9182e-15,
-1.4770e-16, 5.2199e-18, -1.7362e-19, 5.4542e-21, -1.6232e-22],
[ 1.0000e+00, -8.9043e-01, 3.9643e-01, -1.1766e-01, 2.6193e-02,
-4.6645e-03, 6.9224e-04, -8.8055e-05, 9.8008e-06, -9.6966e-07,
8.6341e-08, -6.9891e-09, 5.1861e-10, -3.5522e-11, 2.2592e-12,
-1.3411e-13, 7.4636e-15, -3.9093e-16, 1.9338e-17, -9.0629e-19]]),
tensor([3.7306, 2.1886]))

对模型进行训练和测试

首先让我们[实现一个函数来评估模型在给定数据集上的损失]。

1
2
3
4
5
6
7
8
9
def evaluate_loss(net, data_iter, loss):  #@save  
"""评估给定数据集上模型的损失"""
metric = d2l.Accumulator(2) # 损失的总和,样本数量
for X, y in data_iter:
out = net(X)
y = y.reshape(out.shape)
l = loss(out, y)
metric.add(l.sum(), l.numel())
return metric[0] / metric[1]

现在[定义训练函数]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def train(train_features, test_features, train_labels, test_labels,  
num_epochs=400):
loss = nn.MSELoss(reduction='none')
input_shape = train_features.shape[-1]
# 不设置偏置,因为我们已经在多项式中实现了它
net = nn.Sequential(nn.Linear(input_shape, 1, bias=False))
batch_size = min(10, train_labels.shape[0])
train_iter = d2l.load_array((train_features, train_labels.reshape(-1,1)),
batch_size)
test_iter = d2l.load_array((test_features, test_labels.reshape(-1,1)),
batch_size, is_train=False)
trainer = torch.optim.SGD(net.parameters(), lr=0.01)
animator = d2l.Animator(xlabel='epoch', ylabel='loss', yscale='log',
xlim=[1, num_epochs], ylim=[1e-3, 1e2],
legend=['train', 'test'])
for epoch in range(num_epochs):
d2l.train_epoch_ch3(net, train_iter, loss, trainer)
if epoch == 0 or (epoch + 1) % 20 == 0:
animator.add(epoch + 1, (evaluate_loss(net, train_iter, loss),
evaluate_loss(net, test_iter, loss)))
print('weight:', net[0].weight.data.numpy())

[三阶多项式函数拟合(正常)]

我们将首先使用三阶多项式函数,它与数据生成函数的阶数相同。
结果表明,该模型能有效降低训练损失和测试损失。
学习到的模型参数也接近真实值

1
2
3
# 从多项式特征中选择前4个维度,即1,x,x^2/2!,x^3/3!  
train(poly_features[:n_train, :4], poly_features[n_train:, :4],
labels[:n_train], labels[n_train:])

[线性函数拟合(欠拟合)]

让我们再看看线性函数拟合,减少该模型的训练损失相对困难。
在最后一个迭代周期完成后,训练损失仍然很高。
当用来拟合非线性模式(如这里的三阶多项式函数)时,线性模型容易欠拟合。

1
2
3
# 从多项式特征中选择前2个维度,即1和x  
train(poly_features[:n_train, :2], poly_features[n_train:, :2],
labels[:n_train], labels[n_train:])

[高阶多项式函数拟合(过拟合)]

现在,让我们尝试使用一个阶数过高的多项式来训练模型。
在这种情况下,没有足够的数据用于学到高阶系数应该具有接近于零的值。
因此,这个过于复杂的模型会轻易受到训练数据中噪声的影响。
虽然训练损失可以有效地降低,但测试损失仍然很高。
结果表明,复杂模型对数据造成了过拟合。

1
2
3
# 从多项式特征中选取所有维度  
train(poly_features[:n_train, :], poly_features[n_train:, :],
labels[:n_train], labels[n_train:], num_epochs=1500)

在接下来的章节中,我们将继续讨论过拟合问题和处理这些问题的方法,例如权重衰减和dropout。

小结

  • 欠拟合是指模型无法继续减少训练误差。过拟合是指训练误差远小于验证误差。
  • 由于不能基于训练误差来估计泛化误差,因此简单地最小化训练误差并不一定意味着泛化误差的减小。机器学习模型需要注意防止过拟合,即防止泛化误差过大。
  • 验证集可以用于模型选择,但不能过于随意地使用它。
  • 我们应该选择一个复杂度适当的模型,避免使用数量不足的训练样本。

权重衰减


:label:sec_weight_decay

前一节我们描述了过拟合的问题,本节我们将介绍一些正则化模型的技术。
我们总是可以通过去收集更多的训练数据来缓解过拟合。
但这可能成本很高,耗时颇多,或者完全超出我们的控制,因而在短期内不可能做到。
假设我们已经拥有尽可能多的高质量数据,我们便可以将重点放在正则化技术上。

回想一下,在多项式回归的例子( :numref:sec_model_selection)中,
我们可以通过调整拟合多项式的阶数来限制模型的容量
实际上,限制特征的数量是缓解过拟合的一种常用技术。
然而,简单地丢弃特征对这项工作来说可能过于生硬。
我们继续思考多项式回归的例子,考虑高维输入可能发生的情况。
多项式对多变量数据的自然扩展称为单项式(monomials),
也可以说是变量幂的乘积。
单项式的阶数是幂的和。
例如,都是3次单项式。

注意,随着阶数的增长,带有阶数的项数迅速增加。
给定个变量,阶数为的项的个数为
,即
因此即使是阶数上的微小变化,比如从,也会显著增加我们模型的复杂性。
仅仅通过简单的限制特征数量(在多项式回归中体现为限制阶数),可能仍然使模型在过简单和过复杂中徘徊,
我们需要一个更细粒度的工具来调整函数的复杂性,使其达到一个合适的平衡位置。

范数与权重衰减

在 :numref:subsec_lin-algebra-norms中,
我们已经描述了范数和范数,
它们是更为一般的范数的特殊情况。
在训练参数化机器学习模型时,
权重衰减(weight decay) 是最广泛使用的正则化的技术之一,
它通常也被称为 正则化
这项技术通过函数与零的距离来衡量函数的复杂度
因为在所有函数中,函数(所有输入都得到值
在某种意义上是最简单的。
但是我们应该如何精确地测量一个函数和零之间的距离呢?
没有一个正确的答案。

一种简单的方法是通过线性函数

中的权重向量的某个范数来度量其复杂性,
例如
要保证权重向量比较小,
最常用方法是将其范数作为惩罚项加到最小化损失的问题中。
将原来的训练目标最小化训练标签上的预测损失
调整为最小化预测损失和惩罚项之和
现在,如果我们的权重向量增长的太大,
我们的学习算法可能会更集中于最小化权重范数
这正是我们想要的。
让我们回顾一下 :numref:sec_linear_regression中的线性回归例子。
我们的损失由下式给出:

回想一下,是样本的特征,
是样本的标签,
是权重和偏置参数。
为了惩罚权重向量的大小,
我们必须以某种方式在损失函数中添加
但是模型应该如何平衡这个新的额外惩罚的损失?
实际上,我们通过正则化常数来描述这种权衡,
这是一个非负超参数,我们使用验证数据拟合:

对于,我们恢复了原来的损失函数。
对于,我们限制的大小。
这里我们仍然除以:当我们取一个二次函数的导数时,
会抵消,以确保更新表达式看起来既漂亮又简单。
为什么在这里我们使用平方范数而不是标准范数(即欧几里得距离)?
我们这样做是为了便于计算。
通过平方范数,我们去掉平方根,留下权重向量每个分量的平方和。
这使得惩罚的导数很容易计算:导数的和等于和的导数。

此外,为什么我们首先使用范数,而不是范数
事实上,这个选择在整个统计领域中都是有效的和受欢迎的。
正则化线性模型构成经典的岭回归(ridge regression)算法,
正则化线性回归是统计学中类似的基本模型,
通常被称为套索回归(lasso regression)。
使用范数的一个原因是它对权重向量的大分量施加了巨大的惩罚
这使得我们的学习算法偏向于在大量特征上均匀分布权重的模型
在实践中,这可能使它们对单个变量中的观测误差更为稳定
相比之下,**惩罚会导致模型将权重集中在一小部分特征上
而将其他权重清除为零
这称为
特征选择(feature selection)**,这可能是其他场景下需要的。

使用与 :eqref:eq_linreg_batch_update中的相同符号,
正则化回归的小批量随机梯度下降更新如下式:

根据之前章节所讲的,我们根据估计值与观测值之间的差异来更新
然而,我们同时也在试图将的大小缩小到零
这就是为什么这种方法有时被称为权重衰减
我们仅考虑惩罚项,优化算法在训练的每一步衰减权重。
与特征选择相比,权重衰减为我们提供了一种连续的机制来调整函数的复杂度。
较小的值对应较少约束的
而较大的值对的约束更大。

是否对相应的偏置进行惩罚在不同的实践中会有所不同,
在神经网络的不同层中也会有所不同。
通常,网络输出层的偏置项不会被正则化

高维线性回归

我们通过一个简单的例子来演示权重衰减。

1
2
3
4
%matplotlib inline  
import torch
from torch import nn
from d2l import torch as d2l

首先,我们[像以前一样生成一些数据],生成公式如下:

()

我们选择标签是关于输入的线性函数。
标签同时被均值为0,标准差为0.01高斯噪声破坏。
为了使过拟合的效果更加明显,我们可以将问题的维数增加到
并使用一个只包含20个样本的小训练集。

1
2
3
4
5
6
n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5  
true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05
train_data = d2l.synthetic_data(true_w, true_b, n_train)
train_iter = d2l.load_array(train_data, batch_size)
test_data = d2l.synthetic_data(true_w, true_b, n_test)
test_iter = d2l.load_array(test_data, batch_size, is_train=False)

从零开始实现

下面我们将从头开始实现权重衰减,只需将的平方惩罚添加到原始目标函数中。

[初始化模型参数]

首先,我们将定义一个函数来随机初始化模型参数。

1
2
3
4
def init_params():  
w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
return [w, b]

(定义范数惩罚)

实现这一惩罚最方便的方法是对所有项求平方后并将它们求和。

1
2
def l2_penalty(w):  
return torch.sum(w.pow(2)) / 2

[定义训练代码实现]

下面的代码将模型拟合训练数据集,并在测试数据集上进行评估。
从 :numref:chap_linear以来,线性网络和平方损失没有变化,
所以我们通过d2l.linregd2l.squared_loss导入它们。
唯一的变化是损失现在包括了惩罚项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def train(lambd):  
w, b = init_params()
net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
num_epochs, lr = 100, 0.003
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
# 增加了L2范数惩罚项,
# 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
l = loss(net(X), y) + lambd * l2_penalty(w)
l.sum().backward()
d2l.sgd([w, b], lr, batch_size)
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数是:', torch.norm(w).item())

[忽略正则化直接训练]

我们现在用lambd = 0禁用权重衰减后运行这个代码。
注意,这里训练误差有了减少,但测试误差没有减少,
这意味着出现了严重的过拟合。

1
train(lambd=0)

[使用权重衰减]

下面,我们使用权重衰减来运行代码。
注意,在这里训练误差增大,但测试误差减小。
这正是我们期望从正则化中得到的效果。

1
train(lambd=3)

[简洁实现]

由于权重衰减在神经网络优化中很常用,
深度学习框架为了便于我们使用权重衰减,
将权重衰减集成到优化算法中,以便与任何损失函数结合使用。
此外,这种集成还有计算上的好处,
允许在不增加任何额外的计算开销的情况下向算法中添加权重衰减。
由于更新的权重衰减部分仅依赖于每个参数的当前值,
因此优化器必须至少接触每个参数一次。

在下面的代码中,我们在实例化优化器时直接通过weight_decay指定weight decay超参数。
默认情况下,PyTorch同时衰减权重和偏移。
这里我们只为权重设置了weight_decay,所以偏置参数不会衰减。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def train_concise(wd):  
net = nn.Sequential(nn.Linear(num_inputs, 1))
for param in net.parameters():
param.data.normal_()
loss = nn.MSELoss(reduction='none')
num_epochs, lr = 100, 0.003
# 偏置参数没有衰减
trainer = torch.optim.SGD([
{"params":net[0].weight,'weight_decay': wd},
{"params":net[0].bias}], lr=lr)
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test'])
for epoch in range(num_epochs):
for X, y in train_iter:
trainer.zero_grad()
l = loss(net(X), y)
l.mean().backward()
trainer.step()
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1,
(d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数:', net[0].weight.norm().item())

[这些图看起来和我们从零开始实现权重衰减时的图相同]。
然而,它们运行得更快,更容易实现。
对于更复杂的问题,这一好处将变得更加明显。

1
train_concise(0)
1
train_concise(3)

到目前为止,我们只接触到一个简单线性函数的概念。
此外,由什么构成一个简单的非线性函数可能是一个更复杂的问题。
例如,再生核希尔伯特空间(RKHS)
允许在非线性环境中应用为线性函数引入的工具。
不幸的是,基于RKHS的算法往往难以应用到大型、高维的数据。

小结

  • 正则化是处理过拟合的常用方法:在训练集的损失函数中加入惩罚项,以降低学习到的模型的复杂度。
  • 保持模型简单的一个特别的选择是使用惩罚的权重衰减。这会导致学习算法更新步骤中的权重衰减。
  • 权重衰减功能在深度学习框架的优化器中提供。
  • 在同一训练代码实现中,不同的参数集可以有不同的更新行为。

暂退法(Dropout)


:label:sec_dropout

在 :numref:sec_weight_decay 中,
我们介绍了通过惩罚权重的范数来正则化统计模型的经典方法。
在概率角度看,我们可以通过以下论证来证明这一技术的合理性:
我们已经假设了一个先验,即权重的值取自均值为0的高斯分布。
更直观的是,我们希望模型深度挖掘特征,即将其权重分散到许多特征中,
而不是过于依赖少数潜在的虚假关联。

扰动的稳健性

在2014年,斯里瓦斯塔瓦等人 :cite:Srivastava.Hinton.Krizhevsky.ea.2014
就如何将毕晓普的想法应用于网络的内部层提出了一个想法:
在训练过程中,他们建议在计算后续层之前向网络的每一层注入噪声
因为当训练一个有多层的深层网络时,注入噪声只会在输入-输出映射上增强平滑性

这个想法被称为暂退法(dropout)。
暂退法在前向传播过程中,计算每一内部层的同时注入噪声,这已经成为训练神经网络的常用技术。
这种方法之所以被称为暂退法,因为我们从表面上看是在训练过程中丢弃(drop out)一些神经元。
在整个训练过程的每一次迭代中,标准暂退法包括在计算下一层之前将当前层中的一些节点置零

需要说明的是,暂退法的原始论文提到了一个关于有性繁殖的类比:
神经网络过拟合与每一层都依赖于前一层激活值相关,称这种情况为“共适应性”。
作者认为,暂退法会破坏共适应性,就像有性生殖会破坏共适应的基因一样。

那么关键的挑战就是如何注入这种噪声。
一种想法是以一种无偏向(unbiased)的方式注入噪声。
这样在固定住其他层时,每一层的期望值等于没有噪音时的值。

在毕晓普的工作中,他将高斯噪声添加到线性模型的输入中。
在每次训练迭代中,他将从均值为零的分布
采样噪声添加到输入
从而产生扰动点
预期是

在标准暂退法正则化中,通过按保留(未丢弃)的节点的分数进行规范化来消除每一层的偏差
换言之,每个中间活性值暂退概率由随机变量替换,如下所示:

根据此模型的设计,其期望值保持不变,即

实践中的暂退法

回想一下 :numref:fig_mlp中带有1个隐藏层和5个隐藏单元的多层感知机。
当我们将暂退法应用到隐藏层,以的概率将隐藏单元置为零时,
结果可以看作一个只包含原始神经元子集的网络。
比如在 :numref:fig_dropout2中,删除了
因此输出的计算不再依赖于,并且它们各自的梯度在执行反向传播时也会消失。
这样,输出层的计算不能过度依赖于的任何一个元素。

:label:`fig_dropout2`

通常,我们在测试时不用暂退法。
给定一个训练好的模型和一个新的样本,我们不会丢弃任何节点,因此不需要标准化。
然而也有一些例外:一些研究人员在测试时使用暂退法,
用于估计神经网络预测的“不确定性”:
如果通过许多不同的暂退法遮盖后得到的预测结果都是一致的,那么我们可以说网络发挥更稳定。

从零开始实现

要实现单层的暂退法函数,
我们从均匀分布中抽取样本,样本数与这层神经网络的维度一致。
然后我们保留那些对应样本大于的节点,把剩下的丢弃。

在下面的代码中,(我们实现 dropout_layer 函数,
该函数以dropout的概率丢弃张量输入X中的元素
),
如上所述重新缩放剩余部分:将剩余部分除以1.0-dropout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch  
from torch import nn
from d2l import torch as d2l


def dropout_layer(X, dropout):
assert 0 <= dropout <= 1
# 在本情况中,所有元素都被丢弃
if dropout == 1:
return torch.zeros_like(X)
# 在本情况中,所有元素都被保留
if dropout == 0:
return X
mask = (torch.rand(X.shape) > dropout).float()
return mask * X / (1.0 - dropout)

我们可以通过下面几个例子来[测试dropout_layer函数]。
我们将输入X通过暂退法操作,暂退概率分别为0、0.5和1。

1
2
3
4
5
X= torch.arange(16, dtype = torch.float32).reshape((2, 8))  
print(X)
print(dropout_layer(X, 0.))
print(dropout_layer(X, 0.5))
print(dropout_layer(X, 1.))
1
2
3
4
5
6
7
8
9
tensor([[ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.],
[ 8., 9., 10., 11., 12., 13., 14., 15.]])
tensor([[ 0., 1., 2., 3., 4., 5., 6., 7.],
[ 8., 9., 10., 11., 12., 13., 14., 15.]])
tensor([[ 0., 0., 4., 6., 0., 0., 0., 14.],
[16., 18., 0., 22., 0., 0., 28., 0.]])
tensor([[0., 0., 0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0., 0., 0.]])

定义模型参数

同样,我们使用 :numref:sec_fashion_mnist中引入的Fashion-MNIST数据集。
我们[定义具有两个隐藏层的多层感知机,每个隐藏层包含256个单元]。

1
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256

定义模型

我们可以将暂退法应用于每个隐藏层的输出(在激活函数之后),
并且可以为每一层分别设置暂退概率:
常见的技巧是在靠近输入层的地方设置较低的暂退概率。
下面的模型将第一个和第二个隐藏层的暂退概率分别设置为0.2和0.5,
并且暂退法只在训练期间有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
dropout1, dropout2 = 0.2, 0.5  

class Net(nn.Module):
def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2,
is_training = True):
super(Net, self).__init__()
self.num_inputs = num_inputs
self.training = is_training
self.lin1 = nn.Linear(num_inputs, num_hiddens1)
self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
self.lin3 = nn.Linear(num_hiddens2, num_outputs)
self.relu = nn.ReLU()

def forward(self, X):
H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))
# 只有在训练模型时才使用dropout
if self.training == True:
# 在第一个全连接层之后添加一个dropout层
H1 = dropout_layer(H1, dropout1)
H2 = self.relu(self.lin2(H1))
if self.training == True:
# 在第二个全连接层之后添加一个dropout层
H2 = dropout_layer(H2, dropout2)
out = self.lin3(H2)
return out


net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)

[训练和测试]

这类似于前面描述的多层感知机训练和测试。

1
2
3
4
5
num_epochs, lr, batch_size = 10, 0.5, 256  
loss = nn.CrossEntropyLoss(reduction='none')
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

[简洁实现]

对于深度学习框架的高级API,我们只需在每个全连接层之后添加一个Dropout层,
将暂退概率作为唯一的参数传递给它的构造函数。
在训练时,Dropout层将根据指定的暂退概率随机丢弃上一层的输出(相当于下一层的输入)。
在测试时,Dropout层仅传递数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
net = nn.Sequential(nn.Flatten(),  
nn.Linear(784, 256),
nn.ReLU(),
# 在第一个全连接层之后添加一个dropout层
nn.Dropout(dropout1),
nn.Linear(256, 256),
nn.ReLU(),
# 在第二个全连接层之后添加一个dropout层
nn.Dropout(dropout2),
nn.Linear(256, 10))

def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);

接下来,我们[对模型进行训练和测试]。

1
2
trainer = torch.optim.SGD(net.parameters(), lr=lr)  
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

小结

  • 暂退法在前向传播过程中,计算每一内部层的同时丢弃一些神经元。
  • 暂退法可以避免过拟合,它通常与控制权重向量的维数和大小结合使用的。
  • 暂退法将活性值替换为具有期望值的随机变量。
  • 暂退法仅在训练期间使用。

前向传播、反向传播和计算图


:label:sec_backprop

我们已经学习了如何用小批量随机梯度下降训练模型。
然而当实现该算法时,我们只考虑了通过前向传播(forward propagation)所涉及的计算。
在计算梯度时,我们只调用了深度学习框架提供的反向传播函数,而不知其所以然。

梯度的自动计算(自动微分)大大简化了深度学习算法的实现。
在自动微分之前,即使是对复杂模型的微小调整也需要手工重新计算复杂的导数,
学术论文也不得不分配大量页面来推导更新规则。
本节将通过一些基本的数学和计算图,
深入探讨反向传播的细节。
首先,我们将重点放在带权重衰减(正则化)的单隐藏层多层感知机上。

前向传播

前向传播(forward propagation或forward pass)
指的是:按顺序(从输入层到输出层)计算和存储神经网络中每层的结果

我们将一步步研究单隐藏层神经网络的机制,
为了简单起见,我们假设输入样本是
并且我们的隐藏层不包括偏置项。
这里的中间变量是:

其中
是隐藏层的权重参数。
将中间变量通过激活函数后,
我们得到长度为的隐藏激活向量:

隐藏变量也是一个中间变量。
假设输出层的参数只有权重
我们可以得到输出层变量,它是一个长度为的向量:

假设损失函数为,样本标签为,我们可以计算单个数据样本的损失项,

根据正则化的定义,给定超参数,正则化项为


:eqlabel:eq_forward-s

其中矩阵的Frobenius范数是将矩阵展平为向量后应用的范数。
最后,模型在给定数据样本上的正则化损失为:

在下面的讨论中,我们将称为目标函数(objective function)。

前向传播计算图

绘制计算图有助于我们可视化计算中操作符和变量的依赖关系。
:numref:fig_forward 是与上述简单网络相对应的计算图,
其中正方形表示变量,圆圈表示操作符。
左下角表示输入,右上角表示输出。
注意显示数据流的箭头方向主要是向右和向上的。

:label:`fig_forward`

## 反向传播

反向传播(backward propagation或backpropagation)指的是计算神经网络参数梯度的方法
简言之,该方法根据微积分中的链式规则,按相反的顺序从输出层到输入层遍历网络。
该算法存储了计算某些参数梯度时所需的任何中间变量(偏导数)
假设我们有函数
其中输入和输出是任意形状的张量。
利用链式法则,我们可以计算关于的导数

在这里,我们使用运算符在执行必要的操作(如换位和交换输入位置)后将其参数相乘。
对于向量,这很简单,它只是矩阵-矩阵乘法。
对于高维张量,我们使用适当的对应项。
运算符指代了所有的这些符号。

回想一下,在计算图 :numref:fig_forward中的单隐藏层简单网络的参数是

反向传播的目的是计算梯度

为此,我们应用链式法则,依次计算每个中间变量和参数的梯度。
计算的顺序与前向传播中执行的顺序相反,因为我们需要从计算图的结果开始,并朝着参数的方向努力。第一步是计算目标函数相对于损失项和正则项的梯度。

接下来,我们根据链式法则计算目标函数关于输出层变量的梯度:

接下来,我们计算正则化项相对于两个参数的梯度:

现在我们可以计算最接近输出层的模型参数的梯度

使用链式法则得出:


:eqlabel:eq_backprop-J-h

为了获得关于的梯度,我们需要继续沿着输出层到隐藏层反向传播。
关于隐藏层输出的梯度由下式给出:

由于激活函数是按元素计算的,
计算中间变量的梯度
需要使用按元素乘法运算符,我们用表示:

最后,我们可以得到最接近输入层的模型参数的梯度

根据链式法则,我们得到:

训练神经网络

在训练神经网络时,前向传播和反向传播相互依赖。
对于前向传播,我们沿着依赖的方向遍历计算图并计算其路径上的所有变量。
然后将这些用于反向传播,其中计算顺序与计算图的相反。

以上述简单网络为例:一方面,在前向传播期间计算正则项
:eqref:eq_forward-s取决于模型参数的当前值。
它们是由优化算法根据最近迭代的反向传播给出的。
另一方面,反向传播期间参数 :eqref:eq_backprop-J-h的梯度计算,
取决于由前向传播给出的隐藏变量的当前值。

因此,在训练神经网络时,在初始化模型参数后,
我们交替使用前向传播和反向传播,利用反向传播给出的梯度来更新模型参数。
注意,反向传播重复利用前向传播中存储的中间值,以避免重复计算
带来的影响之一是我们需要保留中间值,直到反向传播完成
这也是训练比单纯的预测需要更多的内存(显存)的原因之一
此外,这些中间值的大小与网络层的数量和批量的大小大致成正比
因此,使用更大的批量来训练更深层次的网络更容易导致内存不足(out of memory)错误

小结

  • 前向传播在神经网络定义的计算图中按顺序计算和存储中间变量,它的顺序是从输入层到输出层。
  • 反向传播按相反的顺序(从输出层到输入层)计算和存储神经网络的中间变量和参数的梯度。
  • 在训练深度学习模型时,前向传播和反向传播是相互依赖的。
  • 训练比预测需要更多的内存。

数值稳定性和模型初始化


:label:sec_numerical_stability

到目前为止,我们实现的每个模型都是根据某个预先指定的分布来初始化模型的参数。
有人会认为初始化方案是理所当然的,忽略了如何做出这些选择的细节。甚至有人可能会觉得,初始化方案的选择并不是特别重要。
相反,初始化方案的选择在神经网络学习中起着举足轻重的作用
它对保持数值稳定性至关重要。
此外,这些初始化方案的选择可以与非线性激活函数的选择有趣的结合在一起。
我们选择哪个函数以及如何初始化参数可以决定优化算法收敛的速度有多快
糟糕选择可能会导致我们在训练时遇到梯度爆炸或梯度消失。
本节将更详细地探讨这些主题,并讨论一些有用的启发式方法。
这些启发式方法在整个深度学习生涯中都很有用。

梯度消失和梯度爆炸

考虑一个具有层、输入和输出的深层网络。
每一层由变换定义,
该变换的参数为权重
其隐藏变量是(令 )。
我们的网络可以表示为:

如果所有隐藏变量和输入都是向量,
我们可以将关于任何一组参数的梯度写为下式:

$$\partial_{\mathbf{W}^{(l)}} \mathbf{o} = \underbrace{\partial_{\mathbf{h}^{(L-1)}} \mathbf{h}^{(L)}}{ \mathbf{M}^{(L)} \stackrel{\mathrm{def}}{=}} \cdot \ldots \cdot \underbrace{\partial{\mathbf{h}^{(l)}} \mathbf{h}^{(l+1)}}{ \mathbf{M}^{(l+1)} \stackrel{\mathrm{def}}{=}} \underbrace{\partial{\mathbf{W}^{(l)}} \mathbf{h}^{(l)}}_{ \mathbf{v}^{(l)} \stackrel{\mathrm{def}}{=}}.$$

换言之,该梯度是个矩阵

与梯度向量 的乘积。
因此,我们容易受到数值下溢问题的影响.
当将太多的概率乘在一起时,这些问题经常会出现。
在处理概率时,一个常见的技巧是切换到对数空间,
即将数值表示的压力从尾数转移到指数。
不幸的是,上面的问题更为严重:
最初,矩阵 可能具有各种各样的特征值。
他们可能很小,也可能很大;
他们的乘积可能非常大,也可能非常小。

不稳定梯度带来的风险不止在于数值表示;
不稳定梯度也威胁到我们优化算法的稳定性。
我们可能面临一些问题。
要么是梯度爆炸(gradient exploding)问题:
参数更新过大,破坏了模型的稳定收敛
要么是梯度消失(gradient vanishing)问题:
参数更新过小,在每次更新时几乎不会移动,导致模型无法学习

(梯度消失)

曾经sigmoid函数( :numref:sec_mlp提到过)很流行,
因为它类似于阈值函数。
由于早期的人工神经网络受到生物神经网络的启发,
神经元要么完全激活要么完全不激活(就像生物神经元)的想法很有吸引力。
然而,它却是导致梯度消失问题的一个常见的原因,
让我们仔细看看sigmoid函数为什么会导致梯度消失。

1
2
3
4
5
6
7
8
9
10
%matplotlib inline  
import torch
from d2l import torch as d2l

x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.sigmoid(x)
y.backward(torch.ones_like(x))

d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()],
legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))

正如上图,当sigmoid函数的输入很大或是很小时,它的梯度都会消失。
此外,当反向传播通过许多层时,除非我们在刚刚好的地方,
这些地方sigmoid函数的输入接近于零,否则整个乘积的梯度可能会消失。
当我们的网络有很多层时,除非我们很小心,否则在某一层可能会切断梯度。
事实上,这个问题曾经困扰着深度网络的训练。
因此,更稳定的ReLU系列函数已经成为从业者的默认选择(虽然在神经科学的角度看起来不太合理)。

[梯度爆炸]

相反,梯度爆炸可能同样令人烦恼。
为了更好地说明这一点,我们生成100个高斯随机矩阵,并将它们与某个初始矩阵相乘。
对于我们选择的尺度(方差),矩阵乘积发生爆炸。
当这种情况是由于深度网络的初始化所导致时,我们没有机会让梯度下降优化器收敛

1
2
3
4
5
6
M = torch.normal(0, 1, size=(4,4))  
print('一个矩阵 \n',M)
for i in range(100):
M = torch.mm(M,torch.normal(0, 1, size=(4, 4)))

print('乘以100个矩阵后\n', M)
1
2
3
4
5
6
7
8
9
10
11
一个矩阵 
tensor([[-1.4328, 0.4653, 1.3210, 0.8762],
[-0.9540, -1.9165, 1.6366, 0.8213],
[ 0.0505, -1.4785, -0.5515, -0.3483],
[-0.1134, 0.6420, 1.9380, 0.5762]])
乘以100个矩阵后
tensor([[-1.1900e+27, -1.0653e+27, 5.8650e+26, 1.8106e+26],
[-1.7044e+27, -1.5258e+27, 8.4003e+26, 2.5933e+26],
[ 4.2333e+26, 3.7897e+26, -2.0864e+26, -6.4408e+25],
[-2.2590e+27, -2.0222e+27, 1.1133e+27, 3.4370e+26]])

打破对称性

神经网络设计中的另一个问题是其参数化所固有的对称性。
假设我们有一个简单的多层感知机,它有一个隐藏层和两个隐藏单元。
在这种情况下,我们可以对第一层的权重进行重排列,
并且同样对输出层的权重进行重排列,可以获得相同的函数。
第一个隐藏单元与第二个隐藏单元没有什么特别的区别。
换句话说,我们在每一层的隐藏单元之间具有排列对称性。

假设输出层将上述两个隐藏单元的多层感知机转换为仅一个输出单元。
想象一下,如果我们将隐藏层的所有参数初始化为
为常量,会发生什么?
在这种情况下,在前向传播期间,两个隐藏单元采用相同的输入和参数,
产生相同的激活,该激活被送到输出单元。
在反向传播期间,根据参数对输出单元进行微分,
得到一个梯度,其元素都取相同的值。
因此,在基于梯度的迭代(例如,小批量随机梯度下降)之后,
的所有元素仍然采用相同的值。
这样的迭代永远不会打破对称性,我们可能永远也无法实现网络的表达能力。
隐藏层的行为就好像只有一个单元。
请注意,虽然小批量随机梯度下降不会打破这种对称性,但暂退法正则化可以。

参数初始化

解决(或至少减轻)上述问题的一种方法是进行参数初始化,
优化期间的注意和适当的正则化也可以进一步提高稳定性。

默认初始化

在前面的部分中,例如在 :numref:sec_linear_concise中,
我们使用正态分布来初始化权重值。如果我们不指定初始化方法,
框架将使用默认的随机初始化方法,对于中等难度的问题,这种方法通常很有效。

Xavier初始化

:label:subsec_xavier

让我们看看某些没有非线性的全连接层输出(例如,隐藏变量)的尺度分布。
对于该层输入及其相关权重,输出由下式给出

权重都是从同一分布中独立抽取的。
此外,让我们假设该分布具有零均值和方差
请注意,这并不意味着分布必须是高斯的,只是均值和方差需要存在。
现在,让我们假设层的输入也具有零均值和方差
并且它们独立于并且彼此独立。
在这种情况下,我们可以按如下方式计算的平均值和方差:

保持方差不变的一种方法是设置
现在考虑反向传播过程,我们面临着类似的问题,尽管梯度是从更靠近输出的层传播的。
使用与前向传播相同的推断,我们可以看到,除非
否则梯度的方差可能会增大,其中是该层的输出的数量。
这使得我们进退两难:我们不可能同时满足这两个条件。
相反,我们只需满足:

这就是现在标准且实用的Xavier初始化的基础,
它以其提出者 :cite:Glorot.Bengio.2010 第一作者的名字命名。
通常,Xavier初始化从均值为零,方差

的高斯分布中采样权重。
我们也可以将其改为选择从均匀分布中抽取权重时的方差。
注意均匀分布的方差为
代入到的条件中,将得到初始化值域:

尽管在上述数学推理中,“不存在非线性”的假设在神经网络中很容易被违反,
但Xavier初始化方法在实践中被证明是有效的。

额外阅读

上面的推理仅仅触及了现代参数初始化方法的皮毛。
深度学习框架通常实现十几种不同的启发式方法。
此外,参数初始化一直是深度学习基础研究的热点领域。
其中包括专门用于参数绑定(共享)、超分辨率、序列模型和其他情况的启发式算法。
例如,Xiao等人演示了通过使用精心设计的初始化方法
:cite:Xiao.Bahri.Sohl-Dickstein.ea.2018
可以无须架构上的技巧而训练10000层神经网络的可能性。

小结

  • 梯度消失和梯度爆炸是深度网络中常见的问题。在参数初始化时需要非常小心,以确保梯度和参数可以得到很好的控制。
  • 需要用启发式的初始化方法来确保初始梯度既不太大也不太小。
  • ReLU激活函数缓解了梯度消失问题,这样可以加速收敛。
  • 随机初始化是保证在进行优化前打破对称性的关键。
  • Xavier初始化表明,对于每一层,输出的方差不受输入数量的影响,任何梯度的方差不受输出数量的影响。

练习

  1. 除了多层感知机的排列对称性之外,还能设计出其他神经网络可能会表现出对称性且需要被打破的情况吗?
  2. 我们是否可以将线性回归或softmax回归中的所有权重参数初始化为相同的值?
  3. 在相关资料中查找两个矩阵乘积特征值的解析界。这对确保梯度条件合适有什么启示?
  4. 如果我们知道某些项是发散的,我们能在事后修正吗?看看关于按层自适应速率缩放的论文 :cite:You.Gitman.Ginsburg.2017

环境和分布偏移


总而言之,机器学习的许多应用中都存在类似的问题:
通过将基于模型的决策引入环境,我们可能会破坏模型。

分布偏移的类型

首先,我们考虑数据分布可能发生变化的各种方式,以及为挽救模型性能可能采取的措施。
在一个经典的情景中,假设训练数据是从某个分布中采样的,
但是测试数据将包含从不同分布中抽取的未标记样本。
一个清醒的现实是:如果没有任何关于之间相互关系的假设,
学习到一个分类器是不可能的。

考虑一个二元分类问题:区分狗和猫。
如果分布可以以任意方式偏移,那么我们的情景允许病态的情况,
即输入的分布保持不变:
但标签全部翻转:
换言之,如果将来所有的“猫”现在都是狗,而我们以前所说的“狗”现在是猫。
而此时输入的分布没有任何改变,
那么我们就不可能将这种情景与分布完全没有变化的情景区分开。

幸运的是,在对未来我们的数据可能发生变化的一些限制性假设下,
有些算法可以检测这种偏移,甚至可以动态调整,提高原始分类器的精度。

协变量偏移

在不同分布偏移中,协变量偏移可能是最为广泛研究的。
这里我们假设:虽然输入的分布可能随时间而改变,
但标签函数(即条件分布)没有改变。
统计学家称之为协变量偏移(covariate shift),
因为这个问题是由于协变量(特征)分布的变化而产生的。
虽然有时我们可以在不引用因果关系的情况下对分布偏移进行推断,
但在我们认为导致的情况下,协变量偏移是一种自然假设。

考虑一下区分猫和狗的问题:训练数据包括 :numref:fig_cat-dog-train中的图像。

:label:fig_cat-dog-train

在测试时,我们被要求对 :numref:fig_cat-dog-test中的图像进行分类。

区分猫和狗的测试数据
:label:fig_cat-dog-test

训练集由真实照片组成,而测试集只包含卡通图片。
假设在一个与测试集的特征有着本质不同的数据集上进行训练,
如果没有方法来适应新的领域,可能会有麻烦。

标签偏移

标签偏移(label shift)描述了与协变量偏移相反的问题。
这里我们假设标签边缘概率可以改变,
但是类别条件分布在不同的领域之间保持不变。
当我们认为导致时,标签偏移是一个合理的假设。
例如,预测患者的疾病,我们可能根据症状来判断,
即使疾病的相对流行率随着时间的推移而变化。
标签偏移在这里是恰当的假设,因为疾病会引起症状。
在另一些情况下,标签偏移和协变量偏移假设可以同时成立。
例如,当标签是确定的,即使导致,协变量偏移假设也会得到满足。
有趣的是,在这些情况下,使用基于标签偏移假设的方法通常是有利的。
这是因为这些方法倾向于包含看起来像标签(通常是低维)的对象,
而不是像输入(通常是高维的)对象。

概念偏移

我们也可能会遇到概念偏移(concept shift):
当标签的定义发生变化时,就会出现这种问题。
这听起来很奇怪——一只猫就是一只猫,不是吗?
然而,其他类别会随着不同时间的用法而发生变化。
精神疾病的诊断标准、所谓的时髦、以及工作头衔等等,都是概念偏移的日常映射。
事实证明,假如我们环游美国,根据所在的地理位置改变我们的数据来源,
我们会发现关于“软饮”名称的分布发生了相当大的概念偏移,
如 :numref:fig_popvssoda 所示。

美国软饮名称的概念偏移
:width:400px
:label:fig_popvssoda

如果我们要建立一个机器翻译系统,
的分布可能会因我们的位置不同而得到不同的翻译。
这个问题可能很难被发现。
所以,我们最好可以利用在时间或空间上逐渐发生偏移的知识。

分布偏移示例

在深入研究形式体系和算法之前,我们可以讨论一些协变量偏移或概念偏移可能并不明显的具体情况。

医学诊断

假设我们想设计一个检测癌症的算法,从健康人和病人那里收集数据,然后训练算法。
它工作得很好,有很高的精度,然后我们得出了已经准备好在医疗诊断上取得成功的结论。
请先别着急。

收集训练数据的分布和在实际中遇到的数据分布可能有很大的不同。
这件事在一个不幸的初创公司身上发生过,我们中的一些作者几年前和他们合作过。
他们正在研究一种血液检测方法,主要针对一种影响老年男性的疾病,
并希望利用他们从病人身上采集的血液样本进行研究。
然而,从健康男性身上获取血样比从系统中已有的病人身上获取要困难得多。
作为补偿,这家初创公司向一所大学校园内的学生征集献血,作为开发测试的健康对照样本。
然后这家初创公司问我们是否可以帮助他们建立一个用于检测疾病的分类器。

正如我们向他们解释的那样,用近乎完美的精度来区分健康和患病人群确实很容易。
然而,这可能是因为受试者在年龄、激素水平、体力活动、
饮食、饮酒以及其他许多与疾病无关的因素上存在差异。
这对检测疾病的分类器可能并不适用。
这些抽样可能会遇到极端的协变量偏移。
此外,这种情况不太可能通过常规方法加以纠正。
简言之,他们浪费了一大笔钱。

自动驾驶汽车

对于一家想利用机器学习来开发自动驾驶汽车的公司,一个关键部件是“路沿检测器”。
由于真实的注释数据获取成本很高,他们想出了一个“聪明”的想法:
将游戏渲染引擎中的合成数据用作额外的训练数据。
这对从渲染引擎中抽取的“测试数据”非常有效,但应用在一辆真正的汽车里真是一场灾难。
正如事实证明的那样,路沿被渲染成一种非常简单的纹理。
更重要的是,所有的路沿都被渲染成了相同的纹理,路沿检测器很快就学习到了这个“特征”。

当美军第一次试图在森林中探测坦克时,也发生了类似的事情。
他们在没有坦克的情况下拍摄了森林的航拍照片,然后把坦克开进森林,拍摄了另一组照片。
使用这两组数据训练的分类器似乎工作得很好。
不幸的是,分类器仅仅学会了如何区分有阴影的树和没有阴影的树:
第一组照片是在清晨拍摄的,而第二组是在中午拍摄的。

非平稳分布

当分布变化缓慢并且模型没有得到充分更新时,就会出现更微妙的情况:
非平稳分布(nonstationary distribution)。
以下是一些典型例子:

  • 训练一个计算广告模型,但却没有经常更新(例如,一个2009年训练的模型不知道一个叫iPad的不知名新设备刚刚上市);
  • 建立一个垃圾邮件过滤器,它能很好地检测到所有垃圾邮件。但是,垃圾邮件发送者们变得聪明起来,制造出新的信息,看起来不像我们以前见过的任何垃圾邮件;
  • 建立一个产品推荐系统,它在整个冬天都有效,但圣诞节过后很久还会继续推荐圣诞帽。

更多轶事

  • 建立一个人脸检测器,它在所有基准测试中都能很好地工作,但是它在测试数据上失败了:有问题的例子是人脸充满了整个图像的特写镜头(训练集中没有这样的数据)。
  • 为美国市场建立了一个网络搜索引擎,并希望将其部署到英国。
  • 通过在一个大的数据集来训练图像分类器,其中每一个大类的数量在数据集近乎是平均的,比如1000个类别,每个类别由1000个图像表示。但是将该系统部署到真实世界中,照片的实际标签分布显然是不均匀的。

分布偏移纠正

正如我们所讨论的,在许多情况下训练和测试分布是不同的。
在一些情况下,我们很幸运,不管协变量、标签或概念如何发生偏移,模型都能正常工作。
在另一些情况下,我们可以通过运用策略来应对这种偏移,从而做得更好。
本节的其余部分将着重于应对这种偏移的技术细节。

经验风险与实际风险

:label:subsec_empirical-risk-and-risk

首先我们反思一下在模型训练期间到底发生了什么?
训练数据
的特征和相关的标签经过迭代,在每一个小批量之后更新模型的参数。
为了简单起见,我们不考虑正则化,因此极大地降低了训练损失:

$$\mathop{\mathrm{minimize}}f \frac{1}{n} \sum{i=1}^n l(f(\mathbf{x}_i), y_i),$$
:eqlabel:eq_empirical-risk-min

其中是损失函数,用来度量:
给定标签,预测的“糟糕程度”。
统计学家称 :eqref:eq_empirical-risk-min中的这一项为经验风险。
经验风险(empirical risk)是为了近似 真实风险(true risk),
整个训练数据上的平均损失,即从其真实分布
抽取的所有数据的总体损失的期望值:


:eqlabel:eq_true-risk

然而在实践中,我们通常无法获得总体数据。
因此,经验风险最小化即在 :eqref:eq_empirical-risk-min中最小化经验风险,
是一种实用的机器学习策略,希望能近似最小化真实风险。

协变量偏移纠正

:label:subsec_covariate-shift-correction

假设对于带标签的数据
我们要评估
然而观测值是从某些源分布中得出的,
而不是从目标分布中得出的。
幸运的是,依赖性假设意味着条件分布保持不变,即:

如果源分布是“错误的”,
我们可以通过在真实风险的计算中,使用以下简单的恒等式来进行纠正:

换句话说,我们需要根据数据来自正确分布与来自错误分布的概率之比,
来重新衡量每个数据样本的权重:

将权重代入到每个数据样本中,
我们可以使用”加权经验风险最小化“来训练模型:

$$\mathop{\mathrm{minimize}}f \frac{1}{n} \sum{i=1}^n \beta_i l(f(\mathbf{x}_i), y_i).$$
:eqlabel:eq_weighted-empirical-risk-min

由于不知道这个比率,我们需要估计它。
有许多方法都可以用,包括一些花哨的算子理论方法,
试图直接使用最小范数或最大熵原理重新校准期望算子。
对于任意一种这样的方法,我们都需要从两个分布中抽取样本:
“真实”的分布,通过访问测试数据获取;
训练集,通过人工合成的很容易获得。
请注意,我们只需要特征
不需要访问标签

在这种情况下,有一种非常有效的方法可以得到几乎与原始方法一样好的结果:
对数几率回归(logistic regression)。
这是用于二元分类的softmax回归(见 :numref:sec_softmax)的一个特例。
综上所述,我们学习了一个分类器来区分从抽取的数据
和从抽取的数据。
如果无法区分这两个分布,则意味着相关的样本可能来自这两个分布中的任何一个。
另一方面,任何可以很好区分的样本都应该相应地显著增加或减少权重。

为了简单起见,假设我们分别从
两个分布中抽取相同数量的样本。
现在用标签表示:从抽取的数据为,从抽取的数据为
然后,混合数据集中的概率由下式给出

因此,如果我们使用对数几率回归方法,其中

是一个参数化函数),则很自然有:

因此,我们需要解决两个问题:
第一个问题是关于区分来自两个分布的数据;
第二个问题是关于 :eqref:eq_weighted-empirical-risk-min
中的加权经验风险的最小化问题。
在这个问题中,我们将对其中的项加权

现在,我们来看一下完整的协变量偏移纠正算法。
假设我们有一个训练集
和一个未标记的测试集
对于协变量偏移,我们假设来自某个源分布,
来自目标分布。
以下是纠正协变量偏移的典型算法:

  1. 生成一个二元分类训练集:
  2. 用对数几率回归训练二元分类器得到函数
  3. 使用或更好的为常量)对训练数据进行加权。
  4. 使用权重进行 :eqref:eq_weighted-empirical-risk-min的训练。

请注意,上述算法依赖于一个重要的假设:
需要目标分布(例如,测试分布)中的每个数据样本在训练时出现的概率非零。
如果我们找到的点,
那么相应的重要性权重会是无穷大。

标签偏移纠正

假设我们处理的是个类别的分类任务。
使用 :numref:subsec_covariate-shift-correction中相同符号,
中分别是源分布(例如训练时的分布)和目标分布(例如测试时的分布)。
假设标签的分布随时间变化:
但类别条件分布保持不变:
如果源分布是“错误的”,
我们可以根据 :eqref:eq_true-risk中定义的真实风险中的恒等式进行更正:

这里,重要性权重将对应于标签似然比率

标签偏移的一个好处是,如果我们在源分布上有一个相当好的模型,
那么我们可以得到对这些权重的一致估计,而不需要处理周边的其他维度。
在深度学习中,输入往往是高维对象(如图像),而标签通常是低维(如类别)。

为了估计目标标签分布,我们首先采用性能相当好的现成的分类器(通常基于训练数据进行训练),
并使用验证集(也来自训练分布)计算其混淆矩阵。
混淆矩阵是一个矩阵,
其中每列对应于标签类别,每行对应于模型的预测类别。
每个单元格的值是验证集中,真实标签为
而我们的模型预测为的样本数量所占的比例。

现在,我们不能直接计算目标数据上的混淆矩阵,
因为我们无法看到真实环境下的样本的标签,
除非我们再搭建一个复杂的实时标注流程。
然而,我们所能做的是将所有模型在测试时的预测取平均数,
得到平均模型输出
其中第个元素是我们模型预测测试集中的总预测分数。

结果表明,如果我们的分类器一开始就相当准确,
并且目标数据只包含我们以前见过的类别,
以及如果标签偏移假设成立(这里最强的假设),
我们就可以通过求解一个简单的线性系统来估计测试集的标签分布

因为作为一个估计,
对所有成立,
其中维标签分布向量的第元素。
如果我们的分类器一开始就足够精确,那么混淆矩阵将是可逆的,
进而我们可以得到一个解

因为我们观测源数据上的标签,所以很容易估计分布
那么对于标签为的任何训练样本
我们可以使用我们估计的比率来计算权重
并将其代入 :eqref:eq_weighted-empirical-risk-min中的加权经验风险最小化中。

概念偏移纠正

概念偏移很难用原则性的方式解决。
例如,在一个问题突然从“区分猫和狗”偏移为“区分白色和黑色动物”的情况下,
除了从零开始收集新标签和训练,别无妙方。
幸运的是,在实践中这种极端的偏移是罕见的。
相反,通常情况下,概念的变化总是缓慢的。
比如下面是一些例子:

  • 在计算广告中,新产品推出后,旧产品变得不那么受欢迎了。这意味着广告的分布和受欢迎程度是逐渐变化的,任何点击率预测器都需要随之逐渐变化;
  • 由于环境的磨损,交通摄像头的镜头会逐渐退化,影响摄像头的图像质量;
  • 新闻内容逐渐变化(即新新闻的出现)。

在这种情况下,我们可以使用与训练网络相同的方法,使其适应数据的变化。
换言之,我们使用新数据更新现有的网络权重,而不是从头开始训练。

学习问题的分类法

有了如何处理分布变化的知识,我们现在可以考虑机器学习问题形式化的其他方面。

批量学习

批量学习(batch learning)中,我们可以访问一组训练特征和标签

我们使用这些特性和标签训练
然后,我们部署此模型来对来自同一分布的新数据进行评分。
例如,我们可以根据猫和狗的大量图片训练猫检测器。
一旦我们训练了它,我们就把它作为智能猫门计算视觉系统的一部分,来控制只允许猫进入。
然后这个系统会被安装在客户家中,基本再也不会更新。

在线学习

除了“批量”地学习,我们还可以单个“在线”学习数据
更具体地说,我们首先观测到
然后我们得出一个估计值
只有当我们做到这一点后,我们才观测到
然后根据我们的决定,我们会得到奖励或损失。
许多实际问题都属于这一类。
例如,我们需要预测明天的股票价格,
这样我们就可以根据这个预测进行交易。
在一天结束时,我们会评估我们的预测是否盈利。
换句话说,在在线学习(online learning)中,我们有以下的循环。
在这个循环中,给定新的观测结果,我们会不断地改进我们的模型。

$$
\mathrm{model} ~ f_t \longrightarrow
\mathrm{data} ~ \mathbf{x}_t \longrightarrow
\mathrm{estimate} ~ f_t(\mathbf{x}_t) \longrightarrow
\mathrm{observation} ~ y_t \longrightarrow
\mathrm{loss} ~ l(y_t, f_t(\mathbf{x}t)) \longrightarrow
\mathrm{model} ~ f
{t+1}
$$

老虎机

老虎机(bandits)是上述问题的一个特例。
虽然在大多数学习问题中,我们有一个连续参数化的函数(例如,一个深度网络)。
但在一个老虎机问题中,我们只有有限数量的手臂可以拉动。
也就是说,我们可以采取的行动是有限的。
对于这个更简单的问题,可以获得更强的最优性理论保证,这并不令人惊讶。
我们之所以列出它,主要是因为这个问题经常被视为一个单独的学习问题的情景。

控制

在很多情况下,环境会记住我们所做的事。
不一定是以一种对抗的方式,但它会记住,而且它的反应将取决于之前发生的事情。
例如,咖啡锅炉控制器将根据之前是否加热锅炉来观测到不同的温度。
在这种情况下,PID(比例—积分—微分)控制器算法是一个流行的选择。
同样,一个用户在新闻网站上的行为将取决于之前向她展示的内容(例如,大多数新闻她只阅读一次)。
许多这样的算法形成了一个环境模型,在这个模型中,他们的行为使得他们的决策看起来不那么随机。
近年来,控制理论(如PID的变体)也被用于自动调整超参数,
以获得更好的解构和重建质量,提高生成文本的多样性和生成图像的重建质量
:cite:Shao.Yao.Sun.ea.2020

强化学习

强化学习(reinforcement learning)强调如何基于环境而行动,以取得最大化的预期利益。
国际象棋、围棋、西洋双陆棋或星际争霸都是强化学习的应用实例。
再比如,为自动驾驶汽车制造一个控制器,或者以其他方式对自动驾驶汽车的驾驶方式做出反应
(例如,试图避开某物体,试图造成事故,或者试图与其合作)。

考虑到环境

上述不同情况之间的一个关键区别是:
在静止环境中可能一直有效的相同策略,
在环境能够改变的情况下可能不会始终有效。
例如,一个交易者发现的套利机会很可能在他开始利用它时就消失了。
环境变化的速度和方式在很大程度上决定了我们可以采用的算法类型。
例如,如果我们知道事情只会缓慢地变化,
就可以迫使任何估计也只能缓慢地发生改变。
如果我们知道环境可能会瞬间发生变化,但这种变化非常罕见,
我们就可以在使用算法时考虑到这一点。
当一个数据科学家试图解决的问题会随着时间的推移而发生变化时,
这些类型的知识至关重要。

机器学习中的公平、责任和透明度

最后,重要的是,当我们部署机器学习系统时,
不仅仅是在优化一个预测模型,
而通常是在提供一个会被用来(部分或完全)进行自动化决策的工具。
这些技术系统可能会通过其进行的决定而影响到每个人的生活。

从考虑预测到决策的飞跃不仅提出了新的技术问题,
而且还提出了一系列必须仔细考虑的伦理问题。
如果我们正在部署一个医疗诊断系统,我们需要知道它可能适用于哪些人群,哪些人群可能无效。
忽视对一个亚群体的幸福的可预见风险可能会导致我们执行劣质的护理水平。
此外,一旦我们规划整个决策系统,我们必须退后一步,重新考虑如何评估我们的技术。
在这个视野变化所导致的结果中,我们会发现精度很少成为合适的衡量标准。
例如,当我们将预测转化为行动时,我们通常会考虑到各种方式犯错的潜在成本敏感性。
举个例子:将图像错误地分到某一类别可能被视为种族歧视,而错误地分到另一个类别是无害的,
那么我们可能需要相应地调整我们的阈值,在设计决策方式时考虑到这些社会价值。
我们还需要注意预测系统如何导致反馈循环。
例如,考虑预测性警务系统,它将巡逻人员分配到预测犯罪率较高的地区。
很容易看出一种令人担忧的模式是如何出现的:

  1. 犯罪率高的社区会得到更多的巡逻;
  2. 因此,在这些社区中会发现更多的犯罪行为,输入可用于未来迭代的训练数据;
  3. 面对更多的积极因素,该模型预测这些社区还会有更多的犯罪;
  4. 下一次迭代中,更新后的模型会更加倾向于针对同一个地区,这会导致更多的犯罪行为被发现等等。

通常,在建模纠正过程中,模型的预测与训练数据耦合的各种机制都没有得到解释,
研究人员称之为“失控反馈循环”的现象。
此外,我们首先要注意我们是否解决了正确的问题。
比如,预测算法现在在信息传播中起着巨大的中介作用,
个人看到的新闻应该由他们喜欢的Facebook页面决定吗?
这些只是在机器学习职业生涯中可能遇到的令人感到“压力山大”的道德困境中的一小部分。

小结

  • 在许多情况下,训练集和测试集并不来自同一个分布。这就是所谓的分布偏移。
  • 真实风险是从真实分布中抽取的所有数据的总体损失的预期。然而,这个数据总体通常是无法获得的。经验风险是训练数据的平均损失,用于近似真实风险。在实践中,我们进行经验风险最小化。
  • 在相应的假设条件下,可以在测试时检测并纠正协变量偏移和标签偏移。在测试时,不考虑这种偏移可能会成为问题。
  • 在某些情况下,环境可能会记住自动操作并以令人惊讶的方式做出响应。在构建模型时,我们必须考虑到这种可能性,并继续监控实时系统,并对我们的模型和环境以意想不到的方式纠缠在一起的可能性持开放态度。

练习

  1. 当我们改变搜索引擎的行为时会发生什么?用户可能会做什么?广告商呢?
  2. 实现一个协变量偏移检测器。提示:构建一个分类器。
  3. 实现协变量偏移纠正。
  4. 除了分布偏移,还有什么会影响经验风险接近真实风险的程度?