这里我们介绍梯度下降的基本概念。尽管很少实际中很少用到,但是了解梯度下降有益于理解随机梯度下降算法的关键。例如,由于学习率过高,优化问题可能会有所不同,这在梯度下降中也会出现。同样预处理是梯度下降中的常用技术。我们先从简单的入手。
1 一维梯度下降
一维梯度下降是一个很好的例子,可以用于理解梯度下降算法如何减小目标函数的值。对于连续可微的实值函数 f : R → R f: \mathbb{R} \rightarrow \mathbb{R} f:R→R。使用泰勒展开公式:
f ( x + ϵ ) = f ( x ) + ϵ f ′ ( x ) + O ( ϵ 2 ) f(x + \epsilon) = f(x) + \epsilon f'(x) + \mathcal{O}(\epsilon^2) f(x+ϵ)=f(x)+ϵf′(x)+O(ϵ2)
也就是说, f ( x + ϵ ) f(x+\epsilon) f(x+ϵ)近似为 f ( x ) f(x) f(x) 以及在 x x x处的一阶导数 f ′ ( x ) f'(x) f′(x)之和。假设 ϵ \epsilon ϵ 向梯度相反移动将会减少 f f f是合理的。为了简化,我们选择固定步长 η > 0 \eta > 0 η>0以及 ϵ = − η f ′ ( x ) \epsilon = -\eta f'(x) ϵ=−ηf′(x)。然后带入到泰勒展开中:
f ( x − η f ′ ( x ) ) = f ( x ) − η f ′ 2 ( x ) + O ( η 2 f ′ 2 ( x ) ) . f(x - \eta f'(x)) = f(x) - \eta f'^2(x) + \mathcal{O}(\eta^2 f'^2(x)). f(x−ηf′(x))=f(x)−ηf′2(x)+O(η2f′2(x)).
假设 f ′ ( x ) ≠ 0 f'(x) \neq 0 f′(x)=0 一直成立,因为 η f ′ 2 ( x ) > 0 \eta f'^2(x)>0 ηf′2(x)>0,将会起作用。如果选择的 η \eta η足够小的话,可以得到:
f ( x − η f ′ ( x ) ) ⪅ f ( x ) f(x - \eta f'(x)) \lessapprox f(x) f(x−ηf′(x))⪅f(x)
也就是说,我们通过
x ← x − η f ′ ( x ) x \leftarrow x - \eta f'(x) x←x−ηf′(x)
来迭代 x x x, f ( x ) f(x) f(x)函数的值也许会减小。在选择一个初始值 x x x 和一个常数 η > 0 \eta > 0 η>0然后一直迭代 x x x 直到达到停止条件,如, 当 ∣ f ′ ( x ) ∣ |f'(x)| ∣f′(x)∣足够小或是迭代次数达到一定值。
为了简单起见我们选择 f ( x ) = x 2 f(x)=x^2 f(x)=x2 作为目标函数来解释如何实现梯度下降。虽然我们知道 x = 0 x=0 x=0 时 f ( x ) f(x) f(x)为的最小值, 我们将使用一个简单的函数来观察 x x x的变化。
from d2l import mxnet as d2l
from mxnet import npx, np
import plotly.graph_objs as go
npx.set_np()
f = lambda x:x**2
gradf = lambda x: 2*x
我们使用 x=10 作为初始值并假设 η=0.2 。使用梯度下降进行迭代 x 10次。
def gd(eta):
x = 10.0
result = [x]
for i in range(10):
x -= eta*gradf(x)
result.append(float(x))
print(f'[epoch {i+1}] x: {x:.3f}')
return result
res = gd(0.2)
使用plotly做可视化:
def show_trace(res):
n = max(abs(min(res)), abs(max(res)))
x = np.arange(-n, n, 0.01)
fig = go.Figure()
fig.add_trace(go.Scatter(x=x.tolist(), y=f(x).tolist() , mode='lines'))
fig.add_trace(go.Scatter(x=res, y=f(np.array(res)).tolist() , mode='lines+markers', marker={
'size':8}))
fig.update_layout(width=500, height = 380, xaxis_title='x', yaxis_title='f(x)')
fig.show()
show_trace(res)
1.1 学习率
学习率 η 可以由算法设计者设置。如果我们使用的学习率太小,将导致 x 更新非常缓慢,需要更多迭代才能获得更好的解决方案。为了说明在这种情况下会发生什么,请考虑针对 η=0.05 。如我们所见,即使经过10个步骤,我们仍然离最佳解决方案还差得很远。
show_trace(gd(0.05))
相反,如果我们使用过高的学习率, ∣ η f ′ ( x ) ∣ \left|\eta f'(x)\right| ∣ηf′(x)∣ 对于一阶泰勒展开式而言,可能太大。 就是说 O ( η 2 f ′ 2 ( x ) ) \mathcal{O}(\eta^2 f'^2(x)) O(η2f′2(x))的影响更大。 在这种情况下,我们不能保证 x x x 能够降低 f ( x ) f(x) f(x)。例如, 当我们将学习率设置为 η = 1.1 \eta=1.1 η=1.1, x x x 越过了 x = 0 x=0 x=0 并且逐步发散。
show_trace(gd(1.1))
1.2 局部极小值
请考虑以下情况: f ( x ) = x ⋅ cos c x f(x) = x \cdot \cos c x f(x)=x⋅coscx。此函数具有无限多个局部最小值。下面展示了高学习率导致较差的局部最小值。
c = np.array(0.15 * np.pi)
f = lambda x: x * np.cos(c * x)
gradf = lambda x: np.cos(c * x) - c * x * np.sin(c * x)
show_trace(gd(2))
2.2 多元梯度下降
为了了解算法在实际中如何工作,这里构造了一个函数 f ( x ) = x 1 2 + 2 x 2 2 f(\mathbf{x})=x_1^2+2x_2^2 f(x)=x12+2x22 以及一个二维向量 x = [ x 1 , x 2 ] ⊤ \mathbf{x} = [x_1, x_2]^\top x=[x1,x2]⊤作为输入,标量作为输出。梯度由下式给出 ∇ f ( x ) = [ 2 x 1 , 4 x 2 ] ⊤ \nabla f(\mathbf{x}) = [2x_1, 4x_2]^\top ∇f(x)=[2x1,4x2]⊤。我们将观察到轨迹 x \mathbf{x} x从初始位置 [ − 5 , − 2 ] [-5, -2] [−5,−2]开始下降。
def train_2d(trainer, steps=20):
"""优化二维目标"""
# s1和s2是内部状态变量
x1, x2, s1, s2 = -5, -2, 0, 0
results = [[x1], [x2]]
for i in range(steps):
x1, x2, s1, s2 = trainer(x1, x2, s1, s2)
results[0].append(x1)
results[1].append(x2)
return results
def show_trace_2d(f, results):
"""2D可视化"""
fig = go.Figure()
x1, x2 = np.meshgrid(np.arange(-5.5, 1.0, 0.1), np.arange(-3.0, 1.0, 0.1))
fig.add_trace(go.Contour(x=x1[0].tolist(), y=x2.T[0].tolist(), z=f(x1, x2).tolist(),showscale=False , colorscale='YlGnBu'))
fig.add_trace(go.Scatter(x=results[0], y=results[1] , mode='lines+markers', marker={
'size':8}))
fig.update_layout(width=500, height = 380, xaxis_title='x1', yaxis_title='x2')
fig.show()
接下来,我们观察优化变量的轨迹 x x x 学习率 η = 0.1 η=0.1 η=0.1 。我们可以看到,经过20个步骤, x x x 在接近其最小值 [ 0 , 0 ] [0,0] [0,0] 。尽管进展相当缓慢,但进展还是相当不错的。
f = lambda x1, x2: x1 ** 2 + 2 * x2 ** 2 # 目的函数
gradf = lambda x1, x2: (2 * x1, 4 * x2) # 辅助函数
def gd(x1, x2, s1, s2):
(g1, g2) = gradf(x1, x2) # 计算梯度
return (x1 - eta * g1, x2 - eta * g2, 0, 0) # 更新
eta = 0.1
show_trace_2d(f, train_2d(gd))
3.随机梯度下降
3.1 随机梯度更新
在深度学习中,目标函数通常是训练数据集中每个示例的损失函数的平均值。 假设 n n n 个样本 以 f i ( x ) f_i(\mathbf{x}) fi(x) 为损失函数的数据集, 索引 i i i, 参数向量 x \mathbf{x} x, 我们的目标函数如下:
f ( x ) = 1 n ∑ i = 1 n f i ( x ) . f(\mathbf{x}) = \frac{1}{n} \sum_{i = 1}^n f_i(\mathbf{x}). f(x)=n1i=1∑nfi(x).
目标函数在 x \mathbf{x} x的梯度计算如下:
∇ f ( x ) = 1 n ∑ i = 1 n ∇ f i ( x ) . \nabla f(\mathbf{x}) = \frac{1}{n} \sum_{i = 1}^n \nabla f_i(\mathbf{x}). ∇f(x)=n1i=1∑n∇fi(x).
如果使用梯度下降,则每个自变量迭代的计算成本为 O ( n ) \mathcal{O}(n) O(n), 随着 n n n线性增加。 当模型训练数据集很大时,每次迭代的梯度下降成本将非常高。
随机梯度下降 (SGD) 降低了每次迭代的计算成本. 在随机梯度下降的每次迭代中, 我们统一采样了一个索引 i ∈ { 1 , … , n } i\in\{1,\ldots, n\} i∈{ 1,…,n} 来获取数据, 并且计算梯度 ∇ f i ( x ) \nabla f_i(\mathbf{x}) ∇fi(x) 来更新 x \mathbf{x} x:
x ← x − η ∇ f i ( x ) . \mathbf{x} \leftarrow \mathbf{x} - \eta \nabla f_i(\mathbf{x}). x←x−η∇fi(x).
η \eta η 是学习速率。 每次迭代的计算成本从 O ( n ) \mathcal{O}(n) O(n) 下降到常数 O ( 1 ) \mathcal{O}(1) O(1). 随机梯度 ∇ f i ( x ) \nabla f_i(\mathbf{x}) ∇fi(x) 是梯度 ∇ f ( x ) \nabla f(\mathbf{x}) ∇f(x)的无偏估计
E i ∇ f i ( x ) = 1 n ∑ i = 1 n ∇ f i ( x ) = ∇ f ( x ) . \mathbb{E}_i \nabla f_i(\mathbf{x}) = \frac{1}{n} \sum_{i = 1}^n \nabla f_i(\mathbf{x}) = \nabla f(\mathbf{x}). Ei∇fi(x)=n1i=1∑n∇fi(x)=∇f(x).
平均而言,随机梯度是对梯度的良好估计。
现在,我们将通过向梯度中添加平均值为0,方差为1的随机噪声来模拟SGD,将其与梯度下降进行比较。
f = lambda x1, x2: x1 ** 2 + 2 * x2 ** 2 # 目标函数
gradf = lambda x1, x2: (2 * x1, 4 * x2) # 辅助函数
trans = lambda x: x.tolist()[0]
def sgd(x1, x2, s1, s2):
global lr # 学习率调度
(g1, g2) = gradf(x1, x2)
# Simulate noisy gradient
g1 += np.random.normal(0.0, 1, (1,))
g2 += np.random.normal(0.0, 1, (1,))
eta_t = eta * lr() # 时间t的学习率
return (trans(x1 - eta_t * g1), trans(x2 - eta_t * g2), 0, 0) # 更新变量
eta = 0.1
lr = (lambda: 1) # 保持学习率
show_trace_2d(f, train_2d(sgd, steps=50))
由于梯度增加了随机性,SGD中变量的轨迹比上一节中在梯度下降中观察到的更加不稳定。这是由于实验所添加的噪声使模拟的随机梯度的准确度下降。在实际中,这些噪声通常指训练数据集中的无意义的干扰。
3.2 动态学习率
使用随时间变化的学习率函数 η ( t ) \eta(t) η(t)可以增加控制优化算法收敛的复杂性。需要搞清楚以何种速度衰减 η \eta η 。如果太快, 将会过早停止优化,如果太慢,就会浪费大量时间。有一些用于调整的基本策略:
η ( t ) = η i if t i ≤ t ≤ t i + 1 分 段 常 数 η ( t ) = η 0 ⋅ e − λ t 指 数 η ( t ) = η 0 ⋅ ( β t + 1 ) − α 多 项 式 \begin{aligned} \eta(t) & = \eta_i \text{ if } t_i \leq t \leq t_{i+1} && \mathrm{分段常数} \\ \eta(t) & = \eta_0 \cdot e^{-\lambda t} && \mathrm{指数} \\ \eta(t) & = \eta_0 \cdot (\beta t + 1)^{-\alpha} && \mathrm{多项式} \end{aligned} η(t)η(t)η(t)=ηi if ti≤t≤ti+1=η0⋅e−λt=η0⋅(βt+1)−α分段常数指数多项式
在第一种情况下,例如,每当优化进度停滞时,我们都会降低学习率。这是训练深度网络的常用策略。另外,我们可以通过指数衰减更加主动地降低它。不幸的是,这导致算法收敛之前过早停止。一个流行的选择是多项式衰减 以 α = 0.5 \alpha = 0.5 α=0.5。
import math
def exponential():
global ctr
ctr += 1
return math.exp(-0.1 * ctr)
ctr = 1
lr = exponential # 设置动态更新的学习率
show_trace_2d(f, train_2d(sgd, steps=1000))
跟预期的相同,参数的差异明显减小。但是,这是以无法收敛到最佳解决方案为代价。即使经过1000步,我们仍然离最佳解决方案还很远。实际上,该算法根本无法收敛。另一方面,如果我们使用多项式衰减,则学习率衰减与步数的平方根成反比,则收敛是好的。
def polynomial():
global ctr
ctr += 1
return (1 + 0.1 * ctr)**(-0.5)
ctr = 1
lr = polynomial
show_trace_2d(f, train_2d(sgd, steps=50))
4.参考
https://d2l.ai/chapter_optimization/gd.html
5.代码
来源:oschina
链接:https://my.oschina.net/u/4344048/blog/4658017