【神经网络】梯度消失与梯度爆炸问题
梯度消失与梯度爆炸问题
Glorot 和 He 初始化
- 我们需要信号在两个方向上正确流动:进行预测时,信号为正向;在反向传播梯度时,信号为反向。我们需要每层输出的方差等于输入的方差,并且在反方向流过某层之前和之后的梯度具有相同的方差。Glorot初始化(使用逻辑激活函数时)按照下列公式随机初始化每层的连接权重,其中
f
a
n
a
v
g
=
(
f
a
n
i
n
+
f
a
n
o
u
t
)
/
2
fan_{avg}=(fan_{in}+fan_{out})/2
使用Glorot初始化可以大大加快训练速度。
- 默认情况下,Keras使用具有均匀分布的Glorot初始化。创建层时,可以通过设置 kernel_initializer="he_uniform"或kernel_initializer="he_normal"来将其更改为He初始化。
keras.layers.Dense(10, activation="relu", kernel_initializer="he_normal")
- 如果要使用均匀分布,但基于fanavg而不是fanin进行He初始化,则可以使用Variance Scaling:
he_avg_init = keras.initializers.VarianceScaling(scale=2., mode='fan_avg', distribution='uniform')
keras.layers.Dense(10, activation="sigmoid", kenel_initializer=he_avg_init)
非饱和激活函数
- ReLU函数的变体,例如leaky ReLU。该函数定义为
L
e
a
k
y
R
e
L
U
α
=
m
a
x
(
α
z
,
z
)
LeakyReLU_{\alpha}=max(\alpha_{z},z)
超参数α
\alpha
α
=
0.2
\alpha = 0.2
α
=
0.01
\alpha = 0.01
- 2015年提出了一种新的激活函数,称为指数线性单元(Exponential Linear Unit,ELU)。
超参数α
\alpha
z
z
对于z
<
0
z<0
- 产生自归一化的条件:
- 输入特征必须是标准化的(平均值为0,标准差为1);
- 每个隐藏层的权重必须使用LeCun正态初始化。在Keras中,这意味着设置 kernel_initializer=“lecun_normal”。
- 网络的架构必须是顺序的。
- 使用激活函数,通常 SELU>ELU>Leaky ReLU(及其变体)>ReLU>tanh>logistic。
- 使用leaky ReLU 激活函数,创建一个 LeakyReLU 层,并将其添加到想要应用它的层之后的模型中:
model = keras.models.Sequential([
[...]
keras.layers.Dense(10, kernel_initializer="he_normal"),
keras.layers.LeakyReLU(alpha=0.2),
[...]
])
- 对于SELU激活,在创建层时设置activation="selu"和 kernel_initializer=“lecun_normal”:
layer = keras.layers.Dense(10, activation="selu",
kernel_initializer="lecun_normal")
批量归一化
- 2015年提出一种称为批量归一化(BN)的技术来解决梯度消失/梯度爆炸问题。该操作可以使模型学习各层输入的最佳缩放和均值。
- 为了使输入零中心并归一化,该算法需要估计每个输入的均值和标准差。
- 批量归一化算法如下:
1.
η
B
=
1
m
B
∑
i
=
1
m
B
x
(
i
)
2.
δ
2
=
1
m
B
∑
i
=
1
m
B
(
x
(
i
)
−
η
B
)
2
3.
x
^
(
i
)
=
x
(
i
)
−
η
B
δ
2
+
ε
4.
z
(
i
)
=
γ
⊗
x
^
(
i
)
+
β
\begin{array}{ll} 1.&\eta _{B} = \frac{1}{m_B} {\sum_{i = 1}^{m_B} x^{(i)} }\\ 2.&\delta ^2 = \frac{1}{m_B} {\sum_{i = 1}^{m_B} (x^{(i)}-\eta_{B})^2 }\\ 3.&\hat{x}^{(i)} = \frac{x^{(i)}-\eta_{B}}{\sqrt{\delta ^2+\varepsilon } }\\ 4.&z^{(i)}=\gamma \otimes \hat{x}^{(i)}+\beta \end{array}
m
B
m_B
γ
\gamma
⊗
\otimes
β
\beta
z
(
i
)
z^{(i)}
- 在训练期间,BN会归一化其输入,那在测试期间呢?我们需要对单个实例,而不是成批次的实例做出预测,此时计算统计量是不可靠的。
- 大多数批量归一化的实现都是通过使用该层输入的 均值和标准差 的移动平均值 来估计训练期间的最终统计信息。这是Keras在使用BatchNormalization层时自动执行的操作。
- 综上所述,在每个批归一化层学习了四个参数向量:
- 通过常规反向传播学习
γ
\gamma
β
\beta
- 使用指数移动平均值估计的
η
\eta
δ
\delta
- 通过常规反向传播学习
用Keras实现批量归一化
- 实现批量归一化,只需在每个隐藏层的激活函数之前或之后添加一个BatchNormalization层,然后可选地在模型的第一层后添加一个BN层。
import tensorflow as tf
from tensorflow import keras
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.BatchNormalization(),
keras.layers.Dense(300, activation="elu", kernel_initializer="he_normal"),
keras.layers.BatchNormalization(),
keras.layers.Dense(100, activation="elu", kernel_initializer="he_normal"),
keras.layers.BatchNormalization(),
keras.layers.Dense(10, activation="softmax")
])
ESLint
- 看一下第一个BN层的参数。两个是可训练的(通过反向传播),两个不是:
- 在Keras中创建BN层时,还会创建两个操作,在训练期间的每次迭代中,Keras都会调用这两个操作。这些操作会更新移动平均值。
- BN论文的作者主张在激活函数之前添加BN层,要在激活函数之前添加BN层,必须从隐藏层中删除激活函数,并将其作为单独的层添加到BN层之后。此外,由于批量归一化层的每个输入都包含一个偏移参数,因此可以从上一层中删除偏置项(创建时只需传递use_bias=False即可):
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.BatchNormalization(),
keras.layers.Dense(300, kernel_initializer="he_normal", use_bias=False),
keras.layers.BatchNormalization(),
keras.layers.Activation("elu"),
keras.layers.Dense(100, kernel_initializer="he_normal", use_bias=False),
keras.layers.BatchNormalization(),
keras.layers.Activation("elu"),
keras.layers.Dense(10, activation="softmax")
])
- BatchNormalization类具有许多可以调整的超参数,偶尔可能需要调整momentum。BatchNormalization层在更新指数移动平均值时使用此超参数。给定一个新值(即在当前批次中计算的输入均值或标准差的新向量),该层使用以下公式来更新运行时平均
v
^
\hat{v}
\hat{v}\gets \hat{v}\times momentum+v\times(1-momentum)
一个良好的动量值通常接近于1,例如0.9、0.99。
edge
- 另一个重要的超参数是 axis:它确定哪个轴该被归一化。默认为-1,对最后一个轴进行归一化。
梯度裁剪
- 缓解梯度爆炸问题的另一个流行技术是:在反向传播期间裁剪梯度,使它们永远不会超过某个阈值,称为梯度裁剪。这种技术最常用于循环神经网络,因为在RNN难以使用批量归一化。
- 在Keras中实现梯度裁剪,即是在创建优化器时设置 clipvalue 或 clipnorm 参数的问题:
optimizer = keras.optimizers.SGD(clipvalue=1.0)
model.compile(loss="mse", optimizer=optimizer)
- 如果要确保“梯度裁剪”不更改梯度向量的方向,应该设置clipnorm按照范数来裁剪。