参数初始化很简单,但是简单的东西也容易出现知识盲区,本文全文 4000 字,将从数理和代码两个角度带大家认识初始化,希望能给大家带来更加形象的认识。
参数初始化分为:固定值初始化、预训练初始化和随机初始化。
固定初始化是指将模型参数初始化为一个固定的常数,这意味着所有单元具有相同的初始化状态,所有的神经元都具有相同的输出和更新梯度,并进行完全相同的更新,这种初始化方法使得神经元间不存在非对称性,从而使得模型效果大打折扣。
预训练初始化是神经网络初始化的有效方式,比较早期的方法是使用 greedy layerwise auto-encoder 做无监督学习的预训练,经典代表为 Deep Belief Network;而现在更为常见的是有监督的预训练+模型微调。
随机初始化是指随机进行参数初始化,但如果不考虑随机初始化的分布则会导致梯度爆炸和梯度消失的问题。
我们这里主要关注随机初始化的分布状态。
先介绍两个用的比较多的初始化方法:高斯分布和均匀分布。
以均匀分布为例,通常情况下我们会将参数初始化为
class MLP(nn.Module):def __init__(self, neurals, layers):super(MLP, self).__init__()self.linears = nn.ModuleList([nn.Linear(neurals, neurals, bias=False) for i in range(layers)])self.neurals = neuralsdef forward(self, x):for (i, linear) in enumerate(self.linears):x = linear(x)print("layer:{}, std:{}".format(i+1, x.std()))if torch.isnan(x.std()):breakreturn xdef initialize(self):for m in self.modules():if isinstance(m, nn.Linear):a = np.sqrt(1/self.neurals)nn.init.uniform_(m.weight.data, -a, a)neural_nums=256
layers_nums=100
batch_size=16net = MLP(neural_nums, layers_nums)
net.initialize()inputs = torch.randn((batch_size, neural_nums))
output = net(inputs)
输出为
layer:0, std:0.5743116140365601
layer:1, std:0.3258207142353058
layer:2, std:0.18501722812652588
layer:3, std:0.10656329244375229
... ...
layer:95, std:9.287707510161138e-24
layer:96, std:5.310323679717446e-24
layer:97, std:3.170952429065466e-24
layer:98, std:1.7578611563776362e-24
layer:99, std:9.757115839154053e-25
我们可以看到,随着网络层数加深,权重的方差越来越小,直到最后超出精度范围。
我们先通过数学推导来解释一下这个现象,以第一层隐藏层的第一个单元为例。
首先,我们是没有激活函数的线性网络:
其中,n 为输入层神经元个数。
通过方差公式我们有:
这里,我们的输入均值为 0,方差为 1,权重的均值为 0,方差为
此时,神经元的标准差为
通过上式进行计算,每一层神经元的标准差都将会是前一层神经元的
我们可以看一下上面打印的输出,是不是正好验证了这个规律。
而这种初始化方式合理吗?有没有更好的初始化方法?
Xavier Glorot 认为:优秀的初始化应该使得各层的激活值和状态梯度在传播过程中的方差保持一致。即方差一致性。
所以我们需要同时考虑正向传播和反向传播的输入输出的方差相同。
在开始推导之前,我们先引入一些必要的假设:
考虑前向传播:
我们令输入的方差等于输出得到方差:
则有:
此外,我们还要考虑反向传播的梯度状态。
反向传播:
我们也可以得到下一层的方差:
我们取其平均,得到权重的方差为:
此时,均匀分布为:
我们来看下实验部分,只需修改类里面的初始化函数:
class MLP(nn.Module):...def initialize(self):a = np.sqrt(3/self.neurals)for m in self.modules():if isinstance(m, nn.Linear):nn.init.uniform_(m.weight.data, -a, a)
输出结果:
layer:0, std:0.9798752665519714
layer:1, std:0.9927620887756348
layer:2, std:0.9769216179847717
layer:3, std:0.9821343421936035
...
layer:97, std:0.9224138855934143
layer:98, std:0.9622119069099426
layer:99, std:0.9693211317062378
这便达到了我们的目的,即输入和输出的方差保持一致。
但在实际过程中,我们还会使用激活函数,所以我们在 forward 中加入 sigmoid 函数:
class MLP(nn.Module):...def forward(self, x):for (i, linear) in enumerate(self.linears):x = linear(x)x = torch.sigmoid(x)print("layer:{}, std:{}".format(i, x.std()))if torch.isnan(x.std()):breakreturn x...
在看下输出结果:
layer:0, std:0.21153637766838074
layer:1, std:0.13094832003116608
layer:2, std:0.11587061733007431
...
layer:97, std:0.11739246547222137
layer:98, std:0.11711347848176956
layer:99, std:0.11028502136468887
好像还不错,也没有出现方差爆炸的问题。
不知道大家看到这个结果会不会有些疑问:为什么方差不是 1 了?
这是因为 sigmoid 的输出都为正数,所以会影响到均值的分布,所以会导致下一层的输入不满足均值为 0 的条件。我们将均值和方差一并打出:
layer:0, mean:0.5062727928161621
layer:0, std:0.20512282848358154
layer:1, mean:0.47972571849823
layer:1, std:0.12843772768974304
...
layer:98, mean:0.5053208470344543
layer:98, std:0.11949671059846878
layer:99, mean:0.49752169847488403
layer:99, std:0.1192963495850563
可以看到,第一层隐藏层(layer 0)的均值就已经变成了 0.5。
这又会出现什么问题呢?
答案是出现 “zigzag” 现象:
上图摘自李飞飞的 cs231n 课程。
在反向传播过程中:
因为
为此,我们可以使用,改变 sigmoid 的尺度与范围,改用 tanh:
tanh 的收敛速度要比 sigmoid 快,这是因为 tanh 的均值更加接近 0,SGD 会更加接近 natural gradient,从而降低所需的迭代次数。
我们使用 tanh 做一下实验,看下输出结果:
layer:0, mean:-0.011172479018568993
layer:0, std:0.6305743455886841
layer:1, mean:0.0025750682689249516
layer:1, std:0.4874609708786011
...
layer:98, mean:0.0003803471918217838
layer:98, std:0.06665021181106567
layer:99, mean:0.0013235544320195913
layer:99, std:0.06700969487428665
可以看到,在前向传播过程中,均值没有出问题,但是方差一直在减小。
这是因为,输出的数据经过 tanh 后标准差发生了变换,所以在实际初始化过程中我们还需要考虑激活函数的计算增益:
class MLP(nn.Module):...def initialize(self):for m in self.modules():if isinstance(m, nn.Linear):tanh_gain = nn.init.calculate_gain('tanh')a = np.sqrt(3/self.neurals)a *= tanh_gainnn.init.uniform_(m.weight.data, -a, a)
输出为:
layer:0, std:0.7603299617767334
layer:1, std:0.6884239315986633
layer:2, std:0.6604527831077576
...
layer:97, std:0.6512776613235474
layer:98, std:0.643700897693634
layer:99, std:0.6490980386734009
此时,方差就被修正过来了。
当然,在实际过程中我们也不需要自己写,可以直接调用现成的函数:
class MLP(nn.Module):...def initialize(self):a = np.sqrt(3/self.neurals)for m in self.modules():if isinstance(m, nn.Linear):tanh_gain = nn.init.calculate_gain('tanh')nn.init.xavier_uniform_(m.weight.data, gain=tanh_gain)
输出为:
layer:0, std:0.7628788948059082
layer:1, std:0.6932843923568726
layer:2, std:0.6658385396003723
...
layer:97, std:0.6544962525367737
layer:98, std:0.6497417092323303
layer:99, std:0.653872549533844
可以看到其输出是差不多的。
在这里,不知道同学们会不会有一个疑问,为什么 sigmoid 不会出现 tanh 的情况呢?
这是因为 sigmoid 的信息增益为 1,而 tanh 的信息增益为 5/3,理论证明这里就略过了。
tanh 和 sigmoid 有两大缺点:
所以我们经常会用到 ReLU,所以我们试一下效果:
class MLP(nn.Module):def __init__(self, neurals, layers):super(MLP, self).__init__()self.linears = nn.ModuleList([nn.Linear(neurals, neurals, bias=False) for i in range(layers)])self.neurals = neuralsdef forward(self, x):for (i, linear) in enumerate(self.linears):x = linear(x)x = torch.relu(x)print("layer:{}, std:{}".format(i, x.std()))return xdef initialize(self):for m in self.modules():if isinstance(m, nn.Linear):tanh_gain = nn.init.calculate_gain('relu')a = np.sqrt(3/self.neurals)a *= tanh_gainnn.init.uniform_(m.weight.data, -a, a)
输出为:
layer:0, std:1.4423831701278687
layer:1, std:2.3559958934783936
layer:2, std:4.320342540740967
...
layer:97, std:1.3732810130782195e+23
layer:98, std:2.3027095847369547e+23
layer:99, std:4.05964954791109e+23
为什么 Xavier 突然失灵了呢?
这是因为 Xavier 只能针对类似 sigmoid 和 tanh 之类的饱和激活函数,而无法应用于 ReLU 之类的非饱和激活函数。
针对这一问题,何凯明于 2015 年发表了一篇论文《Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification》,给出了解决方案。
在介绍 kaiming 初始化之前,这里补充下饱和激活函数的概念。
同样遵循方差一致性原则。
激活函数为
其中:
我们将其带入,可以得到:
所以参数服从
我们试一下结果:
class MLP(nn.Module):... def initialize(self):a = np.sqrt(3/self.neurals)for m in self.modules():if isinstance(m, nn.Linear):a = np.sqrt(6 / self.neurals)nn.init.uniform_(m.weight.data, -a, a)
输出为:
layer:0, std:0.8505409955978394
layer:1, std:0.8492708802223206
layer:2, std:0.8718656301498413
...
layer:97, std:0.8371583223342896
layer:98, std:0.7432138919830322
layer:99, std:0.6938706636428833
可以看到,结果要好很多。
再试一下凯明均匀分布:
class MLP(nn.Module):... def initialize(self):a = np.sqrt(3/self.neurals)for m in self.modules():if isinstance(m, nn.Linear):nn.init.kaiming_uniform_(m.weight.data)
输出为:
layer:0, std:0.8123029470443726
layer:1, std:0.802753210067749
layer:2, std:0.758887529373169
...
layer:97, std:0.2888352870941162
layer:98, std:0.26769548654556274
layer:99, std:0.2554236054420471
那如果激活函数是 ReLU 的变种怎么办呢?
这里直接给结论:
我们上述介绍的都是以均匀分布为例,而正态分布也是一样的。均值 0,方差也计算出来了,所服从的分布自然可知。
这一节我们来看下源码解析,以 Pytorch 为例子。
def xavier_uniform_(tensor, gain=1.):""" xavier 均匀分布"""fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor)std = gain * math.sqrt(2.0 / float(fan_in + fan_out))a = math.sqrt(3.0) * std return _no_grad_uniform_(tensor, -a, a)def xavier_normal_(tensor, gain=1.):""" xavier 正态分布"""fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor)std = gain * math.sqrt(2.0 / float(fan_in + fan_out))return _no_grad_normal_(tensor, 0., std)def kaiming_uniform_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu'):""" kaiming 均匀分布"""fan = _calculate_correct_fan(tensor, mode)gain = calculate_gain(nonlinearity, a)std = gain / math.sqrt(fan)bound = math.sqrt(3.0) * stdwith torch.no_grad():return tensor.uniform_(-bound, bound)def kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu'):""" kaiming 正态分布"""fan = _calculate_correct_fan(tensor, mode)gain = calculate_gain(nonlinearity, a)std = gain / math.sqrt(fan)with torch.no_grad():return tensor.normal_(0, std)
可以看到,xavier 初始化会调用 _calculate_fan_in_and_fan_out 函数,而 kaiming 初始化会调用 _calculate_correct_fan 函数,具体看下这两个函数。
def _calculate_fan_in_and_fan_out(tensor):""" 计算输入输出的大小"""dimensions &#61; tensor.dim()if dimensions <2:raise ValueError("Fan in and fan out can not be computed for tensor with fewer than 2 dimensions")num_input_fmaps &#61; tensor.size(1)num_output_fmaps &#61; tensor.size(0)receptive_field_size &#61; 1if tensor.dim() > 2:receptive_field_size &#61; tensor[0][0].numel()fan_in &#61; num_input_fmaps * receptive_field_sizefan_out &#61; num_output_fmaps * receptive_field_sizereturn fan_in, fan_outdef _calculate_correct_fan(tensor, mode):""" 根据 mode 计算输入或输出的大小"""mode &#61; mode.lower()valid_modes &#61; [&#39;fan_in&#39;, &#39;fan_out&#39;]if mode not in valid_modes:raise ValueError("Mode {} not supported, please use one of {}".format(mode, valid_modes))fan_in, fan_out &#61; _calculate_fan_in_and_fan_out(tensor)return fan_in if mode &#61;&#61; &#39;fan_in&#39; else fan_out
xavier 初始化是外部传入信息增益&#xff0c;而 kaiming 初始化是在内部包装了信息增益&#xff0c;我们来看下信息增益的函数&#xff1a;
def calculate_gain(nonlinearity, param&#61;None):linear_fns &#61; [&#39;linear&#39;, &#39;conv1d&#39;, &#39;conv2d&#39;, &#39;conv3d&#39;, &#39;conv_transpose1d&#39;, &#39;conv_transpose2d&#39;, &#39;conv_transpose3d&#39;]if nonlinearity in linear_fns or nonlinearity &#61;&#61; &#39;sigmoid&#39;:return 1elif nonlinearity &#61;&#61; &#39;tanh&#39;:return 5.0 / 3elif nonlinearity &#61;&#61; &#39;relu&#39;:return math.sqrt(2.0)elif nonlinearity &#61;&#61; &#39;leaky_relu&#39;:if param is None:negative_slope &#61; 0.01elif not isinstance(param, bool) and isinstance(param, int) or isinstance(param, float):# True/False are instances of int, hence check abovenegative_slope &#61; paramelse:raise ValueError("negative_slope {} not a valid number".format(param))return math.sqrt(2.0 / (1 &#43; negative_slope ** 2))else:raise ValueError("Unsupported nonlinearity {}".format(nonlinearity))
把各个激活函数所对应的信息增益表画下来&#xff1a;
尽管初始化很简单&#xff0c;但从数理角度出发去分析神经网络并不轻松&#xff0c;且需要加上假设才能进行分析。但不管怎么说初始化对于训练神经网络至关重要&#xff0c;那些非常深的网络如 GoogleNet、ResNet 都 stack 了这写方法&#xff0c;并且非常 work。