当前位置: 首页 > news >正文

Rust 中的 Tokio 线程同步机制

懦探畏蔡1. 为什么要研究优化器算法?

它的关联问题:训练为什么要调参,调的是什么参?

如果就这个问题去问各种大语言模型,它们能给出一堆的理由。

但就博主而言,答案只有一个:

干掉调参,解放生产力,榨干算力。

说到底就一个字"穷"。

在多年的研发生涯里,对调参这个事深恶痛绝,为什么辛辛苦苦架构出来的模型,一训练就崩,训练收敛慢到龟速,这严重影响了开发进度,并且增加了很多不可抗力的消耗。

我相信有很多业内同行,都有这种痛,训练了很久,效果依旧很差,泛化能力也不行,然后就开始苦恼,为什么自己没有足够的钱,足够的算力。

明明自己很好的思路,戛然而止,退而求其次。

早年间,博主经常半夜醒来,看训练的损失曲线,生怕训崩。就算没有训崩,自己花费了大量时间精力,却没有很好的回报。

一次又一次,是很打击信心的。

在付出了大量时间和人民币之后,博主终于从泥潭里爬出来了,时光荏苒,这个困扰我九年的问题,画上句号了。

那大语言模型是怎么回答这个问题的。

核心就一句话:

"没有新优化器,下一代模型根本训不起来。"

从理论上看,它是在解决一个尚未被完全理解的复杂高维优化问题,充满挑战与机遇。

解决基础性训练难题——让模型"能学"

从工程上看,它是降低AI研发成本、推动技术普及的关键杠杆。

追求极致的效率与效益——让模型"快学"且"省学"

从性能上看,它是提升模型最终准确性、鲁棒性和泛化能力的决定性因素。

提升模型的终极性能——让模型"学好"

最终达到,拓展AI的技术边界——让"不可能"成为"可能"

当然就这个问题,大家可以自行去追问各家的大语言模型,给出的结论大同小异。

2. 那博主为什么要写这篇博文?

最基本的还是希望抛砖引玉,希望能有更多的同行在力大砖飞,烧钱的当下,不要放弃底层算法的研究。

同时为更多的深度学习小白提供一个新的视角,学习并应用深度学习,温故而知新。

3. 那什么是优化器算法?

优化器算法是驱动机器学习模型学习的"引擎"。它的核心任务是:在训练过程中,根据损失函数计算出的梯度(即方向),以某种策略更新模型的参数,从而最小化损失函数。

可以将训练过程想象成在复杂地形中寻找最低点:

损失函数:代表地形的高度。

模型参数:代表我们在地形中的位置。

梯度:代表我们脚下最陡峭的下坡方向。

优化器:就是那个决定"往哪个方向走、走多大步、以及是否要考虑之前的惯性"的导航策略。

Adam (Adaptive Moment Estimation)

思想:目前最流行和默认的优化器之一。它结合了Momentum和RMSProp的优点。

它计算梯度的一阶矩(均值,提供动量)和二阶矩(未中心化的方差,用于自适应调整学习率)。

然后对这两个矩进行偏差校正,使其在训练初期不那么偏向于0。

优点:

通常收敛速度快。

对超参数的选择相对鲁棒(默认参数通常就能工作得很好)。

能处理噪声和稀疏梯度。

如果把Adam的一阶矩和二阶矩去掉,它就蜕变为SGD。

而随机梯度下降(朴素SGD)是一种优化算法,通过随机选取单个样本来近似梯度,从而迭代更新模型参数,收敛至最小值。

换句话说,朴素SGD是一个没有应用任何先验补充的野蛮人,较于Adam的平滑学习而言,它就像一只无头苍蝇,到处乱撞,也不知道该撞多少次才能收敛至最小值。

4. Adam相较于朴素SGD,它做了哪些改进?

引入动量缓冲m,也就是一阶矩,指数加权平滑梯度,它积累了历史梯度的方向趋势。使得朴素SGD的动荡趋于平稳平滑。

引入自适应步长v,也就是二阶矩,指数加权平均的平方,它积累了历史梯度平方的值趋势。

最终以 grad = m / sqrt(v) 作为目标梯度进行更新。

对于动量一阶矩,基本没啥好说的,就是求历史平均梯度,使得训练平稳。

核心还是自适应步长v,对于频繁更新、梯度大的参数,其二阶矩估计值大,因此实际更新步长会被调小(除以一个大数),避免"步子太大"而越过最优点。

对于不频繁更新、梯度小的参数,则给予更大的相对步长,鼓励其更新。

所以Adam能加速较于朴素SGD训练收敛,二阶矩功不可没。

原本故事到这里,就接近完结了。

在真实的场景下,我们发现Adam还是不够好。

但它的普及使得深度学习遍地开花。

虽然仍是需要调参,但是不像之前那么"玄学"了。

当然在一些场景下,例如GAN的训练,仍然有所争议。

Adam is no better than normalized SGD: Dissecting how adaptivity improves GAN performance | OpenReview

在博主的实测下,此文提及的nSGDA确实比朴素SGD稳健一些。

class nSGDA(torch.optim.Optimizer):

def __init__(

self,

params, # Model parameters

lr: Union[float, torch.Tensor] = 4e-5, # Learning rate (default: 4e-5)

# Coefficients used for computing running averages of gradient (default: 0.9)

momentum: float = 0.9,

# eps (float, optional): term added to the denominator to improve numerical stability (default: 1e-8)

eps: float = 1e-8,

weight_decay: float = 1e-2, # Weight decay (L2 penalty) (default:1e-2)

):

if lr < 0.0:

raise ValueError("Invalid learning rate: {}".format(lr))

if not 0.0 <= eps:

raise ValueError("Invalid epsilon value: {}".format(eps))

if momentum < 0.0 or momentum >= 1.0:

raise ValueError("Invalid momentum value: {}".format(momentum))

if weight_decay < 0.0:

raise ValueError("Invalid weight decay: {}".format(weight_decay))

defaults = dict(

lr=lr,

momentum=momentum,

weight_decay=weight_decay,

eps=eps)

super().__init__(params, defaults)

def step(self, closure=None):

r"""Performs a single optimization step.

Arguments:

closure: A closure that reevaluates the model and returns the loss.

"""

loss = None

if closure is not None:

loss = closure()

for group in self.param_groups:

momentum = group['momentum']

lr = group['lr']

weight_decay = group['weight_decay']

eps = group['eps']

one_minus_momentum = 1.0 - momentum

for p in group['params']:

if p.grad is None:

continue

if p.grad.is_sparse:

raise RuntimeError(

"current optimizer does not support sparse gradients")

state = self.state[p]

# State initialization

if len(state) == 0:

state["m"] = torch.zeros_like(p.grad, memory_format=torch.preserve_format)

m = state['m']

bias_correction = 1.0 - momentum ** state["step"]

if weight_decay != 0:

p.grad = p.grad.add(p.data, alpha=weight_decay)

m.mul_(momentum).add_(p.grad, alpha=one_minus_momentum)

step_size = lr / torch.norm(m.div(bias_correction)).add_(eps).mul_(bias_correction)

p.data.add_(m, alpha=-step_size)

return loss

当你采用Adam调参训练,总是跑崩或者无法收敛,这个时候,稍微尝试一下nSGDA也未尝不可。

而Adam二阶矩的存在也实实在在埋了一个雷 : “过冲”问题

本来“对于不频繁更新、梯度小的参数,则给予更大的相对步长,鼓励其更新。”

是个很好的想法,

但是有一个特例,那就是训练到后期,梯度理论上也会越来越小,这个时候也不应该鼓励其更新。

有可能一更新,跑飞了,这就是后来为什么存在早停(Early Stopping)策略的根由之一。

如果继续训练,有可能从次优解里爬出来,但是更多实际情况是,若这里就是最优解,

由于激进地更新,反而会越跑越远。

理想的情况肯定是,训练到最优解。最后停在最优解上,或者在最优解周围转圈。

但这里有个悖论,

你凭什么认为这里是最优解,而不是次优解,这个标准怎么界定判断。

而且由于数据的稀缺性,我们希望模型在这种情况下,还能有更强大的泛化能力,即使它没见过的数据,也能适配到位。

也就是说,

理想上我们既希望能求到解的思路规律,最好覆盖更多的求解路径,而不是一条最短的求解路径。

绕路没问题,只要这个绕路方式能提升泛化能力。

[1207.0580v1] Improving neural networks by preventing co-adaptation of feature detectors

这就是后来dropout盛行的原因之一,因为简单有效。

让一部分神经元失活,也能求到解。

但是dropout这个技术思路,慎用,用得不好,反而会起反作用。

路漫漫其修远兮,一起努力吧~

5. 后Adam家族时代,百家争鸣

由于这个话题展开,真的可以写一本书了。

所以本文的核心是"速览",博主带着大家看一看这后Adam的各种巧思。

相关的算法实现,可以参考以下项目仓库:

PyTorch:

https://github.com/kozistr/pytorch_optimizer

TensorFlow/Keras:

https://github.com/NoteDance/optimizers

本文没有提及的其他算法,自行移步查阅。

5.1 砍Adam的显存

由于一阶矩m和二阶矩v都需要历史平滑,所以Adam至少要占用两倍的可训练模型参数。

这样一来,只要模型参数一大,那训练的时候 1+2 = 3 至少要存储三份权重。显存很快就不够用了。

所以,针对这个问题,我们开始磨刀霍霍向二阶矩v。

5.1.1 18年的Adafactor

[1804.04235v1] Adafactor: Adaptive Learning Rates with Sublinear Memory Cost

社区比较知名的实现:

transformers/src/transformers/optimization.py at main · huggingface/transformers · GitHub

5.1.2 19年的SM3

[1901.11150] Memory-Efficient Adaptive Optimization

官方实现:

https://github.com/google-research/google-research/tree/master/sm3

Adafactor和SM3都是分解近似的做法。SM3的实现较为复杂,所以基本上没有被推广开来。所以很长一段时间都是Adafactor是主流。

但是Adafactor的实现稍微有些问题。

问题函数:

@staticmethod

def _approx_sq_grad(exp_avg_sq_row, exp_avg_sq_col):

# copy from fairseq's adafactor implementation:

# https://github.com/huggingface/transformers/blob/8395f14de6068012787d83989c3627c3df6a252b/src/transformers/optimization.py#L505

r_factor = (exp_avg_sq_row / exp_avg_sq_row.mean(dim=-1, keepdim=True)).rsqrt_().unsqueeze(-1)

c_factor = exp_avg_sq_col.unsqueeze(-2).rsqrt()

return torch.mul(r_factor, c_factor)

_approx_sq_grad 这个实现丢失了不少精度。

博主认为比较合理的实现,是把sqrt放到最后计算,精度会高些。

@staticmethod

def _approx_sq_grad(row_exp_avg_sq, col_exp_avg_sq):

row_factor = row_exp_avg_sq.unsqueeze(-1)

row_factor = row_factor.mean(dim=-2, keepdim=True).div(row_factor)

col_factor = col_exp_avg_sq.unsqueeze(-2)

return row_factor.div(col_factor).sqrt_()

5.1.3 22年的Amos

[2210.11693]Amos: An Adam-style Optimizer with Adaptive Weight Decay towards Model-Oriented Scale

在Adafactor和SM3之后很长一段时间,砍优化器显存占用这个事情似乎被遗忘了。

直到Amos的出现,它进一步砍掉了v的显存占用,直接采用了平方均值,美其名曰"信息共享"。

显存不够用,又想保住精度,可以考虑采用Amos,当然它较之Adam还有不少改进点。

5.1.4 24年损失作为学习率的奇思妙想

利用损失值(loss)本身来动态调整优化器的学习率,以此作为替代二阶v实现更快的收敛。

非常简单的思路: “损失越大,学习率越大;损失越小,学习率越小。”

AdaLo: Adaptive learning rate optimizer with loss for classification

由于论文没有给出开源实现,也没有搜到第三方实现。

参考论文的思想,实现了该思路,代码实现不完全对应论文内容,仅供参考学习。

# mypy: allow-untyped-defs

from typing import Tuple, Union

import torch

from torch import GradScaler

class AdaLo(torch.optim.Optimizer):

r"""

AdaLo: Adaptive Learning Rate Optimizer with Loss for Classification

paper: https://www.sciencedirect.com/science/article/abs/pii/S0020025524015214

code: https://github.com/cpuimage/AdaLo

usage:

for inputs, labels in dataloader:

def closure(inp=inputs, lbl=labels):

optimizer.zero_grad()

loss = criterion(model(inp), lbl)

loss.backward()

return loss

optimizer.step(closure)

Args:

params: Iterable of parameters to optimize or dicts defining

parameter groups.

lr: Learning rate (not used for step size calculation due to the adaptive learning rate mechanism; retained solely for API consistency)

betas: (beta1, beta2) coefficients for gradient momentum and loss-EMA smoothing respectively

weight_decay: L2 weight decay

kappa: loss scaling factor

eps: float. term added to the denominator to improve numerical stability.

mode: control learning rate adaptation mode ('adversarial' or 'compliant')

'adversarial': decrease learning rate when loss increases (conservative strategy)

'compliant': increase learning rate when loss increases (aggressive strategy)

"""

def __init__(self,

params,

lr: Union[float, torch.Tensor] = 1e-8,

betas: Tuple[float, float] = (0.9, 0.999),

weight_decay: float = 1e-2,

kappa: float = 3.0,

eps: float = 1e-8,

mode: str = 'adversarial'):

if lr < 0.0:

raise ValueError("Invalid learning rate: {}".format(lr))

if betas[0] < 0.0 or betas[0] >= 1.0:

raise ValueError("Invalid beta1 value: {}".format(betas[0]))

if betas[1] < 0.0 or betas[1] >= 1.0:

raise ValueError("Invalid beta2 value: {}".format(betas[1]))

if weight_decay < 0.0:

raise ValueError("Invalid weight decay: {}".format(weight_decay))

defaults = dict(lr=lr, beta1=betas[0], beta2=betas[1], weight_decay=weight_decay, kappa=kappa,

mode=mode, eps=eps)

super(AdaLo, self).__init__(params, defaults)

def step(self, closure=None, scaler: GradScaler = None, loss=None):

already_updated_by_scaler = False

if closure is not None:

with torch.enable_grad():

loss = closure()

if scaler is not None:

scaler.scale(loss).backward()

scaler.unscale_(self)

scaler.step(self, loss=loss)

scaler.update()

already_updated_by_scaler = True

if not already_updated_by_scaler:

for group in self.param_groups:

beta1 = group['beta1']

beta2 = group['beta2']

weight_decay = group['weight_decay']

kappa = group['kappa']

mode = group['mode']

eps = group['eps']

for p in group['params']:

if p.grad is None:

continue

if p.grad.is_sparse:

raise RuntimeError("current optimizer does not support sparse gradients")

state = self.state[p]

if len(state) == 0:

state['m'] = torch.zeros_like(p.data)

state['loss_ema'] = torch.tensor(0.0, device=p.device, dtype=p.dtype)

m = state['m']

loss_ema = state['loss_ema']

m.lerp_(p.grad, 1.0 - beta1)

if loss is not None:

scaled_loss = torch.log1p(loss.detach())

transformed_loss = (torch.tanh(-scaled_loss * 0.5) + 1.0) * 0.5

loss_ema.lerp_(transformed_loss, 1.0 - beta2)

if mode == 'adversarial':

lr_t = loss_ema.div(kappa).clamp_min_(eps)

else:

lr_t = (1.0 - loss_ema).div(kappa).clamp_min_(eps)

if weight_decay != 0:

p.data.mul_(1.0 - lr_t * weight_decay)

p.data.sub_(m * lr_t)

return loss

在一些场景下实测也是很稳健,lr = v = loss 不得不夸一下论文原作者的奇思妙想。

PyTorch官方使用amp混合精度的时候,GradScaler.step里有这么一句。

if "closure" in kwargs:

raise RuntimeError(

"Closure use is not currently supported if GradScaler is enabled."

)

也就是说闭包和amp混合当前不支持一起用。

在AdaLo代码仓库里,博主演示怎么魔改实现闭包和amp可以同时使用,感兴趣的可以阅读具体实现。

在实测过程中,发现 “损失越大,学习率越大;损失越小,学习率越小。”

这个做法在一些场景下比较激进,所以增加了一个新的参数为mode可切换学习率适配模式,默认设为保守模式。

分别对应

- adversarial (保守模式):“损失越大,学习率越小;损失越小,学习率越大。”

- compliant (激进模式) :“损失越大,学习率越大;损失越小,学习率越小。”

5.2 Adam二阶矩v为0的问题

导致v为0有很多原因,在模型训练的不同阶段,由于噪声也好,精度也好,会直接或者间接导致v为0。

前面提到 grad = m / sqrt(v)

早期Adam论文里的解决方案就是直接给v加上一个epsilon,一般设为1e-8,避免除以0。

而后续经过不少团队的实践发现这么做有点鲁莽。

然后就有人开始针对这个问题进行修改。

但是林林总总,都是把epsilon移来移去,例如梯度平方后就加上epsilon,再进行指数加权平均。

也有采用softplus抑制分母过小的做法:

[1908.00700] Calibrating the Adaptive Learning Rate to Improve Convergence of ADAM

grad = m / softplus(sqrt(v))

这个问题一直到了2024年,有新的进展。

[2407.05872v2] Scaling Exponents Across Parameterizations and Optimizers

方法很简单,删除epsilon,采用atan2。

grad = atan2(m, sqrt(v))

从数值稳定的角度来说,atan2确实是稳定了许多,而且基本规避了一些特殊情况下训练跑崩,导致损失为nan的情况。

Adam的betas默认参数是(0.9,0.999) ,也有人觉得这里也存在调参适配问题。

删除epsilon一般都可以理解,但把动量参数也干掉,做成自适应的"胆大妄为",也是挺绝的。

[2510.04988v1] Adaptive Memory Momentum via a Model-Based Framework for Deep Learning Optimization

不管成不成功,效果几何,就这魄力,值得我在此一提。

5.3 Adam的梯度长尾问题

这个很好理解,由于一阶矩m和二阶矩v都采用了指数平均,在不同程度上也是导致梯度长尾的诱因之一。

因为求平均值这个事,就跟奥运比赛打分一样,只用均值很不公平。去掉一个最高分,去掉一个最低分,然后再算平均相对合理一些。

求损失均值的时候一样存在,博主曾经设想过,也许求损失的中位数是一个可行的做法,但也有一定的局限性。

没有经过严格验证的求损失中位数思路的实现,仅供参考:

def soft_median(losses, temperature=None):

if temperature is None:

temperature = max(0.1, 0.5 * losses.std())

if losses.numel() % 2 == 0:

losses = torch.cat([losses, losses.new_zeros(1)])

x_sorted, _ = torch.sort(losses)

n_loss = losses.shape[0]

median_idx = (n_loss - 1) * 0.5

idxs = torch.arange(n_loss, device=losses.device, dtype=losses.dtype)

weights = torch.softmax(-torch.abs(idxs - median_idx) / temperature, dim=0)

return torch.dot(weights, x_sorted)

同样的,梯度在训练过程中变化很大,一些长尾样本带来的贡献就会被淹没掉。

带来的后果,不是过拟合,就是泛化差,能拿到次优解那是属于幸运儿了。

这个方向的研究多,也不多,因为很多长尾问题基本上不会考虑在优化器里解决,一般会采用损失加权惩罚的思路来缓解。

这篇论文可以帮助进一步理解梯度长尾问题。

[2201.05938v2] GradTail: Learning Long-Tailed Data Using Gradient-based Sample Weighting

当然它不是一个主流的方案和思路,主流的方案更多的是采用元学习之类的做法,局限性也比较大。

那该如何直观地洞察梯度长尾呢?

采用TensorBoard,对参数和梯度进行可视化,查看其直方图,非常直观。

示例如下:

参数直方图:

20251007-133453

从参数权重的分布来看,蓝色左边一直在拖尾,红色的左边尾巴开始右移聚拢。从参数来看,可以看到一些趋势,但不够直观。

我们再来看其对应的梯度直方图:

20251007-133459

http://www.dtcms.com/a/611308.html

相关文章:

  • 平台网站建设方案书建设视频网站费用吗
  • 网站关键词字符编辑昌江网站建设
  • CRS税务合规解决方案:马来西亚税号 vs 新加坡自雇EP全面解析(中国卖家税务规划指南)
  • 基于Rust实现高性能数据处理引擎
  • 可以做网站引导页的页面中文域名注册 .网站
  • 问答社区网站建设艺术风格网站
  • 江苏企业建设网站公司网页制作基础教程黄洪杰
  • 做技术网站赚钱吗太原网络广告公司
  • 住房和城乡建设部网站注册山东网站建设最便宜
  • Redhat 8.10 离线升级 Redhat 9.6
  • 百日挑战——单词篇(第二十二天)
  • 管家婆单机软件如何在SQL2008R2进行账套升级?
  • 企业网站建设层次简述企业形象管理咨询的基本内容
  • opencv 学习: 09 邻近像素处理,以高通滤波图片锐化为例
  • 湖北省建设工程造价管理协会网站优化软件
  • 建设vip网站相关视频企业营业执照
  • cnzz统计代码放在网站泉州微信网站建设
  • 算法备案全攻略:材料清单与避坑指南
  • 做外贸电商网站有哪个国家最新防疫政策
  • 诸几建设银行网站洛可可在线设计平台
  • 电子商务网站开发价格安徽工程造价信息网
  • 快看点自媒体平台网站怎样做优化
  • 微信小程序 点击某个marker改变其大小
  • 51Sim 4DGS闭环仿真架构,让基于真实数据的闭环仿真成为可能
  • 基于质谱的蛋白质组学能用来研究多肽的结构和功能吗?
  • 网站速度优化方案一般可以在哪些网站做推广
  • deep-oc-sort——yolov5/8/9/10/11/12/13+deep-oc-sort算法的目标跟踪实现
  • Gitee使用笔记
  • 看摄影作品的网站长沙seo研究中心
  • 襄樊网站制作公司怎么在阿里云建网站