我们将讲解一些梯度下降优化算法,对它们如何工作给出一些直观的理解,然后在PyTorch中实现每一个算法。
在我们开始沿着这条路走之前,先澄清一下有时会困惑的东西。反向传播是一种计算神经网络中参数相对于损失的梯度的算法。梯度下降优化算法利用每个参数梯度来计算如何更新参数以减少损失。
另一个小提示:这些实现更多的是出于教育目的,当实际训练神经网络时,你一定要使用torch.optim中提供的适当的PyTorch优化器。我已经尽力确保这些实现是正确的,但我更倾向于可读性而不是效率。
最后,我并不声称存在“最佳”优化器,因为根本就没有。最近的一篇论文表明,在所有问题上,没有一个优化器始终比其他的优化器更好。这里显示的优化器做得更好的是在单个问题上,使用带有单个隐藏层的MLP对MNIST数字进行分类。我也没有尝试在不同的学习速率和优化器超参数之间进行公平的网格搜索。有些优化器我们只使用默认值,有些则尝试一些不同的超参数,看看会发生什么。
话虽如此,如果有疑问,使用带有默认超参数的Adam通常是一个很好的起点。
导入必要的库
import matplotlib.pyplot as plt
import numpy as np
import random
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.data as data
import torchvision.datasets as datasets
import torchvision.transforms as transforms
import tqdm
预处理
# 设置随机种子,结果可重复
torch.manual_seed(1234)
random.seed(1234)
np.random.seed(1234)
# 我们所有的实验都使用MNIST数据集。MNIST由手绘数字组成,从0到9,用28×28像素黑白图像表示。
# 我们将使用预先计算的平均值和标准差对图像进行归一化,并进行一些数据增强,
# 即:对图像进行随机旋转和裁剪。
# 注意,我们只获得训练数据,因为我们只关心这些优化器如何最小化损失,而不是体系结构如何泛化。
#在实践中,较低的训练损失并不一定意味着由于过拟合而导致的更好的验证/测试损失。
mean = 0.13066048920154572
std = 0.30810779333114624
train_transforms=transforms.Compose([
transforms.RandomRotation(5),
transforms.RandomCrop(28,padding=2),
transforms.ToTensor(),
transforms.Normalize(mean=[mean],std=[std])
])
train_data=datasets.MNIST(root="D:\\mnist", train=True, download=True, transform=train_transforms)
# 为数据创建迭代器
# 批大小是任意选择的,如果使用不同的批大小,结果可能会不同。
# 注意,在实践中,更大的批量通常允许您使用更大的学习速率。
batch_size=128
train_iterator=data.DataLoader(train_data, shuffle=True, batch_size=batch_size)
多层感知机(MLP)
模型参数使用init_params 函数来初始化,因为在使用ReLU激活函数时,这通常会做得很好。bias被初始化为零,这是很常见的。
初始化方案也会改变优化器的结果,但我相信我们在这里使用的方案是合理的。
class MLP(nn.Module):
def __init__(self, input_dim, hid_dim, output_dim):
super().__init__()
self.layer1=nn.Linear(input_dim, hid_dim)
self.layer2=nn.Linear(hid_dim,hid_dim)
self.layer3=nn.Linear(hid_dim,output_dim)
self.init_params()
def init_params(self):
for n,p in self.named_parameters():
if "weight" in n:
nn.init.kaiming_normal_(p, nonlinearity='relu')
elif 'bias' in n:
nn.init.constant_(p,0)
def forward(self,x):
# x = [batch, channels, height, width]
batch_size,*_=x.shape
x=x.view(batch_size,-1)
x=F.relu(self.layer1(x))
x=F.relu(self.layer2(x))
x=self.layer3(x)
return x
训练参数配置
# 我们的模型使用256维的隐藏层。同样,这是任意选择的,较小的值也可以工作。
input_dim=28*28
hid_dim=256
output_dim=10
model=MLP(input_dim,hid_dim,output_dim)
# 每个样本属于一个类的监督学习几乎总是使用交叉熵损失。
criterion=nn.CrossEntropyLoss()
# 然后我们会用.to方法把模型和损失函数放到我们的GPU上,如果我们有的话。
device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
model=model.to(device)
criterion=criterion.to(device)
辅助函数
接下来,我们将定义一些函数,用于使用优化器训练模型并绘制结果。
# train_epoch函数执行单个epoch的训练,并返回每个批次的损失列表。
def train_epoch(iterator, model, optimizer, criterion, device):
losses=[]
for images,labels in tqdm.tqdm(iterator):
images=images.to(device)
labels=labels.to(device)
optimizer.zero_grad()
predictions=model(images)
loss=criterion(predictions,labels)
loss.backward()
optimizer.step()
losses.append(loss.item())
return losses
# train初始化一个模型,然后训练n_epoch次,存储并返回所有epochs中每批的损失。
def train(train_iterator, model, optimizer, criterion, device, n_epochs=5):
losses=[]
model.init_params()
for epoch in range(n_epochs):
epoch_losses=train_epoch(train_iterator, model, optimizer, criterion,device)
losses.extend(epoch_losses)
return losses
# 我们有两个查看结果的函数。
# plot_loss函数用于绘制单个实验的结果。plot_losses函数绘制多个实验的结果,用于比较优化器之间的差异。
def plot_loss(loss, title=None, ymin=0, ymax=None, figsize=(15,5)):
fig,ax=plt.subplots(figsize=figsize)
ax.plot(loss)
ax.set_title(title)
ax.set_ylabel("Loss")
ax.set_xlabel("Update Steps")
ax.set_ylim(ymin=ymin,ymax=ymax)
ax.grid()
def plot_losses(losses, labels, title=None, ymin=0, ymax=None, figsize=(15,5)):
fig,ax=plt.subplots(figsize=figsize)
for loss,label in zip(losses, labels):
ax.plot(loss, label=label)
ax.set_title(title)
ax.set_ylabel("Loss")
ax.set_xlabel("Update Steps")
ax.set_ylim(ymin=ymin,ymax=ymax)
ax.grid()
ax.legend(loc="upper right")
SGD
随机梯度下降是最简单的优化算法,所以它是一个很好的开始。我们用当前模型参数 θ t \theta_t θt,减去这些参数的梯度 ∇ θ J ( θ t ) \nabla_\theta J(\theta_t) ∇θJ(θt),乘以“学习率” η \eta η。
我们可以把学习率看作是控制参数更新幅度的参数。如果我们的学习率太小,那么我们的参数更新也太小,我们无法在合理的时间内训练我们的模型。相反,如果我们的学习率太大,那么参数更新的大小就会太大,学习就会变得不稳定!如果您的损失得到NaN值,那么首先要尝试的事情之一就是降低学习率。
SGD算法为: θ t + 1 = θ t − η ∇ θ J ( θ t ) \theta_{t+1}= \theta_t-\eta\nabla_\theta J(\theta_t) θt+1=θt−η∇θJ(θt)。
然而,我们不仅有一组参数,
θ
\theta
θ,我们有多个参数:层1的权值,层1的偏差,层2的权值,层2的偏差,等等。所以我们将参数下标为
i
i
i:
θ
t
+
1
,
i
=
θ
t
,
i
−
η
∇
θ
J
(
θ
t
,
i
)
\theta_{t+1,i} = \theta_{t,i}-\eta\nabla_\theta J(\theta_{t,i})
θt+1,i=θt,i−η∇θJ(θt,i)
我们用减法是因为我们想要降低梯度,并移动到一个更低的损失值。加法会使梯度上升,因此称为梯度上升。
最后要提到的是不同的梯度下降法:梯度下降法、随机梯度下降法、小批量梯度下降法和在线梯度下降法。
梯度下降是指我们使用训练集中的每个例子来计算梯度,然后进行单个参数更新。这相对较慢,因为在我们的实验中,这意味着只有在看到所有60000个示例后才更新参数。另一个极端是随机梯度下降这意味着我们在每个例子之后都会更新参数。这通常是非常嘈杂的,所以一个trade-off是在我们看过一批例子后更新参数,小批量梯度下降。最后是在线梯度下降,这通常意味着我们的模型正在生产中,并不断地提供新的例子,以更新其参数。
令人困惑的是,梯度下降有时被称为批梯度下降,其中整个数据集计为一个巨大的批,因此使用批数据的例子被称为一个小批。
在PyTorch中,优化器被称为随机梯度下降,尽管它可以执行上述任何梯度下降变体。一般的经验法则是,现在当有人提到随机梯度下降时,他们指的是小批量梯度下降。
不管怎样,让我们来看看实现。所有优化器都需要一种方法来跟踪它们应该更新的参数model_params和学习率lr。PyTorch中的SGD没有默认学习率,但1e-3是其他优化器的常见默认学习率值,所以我们在这里使用它。所有优化器都需要一个zero_grad函数来删除从上次更新步骤中计算出来的梯度,以及一个step函数来执行参数更新。
注意,任何带有末尾下划线的PyTorch方法,例如.sub_,意味着在本地操作。这些就地操作通常要比非就地操作快得多。
class SGD(object):
def __init__(self, model_params, lr=1e-3):
self.model_params=list(model_params)
self.lr=lr
def zero_grad(self):
for param in self.model_params:
param.grad=None
@torch.no_grad()
def step(self):
for param in self.model_params:
param.sub_(self.lr*param.grad)
我们可以这样定义自己的优化器:
optimizer=SGD(model.parameters())
然后我们开始训练自己的模型
sgd_loss = train(train_iterator, model, optimizer, criterion, device)
plot_loss(sgd_loss, 'SGD with lr=1e-3')
看起来是合理的,当我们的参数被随机初始化时,损失从一个高值开始,然后稳步减少。如果不将它与其他优化器进行比较,我们就无法判断它到底有多“好”,所以让我们现在就开始做吧。
SGD with Momentum
一种思考SGD的方法是将球从山上滚下来,其中高梯度区域是陡峭的部分,低梯度区域是非常平坦的区域。有时,整体最小值,即损失最小的点,位于巨大平坦区域的中央。问题在于,因为这些平坦区域具有较小的梯度,所以它们也提供了较小的更新步骤,从而导致学习缓慢。
如果我们扩展“球从山上滚下来”的类比呢?我们想要在我们的优化器中添加一些东西,使它在滚下陡峭的山坡时保持“动量”,同时穿越平坦区域。
这就是SGD with momentum做的事情!我们的参数更新现在使用速度
v
v
v来计算,它取决于当前的梯度乘以学习率加上先前的速度乘以动量
γ
\gamma
γ。
v
t
,
i
=
γ
.
v
t
−
1
,
i
+
η
.
∇
θ
J
(
θ
t
,
i
)
v_{t,i}=\gamma.v_{t-1,i}+\eta.\nabla_\theta J(\theta_{t,i})
vt,i=γ.vt−1,i+η.∇θJ(θt,i)
θ
t
+
1
,
i
=
θ
t
,
i
−
v
t
,
i
\theta_{t+1,i}=\theta_{t,i}-v_{t,i}
θt+1,i=θt,i−vt,i
如果动量为零,那么我们完全不关心之前的速度,这个算法就变成SGD。常用的动量值通常在0.9左右。
PyTorch的优化器有时与实际的算法有一点不同。PyTorch版本的SGD带有动量,将学习率移出了速度方程:
v
t
,
i
=
γ
.
v
t
−
1
,
i
+
∇
θ
J
(
θ
t
,
i
)
v_{t,i}=\gamma.v_{t-1,i}+\nabla_\theta J(\theta_{t,i})
vt,i=γ.vt−1,i+∇θJ(θt,i)
θ
t
+
1
,
i
=
θ
t
,
i
−
η
.
v
t
,
i
\theta_{t+1,i}=\theta_{t,i}-\eta.v_{t,i}
θt+1,i=θt,i−η.vt,i
注意,速度
v
{v}
v是对应于模型参数的张量列表,所以我们在模型中存储了每个参数的速度。
class SGDMomentum(object):
def __init__(self, model_params, lr=1e-3, momentum=0.9):
self.model_params=list(model_params)
self.lr=lr
self.momentum=momentum
self.v=[torch.zeros_like(p) for p in self.model_params]
def zero_grad(self):
for param in self.model_params:
param.grad=None
@torch.no_grad()
def step(self):
for param,v in zip(self.model_params, self.v):
v.mul_(self.momentum).add_(param.grad)
param.sub_(self.lr*v)
# 定义自己的优化器
optimizer=SGDMomentum(model.parameters())
sgd_momentum_loss=train(train_iterator, model, optimizer, criterion, device)
plot_loss(sgd_momentum_loss, "SGDMomentum with lr-1e-3, momentum=0.9")
# 比较SGD和SGD with momentum
losses=[sgd_loss, sgd_momentum_loss]
labels=["sgd", "sgd_momentum"]
plot_losses(losses, labels, "SGD VS SGDMomentum")
正如我们所看到的,momentum不仅帮助我们达到最低SGD损失,大约在0.25,而且在很短的时间内,它还让我们的整体损失更低!
SGD还有另一种带有动量的变体,称为Nesterov加速梯度(NAG),但我们不打算在这里实现它,原因如下:
- 实现它有多种不同的方法,请参阅这篇优秀的文章,了解有关变体的详细信息,PyTorch以一种完全不同的方式实现它。
- 我个人发现,与常规momentum相比,它从未提供过明显的改善。
- 它不是很常用。
Adagrad(1)
SGD的一个缺点是,我们在所有参数中使用单一的学习率,并且这个学习率在整个训练过程中是固定的。
理想情况下,更新频率较高的参数学习率较低,而更新频率较低的参数学习率较高。
这就是Adagrad所做的。我们使用
G
t
,
i
G_{t,i}
Gt,i,它是参数
i
i
i到(包括)时间步长
t
t
t的梯度平方和。
G
t
,
i
G_{t,i}
Gt,i被初始化为某个值,通常默认为0。随着参数梯度平方的积累,
G
t
,
i
G_{t,i}
Gt,i会增加,从而降低了参数
i
i
i的学习率。
θ
t
+
1
,
i
=
θ
t
,
i
−
η
G
t
,
i
+
ϵ
.
∇
θ
J
(
θ
t
,
i
)
\theta_{t+1,i}=\theta_{t,i}-\frac{\eta}{\sqrt{G_{t,i}}+\epsilon}.\nabla_\theta J(\theta_{t,i})
θt+1,i=θt,i−Gt,i+ϵη.∇θJ(θt,i)
G
t
,
i
=
G
t
−
1
,
i
+
(
∇
θ
J
(
θ
t
,
i
)
)
2
G_{t,i}=G_{t-1,i}+(\nabla_\theta J(\theta_{t,i}))^2
Gt,i=Gt−1,i+(∇θJ(θt,i))2
ϵ
\epsilon
ϵ是一个很小的数,用来避免分母被零除掉。有时你会看到
ϵ
\epsilon
ϵ在根号内,有时在根号外。PyTorch把它留在外面,所以我们也这样。
我们在下面实现Adagrad,将 G G G初始化为一个称为acc_sqr_gradients的张量列表,并使用std更新方程的分母。
class Adagrad(object):
def __init__(self, model_params, lr=1e-2, init_acc_sqr_grad=0, eps=1e-10):
self.model_params=list(model_params)
self.lr=lr
self.acc_sqr_grads=[torch.full_like(p,init_acc_sqr_grad) for p in self.model_params]
self.eps=eps
def zero_grad(self):
for param in self.model_params:
param.grad=None
@torch.no_grad()
def step(self):
for param, acc_sqr_grad in zip(self.model_params, self.acc_sqr_grads):
acc_sqr_grad.add_(param.grad * param.grad)
std=acc_sqr_grad.sqrt().add(self.eps)
param.sub_((self.lr/std)*param.grad)
# 定义自己的优化器
optimizer=Adagrad(model.parameters())
adagrad_loss=train(train_iterator, model, optimizer, criterion, device)
plot_loss(adagrad_loss, "Adagrad with lr=1e-2, init_acc_sqr_grad=0, eps=1e-10")
我们可以看到损失值的初始峰值。这是由于初始
G
G
G值非常小,因此学习率除以一个非常小的数字,使它非常大。非常大的学习率通常会导致不稳定的训练,从而产生更高的损失值。
让我们调整一下开始,以便更好地了解最终损失值是多少。
plot_loss(adagrad_loss, "Adagrad with lr=1e-2, init_acc_sqr_grad=0, eps=1e-10",ymax=5.0)
我们来比较一下Adagrad与SGD优化算法
losses=[sgd_loss, sgd_momentum_loss, adagrad_loss]
labels=["sgd", "sgd_momentum", "adagrad_loss"]
plot_losses(losses, labels, "SGD vs. SGDMomentum vs. Adagrad")
Adagrad轻松地击败了其他两种优化方法,但最初的损失看起来并不太好。也许如果我们去掉最初的峰值就能让Adagrad表现得更好?让我们为 G G G尝试一些不同的初始值,并将它们全部存储在adagrad_losses字典中。字典中的每个键将是初始 G G G值,字典的值将是每批训练损失的列表。
adagrad_losses={0:adagrad_loss}
optimizer=Adagrad(model.parameters(), init_acc_sqr_grad=1.0)
adagrad_losses[1.0]=train(train_iterator, model, optimizer, criterion, device)
optimizer=Adagrad(model.parameters(), init_acc_sqr_grad=0.1)
adagrad_losses[0.1]=train(train_iterator, model, optimizer, criterion, device)
optimizer=Adagrad(model.parameters(), init_acc_sqr_grad=0.01)
adagrad_losses[0.01]=train(train_iterator, model, optimizer, criterion, device)
optimizer=Adagrad(model.parameters(), init_acc_sqr_grad=0.001)
adagrad_losses[0.001]=train(train_iterator, model, optimizer, criterion, device)
比较5个训练结果
labels,losses=zip(*adagrad_losses.items())
plot_losses(losses, labels, "Adagrad init_acc_sqr_grad Value Comparison", ymax=5.0)
我们可以看到,Adagrad的性能随着初始 G G G值的降低而增加,但 G G G的降低也增加了训练开始时损失的初始峰值。
为什么初始 G G G值增加时,性能下降?这是Adagrad的主要缺点:由于 G G G在每个time-step单调增加,它将学习率除以之前所有 G G G的累加和的平方根。从初始 G G G值为1.0的结果可以看出,这些较小的步长实际上增加了模型收敛所需的时间,在极端情况下会导致步长接近零,这意味着参数将完全停止更新。
在实践中,我们确实希望在训练时学习率降低,但理想情况下不希望学习率为零。
让我们将Adagrad与SGD优化器进行比较,以便更好地了解它们的比较情况。
losses = [sgd_loss, sgd_momentum_loss, adagrad_loss]
labels = ["sgd", "sgd momentum", "adagrad"]
plot_losses(losses, labels, "SGD Optimizers vs. Adagrad", ymax=5.0)
AdaGrad(2)
Adagrad(简称自适应梯度)在参数空间更为平缓的方向,会取得更大的步伐(因为平缓,所以历史梯度平方和较小,对应学习下降的幅度较小),并且能够使得陡峭的方向变得平缓,从而加快训练速度。
损失的偏导数最大的参数其学习率相应的下降较快,而偏导数较小的参数的学习率下降相对较小。
梯度下降算法的一个问题是搜索空间中每个变量或维度的步长(学习率)都相同。使用针对每个变量定制的步长可以实现更好的性能,允许在具有一致陡峭梯度的维度上进行较大的运动,并在具有较不陡峭梯度的维度上进行较小的运动。
AdaGrad 旨在专门探索为搜索空间中的每个维度自动定制步长的想法。
- 你可以在没有类的情况下实现它:
def Adagrad(data):
gradient_sums=np.zeros(theta.shape[0])
for t in range(num_iterations):
gradients=compute_gradients(data, weights)
gradient_sums += gradients ** 2
gradient_update = gradients / (np.sqrt(gradient_sums + epsilon))
weights = weights - lr * gradient_update
return weights
def adagrad(params, states, hyperparams):
eps = 1e-6
for p, s in zip(params, states):
s[:] += p.grad.square()
p[:] -= hyperparams['lr'] * p.grad / (s + eps).sqrt()
在一些问题中,很多时候在数据中最关键的信息不是经常出现。因此,如果您正在处理的用例与稀疏数据相关,Adagrad可能是有用的。
- PyTorch中你可以使用下面的命令调用算法:
torch.optim.Adagrad(params, lr=0.01, lr_decay=0, weight_decay=0, initial_accumulator_value=0, eps=1e-10)
- adagrad的一个简单的python实现:
import numpy as np
from random import sample
import math
def adagrad(f_grad,x0,data,args,stepsize = 1e-2,fudge_factor = 1e-6,max_it=1000,minibatchsize=None,minibatch_ratio=0.01):
# f_grad returns the loss functions gradient
# x0 are the initial parameters (a starting point for the optimization)
# data is a list of training data
# args is a list or tuple of additional arguments passed to fgrad
# stepsize is the global stepsize for adagrad
# fudge_factor is a small number to counter numerical instabiltiy
# max_it is the number of iterations adagrad will run
# minibatchsize if given is the number of training samples considered in each iteration
# minibatch_ratio if minibatchsize is not set this ratio will be used to determine the batch size dependent on the length of the training data
#d-dimensional vector representing diag(Gt) to store a running total of the squares of the gradients.
gti=np.zeros(x0.shape[0])
ld=len(data)
if minibatchsize is None:
minibatchsize = int(math.ceil(len(data)*minibatch_ratio))
w=x0
for t in xrange(max_it):
s=sample(xrange(ld),minibatchsize)
sd=[data[idx] for idx in s]
grad=f_grad(w,sd,*args)
gti+=grad**2
adjusted_grad = grad / (fudge_factor + np.sqrt(gti))
w = w - stepsize*adjusted_grad
return w
但是也有一些缺点,像它是计算昂贵的,学习率也在下降,这使得它在训练中很慢。
从零开始实现AdaGrad梯度下降
# 在测试函数的等高线图上绘制adagrad搜索的例子
from math import sqrt
from numpy import asarray
from numpy import arange
from numpy.random import rand
from numpy.random import seed
from numpy import meshgrid
from matplotlib import pyplot
from mpl_toolkits.mplot3d import Axes3D
# objective function
def objective(x, y):
return x**2.0 + y**2.0
# derivative of objective function
def derivative(x, y):
return asarray([x * 2.0, y * 2.0])
# gradient descent algorithm with adagrad
def adagrad(objective, derivative, bounds, n_iter, step_size):
# track all solutions
solutions = list()
# generate an initial point
solution = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
# list of the sum square gradients for each variable
sq_grad_sums = [0.0 for _ in range(bounds.shape[0])]
# run the gradient descent
for it in range(n_iter):
# calculate gradient
gradient = derivative(solution[0], solution[1])
# update the sum of the squared partial derivatives
for i in range(gradient.shape[0]):
sq_grad_sums[i] += gradient[i]**2.0
# build solution
new_solution = list()
for i in range(solution.shape[0]):
# calculate the learning rate for this variable
alpha = step_size / (1e-8 + sqrt(sq_grad_sums[i]))
# calculate the new position in this variable
value = solution[i] - alpha * gradient[i]
new_solution.append(value)
# store the new solution
solution = asarray(new_solution)
solutions.append(solution)
# evaluate candidate point
solution_eval = objective(solution[0], solution[1])
# report progress
print('>%d f(%s) = %.5f' % (it, solution, solution_eval))
return solutions
# seed the pseudo random number generator
seed(1)
# define range for input
bounds = asarray([[-1.0, 1.0], [-1.0, 1.0]])
# define the total iterations
n_iter = 50
# define the step size
step_size = 0.1
# perform the gradient descent search
solutions = adagrad(objective, derivative, bounds, n_iter, step_size)
# sample input range uniformly at 0.1 increments
xaxis = arange(bounds[0,0], bounds[0,1], 0.1)
yaxis = arange(bounds[1,0], bounds[1,1], 0.1)
# create a mesh from the axis
x, y = meshgrid(xaxis, yaxis)
# compute targets
results = objective(x, y)
# create a filled contour plot with 50 levels and jet color scheme
pyplot.contourf(x, y, results, levels=50, cmap='jet')
# plot the sample as black circles
solutions = asarray(solutions)
pyplot.plot(solutions[:, 0], solutions[:, 1], '.-', color='w')
# show the plot
pyplot.show()
运行示例执行搜索,白色点从高于最优条件开始,逐步接近最佳状态的中心点。
Adadelta(1)
在Adadelta优化器中,你不需要初始的学习率,可以通过定义如下函数来使用它,而无需任何torch方法:
s
=
ρ
s
+
(
1
−
ρ
)
g
2
s=\rho s+(1-\rho)g^2
s=ρs+(1−ρ)g2
def Adadelta(weights, sqrs, deltas, rho, batch_size):
eps_stable=1e-5
for weight,sqr,delta in zip(weights, sqrs, deltas):
g=weight.grad/batch_size
sqr[:]=rho*sqr + (1. - rho)*torch.square(g)
cur_delta=torch.sqrt(delta+eps_stable)/torch.sqrt(sqr+eps_stable)*g
delta[:]=rho*delta+(1.-rho)*cur_delta*cur_delta
# 原地更新权重
weight.data -= cur_delta
在PyTorch的帮助下,你可以用一行代码完成同样的工作,如下所示:
torch.optim.Adadelta(params,lr=1.0,rho=0.9,eps=1e-6,weight_decay=0)
样例展示,样例使用flag=2模式
# flag=0
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
# flag=1
def Adadelta(weights, sqrs, deltas, rho, batch_size):
eps_stable = 1e-5
for weight, sqr, delta in zip(weights, sqrs, deltas):
g = weight.grad.data / batch_size
sqr[:] = rho * sqr + (1. - rho) * torch.square(g)
cur_delta = torch.sqrt(delta + eps_stable) / torch.sqrt(sqr + eps_stable) * g
delta[:] = rho * delta + (1. - rho) * cur_delta * cur_delta
# 原地更新权重
weight.data -= cur_delta
import numpy as np
import torch
from torchvision.datasets import MNIST # 导入 pytorch 内置的 mnist 数据
from torch.utils.data import DataLoader
from torch import nn
import time
import matplotlib.pyplot as plt
def data_tf(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('D:\\mnist', train=True, transform=data_tf, download=True) # 载入数据集,申明定义的数据变换
test_set = MNIST('D:\\mnist', train=False, transform=data_tf, download=True)
batch_size=64
# 定义 loss 函数
criterion = nn.CrossEntropyLoss()
train_data = DataLoader(train_set, batch_size=batch_size, shuffle=True)
# 使用 Sequential 定义 3 层神经网络
net = nn.Sequential(
nn.Linear(784, 200),
nn.ReLU(),
nn.Linear(200, 10),
)
# flag=2
optimizer = torch.optim.Adadelta(net.parameters(), rho=0.9)
# 初始化梯度平方项和 delta 项
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() # 记时开始
for e in range(5):
train_loss = 0
for idx,(im, label) in enumerate(train_data):
# 前向传播
out = net(im)
loss = criterion(out, label)
# 反向传播
net.zero_grad()
loss.backward()
optimizer.step() # if flag=2
# adadelta(net.parameters(), sqrs, deltas, 0.9) # rho 设置为 0.9 # if flag=1
# Adadelta(net.parameters(), sqrs, deltas, 0.9, batch_size=batch_size) # if flag=0
# 记录误差
train_loss += loss.item()
if idx % 20 == 0:
losses.append(loss.item())
print('epoch: {}, Train Loss: {:.6f}'
.format(e, train_loss / len(train_data)))
end = time.time() # 计时结束
print('使用时间: {:.5f} s'.format(end - start))
x_axis = np.linspace(0, 5, len(losses), endpoint=True)
plt.semilogy(x_axis, losses, label='pytorch rho=0.99')
plt.legend(loc='best')
plt.show()
Adadelta(2)
我们所有的更新步长方程可以写成:
θ
t
+
1
,
i
=
θ
t
,
i
+
Δ
t
,
i
\theta_{t+1,i}=\theta_{t,i} + \Delta_{t,i}
θt+1,i=θt,i+Δt,i
其中
Δ
θ
t
,
i
\Delta \theta_{t,i}
Δθt,i是参数更新的大小,即在SGD中:
Δ
θ
t
,
i
=
−
η
.
∇
θ
J
(
θ
t
,
i
)
\Delta \theta_{t,i}=-\eta.\nabla_\theta J(\theta_{t,i})
Δθt,i=−η.∇θJ(θt,i)
在Adagrad中:
Δ
θ
t
,
i
=
−
η
G
t
,
i
+
ϵ
∇
θ
J
(
θ
t
,
i
)
\Delta \theta_{t,i}=-\frac{\eta}{\sqrt{G_{t,i}}+\epsilon}\nabla_\theta J(\theta_{t,i})
Δθt,i=−Gt,i+ϵη∇θJ(θt,i)
Adagrad算法的问题是
G
G
G是单调递增的。Adadelta通过将Adagrad算法的
G
t
,
i
G_{t,i}
Gt,i替换为
E
[
g
2
]
t
,
i
E[g^2]_{t,i}
E[g2]t,i方式来解决这个问题,
E
[
g
2
]
t
,
i
E[g^2]_{t,i}
E[g2]t,i是迄今为止梯度的平方的指数移动平均。
E
[
g
2
]
t
,
i
=
ρ
E
[
g
2
]
t
−
1
,
i
+
(
1
−
ρ
)
g
t
,
i
2
E[g^2]_{t,i}=\rho E[g^2]_{t-1,i} + (1-\rho)g^2_{t,i}
E[g2]t,i=ρE[g2]t−1,i+(1−ρ)gt,i2
其中
g
t
,
i
=
∇
θ
J
(
θ
t
,
i
)
g_{t,i} = \nabla_\theta J(\theta_{t,i})
gt,i=∇θJ(θt,i),我们这样做只是为了简化符号,
ρ
\rho
ρ控制我们对指数移动平均中的前一个梯度的关心程度,
ρ
=
0
\rho=0
ρ=0意味着我们根本不关心它们。
这意味着我们的更新步骤方程是:
Δ
θ
t
,
i
=
−
η
E
[
g
2
]
t
,
i
+
ϵ
.
g
t
,
i
\Delta \theta_{t,i}=-\frac{\eta}{\sqrt{E[g^2]_{t,i}+\epsilon}}.g_{t,i}
Δθt,i=−E[g2]t,i+ϵη.gt,i
注意
ϵ
\epsilon
ϵ项现在已经移动到平方根里面了,这是我们从PyTorch复制的。
上述方程的问题,以及迄今为止看到的所有更新方程的问题,是更新的单位与参数的单位不匹配。更新的单位为
δ
J
δ
θ
\frac{\delta J}{\delta \theta}
δθδJ,如果我们假设损失函数是无单位的,则简化为
1
units of
θ
\frac{1}{\text{units of}\theta}
units ofθ1。然而,我们希望更新方程的单位为
θ
\theta
θ。
为了解决这个问题,Adadelta方程使用了第二个指数移动平均,但这个是参数更新。
为了得到最终的Adadelta方程,我们进行第一次尝试,将
η
\eta
η替换为参数平方更新的指数移动平均值:
E
[
Δ
θ
2
]
t
−
1
,
i
=
ρ
E
[
Δ
θ
2
]
t
−
2
,
i
+
(
1
−
ρ
)
Δ
θ
t
−
1
,
i
2
E[\Delta\theta^2]_{t-1,i}=\rho E[\Delta\theta^2]_{t-2,i} + (1-\rho )\Delta\theta^2_{t-1,i}
E[Δθ2]t−1,i=ρE[Δθ2]t−2,i+(1−ρ)Δθt−1,i2
最终,我们得到:
Δ
θ
t
,
i
=
−
η
G
t
,
i
+
ϵ
∇
θ
J
(
θ
t
,
i
)
\Delta \theta_{t,i}=-\frac{\eta}{\sqrt{G_{t,i}}+\epsilon}\nabla_\theta J(\theta_{t,i})
Δθt,i=−Gt,i+ϵη∇θJ(θt,i)
Δ
θ
t
,
i
=
−
E
[
Δ
θ
2
]
t
−
1
,
i
+
ϵ
E
[
g
2
]
t
,
i
+
ϵ
.
g
t
,
i
\Delta\theta_{t,i}=-\frac{\sqrt{E[\Delta\theta^2]_{t-1,i}+\epsilon}}{\sqrt{E[g^2]_{t,i}+\epsilon}}.g_{t,i}
Δθt,i=−E[g2]t,i+ϵE[Δθ2]t−1,i+ϵ.gt,i
现在的单位是
θ
2
θ
=
θ
\frac{\theta^2}{\theta} = \theta
θθ2=θ。
这意味着我们甚至不需要使用学习率值,但是在PyTorch实现中他们使用了一个(默认为1.0),所以最终会:
Δ
θ
t
,
i
=
−
η
E
[
Δ
θ
2
]
t
−
1
,
i
+
ϵ
E
[
g
2
]
t
,
i
+
ϵ
.
g
t
,
i
\Delta \theta_{t,i}=-\eta \frac{\sqrt{E[\Delta\theta^2]_{t-1,i}+\epsilon}}{\sqrt{E[g^2]_{t,i}+\epsilon}}.g_{t,i}
Δθt,i=−ηE[g2]t,i+ϵE[Δθ2]t−1,i+ϵ.gt,i
因此:
θ
t
+
1
,
i
=
θ
t
,
i
−
η
E
[
Δ
θ
2
]
t
−
1
,
i
+
ϵ
E
[
g
2
]
t
,
i
+
ϵ
.
g
t
,
i
\theta_{t+1,i}=\theta_{t,i}-\eta\frac{\sqrt{E[\Delta\theta^2]_{t-1,i}+\epsilon}}{\sqrt{E[g^2]_{t,i}+\epsilon}}.g_{t,i}
θt+1,i=θt,i−ηE[g2]t,i+ϵE[Δθ2]t−1,i+ϵ.gt,i
PyTorch还将eps默认值从1e-10更改为1e-6。
class Adadelta(object):
def __init__(self, model_params, lr=1.0, rho=0.9, eps=1e-6):
self.model_params=list(model_params)
self.lr=lr
self.rho=rho
self.eps=eps
self.avg_sqr_grads=[torch.zeros_like(p) for p in self.model_params]
self.avg_sqr_deltas = [torch.zeros_like(p) for p in self.model_params]
def zero_grad(self):
for param in self.model_params:
param.grad=None
@torch.no_grad()
def step(self):
for param, avg_sqr_grad, avg_sqr_delta in zip(self.model_params,
self.avg_sqr_grads,
self.avg_sqr_deltas):
avg_sqr_grad.mul_(self.rho).add_(param.grad**2*(1-self.rho))
std=avg_sqr_grad.add(self.eps).sqrt()
delta=avg_sqr_delta.add(self.eps).sqrt().div(std).mul(param.grad)
param.sub_(self.lr*delta)
avg_sqr_delta.mul_(self.rho).add_(delta**2*(1-self.rho))
optimizer=Adadelta(model.parameters())
adadelta_loss=train(train_iterator, model, optimizer, criterion, device)
plot_loss(adadelta_loss, "Adadelta with lr=1.0, rho=0.9, eps=1e-6")
我们可以看到,由于分子项(参数更新的指数移动平均)一开始很小,我们避免了损失的初始峰值。
让我们将Adadelta和目前所有的算法进行比较。
losses = [sgd_loss, sgd_momentum_loss, adagrad_loss, adadelta_loss]
labels=["sgd", "sgd momentum", "adagrad", "adadelta"]
plot_losses(losses, labels, "SGD vs. SGD momentum vs. Adagrad vs. Adadelta", ymax=5.0)
我们可以看到Adagrad和Adadelta有相当的性能,但adadadelta没有很大的初始损失峰值。
RMSprop
还记得Adadelta方程的“第一次尝试”吗?我们说的那个有更新单位和参数单位不匹配的问题?事实证明你可以完全忽略这些单位不匹配的事实,这给了你RMSprop算法!
θ
t
+
1
=
θ
t
−
η
E
[
g
2
]
t
,
i
+
ϵ
.
g
t
,
i
\theta_{t+1}=\theta_t-\frac{\eta}{\sqrt{E[g^2]_{t,i}+\epsilon}}.g_{t,i}
θt+1=θt−E[g2]t,i+ϵη.gt,i
在PyTorch中,他们将
ϵ
\epsilon
ϵ项移回平方根之外,类似于Adagrad。这给了我们:
θ
t
+
1
=
θ
t
−
η
E
[
g
2
]
t
,
i
+
ϵ
.
g
t
,
i
\theta_{t+1}=\theta_t-\frac{\eta}{\sqrt{E[g^2]_{t,i}}+\epsilon}.g_{t,i}
θt+1=θt−E[g2]t,i+ϵη.gt,i
在PyTorch实现中,他们还将默认学习率改为1e-2,将rho重命名为alpha,同时给它一个新的默认值0.99,并将默认eps改为1e-8。
class RMSprop(object):
def __init__(self, model_params, lr=1e-2, alpha=0.99, eps=1e-8):
self.model_params=list(model_params)
self.lr=lr
self.alpha=alpha
self.eps=eps
self.avg_sqr_grads=[torch.zeros_like(p) for p in self.model_params]
def zero_grad(self):
for param in self.model_params:
param.grad=None
@torch.no_grad()
def step(self):
for param, avg_sqr_grad in zip(self.model_params, self.avg_sqr_grads):
avg_sqr_grad.mul_(self.alpha).add_(param.grad**2*(1-self.alpha))
std=avg_sqr_grad.sqrt().add(self.eps)
param.sub_((self.lr/std)*param.grad)
optimizer=RMSprop(model.parameters())
rmsprop_loss=train(train_iterator, model, optimizer, criterion, device)
plot_loss(rmsprop_loss, "RMSprop with lr=1e-2, alpha=0.99, eps=1e-8")
我们遇到了与Adagrad类似的问题,初始时间步长中的小分母导致大步长,在训练的早期阶段造成巨大的损失值峰值。
让我们放大感兴趣的部分,以便更好地了解正在发生的事情。
plot_loss(rmsprop_loss, "RMSprop with lr=1e-2, alpha=0.99, eps=1e-8", ymax=5.0)
需要注意的一点是,RMSprop的PyTorch实现与TensorFlow实现主要在两个方面有所不同。在TensorFlow中, ϵ \epsilon ϵ项在平方根中,就像在初始RMSprop算法中一样,并且平方梯度的指数移动平均被初始化为1而不是0。
这个TensorFlow变体显然比PyTorch版本更稳定,所以我们将在这里实现它,看看它是否提供了任何明显的改进。
class RMSpropAlt(object):
def __init__(self, model_params, lr=1e-2, alpha=0.99, eps=1e-8):
self.model_params=list(model_params)
self.lr=lr
self.alpha=alpha
self.eps=eps
self.avg_sqr_grads=[torch.ones_like(p) for p in self.model_params]
def zero_grad(self):
for param in self.model_params:
param.grad=None
@torch.no_grad()
def step(self):
for param, avg_sqr_grad in zip(self.model_params, self.avg_sqr_grads):
avg_sqr_grad.mul_(self.alpha).add_(param.grad**2*(1-self.alpha)
std=avg_sqr_grad.add(self.eps).sqrt()
param.sub_((self.lr/std)*param.grad)
optimizer=RMSpropAlt(model.parameters())
rmspropalt_loss=train(train_iterator,model,optimizer,criterion,device)
plot_loss(rmspropalt_loss, "RMSpropAlt with lr=1e-2, alpha=0.99, eps=1e-8")
TensorFlow变体没有较大的初始损失峰值,但它实际上看起来比PyTorch RMSprop实现更不稳定,即使在训练期间忽略较大峰值时也是如此。
losses=[rmsprop_loss, rmspropalt_loss]
labels=["rmsprop", "rmspropalt"]
plot_losses(losses, labels, "RMSprop vs. RMSpropAlt", ymax=5.0)
TensorFlow显然不太稳定。然而,这是一个样本大小为1的实验,所以我们不能对这两个RMSprop版本做出任何一般性的声明。
让我们把这两个和Adagrad和Adadelta做一个快速的比较:
losses=[adagrad_loss, adadelta_loss, rmsprop_loss, rmspropalt_loss]
labels=["adagrad", "adadelta", "rmsprop", "rmspropalt"]
plot_losses(losses, labels, 'Adagrad vs. Adadelta vs. RMSprop vs. RMSpropAlt', ymax=5.0)
我们可以看到,两个RMSprop变种都不如Adagrad或Adadelta。
RMSprop通常用于强化学习,其中一个超参数eps参数做了很多调整。让我们尝试一些值,看看它们在我们的实验设置中是否有什么不同。
rmsprop_losses={1e-8:rmsprop_loss}
optimizer=RMSprop(model.parameters(),eps=1e-6)
rmsprop_losses[1e-6]=train(train_iterator, model, optimizer,criterion,device)
optimizer=RMSprop(model.parameters(),eps=1e-4)
rmsprop_losses[1e-4]=train(train_iterator, model, optimizer,criterion,device)
optimizer=RMSprop(model.parameters(),eps=1e-2)
rmsprop_losses[1e-2]=train(train_iterator, model, optimizer,criterion,device)
optimizer=RMSprop(model.parameters(),eps=1)
rmsprop_losses[1]=train(train_iterator, model, optimizer,criterion,device)
labels,losses=zip(*rmsprop_losses.items())
plot_losses(losses,labels,"RMSprop eps Value Comparison", ymax=5.0)
增加eps可使性能提高一点,eps=1e-2时性能最好,然后性能开始下降,eps=1时性能最差。
让我们用TensorFlow的RMSprop来做同样的事情。
rmspropalt_losses={1e-8:rmspropalt_loss}
optimizer=RMSpropAlt(model.parameters(),eps=1e-6)
rmspropalt_losses[1e-6]=train(train_iterator, model, optimizer,criterion,device)
optimizer=RMSpropAlt(model.parameters(),eps=1e-4)
rmspropalt_losses[1e-4]=train(train_iterator, model, optimizer,criterion,device)
optimizer=RMSpropAlt(model.parameters(),eps=1e-2)
rmspropalt_losses[1e-2]=train(train_iterator, model, optimizer,criterion,device)
optimizer=RMSpropAlt(model.parameters(),eps=1)
rmspropalt_losses[1]=train(train_iterator, model, optimizer,criterion,device)
labels,losses=zip(*rmspropalt_losses.items())
plot_losses(losses,labels,"RMSpropAlt eps Value Comparison", ymax=5.0)
这里也是一样,最好的eps也在1e-2左右。
losses=[rmsprop_losses[1e-2],rmspropalt_losses[1e-2]]
labels=["rmsprop", "rmspropalt"]
plot_losses(losses,labels,"RMSprop vs. RMSpropAlt, both with eps=1e-2",ymax=5.0)
看起来RMSprop的PyTorch实现在这里仍然更优越,所以现在让我们再次将它与Adagrad和Adadelta进行比较。
losses=[adagrad_loss, adadelta_loss, rmsprop_losses[1e-2]]
labels=["adagrad", "adadelta", "rmsprop"]
plot_losses(losses, labels, "Adagrad vs. Adadelta vs. RMSprop", ymax=5.0)
他们都很接近,也许RMSprop稍微差一点?
让我们放大。
losses=[adagrad_loss, adadelta_loss, rmsprop_losses[1e-2]]
labels=["adagrad", "adadelta", "rmsprop"]
plot_losses(losses, labels, "Adagrad vs. Adadelta vs. RMSprop", ymax=1.0)
还是有点难以判断。
我们用移动平均线平滑损失曲线怎么样?
def moving_average(x, w=5):
return np.convolve(x, np.ones(w), "valid")/w
losses=[adagrad_loss, adadelta_loss, rmsprop_losses[1e-2]]
smoothed_losses=[moving_average(loss) for loss in losses]
labels=["adagrad","adadelta","rmsprop"]
plot_losses(smoothed_losses, labels, "Smoothed Adagrad vs. Adadelta vs. RMSprop",ymax=1.0)
RMSprop看起来是三款优化器中效果最差的,但也差不了多少。
Adam
RMSprop的想法是随着时间的推移降低步长,同时使用指数移动平均来避免饱和,这似乎是可行的。如果我们给它加上动量呢?这样得到Adam。
Adam具有梯度的指数移动平均,如可以添加到SGD中的动量项,以及平方梯度的指数移动平均,如RMSprop和Adadelta。
θ
t
+
1
=
θ
t
−
η
.
m
t
,
i
v
t
,
i
+
ϵ
\theta_{t+1}=\theta_t-\eta.\frac{m_{t,i}}{\sqrt v_{t,i}+\epsilon}
θt+1=θt−η.vt,i+ϵmt,i
其中
m
t
,
i
=
β
1
m
t
−
1
,
i
+
(
1
−
β
1
)
g
t
,
i
m_{t,i}=\beta_1 m_{t-1,i} + (1-\beta_1)g_{t,i}
mt,i=β1mt−1,i+(1−β1)gt,i
v t , i = β 2 v t − 1 , i + ( 1 − β 2 ) g t , i 2 v_{t,i}=\beta_2 v_{t-1,i} + (1-\beta_2)g^2_{t,i} vt,i=β2vt−1,i+(1−β2)gt,i2
Adam的 m t , i m_{t,i} mt,i等于SGD的 v t , i v_{t,i} vt,i,如果它有 ( 1 − γ ) (1-\gamma) (1−γ)项。Adam的 v t , i = E [ g 2 ] t , i v_{t,i} = E[g^2]_{t,i} vt,i=E[g2]t,i,用 ρ \rho ρ替换为 β 2 \beta_2 β2。
当 m m m和 v v v初始化为0, β 1 \beta_1 β1和 β 2 \beta_2 β2初始化为接近1时,在最初的几个更新步骤中计算的 m m m和 v v v值“偏向”非常小的值。这就是我们在Adagrad, Adadelta和RMSprop的第一步看到了巨大的损失。
为了解决这个问题,Adam使用了“偏差修正”的
m
m
m和
v
v
v,计算如下:
m
t
,
i
^
=
m
t
,
i
1
−
β
1
t
\hat{m_{t,i}}=\frac{m_{t,i}}{1-\beta^t_1}
mt,i^=1−β1tmt,i
v
t
,
i
^
=
v
t
,
i
1
−
β
2
t
\hat{v_{t,i}}=\frac{v_{t,i}}{1-\beta^t_2}
vt,i^=1−β2tvt,i
最终的Adam方程如下:
θ
t
+
1
=
θ
t
−
η
.
m
t
,
i
^
v
t
,
i
^
+
ϵ
\theta_{t+1}=\theta_t-\eta.\frac{\hat{m_{t,i}}}{\sqrt {\hat{v_{t,i}}}+\epsilon}
θt+1=θt−η.vt,i^+ϵmt,i^
注意,第一次调用step时的偏差校正值是用
t
=
1
t = 1
t=1而不是
t
=
0
t = 0
t=0来计算的。
class Adam(object):
def __init__(self, model_params, lr=1e-3, betas=(0.9,0.99),eps=1e-8):
self.model_params=list(model_params)
self.lr=lr
self.beta_1,self.beta_2=betas
self.eps=eps
self.avg_grads=[torch.zeros_like(p) for p in self.model_params]
self.avg_sqr_grads=[torch.zeros_like(p) for p in self.model_params]
self.n_steps=0
def zero_grad(self):
for param in self.model_params:
param.grad=None
@torch.no_grad()
def step(self):
for param,avg_grad,avg_sqr_grad in zip(self.model_params, self.avg_grads,self.avg_sqr_grads):
self.n_steps+=1
avg_grad.mul_(self.beta_1).add_(param.grad*(1-self.beta_1))
avg_sqr_grad.mul_(self.beta_2).add_(param.grad**2*(1-self.beta_2))
avg_grad_corrected = avg_grad.div(1-self.beta_1**self.n_steps)
avg_sqr_grad_corrected=avg_sqr_grad.div(1-self.beta_2**self.n_steps)
std=avg_sqr_grad_corrected.sqrt().add(self.eps)
param.sub_(self.lr * avg_grad_corrected/std)
optimizer=Adam(model.parameters())
adam_loss=train(train_iterator, model, optimizer, criterion, device)
plot_loss(adam_loss, "Adam with lr=1e-3, betas=(0.9, 0.99), eps=1e-8")
目前看来情况不错,损失没有大幅增加。让我们将它与其他优化算法进行比较。
losses=[adagrad_loss, adadelta_loss,rmsprop_losses[1e-2],adam_loss]
labels=["adagrad", "adadelta", "rmsprop", "adam"]
plot_losses(losses, labels, "Adagrad vs. Adadelta vs. RMSprop vs. Adam ymax=3.0", ymax=3.0)
Adam似乎与Adagrad和Adadelta是一致的,而且肯定比RMSprop更好。
让我们放大看看。
losses=[adagrad_loss, adadelta_loss,rmsprop_losses[1e-2],adam_loss]
labels=["adagrad", "adadelta", "rmsprop", "adam"]
plot_losses(losses, labels, "Adagrad vs. Adadelta vs. RMSprop vs. Adam ymax=0.5", ymax=0.5)
同样,Adagrad,Adadelta和Adam看起来效果相同。
我们可以对训练损失进行平均,看看是否有明显的区别。
losses=[adagrad_loss, adadelta_loss,rmsprop_losses[1e-2],adam_loss]
smoothed_losses=[moving_average(loss) for loss in losses]
labels=["adagrad", "adadelta", "rmsprop", "adam"]
plot_losses(smoothed_losses, labels, "Smoothed Adagrad vs. Adadelta vs. RMSprop vs. Adam ymax=0.5", ymax=0.5)
看起来Adagrad,Adadelta和Adam在这个实验中效果几乎是相同的。
RAdam
Adam优化器的自适应学习率存在较大的方差。在训练初期,由于缺乏采样的样本数量,自适应学习率的方差比训练后期要大,大致会有500倍的差异。通过引入修正因子,作者提出了Adam算法的新变种RAdam(Rectified Adam)。
在某些情况下,由于衰减率和潜在方差的驱动,RAdam可以退化为SGD。
class RAdam(object):
def __init__(self, model_params, lr=1e-3, betas=(0.9,0.99),eps=1e-8):
self.model_params=list(model_params)
self.lr=lr
self.beta_1,self.beta_2=betas
self.eps=eps
self.avg_grads=[torch.zeros_like(p) for p in self.model_params]
self.avg_sqr_grads=[torch.zeros_like(p) for p in self.model_params]
self.n_steps=0
self.weight_decay=weight_decay
self.rho_inf=2/(1-self.beta_2)-1
def zero_grad(self):
for param in self.model_params:
param.grad=None
@torch.no_grad()
def step(self):
for param,avg_grad,avg_sqr_grad in zip(self.model_params, self.avg_grads,self.avg_sqr_grads):
self.n_steps+=1
avg_grad.mul_(self.beta_1).add_(param.grad*(1-self.beta_1))
avg_sqr_grad.mul_(self.beta_2).add_(param.grad**2*(1-self.beta_2))
avg_grad_corrected = avg_grad.div(1-self.beta_1**self.n_steps)
rho=self.rho_inf-2*self.n_steps*(self.beta_2**self.n_steps)/(1-self.beta_2**self.n_steps)
if rho>4:
avg_sqr_grad_corrected=avg_sqr_grad.div(1-self.beta_2**self.n_steps)
rt=(((rho-4)*(rho-2)*self.rho_inf)/((self.rho_inf-4)*(self.rho_inf-2)*rho.add(self.eps)))
rt=np.sqrt(rt)
std=avg_sqr_grad_corrected.sqrt().add(self.eps)
param.sub_(self.lr*rt*avg_grad_corrected/std)
else:
param.sub_(self.lr*avg_grad_corrected)
optimizer=RAdam(model.parameters())
radam_loss=train(train_iterator, model, optimizer, criterion, device)
plot_loss(radam_loss, "RAdam with lr=1e-3, betas=(0.9, 0.99), eps=1e-8")
效果还是不错的,让我们看一下与Adam的对比结果:
losses=[adam_loss, radam_loss]
labels=["adam", "radam"]
plot_losses(losses, labels, "RAdam vs. Adam", ymax=5)
前几个周期RAdam比Adam方法慢,但是在后期的收敛速度是比Adam要更快的。RAdam算法对初始学习率是具有鲁棒性的,可以适应更宽范围内的变化。在从0.003到0.1范围内,RAdam表现出了一致的性能,训练曲线末端高度重合。
让我们看一下各种优化器的对比结果:
losses = [sgd_loss, sgd_momentum_loss, adagrad_loss, adadelta_loss, rmsprop_loss, adam_loss, radam_loss]
labels = ["sgd", "sgd_momentum", "adagrad", "adadelta", "rmsprop", "adam", "radam"]
plot_losses(losses, labels, "RAdam vs. Adam", ymax=5)
AdamW
AdamW是在Adam+L2正则化的基础上进行改进的算法。如果引入L2正则项,在计算梯度的时候会加上对正则项求梯度的结果。
class AdamW(object):
def __init__(self, model_params, wd=0, lr=1e-3, betas=(0.9,0.99),eps=1e-8, warmup=1000):
self.model_params=list(model_params)
self.lr=lr
self.beta_1,self.beta_2=betas
self.eps=eps
self.avg_grads=[torch.zeros_like(p) for p in self.model_params]
self.avg_sqr_grads=[torch.zeros_like(p) for p in self.model_params]
self.n_steps=0
self.wd=wd
self.warmup=warmup
def zero_grad(self):
for param in self.model_params:
param.grad=None
@torch.no_grad()
def step(self):
for param,avg_grad,avg_sqr_grad in zip(self.model_params, self.avg_grads,self.avg_sqr_grads):
self.n_steps+=1
param.grad.add_(self.wd*param)
avg_grad.mul_(self.beta_1).add_(param.grad*(1-self.beta_1))
avg_sqr_grad.mul_(self.beta_2).add_(param.grad**2*(1-self.beta_2))
avg_grad_corrected = avg_grad.div(1-self.beta_1**self.n_steps)
avg_sqr_grad_corrected=avg_sqr_grad.div(1-self.beta_2**self.n_steps)
std=avg_sqr_grad_corrected.sqrt().add(self.eps)
if self.warmup>self.n_steps:
scheduled_lr= 1e-6 + self.n_steps * (self.lr - 1e-6) / self.warmup
else:
scheduled_lr=self.lr
param.sub_(scheduled_lr * (avg_grad_corrected/std + self.wd*param))
optimizer=AdamW(model.parameters())
adamw_loss=train(train_iterator, model, optimizer, criterion, device)
plot_loss(adamw_loss, "AdamW with wd=0, lr=1e-3, betas=(0.9, 0.99), eps=1e-8, warmup=1000")
不同wd值,损失趋势图:
optimizer = AdamW(model.parameters())
adamw_loss_0 = train(train_iterator, model, optimizer, criterion, device)
optimizer = AdamW(model.parameters(), wd=1)
adamw_loss_1 = train(train_iterator, model, optimizer, criterion, device)
optimizer = AdamW(model.parameters(), wd=0.1)
adamw_loss_01 = train(train_iterator, model, optimizer, criterion, device)
optimizer = AdamW(model.parameters(), wd=0.01)
adamw_loss_001 = train(train_iterator, model, optimizer, criterion, device)
plot_losses(adamw_loss_001, "AdamW with wd=0.01, lr=1e-3, betas=(0.9, 0.99), eps=1e-8, warmup=1000")
losses = [adamw_loss_1, adamw_loss_0, adamw_loss_01, adamw_loss_001]
labels = ["adamw_loss_1", "adamw_loss_0", "adamw_loss_01", "adamw_loss_001"]
plot_losses(losses, labels, "adamw_loss_1, adamw_loss_0, adamw_loss_01, adamw_loss_001", ymax=5)
总结
希望我们对这些算法有了更多的了解,不再把它们当作神秘的黑盒。实验表明,所有的自适应学习率算法都优于固定学习率算法。在这些自适应学习速率算法中,RMSprop显然排在最后,但剩下的Adagrad、Adadelta、Adam、AdamW和RAdam都非常相似。
正如本文开头所提到的,这些实验显示了每个优化算法在单个模型架构和任务上的性能。RMSprop在本文例子中输给了Adam,并不意味着它会一直会这样。
参考资料
https://blog.csdn.net/weixin_40245436/article/details/86723332
https://analyticsindiamag.com/ultimate-guide-to-pytorch-optimizers/
https://zhuanlan.zhihu.com/p/29920135
https://github.com/benbo/adagrad/blob/master/adagrad.py
https://github.com/bentrevett/a-tour-of-pytorch-optimizers