查看全课大纲(第 12 / 69 节)
- 1.NLP 的研究领域、应用与学习路径
- 2.自然语言的基本术语
- 3.信息论基础:信息熵与交叉熵
- 4.语言的进化与语义变迁
- 5.主观概率、客观概率与条件概率
- 6.贝叶斯原理与推理
- 7.随机变量:二项式、期望方差与联合概率
- 8.常见概率分布
- 9.辛普森悖论与贝叶斯解题实例
- 10.语言模型:语言概率与词袋模型
- 11.n-gram 语言模型(二元 / 三元 / N-Gram)
- 12.语言模型评价:交叉熵、困惑度与插值
- 13.朴素贝叶斯文本分类:垃圾邮件
- 14.拼写检查
- 15.中文分词:构词法、最大匹配、最优路径与 Jieba
- 16.词表示问题与语义相似度
- 17.TF-IDF 权重处理
- 18.One-Hot 表达与词表示的局限
- 19.Word2Vec 核心思想
- 20.Word2Vec 训练过程
- 21.Skip-gram 模型详解
- 22.词性标注任务与标注方法(规则 + N-Gram)
- 23.EM 算法与混合模型
- 24.高斯混合模型 GMM
- 25.从混合模型到隐马尔可夫模型
- 26.隐马尔可夫模型(HMM)推导与维特比解码
- 27.线性链条件随机场 CRF
- 28.贝叶斯网络与图模型
- 29.隐马尔可夫链与 HMM 推导
- 30.PLSA 主题模型
- 31.LSI 与 SVD 向量表达
- 32.LDA 与狄利克雷分布
- 33.主题模型实验复现(困惑度 / 分类 / 相似度 / 延展)
- 34.上下文无关文法(CFG)与句法分析
- 35.概率上下文无关文法 PCFG
- 36.统计机器翻译:词对齐与翻译模型
- 37.统计机器翻译中的对齐模型与参数估计
- 38.从统计机器翻译到神经机器翻译
- 39.神经元与反向传播速览(指向深度学习课)
- 40.CNN 原理及其在 NLP 中的应用
- 41.循环神经网络的基本原理与 Elman / Jordan 网络
- 42.LSTM:长短期记忆网络的核心思想与分步实现
- 43.Encoder-Decoder 框架
- 44.Seq2Seq 与早期注意力机制
- 45.注意力机制的背景与动机
- 46.注意力模型实现原理与匹配函数
- 47.记忆网络:组成与计算
- 48.注意力机制的应用与历史延展
- 49.Transformer 结构:Encoder-Decoder 栈与 Self-Attention
- 50.Multi-Head Attention、FFNN 与位置编码
- 51.层归一化、损失函数与 Universal Transformer 延展
- 52.预训练范式:从 ELMo 到 BERT
- 53.BERT 与掩码语言模型
- 54.BERT 下游任务微调与 GLUE 口径
- 55.GPT 与自回归语言建模
- 56.GPT-1 到 GPT-4 的演进
- 57.指令微调与 RLHF
- 58.分词器:BPE、WordPiece 与中文词表
- 59.上下文窗口与长文本处理
- 60.能力边界、成本与适用场景
- 61.文本分类实战:从朴素贝叶斯到逻辑回归
- 62.命名实体识别实战
- 63.文本生成与摘要实战
- 64.语义检索与向量库实战
- 65.RAG 知识库问答
- 66.NLP 评估指标:BLEU / ROUGE / BERTScore 与人工评估
- 67.幻觉与事实性问题
- 68.文本与知识结合(知识图谱压缩)
- 69.跨媒体信息检索与未来展望
语言模型评价:交叉熵、困惑度与插值
小象实战讲义 · 自然语言处理
在上一节中,我们学习了如何构建n-gram语言模型来估计句子出现的概率。然而,面对多种模型(如unigram, bigram, trigram),我们如何客观地评价它们的优劣?哪种模型更能捕捉语言的规律?本节将引入信息论中的核心工具——交叉熵(Cross-Entropy)和困惑度(Perplexity),为语言模型提供一套可量化的评价标准。我们还将学习插值法(Interpolation),一种结合不同阶数n-gram模型优势的平滑技术。学完本节,你将能够计算并比较不同语言模型的性能,并理解其背后的信息论意义。
💡 核心导读
- 交叉熵:衡量真实语言分布与我们模型预测分布之间的“距离”。交叉熵越低,模型预测下一个词的能力越强。
- 困惑度:交叉熵的指数形式,直观理解为模型在预测下一个词时的“平均分支因子”或不确定性。困惑度越低,模型越好。
- 信息论视角:交叉熵和困惑度与数据压缩密切相关。一个好的语言模型能更高效地编码文本,平均每个词所需的比特数更少。
- 模型插值:通过加权组合不同阶数的n-gram模型(如unigram, bigram, trigram),可以缓解数据稀疏问题,获得更鲁棒的概率估计。
1. 从熵到交叉熵:衡量预测的不确定性
在信息论中,熵(Entropy) 衡量的是一个随机变量 的不确定性。对于一个离散概率分布 ,其熵定义为: 熵的单位是比特(bit)。它表示,如果我们想对事件 进行最优编码(如霍夫曼编码),平均每个事件需要的最少比特数。
交叉熵(Cross-Entropy) 则用于衡量当我们使用一个估计的分布 来近似真实分布 时,编码事件所需的平均比特数。其定义为: 在语言模型的上下文中:
- 是真实但未知的语言分布(一个词序列出现的真实概率)。
- 是我们的语言模型(如n-gram模型)给出的概率分布 。
对于一个由 个词组成的测试句子 ,其每个词的交叉熵可以近似为: 这里, 是我们的模型赋予整个句子的概率。这个公式的直观理解是:模型给句子的概率越高( 值越大),负对数就越小,交叉熵也就越低,说明模型预测得越好。
2. 困惑度:更直观的评价指标
困惑度(Perplexity,PPL) 是语言模型评价中最常用的指标,它直接由交叉熵定义而来: 将每个词的交叉熵公式代入,对于一个测试句子,其困惑度近似为:
困惑度的直观解释:
- 平均分支因子:假设模型在预测每个词时,面临一个均匀分布的选择。困惑度的值等于这个均匀分布的“选项”数量。例如,困惑度为64,意味着模型在预测每个词时,其不确定性相当于在64个等概率的词中随机猜测。
- 与交叉熵的关系:较低的交叉熵直接对应较低的困惑度。两者都指示更好的模型性能。
- 数据压缩视角:困惑度也反映了压缩效率。一个困惑度为 的模型,平均每个词需要 个比特来编码。困惑度越低,压缩率越高。
让我们通过一个具体的例子来理解交叉熵和困惑度的计算,并比较不同模型。
# ========== 1. 定义微型语料库和测试句子 ==========
# 为了演示,我们使用一个极小的语料库和句子
corpus = [
"i spent three years",
"before the mast",
"i spent money"
]
test_sentence = ["i", "spent", "three", "years", "before", "the", "mast"]
M = len(test_sentence) # 句子长度
# ========== 2. 构建Unigram模型概率 ==========
from collections import Counter
import math
# 将所有句子合并并分词
all_words = []
for sent in corpus:
all_words.extend(sent.split())
vocab = set(all_words)
V = len(vocab) # 词汇表大小
# 计算unigram计数和概率(使用加1平滑)
unigram_counts = Counter(all_words)
def p_unigram(word):
# 加1平滑 (Laplace smoothing)
return (unigram_counts.get(word, 0) + 1) / (len(all_words) + V)
# ========== 3. 构建Bigram模型概率 ==========
# 创建bigram计数
bigram_counts = {}
context_counts = {}
for sent in corpus:
words = sent.split()
for i in range(len(words) - 1):
bigram = (words[i], words[i+1])
bigram_counts[bigram] = bigram_counts.get(bigram, 0) + 1
context_counts[words[i]] = context_counts.get(words[i], 0) + 1
def p_bigram(w1, w2):
# 使用加1平滑
count_bigram = bigram_counts.get((w1, w2), 0)
count_context = context_counts.get(w1, 0)
# 平滑后概率: (c(w1,w2)+1) / (c(w1)+V)
return (count_bigram + 1) / (count_context + V)
# ========== 4. 计算句子概率、交叉熵和困惑度 ==========
# 计算unigram模型下的句子概率(假设词独立)
log_prob_uni = 0.0
for word in test_sentence:
prob = p_unigram(word)
log_prob_uni += math.log2(prob)
# 计算bigram模型下的句子概率(需要句首标记,这里简化处理)
# 假设句子以"<s>"开始,计算 P(w1|<s>) * P(w2|w1) * ... * P(w7|w6)
log_prob_bi = 0.0
# 第一个词的概率:P(i | <s>),用unigram近似,因为我们的语料没有句首标记
log_prob_bi += math.log2(p_unigram(test_sentence[0]))
for i in range(1, M):
prob_bi = p_bigram(test_sentence[i-1], test_sentence[i])
log_prob_bi += math.log2(prob_bi)
# 计算每个词的交叉熵
cross_entropy_uni = - (1 / M) * log_prob_uni
cross_entropy_bi = - (1 / M) * log_prob_bi
# 计算困惑度
perplexity_uni = 2 ** cross_entropy_uni
perplexity_bi = 2 ** cross_entropy_bi
print(f"测试句子: '{' '.join(test_sentence)}'")
print(f"句子长度 M = {M}")
print()
print("=== Unigram 模型 ===")
print(f" 句子对数概率 (log2): {log_prob_uni:.4f}")
print(f" 每个词交叉熵: {cross_entropy_uni:.4f} bits")
print(f" 困惑度: {perplexity_uni:.4f}")
print()
print("=== Bigram 模型 ===")
print(f" 句子对数概率 (log2): {log_prob_bi:.4f}")
print(f" 每个词交叉熵: {cross_entropy_bi:.4f} bits")
print(f" 困惑度: {perplexity_bi:.4f}")测试句子: 'i spent three years before the mast'
句子长度 M = 7
=== Unigram 模型 ===
句子对数概率 (log2): -21.0196
每个词交叉熵: 3.0028 bits
困惑度: 8.0155
=== Bigram 模型 ===
句子对数概率 (log2): -16.1536
每个词交叉熵: 2.3077 bits
困惑度: 4.9508结果分析:
- Bigram 模型的交叉熵是 2.3077 bits,Unigram 模型是 3.0028 bits,前者更低。
- 相应地,Bigram 的困惑度 4.9508 也低于 Unigram 的 8.0155(8.0155 = ,4.9508 = ,正好是交叉熵取 2 为底的指数)。
- 这意味着,在这个微型语料库和这条测试句上,Bigram 是更好的模型,它预测下一个词的不确定性更小。从数据压缩角度看,用 Bigram 编码每个词平均约 2.3077 个比特,Unigram 约需 3.0028 个比特——每词省下约 0.7 比特。
信息论意义:这个例子印证了课件中的观点:更好的语言模型(如bigram)能更有效地“压缩”语言,平均每个词所需的编码位数更少。课件中提到的Moby Dick例子显示,bigram模型将句子“I spent three years before the mast”的交叉熵降至约6 bits/word,而unigram模型约为11 bits/word,压缩效率差异显著。
3. 模型插值:结合不同阶数的优势
n-gram模型面临一个权衡:高阶模型(如trigram)能捕捉更长的上下文依赖,但数据稀疏问题严重(许多n-gram在训练中从未出现);低阶模型(如unigram)数据估计更可靠,但忽略了上下文信息。
插值法(Interpolation) 提供了一种解决方案:将不同阶数的n-gram模型通过线性加权的方式组合起来。例如,一个插值的三元模型概率可以定义为: 其中:
- 是unigram概率。
- 是bigram概率。
- 是trigram概率。
- 是插值参数(或混合权重),满足 且 。
插值法的优点:
- 缓解稀疏性:即使 trigram 概率 为零,只要高阶项对应的权重不全为零、且低阶概率不为零,最终插值概率就不为零。注意条件里必须带上“”:若 、,插值退化成 trigram 本身,零概率照旧。
- 灵活利用信息:模型可以自动学习权重 ,决定在何种情况下更依赖哪种阶数的信息。
- 平滑效果:它本身就是一种有效的平滑技术。
如何确定最优的 值?通常在一个留存数据集(held-out dataset) 上进行优化,目标是使该数据集上的困惑度最小化。
# —— 重新构建上文用到的微型数据(本节各代码块相互独立,可单独运行)
corpus = [
"i spent three years",
"before the mast",
"i spent money"
]
test_sentence = ["i", "spent", "three", "years", "before", "the", "mast"]
M = len(test_sentence) # 句子长度
from collections import Counter
import math
all_words = []
for sent in corpus:
all_words.extend(sent.split())
vocab = set(all_words)
V = len(vocab) # 词汇表大小
unigram_counts = Counter(all_words)
def p_unigram(word):
# 加1平滑 (Laplace smoothing)
return (unigram_counts.get(word, 0) + 1) / (len(all_words) + V)
bigram_counts = {}
context_counts = {}
for sent in corpus:
words = sent.split()
for i in range(len(words) - 1):
bigram = (words[i], words[i+1])
bigram_counts[bigram] = bigram_counts.get(bigram, 0) + 1
context_counts[words[i]] = context_counts.get(words[i], 0) + 1
def p_bigram(w1, w2):
# 使用加1平滑
count_bigram = bigram_counts.get((w1, w2), 0)
count_context = context_counts.get(w1, 0)
# 平滑后概率: (c(w1,w2)+1) / (c(w1)+V)
return (count_bigram + 1) / (count_context + V)
log_prob_uni = 0.0
for word in test_sentence:
prob = p_unigram(word)
log_prob_uni += math.log2(prob)
log_prob_bi = 0.0
log_prob_bi += math.log2(p_unigram(test_sentence[0]))
for i in range(1, M):
prob_bi = p_bigram(test_sentence[i-1], test_sentence[i])
log_prob_bi += math.log2(prob_bi)
cross_entropy_uni = - (1 / M) * log_prob_uni
cross_entropy_bi = - (1 / M) * log_prob_bi
perplexity_uni = 2 ** cross_entropy_uni
perplexity_bi = 2 ** cross_entropy_bi
# ========== 5. 插值法计算示例 ==========
# 假设我们要计算 P(three | i, spent) 的插值概率
# 沿用前面的概率函数;trigram 项为演示插值而人为设定一个很小的值
# 定义上下文和目标词
context = ("i", "spent")
target = "three"
# 获取各阶概率估计(使用之前定义的函数)
p_uni = p_unigram(target) # P(three)
p_bi = p_bigram(context[1], target) # P(three | spent)
# 为演示插值,人为设定一个很小的 trigram 概率(若要用观测值,需先构建 trigram 计数并按平滑规则计算)
p_tri = 0.001 # 人为设定的 P(three | i, spent)
# 定义一组插值权重(这些权重通常从留存数据学习得到)
lambda1, lambda2, lambda3 = 0.1, 0.3, 0.6 # 总和为1
# 计算插值概率
p_interp = lambda1 * p_uni + lambda2 * p_bi + lambda3 * p_tri
print("=== 插值法计算示例 ===")
print(f"上下文: {context}")
print(f"目标词: '{target}'")
print(f" Unigram 概率 P({target}) = {p_uni:.6f}")
print(f" Bigram 概率 P({target} | {context[1]}) = {p_bi:.6f}")
print(f" Trigram 概率 P({target} | {context[0]}, {context[1]}) = {p_tri:.6f} (虚构)")
print(f"插值权重: λ1={lambda1}, λ2={lambda2}, λ3={lambda3}")
print(f"插值概率 P_interp({target} | {context[0]}, {context[1]}) = {p_interp:.6f}")=== 插值法计算示例 ===
上下文: ('i', 'spent')
目标词: 'three'
Unigram 概率 P(three) = 0.111111
Bigram 概率 P(three | spent) = 0.200000
Trigram 概率 P(three | i, spent) = 0.001000 (虚构)
插值权重: λ1=0.1, λ2=0.3, λ3=0.6
插值概率 P_interp(three | i, spent) = 0.071711在这个虚构的例子中,尽管我们赋予trigram模型最高的权重(0.6),但由于其估计概率很低(0.001,注意这里不是 0),插值结果 0.071711 主要由 unigram 的 0.111111 与 bigram 的 0.2 拉起来:0.1*0.111111 + 0.3*0.2 + 0.6*0.001 = 0.011111 + 0.06 + 0.0006 = 0.071711。它演示的是低概率被抬升,而不是”零概率被救回”;要看零概率救回,得把 设成 0 再看结果。
4. 评价指标的应用与局限
在实际应用中,超参数的选择与最终报告要分在两个数据集上做。阶数 、平滑方法、插值权重 这些超参数,必须用留存集(held-out)或验证集来选——在那上面反复比较、挑出最好的一组;等超参数全部锁定后,才在从未参与过任何选择的独立测试集上算一次最终困惑度,作为对外报告的泛化成绩。
把测试集拿来比较不同阶数或不同平滑方法,实质是在测试集上做模型选择:选出来的模型已经”见过”测试集的信息,报出的困惑度不再是干净的泛化估计,而是偏乐观的。这也是本节前面说”在留存集上优化 “的原因——两处必须口径一致。
几点重要认识:
- 相对性:困惑度是一个相对指标,用于比较不同的模型。单独一个困惑度数值(如247)没有绝对意义,但通过与基线模型(如unigram)或其他模型对比,我们可以判断改进程度。
- 与任务的相关性:更低的困惑度通常意味着更好的语言模型,但不一定直接转化为下游任务(如机器翻译、语音识别)的性能提升。最终仍需在具体任务上验证。
- 历史参考:课件中提到,在著名的布朗语料库(Brown Corpus)上,1992年最好的语言模型达到了约247的困惑度,对应每个词约7.9比特的交叉熵。这为我们提供了一个历史基准。
📝 动手练一练
- 概念辨析:假设模型A在测试集上的困惑度为50,模型B的困惑度为100。哪个模型更好?为什么?从“平均分支因子”的角度如何理解这个差异?
👉 点击查看参考答案
模型A更好。因为困惑度越低,代表模型在预测下一个词时的不确定性越小,模型对语言的建模能力越强。
从“平均分支因子”理解:
- 模型A的困惑度为50,意味着该模型在预测每个词时,其不确定性相当于在一个均匀分布的、包含50个词的词表中进行随机选择。
- 模型B的困惑度为100,相当于要在100个等概率的词中猜测。
- 显然,模型A的猜测空间更小,预测更准确,因此是更好的模型。
- 手算练习:假设一个极小的语言只有两个词
{A, B}。一个unigram语言模型给出概率:, 。现有测试序列为A B。- a) 计算该序列在模型下的概率。
- b) 计算该序列的每个词交叉熵。
- c) 计算模型的困惑度。
👉 点击查看参考答案
a) 序列概率:
b) 每个词交叉熵: 首先计算序列的对数概率(以2为底):
序列长度 。 每个词交叉熵
c) 困惑度:
也可直接算:,两条路结果相同。
这里顺带排掉一个常见算错: 不是 ( 是 ),若误用它,交叉熵会算成 、困惑度算成 ,与 2.5 差开。精确算法是 ,于是 ,。所以最终答案:困惑度 = 2.5,每个词交叉熵 ≈ 1.3219 比特。
本章小结
本节我们为语言模型建立了核心的评价体系。交叉熵和困惑度源于信息论,为我们提供了量化模型预测能力的工具:交叉熵越低(困惑度越低),模型越好。这一评价标准与数据压缩效率紧密相连。此外,我们学习了插值法,通过线性组合不同阶数的n-gram模型来平衡上下文依赖与数据稀疏性,这是一种强大且实用的平滑技术。
现在,我们已经掌握了语言模型从构建(n-gram)到平滑(多种技术)再到评价(困惑度)的完整流程。这些经典方法奠定了统计语言模型的基础,其核心思想——用概率描述语言序列,并通过优化概率评价模型——至今仍在深度学习中延续。
📋 行动清单
- 理解交叉熵和困惑度的定义、计算方式及其信息论解释(压缩效率)。
- 能够手动计算给定句子在简单n-gram模型下的困惑度。
- 掌握模型插值法的动机、公式和应用场景,理解其作为一种平滑技术的作用。
—— 小象教研组