Task07: 凸优化;梯度下降;优化算法进阶 学习笔记

Task07: 凸优化;梯度下降;优化算法进阶学习笔记

凸优化

优化与估计
尽管优化方法可以最小化深度学习中的损失函数值,但本质上优化方法达到的目标与深度学习的目标并不相同。

优化方法目标:训练集损失函数值
深度学习目标:测试集损失函数值(泛化性)
%matplotlib inline
import sys
sys.path.append(’/home/kesci/input’)
import d2lzh1981 as d2l
from mpl_toolkits import mplot3d # 三维画图
import numpy as np
def f(x): return x * np.cos(np.pi * x)
def g(x): return f(x) + 0.2 * np.cos(5 * np.pi * x)

d2l.set_figsize((5, 3))
x = np.arange(0.5, 1.5, 0.01)
fig_f, = d2l.plt.plot(x, f(x),label=“train error”)
fig_g, = d2l.plt.plot(x, g(x),’–’, c=‘purple’, label=“test error”)
fig_f.axes.annotate(‘empirical risk’, (1.0, -1.2), (0.5, -1.1),arrowprops=dict(arrowstyle=’->’))
fig_g.axes.annotate(‘expected risk’, (1.1, -1.05), (0.95, -0.5),arrowprops=dict(arrowstyle=’->’))
d2l.plt.xlabel(‘x’)
d2l.plt.ylabel(‘risk’)
d2l.plt.legend(loc=“upper right”)

优化在深度学习中的挑战
局部最小值
鞍点
梯度消失
局部最小值
f(x)=xcosπx

def f(x):
return x * np.cos(np.pi * x)

d2l.set_figsize((4.5, 2.5))
x = np.arange(-1.0, 2.0, 0.1)
fig, = d2l.plt.plot(x, f(x))
fig.axes.annotate(‘local minimum’, xy=(-0.3, -0.25), xytext=(-0.77, -1.0),
arrowprops=dict(arrowstyle=’->’))
fig.axes.annotate(‘global minimum’, xy=(1.1, -0.95), xytext=(0.6, 0.8),
arrowprops=dict(arrowstyle=’->’))
d2l.plt.xlabel(‘x’)
d2l.plt.ylabel(‘f(x)’);

鞍点
x = np.arange(-2.0, 2.0, 0.1)
fig, = d2l.plt.plot(x, x**3)
fig.axes.annotate(‘saddle point’, xy=(0, -0.2), xytext=(-0.52, -5.0),
arrowprops=dict(arrowstyle=’->’))
d2l.plt.xlabel(‘x’)
d2l.plt.ylabel(‘f(x)’);

A=⎡⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢∂2f∂x21∂2f∂x2∂x1⋮∂2f∂xn∂x1∂2f∂x1∂x2∂2f∂x22⋮∂2f∂xn∂x2⋯⋯⋱⋯∂2f∂x1∂xn∂2f∂x2∂xn⋮∂2f∂x2n⎤⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥

e.g.

x, y = np.mgrid[-1: 1: 31j, -1: 1: 31j]
z = x2 - y2

d2l.set_figsize((6, 4))
ax = d2l.plt.figure().add_subplot(111, projection=‘3d’)
ax.plot_wireframe(x, y, z, **{‘rstride’: 2, ‘cstride’: 2})
ax.plot([0], [0], [0], ‘ro’, markersize=10)
ticks = [-1, 0, 1]
d2l.plt.xticks(ticks)
d2l.plt.yticks(ticks)
ax.set_zticks(ticks)
d2l.plt.xlabel(‘x’)
d2l.plt.ylabel(‘y’);

梯度消失
x = np.arange(-2.0, 5.0, 0.01)
fig, = d2l.plt.plot(x, np.tanh(x))
d2l.plt.xlabel(‘x’)
d2l.plt.ylabel(‘f(x)’)
fig.axes.annotate(‘vanishing gradient’, (4, 1), (2, 0.0) ,arrowprops=dict(arrowstyle=’->’))
Text(2, 0.0, ‘vanishing gradient’)

凸性 (Convexity)
基础
集合
Image NameImage NameImage Name

函数
λf(x)+(1−λ)f(x′)≥f(λx+(1−λ)x′)

def f(x):
return 0.5 * x**2 # Convex

def g(x):
return np.cos(np.pi * x) # Nonconvex

def h(x):
return np.exp(0.5 * x) # Convex

x, segment = np.arange(-2, 2, 0.01), np.array([-1.5, 1])
d2l.use_svg_display()
_, axes = d2l.plt.subplots(1, 3, figsize=(9, 3))

for ax, func in zip(axes, [f, g, h]):
ax.plot(x, func(x))
ax.plot(segment, func(segment),’–’, color=“purple”)
# d2l.plt.plot([x, segment], [func(x), func(segment)], axes=ax)

Jensen 不等式
∑iαif(xi)≥f(∑iαixi) and Ex[f(x)]≥f(Ex[x])

性质
无局部极小值
与凸集的关系
二阶条件
无局部最小值
证明:假设存在 x∈X 是局部最小值,则存在全局最小值 x′∈X , 使得 f(x)>f(x′) , 则对 λ∈(0,1] :

f(x)>λf(x)+(1−λ)f(x′)≥f(λx+(1−λ)x′)

与凸集的关系
对于凸函数 f(x) ,定义集合 Sb:={x|x∈X and f(x)≤b} ,则集合 Sb 为凸集

证明:对于点 x,x′∈Sb , 有 f(λx+(1−λ)x′)≤λf(x)+(1−λ)f(x′)≤b , 故 λx+(1−λ)x′∈Sb
f(x,y)=0.5x2+cos(2πy)
x, y = np.meshgrid(np.linspace(-1, 1, 101), np.linspace(-1, 1, 101),
indexing=‘ij’)

z = x**2 + 0.5 * np.cos(2 * np.pi * y)

#Plot the 3D surface
d2l.set_figsize((6, 4))
ax = d2l.plt.figure().add_subplot(111, projection=‘3d’)
ax.plot_wireframe(x, y, z, **{‘rstride’: 10, ‘cstride’: 10})
ax.contour(x, y, z, offset=-1)
ax.set_zlim(-1, 1.5)

#Adjust labels
for func in [d2l.plt.xticks, d2l.plt.yticks, ax.set_zticks]:
func([-1, 0, 1])

凸函数与二阶导数
f′′(x)≥0⟺f(x) 是凸函数

必要性 ( ⇐ ):

对于凸函数:

12f(x+ϵ)+12f(x−ϵ)≥f(x+ϵ2+x−ϵ2)=f(x)

故:

f′′(x)=limε→0f(x+ϵ)−f(x)ϵ−f(x)−f(x−ϵ)ϵϵ

f′′(x)=limε→0f(x+ϵ)+f(x−ϵ)−2f(x)ϵ2≥0

充分性 ( ⇒ ):

令 a

f(x)−f(a)=(x−a)f′(α) for some α∈[a,x] and f(b)−f(x)=(b−x)f′(β) for some β∈[x,b]

根据单调性,有 f′(β)≥f′(α) , 故:

f(b)−f(a)=f(b)−f(x)+f(x)−f(a)=(b−x)f′(β)+(x−a)f′(α)≥(b−a)f′(α)

def f(x):
return 0.5 * x**2

x = np.arange(-2, 2, 0.01)
axb, ab = np.array([-1.5, -0.5, 1]), np.array([-1.5, 1])

d2l.set_figsize((3.5, 2.5))
fig_x, = d2l.plt.plot(x, f(x))
fig_axb, = d2l.plt.plot(axb, f(axb), ‘-.’,color=“purple”)
fig_ab, = d2l.plt.plot(ab, f(ab),‘g-.’)

fig_x.axes.annotate(‘a’, (-1.5, f(-1.5)), (-1.5, 1.5),arrowprops=dict(arrowstyle=’->’))
fig_x.axes.annotate(‘b’, (1, f(1)), (1, 1.5),arrowprops=dict(arrowstyle=’->’))
fig_x.axes.annotate(‘x’, (-0.5, f(-0.5)), (-1.5, f(-0.5)),arrowprops=dict(arrowstyle=’->’))
Text(-1.5, 0.125, ‘x’)

限制条件
minimizexf(x) subject to ci(x)≤0 for all i∈{1,…,N}

拉格朗日乘子法
Boyd & Vandenberghe, 2004

L(x,α)=f(x)+∑iαici(x) where αi≥0

惩罚项
欲使 ci(x)≤0 , 将项 αici(x) 加入目标函数,如多层感知机章节中的 λ2||w||2
投影
ProjX(x)=argminx′∈X∥x−x′∥2

梯度下降

(Boyd & Vandenberghe, 2004)

%matplotlib inline
import numpy as np
import torch
import time
from torch import nn, optim
import math
import sys
sys.path.append(’/home/kesci/input’)
import d2lzh1981 as d2l
一维梯度下降
证明:沿梯度反方向移动自变量可以减小函数值

泰勒展开:

f(x+ϵ)=f(x)+ϵf′(x)+O(ϵ2)
代入沿梯度方向的移动量 ηf′(x):

f(x−ηf′(x))=f(x)−ηf′2(x)+O(η2f′2(x))
f(x−ηf′(x))≲f(x)
x←x−ηf′(x)
e.g.

f(x)=x2
def f(x):
return x**2 # Objective function

def gradf(x):
return 2 * x # Its derivative

def gd(eta):
x = 10
results = [x]
for i in range(10):
x -= eta * gradf(x)
results.append(x)
print(‘epoch 10, x:’, x)
return results

res = gd(0.2)
epoch 10, x: 0.06046617599999997
def show_trace(res):
n = max(abs(min(res)), abs(max(res)))
f_line = np.arange(-n, n, 0.01)
d2l.set_figsize((3.5, 2.5))
d2l.plt.plot(f_line, [f(x) for x in f_line],’-’)
d2l.plt.plot(res, [f(x) for x in res],’-o’)
d2l.plt.xlabel(‘x’)
d2l.plt.ylabel(‘f(x)’)

show_trace(res)

学习率
show_trace(gd(0.05))
epoch 10, x: 3.4867844009999995

show_trace(gd(1.1))
epoch 10, x: 61.917364224000096

局部极小值
e.g.

f(x)=xcoscx
c = 0.15 * np.pi

def f(x):
return x * np.cos(c * x)

def gradf(x):
return np.cos(c * x) - c * x * np.sin(c * x)

show_trace(gd(2))
epoch 10, x: -1.528165927635083

多维梯度下降
∇f(x)=[
∂f(x)
∂x1

,
∂f(x)
∂x2

,…,
∂f(x)
∂xd

]⊤
f(x+ϵ)=f(x)+ϵ⊤∇f(x)+O(‖ϵ‖2)
x←x−η∇f(x)
def train_2d(trainer, steps=20):
x1, x2 = -5, -2
results = [(x1, x2)]
for i in range(steps):
x1, x2 = trainer(x1, x2)
results.append((x1, x2))
print(‘epoch %d, x1 %f, x2 %f’ % (i + 1, x1, x2))
return results

def show_trace_2d(f, results):
d2l.plt.plot(*zip(*results), ‘-o’, color=’#ff7f0e’)
x1, x2 = np.meshgrid(np.arange(-5.5, 1.0, 0.1), np.arange(-3.0, 1.0, 0.1))
d2l.plt.contour(x1, x2, f(x1, x2), colors=’#1f77b4’)
d2l.plt.xlabel(‘x1’)
d2l.plt.ylabel(‘x2’)
f(x)=x
2
1
+2x
2
2
eta = 0.1

def f_2d(x1, x2): # 目标函数
return x1 ** 2 + 2 * x2 ** 2

def gd_2d(x1, x2):
return (x1 - eta * 2 * x1, x2 - eta * 4 * x2)

show_trace_2d(f_2d, train_2d(gd_2d))
epoch 20, x1 -0.057646, x2 -0.000073

自适应方法
牛顿法
在 x+ϵ 处泰勒展开:

f(x+ϵ)=f(x)+ϵ⊤∇f(x)+
1
2

ϵ⊤∇∇⊤f(x)ϵ+O(‖ϵ‖3)
最小值点处满足: ∇f(x)=0, 即我们希望 ∇f(x+ϵ)=0, 对上式关于 ϵ 求导,忽略高阶无穷小,有:

∇f(x)+Hfϵ=0 and hence ϵ=−H
−1
f
∇f(x)
c = 0.5

def f(x):
return np.cosh(c * x) # Objective

def gradf(x):
return c * np.sinh(c * x) # Derivative

def hessf(x):
return c**2 * np.cosh(c * x) # Hessian

#Hide learning rate for now
def newton(eta=1):
x = 10
results = [x]
for i in range(10):
x -= eta * gradf(x) / hessf(x)
results.append(x)
print(‘epoch 10, x:’, x)
return results

show_trace(newton())
epoch 10, x: 0.0

c = 0.15 * np.pi

def f(x):
return x * np.cos(c * x)

def gradf(x):
return np.cos(c * x) - c * x * np.sin(c * x)

def hessf(x):
return - 2 * c * np.sin(c * x) - x * c**2 * np.cos(c * x)

show_trace(newton())
epoch 10, x: 26.83413291324767

show_trace(newton(0.5))
epoch 10, x: 7.269860168684531

收敛性分析
只考虑在函数为凸函数, 且最小值点上 f″(x∗)>0 时的收敛速度:

令 xk 为第 k 次迭代后 x 的值, ek:=xk−x∗ 表示 xk 到最小值点 x∗ 的距离,由 f′(x∗)=0:

0=f′(xk−ek)=f′(xk)−ekf′′(xk)+
1
2

e
2
k
f′′′(ξk)for some ξk∈[xk−ek,xk]
两边除以 f″(xk), 有:

ek−f′(xk)/f′′(xk)=
1
2

e
2
k
f′′′(ξk)/f′′(xk)
代入更新方程 xk+1=xk−f′(xk)/f′′(xk), 得到:

xk−x∗−f′(xk)/f′′(xk)=
1
2

e
2
k
f′′′(ξk)/f′′(xk)
xk+1−x∗=ek+1=
1
2

e
2
k
f′′′(ξk)/f′′(xk)

1
2

f′′′(ξk)/f′′(xk)≤c 时,有:

ek+1≤ce
2
k
预处理 (Heissan阵辅助梯度下降)
x←x−ηdiag(Hf)−1∇x
梯度下降与线性搜索(共轭梯度法)
随机梯度下降
随机梯度下降参数更新
对于有 n 个样本对训练数据集,设 fi(x) 是第 i 个样本的损失函数, 则目标函数为:

f(x)=
1
n

n

i=1 fi(x)
其梯度为:

∇f(x)=
1
n

n

i=1 ∇fi(x)
使用该梯度的一次更新的时间复杂度为 O(n)
随机梯度下降更新公式 O(1):

x←x−η∇fi(x)
且有:

Ei∇fi(x)=
1
n

n

i=1 ∇fi(x)=∇f(x)
e.g.

f(x1,x2)=x
2
1
+2x
2
2
def f(x1, x2):
return x1 ** 2 + 2 * x2 ** 2 # Objective

def gradf(x1, x2):
return (2 * x1, 4 * x2) # Gradient

def sgd(x1, x2): # Simulate noisy gradient
global lr # Learning rate scheduler
(g1, g2) = gradf(x1, x2) # Compute gradient
(g1, g2) = (g1 + np.random.normal(0.1), g2 + np.random.normal(0.1))
eta_t = eta * lr() # Learning rate at time t
return (x1 - eta_t * g1, x2 - eta_t * g2) # Update variables

eta = 0.1
lr = (lambda: 1) # Constant learning rate
show_trace_2d(f, train_2d(sgd, steps=50))
epoch 50, x1 -0.027566, x2 0.137605

动态学习率
η(t)=ηi if ti≤t≤ti+1 piecewise constant η(t)=η0⋅e−λt exponential η(t)=η0⋅(βt+1)−α polynomial
def exponential():
global ctr
ctr += 1
return math.exp(-0.1 * ctr)

ctr = 1
lr = exponential # Set up learning rate
show_trace_2d(f, train_2d(sgd, steps=1000))
epoch 1000, x1 -0.677947, x2 -0.089379

def polynomial():
global ctr
ctr += 1
return (1 + 0.1 * ctr)**(-0.5)

ctr = 1
lr = polynomial # Set up learning rate
show_trace_2d(f, train_2d(sgd, steps=50))
epoch 50, x1 -0.095244, x2 -0.041674

小批量随机梯度下降
读取数据
读取数据

def get_data_ch7(): # 本函数已保存在d2lzh_pytorch包中方便以后使用
data = np.genfromtxt(’/home/kesci/input/airfoil4755/airfoil_self_noise.dat’, delimiter=’\t’)
data = (data - data.mean(axis=0)) / data.std(axis=0) # 标准化
return torch.tensor(data[:1500, :-1], dtype=torch.float32),
torch.tensor(data[:1500, -1], dtype=torch.float32) # 前1500个样本(每个样本5个特征)

features, labels = get_data_ch7()
features.shape
torch.Size([1500, 5])
import pandas as pd
df = pd.read_csv(’/home/kesci/input/airfoil4755/airfoil_self_noise.dat’, delimiter=’\t’, header=None)
df.head(10)
0 1 2 3 4 5
0 800 0.0 0.3048 71.3 0.002663 126.201
1 1000 0.0 0.3048 71.3 0.002663 125.201
2 1250 0.0 0.3048 71.3 0.002663 125.951
3 1600 0.0 0.3048 71.3 0.002663 127.591
4 2000 0.0 0.3048 71.3 0.002663 127.461
5 2500 0.0 0.3048 71.3 0.002663 125.571
6 3150 0.0 0.3048 71.3 0.002663 125.201
7 4000 0.0 0.3048 71.3 0.002663 123.061
8 5000 0.0 0.3048 71.3 0.002663 121.301
9 6300 0.0 0.3048 71.3 0.002663 119.541
从零开始实现
def sgd(params, states, hyperparams):
for p in params:
p.data -= hyperparams[‘lr’] * p.grad.data
#本函数已保存在d2lzh_pytorch包中方便以后使用
def train_ch7(optimizer_fn, states, hyperparams, features, labels,
batch_size=10, num_epochs=2):
# 初始化模型
net, loss = d2l.linreg, d2l.squared_loss

w = torch.nn.Parameter(torch.tensor(np.random.normal(0, 0.01, size=(features.shape[1], 1)), dtype=torch.float32),
                       requires_grad=True)
b = torch.nn.Parameter(torch.zeros(1, dtype=torch.float32), requires_grad=True)

def eval_loss():
    return loss(net(features, w, b), labels).mean().item()

ls = [eval_loss()]
data_iter = torch.utils.data.DataLoader(
    torch.utils.data.TensorDataset(features, labels), batch_size, shuffle=True)

for _ in range(num_epochs):
    start = time.time()
    for batch_i, (X, y) in enumerate(data_iter):
        l = loss(net(X, w, b), y).mean()  # 使用平均损失
        
        # 梯度清零
        if w.grad is not None:
            w.grad.data.zero_()
            b.grad.data.zero_()
            
        l.backward()
        optimizer_fn([w, b], states, hyperparams)  # 迭代模型参数
        if (batch_i + 1) * batch_size % 100 == 0:
            ls.append(eval_loss())  # 每100个样本记录下当前训练误差
# 打印结果和作图
print('loss: %f, %f sec per epoch' % (ls[-1], time.time() - start))
d2l.set_figsize()
d2l.plt.plot(np.linspace(0, num_epochs, len(ls)), ls)
d2l.plt.xlabel('epoch')
d2l.plt.ylabel('loss')

def train_sgd(lr, batch_size, num_epochs=2):
train_ch7(sgd, None, {‘lr’: lr}, features, labels, batch_size, num_epochs)
对比

train_sgd(1, 1500, 6)
loss: 0.244373, 0.009881 sec per epoch

train_sgd(0.005, 1)
loss: 0.245968, 0.463836 sec per epoch

train_sgd(0.05, 10)
loss: 0.243900, 0.065017 sec per epoch

简洁实现
#本函数与原书不同的是这里第一个参数优化器函数而不是优化器的名字
#例如: optimizer_fn=torch.optim.SGD, optimizer_hyperparams={“lr”: 0.05}
def train_pytorch_ch7(optimizer_fn, optimizer_hyperparams, features, labels,
batch_size=10, num_epochs=2):
# 初始化模型
net = nn.Sequential(
nn.Linear(features.shape[-1], 1)
)
loss = nn.MSELoss()
optimizer = optimizer_fn(net.parameters(), **optimizer_hyperparams)

def eval_loss():
    return loss(net(features).view(-1), labels).item() / 2

ls = [eval_loss()]
data_iter = torch.utils.data.DataLoader(
    torch.utils.data.TensorDataset(features, labels), batch_size, shuffle=True)

for _ in range(num_epochs):
    start = time.time()
    for batch_i, (X, y) in enumerate(data_iter):
        # 除以2是为了和train_ch7保持一致, 因为squared_loss中除了2
        l = loss(net(X).view(-1), y) / 2 
        
        optimizer.zero_grad()
        l.backward()
        optimizer.step()
        if (batch_i + 1) * batch_size % 100 == 0:
            ls.append(eval_loss())
# 打印结果和作图
print('loss: %f, %f sec per epoch' % (ls[-1], time.time() - start))
d2l.set_figsize()
d2l.plt.plot(np.linspace(0, num_epochs, len(ls)), ls)
d2l.plt.xlabel('epoch')
d2l.plt.ylabel('loss')

train_pytorch_ch7(optim.SGD, {“lr”: 0.05}, features, labels, 10)
loss: 0.243770, 0.047664 sec per epoch

优化算法进阶

11.6 Momentum
在 Section 11.4 中,我们提到,目标函数有关自变量的梯度代表了目标函数在自变量当前位置下降最快的方向。因此,梯度下降也叫作最陡下降(steepest descent)。在每次迭代中,梯度下降根据自变量当前位置,沿着当前位置的梯度更新自变量。然而,如果自变量的迭代方向仅仅取决于自变量当前位置,这可能会带来一些问题。对于noisy gradient,我们需要谨慎的选取学习率和batch size, 来控制梯度方差和收敛的结果。

gt=∂w1|Bt|∑i∈Btf(xi,wt−1)=1|Bt|∑i∈Btgi,t−1.

An ill-conditioned Problem
Condition Number of Hessian Matrix:

condH=λmaxλmin

where λmax,λmin is the maximum amd minimum eignvalue of Hessian matrix.

让我们考虑一个输入和输出分别为二维向量 x=[x1,x2]⊤ 和标量的目标函数:

f(x)=0.1x21+2x22

condH=40.2=20→ill-conditioned

Maximum Learning Rate
For f(x) , according to convex optimizaiton conclusions, we need step size η .
To guarantee the convergence, we need to have η .
Supp: Preconditioning
在二阶优化中,我们使用Hessian matrix的逆矩阵(或者pseudo inverse)来左乘梯度向量 i.e.Δx=H−1g ,这样的做法称为precondition,相当于将 H 映射为一个单位矩阵,拥有分布均匀的Spectrum,也即我们去优化的等价标函数的Hessian matrix为良好的identity matrix。

与Section 11.4一节中不同,这里将 x21 系数从 1 减小到了 0.1 。下面实现基于这个目标函数的梯度下降,并演示使用学习率为 0.4 时自变量的迭代轨迹。

%matplotlib inline
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l
import torch

eta = 0.4

def f_2d(x1, x2):
return 0.1 * x1 ** 2 + 2 * x2 ** 2

def gd_2d(x1, x2, s1, s2):
return (x1 - eta * 0.2 * x1, x2 - eta * 4 * x2, 0, 0)

d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))
epoch 20, x1 -0.943467, x2 -0.000073

可以看到,同一位置上,目标函数在竖直方向( x2 轴方向)比在水平方向( x1 轴方向)的斜率的绝对值更大。因此,给定学习率,梯度下降迭代自变量时会使自变量在竖直方向比在水平方向移动幅度更大。那么,我们需要一个较小的学习率从而避免自变量在竖直方向上越过目标函数最优解。然而,这会造成自变量在水平方向上朝最优解移动变慢。

下面我们试着将学习率调得稍大一点,此时自变量在竖直方向不断越过最优解并逐渐发散。

Solution to ill-condition
Preconditioning gradient vector: applied in Adam, RMSProp, AdaGrad, Adelta, KFC, Natural gradient and other secord-order optimization algorithms.
Averaging history gradient: like momentum, which allows larger learning rates to accelerate convergence; applied in Adam, RMSProp, SGD momentum.
eta = 0.6
d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))
epoch 20, x1 -0.387814, x2 -1673.365109

Momentum Algorithm
动量法的提出是为了解决梯度下降的上述问题。设时间步 t 的自变量为 xt ,学习率为 ηt 。 在时间步 t=0 ,动量法创建速度变量 m0 ,并将其元素初始化成 0。在时间步 t>0 ,动量法对每次迭代的步骤做如下修改:

mtxt←βmt−1+ηtgt,←xt−1−mt,

Another version:

mtxt←βmt−1+(1−β)gt,←xt−1−αtmt,

αt=ηt1−β

其中,动量超参数 β 满足 0≤β<1 。当 β=0 时,动量法等价于小批量随机梯度下降。

在解释动量法的数学原理前,让我们先从实验中观察梯度下降在使用动量法后的迭代轨迹。

def momentum_2d(x1, x2, v1, v2):
v1 = beta * v1 + eta * 0.2 * x1
v2 = beta * v2 + eta * 4 * x2
return x1 - v1, x2 - v2, v1, v2

eta, beta = 0.4, 0.5
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))
epoch 20, x1 -0.062843, x2 0.001202

可以看到使用较小的学习率 η=0.4 和动量超参数 β=0.5 时,动量法在竖直方向上的移动更加平滑,且在水平方向上更快逼近最优解。下面使用较大的学习率 η=0.6 ,此时自变量也不再发散。

eta = 0.6
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))
epoch 20, x1 0.007188, x2 0.002553

Exponential Moving Average
为了从数学上理解动量法,让我们先解释一下指数加权移动平均(exponential moving average)。给定超参数 0≤β<1 ,当前时间步 t 的变量 yt 是上一时间步 t−1 的变量 yt−1 和当前时间步另一变量 xt 的线性组合:

yt=βyt−1+(1−β)xt.

我们可以对 yt 展开:

yt=(1−β)xt+βyt−1=(1−β)xt+(1−β)⋅βxt−1+β2yt−2=(1−β)xt+(1−β)⋅βxt−1+(1−β)⋅β2xt−2+β3yt−3=(1−β)∑i=0tβixt−i

(1−β)∑i=0tβi=1−βt1−β(1−β)=(1−βt)

Supp
Approximate Average of 11−β Steps

令 n=1/(1−β) ,那么 (1−1/n)n=β1/(1−β) 。因为

limn→∞(1−1n)n=exp(−1)≈0.3679,

所以当 β→1 时, β1/(1−β)=exp(−1) ,如 0.9520≈exp(−1) 。如果把 exp(−1) 当作一个比较小的数,我们可以在近似中忽略所有含 β1/(1−β) 和比 β1/(1−β) 更高阶的系数的项。例如,当 β=0.95 时,

yt≈0.05∑i=0190.95ixt−i.

因此,在实际中,我们常常将 yt 看作是对最近 1/(1−β) 个时间步的 xt 值的加权平均。例如,当 γ=0.95 时, yt 可以被看作对最近20个时间步的 xt 值的加权平均;当 β=0.9 时, yt 可以看作是对最近10个时间步的 xt 值的加权平均。而且,离当前时间步 t 越近的 xt 值获得的权重越大(越接近1)。

由指数加权移动平均理解动量法
现在,我们对动量法的速度变量做变形:

mt←βmt−1+(1−β)(ηt1−βgt).

Another version:

mt←βmt−1+(1−β)gt.

xt←xt−1−αtmt,

αt=ηt1−β

由指数加权移动平均的形式可得,速度变量 vt 实际上对序列 {ηt−igt−i/(1−β):i=0,…,1/(1−β)−1} 做了指数加权移动平均。换句话说,相比于小批量随机梯度下降,动量法在每个时间步的自变量更新量近似于将前者对应的最近 1/(1−β) 个时间步的更新量做了指数加权移动平均后再除以 1−β 。所以,在动量法中,自变量在各个方向上的移动幅度不仅取决当前梯度,还取决于过去的各个梯度在各个方向上是否一致。在本节之前示例的优化问题中,所有梯度在水平方向上为正(向右),而在竖直方向上时正(向上)时负(向下)。这样,我们就可以使用较大的学习率,从而使自变量向最优解更快移动。

Implement
相对于小批量随机梯度下降,动量法需要对每一个自变量维护一个同它一样形状的速度变量,且超参数里多了动量超参数。实现中,我们将速度变量用更广义的状态变量states表示。

def get_data_ch7():
data = np.genfromtxt(’/home/kesci/input/airfoil4755/airfoil_self_noise.dat’, delimiter=’\t’)
data = (data - data.mean(axis=0)) / data.std(axis=0)
return torch.tensor(data[:1500, :-1], dtype=torch.float32),
torch.tensor(data[:1500, -1], dtype=torch.float32)

features, labels = get_data_ch7()

def init_momentum_states():
v_w = torch.zeros((features.shape[1], 1), dtype=torch.float32)
v_b = torch.zeros(1, dtype=torch.float32)
return (v_w, v_b)

def sgd_momentum(params, states, hyperparams):
for p, v in zip(params, states):
v.data = hyperparams[‘momentum’] * v.data + hyperparams[‘lr’] * p.grad.data
p.data -= v.data
我们先将动量超参数momentum设0.5

d2l.train_ch7(sgd_momentum, init_momentum_states(),
{‘lr’: 0.02, ‘momentum’: 0.5}, features, labels)
loss: 0.243297, 0.057950 sec per epoch

将动量超参数momentum增大到0.9

d2l.train_ch7(sgd_momentum, init_momentum_states(),
{‘lr’: 0.02, ‘momentum’: 0.9}, features, labels)
loss: 0.260418, 0.059441 sec per epoch

可见目标函数值在后期迭代过程中的变化不够平滑。直觉上,10倍小批量梯度比2倍小批量梯度大了5倍,我们可以试着将学习率减小到原来的1/5。此时目标函数值在下降了一段时间后变化更加平滑。

d2l.train_ch7(sgd_momentum, init_momentum_states(),
{‘lr’: 0.004, ‘momentum’: 0.9}, features, labels)
loss: 0.243650, 0.063532 sec per epoch

Pytorch Class
在Pytorch中,torch.optim.SGD已实现了Momentum。

d2l.train_pytorch_ch7(torch.optim.SGD, {‘lr’: 0.004, ‘momentum’: 0.9},
features, labels)
loss: 0.243692, 0.048604 sec per epoch

11.7 AdaGrad
在之前介绍过的优化算法中,目标函数自变量的每一个元素在相同时间步都使用同一个学习率来自我迭代。举个例子,假设目标函数为 f ,自变量为一个二维向量 [x1,x2]⊤ ,该向量中每一个元素在迭代时都使用相同的学习率。例如,在学习率为 η 的梯度下降中,元素 x1 和 x2 都使用相同的学习率 η 来自我迭代:

x1←x1−η∂f∂x1,x2←x2−η∂f∂x2.

在“动量法”一节里我们看到当 x1 和 x2 的梯度值有较大差别时,需要选择足够小的学习率使得自变量在梯度值较大的维度上不发散。但这样会导致自变量在梯度值较小的维度上迭代过慢。动量法依赖指数加权移动平均使得自变量的更新方向更加一致,从而降低发散的可能。本节我们介绍AdaGrad算法,它根据自变量在每个维度的梯度值的大小来调整各个维度上的学习率,从而避免统一的学习率难以适应所有维度的问题 [1]。

Algorithm
AdaGrad算法会使用一个小批量随机梯度 gt 按元素平方的累加变量 st 。在时间步0,AdaGrad将 s0 中每个元素初始化为0。在时间步 t ,首先将小批量随机梯度 gt 按元素平方后累加到变量 st :

st←st−1+gt⊙gt,

其中 ⊙ 是按元素相乘。接着,我们将目标函数自变量中每个元素的学习率通过按元素运算重新调整一下:

xt←xt−1−ηst+ϵ−−−−−√⊙gt,

其中 η 是学习率, ϵ 是为了维持数值稳定性而添加的常数,如 10−6 。这里开方、除法和乘法的运算都是按元素运算的。这些按元素运算使得目标函数自变量中每个元素都分别拥有自己的学习率。

Feature
需要强调的是,小批量随机梯度按元素平方的累加变量 st 出现在学习率的分母项中。因此,如果目标函数有关自变量中某个元素的偏导数一直都较大,那么该元素的学习率将下降较快;反之,如果目标函数有关自变量中某个元素的偏导数一直都较小,那么该元素的学习率将下降较慢。然而,由于 st 一直在累加按元素平方的梯度,自变量中每个元素的学习率在迭代过程中一直在降低(或不变)。所以,当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad算法在迭代后期由于学习率过小,可能较难找到一个有用的解。

下面我们仍然以目标函数 f(x)=0.1x21+2x22 为例观察AdaGrad算法对自变量的迭代轨迹。我们实现AdaGrad算法并使用和上一节实验中相同的学习率0.4。可以看到,自变量的迭代轨迹较平滑。但由于 st 的累加效果使学习率不断衰减,自变量在迭代后期的移动幅度较小。

%matplotlib inline
import math
import torch
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l

def adagrad_2d(x1, x2, s1, s2):
g1, g2, eps = 0.2 * x1, 4 * x2, 1e-6 # 前两项为自变量梯度
s1 += g1 ** 2
s2 += g2 ** 2
x1 -= eta / math.sqrt(s1 + eps) * g1
x2 -= eta / math.sqrt(s2 + eps) * g2
return x1, x2, s1, s2

def f_2d(x1, x2):
return 0.1 * x1 ** 2 + 2 * x2 ** 2

eta = 0.4
d2l.show_trace_2d(f_2d, d2l.train_2d(adagrad_2d))
epoch 20, x1 -2.382563, x2 -0.158591

下面将学习率增大到2。可以看到自变量更为迅速地逼近了最优解。

eta = 2
d2l.show_trace_2d(f_2d, d2l.train_2d(adagrad_2d))
epoch 20, x1 -0.002295, x2 -0.000000

Implement
同动量法一样,AdaGrad算法需要对每个自变量维护同它一样形状的状态变量。我们根据AdaGrad算法中的公式实现该算法。

def get_data_ch7():
data = np.genfromtxt(’/home/kesci/input/airfoil4755/airfoil_self_noise.dat’, delimiter=’\t’)
data = (data - data.mean(axis=0)) / data.std(axis=0)
return torch.tensor(data[:1500, :-1], dtype=torch.float32),
torch.tensor(data[:1500, -1], dtype=torch.float32)

features, labels = get_data_ch7()

def init_adagrad_states():
s_w = torch.zeros((features.shape[1], 1), dtype=torch.float32)
s_b = torch.zeros(1, dtype=torch.float32)
return (s_w, s_b)

def adagrad(params, states, hyperparams):
eps = 1e-6
for p, s in zip(params, states):
s.data += (p.grad.data**2)
p.data -= hyperparams[‘lr’] * p.grad.data / torch.sqrt(s + eps)
使用更大的学习率来训练模型。

d2l.train_ch7(adagrad, init_adagrad_states(), {‘lr’: 0.1}, features, labels)
loss: 0.242258, 0.061548 sec per epoch

Pytorch Class
通过名称为“adagrad”的Trainer实例,我们便可使用Pytorch提供的AdaGrad算法来训练模型。

d2l.train_pytorch_ch7(torch.optim.Adagrad, {‘lr’: 0.1}, features, labels)
loss: 0.243800, 0.060953 sec per epoch

11.8 RMSProp
我们在“AdaGrad算法”一节中提到,因为调整学习率时分母上的变量 st 一直在累加按元素平方的小批量随机梯度,所以目标函数自变量每个元素的学习率在迭代过程中一直在降低(或不变)。因此,当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad算法在迭代后期由于学习率过小,可能较难找到一个有用的解。为了解决这一问题,RMSProp算法对AdaGrad算法做了修改。该算法源自Coursera上的一门课程,即“机器学习的神经网络”。

Algorithm
我们在“动量法”一节里介绍过指数加权移动平均。不同于AdaGrad算法里状态变量 st 是截至时间步 t 所有小批量随机梯度 gt 按元素平方和,RMSProp算法将这些梯度按元素平方做指数加权移动平均。具体来说,给定超参数 0≤γ0 计算

vt←βvt−1+(1−β)gt⊙gt.

和AdaGrad算法一样,RMSProp算法将目标函数自变量中每个元素的学习率通过按元素运算重新调整,然后更新自变量

xt←xt−1−αvt+ϵ−−−−−√⊙gt,

其中 η 是学习率, ϵ 是为了维持数值稳定性而添加的常数,如 10−6 。因为RMSProp算法的状态变量 st 是对平方项 gt⊙gt 的指数加权移动平均,所以可以看作是最近 1/(1−β) 个时间步的小批量随机梯度平方项的加权平均。如此一来,自变量每个元素的学习率在迭代过程中就不再一直降低(或不变)。

照例,让我们先观察RMSProp算法对目标函数 f(x)=0.1x21+2x22 中自变量的迭代轨迹。回忆在“AdaGrad算法”一节使用的学习率为0.4的AdaGrad算法,自变量在迭代后期的移动幅度较小。但在同样的学习率下,RMSProp算法可以更快逼近最优解。

%matplotlib inline
import math
import torch
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l

def rmsprop_2d(x1, x2, s1, s2):
g1, g2, eps = 0.2 * x1, 4 * x2, 1e-6
s1 = beta * s1 + (1 - beta) * g1 ** 2
s2 = beta * s2 + (1 - beta) * g2 ** 2
x1 -= alpha / math.sqrt(s1 + eps) * g1
x2 -= alpha / math.sqrt(s2 + eps) * g2
return x1, x2, s1, s2

def f_2d(x1, x2):
return 0.1 * x1 ** 2 + 2 * x2 ** 2

alpha, beta = 0.4, 0.9
d2l.show_trace_2d(f_2d, d2l.train_2d(rmsprop_2d))
epoch 20, x1 -0.010599, x2 0.000000

Implement
接下来按照RMSProp算法中的公式实现该算法。

def get_data_ch7():
data = np.genfromtxt(’/home/kesci/input/airfoil4755/airfoil_self_noise.dat’, delimiter=’\t’)
data = (data - data.mean(axis=0)) / data.std(axis=0)
return torch.tensor(data[:1500, :-1], dtype=torch.float32),
torch.tensor(data[:1500, -1], dtype=torch.float32)

features, labels = get_data_ch7()

def init_rmsprop_states():
s_w = torch.zeros((features.shape[1], 1), dtype=torch.float32)
s_b = torch.zeros(1, dtype=torch.float32)
return (s_w, s_b)

def rmsprop(params, states, hyperparams):
gamma, eps = hyperparams[‘beta’], 1e-6
for p, s in zip(params, states):
s.data = gamma * s.data + (1 - gamma) * (p.grad.data)**2
p.data -= hyperparams[‘lr’] * p.grad.data / torch.sqrt(s + eps)
我们将初始学习率设为0.01,并将超参数 γ 设为0.9。此时,变量 st 可看作是最近 1/(1−0.9)=10 个时间步的平方项 gt⊙gt 的加权平均。

d2l.train_ch7(rmsprop, init_rmsprop_states(), {‘lr’: 0.01, ‘beta’: 0.9},
features, labels)
loss: 0.243334, 0.063004 sec per epoch

Pytorch Class
通过名称为“rmsprop”的Trainer实例,我们便可使用Gluon提供的RMSProp算法来训练模型。注意,超参数 γ 通过gamma1指定。

d2l.train_pytorch_ch7(torch.optim.RMSprop, {‘lr’: 0.01, ‘alpha’: 0.9},
features, labels)
loss: 0.244934, 0.062977 sec per epoch

11.9 AdaDelta
除了RMSProp算法以外,另一个常用优化算法AdaDelta算法也针对AdaGrad算法在迭代后期可能较难找到有用解的问题做了改进 [1]。有意思的是,AdaDelta算法没有学习率这一超参数。

Algorithm
AdaDelta算法也像RMSProp算法一样,使用了小批量随机梯度 gt 按元素平方的指数加权移动平均变量 st 。在时间步0,它的所有元素被初始化为0。给定超参数 0≤ρ0 ,同RMSProp算法一样计算

st←ρst−1+(1−ρ)gt⊙gt.

与RMSProp算法不同的是,AdaDelta算法还维护一个额外的状态变量 Δxt ,其元素同样在时间步0时被初始化为0。我们使用 Δxt−1 来计算自变量的变化量:

g′t←Δxt−1+ϵst+ϵ−−−−−−−−−√⊙gt,

其中 ϵ 是为了维持数值稳定性而添加的常数,如 10−5 。接着更新自变量:

xt←xt−1−g′t.

最后,我们使用 Δxt 来记录自变量变化量 g′t 按元素平方的指数加权移动平均:

Δxt←ρΔxt−1+(1−ρ)g′t⊙g′t.

可以看到,如不考虑 ϵ 的影响,AdaDelta算法与RMSProp算法的不同之处在于使用 Δxt−1−−−−−√ 来替代超参数 η 。

Implement
AdaDelta算法需要对每个自变量维护两个状态变量,即 st 和 Δxt 。我们按AdaDelta算法中的公式实现该算法。

def init_adadelta_states():
s_w, s_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
delta_w, delta_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
return ((s_w, delta_w), (s_b, delta_b))

def adadelta(params, states, hyperparams):
rho, eps = hyperparams[‘rho’], 1e-5
for p, (s, delta) in zip(params, states):
s[:] = rho * s + (1 - rho) * (p.grad.data**2)
g = p.grad.data * torch.sqrt((delta + eps) / (s + eps))
p.data -= g
delta[:] = rho * delta + (1 - rho) * g * g
d2l.train_ch7(adadelta, init_adadelta_states(), {‘rho’: 0.9}, features, labels)
loss: 0.243485, 0.084914 sec per epoch

Pytorch Class
通过名称为“adadelta”的Trainer实例,我们便可使用pytorch提供的AdaDelta算法。它的超参数可以通过rho来指定。

d2l.train_pytorch_ch7(torch.optim.Adadelta, {‘rho’: 0.9}, features, labels)
loss: 0.267756, 0.061329 sec per epoch

11.10 Adam
Adam算法在RMSProp算法基础上对小批量随机梯度也做了指数加权移动平均 [1]。下面我们来介绍这个算法。

Algorithm
Adam算法使用了动量变量 mt 和RMSProp算法中小批量随机梯度按元素平方的指数加权移动平均变量 vt ,并在时间步0将它们中每个元素初始化为0。给定超参数 0≤β1<1 (算法作者建议设为0.9),时间步 t 的动量变量 mt 即小批量随机梯度 gt 的指数加权移动平均:

mt←β1mt−1+(1−β1)gt.

和RMSProp算法中一样,给定超参数 0≤β2<1 (算法作者建议设为0.999), 将小批量随机梯度按元素平方后的项 gt⊙gt 做指数加权移动平均得到 vt :

vt←β2vt−1+(1−β2)gt⊙gt.

由于我们将 m0 和 s0 中的元素都初始化为0, 在时间步 t 我们得到 mt=(1−β1)∑ti=1βt−i1gi 。将过去各时间步小批量随机梯度的权值相加,得到 (1−β1)∑ti=1βt−i1=1−βt1 。需要注意的是,当 t 较小时,过去各时间步小批量随机梯度权值之和会较小。例如,当 β1=0.9 时, m1=0.1g1 。为了消除这样的影响,对于任意时间步 t ,我们可以将 mt 再除以 1−βt1 ,从而使过去各时间步小批量随机梯度权值之和为1。这也叫作偏差修正。在Adam算法中,我们对变量 mt 和 vt 均作偏差修正:

m^t←mt1−βt1,

v^t←vt1−βt2.

接下来,Adam算法使用以上偏差修正后的变量 m^t 和 m^t ,将模型参数中每个元素的学习率通过按元素运算重新调整:

g′t←ηmtvt−−√+ϵ,

其中 η 是学习率, ϵ 是为了维持数值稳定性而添加的常数,如 10−8 。和AdaGrad算法、RMSProp算法以及AdaDelta算法一样,目标函数自变量中每个元素都分别拥有自己的学习率。最后,使用 g′t 迭代自变量:

xt←xt−1−g′t.

Implement
我们按照Adam算法中的公式实现该算法。其中时间步 t 通过hyperparams参数传入adam函数。

%matplotlib inline
import torch
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l

def get_data_ch7():
data = np.genfromtxt(’/home/kesci/input/airfoil4755/airfoil_self_noise.dat’, delimiter=’\t’)
data = (data - data.mean(axis=0)) / data.std(axis=0)
return torch.tensor(data[:1500, :-1], dtype=torch.float32),
torch.tensor(data[:1500, -1], dtype=torch.float32)

features, labels = get_data_ch7()

def init_adam_states():
v_w, v_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
s_w, s_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
return ((v_w, s_w), (v_b, s_b))

def adam(params, states, hyperparams):
beta1, beta2, eps = 0.9, 0.999, 1e-6
for p, (v, s) in zip(params, states):
v[:] = beta1 * v + (1 - beta1) * p.grad.data
s[:] = beta2 * s + (1 - beta2) * p.grad.data**2
v_bias_corr = v / (1 - beta1 ** hyperparams[‘t’])
s_bias_corr = s / (1 - beta2 ** hyperparams[‘t’])
p.data -= hyperparams[‘lr’] * v_bias_corr / (torch.sqrt(s_bias_corr) + eps)
hyperparams[‘t’] += 1
d2l.train_ch7(adam, init_adam_states(), {‘lr’: 0.01, ‘t’: 1}, features, labels)
loss: 0.242722, 0.089254 sec per epoch

Pytorch Class
d2l.train_pytorch_ch7(torch.optim.Adam, {‘lr’: 0.01}, features, labels)
loss: 0.242389, 0.073228 sec per epoch

你可能感兴趣的:(Task07: 凸优化;梯度下降;优化算法进阶 学习笔记)