神经网络训练过程中的参数学习是基于梯度下降法进行优化的。梯度下降法需要在开始训练时给每一个权重赋一个初始值。这个初始值的选取十分关键。

权重初始化

在感知器和logistic回归的训练中,我们一般将权重全部初始化为0。但是这在神经网络的训练中会存在一些问题。因为如果权重都为0,在第一遍前向计算时,所有的隐层神经元的激活值都相同。这样会导致深层神经元没有区分性。这种现象也称为对称权重现象。

随机初始化权重的一个问题是如何选取随机初始化的区间。如果权重取的太小,一是会导致神经元的输入过小,经过多层之后信号就慢慢消失了;二是还会使得Sigmoid 型激活函数丢失非线性的能力。以Logistic 函数为例,在0 附近基本上是近似线性的。这样多层神经网络的优势也就不存在了。如果权重取的太大,会导致输入状态过大。对于Sigmoid 型激活函数来说,激活值变得饱和,从而导致梯度接近于0。

因此,要高效地训练神经网络,给权重选取一个合适的随机初始化区间是非常重要的。一般而言,权重初始化的区间应该根据神经元的性质进行差异化的设置。如果一个神经元的输入连接很多,它的每个输入连接上的权重就应该小一些,以避免神经元的输出过大(当激活函数为ReLU时)或过饱和(当激活函数为Sigmoid 函数时)。总的来说,权重初始化的目的是防止在深度神经网络的前向传播过程中层激活函数的输出损失梯度出现爆炸或消失。如果发生任何一种情况,损失梯度太大或太小,就无法有效地向后传播,并且即便可以向后传播,网络也需要花更长时间来达到收敛。

我们需要牢记权重初始化的目的是为了让神经网络在训练过程中学习到有用的信息。因此权重初始化至少满足以下条件:

  • 初始化必要条件一:各层激活值不会出现饱和现象。

  • 初始化必要条件二:各层激活值不为0

如何初始化权重

举个简单的例子,假设我们有一个包含网络输入的向量xx。训练神经网络的标准做法,是让输入值落入类似一个均值为 0,标准差为 1 的正态分布中,以确保其被归一化。

1
2
3
4
5
6
7
8
9
import torch
x = torch.randn(512)

for i in range(100):
a = torch.randn(512,512)
x = x @ a

print(x.mean(),x.std())
#output: tensor(nan) tensor(nan)

让我们假设有一个没有激活函数的简单的 100 层网络,并且每层都有一个包含这层权重的矩阵aa。为了完成单个前向传播,我们必须对每层输入和权重进行矩阵乘法,总共 100 次连续的矩阵乘法。

事实证明,把权重值用标准正态分布进行初始化并不是一个好主意。为了弄明白个中原因,我们可以模拟网络的前向传播。

1
2
3
4
5
6
7
8
9
10
import torch
x = torch.randn(512)

for i in range(100):
a = torch.randn(512,512)
x = x @ a
if torch.isnan(x.std()):
break
print(i)
#output: 28

呃!在这 100 次矩阵乘法某次运算中,层输出变得非常大,甚至计算机都无法识别其标准差和均值。我们实际上可以看到产生这种结果需要多长时间。

在网络的第 29 个激活层输出发生梯度爆炸,很明显网络的权重初始化值过大。不仅如此,我们同时还要防止层输出发生梯度消失。为了看看当网络权重初始值太小时会发生什么 - 我们将缩小例子的权重值,使它们仍然落入平均值为 0 的正态分布内,而标准差为 0.01。

1
2
3
4
5
6
7
8
import torch
x = torch.randn(512)

for i in range(100):
a = torch.randn(512,512) * 0.01
x = x @ a
print(x.mean(),x.std())
#output: tensor(0.) tensor(0.)

在上述假设的前向传播过程中,激活层输出出现了完全消失的现象。总结一下,权重初始值太大或者太小,网络都将无法很好地进行学习。

怎样才能找到最佳值?

如上所述,神经网络前向传播在数学上只需做连续的矩阵乘法。如果输出yy 是输入向量 xx和权重矩阵 aa 之间的矩阵乘法之积,则 yy 中的第 ii 个元素被定义为:

yi=k=0n1ai,kxky_i = \sum_{k=0}^{n-1} a_{i,k} x_{k}

可以证明,在某给定层,根据标准正态分布初始化的输入xxaa乘积,通常具有非常接近输入连接数平方根的标准差,首先,给出随机变量方差的两个性质:

  • 假设随机变量XX和随机变量YY相互独立,则有 :

Var(X+Y)=Var(X)+Var(Y)Var(X+Y)=Var(X)+Var(Y)

  • 假设随机变量XX和随机变量YY相互独立,且E(X)=E(Y)=0E(X)=E(Y)=0,则有

Var(XY)=Var(X)Var(Y)Var(XY)=Var(X)Var(Y)

假设E(x)=0Var(x)=1E(x)=0,Var(x)=1。假设WWXX相互独立,则可得到方差为

Var(yi)=Var(k=0n1aikxk)=k=0n1Var(aik)Var(xk)=k=0n1Var(aik)=k=0n11=n \begin{align*} Var(y_i)= & Var(\sum_{k=0}^{n-1}a_{ik}x_k) \\ = & \sum_{k=0}^{n-1}Var(a_{ik})Var(x_k) \\ = & \sum_{k=0}^{n-1}Var(a_{ik}) \\ = & \sum_{k=0}^{n-1} 1 \\ = & n \\ \end{align*}

在我们的例子中是512\sqrt{512}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch
import math
torch.manual_seed(42)
mean,var = 0.0,0.0
for i in range(10000):
x = torch.randn(512)
a = torch.randn(512,512)
y = x @ a
mean += y.mean().item()
var += y.pow(2).mean().item()
print(mean / 10000,math.sqrt(var / 10000))
#output: 0.013872916009649634 22.62687118942869
print(math.sqrt(512))
#output: 22.627416997969522

如果我们从矩阵乘法定义来看这个值就再正常不过了:为了计算yy,我们将输入向量xx 的某个元素乘以权重矩阵aa的一列所得的512个乘积相加。在我们的例子中使用了标准正态分布来初始化xxaa,所以这 512 个乘积的均值都为 0,标准差都为 1。

1
2
3
4
5
6
7
8
9
10
11
12
import torch
import math
torch.manual_seed(42)
mean,var = 0.0,0.0
for i in range(10000):
x = torch.randn(1)
a = torch.randn(1)
y = x * a
mean += y.item()
var += y.pow(2).item()
print(mean / 10000,math.sqrt(var / 10000))
#output: -0.0033513624027884134 1.0174967042873966

然后,这512个乘积的总和的均值为0,方差为512,因此标准差为512\sqrt{512}

这就是为什么在上面的例子中层输出在29次连续的矩阵乘法后会发生梯度爆炸。这个简单的100层网络架构中,我们想要的是每层输出具有大约1的标准差,这样就可以使我们在尽可能多的网络层上重复矩阵乘法,而不会发生梯度爆炸或消失。

如果我们首先通过将权重矩阵aa的各随机选择值除以512\sqrt{512}来对其进行缩小,那么生成输出yy的某个元素的输入元素与权重乘积的方差通常只有1512\frac{1}{\sqrt{512}}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch
import math
torch.manual_seed(42)
mean,var = 0.0,0.0
for i in range(10000):
x = torch.randn(1)
a = torch.randn(1) * math.sqrt(1/512)
y = x * a
mean += y.item()
var += y.pow(2).item()
print(mean / 10000,var / 10000)
#output: -0.00014811066253613917 0.002022069358027098
print(1/512)
#output: 0.001953125

这意味着矩阵yy的标准差(由输入xx和权重aa通过矩阵乘法生成的512个值组成)将是1。我们可以通过实验来验证。

1
2
3
4
5
6
7
8
9
10
11
12
import torch
import math
torch.manual_seed(42)
mean,var = 0.0,0.0
for i in range(10000):
x = torch.randn(512)
a = torch.randn(512,512) * math.sqrt(1/512)
y = x @ a
mean += y.mean().item()
var += y.pow(2).mean().item()
print(mean / 10000,math.sqrt(var / 10000))
#output: 0.0006131021328503266 0.9999758617855519

现在让我们重新运行之前的100层神经网络。与之前一样,我们首先从[-1,1]内的标准正态分布中随机选择层权重值,但这次我们用1n\frac{1}{\sqrt{n}}来缩小权重,其中nn是每层的网络输入连接数,在我们的例子是512。

1
2
3
4
5
6
7
8
9
10
import torch
import math
torch.manual_seed(42)

x = torch.randn(512)
for i in range(100):
a = torch.randn(512,512) * math.sqrt(1/512)
x = x @ a
print(x.mean(),x.std())
#output: tensor(0.0057) tensor(0.5297)

搞定!即使连续传播了100层之后,层输出也没有发生梯度爆炸或者消失。

虽然乍一看似乎真的搞定了,但现实世界的神经网络并不像我们这个例子那么简单。因为为简单起见,我们省略了激活函数。但是,在实际中我们永远不会这样做。正是因为有了这些置于网络层末端的非线性激活函数,深度神经网络才能非常近似地模拟真实世界那些错综复杂的现象,并且生成那些令人惊讶的预测,例如手写样本的分类。

Xavier初始化

Xavier初始化的基本思想是,若对于一层网络的输入和输出可以保持正态分布且方差相近,这样就可以避免输出趋向于0,从而避免梯度弥散情况。

前向传播

我们假设:

Y=WX+BY = WX+B

其中,YWXBY、W、X和B为随机变量,且wRu×dw \in R^{u \times d}xRdx \in R^{d}y,bRuy,b \in R^{u}

要满足前向传播过程中方差保持一直,则Var(Yi)=var(Xj)Var(Y_i) = var(X_j)

在计算前,我们做如下假设:

  1. WXBW、X、B为相互独立变量
  2. WijW_{ij}之间独立同分布,且E(Wij)=0E(W_{ij})=0
  3. BiB_{i}之间独立同分布,且E(Bi)=0E(B_{i})=0
  4. XjX_{j}之间独立同分布,且E(Xj)=0E(X_{j})=0

假设1说明输入数据和模型无相关性;假设2、3和4说明输入数据内部和模型内部的数据无相关性,且均值为0(个人认为后面3个假设过强,可能与实际情况不符);

则有:

Var(Yi)=Var(WiX+Bi)=Var(j=1dWi,jXj+Bi)=dVar(Wi,jXj)=d(E(wij2)E(Xj2)E2(wij)E2(Xj))=dVar(Wij)Var(Xj)\begin{align} Var(Y_i)= & Var(W_{i}X+B_i) \\ = & Var(\sum_{j=1}^{d}W_{i,j}X_j+B_i) \\ = & d*Var(W_{i,j}X_j) \\ = & d*(E(w_{ij}^2)E(X_j^2) - E^2(w_{ij})E^2(X_j)) \\ = & d * Var(W_{ij})Var(X_j) \end{align}

其中, dd是输入向量的维度。若要实现 Var(Yi)=Var(Xj)Var(Y_i)=Var(X_j) ,则必须满足 dVar(Wij)=1d*Var(W_{ij})=1 ,即: Var(Wij)=1dVar(W_{ij})=\frac{1}{d} ,进一步得出初始化方式:

  • WijW_{ij} 服从正态分布,则 WijNormal(0,1d)W_{ij} \sim Normal(0,\frac{1}{d})
  • WijW_{ij} 服从均匀分布,则 WijUniform(3d,3d)W_{ij} \sim Uniform(-\sqrt{\frac{3}{d}},\sqrt{\frac{3}{d}})

反向传播

我们假设:

ΔX=WTΔY\Delta X = W^T \Delta Y

其中, WWΔY\Delta YΔX\Delta X 是随机独立变量, wRu×dw \in R^{u \times d}ΔyRu\Delta y \in R^uΔxRd\Delta x \in R^d

要满足反向传播过程中方差保持一直,则Var(ΔYi)=var(ΔXj)Var(\Delta Y_i) = var(\Delta X_j)

在计算前,我们做如下假设:

  1. ΔYW\Delta Y、W为相互独立变量
  2. WijW_{ij}之间独立同分布,且E(Wij)=0E(W_{ij})=0
  3. ΔYi\Delta Y_{i}之间独立同分布,且E(ΔYi)=0E(\Delta Y_{i})=0

同样,个人认为假设2和3过强了,可能与现实不符。

其中, uu是FC这一层的神经元个数,也就是FC前向输出的维度。若要满足Var(ΔXj)=Var(ΔYi)Var(\Delta X_j)=Var(\Delta Y_i) ,必须保证 uVar(Wij)=1u * Var(W_{ij}) =1 ,即 Var(Wij)=1uVar(W_{ij}) = \frac{1}{u} ,进一步得出初始化方式:

  • WijW_{ij} 服从正态分布,则 WijNormal(0,1u)W_{ij} \sim Normal(0,\frac{1}{u})
  • WijW_{ij} 服从均匀分布,则 WijUniform(3u,3u)W_{ij} \sim Uniform(-\sqrt{\frac{3}{u}},\sqrt{\frac{3}{u}})

取调和平均数

根据上面的推导可以看出,除非 d=ud=u,否则我们无法同时保证前后向传播的过程中的Variance不发生变化,所以原论文中对 Var(Wij)Var(W_{ij}) 取了一个调和平均数: Var(Wij)=2d+uVar(W_{ij}) = \frac{2}{d+u} ,进一步得到模型初始化方式:

  • WijW_{ij} 服从正态分布,则 WijNormal(0,2d+u)W_{ij} \sim Normal(0,\frac{2}{d+u})
  • WijW_{ij} 服从均匀分布,则 WijUniform(6d+u,6d+u)W_{ij} \sim Uniform(-\sqrt{\frac{6}{d+u}},\sqrt{\frac{6}{d+u}})

实验

我们在假设的100层网络每一层之后添加双曲正切激活函数,然后看看当使用我们自己的权重初始化方案时会发生什么,这里层权重按1n\frac{1}{\sqrt{n}}缩小。

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch
import math
torch.manual_seed(42)

def tanh(x):
return torch.tanh(x)

x = torch.randn(512)
for i in range(100):
a = torch.randn(512,512) * math.sqrt(1/512)
x = tanh(x @ a)
print(x.mean(),x.std())
#output: tensor(0.0021) tensor(0.0526)

第100层的激活输出的标准偏差低至约0.05。这绝对是偏小的,但至少激活并没有完全消失!

现在回想起来,发现我们自己的权重初始化策略还是很直观的。但你可能会惊讶地发现,就在2010年,这还不是初始化权重层的传统方法。当Xavier Glorot和Yoshua Bengio发表了他们的标志性文章Understanding the difficulty of training deep feedforward neural networks,他们对比实验中的“常用启发式”是根据[-1,1]中的均匀分布来初始化权重,然后按 1n\frac{1}{\sqrt{n}} 的比例缩放。

事实证明,这种“标准”方法实际上并不能很好地发挥作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch
import math
torch.manual_seed(42)

def tanh(x):
return torch.tanh(x)

x = torch.randn(512)
for i in range(100):
a = torch.Tensor(512,512).uniform_(-1,1) * math.sqrt(1/512)
x = tanh(x @ a)
print(x.mean(),x.std())
#output: tensor(4.0375e-26) tensor(6.9527e-25)

使用“标准”权重初始化方法重新运行我们的100层tanh网络会导致激活梯度变得无限小 - 就像消失一样了一样。这种糟糕的结果实际上促使Glorot和Bengio提出他们自己的权重初始化策略,他们在他们的论文中称之为“正则化初始化”,现在通常被称为“Xavier初始化”。

Glorot和Bengio认为Xavier权重初始化将保持激活函数和反向传播梯度的方差,一直向上或向下传播到神经网络的每一层。在他们的实验中,他们观察到Xavier初始化使一个5层网络能够将每层的权重梯度维持在基本一致的方差上。相反,若不使用Xavier初始化,直接使用“标准”初始化会导致网络较低层(较高)的权值梯度与最上层(接近于零)的权值梯度之间的差异更大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch
import math
torch.manual_seed(42)

def tanh(x):
return torch.tanh(x)

def xavier(m,h):
return torch.Tensor(m,h).uniform_(-1,1) * math.sqrt(6/(m+h))

x = torch.randn(512)
for i in range(100):
a = xavier(512,512)
x = tanh(x @ a)
print(x.mean(),x.std())
#output: tensor(0.0018) tensor(0.0543)

在我们的实验网络中,Xavier初始化方法与我们之前自定义方法非常相似,之前的方法是从随机正态分布中采样值,并通过传入网络连接数n的平方根进行缩放。

Kaiming初始化

Kaiming初始化基本思想是,当使用ReLU做为激活函数时,Xavier的效果不好,原因在于,当RelU的输入小于0时,其输出为0,相当于该神经元被关闭了,影响了输出的分布模式。

因此Kaiming初始化,在Xavier的基础上,假设每层网络有一半的神经元被关闭,于是其分布的方差也会变小。经过验证发现当对初始化值缩小一半时效果最好,故Kaiming初始化可以认为是Xavier初始/2的结果。

前向传播

假设网络的表达式为:

Z=f(X)Z = f(X)

Y=WZ+BY = WZ+B

其中, XXZZYYWWBB 为独立随机变量, ff为ReLU激活函数,且 wRu×dw \in R^{u \times d}xzRdx,z \in R^{d}ybRuy,b \in R^{u}

若要做到前向计算信号强度不变,就需要满足: Var(Yi)=Var(Xj)Var(Y_i)=Var(X_j), 由于是使用的时ReLU激活函数,即仅正半轴有值,因此可以得到:Var(Zj)=12Var(Xj)Var(Z_j) = \frac{1}{2}Var(X_j) ,进一步可得到:

  • WijW_{ij} 服从正态分布,则 WijNormal(0,2d)W_{ij} \sim Normal(0,\frac{2}{d})
  • WijW_{ij} 服从均匀分布,则 WijUniform(6d,6d)W_{ij} \sim Uniform(-\sqrt{\frac{6}{d}},\sqrt{\frac{6}{d}})

当第l 层神经元使用ReLU激活函数时,通常有一半的神经元输出为0,因此其分布的方差也近似为使用Logistic 作为激活函数时的一半。这样,只考虑前向传播时,参数wli的理想方差为

实验

从概念上讲,当使用关于零对称且在[-1,1]内有输出的激活函数(例如softsign和tanh)时,我们希望每层的激活输出的平均值为0,平均标准偏差大约为1,这是有道理的。这正是我们的自定义方法和Xavier都能实现的。但是,如果我们使用ReLU激活函数呢?以同样的方式缩放随机初始权重值是否仍然有意义?

为了看看会发生什么,让我们在先前假设的网络层中使用ReLU激活来代替tanh,并观察其输出的预期标准偏差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import torch
import math
torch.manual_seed(42)

def relu(x):
return x.clamp_min(0.)

mean,var = 0.0,0.0
for i in range(10000):
x = torch.randn(512)
a = torch.randn(512,512)
y = relu(x @ a)
mean += y.mean().item()
var += y.pow(2).mean().item()
print(mean / 10000,math.sqrt(var / 10000))
#output: 9.029411460781098 16.006880666442754
print(math.sqrt(512/2))
#output: 16.0

事实证明,当使用ReLU激活时,单个层的平均标准偏差将非常接近输入连接数的平方根除以2的平方根,在我们的例子中也就是5122\sqrt{\frac{512}{2}}

通过该值缩放权重矩阵aa将使每个单独的ReLU层平均具有1的标准偏差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch
import math
torch.manual_seed(42)

def relu(x):
return x.clamp_min(0.)

mean,var = 0.0,0.0
for i in range(10000):
x = torch.randn(512)
a = torch.randn(512,512)*math.sqrt(2/512)
y = relu(x @ a)
mean += y.mean().item()
var += y.pow(2).mean().item()
print(mean / 10000,math.sqrt(var / 10000))
#output: 0.5643382162988186 1.0004300416526721

正如我们之前所展示的那样,保持层激活的标准偏差大约为1将允许我们在深度神经网络中堆叠更多层而不会出现梯度爆炸或消失。

接下来实现我们自己的Kaiming初始化版本,并验证如果在我们假设的100层网络的所有层使用ReLU,它确实可以防止激活输出爆炸或消失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch
import math
torch.manual_seed(42)

def relu(x):
return x.clamp_min(0.)

def kaiming(m,h):
return torch.randn(m,h)*math.sqrt(2/m)

x = torch.randn(512)
for i in range(100):
a = kaiming(512,512)
x = relu(a @ x)
print(x.mean(),x.std())
#output: tensor(0.2067) tensor(0.3064)

作为最后的比较,如果我们使用Xavier初始化,那么将会发生以下情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch
import math
torch.manual_seed(42)

def relu(x):
return x.clamp_min(0.)

def xavier(m,h):
return torch.Tensor(m,h).uniform_(-1,1) * math.sqrt(6/(m+h))

x = torch.randn(512)
for i in range(100):
a = xavier(512,512)
x = relu(a @ x)
print(x.mean(),x.std())
#output: tensor(8.5957e-16) tensor(1.2593e-15)

看!当使用Xavier初始化权重时,第100层的激活输出几乎完全消失了!

顺便提一下,当他们使用ReLU训练更深层的网络时。何凯明等人发现使用Xavier初始化的30层CNN完全停止并且不再学习。然而,当根据上面概述的三步初始化相同的网络时,它的收敛效果非常好。

对我们来说,故事的寓意是,我们从头开始训练的任何网络,特别是计算机视觉应用,几乎肯定会包含ReLU激活函数,并且是深度的。在这种情况下,Kaiming初始化应该是我们的首选权重初始化策略。

参考

  1. https://zhuanlan.zhihu.com/p/64464584
  2. https://towardsdatascience.com/weight-initialization-in-neural-networks-a-journey-from-the-basics-to-kaiming-954fb9b47c79