小菜鸟

java菜鸟号正在起航

当大模型“忘记”中间内容时,我们在失去什么

你一定遇到过这样的场景:把一份几十页的技术文档喂给大模型,开头和结尾它都能准确复述,但中间某个关键接口的参数定义却开始胡言乱语。即便上下文窗口扩到百万Token,“Lost in the Middle”依然顽固存在。

这并非单一机制所致。自回归架构的因果掩码偏置、训练中形成的U型位置注意力偏差、Attention Sink对首Token的过度依赖——三者叠加,导致中间区域的语义依赖在推断中被系统性弱化。概率图模型的独特价值,不在于解释这些偏置的成因,而在于它提供了一套结构化损失函数的设计语言,将分散的架构缺陷转化为统一的可优化目标。没有这套语言,修复手段只是孤立trick;有了它,我们才能系统推导、验证并组合这些手段。

本文用PGM的工程透镜重新审视大模型:长上下文依赖为何瓦解?推理路径如何诊断?更重要的是,这套设计语言能带来哪些有明确实现路径但需权衡代价的启示,以及哪些尚未成熟的研究方向。如果你也曾对着Attention Map皱眉,怀疑模型“看到了所有Token却没理解它们之间的关系”,这篇文章就是为你写的。

词嵌入:让动态图成为可能的工程使能器

传统PGM在语言任务前撞上了一堵墙:高维离散空间的相似度计算不可行。“汽车”和“轿车”在图中是两个完全独立的节点,要让模型知道它们语义相近,只能手动加边或依赖共现统计。词汇表扩大到十万级时,这种方式立刻崩溃——稀疏性让绝大多数节点对无连接,未见过的词组合概率归零。这正是关联规则时代的遗留病灶。

词嵌入的真正价值,在于它重构了图节点的语义空间。当每个词被映射到低维连续向量后,相似度通过向量距离隐式表达,不再依赖显式边或共现计数。即使“汽车”和“轿车”从未同现,只要上下文分布相似,向量就会自动靠近。图结构中的“潜在依赖”得以被数据驱动地推断出来。

阅读全文 »

为什么你的Loss函数不叫“KL散度”?PyTorch分类损失实战指南

训练分类模型时,我们几乎总是用nn.CrossEntropyLoss()。但翻开信息论教材,衡量分布差异的正统指标是KL散度。既然KL散度才是分布距离的度量,为什么框架的分类损失都叫交叉熵?

真正用到KL散度时(比如知识蒸馏或标签平滑),nn.KLDivLoss的参数设计又容易让人踩坑:input要不要取log?target是什么格式?reduction怎么选?

本文解决三个工程问题:为什么优化交叉熵等于优化KL散度;PyTorch分类Loss如何按任务选型;KL散度在蒸馏和标签平滑中的正确用法。

一、交叉熵与KL散度的优化等价性

测量误差类比

用一把有固定偏差的尺子测零件长度,读数包含零件真实长度(固定)和系统误差(待消除)。交叉熵是带基准的读数,KL散度是扣除基准后的净误差。真实分布P由数据集决定,其熵H(P)是常数,最小化交叉熵即最小化KL散度。

公式表达为 H(P,Q) = H(P) + D_KL(P||Q),H(P)为常数项,优化方向一致。

PyTorch参数映射:F.kl_div(input=log_Q, target=P) 计算 D_KL(P||Q)。input为模型预测的对数概率,target为真实分布的概率值。若target本身已是log-prob(如教师返回log_softmax),应改用 F.kl_div(log_Q, log_P.exp()) 或手写 (P * (log_P - log_Q)).sum(),避免对log-prob重复取exp引入数值误差。

定义式验证

阅读全文 »

为什么你的 PyTorch 代码“能跑”却“不对”

你可能写过这样的代码:前向传播一切正常,loss 也在下降,但某个关键参数始终不更新;或者 loss.backward() 突然抛出 RuntimeError,提示你“trying to backward through the graph a second time”;又或者训练几个 epoch 后显存悄悄涨满,而你明明已经用了 no_grad。

这些问题的根源,往往不在模型结构或数据加载器里,而在你对计算图的心智模型上。PyTorch 的动态图机制给了你极大的灵活性,但也把调试的责任完全交给了你。本文不讲拓扑排序的数学证明,也不带你从零实现自动微分引擎,我们只聊一件事:作为框架使用者,如何建立正确的计算图直觉,并在报错时快速定位到图结构层面的问题。

张量不只是数组:它携带了整张计算图的线索

在 PyTorch 中,张量从来不是孤立的数据块。当你执行 y = x * 2 + 1 时,y 不仅存储了数值结果,还悄悄记住了一件事:“我是由 MulBackward0 和 AddBackward0 两个操作生成的,我的父节点是 x*2 的结果和常量 1。”

下面这段代码展示了这种“记忆”是如何被编码的:

import torch

x = torch.tensor([2.0], requires_grad=True)
w = torch.tensor([3.0], requires_grad=True)
b = torch.tensor([1.0])  # 注意:b 没有 requires_grad

y = w * x + b
loss = y.sum()

print(f"loss.grad_fn: {loss.grad_fn}")        # SumBackward0
print(f"y.grad_fn: {y.grad_fn}")              # AddBackward0
print(f"x.is_leaf: {x.is_leaf}, x.grad_fn: {x.grad_fn}")  # True, None
print(f"b.requires_grad: {b.requires_grad}")  # False

这里有三个关键观察点。首先,grad_fn 是图的边,每个非叶子张量的 grad_fn 指向生成它的算子,该算子又持有对输入张量的引用,这就是反向传播的路径。其次,叶子节点是图的起点,只有 requires_grad=True 且由用户直接创建的张量才是叶子节点,它们的 grad_fn 为 None,梯度最终累积到 .grad 属性上。最后,不参与梯度的张量不保存激活值。b 虽参与前向计算,但因 requires_grad=False,PyTorch 不会为其创建 grad_fn,更重要的是,不会为计算它的梯度而保存上游中间激活值。例如一个冻结的 Linear 层,其输入激活值无需保存用于权重梯度计算;若偏置也被冻结,则输入激活值完全不被保留。这才是冻结参数显著省显存的根本原因,grad_fn 元数据本身的开销其实微不足道。

阅读全文 »

当梯度变成玄学

你大概率经历过这样的时刻:模型loss突然炸成NaN,或者训练了三天发现权重纹丝不动。你检查了数据、调小了学习率、换了初始化方法,最后在一篇论坛帖子里看到有人说“试试把某个算子换成数值稳定的版本”,问题莫名其妙地解决了。

你松了一口气,但心里清楚:自己并没有真正理解为什么。

这种无力感的根源,是我们把 .backward() 当成了一个黑盒。深度学习框架把微积分封装得太过优雅,以至于我们忘记了——计算机其实不懂极限,不懂链式法则,更不懂什么偏导数。它只会做加减乘除和内存读写。当我们写下 loss.backward() 时,机器执行的并不是数学意义上的“求导”,而是一套精心设计的工程流程。

这套流程不是凭空出现的。在它成为工业标准之前,人们尝试过人肉推导公式、用差分逼近斜率、用符号系统代数求导。每一种实践的局限,都精确地指向了下一个突破的方向。

这篇文章不会给你更多调试技巧。它会带你重走这条从“笨办法”到“自动微分”的演进之路。当你理解了机器求导的真实面貌,那些曾经神秘的梯度异常,将不再是玄学,而是可追溯、可解释的工程现象。

起点与困境:从人肉实践到机器算法

在 .backward() 成为默认选项之前,实践者面对三种选择。第一种是人肉推导,也就是手动微分;后两种是真正的计算机求导算法:数值微分和符号微分。人肉推导是自动微分出现之前的默认实践,后两者是算法尝试。它们各自的局限共同指向了自动微分的必要性。

手动微分:默认实践的不可扩展天花板

# 两层MLP的前向与手写梯度
h = torch.relu(x @ W1 + b1)
y = h @ W2 + b2
loss = ((y - target) ** 2).mean()

# 手动推导的梯度(仅展示W1部分)
dy = 2 * (y - target) / y.size(0)
dh = dy @ W2.T
dW1 = x.T @ (dh * (h > 0))  # ReLU导数硬编码,漏掉就静默错误

严格来说,这不是“让计算机求导”,而是人完成全部数学推导后,机器仅负责执行写好的梯度代码。它是没有算法时的默认状态——当参数只有几个时完全可行,但当模型膨胀到数十亿参数、网络结构频繁迭代时,其不可扩展的天花板便暴露无遗。任何结构调整都意味着重推整条链式法则,工程上已不可持续。

阅读全文 »

为什么说线性模型是深度学习的基石?从 Scikit-Learn 谈起

很多刚接触深度学习的人,容易陷入一个误区。大家往往一上来就去研究复杂的网络结构,觉得基础的线性模型太简单,甚至有点过时。但如果你去拆解那些庞大的网络,你会发现,它们最后输出结果的那一层,往往还是最基础的线性模型。不理解这些基础,就很难真正看懂深度学习。这篇文章我们就从大家最熟悉的 Scikit-Learn 库开始,把线性模型这条线彻底理顺。

预测连续值,从房价预测说起

假设我们要预测房价。影响房价的因素有很多,比如面积、房间数、到地铁的距离。线性模型的做法非常直白,给每个因素分配一个权重,然后相乘相加,最后加上一个基础底价。这就是线性回归的全部秘密。

理论上,只要特征和权重没有限制,线性回归的输出范围就可以是从负无穷到正无穷。Scikit-Learn 实现这个过程只需要几行代码。假设数据已经划分成训练集和测试集。

from sklearn.linear_model import LinearRegression

# 假设 X 是房屋特征,y 是房价
model = LinearRegression()
model.fit(X_train, y_train)
predictions = model.predict(X_test)

这看起来很简单,但在实际工程中,我们很容易遇到一个大坑,那就是过拟合。如果你的数据里不仅有面积,还有“前房主养的宠物数量”这种毫无关联的特征,模型为了在训练集上拿高分,可能会强行给宠物特征分配一个权重。这导致模型“死记硬背”了训练数据,在预测新房时一塌糊涂。

为了解决这个问题,我们需要给模型戴上紧箍咒,也就是正则化。常见的紧箍咒有两种。

阅读全文 »
0%