VLA 系统学习第 8 课:Optimizer 到底在干什么?——从 Learning Rate 到 SGD、Momentum 和 Adam

第七课标准答案

先把上一课的 9 个问题完整收掉,然后直接进入第 8 课。今天的主线会继续沿着同一条训练链往后走:

\ \\text{Gradient} \\rightarrow \\text{Learning Rate} \\rightarrow \\text{Optimizer} \\rightarrow \\text{Parameter Update} \\


1. 什么叫计算图?为什么 Forward 时要保留运算关系?

计算图可以理解成:

记录"一个结果是由哪些变量经过哪些运算得到的"这条关系链。

例如:

\ z=wx \\\ L=z\^2 \\

可以画成:

复制代码
w ──┐
    × → z → square → L
x ──┘

Forward 时当然要先算:

\ w,x\\rightarrow z\\rightarrow L \\

但训练还需要知道:

最终的 \(L\) 到底和前面的 \(w\) 有什么关系?

只有保留这条关系,Backward 才能从:

\ L \\

一路反向找到:

\ w \\

并计算:

\ \\frac{\\partial L}{\\partial w} \\

所以计算图不是为了"画图好看",而是为了让自动微分知道:

梯度应该沿哪条计算路径往回传。


2. 为什么

\ \\frac{\\partial L}{\\partial w} = \\frac{\\partial L}{\\partial z} \\frac{\\partial z}{\\partial w} \\

?

因为 \(w\) 没有直接决定 \(L\)。

它先影响:

\ z \\

而 \(z\) 再影响:

\ L \\

也就是:

\ w\\rightarrow z\\rightarrow L \\

所以 \(w\) 对 \(L\) 的影响,需要把中间两段影响连起来:

\ w\\rightarrow z \\

这一段:

\ \\frac{\\partial z}{\\partial w} \\

以及:

\ z\\rightarrow L \\

这一段:

\ \\frac{\\partial L}{\\partial z} \\

最终:

\ \\frac{\\partial L}{\\partial w} = \\frac{\\partial L}{\\partial z} \\frac{\\partial z}{\\partial w} \\

这就是 Chain Rule,链式法则。


3. 已知

\ x=2,\\qquad w=3 \\\ z=wx,\\qquad L=z\^2 \\

求:

\ \\frac{\\partial L}{\\partial w} \\

先 Forward:

\ z=3\\times2=6 \\

然后:

\ L=6\^2=36 \\

现在反向。

因为:

\ L=z\^2 \\

所以:

\ \\frac{\\partial L}{\\partial z}=2z=12 \\

又因为:

\ z=wx \\

所以:

\ \\frac{\\partial z}{\\partial w}=x=2 \\

于是:

\ \\frac{\\partial L}{\\partial w} = 12\\times2 \\

得到:

\ \\boxed{24} \\


4. ReLU 在正数和负数区域的梯度分别是什么?

ReLU:

\ ReLU(x)=\\max(0,x) \\

所以:

\ x\>0 \\

时:

\ ReLU(x)=x \\

导数:

\ \\frac{dReLU}{dx}=1 \\

而:

\ x\<0 \\

时:

\ ReLU(x)=0 \\

导数:

\ \\frac{dReLU}{dx}=0 \\

所以:

\ \\boxed{ ReLU'(x)= \\begin{cases} 0,\&x\<0\\\\ 1,\&x\>0 \\end{cases} } \\

这意味着如果 Forward 时某个值小于 0,那么这一位置在 Backward 时梯度会被截断为 0。


5. loss.backward() 后,是 weight 变了还是 weight.grad 有了结果?

是:

\ \\boxed{weight.grad} \\

有了结果。

例如:

复制代码
loss.backward()

以后:

复制代码
layer.weight

参数本身还没有因为这句话而更新。

但:

复制代码
layer.weight.grad

里面已经存着:

\ \\frac{\\partial L}{\\partial W} \\

真正修改 Weight 的是:

复制代码
optimizer.step()

所以必须记住:

\ \\boxed{ backward = 算梯度 } \\\ \\boxed{ step = 改参数 } \\


6. 为什么 nn.Linear 的 Weight 不需要手动设置 requires_grad=True?

因为:

复制代码
nn.Linear(...)

内部的:

复制代码
weight
bias

不是普通 Tensor,而是被 PyTorch 注册为:

Parameter

这些可训练参数默认就会参与梯度计算。

所以你通常会看到:

复制代码
for name, param in model.named_parameters():    print(name, param.requires_grad)

输出类似:

复制代码
fc1.weight True
fc1.bias   True
fc2.weight True
fc2.bias   True

7. nn.Parameter 和普通 Tensor 最核心的区别是什么?

普通 Tensor:

复制代码
x = torch.tensor(...)

本质上只是数据。

而 nn.Parameter 可以理解成:

被 nn.Module 识别、注册和管理的可训练 Tensor。

所以它可以被:

复制代码
model.parameters()

找到。

Optimizer 才能进一步拿到它:

复制代码
optimizer = Adam(model.parameters())

然后训练时更新它。


8. 为什么 Observation 一般不需要梯度,而模型 Parameter 需要?

因为我们训练的目标不是修改 Observation。

例如输入:

\ o_t \\

它只是数据。

我们真正想改变的是:

\ \\theta \\

也就是:

\ W,b,\\ldots \\

所以训练关心:

\ \\frac{\\partial L}{\\partial W} \\\ \\frac{\\partial L}{\\partial b} \\

而通常不需要:

\ \\frac{\\partial L}{\\partial o_t} \\

因此 Robot Observation 通常作为普通 Tensor 使用,而 Parameter 要保存梯度。


9. 五行训练代码完整因果关系是什么?

复制代码
optimizer.zero_grad()pred = model(obs)loss = loss_fn(pred, action)loss.backward()optimizer.step()

完整关系是:

复制代码
清空旧梯度
↓
当前参数做 Forward
↓
得到预测 Action
↓
和 Expert Action 比较
↓
得到 Loss
↓
Backward
↓
沿计算图计算每个 Parameter 的 Gradient
↓
Optimizer读取 Gradient
↓
修改 Parameter

数学上就是:

\ O \\rightarrow \\pi_\\theta \\rightarrow \\hat A \\

然后:

\ L(\\hat A,A) \\

接着:

\ \\nabla_\\theta L \\

最后:

\ \\theta_{\\text{new}} = \\theta_{\\text{old}} +\\text{某种更新量} \\

到底这个"更新量"怎么算,就是下一课。


VLA 系统学习第 8 课:Optimizer 到底在干什么?------从 Learning Rate 到 SGD、Momentum 和 Adam

上一课我们已经知道:

\ loss.backward() \\

会得到:

\ \\nabla_\\theta L \\

也就是每个参数对应的梯度。

但梯度只回答:

往哪个方向改,Loss 会变大或变小?

它还没有完整回答:

每次到底应该改多少?

这就是 Optimizer 要解决的问题。

最基础的更新公式:

\ \\theta_{\\text{new}} = \\theta_{\\text{old}} - \\eta\\nabla_\\theta L \\

这里新出现的:

\ \\eta \\

就是:

Learning Rate

学习率。


一、先只看一个参数,理解 Learning Rate

假设模型只有一个参数:

\ w \\

当前:

\ w=1 \\

Backward 算出:

\ \\frac{\\partial L}{\\partial w}=-16 \\

梯度是负数。

说明:

如果 \(w\) 增大,Loss 会下降。

最基础更新:

\ w_{\\text{new}} = w_{\\text{old}} - \\eta \\frac{\\partial L}{\\partial w} \\

假设:

\ \\eta=0.01 \\

那么:

\ w_{\\text{new}} = 1-0.01\\times(-16) \\

得到:

\ w_{\\text{new}}=1.16 \\

所以参数往正确方向增加了一点。

这里:

\ \\eta \\

控制的就是:

这一步迈多大。


二、Learning Rate 为什么不能随便设?

这可以用"下山"来理解。

假设 Loss 是山的高度。

我们的目标:

\ \\min_\\theta L(\\theta) \\

就是:

从山上走到最低处。

Gradient 告诉你:

哪边是上坡最快的方向。

因此负 Gradient:

\ -\\nabla L \\

告诉你:

哪边大致是下坡方向。

但是知道方向之后,还需要决定:

一步迈多大?

这就是 Learning Rate。


三、Learning Rate 太小会发生什么?

假设正确方向已经知道,但:

\ \\eta=0.0000001 \\

那么每一步:

\ \\Delta w = -\\eta\\frac{\\partial L}{\\partial w} \\

都非常小。

模型虽然在学习,但可能像:

复制代码
1.000000
→ 1.000002
→ 1.000004
→ 1.000006
→ ...

训练会非常慢。

所以可能出现:

复制代码
Loss 在下降
但是下降得极其缓慢

这通常意味着:

Learning Rate 可能过小,或者还有其他优化问题。


四、Learning Rate 太大又会怎样?

假设最低点在:

\ w=3 \\

当前:

\ w=1 \\

本来应该慢慢:

复制代码
1
→ 1.4
→ 1.8
→ 2.2
→ 2.6
→ 2.9
→ 3

但如果步子特别大:

复制代码
1
→ 5
→ 0
→ 7
→ -2
→ ...

会不断跨过最低点。

于是 Loss 可能:

复制代码
下降
↓
突然上升
↓
再下降
↓
剧烈振荡

更严重时数值直接爆掉,可能看到:

复制代码
loss = nan

所以:

\ \\boxed{ Learning\\ Rate太小 \\rightarrow 学得太慢 } \\\ \\boxed{ Learning\\ Rate太大 \\rightarrow 震荡甚至发散 } \\


五、最基础的 Optimizer:Gradient Descent

最简单更新规则就是:

\ \\theta_{t+1} = \\theta_t - \\eta\\nabla_\\theta L \\

意思是:

  1. 算当前 Gradient;
  2. 乘 Learning Rate;
  3. 沿负梯度方向更新。

但真实训练通常不是用整个 Dataset 一次性算梯度,而是使用一个 Batch。

因此我们经常说:

Stochastic Gradient Descent

也就是:

SGD

PyTorch:

复制代码
optimizer = torch.optim.SGD(    model.parameters(),    lr=0.01)

这里:

复制代码
model.parameters()

告诉 Optimizer:

更新哪些参数。

而:

复制代码
lr=0.01

告诉它:

每次更新的基础步长有多大。


六、为什么叫 Stochastic?

理想情况下,可以用整个 Dataset 计算真正的平均梯度:

\ \\nabla_\\theta L_{\\text{all data}} \\

但假设 Dataset 有:

\ 10\^7 \\

个样本。

每更新一次参数都把全部数据算一遍,成本太高。

所以实际中通常每次只拿一个 Batch:

\ B=32 \\

或者:

\ B=64 \\

根据当前 Batch 估计:

\ \\nabla_\\theta L \\

因此这个梯度带有一定随机性和噪声。

这就是 Stochastic 的来源。

实际上深度学习中常见的所谓 SGD,很多时候更准确地说是:

Mini-batch SGD


七、Batch 不同,Gradient 为什么也不同?

假设 Dataset 中有:

复制代码
Batch 1:
一组比较简单的数据

Batch 2:
一组偏左的数据

Batch 3:
一组偏右的数据

每个 Batch 对 Loss 的贡献不同。

所以算出来:

\ \\nabla_\\theta L_1 \\\ \\nabla_\\theta L_2 \\\ \\nabla_\\theta L_3 \\

不一定完全相同。

于是参数更新可能:

复制代码
这一批稍微往左改
↓
下一批稍微往右改
↓
再下一批又往另一个方向改

这也是训练曲线不会像一条完美平滑直线下降的原因之一。


八、为什么纯 SGD 有时会抖?

想象一个狭长山谷。

最低点在谷底。

但是梯度每次都有一点不同。

可能出现:

复制代码
左
→ 右
→ 左
→ 右
→ 左

虽然整体正在向谷底走,但横向一直来回震荡。

这时可以引入:

Momentum

动量。


九、Momentum 的直觉是什么?

想象一个球从山坡上滚下来。

它不会每一步都完全只看当前坡度。

它还有:

之前运动留下来的惯性。

如果连续几步都在向同一个方向走,那么这个方向的速度会逐渐积累。

这就是 Momentum 的直觉。


十、Momentum 的基本数学思想

最基础的一种写法:

\ v_t = \\beta v_{t-1} + \\nabla_\\theta L_t \\

然后:

\ \\theta_{t+1} = \\theta_t - \\eta v_t \\

这里:

\ v_t \\

可以理解为:

累积的更新趋势。

\ \\beta \\

控制:

之前的方向保留多少。

例如:

\ \\beta=0.9 \\

意味着过去的运动趋势会被较强保留。

所以如果过去连续很多次都:

往右是正确方向,

那么即使这一批数据梯度稍微抖一下,整体也不会马上掉头。


十一、Momentum 为什么能减少震荡?

假设横向梯度:

复制代码
+1
-1
+1
-1

不断来回变。

长期来看互相抵消。

而真正朝谷底的方向一直是:

复制代码
-2
-2
-2
-2

那么 Momentum 会逐渐积累稳定方向。

结果就是:

复制代码
横向抖动减弱
↓
主方向速度增强
↓
更快走向低 Loss 区域

所以 Momentum 主要解决:

SGD 更新方向容易受当前 Batch 噪声影响的问题。


十二、PyTorch 里的 SGD + Momentum

可以写:

复制代码
optimizer = torch.optim.SGD(    model.parameters(),    lr=0.01,    momentum=0.9)

现在 Optimizer 不再只看:

\ 当前Gradient \\

而是同时利用:

\ 当前Gradient + 过去更新趋势 \\


十三、那为什么现代深度学习经常用 Adam?

接下来是你以后会极其频繁看到的:

复制代码
torch.optim.Adam(...)

甚至:

复制代码
torch.optim.AdamW(...)

先不讲 AdamW,先把 Adam 的核心直觉搞清楚。

SGD 有一个比较简单的问题:

所有参数基本共享同一个全局 Learning Rate。

但是神经网络不同参数的梯度尺度可能差别非常大。

例如:

\ w_1.grad=0.0001 \\

而:

\ w_2.grad=100 \\

如果都乘:

\ \\eta=0.01 \\

那么两个参数的实际更新幅度差异会非常巨大。

Adam 想做的事情之一就是:

根据每个参数自身过去的梯度情况,自适应地调整它的有效更新尺度。


十四、Adam 同时记住两件事

Adam 会维护两类统计量。

第一类:

梯度最近大概往哪个方向走。

可以写成一阶矩:

\ m_t \\

第二类:

梯度最近大概有多大。

可以写成二阶矩:

\ v_t \\

先不要被"矩"这个词吓到。

现在只需要理解:

\ m_t \\

大致像:

带 Momentum 的平均方向。

而:

\ v_t \\

大致反映:

梯度大小的历史信息。


十五、Adam 的一阶矩

核心思想类似:

\ m_t = \\beta_1m_{t-1} + (1-\\beta_1)g_t \\

这里:

\ g_t=\\nabla_\\theta L_t \\

也就是当前 Gradient。

如果:

\ \\beta_1=0.9 \\

那么:

\ m_t \\

会保留过去的方向趋势。

所以这一部分和 Momentum 很像。


十六、Adam 的二阶矩

Adam 还会记录:

\ v_t = \\beta_2v_{t-1} + (1-\\beta_2)g_t\^2 \\

注意:

\ g_t\^2 \\

表示梯度平方。

因此 \(v_t\) 大致反映:

最近这个参数的梯度通常有多大。

如果某个参数梯度长期特别大,Adam 会相应缩小它的有效步长。

如果某个参数梯度长期比较小,Adam 可以相对给它更合适的更新尺度。


十七、Adam 最终怎么更新?

先忽略 bias correction 等细节,直觉上可以看成:

\ \\theta_{t+1} = \\theta_t - \\eta \\frac{m_t}{\\sqrt{v_t}+\\epsilon} \\

这里:

\ m_t \\

提供:

稳定的更新方向。

\ \\sqrt{v_t} \\

提供:

对梯度尺度的调节。

\ \\epsilon \\

是一个很小的数,用于数值稳定,避免除零等问题。

所以 Adam 的核心可以先记成:

\ \\boxed{ Momentum思想 + 每个参数自己的自适应步长 } \\


十八、为什么 Adam 经常比最基础 SGD 更容易直接用?

因为它对不同 Parameter 的梯度尺度进行了自适应调整。

现实网络里:

复制代码
Vision Encoder
Transformer
MLP
Action Head

不同层的梯度尺度可能很不一样。

Adam 往往能让训练初期更容易稳定起来。

所以你以后会大量看到:

复制代码
optimizer = torch.optim.Adam(    model.parameters(),    lr=1e-4)

或者:

复制代码
optimizer = torch.optim.AdamW(...)

尤其在 Transformer、VLM、VLA 中非常常见。


十九、Adam 是不是就不需要 Learning Rate 了?

不是。

这是非常容易误解的地方。

即使 Adam 会自适应不同参数的更新尺度,仍然需要:

\ \\eta \\

也就是基础 Learning Rate。

例如:

复制代码
optimizer = torch.optim.Adam(    model.parameters(),    lr=1e-4)

这里:

\ 10\^{-4} \\

仍然非常重要。

Adam 不是:

自动把 Learning Rate 完全解决了。

而是:

在给定基础 Learning Rate 的情况下,对每个参数的更新进一步进行自适应调整。


二十、为什么不同 Optimizer 的 step() 行为不同?

现在你终于可以真正理解:

复制代码
optimizer.step()

并不是固定的一种数学操作。

如果 Optimizer 是 SGD:

复制代码
torch.optim.SGD(...)

它按照 SGD 的规则更新。

如果是 SGD + Momentum:

复制代码
torch.optim.SGD(..., momentum=0.9)

它会利用历史速度。

如果是 Adam:

复制代码
torch.optim.Adam(...)

它会利用一阶矩和二阶矩。

所以:

复制代码
optimizer.step()

只是一套统一接口。

真正采用什么更新公式,取决于:

复制代码
optimizer

这个对象具体是哪一种 Optimizer。

这和前面学习的面向对象思路又连起来了。


二十一、把代码真正看懂

现在:

复制代码
optimizer = torch.optim.Adam(    model.parameters(),    lr=1e-3)

可以完整翻译成:

创建一个 Adam Optimizer 对象,让它管理 model.parameters() 中所有可训练参数,并使用基础学习率 \(10^{-3}\)。

训练:

复制代码
optimizer.zero_grad()pred = model(obs)loss = loss_fn(pred, action)loss.backward()optimizer.step()

现在最后一句真正意味着:

Adam 读取每个 Parameter 当前的 .grad,结合自己保存的历史梯度统计,根据 Adam 更新规则修改模型参数。


二十二、Optimizer 自己也有"状态"

这是一个很重要但很自然的新认识。

模型有参数:

复制代码
W
b

而 Adam 自己也会保存:

复制代码
m
v
step count
...

这些东西不是模型本身的预测参数,但训练恢复时非常重要。

因此 Checkpoint 往往不仅保存:

复制代码
model.state_dict()

还会保存:

复制代码
optimizer.state_dict()

为什么?

因为如果你训练到第:

\ 50000 \\

步突然停掉,

只加载 Model Parameter,Adam 过去积累的:

\ m_t,\\quad v_t \\

就全部丢了。

恢复训练可能和真正连续训练不完全一样。

所以以后看到:

复制代码
checkpoint = {    "model": model.state_dict(),    "optimizer": optimizer.state_dict()}

你应该知道:

一个保存模型本身,一个保存 Optimizer 的训练状态。


二十三、为什么训练 Loss 会抖动而不是一直下降?

现在我们已经能解释很多真实训练现象。

每个 Batch 不一样,所以:

\ g_t \\

不一样。

参数更新:

\ \\theta_t\\rightarrow\\theta_{t+1} \\

之后,下一个 Batch 又可能比较难。

因此 Loss 可能:

复制代码
0.80
↓
0.61
↓
0.65
↓
0.52
↓
0.55
↓
0.41

虽然不是每一步都下降,但整体趋势下降。

这通常并不奇怪。

特别是 Mini-batch Training 中,Gradient 本身就是带噪声的估计。

所以判断训练是否正常不能只看:

某一步 Loss 有没有变大。

要看一段训练过程中的总体趋势。


二十四、Learning Rate 太大时你可能实际看到什么?

可能看到:

复制代码
Loss突然剧烈震荡

或者:

复制代码
Loss持续增大

甚至:

复制代码
nan
inf

原因之一就是参数一步更新太大:

\ \\Delta\\theta \\

过大。

于是:

复制代码
当前参数还算合理
↓
一次step
↓
参数跑到很差的区域
↓
输出变得极端
↓
Loss变大
↓
梯度进一步异常

最后训练数值崩溃。

当然 nan 不一定只由 Learning Rate 导致,但它是常见排查方向之一。


二十五、Learning Rate 太小时你可能看到什么?

例如:

复制代码
Epoch 1  Loss = 0.800
Epoch 2  Loss = 0.799
Epoch 3  Loss = 0.798
Epoch 4  Loss = 0.797

整个训练非常慢。

原因可能是:

\ \\Delta\\theta = -\\eta g \\

中:

\ \\eta \\

太小。

即使 Gradient 方向正确,参数每次也只动一点点。


二十六、Learning Rate 和 Batch Size 有没有关系?

有,但现在先建立直觉,不急着上复杂经验公式。

Batch Size 越小:

Gradient 通常噪声更大。

Batch Size 越大:

Gradient 通常更稳定,更接近整个 Dataset 的平均趋势。

例如:

\ B=1 \\

每次只看一个样本。

这个样本如果很特殊,Gradient 就可能偏得比较厉害。

而:

\ B=256 \\

把很多样本平均起来,梯度往往更稳定。

但 Batch 越大:

  • 显存需求越高;
  • 每个 Step 成本更高;
  • 优化性质也会变化。

后面讲完整训练配置时再系统分析。


二十七、现在可以第一次理解 Training Configuration

以后看到训练配置:

复制代码
batch_size: 64
learning_rate: 1e-4
optimizer: adam
epochs: 100

它们不是四个互不相关的参数。

而是在描述同一件事:

复制代码
一次拿多少数据?
→ batch_size

算出梯度后基础步子多大?
→ learning_rate

用什么规则更新参数?
→ optimizer

整个Dataset反复学习多少轮?
→ epochs

这就是训练系统。


二十八、把前几课全部接起来

现在我们已经从 Robot Dataset 一直走到真正参数更新。

完整链:

\ \\boxed{ Demonstration \\rightarrow Dataset \\rightarrow Batch \\rightarrow Forward \\rightarrow Predicted\\ Action \\rightarrow Loss \\rightarrow Backward \\rightarrow Gradient \\rightarrow Optimizer \\rightarrow Parameter\\ Update } \\

Optimizer 这一段又可以展开:

\ Gradient \\rightarrow Learning\\ Rate \\rightarrow Update\\ Rule \\rightarrow \\theta_{\\text{new}} \\

如果是 SGD:

\ \\theta_{t+1} = \\theta_t-\\eta g_t \\

如果加入 Momentum:

\ \\text{Current Gradient} + \\text{Previous Direction} \\

如果是 Adam:

\ \\text{Direction History} + \\text{Gradient Scale History} + \\text{Base Learning Rate} \\

共同决定下一步更新。


第八课自测

  1. Gradient 和 Learning Rate 分别回答什么问题?

  2. 如果:

\ w=2 \\

梯度:

\ \\frac{\\partial L}{\\partial w}=5 \\

学习率:

\ \\eta=0.1 \\

按照最基础 Gradient Descent,新的 \(w\) 是多少?

  1. 为什么 Learning Rate 太大可能导致 Loss 震荡甚至发散?

  2. 为什么 Learning Rate 太小会导致训练非常慢?

  3. Mini-batch SGD 中为什么每一个 Batch 算出来的 Gradient 不一定相同?

  4. Momentum 相比最基础 SGD 多使用了什么信息?

  5. Adam 中:

\ m_t \\

和:

\ v_t \\

可以分别先怎样理解?

  1. Adam 会自动消除 Learning Rate 这个超参数吗?为什么?

  2. 为什么恢复 Adam 训练时,除了 model.state_dict(),还经常需要恢复 optimizer.state_dict()?

  3. 现在解释:

    optimizer.step()

时,为什么不能简单说成"执行梯度下降"?

因为不同 Optimizer 的具体更新规则并不相同。

相关推荐
镜子AI1 小时前
教培机构多模态内容跨模态检索系统:基于CLIP的统一表征与图文混合召回算法
人工智能·python·算法·机器学习
belldeep1 小时前
AI-3D 真人剧如何制作
人工智能·3d·ai
落魄大学生之流水线上谋生计1 小时前
Selenium 入门到实战:用 Python 写出稳定的浏览器自动化
python·selenium·自动化
会编程的吕洞宾1 小时前
Spring AI 2.0 会话记忆实战,让 AI 助手记住每一次对话
java·人工智能·spring
浩风祭月1 小时前
ChatGPT Work和Codex为什么共用额度?在哪里看剩余量
人工智能·chatgpt·plus·codex·chatgpt work·chatgpt额度·用量查询
阡陌数智1 小时前
RAG 系统的召回退化:向量库、分块、重排全链路问题排查与优化实践
开发语言·人工智能·语言模型·自然语言处理·推荐算法
嘉立创FPC苗工1 小时前
柔性电路板(FPC)补强:材料选型、工艺与设计全解
人工智能
小宋10211 小时前
Agent 工具升级如何不破坏线上:Tool Schema 版本兼容与契约测试
java·人工智能·后端·spring
莪_幻尘1 小时前
Agent 体检:乱编、连锁、失忆,给 Agent 做一次五维体检
前端·人工智能·agent