第八章 深度学习中的优化
2020-2-15 深度学习笔记8 - 深度学习中的优化1(与纯优化区别-基于梯度下降,神经网络优化-下降到足够小即可)
基本算法
1.随机梯度下降(SGD)–应用最多
随机梯度下降
(SGD)及其变种很可能是一般机器学习中应用最多的优化算法,特别是在深度学习中。按照数据生成分布抽取m个小批量(独立同分布的)样本,通过计算它们梯度均值,我们可以得到梯度的无偏估计 。
SGD 算法中的一个关键参数是学习率。
因为 SGD 中梯度估计引入的噪声源(m个训练样本的随机采样)并不会在极小点处消失,所以在实践中,有必要随着时间的推移逐渐降低学习率,因此我们将第k步迭代的学习率记作
ϵ
k
\epsilon_k
ϵk(相比之下,当我们使用批量梯度下降到达极小点时,整个代价函数的真实梯度会变得很小,之后为 0,因此批量梯度下降可以使用固定的学习率)保证 SGD 收敛的一个充分条件是:
∑
k
=
1
∞
ϵ
k
=
∞
\sum_{k=1}^∞ϵ_k=∞
k=1∑∞ϵk=∞
且
∑
k
=
1
∞
ϵ
k
2
<
∞
\sum_{k=1}^∞ϵ^2_k<∞
k=1∑∞ϵk2<∞
实践中,一般会线性衰减学习率直到第
τ
\tau
τ次迭代:
ϵ
k
=
(
1
−
α
)
ϵ
0
+
α
ϵ
τ
ϵ_k=(1−α)ϵ_0+αϵ_τ
ϵk=(1−α)ϵ0+αϵτ
其中
α
=
k
τ
\alpha = \frac{k}{\tau}
α=τk。 在
τ
\tau
τ步迭代之后,一般使
ϵ
\epsilon
ϵ保持常数。
使用线性策略时,需要选择的参数为 ϵ 0 \epsilon_{0} ϵ0, ϵ τ \epsilon_{\tau} ϵτ, τ \tau τ。通常τ被设为需要反复遍历训练集几百次的迭代次数。通常 ϵ τ \epsilon_{\tau} ϵτ应设为大约 ϵ 0 \epsilon_{0} ϵ0的1%。
主要问题是如何设置
ϵ
0
\epsilon_{0}
ϵ0。
如果学习率太大,学习曲线将会剧烈振荡,代价函数值通常会明显增加。温和的振荡是良好的,容易在训练随机代价函数(例如使用Dropout的代价函数)时出现。
如果学习率太小,那么学习过程会很缓慢。如果初始学习率太低,那么学习可能会卡在一个相当高的代价值。
通常,就总训练时间和最终代价值而言,最优初始学习率会高于大约迭代100次左右后达到最佳效果的学习率。因此,通常最好是检测最早的几轮迭代,选择一个比在效果上表现最佳的学习率,但又不能太大导致严重的震荡。
SGD 及相关的小批量亦或更广义的基于梯度优化的在线学习算法,一个重要的性质是每一步更新的计算时间不依赖训练样本数目的多寡。即使训练样本数目非常大时,它们也能收敛。
对于足够大的数据集,SGD可能会在处理整个训练集之前就收敛到最终测试集误差的某个固定容差范围内。
随机梯度下降仍然是非常受欢迎的优化方法,但其学习过程有时会很慢。
【python实现】
以线性回归为例:
平方误差损失函数【公式
J
(
θ
)
=
1
2
(
y
−
y
ˉ
)
2
J(\theta)=\frac12(y-\bar{y})^2
J(θ)=21(y−yˉ)2,y为真实值,
y
ˉ
\bar{y}
yˉ为预测值】的最小化的优化问题
J
(
X
)
=
1
2
m
(
f
(
X
)
−
y
)
2
J(X)=\frac12m(f(X)-y)^2
J(X)=21m(f(X)−y)2, f(X)=wX,m为样本总数, 最优化函数J(X)对w的梯度为X(wX-y)
输入X:n*d, y:n*1
初始化W:d*1
import numpy as np
from numpy.linalg import norm, inv
from itertools import cycle
def grad(X, y, W):
return X.T @ (X @ W - y) / X.shape[0]
def sgd(X, y, epsilon=1e-2, max_iter=3600, eta=0.1):
# 随机梯度下降
i = 0
W = np.ones((X.shape[1], 1))
while i < max_iter:
indx = np.random.randint(X.shape[0], size=1) # 随机选择一个样本
X_s, y_s = X[(indx)], y[(indx)]
Grad = grad(X_s, y_s, W)
if norm(Grad) < epsilon:
break
W -= eta * Grad
i += 1
return W
说明:
1.X.T,矩阵转置
2.tf.matmul(A,C)=np.dot(A,C)= A@C都属于叉乘(2个矩阵相乘,矢量积)
2-动量(momentum)
损失通常高度敏感于参数空间中的某些方向,而不敏感于其他。动量算法可以在一定程度缓解这些问题,但这样做的代价是引入了另一个超参数【在开始学习过程之前设置值的参数,而不是通过训练得到的参数数据】。
动量算法主要有两个作用:
1、解决随机梯度下降算法梯度的高方差问题,使摆动不至于太剧烈:增加动量项,可以近似认为增加了梯度的采样的样本数(最近时间的梯度会有比较大的权重),根据 σ n \frac σ{\sqrt n} nσ可知,方差减小
2、加大了步长,提高了收敛速度:每一次梯度都包含正确的梯度方向和方差引起的摆动,增加动量,相当于将之前多个梯度叠加,增加了共同方向(期望梯度方向),因此,相等与增大了步长。
如果动量算法总是观测到梯度
g
g
g,那么它会在方向
−
g
-g
−g上不停加速,直到达到最终速度,其中步长大小为
ϵ
∥
g
∥
1
−
α
\frac{ϵ∥g∥}{1−α}
1−αϵ∥g∥
因此将动量的超参数视为
1
1
−
α
\frac{1}{1-\alpha}
1−α1有助于理解。 例如,
α
=
0.9
\alpha=0.9
α=0.9对应着最大速度
10
10
10倍于梯度下降算法。
在实践中, α \alpha α的一般取值为 0.5 0.5 0.5, 0.9 0.9 0.9和 0.99 0.99 0.99。 和学习率一样, α \alpha α也会随着时间不断调整。 一般初始值是一个较小的值,随后会慢慢变大。 随着时间推移调整 α \alpha α没有收缩 ϵ \epsilon ϵ重要。
【这段很有用】
我们可以将动量算法视为模拟连续时间下牛顿动力学下的粒子。这种物理类比有助于直觉上理解动量和梯度下降算法如何表现。如果代价函数的梯度是唯一的力,那么粒子可能永远不会停下来。想象一下,假设理想情况下冰面没有摩擦,一个冰球从山谷一端下滑,上升到另一端,永远来回振荡。要解决这个问题,我们添加一个正比于−v(t)的力。在物理术语中,此力对应于粘性阻力,就像粒子必须通过一个抵抗介质,如糖浆。这会导致粒子随着时间推移逐渐失去能量,最终收敛到局部极小点。
【python实现】
step1、初始化V
def initialize_velocity(parameters):
#Initializes the velocity as a python dictionary with:
#- keys: “dW1”, “db1”, …, “dWL”, “dbL”
#- values: numpy arrays of zeros of the same shape as the corresponding gradients/parameters.
#Arguments:
#parameters – python dictionary containing your parameters.
#parameters[‘W’ + str(l)] = Wl
#parameters[‘b’ + str(l)] = bl
#Returns:
#v -- python dictionary containing the current velocity.
# v['dW' + str(l)] = velocity of dWl
# v['db' + str(l)] = velocity of dbl
L = len(parameters) // 2 # number of layers in the neural networks
v = {}
# Initialize velocity
for l in range(L):
### START CODE HERE ### (approx. 2 lines)
v["dW" + str(l+1)] = np.zeros_like(parameters["W" + str(l+1)])
v["db" + str(l+1)] = np.zeros_like(parameters["b" + str(l+1)])
### END CODE HERE ###
return v
step2、更新权值
def update_parameters_with_momentum(parameters, grads, v, beta, learning_rate):
#Update parameters using Momentum
#Arguments:
#parameters -- python dictionary containing your parameters:
# parameters['W' + str(l)] = Wl
# parameters['b' + str(l)] = bl
#grads -- python dictionary containing your gradients for each parameters:
# grads['dW' + str(l)] = dWl
# grads['db' + str(l)] = dbl
#v -- python dictionary containing the current velocity:
# v['dW' + str(l)] = ...
# v['db' + str(l)] = ...
#beta -- the momentum hyperparameter, scalar
#learning_rate -- the learning rate, scalar
#Returns:
#parameters -- python dictionary containing your updated parameters
#v -- python dictionary containing your updated velocities
L = len(parameters) // 2 # number of layers in the neural networks
# Momentum update for each parameter
for l in range(L):
### START CODE HERE ### (approx. 4 lines)
# compute velocities
v["dW" + str(l+1)] = beta *v["dW" + str(l+1)] +(1-beta)*grads["dW" + str(l+1)]
v["db" + str(l+1)] = beta *v["db" + str(l+1)] +(1-beta)*grads["db" + str(l+1)]
# update parameters
parameters["W" + str(l+1)] = parameters["W" + str(l+1)] - learning_rate * v["dW" + str(l+1)]
parameters["b" + str(l+1)] = parameters["b" + str(l+1)] - learning_rate * v["db" + str(l+1)]
### END CODE HERE ###
return parameters, v
超参数 :α和β,β一般取0.9,0.99
3.Nesterov动量-添加较正因子
受Nesterov加速梯度算法启发,Sutskever et al. 提出了动量算法的一个变种。 这种情况的更新规则如下:
v
←
α
v
−
ϵ
∇
θ
[
1
m
∑
i
=
1
m
L
(
f
(
x
(
i
)
;
θ
+
α
v
)
,
y
(
i
)
)
]
,
θ
←
θ
+
v
,
v←αv−ϵ∇_θ[\frac1m \sum_{i=1}^mL(f(x^{(i)};θ+αv),y^{(i)})],θ←θ+v,
v←αv−ϵ∇θ[m1i=1∑mL(f(x(i);θ+αv),y(i))],θ←θ+v,
其中参数
α
\alpha
α和
ϵ
\epsilon
ϵ发挥了和标准动量方法中类似的作用。 Nesterov 动量和标准动量之间的区别体现在梯度计算上。 Nesterov 动量中,梯度计算在施加当前速度之后。 因此,Nesterov 动量可以解释为往标准动量方法中添加了一个校正因子。
参数初始化策略
深度学习模型的训练算法通常是迭代的,因此要求使用者指定一些开始迭代的初始点。训练深度模型是一个足够困难的问题,以致于大多数算法都很大程度地受到初始化选择的影响。当学习收敛时,初始点可以决定学习收敛得多快,以及是否收敛到一个代价高或低的点。
我们对于初始点如何影响泛化的理解是相当原始的,几乎没有提供如何选择初始点的任何指导。也许完全确知的唯一特性是初始参数需要在不同单元间破坏对称性。如果具有相同激活函数的两个隐藏单元连接到相同的输入,那么这些单元必须具有不同的初始参数。如果它们具有相同的初始参数,然后应用到确定性损失和模型的确定性学习算法将一直以相同的方式更新这两个单元。
我们几乎总是初始化模型的权重为高斯或均匀分布中随机抽取的值。高斯或均匀分布的选择似乎不会有很大的差别,但也没有被详尽地研究。然而,初始分布的大小确实对优化过程的结果和网络泛化能力都有很大的影响。
更大的初始权重具有更强的破坏对称性的作用,有助于避免冗余的单元,也有助于避免在每层线性成分的前向或反向传播中丢失信号——矩阵中更大的值在矩阵乘法中有更大的输出。如果初始权重太大,那么会在前向传播或反向传播中产生爆炸的值。
关于如何初始化网络,正则化和优化有着非常不同的观点。优化观点建议权重应该足够大以成功传播信息,但是正则化希望其小一点。
有些启发式方法可用于选择权重的初始大小。 一种初始化
m
m
m个输入和
n
n
n输出的全连接层的权重的启发式方法是从分布
U
(
−
1
m
,
1
m
)
U(-\frac{1}{\sqrt{m}}, \frac{1}{\sqrt{m}})
U(−m1,m1)中采样权重, 而Glorot and Bengio建议使用标准初始化
(normalized initialization)
W
(
i
,
j
)
∼
U
(
−
6
m
+
n
,
6
m
+
n
)
W_{(i,j)}∼U(−\sqrt{\frac6{m+n}},\sqrt{\frac6{m+n}})
W(i,j)∼U(−m+n6,m+n6)
这种启发式方法初始化所有的层,折衷于使其具有相同激活方差和使其具有相同梯度方差之间。
计算资源允许的话,将每层权重的初始数值范围设为超参数通常是个好主意,使用超参数搜索算法,如随机搜索,挑选这些数值范围。
设置偏置的方法必须和设置权重的方法协调。设置偏置为零通常在大多数权重初始化方案中是可行的。存在一些我们可能设置偏置为非零值的情况:
- 如果偏置是作为输出单元,那么初始化偏置以获取正确的输出边缘统计通常是有利的。
- 有时,我们可能想要选择偏置以避免初始化引起太大饱和。例如,我们可能会将 ReLU 的隐藏单元设为 0.1 而非 0,以避免 ReLU 在初始化时饱和。
- 有时,一个单元会控制其他单元能否参与到等式中。在这种情况下,我们有一个单元输出u,另一个单元h ∈ [0, 1],那么我们可以将h视作门,以决定uh ≈ 1还是uh ≈ 0。在这种情形下,我们希望设置偏置h,使得在初始化的大多数情况下h ≈ 1。否则,u没有机会学习。