语言模型:用上下文估计下一个词
文章目录
摘要:Embedding 处理的是单个离散 ID;车评、故障描述则是有顺序的符号串。**语言模型(Language Model)**要估计序列中某个 token 出现的概率。本文从汽车短句填空出发,讲清 token、N-gram、上下文与 RNN 台阶,并用可运行的计数型 bigram / trigram 代码演示「已知上文,估计下一词」。适合读完 Embedding 单元、准备进入序列建模的读者。
1. 选词填空
设想句子:
这辆车怠速时抖动明显,可能是 _______ 。
人会根据前文想到火花塞、积碳、油品等。语言模型做的是同一类事:在给定前文(有时也含后文)时,估计各候选 token 或短语的概率,再供应用选取或采样。

| 概率(示意) | 候选 |
|---|---|
| 较高 | 火花塞问题 |
| 中等 | 积碳较重 |
| 较低 | 轮胎气压不足 |
表中数字只是示意。真实模型会在整个词表上给出分布。取最大概率可用于补全;在高于阈值的候选里采样,则更适合续写时增加多样性。
会「填空」之后,续写、翻译、摘要都可以看成同一能力的不同打包方式:核心仍是序列上的条件概率。
2. 语言模型的估计目标
语言模型估计:在更长的 token 序列中,某个 token 或某段 token 出现的概率。
记序列为 x 1 , x 2 , … , x t x_1,x_2,\ldots,x_t x1,x2,…,xt。用链式法则把联合概率拆开:
P ( x 1 , … , x t ) = P ( x 1 ) P ( x 2 ∣ x 1 ) P ( x 3 ∣ x 1 , x 2 ) ⋯ P ( x t ∣ x 1 , … , x t − 1 ) P(x_1,\ldots,x_t)=P(x_1)\,P(x_2\mid x_1)\,P(x_3\mid x_1,x_2)\cdots P(x_t\mid x_1,\ldots,x_{t-1}) P(x1,…,xt)=P(x1)P(x2∣x1)P(x3∣x1,x2)⋯P(xt∣x1,…,xt−1)
| 符号 | 含义 |
|---|---|
| x i x_i xi | 第 i 个 token |
| t t t | 序列长度 |
| P ( x t ∣ x 1 , … , x t − 1 ) P(x_t\mid x_1,\ldots,x_{t-1}) P(xt∣x1,…,xt−1) | 已知前文时,下一个 token 的条件概率 |
训练与推理时,模型反复处理「已知前面,下一个是谁」。生成时就是一次次取出下一个 token,拼成更长文本。
这和预测油耗数值不同:标签空间是离散词表,输出常是很大的 Softmax(第 27 篇多类思想,词表可大得多)。第 29~32 篇的 Embedding 仍然有用:每个 token 先变成稠密向量,再进入后续序列结构。
3. Token:建模的最小单位
Token是语言建模的原子单位,可以是整词、子词或字符。

| 粒度 | 例子 | 特点 |
|---|---|---|
| 词 | 发动机 | 直观,词表易膨胀 |
| 子词 | 涡轮 / 增压 | 现代默认;兼顾词表与罕见词 |
| 字符 | 抖 / 动 | 词表小,序列更长 |
现代系统多用子词(subword):不必为每个单词单独建行,又比纯字符更容易带上语义。分词与语言有关;英文里常见的「约若干字符对应一个 token」不能硬套中文。
入门阶段可以用空格或简单按词切分做实验。下面用汽车相关中文短句,按字切分(把每个汉字当一个 token),方便演示,不依赖额外分词库:
def tokenize(text: str) -> list[str]:
"""入门演示:按字切分;工程上多用子词分词器。"""
return [ch for ch in text if not ch.isspace()]
s = "油耗比预期更高"
print(tokenize(s))
# ['油', '耗', '比', '预', '期', '更', '高']
罕见词、拼写变体常靠子词组合覆盖,和第 29 篇用 UNK 处理未见品牌是同一类压力,只是文本里更普遍。
4. N-gram:固定窗口的上下文
N-gram是长度为 N N N 的有序连续片段。 N = 2 N=2 N=2 称二元(bigram), N = 3 N=3 N=3 称三元(trigram)。

对「油耗比预期更高」按字切分后:
| N | 部分例子 |
|---|---|
| 2 | 油耗、耗比、比预、预期、期更、更高 |
| 3 | 油耗比、耗比预、比预期、预期更、期更高 |
4.1 用计数估计条件概率
bigram 模型近似认为:下一个 token 只依赖前一个:
P ( x t ∣ x 1 , … , x t − 1 ) ≈ P ( x t ∣ x t − 1 ) P(x_t\mid x_1,\ldots,x_{t-1})\approx P(x_t\mid x_{t-1}) P(xt∣x1,…,xt−1)≈P(xt∣xt−1)
用最大似然(计数比)估计:
P ( w ∣ u ) = C ( u , w ) C ( u ) P(w\mid u)=\frac{C(u,w)}{C(u)} P(w∣u)=C(u)C(u,w)
其中 C ( u , w ) C(u,w) C(u,w) 是二元组 ( u , w ) (u,w) (u,w) 出现次数, C ( u ) C(u) C(u) 是 u u u 作为前文出现的次数。trigram 则条件在前两个 token 上: P ( w ∣ u , v ) = C ( u , v , w ) / C ( u , v ) P(w\mid u,v)=C(u,v,w)/C(u,v) P(w∣u,v)=C(u,v,w)/C(u,v)。
| 优点 | 限制 |
|---|---|
| 实现直观,易统计 | 上下文长度钉死在 N-1 |
| 小语料上可快速做基线 | N 增大后组合稀疏 |
| 未见过的续写概率为 0(需平滑) |
4.2 代码:从语料统计 bigram 并预测下一词
下面用几条汽车相关句子训练一个极简 bigram,并对「怠速时抖动明显可能是」做下一字预测。为避免「没见过就概率为 0」,使用加一平滑(Laplace):
P ( w ∣ u ) = C ( u , w ) + 1 C ( u ) + V P(w\mid u)=\frac{C(u,w)+1}{C(u)+V} P(w∣u)=C(u)+VC(u,w)+1
其中 V V V 是词表大小。
from collections import Counter, defaultdict
def build_bigram(corpus: list[str]):
"""corpus: 若干句子;返回 bigram 计数、前文计数、词表。"""
bigram = Counter()
unigram = Counter()
vocab = set()
for sent in corpus:
toks = tokenize(sent)
vocab.update(toks)
for i in range(len(toks) - 1):
u, w = toks[i], toks[i + 1]
bigram[(u, w)] += 1
unigram[u] += 1
return bigram, unigram, sorted(vocab)
def next_probs(prev: str, bigram, unigram, vocab, top_k: int = 5):
"""已知前一个 token,返回加一平滑后的下一 token 概率(Top-K)。"""
V = len(vocab)
cu = unigram[prev]
scored = []
for w in vocab:
p = (bigram[(prev, w)] + 1) / (cu + V)
scored.append((w, p))
scored.sort(key=lambda x: x[1], reverse=True)
return scored[:top_k]
corpus = [
"怠速时抖动明显可能是火花塞问题",
"冷车怠速抖动可能是积碳较重",
"怠速不稳也可能是油品问题",
"油耗比预期更高需要检查轮胎气压",
"更换火花塞后怠速仍抖动要再排查",
]
bigram, unigram, vocab = build_bigram(corpus)
print("词表大小 V =", len(vocab))
print("前文「是」的下一字 Top-5:")
for w, p in next_probs("是", bigram, unigram, vocab):
print(f" {w}: {p:.4f}")
语料很小,结果只用于理解流程:先切分 → 计数 → 平滑 → 按条件概率排序。工程上会换子词分词、更大语料,并做插值或回退(backoff),但「条件概率来自计数」这一步不变。
把窗口改成 2 个前文,就是 trigram。上下文变长,组合立刻变稀——同一小语料上,许多三元组计数为 0,更依赖平滑或更大的数据。
评估计数语言模型时,除了人工看续写,还常用困惑度(perplexity):在验证句子上,下一词概率越准,困惑度越低。入门阶段不必先推公式细节,记住「验证集上比哪个模型更不吃惊」即可;样本很少时,困惑度波动也会很大,需配合人工抽查。
5. 上下文对预测的作用
**上下文(context)**指目标 token 之前或之后、有助于判断的信息。

「橙色很」单独出现时,下一词可能是「甜」或「鲜艳」。若前文是「这辆车的车漆是橙色很」,则「鲜艳」一类更合理。第 30 篇静态嵌入的一词多义,在这里变成「需要足够上下文才能消歧」。
3-gram 只提供前两个 token,对很多歧义不够。更长的 N-gram 能多看几步,却撞上稀疏性。传统计数语言模型的核心张力是:既要长上下文,又要每个条件在数据里反复出现。
故障工单里,关键线索可能在几句之前(「更换火花塞后仍…」「冷车时更明显」)。只看最后一两个字,容易续出通顺却离谱的诊断。可以用代码感受「窗口太短」:
def predict_with_prefix(prefix: str, bigram, unigram, vocab, n_next: int = 4):
"""从 prefix 最后一个字起,贪心续写 n_next 步(演示用)。"""
toks = tokenize(prefix)
out = list(toks)
for _ in range(n_next):
prev = out[-1]
w, _ = next_probs(prev, bigram, unigram, vocab, top_k=1)[0]
out.append(w)
return "".join(out)
print(predict_with_prefix("怠速时抖动明显可能是", bigram, unigram, vocab))
# 小语料上续写不稳定,正好说明:数据少 + 窗口短时,模型很脆
6. 比 N-gram 再多看一些:RNN
循环神经网络(Recurrent Neural Network, RNN) 按顺序读入 token,逐步更新隐状态 h t h_t ht,使当前预测能携带更早信息,而不是只死记固定长度的最后 N − 1 N-1 N−1 个 token。
一种常见形式:
h t = tanh ( W x h x t + W h h h t − 1 + b h ) h_t = \tanh(W_{xh}x_t + W_{hh}h_{t-1} + b_h) ht=tanh(Wxhxt+Whhht−1+bh)
y ^ t = s o f t m a x ( W h y h t + b y ) \hat{y}_t = \mathrm{softmax}(W_{hy}h_t + b_y) y^t=softmax(Whyht+by)
其中 x t x_t xt 常是 token 的 Embedding 向量, y ^ t \hat{y}_t y^t 是词表上的概率分布。

| 方式 | 上下文来源 | 常见瓶颈 |
|---|---|---|
| N-gram | 固定前 N-1 个 token | 短;或 N 大则稀疏 |
| RNN | 逐步累积的隐状态 | 很长依赖仍弱;梯度易消失 |
| 更大的序列模型 | 可同时利用更长范围 | 下一篇展开 |
概念上,用 Embedding + 线性层也可以「假装」下一步预测(便于和前面篇章衔接);真正的 RNN 会把 h t − 1 h_{t-1} ht−1 一并输入。下面用 PyTorch 示意「token id → 嵌入 → 输出词表 logits」的一步(隐状态递推在正式 RNN 单元里完成):
import torch
import torch.nn as nn
vocab_size, emb_dim, hidden = 50, 16, 32
emb = nn.Embedding(vocab_size, emb_dim)
# 简化:只用当前嵌入预测下一词;完整 RNN 还应输入 h_{t-1}
out = nn.Linear(emb_dim, vocab_size)
token_id = torch.tensor([3]) # 当前 token 下标
logits = out(emb(token_id)) # (1, vocab_size)
probs = torch.softmax(logits, dim=-1)
pred = int(torch.argmax(probs, dim=-1))
print("下一 token 下标(示意):", pred)
RNN 是理解「序列 + 上下文」的台阶,却不是终点。更长依赖上,逐步传递容易丢信息;大规模语言模型强调在可行算力下同时利用更完整的上下文。
7. 与汽车例子、Embedding 的衔接

| 场景 | 序列来源 | 更合适的做法 |
|---|---|---|
| 车评 | 评论文本 | 语言模型 / 序列模型 |
| 故障工单 | 技师描述 | 序列模型 + 人工校验 |
| 配置说明 | 说明书句子 | 补全、生成式问答 |
| 品牌 / 车型 ID | 表格字段 | 第 29 篇 Embedding,不必强行 LM |
结构化 ID 继续用离散 Embedding + 表格模型。只有输入本身是自然语言序列时,才需要本篇视角。Token 嵌入与品牌嵌入都是查表;差别在于语言模型还要处理顺序、可变长度,并在大词表上做多类预测。
7.1 小流程回顾
准备语料(车评 / 工单句子)
→ 分词或子词切分,得到 token 序列
→ 选模型:计数 N-gram(基线)或神经网络 LM
→ 训练:最大化下一词条件概率(或等价损失)
→ 评估:困惑度、续写抽查、业务侧人工抽检
→ 上线:生成结果加规则 / 人工,避免直接当诊断结论
8. 能力边界与常见误区
| 情况 | 说明 |
|---|---|
| 把「概率高」当成「懂维修」 | 优化目标是 token 概率,不是因果诊断 |
| 认为 N 越大一定越好 | 数据不够时更长 N-gram 更稀 |
| 忽略语言与分词差异 | 英文经验长度不能硬套中文 |
| 短窗口处理长工单 | 关键线索可能在窗口外 |
| 表格 ID 强行改成生成式 LM | 常无必要 |
| 生成结果不校验就当结论 | 可能胡编,需把关 |
| 小语料上解读 Top-1 续写 | 方差大,只适合理解算法 |
适用前提:有足够文本语料、任务确实依赖语序与用词。若只有品牌、排量等表格字段,回到 Embedding 与监督学习即可。
9. 术语与延伸阅读
| 术语 | 含义 |
|---|---|
| 语言模型 Language Model | 估计序列中 token(段)概率的模型 |
| Token | 建模最小单位:词 / 子词 / 字符等 |
| 子词 subword | 介于字符与整词之间的切分单位 |
| N-gram | 长度为 N 的连续 token 片段 |
| 上下文 context | 有助于预测目标 token 的前后信息 |
| 加一平滑 Laplace | 计数加 1,避免零概率 |
| RNN | 按序读入序列并维护隐状态的网络 |
| 资源 | 适合看什么 |
|---|---|
| 专栏第 29~32 篇 | Token / ID 嵌入 |
| 专栏第 27 篇 | 大词表 Softmax |
| PyTorch Embedding | Token 嵌入层 |
| Hugging Face Tokenizers | 子词分词 |
| NLTK | 经典 n-gram 工具 |
10. 小结
语言模型估计的是:在上下文中,下一个 token 有多可能。Token 是原子单位;N-gram 用固定短窗口做条件概率,可用计数 + 平滑快速实现,但受长度与稀疏性限制;上下文越长越有助于消歧,却更难用纯计数估稳。RNN 能多利用历史,对很长依赖仍不足。Embedding 继续作为 token 的稠密表示;讨论重心已从单字段 ID 转到整段序列。
下一篇会说明更大的语言模型在上下文利用上与 N-gram / 常规 RNN 的差别:如何在可行计算下同时利用更长上下文,以及这与参数规模、训练数据的关系(概念为主,不绑定特定产品)。
系列导航:
- 上一篇:【机器学习】(32)—— Embedding 串讲
- 下一篇(预告):更大的语言模型:一次利用更长的上下文
如果本篇对你有帮助,欢迎点赞、收藏、关注博主,机器学习专栏持续更新中,下次更新不迷路。
—— 语言模型&spm=1001.2101.3001.5002&articleId=163506164&d=1&t=3&u=5374a0219994432d9245c3014465ba95)
173

被折叠的 条评论
为什么被折叠?



