小菜鸟

java菜鸟号正在起航

为什么你的 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)

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

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

阅读全文 »

下一个词最可能是什么:手写一个 Bigram 语言模型

你每天都在用语言模型

你在 IDE 里敲下 res 三个字母,补全列表已经弹出 response_body、response_time 两个候选。你按下 Tab,整行补完。这个动作背后没有魔法,编辑器只做了一件事:估算在当前上下文之后,下一个词最可能是什么。把这件事做成一个可以训练、可以查询的模型,就是语言模型的全部工作。

本文不从公式出发。我们从一份只有两句话、六个词的语料库开始,亲手把大模型的统计学祖先造一遍:数次数、做除法、填一张表。走完这一程,你会清楚三件事:语言模型算的到底是哪个概率;为什么数次数这种朴素操作是正经的统计估计而不是经验口诀;以及这张表天生带什么缺陷,逼出了后续所有的平滑技巧乃至神经网络。

阅读门槛只有会写循环和会查表。文中会出现公式,但每个公式都先落在代码或直觉上,再出现符号。

把预测写成一张表

把预测下一个词写成程序需求:输入已经出现的词,输出下一个词的概率分布。最诚实的实现是一张哈希表,键是完整前文,值是各后继词的计数。麻烦在于键的空间随前文长度指数膨胀,真实语料填不满它的零头,绝大多数查询永远 miss。一张几乎查不到的表,等于没有表。

统计语言模型的出路是主动丢信息:不看完整前文,只看最近一个词。我们把用来做依据的前一个词叫前驱词,把要预测的那个词叫后继词。只靠前驱词预测后继词的模型就是 Bigram,键缩短成一个词,表的大小降到词汇量级别,一次遍历语料就能填完。代价是模型变健忘,决定后继词的只有紧邻的前驱词,更远的上下文一律无视。本文全程使用 Bigram,因为它小到可以手算,又完整保留了这套方法的全部优点与全部缺陷。

截断历史之后,句子概率可以拆成连乘:整句概率等于逐词条件概率的乘积,每个条件概率只询问前一个词。下一节我们就把这套连乘套到一个六词语料上,亲手算一遍。

手算一遍:六词语料上的 Bigram

理论说完,直接上手。假设语料库只有两句话:

datawhale agent learns
datawhale agent works

现在要计算新句子 datawhale agent learns 出现的概率。按 Bigram 假设,它拆成三个因子相乘:

阅读全文 »

从调参玄学到概率建模:大模型时代的贝叶斯生存指南

调参靠经验?不如试试概率思维。

为什么加了L2正则化就不容易过拟合?为什么微调大语言模型时,学习率通常要比预训练小一个数量级?为什么同样的模型结构,换一批数据训练出来的性能波动很大?

很多工程师能熟练运用这些经验法则,却很难说清它们背后是否藏着统一的逻辑。

事实上,这些看似分散的工程技巧,都可以被同一个概率框架优雅地解释。机器学习真正要处理的,从来都是不确定性——而非某个虚幻的“正确”参数值。

这种视角在当下显得尤为迫切。一个拥有百亿参数的模型,仅用几千条高质量数据微调后,居然没有陷入严重的过拟合。

若按频率学派的经典理论,这几乎不可能发生;但在贝叶斯视角下,这恰恰是先验信息与似然达成新平衡的自然结果。理解这一点,你就不再只是调参的执行者,而是能看透模型行为的设计者。

本文将从你最熟悉的工程实践出发,按需引入基础概率概念,逐步拆解贝叶斯定理如何统一解释正则化、早停与微调现象。

本文不堆砌公式推导,只聚焦于“这个概念对应代码里的哪一行”“这个直觉如何指导你的下一次实验”。文章最后,也会坦诚讨论贝叶斯思维在大模型时代的局限与前沿。

从点估计到分布:为什么模型会记住噪声

以线性回归过拟合为例,训练误差持续下降但验证误差反弹的经典曲线,本质上揭示了模型正在“记住”噪声而非学习规律。

这里的关键词是「方差」。不谈数学定义,只把它理解为模型输出对输入扰动的敏感度。高频震荡的过拟合曲线,反映了模型对训练样本扰动的高度敏感,这与参数估计方差过大是同一现象的两个侧面。

当我们说“参数估计的方差”时,其实已隐含地将参数看作随机变量,其取值依赖于训练样本。而似然函数则刻画了“给定参数时数据的生成机制”。

阅读全文 »
0%