You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

语言模型中熵与交叉熵的差异解析:字符级Unigram模型实践示例

熵与交叉熵的差异:以字符级名字语料为例

核心概念梳理

熵(Entropy)

熵是衡量真实语言分布P不确定性的指标,对于字符级语言来说,它表示预测下一个字符的平均不确定性。计算时需要先得到真实语言的概率分布(比如n-gram的出现概率),公式为:
$$H(P) = -\sum_{x} P(x) \log_2 P(x)$$
对于序列语言,我们通常计算熵率(每个字符的平均熵),即n-gram熵除以n,用来近似无限长语言序列的真实熵。

交叉熵(Cross-Entropy)

交叉熵衡量模型预测分布Q与真实分布P之间的差异,用来评估模型对真实语言的拟合程度。公式为:
$$H(P,Q) = -\sum_{x} P(x) \log_2 Q(x)$$
在语言模型训练中,P常以独热编码形式出现(因为每个位置的真实字符是确定的,概率为1),交叉熵损失就是基于这个逻辑计算的。

关联

  • 语言熵是交叉熵的下界:当模型Q完全拟合真实分布P时,交叉熵等于熵;模型越差,交叉熵越高。
  • 语言熵反映语言本身的复杂度,交叉熵反映模型对语言的建模能力。

基于给定名字语料的计算方法

我们以提供的20个名字组成的语料为例,分别计算语言熵率和Unigram模型的交叉熵率:

步骤1:预处理语料

把所有名字拼接成一个字符串,统一转为小写(忽略大小写差异):

prenoms = [
    "Alice", "Alfred", "Alina", "Aline", "Alexandre", 
    "Alicia", "Alison", "Alma", "Alva", "Elise", 
    "Elisa", "Eliane", "Alain", "Amélie", "Arline", 
    "Olivier", "Oline", "Alva", "Eliott", "Julien"
]
corpus = ''.join(prenoms).lower()

步骤2:计算语言熵率

用n-gram(这里选n=4)近似真实语言分布:

  1. 统计所有4-gram的出现次数,计算每个4-gram的真实概率P(ngram)
  2. 计算4-gram的熵,再除以4得到每个字符的熵率

步骤3:计算Unigram模型的交叉熵率

Unigram模型假设字符之间相互独立,因此一个n-gram的概率等于每个字符概率的乘积:

  1. 统计每个字符的出现概率(Unigram分布Q(c))
  2. 对每个真实的4-gram,计算模型预测的概率Q(ngram) = Q(c1)*Q(c2)*Q(c3)*Q(c4)
  3. 代入交叉熵公式计算,再除以4得到每个字符的交叉熵率

代码解释与优化

你提供的代码逻辑是正确的,以下是优化后的版本(修正模块导入位置、补充注释):

import math
from collections import Counter 

def get_ngram_distribution(text, n=4):
    """生成n-gram的概率分布"""
    ngrams = [text[i:i+n] for i in range(len(text)-n+1)]
    total_ngrams = len(ngrams)
    count_counter = Counter(ngrams)
    return {ngram: count/total_ngrams for ngram, count in count_counter.items()}

def calculate_language_entropy_rate(text, n=4):
    """计算语言的熵率(每个字符的平均熵)"""
    ngram_dist = get_ngram_distribution(text, n)
    # 计算n-gram的熵
    ngram_entropy = -sum(p * math.log2(p) for _, p in ngram_dist.items())
    # 归一化到每个字符的熵率
    return ngram_entropy / n

def calculate_unigram_cross_entropy_rate(text, n=4):
    """计算Unigram模型的交叉熵率"""
    # 真实n-gram分布(P)
    true_ngram_dist = get_ngram_distribution(text, n)
    # Unigram模型分布(Q)
    unigram_dist = get_ngram_distribution(text, 1)
    
    cross_entropy = 0.0
    for ngram, p_true in true_ngram_dist.items():
        # Unigram模型对该n-gram的预测概率:字符独立,乘积计算
        p_pred = 1.0
        for char in ngram:
            p_pred *= unigram_dist[char]
        # 累加交叉熵项
        cross_entropy -= p_true * math.log2(p_pred)
    
    # 归一化到每个字符的交叉熵率
    return cross_entropy / n

if __name__ == "__main__":
    prenoms = [
        "Alice", "Alfred", "Alina", "Aline", "Alexandre", 
        "Alicia", "Alison", "Alma", "Alva", "Elise", 
        "Elisa", "Eliane", "Alain", "Amélie", "Arline", 
        "Olivier", "Oline", "Alva", "Eliott", "Julien"
    ]
    corpus = ''.join(prenoms).lower()
    
    entropy_rate = calculate_language_entropy_rate(corpus)
    cross_entropy_rate = calculate_unigram_cross_entropy_rate(corpus)
    
    print(f"语言熵率(每个字符):{entropy_rate:.4f} bits")
    print(f"Unigram模型交叉熵率(每个字符):{cross_entropy_rate:.4f} bits")

代码输出说明

运行后会得到两个数值:

  • 语言熵率:反映这个名字语料本身的字符不确定性,数值越小说明语料的字符模式越固定。
  • Unigram模型交叉熵率:反映Unigram模型对这个语料的拟合程度,它一定大于等于语言熵率,差值越大说明模型的建模能力越差(因为Unigram忽略了字符间的依赖关系)。

更简化的示例

假设我们有一个极小的语料:["aaab", "aaac"],拼接后是aaabaaac:

  1. 语言熵率(n=2):
    2-gram有:aa, aa, ab, ba, aa, aa, ac
    分布:aa:4/7, ab:1/7, ba:1/7, ac:1/7
    2-gram熵:$-(4/7 \log2(4/7) + 3*(1/7 \log2(1/7))) ≈ 1.79$
    熵率:$1.79/2 ≈ 0.895$ bits/字符

  2. Unigram交叉熵率:
    Unigram分布:a:6/8, b:1/8, c:1/8
    每个2-gram的模型预测概率:
    aa: (6/8)^2=36/64, ab: (6/8)(1/8)=6/64, ba: (1/8)(6/8)=6/64, ac: (6/8)(1/8)=6/64
    交叉熵:$-(4/7 \log2(36/64) + 1/7 \log2(6/64) + 1/7 \log2(6/64) + 1/7 \log2(6/64)) ≈ 2.02$
    交叉熵率:$2.02/2 ≈ 1.01$ bits/字符

可以看到交叉熵率大于熵率,因为Unigram模型没捕捉到"aa"之后更可能接"a"、"b"或"c"的依赖关系。


内容的提问来源于stack exchange,提问作者curious

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 06:29:58