首页 > 技术知识 > 正文

【深度学习】解析神经网络中的数值稳定性、模型初始化和分布偏移

文章目录 1 概述 1.1 梯度消失和梯度爆炸 1.2 打破对称性 2 参数初始化 3 环境和分布偏移 3.1 协变量偏移 3.2 标签偏移 3.3 医学诊断(举例) 3.4 自动驾驶(举例) 4 pytorch在定义模型时有默认的参数初始化,有时候我们需要自定义参数的初始化,就需要用到torch.nn.init。具体的不同初始化,可以查看pytorch官方文档。 5 forward 的使用 1 概述

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

1.1 梯度消失和梯度爆炸

考虑一个具有$L$层、输入$\mathbf{x}$和输出$\mathbf{o}$的深层网络。每一层$l$由变换$f_l$定义,该变换的参数为权重$\mathbf{W}^{(l)}$,其隐藏变量是$\mathbf{h}^{(l)}$(令 $\mathbf{h}^{(0)} = \mathbf{x}$)。我们的网络可以表示为:

$$\mathbf{h}^{(l)} = f_l (\mathbf{h}^{(l-1)}) \text{ 因此 } \mathbf{o} = f_L \circ \ldots \circ f_1(\mathbf{x}).$$

如果所有隐藏变量和输入都是向量,我们可以将$\mathbf{o}$关于任何一组参数$\mathbf{W}^{(l)}$的梯度写为下式:

$$\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}}{=}}.$$

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

不稳定梯度带来的风险不止在于数值表示。不稳定梯度也威胁到我们优化算法的稳定性。我们可能面临一些问题。

要么是 梯度爆炸(gradient exploding)问题:参数更新过大,破坏了模型的稳定收敛;

要么是 梯度消失(gradient vanishing)问题:参数更新过小,在每次更新时几乎不会移动,导致无法学习。

(梯度消失)

导致梯度消失问题的一个常见的原因是跟在每层的线性运算之后的激活函数$\sigma$。从历史上看,sigmoid函数$1/(1 + \exp(-x))$很流行,因为它类似于阈值函数。由于早期的人工神经网络受到生物神经网络的启发,神经元要么完全激活要么完全不激活(就像生物神经元)的想法很有吸引力。让我们仔细看看sigmoid函数为什么会导致梯度消失。

%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个高斯随机矩阵,并将它们与某个初始矩阵相乘。对于我们选择的尺度(方差 𝜎2=1 ),矩阵乘积发生爆炸。当这种情况是由于深度网络的初始化所导致时,我们没有机会让梯度下降优化器收敛。

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)

一个矩阵

tensor([[-0.7288, 0.2921, 0.7565, -1.0038],

[-0.4058, 0.2981, 0.8697, 0.3928],

[ 0.6933, -0.2752, -1.1430, -0.8349],

[-0.9662, -1.8632, 0.6491, -1.4454]])

乘以100个矩阵后

tensor([[-1.5581e+26, 3.2143e+25, -5.3927e+24, -2.5211e+26],

[-1.5470e+27, 3.1913e+26, -5.3541e+25, -2.5031e+27],

[ 2.3700e+27, -4.8891e+26, 8.2025e+25, 3.8347e+27],

[-2.5288e+27, 5.2168e+26, -8.7523e+25, -4.0918e+27]])

1.2 打破对称性

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

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

2 参数初始化

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

默认初始化

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

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

3.1 协变量偏移

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

考虑一下区分猫和狗的挑战。

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

3.2 标签偏移

这里,我们假设标签边缘概率$P(y)$可以改变,但是类别条件分布$P(\mathbf{x} \mid y)$在不同的领域之间保持不变。当我们认为$y$导致$\mathbf{x}$时,标签偏移是一个合理的假设。例如,我们可能希望根据症状(或其他表现)来预测疾病,即使疾病的相对流行率随着时间的推移而变化。标签偏移在这里是恰当的假设,因为疾病会引起症状。在一些退化的情况下,标签偏移和协变量偏移假设可以同时成立。例如,当标签是确定的,即使$y$导致$\mathbf{x}$,协变量偏移假设也会得到满足。有趣的是,在这些情况下,使用基于标签偏移假设的方法通常是有利的。这是因为这些方法倾向于包含看起来像标签(通常是低维)的对象,而不是像输入的对象,后者在深度学习中往往是高维的。

3.3 医学诊断(举例)

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

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

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

3.4 自动驾驶(举例)

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

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

在许多情况下,训练集和测试集并不来自同一个分布。这就是所谓的分布偏移。 4 pytorch在定义模型时有默认的参数初始化,有时候我们需要自定义参数的初始化,就需要用到torch.nn.init。具体的不同初始化,可以查看pytorch官方文档。

方法一:

1,先定义初始化模型方法;

2,运用apply().

class Net(nn.Module): def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim): super().__init__() self.layer = nn.Sequential( nn.Linear(in_dim, n_hidden_1), nn.ReLU(True), nn.Linear(n_hidden_1, n_hidden_2), nn.ReLU(True), nn.Linear(n_hidden_2, out_dim) ) def forward(self, x): x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) return x # 1. 根据网络层的不同定义不同的初始化方式 def weight_init(m): if isinstance(m, nn.Linear): nn.init.xavier_normal_(m.weight) nn.init.constant_(m.bias, 0) # 也可以判断是否为conv2d,使用相应的初始化方式 elif isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode=fan_out, nonlinearity=relu) # 是否为批归一化层 elif isinstance(m, nn.BatchNorm2d): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0) # 2. 初始化网络结构 model = Net(in_dim, n_hidden_1, n_hidden_2, out_dim) # 3. 将weight_init应用在子模块上 model.apply(weight_init) #torch中的apply函数通过可以不断遍历model的各个模块。实际上其使用的是深度优先算法
<

方法二:

定义在模型中,利用self.modules()来进行循环

class Net(nn.Module): def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim): super().__init__() self.layer = nn.Sequential( nn.Linear(in_dim, n_hidden_1), nn.ReLU(True), nn.Linear(n_hidden_1, n_hidden_2), nn.ReLU(True), nn.Linear(n_hidden_2, out_dim) ) for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode=fan_out, nonlinearity=relu) elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0) def forward(self, x): x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) return x
<

在torch.nn.init中的各种初始化方法中,如nn.init.constant(m.weight, 1), nn.init.constant(m.bias, 0)中第一个参数是tensor,也就是对应的参数。在方法二中,需要了解self.modules()和self.children()的区别。

5 forward 的使用 class Module(nn.Module): def __init__(self): super(Module, self).__init__() # …… def forward(self, x): # …… return x data = ….. #输入数据 # 实例化一个对象 module = Module() # 前向传播 module(data) # 而不是使用下面的 # module.forward(data)

实际上

module(data)

是等价于

module.forward(data)

forward 使用的解释

等价的原因是因为 python calss 中的callinit方法.

class A():

def call

(self):

print(i can be called like a function)

a = A()

a()

out:

i can be called like a function

call里调用其他的函数。

猜你喜欢