神经网络模型一般都是靠随机梯度下降算法进行训练和参数更新。网络的收敛效果很大程度上受到网络参数初值的影响。不合理的网络参数初始化,可能会导致 “梯度消失” 或者 “梯度爆炸” 问题。
神经网络处于稳定状态时,参数和数据应基本保持正负各半,期望为 0。
如果将所有的参数全部初始化为 0 的话,网络中不同的神经元输出必然相同,相同的输出导致梯度也是相同的,同时也会保持更新后的参数保持一致的,这使得模型无法进行训练。较好的一个方案就是随机初始化网络参数,从高斯分布、或者均匀分布中进行初始化。
如果输入神经元个数为 n_in,输出神经元个数为 n_out,则服从高斯分布的随机参数初始化为:
很显然,上面的初始化方式会使得网络输出的数据随着输入神经元的个数而发生变化。我们知道每一层的网络参数都会根据输入数据进行拟合,如果每一层的输入数据方差变化太大,可能会使得网络参数出现较大的调整,难以收敛。并且,经过实验表明对方差进行规范化的随机初始化有更快的收敛效率。我们知道的 xavier 初始化就是一种对方差进行规范化的初始化方式。
1. Xavier 高斯分布
神经元的输入和输出定义如下:
其方差表示如下:
根据两个连续变量乘积的方差公式:
可得,每一项的方差表示如下:
由于我们假设 w 是符合高斯分布其均值为 0,输入的数据也假设均值为 0,上面的式子就可以变为:
总的方差计算的式子就可以写成:
我们希望输入 x 和输出 y 的方差为能够近似相等,无论该方差是多少,两边都约掉就得到:
此时,xavier 初始化的均值为0,方差为 1/N。这里 N 取得是输入和输出神经元的个数的均值,即:
如果我们使用 Xavier 进行正态分布的网络参数初始化,就可以使用均值为0,方差为上面式子进行初始化。
2. Xavier 均匀分布
如果 w 满足均匀分布的话,均匀分布的方差公式为:
为什么均匀分布的公式要除以 12?
https://blog.csdn.net/hellocsz/article/details/90650440
假设:我们想要得到 (-n, n) 的均匀分布,可表示如下:
最后得到 w 在下面的区间,可以得到均匀分布的方差:
这里需要注意的是:我们前面为了保持输入和输出方差近似一致,做了输入均值为 0 的假设。如果使用不同的激活函数,其分布区间也有所不同。或者说,Xavier 初始化并未考虑到引入非线性激活函数对输入的影响。2015 年 He 等人对此提出高进,将非线性映射造成的影响考虑到参数初始化中。
3. PyTorch Xavier API
下面是 PyTorch 中不同激活函数的均匀分布区间的实现:
def xavier_uniform_(tensor: Tensor, gain: float = 1.) -> Tensor: 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)
实现中的 gain 是根据不同的激活函数进行计算:
def calculate_gain(nonlinearity, param=None): """ Return the recommended gain value for the given nonlinearity function. The values are as follows: ================= ==================================================== nonlinearity gain ================= ==================================================== Linear / Identity :math:`1` Conv{1,2,3}D :math:`1` Sigmoid :math:`1` Tanh :math:`\frac{5}{3}` ReLU :math:`\sqrt{2}` Leaky Relu :math:`\sqrt{\frac{2}{1 + \text{negative\_slope}^2}}` SELU :math:`\frac{3}{4}` ================= ==================================================== """ linear_fns = ['linear', 'conv1d', 'conv2d', 'conv3d', 'conv_transpose1d', 'conv_transpose2d', 'conv_transpose3d'] if nonlinearity in linear_fns or nonlinearity == 'sigmoid': return 1 elif nonlinearity == 'tanh': return 5.0 / 3 elif nonlinearity == 'relu': return math.sqrt(2.0) elif nonlinearity == 'leaky_relu': if param is None: negative_slope = 0.01 elif not isinstance(param, bool) and isinstance(param, int) or isinstance(param, float): # True/False are instances of int, hence check above negative_slope = param else: raise ValueError("negative_slope {} not a valid number".format(param)) return math.sqrt(2.0 / (1 + negative_slope ** 2)) elif nonlinearity == 'selu': return 3.0 / 4 # Value found empirically (https://github.com/pytorch/pytorch/pull/50664) else: raise ValueError("Unsupported nonlinearity {}".format(nonlinearity))
我们调用 calculate_gain 函数,并传入不同的激活函数:
import torch.nn as nn if __name__ == '__main__': print('relu:', nn.init.calculate_gain('relu')) print('sigmoid:', nn.init.calculate_gain('sigmoid')) print('tanh:', nn.init.calculate_gain('tanh')) print('leaky_relu:', nn.init.calculate_gain('leaky_relu')) print('selu:', nn.init.calculate_gain('selu'))
程序输出结果:
relu: 1.4142135623730951 sigmoid: 1 tanh: 1.6666666666666667 leaky_relu: 1.4141428569978354 selu: 0.75