Optimizer优化器发展 从SGD到Adam(W)及其对比 (附Pytorch代码)

目录

梯度下降

SGD 

 Momentum动量法

NAG好像也被称为牛顿法

Adagrad 自适应学习率优化算法

RMSProp

Adam

AdamW


梯度下降

        梯度下降(Gradient Descent)是一种用于找到函数最小值的优化算法,它的思路是沿着函数梯度的反方向进行迭代,从而不断地逼近函数的最小值。在机器学习和深度学习中,梯度下降被广泛应用于训练模型的参数,使得损失函数最小化。

梯度下降的公式如下:

其中:

  • θt 是当前时刻的参数向量
  • J(θt​) 是需要被最小化的目标函数(如损失函数)
  • ∇θ​J(θt​) 是目标函数关于参数 θ 的梯度(一般∇符号表示梯度)
  • η 是学习率(learning rate),是一个正的常数

        这里只是简单的提及,网上相关资料有很多想学习还请自行搜索(毕竟我们这次的主题是优化器哈哈)

SGD 

        SGD(Stochastic Gradient Descent, 随机梯度下降)是梯度下降算法的一种变体,它在每次迭代时只使用一个数据样本(或一小批数据样本)来计算梯度,而不是使用整个训练数据集。

SGD的公式如下:

其中:

  • x^(i) 是训练数据集中的第i个样本
  • η∇θ​J(θt​;x(i)) 是损失函数关于当前参数 θt​ 在样本 x^(i) 上的梯度

        是不是好像明白了,但是仔细一想感觉思路还不是很清晰?没关系,我们接下来用一个例子来说明SGD的细节。

假设我们想要基于房屋面积(x)来预测房屋价格(y),使用线性模型:

y = θ0 + θ1 * x

其中:

  • x 是房屋面积,是模型的输入变量
  • y 是房屋价格,是模型需要预测的目标变量
  • θ0是模型的偏置参数(bias)
  • θ1是模型的权重参数,表示面积每增加一单位,价格的变化率

我们有一个训练数据集D,包含N个房屋样本:
D = {(x^(1), y^(1)), (x^(2), y^(2)), ..., (x^(N), y^(N))}

其中:

  • x^(i)是第i个房屋的面积
  • y^(i)是第i个房屋的实际价格

        我们的目标是找到最优的θ0和θ1,使得对于任意一个面积x,我们的模型y = θ0 + θ1 * x能够很好地预测出对应的房屋价格y。

        在随机梯度下降(SGD)中,每一次迭代我们随机采样一个训练样本(x^(i), y^(i)),计算该样本的损失函数关于θ0和θ1的梯度,然后更新θ0和θ1的值。

具体来说,在第t次迭代中:

  1. 随机选取一个训练样本(x^(i), y^(i))                                                                               (注意!这就是Stochastic的体现之处!随机选择xy,而不是传统的全部xy)
  2. 计算该样本的预测值 y_pred = θ0 + θ1 * x^(i)
  3. 计算损失函数(如均方误差) J = (y^(i) - y_pred)^2
  4. 计算J关于θ0和θ1的梯度:
    ∇J(θ0) = -2 * (y^(i) - y_pred)
    ∇J(θ1) = -2 * (y^(i) - y_pred) * x^(i)
  5. 根据学习率η,更新θ0和θ1:
    θ0 = θ0 - η * ∇J(θ0)
    θ1 = θ1 - η * ∇J(θ1)

重复上述步骤,直到收敛或满足停止条件。

        通过这个例子,我们可以看到x^(i)和y^(i)分别是训练数据样本的输入和目标值,而θ0和θ1是我们要学习的模型参数。SGD通过不断地在训练数据上迭代,更新模型参数θ,来找到最优解。

import torch.optim as optim
optimizer = optim.SGD(model.parameters(), lr=0.01)

Momentum动量法

        传统的梯度下降法在更新参数时只考虑了当前梯度的方向,而增加Momentum的梯度下降法则引入了一个"动量"的概念,使参数更新时不仅考虑了当前梯度方向,还考虑了之前的"速度"或"动量"。

        Momentum(动量)的概念借鉴自物理学,特别是在描述物体运动时,动量是质量和速度的乘积,用于描述物体运动的“惯性”。在梯度下降中引入Momentum的想法,是为了模拟这种惯性效果,使参数更新时能够在一定程度上保留之前更新的方向和速度,从而加速学习过程,同时帮助克服优化过程中可能遇到的一些问题,如摆脱局部最小值或横跨平坦区域

其中:

  • θt 为当前时刻的参数
  • vt 为当前时刻的动量向量
  • γ 为动量参数,通常取0.9这个经验值
  • η 为学习率
  • ∇θ​J(θt−1​) 为损失函数关于参数的梯度

对比普通梯度下降:

        可以看出,普通梯度下降直接使用当前梯度更新参数,而Momentum则融合了历史梯度信息。

对比SGD:

        SGD是对单个数据样本计算梯度,可看作是数据级的随机噪声。而Momentum也引入了一种更新过程的扰动,具有类似SGD部分噪声避免陷入局部最优的作用。

Momentum方法的优点:

  1. 指数加权融合了历史梯度,平滑了更新方向
  2. 加速了梯度下降过程,可以跳出局部最优解
  3. 在一定程度上抑制了振荡和oscilation

缺点:

  1. 动量参数γ需要调优,对参数比较敏感
  2. 在较平坦区域可能导致越过最优解

        后续的优化算法如RMSProp、Adam等也融合了动量思想,并在此基础上做了改进。

总之,Momentum是梯度下降法的一种加速优化方式,通过融合历史梯度信息平滑更新方向、加快收敛,且具有一定噪声避免陷入局部最优的优点。合理使用时能够显著提升模型训练效率。

optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

NAG好像也被称为牛顿法

        NAG(Nesterov Accelerated Gradient)是Nesterov提出的一种加速梯度下降的变体,相比经典的动量法,它在计算梯度时更看重当前的动量方向,从而获得了更好的加速效果。

NAG的更新公式如下:

        与经典动量法的区别在于,NAG在计算梯度 η∇θ​J 时,使用的参数值为 θ{t-1} - γv{t-1},即在当前参数值的基础上,先沿当前动量方向移动一步,再评估该位置的梯度。

        这样做相当于能够让算法提前看到前方的地形梯度,如果前面的梯度比当前位置的梯度大,那我就可以把步子迈得比原来大一些,如果前面的梯度比现在的梯度小,那我就可以把步子迈得小一些。

        但当我们讲上式展开,则会发现存在二阶梯度的影响 (参考文章)。所以相当于牛顿法因为考虑了二阶导所以才可以更快的收敛。

NAG方法的优点:

  1. 加速效果更好,比经典动量法收敛更快
  2. 结合了当前动量方向的信息,更贴近实际最优解的下降路径

缺点:

  1. 计算开销较大,需要多次计算损失函数梯度
  2. 动量参数γ设置不当,可能会越过最优解

NAG被认为是经典动量法的改进和升华版本。诸如Adam等现代优化算法也借鉴了NAG思想,即在更新梯度时考虑当前动量方向,以获得更快更好的收敛性能。

在实际应用中,NAG相比经典动量法收敛更快,是最常用的加速梯度下降方法之一,尤其在非平滑的错误面上表现出色。

参考文章(强烈建议去读):
比Momentum更快:揭开Nesterov Accelerated Gradient的真面目 - 知乎 (zhihu.com)

#PyTorch内置的动量优化器默认实现了NAG变体,只需设置nesterov=True即可:
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, nesterov=True)

Adagrad 自适应学习率优化算法

        Adagrad(Adaptive Gradient Algorithm)的核心思想是:为不同的参数适应性地分配不同的学习率,使学习率随着训练时间的推移而自适应地衰减。对于较高梯度的参数,学习率下降较快;对于较低梯度的参数,学习率下降较慢。

对学习率进行优化操作

  • gt 是当前时间步的梯度
  • rt 是截至当前时间步的所有梯度平方的累加和
  • ​η​ 是初始学习率
  • ϵ 是一个很小的平滑项,防止分母为0

对比梯度下降:

仔细看其实就是多了个 1/sqrt(r_t+ε) .

此外,Adagrad公式中分母的平滑项 ϵ 也非常小,主要是为了数值稳定性,确保分母不为0。

        可以看到Adagrad算法对梯度下降的改进就是增加了rt这部分。但你知道为什么是η/sqrt(r_t+ε)这种形式吗?

主要原因有两点:

  1. 自适应学习率

        在Adagrad和RMSProp等算法中,r_t实际上是对每个参数维度的历史梯度平方的累积(Adagrad)或指数加权累积(RMSProp)。较大的r_t意味着该维度的梯度值较大,参数在该方向上可能有较大的振荡,因此需要较小的学习率以保证稳定性。

        相反,较小的r_t意味着该维度的梯度较小,参数在该方向上的更新量可以相对较大一些。因此,η/sqrt(r_t+ε)实现了对不同参数维度自适应的学习率,避免了单一的全局学习率带来的问题。

  1. 数值稳定性

        将学习率η除以sqrt(r_t+ε)还有一个作用是防止分母为0导致数值不稳定。ε是一个很小的正数,确保分母不会为0。

        此外,开平方根的作用是约束学习率的数量级,使其呈现一种下降的趋势,有利于算法收敛。如果直接除以r_t,学习率会下降过快,收敛性能可能会变差。

        综上所述,η/sqrt(r_t+ε)提供了自适应学习率和数值稳定性的优点,是Adagrad/RMSProp等优化算法中一种常见且理论合理的设计选择。通过调节η的初始值和ε,可以控制整体学习率的下降速率。

optimizer = optim.Adagrad(model.parameters(), lr=0.01)

RMSProp

        RMSProp(Root Mean Square Propagation)是一种自适应学习率的优化算法,由Geoff Hinton提出。RMSProp是为了解决梯度下降算法在训练深度神经网络时遇到的一些问题,特别是在处理非稳定目标函数时的效率问题。RMSProp通过调整每个参数的学习率来尝试减少梯度下降的震荡,使得训练过程更加稳定和快速。

        在Adagrad的基础上进一步在学习率的方向上进行优化:

对比Adagrad:

        

与Adagrad的不同之处:

  1. Adagrad是基于所有过去梯度平方的累加和,即:
    G_t = G_(t-1) + g_t^2

  2. 而RMSProp是基于梯度平方的指数加权移动平均值,即:
    s_t = β * s_(t-1) + (1 - β) * g_t^2

  3. Adagrad由于是直接累加,会导致学习率持续快速衰减,后期收敛会变慢

  4. RMSProp 在计算 rt 时引入了衰减率 β,降低了陈旧梯度的权重,这样就避免了学习率过多衰减的问题。一般取 β 接近但小于1的值,如0.9。

        因此,RMSProp相比Adagrad,在保留了自适应调整学习率的优点的同时,也解决了后期收敛变慢的缺陷,所以在实际应用中更为广泛。

        简单来说就是RMSProp 相比 Adagrad 在保留历史梯度信息的策略上更加合理,这使得其在实际应用中表现更加优异。

optimizer = optim.RMSprop(model.parameters(), lr=0.01, alpha=0.99, eps=1e-08)

Adam

        Adam(Adaptive Moment Estimation)是目前深度学习中应用最广泛的优化算法之一。它本质上是将动量(Momentum)和RMSprop两种思想结合到一种算法中

Adam算法的更新公式如下:

第一个动量向量,对梯度做指数加权

第二个动量向量,对梯度平方做指数加权

修正的原因:由于我们一般将参数β设为接近于1,而再mt和vt初始值为0时 η/sqrt(v_t)+ε * mt 部分会趋近于0导致θ无法更新或更新速度极慢。

Adam与其他优化器效果对比:

例2.

Adam优点:

  • 整合了动量和自适应学习率思想,收敛快且能够自动调整
  • 在很多问题上表现优于其他算法
  • 超参数的默认值在很多情况下都能有不错的效果

缺点:

        总的来说,Adam是一种综合了多种优化技术的"缝合怪"算法,巧妙地结合了不同思路的优点,在很多问题上都有出色的表现。因此虽然它理论基础存疑,但在实践中仍被广泛使用作为深度学习模型的默认优化算法之一。未来也可能有更先进的优化算法出现。

optimizer = optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999), eps=1e-08)

 参考视频:
优化器 |SGD |Momentum |Adagrad |RMSProp |Adam_哔哩哔哩_bilibili

 想看相关论文请点击这里:

https://arxiv.org/pdf/1609.04747.pdf

AdamW

        虽然Adam从直觉上好像很符合我们的逻辑,但在实验中表明,SGD+momentum 可能比复杂的 Adam 表现更好。于是Adam又惨遭抛弃。

        但是到了 2017 年末,Adam 似乎又重获新生。Ilya Loshchilov 和 Frank Hutter 在他们的论文《Fixing Weight Decay Regularization in Adam》中指出,每个库在 Adam 上实施的权重衰减似乎都是错误的,并提出了一种简单的方法(他们称之为 AdamW)来修复它。AdamW的参数更新公式如下:

对比Adam:

        非常明显的,AdamW 与 Adam 的区别主要在于最后一步参数更新: AdamW在计算出θt后又增加了权重衰减项−ηλθt。

其实具体来说论文中将正则项与权重衰减方法进行了对比,大概意思如下:
        在AdamW中,正则项被添加在参数更新公式中而不是损失函数中,这是因为在自适应梯度算法中,如Adam,L2正则化并不像在标准SGD中那样有效。具体来说,L2正则化和权重衰减对于自适应梯度算法是不同的。L2正则化会导致历史参数和/或梯度幅度较大的权重受到的正则化较少,而权重衰减会以相同的速率对所有权重进行正则化,实际上对具有较大梯度幅度的权重进行更多的正则化。这种差异导致了在Adam中添加正则项以改善泛化性能的必要性。

        不了解L2正则项的请点击这里

现在让我们看看两者的效果对比

        AdamW中的添加正则项是为了改善Adam的正则化效果,通过将权重衰减与基于梯度的更新分离,从而实现更好的泛化性能。添加正则项的目的是为了提高Adam的性能,使其在某些问题上与SGD with momentum竞争力相当,减少从Adam到SGD之间的切换,简化选择数据集/任务特定训练算法和超参数的常见问题。

 相关论文:

[1711.05101] Decoupled Weight Decay Regularization (arxiv.org)

更多AdamW细节请看:当前训练神经网络最快的方式:AdamW优化算法+超级收敛 - 知乎 (zhihu.com)

#PyTorch中没有直接内置AdamW,但可以通过给Adam优化器设置weight_decay参数来近似实现:
optimizer = optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0.01)

代码中:

  • lr 是学习率
  • momentum 是动量系数
  • weight_decay 是权重衰减系数(即L2正则化系数)
  • alphaeps等是各个优化器的其他超参数
  • 20
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
GCN(Graph Convolutional Networks,图卷积网络)是一种用于图像分类、节点分类等图像处理任务的深度学习模型。下面我将介绍如何使用PyTorch实现GCN。 首先,我们需要安装PyTorch和其它必要的库。可以使用以下命令安装: ``` pip install torch torchvision pip install numpy scipy scikit-learn ``` 接下来,我们需要定义一个GCN模型。以下是一个简单的实现: ```python import torch import torch.nn as nn import torch.nn.functional as F class GCN(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim): super(GCN, self).__init__() self.fc1 = nn.Linear(input_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, output_dim) def forward(self, x, adj): x = F.relu(self.fc1(torch.mm(adj, x))) x = self.fc2(torch.mm(adj, x)) return F.log_softmax(x, dim=1) ``` 在上面的代码中,我们定义了一个三层的GCN模型。`input_dim`是输入特征的维度,`hidden_dim`是隐藏层的维度,`output_dim`是输出层的维度。`nn.Linear`是一个线性层,`F.relu`是一个激活函数,`F.log_softmax`是一个softmax函数。 接下来,我们需要定义一个训练函数。以下是一个简单的实现: ```python def train(model, optimizer, criterion, features, adj, labels, idx_train): model.train() optimizer.zero_grad() output = model(features, adj) loss = criterion(output[idx_train], labels[idx_train]) loss.backward() optimizer.step() return model, optimizer, loss.item() ``` 在上面的代码中,我们定义了一个训练函数。`features`是输入特征,`adj`是邻接矩阵,`labels`是标签,`idx_train`是训练样本的索引。我们使用`model.train()`将模型切换到训练模式,然后使用`optimizer.zero_grad()`将梯度清零,使用`model(features, adj)`计算输出,使用`criterion(output[idx_train], labels[idx_train])`计算损失,使用`loss.backward()`计算梯度,使用`optimizer.step()`更新参数。 接下来,我们需要定义一个测试函数。以下是一个简单的实现: ```python def test(model, features, adj, labels, idx_test): model.eval() output = model(features, adj) _, preds = torch.max(output, dim=1) correct = torch.sum(preds[idx_test] == labels[idx_test]) acc = correct.item() / len(idx_test) return acc ``` 在上面的代码中,我们定义了一个测试函数。`features`是输入特征,`adj`是邻接矩阵,`labels`是标签,`idx_test`是测试样本的索引。我们使用`model.eval()`将模型切换到测试模式,然后使用`model(features, adj)`计算输出,使用`torch.max`计算最大值,使用`torch.sum`计算正确的预测数量,使用`acc = correct.item() / len(idx_test)`计算准确率。 以下是一个完整的GCN模型的实现: ```python import torch import torch.nn as nn import torch.nn.functional as F class GCN(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim): super(GCN, self).__init__() self.fc1 = nn.Linear(input_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, output_dim) def forward(self, x, adj): x = F.relu(self.fc1(torch.mm(adj, x))) x = self.fc2(torch.mm(adj, x)) return F.log_softmax(x, dim=1) def train(model, optimizer, criterion, features, adj, labels, idx_train): model.train() optimizer.zero_grad() output = model(features, adj) loss = criterion(output[idx_train], labels[idx_train]) loss.backward() optimizer.step() return model, optimizer, loss.item() def test(model, features, adj, labels, idx_test): model.eval() output = model(features, adj) _, preds = torch.max(output, dim=1) correct = torch.sum(preds[idx_test] == labels[idx_test]) acc = correct.item() / len(idx_test) return acc ``` 接下来,我们需要加载数据。以下是一个简单的实现: ```python import numpy as np def load_data(): adj = np.array([[0, 1, 1, 0], [1, 0, 1, 0], [1, 1, 0, 1], [0, 0, 1, 0]]) features = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 1], [1, 0, 0]]) labels = np.array([0, 1, 2, 0]) idx_train = np.array([0, 1, 2]) idx_test = np.array([3]) return adj, features, labels, idx_train, idx_test ``` 在上面的代码中,我们定义了一个简单的图。`adj`是邻接矩阵,`features`是输入特征,`labels`是标签,`idx_train`是训练样本的索引,`idx_test`是测试样本的索引。 接下来,我们需要训练模型。以下是一个简单的实现: ```python import torch.optim as optim adj, features, labels, idx_train, idx_test = load_data() model = GCN(input_dim=features.shape[1], hidden_dim=16, output_dim=labels.max()+1) optimizer = optim.Adam(model.parameters(), lr=0.01) criterion = nn.CrossEntropyLoss() for epoch in range(100): model, optimizer, loss = train(model, optimizer, criterion, features, adj, labels, idx_train) acc = test(model, features, adj, labels, idx_test) print('Epoch: {:03d}, Loss: {:.4f}, Acc: {:.4f}'.format(epoch, loss, acc)) ``` 在上面的代码中,我们定义了一个优化器和一个损失函数。然后,我们使用一个循环来训练模型,并使用`test`函数测试模型。我们使用`print`函数打印训练和测试的损失和准确率。 以上就是使用PyTorch实现GCN的一些基本步骤。当然,这只是一个简单的实现,实际中还有很多需要改进的地方。希望能对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值