小菜鸟

java菜鸟号正在起航

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

你每天都在用语言模型

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

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

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

把预测写成一张表

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

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

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

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

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

datawhale agent learns
datawhale agent works

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

阅读全文 »

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

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

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

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

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

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

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

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

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

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

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

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

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

阅读全文 »

AI工程师的线性代数直觉指南:从Shape报错到梯度手推

你是否也经历过这样的时刻:PyTorch突然抛出shape mismatch报错,你盯着tensor维度看了五分钟却想不起对应的线代运算;读论文时遇到满屏迹和转置的求导公式只能跳过;模型梯度莫名爆炸或消失,调了一堆参数却没想过问题藏在权重矩阵的奇异值里。

这不是你不够努力,而是教科书里的线性代数和AI工程实践之间,隔着一层没被捅破的窗户纸。

先看一段代码:

import torch
x = torch.randn(64, 128)
W = torch.randn(128, 256)
y = x @ W
grad_W = x.T @ torch.ones_like(y) / 64

如果你能清晰说出为什么x.T在左边、为什么除以64、这个梯度公式和链式法则的关系,这篇文章你可以跳过。如果还不能,比起背诵行列式展开式,重建一套属于AI工程师的线代直觉更能帮你读懂论文、调通模型。

本文将沿着神经网络从初始化到训练再到分析的全生命周期,重新认识六个核心概念。每个概念都绑定你实际会遇到的代码行为或调试场景,读完之后,希望你能把每一行涉及矩阵的代码都看成有几何意义的空间变换,而不是冰冷的数字表格。

初始化阶段:谱范数是起点,而非终点

单位矩阵在AI中代表梯度流的无损通道,不过Xavier/He初始化追求的并非接近单位阵。随机矩阵即使经过正确缩放,也远非单位阵的近似。

初始化的真实目标是让权重矩阵的最大奇异值(谱范数)接近1,保证每层变换对信号幅度的缩放接近恒等,避免梯度在连乘中指数级放大或衰减。单位阵只是这一目标的理想特例,而非逼近对象。

谱范数≈1只是梯度稳定的起点。ReLU的门控效应和训练中的权重漂移,都会让实际梯度尺度偏离理论预期。因此,初始化提供的是优化的合理初态,真正的稳定性要靠训练中持续监控谱范数来保障。

代码验证:谱范数对梯度流的影响(线性链理想模型)

import torch

def test_gradient_flow(scale):
    x = torch.randn(1, 64, requires_grad=True)
    h = x
    for _ in range(5):
        # 使用正交基确保谱范数精确等于scale
        # torch.randn的谱范数期望≈√64+√64≈16,不适合此演示
        W = torch.nn.init.orthogonal_(torch.empty(64, 64)) * scale
        h = h @ W
    loss = h.sum()
    loss.backward()
    return x.grad.norm().item(), scale

for s in [0.5, 1.0, 2.0]:
    grad_norm, spec_norm = test_gradient_flow(s)
    print(f"缩放={s}: 谱范数={spec_norm:.2f}, 梯度范数={grad_norm:.2e}")

💡 这个线性链演示清晰展示了谱范数的作用机制,但实际含ReLU的网络中,梯度还受激活门控和训练漂移影响。遇到梯度异常时,直接计算谱范数torch.linalg.svdvals(W)[0],若显著偏离1则调整初始化缩放因子。注意实际网络中各层fan_in/fan_out不同,Xavier/He正是据此为每层计算不同的方差目标,而非全局统一缩放。打印W @ W.T检验正交性仅适用于正交初始化,对Xavier/He会产生大量假阴性。

逆矩阵:避免显式求逆,善用可逆性思想

高维病态矩阵求逆数值不稳定,O(n³)计算昂贵,更何况我们通常根本不需要精确逆。

比起纠结如何求逆,更重要的是区分三个层次:当确实需要解线性系统Ax=b且A对称正定时,用Cholesky分解等稳定求解器;更多时候用LU、QR等通用分解处理非对称或非正定系统;而最常用的,是利用可逆性思想设计架构——残差连接y=x+F(x)在F雅可比小时天然可逆,BatchNorm通过可逆仿射变换消除协变量偏移,Flow模型直接构建可逆变换链。这些设计保障了信息不丢失,无需任何矩阵求逆操作。

阅读全文 »

稳与简:好模型的两个隐性标尺

我们夸一个医生靠谱,不光看他诊断准不准。还得看两样:碰到说不清症状的病人,他会不会慌?开检查单时,是不是总想多开几项才安心?前者是稳,后者是简。换到机器学习里,这两个词有个更技术的名字:鲁棒学习和稀疏学习。

不过它们管的事儿不太一样。一个管数据乱不乱,一个管模型杂不杂。鲁棒性操心的是输入被扰动了、标签标错了、测试集和训练集长得不像了,模型还能不能扛住。稀疏性则盯着特征那么多哪些是真有用的,参数那么杂能不能只留关键的。目标不同,但都是为了让模型别光会做题,还得做得踏实、说得明白。

鲁棒学习:让模型在混乱中站稳

模型一上真实数据就崩,多半是因为训练时没见过这种“意外”。对付这类问题,路子不少。对抗训练是最常见的,专门防输入被人动手脚;分布鲁棒优化(DRO)则更宽泛些,不管是自然偏移还是某个子群体表现差,它都试着在最坏的分布下保住性能;认证防御走另一条路,直接给出数学保证,说“只要扰动不超过这个半径,结果一定没错”。

拿对抗训练来说,它的完整写法是 minθ E(x,y)∼D [max‖δ‖_p≤ε L(fθ(x+δ), y)]。简单讲就是:一边调参数θ让损失变小,一边允许扰动δ在ℓ_p范数限制的ε球里拼命把损失搞大。这个ε球其实就是扰动的天花板,后面聊“ε设太大导致干净样本变差”时,说的就是这个上限没卡好。这么做下来,模型的决策边界会被磨得圆滑些,小打小闹就不容易把它带沟里去。

稀疏学习:在高维中抓住关键

特征比样本还多的时候,模型特别容易把噪声当规律记下来。这时候就得靠稀疏学习帮它做减法。方法挺多,L1正则化、贪心选择、结构化稀疏都算主流。

L1为什么能选出零权重?关键在次梯度。‖w‖₁在原点不可导,它的次梯度是个区间[-λ, λ]。如果损失函数对某个权重的偏导绝对值没超过λ,那这个权重就可以稳稳停在零上。很多人喜欢用菱形和等高线相切来解释,几何直觉有用,但别当成因果,真正起作用的是次梯度条件。

阅读全文 »

从 print(‘hello’) 开始:用 dis 验证你的 Python 直觉

你在终端敲下 python -m dis "print('hello')",看到几行 LOAD_GLOBAL、PUSH_NULL、CALL 之类的输出,大概会点头:“哦,原来 print 是这样执行的。”然后呢?

这串字节码真正的价值,不在于告诉你 print 怎么跑,而在于它提供了一把可重复验证的尺子。比如你可以立刻追问:如果把 print(‘hello’) 换成 sys.stdout.write(‘hello\n’),哪条指令会消失?字符串 ‘hello’ 是每次调用都重新加载,还是被缓存了?函数名 print 的查找开销到底占多少?这些问题靠读文档或猜是得不到确定答案的,但 dis 能在毫秒级给出可复现的证据。

本文不打算系统讲解 CPython 虚拟机,那是另一个话题。我们只把 dis 当作诊断工具,用一系列“直觉猜测 → dis 验证 → 修正认知”的小实验,解决那些代码看起来没问题、行为却让人困惑的瞬间。所有示例基于 CPython 3.12+,字节码格式在此版本后有较大调整,若你使用更早版本,部分输出可能不一致,建议升级后再跟随操作。

字符串拼接的字节码真相

“f-string 比 % 和 + 拼接都快。”这句话你大概听过无数遍,甚至已经当作编码规范写进了团队文档。但如果你被问到“快在哪里、快多少、有没有例外”,能给出字节码级别的证据吗?

我们先用三段功能等价的代码作为实验对象:

name = "world"
a = f"Hello {name}"
b = "Hello %s" % name
c = "Hello " + name

不看 dis 输出,凭直觉猜一下:哪段指令更少?哪段隐藏开销更大?很多人会认为 f-string 完全在编译期处理,应该最简洁;% 格式化涉及类型检查和格式解析,应该最重;+ 拼接居中。这个直觉对了一半。

用 dis.dis() 分别查看三者,关键差异如下。f-string 的核心指令序列为:

阅读全文 »
0%