深度学习激活函数与正则化问题--潘登同学的深度学习笔记
梯度消失问题(Vanishing Gradients)
在梯度下降中,随着算法反向反馈到前面几层,梯度会越来越小,最终,没有变化,这 时或许还没有收敛到比较好的解,这就是梯度消失问题,深度学习遭受不稳定的梯度,不同 层学习在不同的速度上
如果我们看sigmoid激活函数,当输入比较大,不管正负,将会饱和在 0 或 1,这样梯度就是 0,因此当反向传播开始,它几乎没有梯度传播回神经网络,所以就会导致只更改高的几 层,低的几层不会变化
Relu的缺点
选择 ReLU 更多因为对于正数时候不饱和,同时因为它计算速度快 但是不幸的是,ReLU 激活函数不完美,有个问题是 dying ReLU,在训练的时候一些神经元死了,当它们输出小于 0,经过ReLU就会变成0,不管以后反向传播的再多,最终改变w也是0
解决方案: Leaky ReLU $$ \max(\alpha * z,z), \alpha =0.01 $$
自己封装 Leaky ReLU
def leaky_relu(z,name=None):
return tf.maximun(0.01*z,z,name=name)
hidden1 = fully_connected(X,n_hidden1,activitation_fn=leaky_relu)
其他Relu变形
- RReLU,Random,$\alpha$ 是一个在给定范围内随机取值的数在训练时,固定的平均值在测试时,过拟合时可以试试
- PReLU,Parametric,$\alpha$ 是一个在训练过程中需要学习的参数,它会被修改在反向传播中,适合大数据集
- ELU,exponential,计算梯度的速度会慢一些,但是整体因为没有死的神经元,整体收敛快,超参数 0.01
ELU 可以在tansorflow中直接调用
hidden1 = fully_connected(X,n_hidden1,activation_fn=tf.nn.elu)
参数初始化问题
深度学习模型训练的过程本质是对 weight(即参数 W)进行更新,这需要每个参数有相应的初始值。有人可能会说:“参数初始化有什么难点?直接将所有 weight 初始化为 0 或者初始化为随机数!” 对一些简单的机器学习模型,或当 optimization function 是 convex function 时,这些简单的方法确实有效。然而对于深度学习而言,非线性函数被疯狂叠加,non-convex function,如何选择参数初始值便成为一个值得探讨的问题 --- 其本质是初始参数的选择应使得 objective function 便于被优化。事实上,在学术界这也是一个被 actively 研究的领域。
pre-training
不想从零开始训练神经网络时,我们往往选择一个已经训练好的在任务 A 上的模型(称为 pre-trained model),将其放在任务 B 上做模型调整(称为 fine-tuning)
random initialization
随机初始化是很多人目前经常使用的方法,然而这是有弊端的,一旦随机分布选择不当, 就会导致网络优化陷入困境。
以 tanh 神经网络为例,查看激活值和梯度的分布情况。
可以看出,激活值的方差逐层递减 各层反向传播的梯度(关于状态的梯度)的分布情况:
随着层数的增加,梯度就会越来越接近0,就会产生梯度消失
代码验证
我们创建了一个 10 层的神经网络,非线性变换为 tanh,每一层的参数都是随机正态分布,均值为 0,标准差为 0.01。
import tensorflow as tf # 2.0+的tensorflow
import numpy as np
import matplotlib.pyplot as plt
data = tf.constant(np.random.randn(2000, 800))
layer_sizes = [800 - 50 * i for i in range(0,11)]
num_layers = len(layer_sizes)
fcs = [] # To store fully connected layers' output
fig, axs = plt.subplots(2, 5, figsize=(15, 6), sharey=True)
for i in range(0, num_layers - 1):
X = data if i == 0 else fcs[i - 1]
node_in = layer_sizes[i]
node_out = layer_sizes[i + 1]
W = tf.Variable(np.random.randn(node_in, node_out)) * 0.01
fc = tf.matmul(X, W)
fc = tf.nn.tanh(fc)
fcs.append(fc)
axs[i//5,i%5].hist(fc.numpy()[:,1])
# axs[i//5,i%5].set_xlim([-1,1])
fig.show()
注意横坐标值,如果都固定为[-1,1]区间,那么最终就会看不见输出。随着层数的增加,我们看到输出值迅速向 0 靠拢,在后几层中,几乎所有的输出值都 很接近 0!回忆优化神经网络的 back propagation 算法,根据链式法则,gradient 等于当前函数的 gradient 乘以后一层的 gradient,这意味着输出值计算 gradient 中的乘法因子,直接导致 gradient 很小,使得参数难以被更新!
让我们将初始值调大一些: 均值为 0,标准差为 1。
几乎所有的值集中在-1 或 1 附近,神经元 saturated 了!注意到 tanh 在-1 和 1 附近 的 gradient 都接近 0,这同样导致了 gradient 太小,参数难以被更新。
Xavier initialization
Xavier 初始化的基本思想是保持输入和输出的方差一致,这样就避免了所有输出值都趋向于 0。注意,为了问题的简便,Xavier 初始化的推导过程是基于线性函数的,但是它在一些非线性神经元中也很有效
激活函数 | 采用均匀分布随机,区间[-r,r] | 采用正太分布随机,均值为0 |
---|---|---|
sigmoid | $r=\sqrt{\frac{6}{n_{inputs}+n_{outputs}}}$ | $\sigma=\sqrt{\frac{2}{n_{inputs}+n_{outputs}}}$ |
tanh | $r=4\sqrt{\frac{6}{n_{inputs}+n_{outputs}}}$ | $\sigma=4\sqrt{\frac{2}{n_{inputs}+n_{outputs}}}$ |
Relu | $r=\sqrt{2}\sqrt{\frac{6}{n_{inputs}+n_{outputs}}}$ | $\sigma=\sqrt{2}\sqrt{\frac{2}{n_{inputs}+n_{outputs}}}$ |
代码验证
import tensorflow as tf # 2.0+çš„tensorflow
import numpy as np
import matplotlib.pyplot as plt
data = tf.constant(np.random.randn(2000, 800))
layer_sizes = [800 - 50 * i for i in range(0,11)]
num_layers = len(layer_sizes)
fcs = [] # To store fully connected layers' output
fig, axs = plt.subplots(2, 5, figsize=(15, 6), sharey=True)
for i in range(0, num_layers - 1):
X = data if i == 0 else fcs[i - 1]
node_in = layer_sizes[i]
node_out = layer_sizes[i + 1]
W = tf.Variable(np.random.randn(node_in, node_out)) * np.sqrt(2/(node_in+node_out))
fc = tf.matmul(X, W)
fc = tf.nn.tanh(fc)
fcs.append(fc)
axs[i//5,i%5].hist(fc.numpy()[:,1])
axs[i//5,i%5].set_xlim([-1,1])
fig.show()
输出值在很多层之后依然保持着良好的分布,这很有利于我们优化神经网络!之前谈到 Xavier initialization 是在线性函数上推导得出,这说明它对非线性函数并不具有普适性,所以这个例子仅仅说明它对 tanh 很有效,那么对于目前最常用的 ReLU 神经元呢?
W = tf.Variable(np.random.randn(node_in, node_out)) * np.sqrt(4/(node_in+node_out))
fc = tf.nn.relu(fc)
前面看起来还不错,后面的趋势却是越来越接近 0。
对参数初始化问题的总结
总的来说,无论是参数的初始化,还是激活函数的选取,都只能减轻梯度消失的症状,而不能解决梯度消失问题
梯度爆炸
在反向传播的过程中,很多梯度大于1,在链式求导法则下,累乘结果趋于无穷大,根据梯度下降的做法,在梯度爆炸下将会走很大一步,从而走出最优解域
解决方案
- 使用梯度截断法: Gradient Clipping
- 使用L1、L2正则化,使得w变小,进而使得反向传播的时候使得gradient变小(因为链式求导的时候,w也是作为乘积项的,导致梯度爆炸的原因可能是w过大)
- Batch Normalization,一种归一化手段,主要作用在Activations上面
- 激活函数也会影响,使用Relu优于tanh和sigmoid
- w的初始化,如果一开始w的绝对值比较大,更容易偷渡爆炸或者消失
- 网络Topolopy设计可以在一定程度上解决梯度消失或者爆炸
Normalization
常用的Normalization方法主要有: Batch Normalization(BN,2015 年)、Layer Normalization (LN , 2016 年)、 Instance Normalization ( IN , 2017 年 )、Group Normalization(GN,2018 年)。它们都是从激活函数的输入来考虑、做文章的,以不同的方式对激活函数的输入进行 Norm 的。
我们将输入的 feature map shape 记为[N, C, H, W],其中 N 表示 batch size,即 N 个样本;C 表示通道数;H、W 分别表示特征图的高度、宽度。这几个方法主要的区别就是在:
- BN 是在 batch 上,对 N、H、W 做归一化,而保留通道 C 的维度。BN 对较小的 batch size 效果不好。BN 适用于固定深度的前向神经网络,如 CNN,不适用于 RNN;
- LN 在通道方向上,对 C、H、W 归一化,主要对 RNN 效果明显
- IN 在图像像素上,对 H、W 做归一化,用在风格化迁移
- GN 将 channel 分组,然后再做归一化
Batch Normalization
BN的操作是这样的: 取一个特定的Batch,把这一个Batch中的每个图片的某个通道,作为一个归一化的对象,举例来说$[10,3,28,28]$的图片,当取第一个通道作为归一化的对象的时候,会将10幅图片的第一个通道都拿出来,将10*28*28的数据做归一化....
Batch Normalization 是一种巧妙而粗暴的方法来削弱 bad initialization 的影响,我们想要的是在非线性 activation 之前,输出值应该有比较好的分布(例如高斯分布), 以便于 back propagation 时计算 gradient,更新 weight。
主要思想: 针对每个神经元,使数据在进入激活函数之前,沿着通道计算每个 batch 的均值、方差,‘强迫’数据保持均值为 0,方差为 1 的正态分布,避免发生梯度消失。
为什么要进行BN
- 在深度神经网络训练的过程中,通常以输入网络的每一个 mini-batch 进行训练,这样每个 batch 具有不同的分布,使模型训练起来特别困难。
- Internal Covariate Shift (ICS) 问题:在训练的过程中,激活函数会改变各层数据的 分布,随着网络的加深,这种改变(差异)会越来越大,使模型训练起来特别困难,收敛速 度很慢,会出现梯度消失的问题。(也可以理解为病态矩阵问题,只要有微小的扰动,就会造成很大的改变)
BN的使用位置
全连接层或卷积操作之后,激活函数之前
BN算法过程
Batch Normalization 将输出值强行做一次 Gaussian Normalization 和线性变换
Input:Values of x over a mini-batch: $\mathcal{B} = {x_{1,\ldots,m}}$Parameters to be learned: $\gamma,\beta$
Output:${y_i=NB_{\gamma,\beta}(x_i)}$ $$ \mu_{\mathcal{B}} \leftarrow \frac{1}{m}\sum_{i=1}^mx_i \ \sigma_{\mathcal{B}}^2 \leftarrow \frac{1}{m}\sum_{i=1}^m(x_i-\mu_{\mathcal{B}})^2 \ \hat{x}_i \leftarrow \frac{x_i-\mu_{\mathcal{B}}}{\sqrt{\sigma_{\mathcal{B}}^2 + \epsilon}}\ y_i \leftarrow \gamma\hat{x}_i + \beta \equiv BN_{\gamma,\beta}(x_i) $$
Batch Normalization 中所有的操作都是平滑可导,这使得 back propagation 可以有效运行并学到相应的参数$\gamma,\beta$。需要注意的一点是 Batch Normalization 在 training 和testing 时行为有所差别。Training 时$\mu_{\mathcal{B}}$ 和$\sigma_{\mathcal{B}}$ 由当前 batch 计算得出;在 Testing 时$\mu_{\mathcal{B}}$ 和$\sigma_{\mathcal{B}}$ 应使用 Training 时保存的均值或类似的经过处理的值,而不是由当前 batch 计算。
加入缩放和平移变量(算法的最后一步)的原因是:保证每一次数据经过归一化后还保留原有学习来的特征,同时又能完成归一化操作,加速训练。 这两个参数是用来学习的参数。
BN的作用
- 允许较大的学习率
- 减弱对初始化的强依赖性
- 保持隐藏层中数值的均值、方差不变,让数值更稳定,为后面网络提供坚实的基础
- 有轻微的正则化作用(相当于给隐藏层加入噪声,类似 Dropout)
BN的问题
- 每次是在一个 batch 上计算均值、方差,如果 batch size 太小,则计算的均值、方差不足以代表整个数据分布。
- batch size 太大:会超过内存容量;需要跑更多的 epoch,导致总训练时间变长;会直接固定梯度下降的方向,导致很难更新
Layer Normlization
举例来说$[10,3,28,28]$的图片,当取第一张图片作为归一化的对象的时候,会将该图片的全部都拿出来,将3*28*28的数据做归一化....
Instance Normlization
举例来说$[10,3,28,28]$的图片,当取第一张图片的第一个通道作为归一化的对象的时候,会将该图片该通道全部都拿出来,将28*28的数据做归一化....
Group Normlization
举例来说$[10,4,28,28]$的图片,先将每一张图片的通道数划分为G份(假设为2),当取第一张图片的第一个份作为归一化的对象的时候,会将该图片该份全部都拿出来,将2*28*28的数据做归一化....