神经网络中的参数初始化
标题开了个玩笑哈哈哈,本文对神经网络中的参数初始化相关内容进行了总结和归纳,涵盖常用初始化方法以及在PyTorch中的使用方法,以作备忘。
因个人水平有限,如有问题,欢迎大家提出指正!
参数初始化的对称性
以全连接网络为例,同一层中的任意神经元都是同构的,拥有相同的输入和输出,若是将参数初始化为同样的值,那么无论前向传播还是反向传播的值也是完全相同的。
学习过程永远无法打破这种对称性,最终同一网络层中各个参数仍然是相同的。
因此,我们需要随机初始化神经网络的参数值,以打破这种对称性。
参数初始值过大/过小
初始值过大时,对于Sigmoid、Tanh激活函数来说,会使得输入激活函数的值过大,造成饱和现象;反向传播时,会使得梯度极小,导致梯度消失现象。
此种情况,ReLU函数不受此影响。
初始值过小时,经过多层网络,输出值变得极小;反向传播时,同样会使得梯度极小,导致梯度消失现象。
此种情况,ReLU函数也受此影响。
由上可知,参数初始化方式的选择通常与激活函数有关,暂无研究发现网络结构与初始化方式之间存在关联,所以无需从网络结构的角度考虑选择何种初始化方式。
常见的参数初始化方式
除了普通的正态分布初始化(Normal)、均匀分布初始化(Uniform)以外,还有2010年提出的Xavier初始化、2015年提出的Kaiming初始化方法,这两种方法都有正态化(Normal)和均匀化(Uniform)两种方式。
以下公式中的 f a n _ i n fan\_in fan_in是权值张量中输入神经元的个数, f a n _ o u t fan\_out fan_out是权值张量中输出神经元的个数。
Xavier初始化
也称为Glorot初始化。
方差一致性
Xavier的核心思想是,对于每层网络来说保持输入和输出的方差一致,以避免所有输出值趋向于0,从而避免梯度消失现象。
因此,Xavier的特点是:正向传播时,激活值的方差保持不变;反向传播时,关于状态值的梯度的方差保持不变。
正态化的Xavier初始化 Xavier_normal:
从以0为中心,标准差为 s t d d e v = 2 / ( f a n _ i n + f a n _ o u t ) stddev=\sqrt{2/(fan\_in+fan\_out)} stddev=2/(fan_in+fan_out)的截断正态分布中抽取样本。
均匀化的Xavier初始化 Xavier_uniform:
从 [ − l i m i t , l i m i t ] [-limit, limit] [−limit,limit]中的均匀分布中抽取样本,其中 l i m i t limit limit是 6 / ( f a n _ i n + f a n _ o u t ) \sqrt{6/(fan\_in + fan\_out)} 6/(fan_in+fan_out)。
缺点:
Xavier的推导过程基于以下几个假设
-
激活函数是线性的;
不适用于ReLU,Sigmoid等非线性激活函数。 -
激活值关于0对称;
同样不适用于ReLU,Sigmoid等激活函数。
Kaiming初始化
也称He初始化,MSRA初始化,由何凯明大神提出。
前面提到Xavier初始化不适用于ReLU函数,更核心的原因在于当ReLU的输出小于0时,输出为0,相当于此神经元关闭,影响了输出的分布模式。
因此何凯明提出了适用于ReLU网络的Kaiming初始化方法,在Xavier的基础上,假设每层网络有一半的神经元关闭,于是其分布的方差也会变小。
因此,Kaiming的特点是:正向传播时,状态值的方差保持不变;反向传播时,关于激活值的梯度的方差保持不变。
正态化的Kaiming初始化 He_normal:
从以0为中心,标准差为 s t d d e v = 2 / f a n _ i n stddev=\sqrt{2/fan\_in} stddev=2/fan_in的截断正态分布中抽取样本。
均匀化的Kaiming初始化 He_uniform:
从 [ − l i m i t , l i m i t ] [-limit, limit] [−limit,limit]中的均匀分布中抽取样本,其中 l i m i t limit limit是 6 / f a n _ i n \sqrt{6/fan\_in} 6/fan_in。
PyTorch中的参数初始化
PyTorch在torch.nn.init
中提供了常用的初始化方法函数:
# Xavier方法
# gain根据激活函数的类型设定
# 服从均匀分布U(−a,a), 分布的参数a = gain * sqrt(6/fan_in+fan_out)
torch.nn.init.xavier_uniform_(w, gain=nn.init.calculate_gain('relu'))
# 服从正态分布N(0,std), 分布的标准差std = gain * sqrt(2/fan_in + fan_out)
torch.nn.init.xavier_normal_(w, gain=nn.init.calculate_gain('relu'))
# Kaiming方法
# a为激活函数负半轴的斜率, relu是0
# mode可选为fan_in或fan_out, fan_in使正向传播时,方差一致; fan_out使反向传播时,方差一致
# nonlinearity可选 relu 和 leaky_relu
# 服从均匀分布U(−bound,bound), 分布的参数bound = sqrt(6/(1+a^2)*fan_in)
torch.nn.init.kaiming_uniform_(w, a=0, mode='fan_in', nonlinearity='relu')
# 服从正态分布N(0,std), 分布的标准差std = sqrt(2/(1+a^2)*fan_in)
torch.nn.init.kaiming_normal_(w, a=0, mode='fan_in', nonlinearity='relu')
# 服从均匀分布U(a,b)
torch.nn.init.uniform_(w, a=0, b=1)
# 服从正态分布N(mean, std)
torch.nn.init.normal_(tensor, mean=0, std=1)
# 服从常数分布
torch.nn.init.constant_(w, val=0.3)
PyTorch中的默认初始化方式
在PyTorch中,参数的默认初始化方法定义在各个层的reset_parameters()
方法中。
以torch1.9.1为例:
nn.Linear
源码中的参数初始化方式
def reset_parameters(self) -> None:
init.kaiming_uniform_(self.weight, a=math.sqrt(5))
if self.bias is not None:
fan_in, _ = init._calculate_fan_in_and_fan_out(self.weight)
bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0
init.uniform_(self.bias, -bound, bound)
由上可知,weight使用kaiming均匀分布初始化,bias采用[-bound, bound]均匀分布,其中bound为1/sqrt(fan_in)
,fan_in
为输入神经元个数。
nn.ConvNd
源码中的参数初始化方式:
def reset_parameters(self) -> None:
init.kaiming_uniform_(self.weight, a=math.sqrt(5))
if self.bias is not None:
fan_in, _ = init._calculate_fan_in_and_fan_out(self.weight)
bound = 1 / math.sqrt(fan_in)
init.uniform_(self.bias, -bound, bound)
与nn.Linear
的初始化方式相同。
如何在PyTorch中自定义参数初始化方式?
可以自行编写weight_init(m)
函数,参数m代表module,即网络中的一层。通过isinstance(m, nn.Conv2d)
判断此网络层的类别,并据此选择合适的初始化方法。
from torch.nn import init
# Define the initial function to init the layer's parameters for the network
def weight_init(m):
if isinstance(m, nn.Conv2d):
init.xavier_uniform_(m.weight)
if m.bias:
init.constant(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
init.constant(m.weight, 1)
init.constant(m.bias, 0)
elif isinstance(m, nn.Linear):
init.normal(m.weight, std=1e-3)
if m.bias:
init.constant(m.bias, 0)
先将网络模型实例化,再通过model.apply(weight_init)
让模型调用这种参数初始化方式,这样便完成了对模型中训练参数的初始化。
apply
函数会递归地搜索网络内的所有module并把参数表示的函数应用到所有的module上。
# Define Network
model = Net()
model.apply(weigth_init)
可以将这一过程总结为:
- 定义weight_init函数,并在weight_init中通过判断module的类型来进行不同的参数初始化方式;
- model=Net(…), 创建网络结构;
- model.apply(weight_init), 将weight_init初始化方式应用到每一个module上;
前面也有提到,初始化方式一般是根据激活函数来选择的,因此这里应该是根据每种module的激活函数类型选择初始化方法。
这一点纯属笔者的个人猜测,未经实践或经验的验证,若有不同看法,欢迎提出指正。
参考
初始化方法
深度学习参数初始化(weights initializer)策略大全_LoveMIss-Y的博客-优快云博客_glorot_uniform
PyTorch的初始化函数
PyTorch参数初始化的位置以及如何自定义初始化函数
【pytorch参数初始化】 pytorch默认参数初始化以及自定义参数初始化_华仔168168的博客-优快云博客_pytorch 参数初始化