简介:《大语言模型入门的基础知识》是一份面向初学者与转型开发者的PDF学习材料,聚焦大语言模型的概念框架与语言模型基础,帮助读者建立从背景认知到方法演进的系统理解。压缩包内仅含1个PDF文件,大小约3.24MB,轻量便于通读与检索,适合作为课程预习、技术分享或入门自学的提纲式读物。内容围绕LLM背景、语言模型定义与评估指标展开,并梳理基于规则和统计、神经语言模型、预训练语言模型到大语言模型的发展脉络;其中对N-gram、条件概率、数据稀疏、马尔科夫假设及神经网络语言模型的结构与优缺点有较具体的讲解,可帮助读者理解生成概率、序列建模和模型评估等核心问题。已有129人学习,适合希望用较短时间搭建LLM知识骨架、补齐术语与历史脉络的读者。
1. 语言模型:它其实只在算一个概率,不在“理解”语言
“语言模型”这四个字很容易让人以为它在做语义理解。实际上,它从一开始只做一件事:给一个词序列打分。给定词典 V 和句子 S={W1,W2,…,Wn},它要算出 P(S),即这串词连起来像不像人话。大语言模型也没有脱离这个定义:千亿参数、上万亿 token 训练完,模型最后输出的仍是一个分布——下一个词是它的概率有多大。搞懂这一点,后面所有演进就能串成一条线:N-gram 用马尔科夫假设强行缩减条件概率的上下文,神经网络语言模型把离散计数换成连续向量,Transformer 再用自注意力把“前 n-1 个词”换成整句话的加权。它们其实都是同一个 P(Wn|W1…Wn-1) 在“算得准”和“算得起”之间的不同妥协。这份入门资料的价值,就在于把这条妥协链摆出来,而不是只罗列名词。
2. N-gram:马尔科夫假设怎么把指数级参数压成一张计数表
2.1 链式法则在工程上为什么直接崩掉
按概率论的链式法则,一个句子的概率可以精确拆开:
P(W1,W2,…,Wn) = P(W1) · P(W2|W1) · P(W3|W1,W2) · … · P(Wn|W1,…,Wn-1)
数学上毫无瑕疵,工程上却有两个无法回避的问题。
第一个是参数空间。假设词典 V 里有 5 万个词,理论上“长度为 n 的上下文”有 V^n 种组合。n=3 就是 1.25×10¹⁴ 量级,n=4 直接到 6×10¹⁸。你不可能为每种组合存一个参数,更别说估准。
第二个是数据稀疏。哪怕语料有几十亿词,绝大多数词组合也从没出现过。按最大似然估计,没出现过的组合概率就是 0,而链式法则是连乘——只要中间有一项是 0,整句概率归零。一句完全正常的话,因为某个三词组合没在训练集里出现就被判成“不是人话”,这就是那个经典例子里“预测能力几乎为 0”的来源。
2.2 bigram 的计数实现:C(Wi-1,Wi) 除以 C(Wi-1)
马尔科夫假设是这两条缺陷的折中:一个词出现的概率只与它前面有限的 N-1 个词有关。N=2 就是 bigram,N=3 就是 trigram。这样条件概率从 P(Wn|W1…Wn-1) 缩成 P(Wi|Wi-1),参数从 V^n 掉到 V²,可以靠数数估出来。
实现上只有两个计数器:分子数“前一个词是 A、当前词是 B”出现了多少次,分母数“前一个词是 A”出现了多少次,相除即得。下面这段代码把整个过程写全了:
from collections import Counter import math corpus = [ ["我", "想", "去", "打", "篮球"], ["我", "想", "去", "吃", "晚饭"], ["他", "想", "去", "打", "篮球"], ["我", "不", "想", "去"], ] bigram = Counter() # 分子:C(Wi-1, Wi) prefix = Counter() # 分母:C(Wi-1) for sent in corpus: # 句首句尾补标记,让 P(第一个词|句子开始) 也可计算 tokens = ["<s>"] + sent + ["</s>"] for i in range(1, len(tokens)): bigram[(tokens[i - 1], tokens[i])] += 1 prefix[tokens[i - 1]] += 1 def p(word, prev): """返回 P(word | prev),未登录组合按加一平滑处理""" return (bigram[(prev, word)] + 1) / (prefix[prev] + len(bigram))几个细节值得单独说。句首句尾补<s>和</s>,是为了让“句子第一个词”和“句子结束”也变成可统计的事件,否则 P(我|·) 和 P( |·) 无从算起。分母用的是prefix[prev],也就是“前一个词出现次数”,不是语料总词数,这一点搞混了概率会整体偏小。加一是最朴素的平滑:给每个没出现过的组合记一次虚拟计数,代价是所有概率被轻微稀释,但至少不会出现 0。
有了单步概率,句子概率就是连乘。但工程上没人真去连乘——几十个 0.0x 乘下来很快就浮点下溢成 0。标准做法是取对数再求和:
def sent_logprob(sent): tokens = ["<s>"] + sent + ["</s>"] return sum(math.log(p(tokens[i], tokens[i - 1])) for i in range(1, len(tokens))) print(sent_logprob(["我", "想", "去", "打", "篮球"])) # 约 -9.2 print(sent_logprob(["篮球", "我", "打", "去", "想"])) # 明显更低预测下一个词更简单:固定上文,遍历词表取p(w, prev)最大的那个 w。语料里“我想去打”后面跟过“篮球”,也跟过“晚饭”,谁的条件概率高就选谁——这正好对应资料里那个例子:P(我想去打篮球) > P(我想去打晚饭),所以 mask 填“篮球”,跟人类语感一致。
2.3 n 的选择:视野、参数空间与稀疏性的三方拉扯
n 的取值不是越大越好,它同时被三股力量拉扯:
| n | 参数量级 | 上下文视野 | 稀疏程度 | 实践地位 |
|---|---|---|---|---|
| unigram | V | 0 个词 | 极低 | 只做基线 |
| bigram | V² | 1 个词 | 低 | 常用 |
| trigram | V³ | 2 个词 | 中 | 常用 |
| 4-gram 以上 | V⁴ 起 | 3 个词以上 | 高,大量组合为 0 | 需强平滑才可用 |
视野变大,模型能捕捉的搭配更细,但参数量指数增长、未登录组合暴涨,平滑的负担也随之加重。实践里真正扛住线上流量的是 bigram 和 trigram,4-gram 往上基本要靠回退(backoff)或插值(interpolation)才能用。
这套方法的优缺点都很清晰。优点是极大似然估计训练极其简单,就是数数加除法;它完整包含了前 n-1 个词的信息,没有信息损失;可解释性强到能直接把计数表打出来看。缺点同样致命:只能建模到前 n-1 个词,窗口外的历史全部丢弃;n 增大后参数空间指数爆炸;数据稀疏必然带来 OOV,词典外的词直接算不出概率;泛化能力差——"我打篮球"和"他打篮球"在 bigram 里是两个独立事件,模型不知道"我"和"他"可以互换。
最后这一点,正是下一章要解决的问题:能不能让模型知道“我”和“他”在这个语境下是相近的?
3. 神经网络语言模型:词向量只是附带产物,泛化才是目的
3.1 三层前馈结构:拼接、tanh、softmax
神经网络语言模型的输入不再是词本身,而是词的稠密向量。整个网络只有三层,但每一层都解决 N-gram 的一个具体缺陷。
第一层做查表和拼接:把前 n-1 个词的词向量从矩阵 C 里取出来,首尾拼成一个 (n-1)×m 的长向量 x。第二层是全连接加 tanh 激活,把拼接向量压进一个隐层表示。第三层再映射回词表大小 V,得到每个词作为“下一个词”的未归一化分数,最后 softmax 归一化成概率分布,取最大者即为预测结果。
import numpy as np V, m, h, n = 50000, 128, 256, 3 C = np.random.randn(V, m) * 0.01 # 词向量表,同样是可训练参数 W1 = np.random.randn(h, (n - 1) * m) * 0.01 # 拼接层 -> 隐层 b1 = np.zeros(h) W2 = np.random.randn(V, h) * 0.01 # 隐层 -> 词表 b2 = np.zeros(V) def forward(ctx_ids): x = np.concatenate([C[i] for i in ctx_ids]) # 1. 查表并首尾拼接,(n-1)*m a = np.tanh(W1 @ x + b1) # 2. 隐层,非线性 y = W2 @ a + b2 # 3. logits,长度 V y = y - y.max() # 4. 数值稳定:减去最大值 e = np.exp(y) return e / e.sum() # 5. softmax 归一化形状变化是理解这段代码的关键:输入是两个 int 下标,查表后变成 2×128 的矩阵,拼接后是 256 维向量,隐层输出 256 维,输出层回到 50000 维。第 4 步的减最大值是 log-sum-exp 的标准技巧,防止 exp 溢出,数学上与不减完全等价。tanh 而不是 sigmoid,是因为当年 sigmoid 在深一点的网络里梯度消失更严重。
3.2 参数规模从 V^n 掉到 V·m 的那一步
这是整章最值得记住的一笔账。N-gram 的 trigram 需要覆盖 V³ 种组合,而神经网络语言模型的参数总量是:
V·m(词向量表) + (n-1)·m·h(拼接层权重) + h·V(输出层权重)
代入 V=5 万、m=128、h=256、n=3,大约是 640 万 + 655 万 + 1280 万,合计两千多万,而且这个数字随窗口长度 n 只是线性增长,不随 n 指数增长。在 trigram 上两者的差距是十几个数量级。
更关键的是,参数变少换来的不是能力变弱,而是泛化变强。因为词被映射到连续空间后,语义相近的词向量距离接近,一个从没在训练集里出现过的组合,只要它的词向量落在已知组合附近,网络照样能给出一个非零且合理的概率。N-gram 遇到没见过的组合只能靠平滑硬凑,神经网络则是从向量几何里“推”出来的。这条思路后来被推到极致——词向量本身甚至能作为独立产物拿去做别的事,但在这个结构里,它只是让泛化成立的必要条件,不是目的。
3.3 为什么它没能解决长依赖
理论上网络可以建模任意长的上文,工程上不行,卡在三个地方。
第一是窗口仍然是固定的。输入就是前 n-1 个词,第 n 个词之前的信息一律看不到。想扩大窗口就得加大 n,拼接层权重随之线性膨胀,训练成本跟着涨,而这个窗口是硬边界,不存在“重要就多看一眼”的机制。
第二是梯度问题。序列一长,反向传播的路径就长,梯度在连乘中指数衰减或爆炸,靠 tanh 这种饱和激活更难传递。用循环神经网络(RNN)把窗口放开能缓解一部分,但时间步之间必须串行计算,训练时没法并行,长句一多就慢得没法迭代。
第三是训练目标本身的局限。每个位置只看前 n-1 个词,模型学到的都是局部搭配,句法层面的依赖(比如主语和谓语隔了十几个词)根本没有信号去约束。
这三点合起来,直接指向了下一个阶段的答案:能不能让每个位置一次看到整句话,并且还能量化“该看谁”?
4. 预训练到大语言模型:参数量跨过一条线之后,用法整个变了
4.1 预训练 + 微调:两阶段的成本为什么要分摊
预训练语言模型的思路是:先在超大规模无标注文本上,用自监督目标训练一个通用模型,让它学到通用的语言表示;再在具体下游任务上用少量标注数据微调,把这份表示迁移过去。典型目标有两类:完形填空式的掩码语言建模(遮住一部分词让模型猜),和下一句预测;另一类是纯粹的自回归下一词预测。
这个两阶段设计的经济账很直白。无标注文本几乎免费,标注数据又贵又慢,而且每个任务都要单独标一遍。把最贵的那部分成本(从零学语言)只付一次,摊到后续 N 个任务上,单任务成本立刻降下来。资料里把这一步概括为“预训练学通用表示、微调做任务迁移”,机制上它确实成立:底层学到的句法、搭配、语义关系是跨任务共享的,任务头只需要学最后一层映射。
微调阶段有几个工程惯例,背后都有原因:
# 微调:底层冻结或极小学率,任务头正常训练 for p in model.encoder.parameters(): p.requires_grad = False # 可选:冻结底层,防止小数据集把通用表示带偏 head = nn.Linear(hidden_size, num_labels) opt = torch.optim.AdamW(head.parameters(), lr=2e-5) # 比预训练小一到两个量级学习率取 2e-5 这个量级而不是 1e-4,是因为预训练权重已经在一个很好的局部区域,步子太大会把它踩坏,也就是常说的灾难性遗忘。冻结底层则是在下游数据量很小时更保守的选择:干脆别动通用表示,只学任务头。
4.2 自注意力:把“前 n-1 个词”换成整句加权,代价是 O(L²)
Transformer 的核心是自注意力。每个位置的词生成三个向量:查询 Q、键 K、值 V,然后用 Q 和所有位置的 K 做点积算相关性,softmax 归一化成权重,再对 V 加权求和。一个位置的输出,是整句话里所有位置的加权组合——窗口这个概念被彻底取消了。
有两个细节必须解释,否则容易用错。
一是为什么要除以 √d_k。点积的方差随维度 d_k 线性增长,维度一大,点积结果就会落到 softmax 的饱和区,梯度趋近于零。除以 √d_k 把方差拉回 1 附近,这是让训练稳定的必要条件,不是可选项。
二是为什么必须要位置编码。注意力本身对输入顺序是置换不变的——把词序打乱,算出来的加权结果只是跟着换位置,模型根本不知道谁在前谁在后。位置编码就是把这个信息额外注入进去的补丁。
代价在复杂度上。序列长度 L 时,注意力矩阵是 L×L,计算和显存都是平方级。这直接决定了后来所有长文本方案的动机:不是模型不想看长,是平方项撑不住。相比之下 RNN 是 O(L·d²) 但无法并行,CNN 需要堆叠很多层才能让远距离位置建立联系。三者的取舍可以摆成一张表:
| 结构 | 任意两位置的最短路径 | 并行性 | 单层复杂度 |
|---|---|---|---|
| RNN | O(L) | 时间步串行 | O(L·d²) |
| CNN | O(log L)(需堆叠) | 可并行 | O(k·L·d²) |
| 自注意力 | O(1) | 可并行 | O(L²·d) |
路径长度决定了长依赖好不好学,并行性决定了能不能把大语料吃下去,平方项决定了上下文长度上限。大模型能成立,靠的正是前两项,被卡住的正是第三项。
4.3 规模定律与上下文学习:10B 之后为什么不用改权重了
预训练模型按结构大致分三支:双向编码器(用掩码完形填空训练,擅长理解类任务,分类、抽取、检索);自回归解码器(用下一词预测训练,擅长生成);编码器-解码器(用片段去噪训练,擅长转换类任务,如翻译、摘要)。选型的关键不是哪个更强,而是任务形态匹配哪种训练目标。
真正的转折发生在参数规模上。随着参数从亿级爬到千亿级,模型性能大致随参数量的对数线性上升,这条经验规律支撑了“把模型做大”的路线。资料里提到某 1750 亿参数的自回归模型首次展示了大语言模型的性能,也提到有开源系列推出 130 亿参数版本、有团队做到 5400 亿参数——这些数字的意义不在于刷新纪录,而在于跨过某条线之后,模型的用法变了。
变化的核心是上下文学习:不用调整任何权重,只在输入里给几个任务示例,模型就能照着格式完成新任务。这跟微调是两种完全不同的交付方式。微调要标数据、要训练、要维护每个任务一个权重副本;上下文学习只改 prompt,一次前向就完事。前面几章铺垫的所有机制在这里汇合:自注意力让模型能在上下文里灵活建立示例与问题的对应关系,规模让这种对应关系足够可靠,于是“不改权重”从不可能变成了默认做法。
代价也跟着来了。参数量大意味着算力要求高、训练周期长,而且模型会生成有害或有偏见的内容——因为它是从海量文本里学分布,文本里有什么它就可能复现什么。这就引出了对齐这一步:在预训练之后,用人类偏好数据继续训练,把“会说话”调成“说得可靠、安全”。对齐不是给模型加知识,是给它加约束,让它在本该给高概率的地方给高概率,在本该沉默的地方不开口。
5. 常见问题与排查:这几个翻车现场,入门阶段几乎人人撞一遍
5.1 数据与概率计算阶段
现象一:训练集里明明有的句子,算出来概率是 0 或者 log 值是 -inf。
原因几乎总是数据稀疏:某个 bigram 在语料里从没出现过,计数为 0,连乘把整句概率归零。这不是模型坏了,是极大似然估计的固有行为。解决分两层:短期在 log 前加一个极小值防崩,但这只是让程序不报错,概率仍然是错的;正确做法是上平滑,加一平滑最简单,回退或插值效果更好——前者在低频组合上退到低阶模型,后者把不同阶的估计加权平均。判断标准是看未登录组合在测试集上的占比,占比超过几个百分点,平滑方式就必须换。
现象二:batch 训练时 loss 看着在降,但推理时概率分布明显不对。
八成是 padding 没做 mask。一个 batch 里句子长度不齐,短句要补到统一长度,如果注意力或 softmax 把这些填充位也当正常 token 算进去,归一化的分母就被污染了。现象很隐蔽:训练集上指标还行,换成长度分布不同的验证集立刻掉。排查方法是构造一个 batch 里只有一条句子的场景,和单条推理的结果对比,两者不一致就基本能定位到 mask。解决就是 attention mask 和 loss 的 ignore_index 都要设对,别只设一个。
现象三:换了一份语料重新训,OOV 比例飙升。
原因是词典是上一份语料建的,新语料里出现大量未登录词,按定义它们的概率是 0 或统一映射到<UNK>,信息全丢。解决不是简单加大词典,而是先看 OOV 是分词粒度问题还是领域漂移问题:前者调整分词器并固定版本,后者需要重新统计词频、按覆盖率截断词表。要注意词表一旦换了,之前所有的概率数值都不可比了,这个坑会在下一章细说。
5.2 训练与推理阶段
现象四:模型生成的内容是复读机,同一句反复循环。
这是暴露偏差(exposure bias)的典型表现。训练时用的是 teacher forcing,每个位置喂的都是真实的上一个词;推理时喂的是模型自己上一步的输出。一旦某一步生成偏了,错误会顺着自回归链条累积,模型越走越偏离训练分布,最后退化成高频片段循环。缓解手段有几种:训练时按一定概率混入模型自己的预测(scheduled sampling)、解码时用 beam search 保留多条候选、或者调低温度减少随机性。但根因是训练与推理的目标不一致,这些只是缓解。
现象五:微调完模型在通用能力上明显退化,只有任务指标变好。
学习率给大了,或者训练轮数太多。预训练权重已经处在一个很好的解附近,用预训练级别的学习率去微调,等于把通用表示整个推平。血泪经验是:先冻结底层只训任务头跑通流程,确认数据管道没错,再解冻顶层用小学习率试,2e-5 起步,观察通用验证集上的指标有没有掉。掉了就降学习率或减少轮数,别硬扛。
现象六:上下文窗口塞得越满,模型对中间内容的召回越差。
不是玄学,是位置偏置:注意力对序列首尾的利用通常好于中段,塞满窗口反而稀释了关键信息。解决方式是把最关键的材料放在开头或结尾,中间放次要内容;或者先做一轮检索压缩,别把原始长文直接灌进去。判断有没有踩这个坑,做个对照实验就够:把同一份关键信息分别放在开头、中间、结尾,看回答准确率,差距明显就说明位置在起作用。
6. 评估指标与设计取舍:困惑度低为什么不代表模型好用
语言模型的评估分内在和外在两条路。内在指标是困惑度(perplexity),定义是平均负对数似然取指数:
import math def perplexity(logprobs): # logprobs: 测试集每个位置的真实词对数概率列表 avg_nll = -sum(logprobs) / len(logprobs) return math.exp(avg_nll) # 等效于“模型平均在多少个词里犹豫”取指数的意义是把它换算回“候选词数量”这个直觉量纲:困惑度 30,大致相当于模型每一步都在 30 个词里犹豫。这个指标计算便宜、定义干净,但有三个硬伤:它强依赖词表大小和分词粒度,两个模型词表不同就没有可比性;它衡量的是对真实文本的拟合,不衡量生成质量;它和下游任务表现并不总是一致,困惑度降了但回答变差的情况很常见。
外在指标按任务形态走。分类类任务用准确率、精确率、召回率、F1;生成类任务长期用 n-gram 重叠度指标,比如比较生成文本和参考文本的 1-4 元组重叠比例。这类指标的局限很明确:语义正确但换了说法的生成会被判低分,而它跟人类判断的相关性一直不高。所以现在更常见的做法是三层组合——自动指标做快速回归,用一个能力更强的模型做打分或成对比较,关键场景再上人工评估。
| 指标类型 | 代表 | 衡量什么 | 主要局限 |
|---|---|---|---|
| 内在 | 困惑度 | 对真实文本的拟合程度 | 跨词表不可比,不反映生成质量 |
| 重叠度 | n-gram 重叠类 | 与参考文本的表面相似度 | 惩罚合理改写,与人类判断相关性弱 |
| 任务 | 准确率 / F1 | 具体任务上的正确性 | 需要标注,任务间不可迁移 |
| 偏好 | 成对比较 / 人工 | 人类更接受哪个输出 | 成本高,标注者间一致性难保证 |
真正的设计取舍在这里:参数、数据、算力三者不是同比例堆就能等比例涨,规模带来的是能力上限的抬升,但评估方式必须同步升级,否则你会拿一个只测表面重叠的尺子去量一个已经会改写、会推理的模型,量出来的全是噪声。我自己的习惯是,任何一个新模型或新配置上线前,必须先在固定词表、固定测试集上跑一遍困惑度做基线对照,再跑一遍任务指标,最后抽 20 条人工看生成——三层里任何一层对不上,就先别信另外两层。这个流程看起来笨,但它拦下过的“指标涨了效果却变差”的情况,比任何单点优化都多。希望帮到你。
本文还有配套的精品资源,点击获取