news 2026/10/27 22:36:26

C2_W2优化器作业:numpy与PyTorch复现对照及排查指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C2_W2优化器作业:numpy与PyTorch复现对照及排查指南

1. 这份作业的骨架:C2_W2 到底在测什么

C2_W2 是圈内对某门公开课专项课程"第二门课第二周编程作业"的约定式命名,作业本体是一份中英对照的 Notebook,核心任务不是训练出一个多准的模型,而是把四种优化算法从公式手写一遍,再用 PyTorch 复现一遍做交叉验证。如果你拿到的是C2_W2_Assignment_中英_Pytorch这类命名的文件,说明它已经被后来的人翻译过、并且尝试把 numpy 版本改写成了 PyTorch 版本——这中间会踩的坑,比作业本身难得多。

我第一次打开这份作业时以为它很简单:不就是 mini-batch 梯度下降、Momentum、RMSprop、Adam 四个东西吗。结果真正动手才发现,作业里塞了一堆"看起来是细节、实际决定成败"的设计——指数加权平均的偏差校正、mini-batch 尾巴的处理、学习率衰减的三种写法、1/sqrt 还是 1/(1+decay*t)。这些细节在原版里是用 numpy 手推的,每一行都能对上号;换成 PyTorch 之后,框架帮你隐式地做了其中一部分、又故意不做另一部分,你就很容易写出"跑得通但数值对不上"的代码。

这份内容适合三类人看。第一类是正在做这门作业、想搞清楚每行代码为什么这么写的同学;第二类是已经做完但想用 PyTorch 重写一遍、加深理解的从业者;第三类是根本没做过这份作业、但想系统搞明白"深度学习优化器到底怎么实现"的开发者。三类人的共同需求是一样的:要知道框架封装了什么、暴露了什么、哪些地方必须自己对齐。

提醒:作业里用到的网络非常小,隐藏层只有 [5, 2] 这种量级,很多人以为这是"教学简化",其实是为了让四种优化器在几分钟内跑出可对比的损失曲线。不要因为网络小就跳过维度推导,那才是后面所有 bug 的源头。

2. 原始作业的四个模块与它们的设计意图

2.1 模块拆解:除了优化器,还有两件容易被忽略的事

把作业拆开,实际包含四块内容,其中两块是优化器、两块是"配套基础设施",后者反而更容易出问题。

模块具体内容在教学上的作用复现难度
数据管线random_mini_batches打乱并切批训练稳定性的前提中
参数初始化He 初始化,sqrt(2/n[l-1])让梯度不在起点就爆炸低
优化器GD / Momentum / RMSprop / Adam对比收敛速度高
学习率调度分段、指数、1/(1+decay·t)观察后期精细下降中高

这块表格不是装饰,它对应着我见过的绝大多数"作业跑不通"案例。有人上来就改优化器,结果数据根本没打乱、每个 batch 都是同一批类别,曲线当然平;有人把初始化写成randn * 0.01,梯度小到 Adam 的偏差校正都救不回来。

2.2 为什么原始版本坚持用 numpy 手写

课程讲师故意不给你torch.optim,原因很朴素:优化器的公式只有手写过一遍,你才知道学习率、beta、epsilon 各自在哪个位置起作用。

举个具体例子。Momentum 的更新里有一项偏差校正:

v["dW" + str(l+1)] = beta1 * v["dW" + str(l+1)] + (1 - beta1) * grads["dW" + str(l+1)] v_corrected["dW" + str(l+1)] = v["dW" + str(l+1)] / (1 - np.power(beta1, t)) parameters["W" + str(l+1)] -= learning_rate * v_corrected["dW" + str(l+1)]

t是当前迭代次数,从 1 开始。第一轮迭代时v只有(1-beta1)*g这么大,如果不除1-beta1^t,速度会被压到原来的十分之一,前期几乎不动。这个"除以(1-beta1^t)"的动作,在 PyTorch 的torch.optim.SGD(momentum=...)里是不存在的。框架选择用不同的 momentum 定义(buffer 直接累加不乘1-beta),从而绕开了偏差问题。这就是为什么你拿 numpy 版和 torch 版对照,前十步的参数会差很多——不是谁算错了,是定义不同。

2.3 用 PyTorch 重写的真正价值

重写的价值不在于"少写几行",而在于用两套独立实现互相当裁判。我的做法是:同一个随机种子、同一份初始化权重、同一批 mini-batch 顺序,分别用 numpy 和 torch 跑 200 步,把每一步的W1展平后算欧氏距离。如果距离在 1e-10 量级,说明两边数学一致;如果第 3 步就跳到 0.1,那一定是某处的定义不一样。

这个方法比盯着损失曲线看管用得多。损失曲线是"结果",参数轨迹是"过程",过程对不上而结果看起来差不多,往往意味着两个错误互相抵消了,换个数据集立刻现原形。

3. 数据管线:mini-batch 划分是第一个必踩的坑

3.1 从 numpy 数组到 Tensor 的形状约定差异

原作业里的数据是(特征维, 样本数)的布局,这是经典的"列优先"风格:train_X.shape = (12288, 1080),train_Y.shape = (1, 1080)。而 PyTorch 的Dataset/DataLoader默认按"行优先"取样本,即(样本数, 特征维)。

这个转置看起来只是.T一下,但它引发的问题会一路蔓延到:

  • 参数初始化维度写反,W变成(n_out, n_in)和(n_in, n_out)混用;
  • 反向传播时矩阵乘法顺序错,报错是"mat1 and mat2 shapes cannot be multiplied",但真正错的地方在两小时前写的初始化里;
  • batch 切片切到了特征维上,模型还能跑,只是学了个空。

我的建议是:在数据管线入口处做一次性的、显式的转置,并在紧接着的下一行打印形状。不要依赖"我记得转过"这种记忆。

import numpy as np import torch from torch.utils.data import TensorDataset, DataLoader # 原作业布局: (n_features, m) X_np, Y_np = train_X.T, train_Y.T.squeeze() # -> (m, n_features), (m,) X = torch.from_numpy(X_np).float() Y = torch.from_numpy(Y_np).float().unsqueeze(1) print(X.shape, Y.shape) # 顺手打出来,别省这一行 dataset = TensorDataset(X, Y)

注意.float()不能省。numpy 默认 float64,PyTorch 的参数默认 float32,两边一相乘就报 dtype 不匹配;就算侥幸跑起来,精度也对不上,你根本没法做前面说的那种 1e-10 级别的比对。

3.2 打乱、切批与"最后一截尾巴"

原作业的random_mini_batches有几个关键动作,逐个看:

np.random.seed(seed) permutation = list(np.random.permutation(m)) shuffled_X = X[:, permutation] shuffled_Y = Y[:, permutation].reshape((1, m)) num_complete_minibatches = math.floor(m / mini_batch_size) for k in range(num_complete_minibatches): mini_batch_X = shuffled_X[:, k*mini_batch_size : (k+1)*mini_batch_size] ... # 处理不足一个 batch 的尾巴 if m % mini_batch_size != 0: mini_batch_X = shuffled_X[:, num_complete_minibatches*mini_batch_size : m]

三个细节值得单独拎出来说。

第一个是 X 和 Y 必须用同一个 permutation。我见过有人为了"看起来更随机",对 X 和 Y 各调一次np.random.permutation,结果图像和标签彻底错配,模型训练 100 轮准确率卡在 50% 上下——二分类嘛,瞎猜就是 50%。这个 bug 极难通过看损失曲线发现,因为损失确实在缓慢下降(模型学到了标签的分布先验)。

第二个是尾巴不能丢。作业明确保留了最后不满一批的样本。如果换成drop_last=True,在 m=1080、batch=64 的情况下会丢掉 56 个样本,占比 5%。单次影响不大,但每个 epoch 丢的都是"同一批位置"的样本(因为打乱种子固定的话,被丢的往往是排序后落在末尾的那批),长期下来相当于给数据集做了有偏采样。

第三个是种子的位置。np.random.seed(seed)应该放在函数开头、permutation之前,而不是每个 epoch 重置一次。如果每个 epoch 用同一个种子,你会得到完全相同的打乱顺序,等于没打乱。这个坑我在早期代码里踩过,症状是训练损失下降得异常平滑,好得不像话。

3.3 用 DataLoader 复刻手写采样器的等价性验证

PyTorch 的DataLoader(shuffle=True)内部用的是 torch 自己的随机数发生器,和 numpy 的 permutation 结果不可能一样。所以"数值对齐"这件事要分两步走:

第一步,先关掉随机性做等价性验证。设shuffle=False,手写一个确定性的切批函数,两边跑同样的 batch 顺序,确认单步梯度一致。

第二步,再打开随机性看统计特性。检查每个 epoch 的 batch 数量、最后一个 batch 的大小、整个 epoch 内样本是否被完整覆盖一次。

loader = DataLoader(dataset, batch_size=64, shuffle=True, drop_last=False) for epoch in range(3): seen = 0 for xb, yb in loader: seen += xb.size(0) print(epoch, seen) # 必须等于 m

如果这个数不等于 m,说明drop_last开了,或者数据集本身有问题。这行打印是我在每份复现代码里都保留的,成本几乎为零,收益是省掉半天排查。

4. 四个优化器的 PyTorch 写法与数值对照

4.1 baseline:mini-batch 梯度下降的等价写法

先用最朴素的方式跑通 baseline,不引入任何框架优化器,纯手写:

def sgd_step(params, grads, lr): with torch.no_grad(): for p, g in zip(params, grads): p -= lr * g

对应原作业里的:

for l in range(L): parameters["W" + str(l+1)] -= learning_rate * grads["dW" + str(l+1)] parameters["b" + str(l+1)] -= learning_rate * grads["db" + str(l+1)]

这两段是完全等价的,前提是grads已经由loss.backward()填好。要注意loss.backward()是累加梯度而不是覆盖,所以每个 batch 开头必须optimizer.zero_grad()或手动p.grad = None。手写版本里如果忘了清空,梯度会跨 batch 累积,效果相当于学习率随步数线性增长——前几步看着正常,几十步之后直接发散。这是新手最常见的发散原因之一,而且报错信息完全不会提示你"梯度没清零"。

4.2 Momentum:框架替你做了什么,又漏了什么

这是整份作业里最值得对照的一处。原作业的 Momentum 有偏差校正,PyTorch 的没有,两边定义不同。

项目原作业(numpy)torch.optim.SGD(momentum=β)
速度更新v = βv + (1-β)gbuf = β·buf + g
偏差校正v/(1-β^t)无
参数更新p -= lr·v_correctedp -= lr·buf
β=0.9 时前期步长约lr·g(校正后)约lr·g/(1-β)≈10·lr·g

看最后一行:同样的lr,torch 的 momentum 版本前期实际步长是 numpy 版的约 10 倍。很多人把 numpy 代码直接搬成torch.optim.SGD(params, lr=0.01, momentum=0.9),然后发现损失直接飞掉,第一反应是"学习率太大",于是调到 0.001 能跑了——但这只是把两套不同定义的参数硬凑到一起,换台机器、换个数据就又不灵了。

正确的对齐方式有两种,选一种并写进注释:

  • 方案 A:把lr除以(1-β),即lr_torch = lr_numpy / (1 - beta1),让前期步长一致;
  • 方案 B:自己实现带偏差校正的 momentum,不调用torch.optim。

我一般用方案 B 做教学对照,方案 A 做工程落地。方案 A 的问题在于后期两者还是会分叉,因为偏差校正的影响随 t 增大指数衰减,前期补齐了、后期又偏了。真要严格一致,只能自己写。

4.3 RMSprop 的 epsilon 位置:分母里还是分母外

RMSprop 的公式看着简单,epsilon放哪儿却是个真问题。

原作业写法:

s["dW"] = beta2 * s["dW"] + (1 - beta2) * np.square(grads["dW"]) s_corrected = s["dW"] / (1 - np.power(beta2, t)) parameters["W"] -= learning_rate * grads["dW"] / (np.sqrt(s_corrected) + epsilon)

注意分母是sqrt(s_corrected) + epsilon,即 epsilon 在根号外。PyTorch 的实现同样是sqrt(square_avg) + eps,位置一致,但默认eps=1e-8且没有偏差校正。原作业用epsilon=1e-8,两边默认值刚好对上,这点比 Momentum 幸运。

还有一个隐蔽差异:PyTorch 的 RMSprop 里有alpha参数对应原作业的beta2(注意不是 beta1),默认 0.99,而作业里常用的 RMSprop beta 是 0.999。这两个值差一个数量级,带来的效果差异在前期很明显——0.99 的记忆更短、响应更快,0.999 更平滑但起步慢。抄参数的时候一定要逐个核对,别只看变量名。

4.4 Adam 的两个 beta 与 t 的起始值

Adam 是四个里面最容易"看起来对、实际错"的,因为它有两个偏差校正,而且t的起始值直接决定第一步的步长。

v = beta1 * v + (1 - beta1) * g v_corrected = v / (1 - beta1 ** t) s = beta2 * s + (1 - beta2) * (g ** 2) s_corrected = s / (1 - beta2 ** t) p -= lr * v_corrected / (torch.sqrt(s_corrected) + eps)

t从 1 开始,第一步1-beta1^1 = 0.1,v被放大 10 倍;1-beta2^1 = 0.001,s被放大 1000 倍,开根号后 31.6 倍。分子分母同时被放大,比值大致回到 1 附近,所以 Adam 的第一步步长约等于lr。如果你把t从 0 开始、beta**0 = 1,分母变成 0,直接除零报错——或者在某些实现里被静默处理成 NaN,训练三分钟后损失变 NaN,而你还在找数据的问题。

PyTorch 的torch.optim.Adam内部维护step计数器,行为与原作业一致,betas=(0.9, 0.999)、eps=1e-8也都对得上。唯一需要注意的是:如果你手动改过p.grad或者多次调用step(),计数器会往前走,此时偏差校正的幅度和你以为的不一样。做严格对照实验时,我宁愿自己写优化器循环,可控性更高。

5. 学习率衰减的三条路线与实测差异

5.1 作业里的两个公式:1/(1+decay·t) 与 0.95^t

原作业要求实现:

def update_lr(learning_rate0, epoch_num, decay_rate): return 1 / (1 + decay_rate * epoch_num)

以及练习题里提到的指数版learning_rate0 * 0.95 ** epoch_num。

这两条曲线的形状完全不同,而且差别在后期才显现:

  • 1/(1+decay·t)是双曲衰减,衰减速度越来越慢,但永不为零。decay=1 时,第 1 轮降一半、第 10 轮降到 1/11、第 100 轮降到 1/101。
  • 0.95^t是指数衰减,每轮固定乘 0.95,前期掉得慢、后期掉得"肉眼可见地快",第 50 轮后只剩 7.7%。

我在同一份数据上跑了 50 个 epoch 对比,结论是:中小规模数据集上,双曲衰减更容易在后期继续榨出一点精度,因为它始终保留了一个非零的学习率;指数衰减在后期几乎冻结,适合"早停"策略配合使用。这个结论不绝对,但至少说明不能随手选一个公式了事。

5.2 用 lr_scheduler 复刻作业里的公式

PyTorch 提供了LambdaLR,可以把任意公式塞进去:

import torch.optim.lr_scheduler as lr_scheduler optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9) # 复刻 1/(1+decay*t),注意 scheduler 的 step 从 0 开始 lam = lambda epoch: 1.0 / (1.0 + 1.0 * epoch) scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lam)

这里有个非常容易搞错的地方:LambdaLR传给lr_lambda的epoch从 0 开始,而原作业的epoch_num从 1 开始(因为公式里1 + decay*epoch_num在 epoch_num=0 时等于 1,即不衰减)。如果直接套,你的第一轮就会开始衰减,学习率是 0.005 而不是 0.01。

修正方式有两种:lam = lambda e: 1.0 / (1.0 + decay * (e + 1)),或者接受从 0 开始、把公式理解成"第 0 轮不衰减"。两种都行,但要在注释里写清楚,否则半年后回来读代码的你会骂人。

另外,scheduler.step()的调用位置也影响结果。PyTorch 1.1 之后推荐把scheduler.step()放在 epoch 循环里、所有 batch 训练完之后,而不是每个 batch 之后。放错位置的直接后果是:50 个 epoch、每 epoch 17 个 batch,学习率被衰减了 850 次而不是 50 次,学习率瞬间归零,训练直接卡死。

5.3 衰减过猛的症状与排查

衰减出问题的表现很有辨识度:训练损失在前期正常下降,某个点之后变成一条几乎水平的线,但还没有完全平。

和"模型容量不足"的区别在于:容量不足的平线是全程平滑的、下降速率均匀;衰减过猛的平线是突然"折"一下,前后斜率明显不同。我通常这样确认:

lrs = [] for epoch in range(50): train_one_epoch() lrs.append(optimizer.param_groups[0]["lr"]) scheduler.step() print(lrs[:5], lrs[-5:])

如果最后几个 epoch 的 lr 已经小到 1e-6 量级,而损失还在 0.3 以上,那就是衰太狠了。这时候要么把decay_rate调小,要么改用双曲衰减。

注意:不要用"最后几轮损失下降了多少"来判断学习率是否合适,因为曲线本身就在变缓。要看的是"损失曲线在 lr 骤降的位置有没有出现明显的斜率断点",这需要把 lr 曲线和 loss 曲线画在同一张图上对照。

6. 中英对照版整理时踩过的三个软坑

6.1 术语翻译分歧:同一个词在不同教材里是两个东西

整理双语版时最耗时的不是翻译正文,是统一术语。举几个我实际改过的:

英文常见译法 A常见译法 B我采用的理由
exponentially weighted average指数加权平均指数加权移动平均指数加权平均短,且"移动"在中文语境里容易和滑窗混淆
bias correction偏差修正偏差校正偏差校正与"偏差-方差"里的"偏差"区分度更好
mini-batch小批量微批次小批量代码注释里长度更短,对齐好看
learning rate decay学习率衰减学习率退火学习率衰减"退火"在优化领域另有专门含义

术语不统一的代价不是"读者看不懂",而是你自己在写注释时会前后矛盾。我一开始列了两栏对照,中途忘了,结果同一个 notebook 里bias_correction的注释一会儿写"偏差修正"一会儿写"偏差校正",后来做全文搜索替换才修掉。

建议做法:在文件最开头放一张术语表,用 Markdown 表格写死;写代码注释时只查表,不凭记忆。

6.2 公式与代码注释的同步维护

双语版里公式一般用 LaTeX 写,代码注释里又要重复一遍关键式子,这两处极易脱节。

我的做法是:公式只在 Markdown 单元里写一次,代码注释只引用编号。

# 对应公式 (3): v = beta1 * v + (1 - beta1) * grad # v_hat = v / (1 - beta1 ** t)

这样即使后面调整了符号(比如把beta1改成beta),只需要改公式区和这一行注释,不会出现"注释里写(1-β)、代码里写(1-β1)"的对不上。

另一个实操建议是:把每个优化器的实现拆成独立的 cell,每个 cell 开头一段双语说明、中间代码、结尾一段双语"预期输出"。这样导出成 HTML 或 PDF 时,读者不需要跳到别处找解释。

6.3 文件命名与版本管理

C2_W2_Assignment_中英_Pytorch这种命名的问题在于:里面混了课程编号、语言、框架三个维度,后面一旦要改框架或者加语种,文件名就不够用了。

我现在的习惯是拆开:

c2_w2/ ├── README.md # 术语表 + 环境 + 结论摘要 ├── c2_w2_numpy_ref.ipynb # 原始实现,作为对照基准 ├── c2_w2_torch.ipynb # PyTorch 复现 └── results/ ├── loss_curves.png └── param_diff.csv # 每步参数差的记录

把"对照基准"单独存一份,是这套结构里最重要的一条。因为你在改 torch 版的时候,难免会想"是不是 numpy 版写错了",这时候直接跑参考实现,两分钟就能排除怀疑,不用靠脑子回忆。

7. 一份可复用的排查清单

7.1 数值对不上时的五个高频原因

按我实际遇到的频率排序:

序号症状常见原因快速验证
1第 1 步就对不上初始化不同(种子、缩放系数)打印初始W1[0,:5]两边比对
2前 10 步对不上、之后接近Momentum 定义差异(有无偏差校正)关掉 momentum,纯 SGD 对比
3损失为 NaNAdam 的t从 0 开始,或除以了 0检查beta**t的 t 初值
4损失震荡剧烈batch 没打乱,或梯度没清零打印前 3 个 batch 的标签分布
5精度卡在 50%X 与 Y 用了不同的 permutation单 batch 内检查 x-y 是否配对

第 5 条我单独说明一下怎么验证。取一个 batch,把xb送进模型前向、拿到 logits,然后看xb[i]对应的标签是不是yb[i]。更土但更快的办法是:torch.equal(Y[perm], Y_shuffled)这种检查在打乱逻辑写错时会直接暴露。

7.2 收敛行为的可视化与断言

光看图不够,最好把"应该满足的性质"写成断言,跑一次就过一遍。

# 断言 1: 损失整体单调性(允许小波动) losses = train_history["loss"] violations = sum(1 for i in range(1, len(losses)) if losses[i] > losses[i-1] * 1.2) assert violations < len(losses) * 0.1, f"损失波动过大: {violations} 次" # 断言 2: 梯度范数不应在正常区间外 gn = torch.sqrt(sum((p.grad ** 2).sum() for p in model.parameters())) assert 1e-8 < gn.item() < 1e3, f"梯度范数异常: {gn.item()}" # 断言 3: 参数确实在变 w_before = model[0].weight.detach().clone() train_one_epoch() assert not torch.allclose(w_before, model[0].weight.detach()), "参数没有更新"

这三条断言覆盖了我在实操中见过的绝大多数"静默失败":模型在训练但什么都没学到、梯度爆炸但被 clip 掩盖、优化器忘记绑定参数。写这三行的时间是五分钟,能省掉的时间是半天起。

7.3 我个人的几条经验

第一条,先跑通 numpy 版再碰 torch 版。听起来反直觉——既然要用框架,为什么不直接用?因为 numpy 版的每一步都摆在明面上,你可以打印任意中间量;torch 版的autograd把反向传播藏起来了,出问题时你只能猜。用 numpy 版建立"正确结果长什么样"的直觉,再拿 torch 版去逼近,这个顺序不能反。

第二条,对照实验时把随机种子固定到每个可能的地方:np.random.seed、torch.manual_seed、torch.cuda.manual_seed_all、DataLoader(worker_init_fn=...)。我吃过一次亏:单进程跑得好好的,加到 4 个 worker 之后每次结果都不一样,查了一下午才发现是 worker 的随机状态没同步。

第三条,关于那份中英对照版,别追求一次翻译到位。先机翻一遍跑通代码,再逐段改术语,最后统一格式。反过来做(先精翻再跑)的话,代码报错时你会怀疑是翻译改坏了逻辑,排查成本翻倍。

第四条,学习率相关的结论,永远在你自己的数据上验证一遍再信。作业里给的0.0007是配合那个 12288→5→2→1 的小网络调出来的,换到别的网络上大概率不适用。我在另一份 128 维输入的任务上试过,同样的 0.0007 让模型 20 个 epoch 都没动地方,调到 0.01 才正常。

最后分享一个我最近才养成的小习惯:在 notebook 最末尾留一个"随手记录"的 cell,专门存那些"跑出来了但还没想明白为什么"的现象,比如"Adam 在 epoch 15 附近有个 loss 小凸起,换 seed 后消失"。这些东西写进正文太重,丢掉又可惜,放在末尾当工作日志,过一阵回头看往往能串起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 22:35:03

Docker挂载目录文件消失问题解析与解决方案

1. 问题现象&#xff1a;Docker挂载目录下的文件为何"消失"&#xff1f; 第一次遇到这个问题的场景至今记忆犹新。那是个周五的深夜&#xff0c;我正在用Docker部署一个Node.js应用&#xff0c;按照常规操作把本地开发目录挂载到容器里&#xff1a; docker run -v …

作者头像 李华
网站建设 2026/10/27 22:34:23

AI芯片脉动阵列原理与实操:从矩阵乘法到FPGA实现

1. 为什么AI芯片都在聊脉动阵列第一次听到“脉动阵列”这个词&#xff0c;很多人会以为是什么生物医学概念&#xff0c;其实它是AI芯片里一个非常经典的硬件架构思路。简单说&#xff0c;脉动阵列就是让数据像心跳一样有节奏地在计算单元之间流动&#xff0c;每个单元只做简单的…

作者头像 李华
网站建设 2026/10/27 22:33:41

大模型强化学习中的KL散度惩罚:原理、调优与避坑指南

1. 从KL散度惩罚说起&#xff1a;为什么大模型强化学习绕不开这道坎搞大语言模型强化学习的人&#xff0c;迟早会撞上KL散度惩罚这个玩意儿。我第一次在PPO训练脚本里看到kl_coef这个参数的时候&#xff0c;心里想的是“不就是个正则项嘛&#xff0c;调小一点让模型放开跑不就行…

作者头像 李华
网站建设 2026/10/27 22:33:27

信奥赛P4970题解:贪心算法在字符串处理中的应用

1. 项目概述&#xff1a;信奥刷题与P4970题目解析最近在准备信奥赛的同学应该都深有体会&#xff0c;刷题是提升算法能力的必经之路。今天我要分享的是洛谷P4970"全村最好的嘤嘤刀"这道题的C实现过程。这道题看似简单&#xff0c;但涉及了字符串处理、贪心算法等多个…

作者头像 李华
网站建设 2026/10/27 22:33:16

pygame农场游戏浇水系统:状态机、蓄力与每日结算

1. 浇水系统在农场游戏里到底承担什么角色做到第 16 篇,轮到浇水了。前面几篇把地图、角色移动、工具切换、背包和时间系统都铺完了,很多人做到这一步会下意识觉得浇水就是个小事:按下鼠标、把格子状态改成"湿的"、播个水花动画,收工。真上手写才知道,这个模块是整条…

作者头像 李华