深度学习激活函数与正则化问题

作者: pdnbplus | 发布时间: 2024/06/21 | 阅读量: 174

深度学习激活函数与正则化问题--潘登同学的深度学习笔记

梯度消失问题(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 分别表示特征图的高度、宽度。这几个方法主要的区别就是在:

  1. BN 是在 batch 上,对 N、H、W 做归一化,而保留通道 C 的维度。BN 对较小的 batch size 效果不好。BN 适用于固定深度的前向神经网络,如 CNN,不适用于 RNN;
  2. LN 在通道方向上,对 C、H、W 归一化,主要对 RNN 效果明显
  3. IN 在图像像素上,对 H、W 做归一化,用在风格化迁移
  4. 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

  1. 在深度神经网络训练的过程中,通常以输入网络的每一个 mini-batch 进行训练,这样每个 batch 具有不同的分布,使模型训练起来特别困难。
  2. 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的作用

  1. 允许较大的学习率
  2. 减弱对初始化的强依赖性
  3. 保持隐藏层中数值的均值、方差不变,让数值更稳定,为后面网络提供坚实的基础
  4. 有轻微的正则化作用(相当于给隐藏层加入噪声,类似 Dropout)

BN的问题

  1. 每次是在一个 batch 上计算均值、方差,如果 batch size 太小,则计算的均值、方差不足以代表整个数据分布。
  2. 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的数据做归一化....