【机器学习】(33)—— 语言模型

语言模型:用上下文估计下一个词

摘要: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(x2x1)P(x3x1,x2)P(xtx1,,xt1)

符号含义
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(xtx1,,xt1)已知前文时,下一个 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(xtx1,,xt1)P(xtxt1)

用最大似然(计数比)估计:

P ( w ∣ u ) = C ( u , w ) C ( u ) P(w\mid u)=\frac{C(u,w)}{C(u)} P(wu)=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(wu,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(wu)=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 N1 个 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+Whhht1+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} ht1 一并输入。下面用 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 EmbeddingToken 嵌入层
Hugging Face Tokenizers子词分词
NLTK经典 n-gram 工具

10. 小结

语言模型估计的是:在上下文中,下一个 token 有多可能。Token 是原子单位;N-gram 用固定短窗口做条件概率,可用计数 + 平滑快速实现,但受长度与稀疏性限制;上下文越长越有助于消歧,却更难用纯计数估稳。RNN 能多利用历史,对很长依赖仍不足。Embedding 继续作为 token 的稠密表示;讨论重心已从单字段 ID 转到整段序列。

下一篇会说明更大的语言模型在上下文利用上与 N-gram / 常规 RNN 的差别:如何在可行计算下同时利用更长上下文,以及这与参数规模、训练数据的关系(概念为主,不绑定特定产品)。

系列导航


如果本篇对你有帮助,欢迎点赞、收藏、关注博主,机器学习专栏持续更新中,下次更新不迷路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值