From: https://github.com/L1aoXingyu/code-of-learn-deep-learning-with-pytorch
1 SGD
数据
def data_df(x):
x = np.array(x, dtype='float32') / 255
x = (x -0.5) / 0.5
x = x.reshape((-1, )) # 拉平
x = torch.from_numpy(x)
return x
train_set = MNIST('data/mnist_data/', train=True, transform=data_df, download=True)
test_set = MNIST('data/mnist_data/', train=False, transform=data_df, download=False)
自定义SGD
def sgd_update(parameters, lr):
for param in parameters:
param.data = param.data - lr * param.grad.data
criterion = nn.CrossEntropyLoss()
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 10)
)
losses1 = []
idx = 0
start_time = time.time()
for e in range(5):
train_loss = 0
for im, label in train_data:
im = Variable(im)
label = Variable(label)
out = net(im)
loss = criterion(out, label)
net.zero_grad()
loss.backward()
sgd_update(net.parameters(), 1e-2)
train_loss += loss
if idx % 30 == 0:
losses1.append(train_loss)
idx += 1
print('epoch:{:.4f}, train loss:{:.4f}'.format(e, train_loss / len(train_dat))
end_time = time.time()
print('time use: {:.4f}'.format(end_time - start_time))
内置
train_set = MNIST('data/mnist_data/', train=True, transform=data_df, download=True)
test_set = MNIST('data/mnist_data/', train=False, transform=data_df, download=False)
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
criterion = nn.CrossEntropyLoss()
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 10)
)
optimizer = torch.optim.SGD(net.parameters(), 1e-2)
losses3 = []
idx = 0
start_time = time.time()
for e in range(5):
train_loss = 0
for im, label in train_data:
im = Variable(im)
label = Variable(label)
out = net(im)
loss = criterion(out, label)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.data.numpy()
if idx % 30 == 0:
losses3.append(train_loss)
idx += 1
print('epoch:{}, train loss:{:.4f}'.format(e, train_loss / len(train_data)))
end_time = time.time()
print('time use: {:.4f}'.format(end_time - start_time))
2 动量法
使用梯度下降法,每次都会朝着目标函数下降最快的方向,这也称为最速下降法。这种更新方法看似非常快,实际上存在一些问题。
在设置学习率的时候就不能设置太大,为了防止竖直方向上参数更新太过了,一个较小的学习率又导致了水平方向上参数在更新的时候太过于缓慢,所以就导致最终收敛起来非常慢。
动量法的提出就是为了应对这个问题,我们梯度下降法做一个修改如下
v
i
=
γ
v
i
−
1
+
η
∇
L
(
θ
)
v_i = \gamma v_{i-1} + \eta \nabla L(\theta)
vi=γvi−1+η∇L(θ)
θ
i
=
θ
i
−
1
−
v
i
\theta_i = \theta_{i-1} - v_i
θi=θi−1−vi
其中
v
i
v_i
vi 是当前速度,
γ
\gamma
γ 是动量参数,是一个小于 1的正数,
η
\eta
η 是学习率
相当于每次在进行参数更新的时候,都会将之前的速度考虑进来,每个参数在各方向上的移动幅度不仅取决于当前的梯度,还取决于过去各个梯度在各个方向上是否一致,如果一个梯度一直沿着当前方向进行更新,那么每次更新的幅度就越来越大,如果一个梯度在一个方向上不断变化,那么其更新幅度就会被衰减,这样我们就可以使用一个较大的学习率,使得收敛更快,同时梯度比较大的方向就会因为动量的关系每次更新的幅度减少,如下图
比如我们的梯度每次都等于 g,而且方向都相同,那么动量法在该方向上使参数加速移动,有下面的公式:
v
0
=
0
v_0 = 0
v0=0
v
1
=
γ
v
0
+
η
g
=
η
g
v_1 = \gamma v_0 + \eta g = \eta g
v1=γv0+ηg=ηg
v
2
=
γ
v
1
+
η
g
=
(
1
+
γ
)
η
g
v_2 = \gamma v_1 + \eta g = (1 + \gamma) \eta g
v2=γv1+ηg=(1+γ)ηg
v
3
=
γ
v
2
+
η
g
=
(
1
+
γ
+
γ
2
)
η
g
v_3 = \gamma v_2 + \eta g = (1 + \gamma + \gamma^2) \eta g
v3=γv2+ηg=(1+γ+γ2)ηg
⋯
\cdots
⋯
v
+
∞
=
(
1
+
γ
+
γ
2
+
γ
3
+
⋯
 
)
η
g
=
1
1
−
γ
η
g
v_{+ \infty} = (1 + \gamma + \gamma^2 + \gamma^3 + \cdots) \eta g = \frac{1}{1 - \gamma} \eta g
v+∞=(1+γ+γ2+γ3+⋯)ηg=1−γ1ηg
如果我们把
γ
\gamma
γ 定为 0.9,那么更新幅度的峰值就是原本梯度乘学习率的 10 倍。
本质上说,动量法就仿佛我们从高坡上推一个球,小球在向下滚动的过程中积累了动量,在途中也会变得越来越快,最后会达到一个峰值,对应于我们的算法中就是,动量项会沿着梯度指向方向相同的方向不断增大,对于梯度方向改变的方向逐渐减小,得到了更快的收敛速度以及更小的震荡。
自定义动量法
def sgd_momentum(parameters, vs, lr, gamma):
for param, v in zip(parameters, vs):
v[:] = gamma * v + lr * param.grad.data
param.data = param.data - v
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 10)
)
# 将速度初始化为和参数形状相同的0张量
vs = []
for param in net.parameters():
vs.append(torch.zeros_like(param.data))
losses = []
start_time = time.time()
for e in range(5):
train_loss = 0
for im, label in train_data:
im = Variable(im)
label = Variable(label)
out = net(im)
loss = criterion(out, label)
net.zero_grad()
loss.backward()
sgd_momentum(net.parameters(), vs, 1e-2, 0.9)
train_loss += loss.data
losses.append(loss.data)
print('epoch:{}, train_loss:{:.4f}'.format(e+1, train_loss/len(train_data)))
end_time = time.time()
print('time use:{:.4f}'.format(end_time - start_time))
内置
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 10)
)
optimizer = torch.optim.SGD(net.parameters(), lr=1e-2, momentum=0.9) # 加动量
losses = []
idx = 0
start_time = time.time()
for e in range(5):
train_loss = 0
for im, label in train_data:
im = Variable(im)
label = Variable(label)
out = net(im)
loss = criterion(out, label)
net.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss
if idx % 30 == 0:
losses.append(loss)
idx += 1
print('epoch:{}, train_loss:{:.4f}'.format(e+1, train_loss/len(train_data)))
end_time = time.time()
print('time use:{:.4f}'.format(end_time - start_time))
有无动量优化速率:
3 Adagrad
这个优化算法被称为自适应学习率优化算法,之前我们讲的随机梯度下降以及动量法对所有的参数都使用的固定的学习率进行参数更新,但是不同的参数梯度可能不一样,所以需要不同的学习率才能比较好的进行训练,但是这个事情又不能很好地被人为操作,所以 Adagrad 便能够帮助我们做这件事。
Adagrad 的想法非常简答,在每次使用一个 batch size 的数据进行参数更新的时候,我们需要计算所有参数的梯度,那么其想法就是对于每个参数,初始化一个变量 s 为 0,然后每次将该参数的梯度平方求和累加到这个变量 s 上,然后在更新这个参数的时候,学习率就变为
η
s
+
ϵ
\frac{\eta}{\sqrt{s + \epsilon}}
s+ϵη
这里的
ϵ
\epsilon
ϵ 是为了数值稳定性而加上的,因为有可能 s 的值为 0,那么 0 出现在分母就会出现无穷大的情况,通常
ϵ
\epsilon
ϵ 取
1
0
−
10
10^{-10}
10−10,这样不同的参数由于梯度不同,他们对应的 s 大小也就不同,所以上面的公式得到的学习率也就不同,这也就实现了自适应的学习率。
Adagrad 的核心想法就是,如果一个参数的梯度一直都非常大,那么其对应的学习率就变小一点,防止震荡,而一个参数的梯度一直都非常小,那么这个参数的学习率就变大一点,使得其能够更快地更新
Adagrad 也有一些问题,因为 s 不断累加梯度的平方,所以会越来越大,导致学习率在后期会变得较小,导致收敛乏力的情况,可能无法收敛到表较好的结果,当然后面有一个对其的改进,我们之后会讲到
自定义
def sgd_adgrad(parameters, sqrs, lr):
eps = 1e-10
for param, sqr in zip(parameters, sqrs):
sqr[:] = sqr + param.grad.data ** 2
div = lr / torch.sqrt(sqr + eps) * param.grad.data
param.data = param.data - div
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 100)
)
# 初始化梯度平方项
sqrs = []
for param in net.parameters():
sqrs.append(torch.zeros_like(param.data))
losses = []
idx = 0
start_time = time.time()
for e in range(5):
train_loss = 0
for im, label in train_data:
im = Variable(im)
label = Variable(label)
out = net(im)
loss = criterion(out, label)
net.zero_grad()
loss.backward()
sgd_adgrad(net.parameters(), sqrs, 1e-2)
train_loss += loss.data
if idx%30 == 0:
losses.append(loss.data)
idx += 1
print('epoch:{}, Train loss:{:.4f}'.format(e+1, train_loss/len(train_data)))
end_time = time.time()
print('time use:{:.4f}'.format(end_time - start_time))
内置
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 100)
)
optimizer = torch.optim.Adagrad(net.parameters(), lr=1e-2)
losses = []
idx = 0
start_time = time.time()
for e in range(5):
train_loss = 0
for im, label in train_data:
im = Variable(im)
label = Variable(label)
out = net(im)
loss = criterion(out, label)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.data
if idx%30 == 0:
losses.append(loss.data)
idx += 1
print('epoch:{}, Train loss:{:.4f}'.format(e+1, train_loss/len(train_data)))
end_time = time.time()
print('time use:{:.4f}'.format(end_time - start_time))
4 RMSProp
RMSProp 仍然会使用梯度的平方量,不同于 Adagrad,其会使用一个指数加权移动平均来计算这个 s,也就是
s
i
=
α
s
i
−
1
+
(
1
−
α
)
g
2
s_i = \alpha s_{i-1} + (1 - \alpha) \ g^2
si=αsi−1+(1−α) g2
这里 g 表示当前求出的参数梯度,然后最终更新和 Adagrad 是一样的,学习率变成了
η
s
+
ϵ
\frac{\eta}{\sqrt{s + \epsilon}}
s+ϵη
这里
α
\alpha
α 是一个移动平均的系数,也是因为这个系数,导致了 RMSProp 和 Adagrad 不同的地方,这个系数使得 RMSProp 更新到后期累加的梯度平方较小,从而保证 s 不会太大,也就使得模型后期依然能够找到比较优的结果
自定义
def rmsprop(parameters, sqrs, lr, alpha):
eps = 1e-10
for param, sqr in zip(parameters, sqrs):
sqr[:] = alpha * sqr + (1 - alpha) * param.grad.data**2
div = lr / torch.sqrt(sqr + eps) * param.grad.data
param.data = param.data - div
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 10),
)
sqrs = []
for param in net.parameters():
sqrs.append(torch.zeros_like(param.data))
losses = []
idx = 0
start_time = time.time()
for e in range(5):
train_loss = 0
for im, label in train_data:
im = Variable(im)
label = Variable(label)
out = net(im)
loss = criterion(out, label)
net.zero_grad()
loss.backward()
rmsprop(net.parameters(), sqrs, 1e-3, 0.9)
train_loss += loss.data
if idx%30 == 0:
losses.append(loss.data)
idx += 1
print('epoch:{}, Train Loss:{:.4f}'.format(e+1, train_loss/len(train_data)))
end_time = time.time()
print('time use:{:.4f}'.format(end_time - start_time))
内置
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 10),
)
sqrs = []
for param in net.parameters():
sqrs.append(torch.zeros_like(param.data))
losses2 = []
idx = 0
start_time = time.time()
for e in range(5):
train_loss = 0
for im, label in train_data:
im = Variable(im)
label = Variable(label)
out = net(im)
loss = criterion(out, label)
net.zero_grad()
loss.backward()
rmsprop(net.parameters(), sqrs, 1e-3, 0.999)
train_loss += loss.data
if idx%30 == 0:
losses2.append(loss.data)
idx += 1
print('epoch:{}, Train Loss:{:.4f}'.format(e+1, train_loss/len(train_data)))
end_time = time.time()
print('time use:{:.4f}'.format(end_time - start_time))
5 Adadelta
Adadelta 算是 Adagrad 法的延伸,它跟 RMSProp 一样,都是为了解决 Adagrad 中学习率不断减小的问题,RMSProp 是通过移动加权平均的方式,而 Adadelta 也是一种方法,有趣的是,它并不需要学习率这个参数。
Adadelta 跟 RMSProp 一样,先使用移动平均来计算 s
s
=
ρ
s
+
(
1
−
ρ
)
g
2
s = \rho s + (1 - \rho) g^2
s=ρs+(1−ρ)g2
这里
ρ
\rho
ρ 和 RMSProp 中的
α
\alpha
α 都是移动平均系数,g 是参数的梯度,然后我们会计算需要更新的参数的变化量
g
′
=
Δ
θ
+
ϵ
s
+
ϵ
g
g' = \frac{\sqrt{\Delta \theta + \epsilon}}{\sqrt{s + \epsilon}} g
g′=s+ϵΔθ+ϵg
Δ
θ
\Delta \theta
Δθ 初始为 0 张量,每一步做如下的指数加权移动平均更新
Δ
θ
=
ρ
Δ
θ
+
(
1
−
ρ
)
g
′
2
\Delta \theta = \rho \Delta \theta + (1 - \rho) g'^2
Δθ=ρΔθ+(1−ρ)g′2
最后参数更新如下
θ = θ − g ′ \theta = \theta - g' θ=θ−g′
自定义
def adadelta(parameters, sqrs, deltas, rho):
eps = 1e-6
for param, sqr, delta in zip(parameters, sqrs, deltas):
sqr[:] = rho * sqr + (1 - rho) * param.grad.data ** 2
cur_delta = torch.sqrt(delta + eps) / torch.sqrt(sqr + eps) * param.grad.data
delta[:] = rho * delta + (1 - rho) * cur_delta ** 2
param.data = param.data - cur_delta
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 10),
)
sqrs = []
deltas = []
for param in net.parameters():
sqrs.append(torch.zeros_like(param.data))
deltas.append(torch.zeros_like(param.data))
losses = []
idx = 0
start_time = time.time()
for e in range(5):
train_loss = 0
for im, label in train_data:
im = Variable(im)
label = Variable(label)
out = net(im)
loss = criterion(out, label)
net.zero_grad()
loss.backward()
adadelta(net.parameters(), sqrs, deltas, 0.9)
train_loss += loss.data
if idx%30 == 0:
losses.append(loss.data)
idx += 1
print('epoch:{}, Train Loss:{:.4f}'.format(e+1, train_loss/len(train_data)))
end_time = time.time()
print('time use:{:.4f}'.format(end_time - start_time))
内置
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 10),
)
optimizer = torch.optim.Adadelta(net.parameters(), rho=0.9)
losses = []
idx = 0
start_time = time.time()
for e in range(5):
train_loss = 0
for im, label in train_data:
im = Variable(im)
label = Variable(label)
out = net(im)
loss = criterion(out, label)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.data
if idx%30 == 0:
losses.append(loss.data)
idx += 1
print('epoch:{}, Train Loss:{:.4f}'.format(e+1, train_loss/len(train_data)))
end_time = time.time()
print('time use:{:.4f}'.format(end_time - start_time))
6 Adam
Adam 是一个结合了动量法和 RMSProp 的优化算法,其结合了两者的优点。
Adam 算法会使用一个动量变量 v 和一个 RMSProp 中的梯度元素平方的移动指数加权平均 s,首先将他们全部初始化为 0,然后在每次迭代中,计算他们的移动加权平均进行更新
v
=
β
1
v
+
(
1
−
β
1
)
g
s
=
β
2
s
+
(
1
−
β
2
)
g
2
v = \beta_1 v + (1 - \beta_1) g \\ s = \beta_2 s + (1 - \beta_2) g^2
v=β1v+(1−β1)gs=β2s+(1−β2)g2
在 adam 算法里,为了减轻 v 和 s 被初始化为 0 的初期对计算指数加权移动平均的影响,每次 v 和 s 都做下面的修正
v
^
=
v
1
−
β
1
t
s
^
=
s
1
−
β
2
t
\hat{v} = \frac{v}{1 - \beta_1^t} \\ \hat{s} = \frac{s}{1 - \beta_2^t}
v^=1−β1tvs^=1−β2ts
这里 t 是迭代次数,可以看到,当
0
≤
β
1
,
β
2
≤
1
0 \leq \beta_1, \beta_2 \leq 1
0≤β1,β2≤1 的时候,迭代到后期 t 比较大,那么
β
1
t
\beta_1^t
β1t 和
β
2
t
\beta_2^t
β2t 就几乎为 0,就不会对 v 和 s 有任何影响了,算法作者建议
β
1
=
0.9
\beta_1 = 0.9
β1=0.9,
β
2
=
0.999
\beta_2 = 0.999
β2=0.999。
最后使用修正之后的 v ^ \hat{v} v^ 和 s ^ \hat{s} s^ 进行学习率的重新计算
g
′
=
η
v
^
s
^
+
ϵ
g' = \frac{\eta \hat{v}}{\sqrt{\hat{s} + \epsilon}}
g′=s^+ϵηv^
这里
η
\eta
η 是学习率,
e
p
s
i
l
o
n
epsilon
epsilon 仍然是为了数值稳定性而添加的常数,最后参数更新有
θ i = θ i − 1 − g ′ \theta_i = \theta_{i-1} - g' θi=θi−1−g′
自定义
def adam(parameters, vs, sqrs, lr, t, beta1=0.9, beta2=0.999):
eps = 1e-8
for param, v, sqr in zip(parameters, vs, sqrs):
v[:] = beta1 * v + (1 - beta1) * param.grad.data
sqr[:] = beta2 * sqr + (1 - beta2) * param.grad.data ** 2
v_hat = v / (1 - beta1 ** t)
s_hat = sqr / (1 - beta2 ** t)
param.data = param.data - lr * v_hat / torch.sqrt(s_hat + eps)
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 10),
)
sqrs = []
vs = []
for param in net.parameters():
sqrs.append(torch.zeros_like(param.data))
vs.append(torch.zeros_like(param.data))
t = 1
losses = []
idx = 0
start_time = time.time()
for e in range(5):
train_loss = 0
for im, label in train_data:
im = Variable(im)
label = Variable(label)
out = net(im)
loss = criterion(out, label)
net.zero_grad()
loss.backward()
adam(net.parameters(), vs, sqrs, 1e-3, t)
t += 1
train_loss += loss.data
if idx % 30 == 0:
losses.append(loss.data)
idx += 1
print('epoch:{}, Train Loss:{:.4f}'.format(e+1, train_loss/len(train_data)))
end_time = time.time()
print('time use:{:.4f}'.format(end_time - start_time))
内置
train_data = DataLoader(train_set, batch_size=64, shuffle=True)
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 10),
)
optimizer = torch.optim.Adam(net.parameters(), lr=1e-3)
losses = []
idx = 0
start_time = time.time()
for e in range(5):
train_loss = 0
for im, label in train_data:
im = Variable(im)
label = Variable(label)
out = net(im)
loss = criterion(out, label)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.data
if idx%30 == 0:
losses.append(loss.data)
idx += 1
print('epoch:{}, Train Loss:{:.4f}'.format(e+1, train_loss/len(train_data)))
end_time = time.time()
print('time use:{:.4f}'.format(end_time - start_time))