LDA 困惑度 vs 主题一致性:到底该信哪个指标选 K 值?

LDA 主题数 K 选多少,是老板们问得最多的问题。答案是没有”标准答案”——但有 2 个客观指标可以参考:困惑度(perplexity)主题一致性(coherence)

这两个指标经常打架——困惑度说 K=8 最好,一致性说 K=15 最好。到底听谁的?

我们的 LDA 主题一致性和困惑度评价软件双指标联合判断 + 折线图可视化 解决了这个矛盾。这篇博客讲清楚原理和代码。

一、困惑度(Perplexity):越低越好?

困惑度衡量模型对未见文档的”预测难度”,公式:

1
perplexity(D) = exp(-1/N * Σ log p(w_d))

直觉:困惑度越低,模型越”自信”,聚类效果越好。但 LDA 场景下有个著名的坑——困惑度会随主题数 K 一直下降(因为参数多了总能更好地拟合训练集),所以单看困惑度最小值会选出过大的 K。

1
2
3
4
5
from gensim.models import LdaModel

lda = LdaModel(corpus=corpus, id2word=dictionary, num_topics=8, passes=15)
perp = lda.log_perplexity(corpus)
print(perp) # 越负越好(log 后),但只在同一数据集上比较

踩坑:困惑度的 绝对值没意义,只在同一语料上比较不同 K 才有意义。

二、主题一致性(Coherence):越高越好

一致性衡量主题内 Top N 词的”语义相关程度”——好的主题里 Top 词应该互相能解释(比如主题1的 Top 词是”小米/手机/续航/充电/发热”,这些词明显在聊”手机续航”)。

gensim 提供 4 种 Coherence 算法:

算法 含义 计算量
u_mass 用文档共现计数,-1~1 之间 最快
c_v 用滑动窗口 + NPMI + 余弦相似度 最准,推荐
c_uci 滑动窗口 + PMI
c_npmi 归一化 PMI
1
2
3
4
5
6
7
8
9
10
from gensim.models import CoherenceModel

coh = CoherenceModel(
model=lda,
texts=texts, # 原始分词后的 list of list
dictionary=dictionary,
coherence='c_v' # 推荐用 c_v
)
score = coh.get_coherence()
print(score) # 越高越好,通常 0.3-0.7 算不错

c_v 的核心思想:每个主题取 Top N 词(默认 N=10),对每对词算 NPMI(归一化点互信息)——这俩词在同一篇文档里同现的概率,比随机同现高出多少。NPMI 高 = 这俩词强相关 = 主题内聚。

三、双指标联合判断的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import matplotlib.pyplot as plt
from gensim.models import LdaModel, CoherenceModel
import numpy as np

def evaluate_lda(corpus, dictionary, texts, k_range=range(2, 21)):
perplexities, coherences = [], []
for k in k_range:
lda = LdaModel(
corpus=corpus, id2word=dictionary,
num_topics=k, passes=15,
random_state=42, alpha='auto', eta='auto'
)
# 困惑度
perp = lda.log_perplexity(corpus)
perplexities.append(perp)
# 一致性(c_v)
coh = CoherenceModel(
model=lda, texts=texts,
dictionary=dictionary, coherence='c_v'
).get_coherence()
coherences.append(coh)
print(f'K={k:2d} | Perplexity={perp:8.3f} | Coherence={coh:.4f}')
return list(k_range), perplexities, coherences

ks, perps, cohs = evaluate_lda(corpus, dictionary, texts, range(2, 16))

# 找一致性最高点
best_k = ks[np.argmax(cohs)]
print(f'\n推荐主题数 K = {best_k} (Coherence={max(cohs):.4f})')

四、双指标折线图:一眼看出”肘部”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fig, ax1 = plt.subplots(figsize=(10, 6))

# 困惑度(左轴)
color1 = '#6e5cff'
ax1.set_xlabel('主题数 K', fontsize=12)
ax1.set_ylabel('困惑度(越低越好)', color=color1, fontsize=12)
ax1.plot(ks, perps, 'o-', color=color1, linewidth=2, markersize=8, label='Perplexity')
ax1.tick_params(axis='y', labelcolor=color1)
ax1.grid(True, alpha=.3)

# 一致性(右轴)
ax2 = ax1.twinx()
color2 = '#00d4ff'
ax2.set_ylabel('一致性(越高越好)', color=color2, fontsize=12)
ax2.plot(ks, cohs, 's-', color=color2, linewidth=2, markersize=8, label='Coherence')
ax2.tick_params(axis='y', labelcolor=color2)

# 标注最佳 K
best_k = ks[np.argmax(cohs)]
ax2.axvline(x=best_k, color='#ff5cb3', linestyle='--', alpha=.6)
ax2.annotate(
f'最佳 K = {best_k}\nCoherence = {max(cohs):.3f}',
xy=(best_k, max(cohs)),
xytext=(best_k + 2, max(cohs) - 0.05),
fontsize=11, color='#ff5cb3',
arrowprops=dict(arrowstyle='->', color='#ff5cb3')
)

plt.title('LDA 主题数寻优:困惑度 vs 一致性', fontsize=14, pad=15)
fig.tight_layout()
plt.savefig('lda_evaluation.png', dpi=150, bbox_inches='tight')

怎么看这张图

  1. 困惑度曲线:找”肘部”——曲线从陡降变缓的那个拐点(类似手肘的弯折)。肘部之后继续加主题带来的困惑度收益就很小了。
  2. 一致性曲线:找最高点——这是数学最优解。
  3. 如果俩打架:以一致性最高点为主,困惑度肘部为辅。理由——一致性是直接反映主题质量的指标(Top 词是否相关),困惑度只是间接反映(预测能力)。

五、踩坑大全

踩坑 1:一致性计算很慢,K 范围别太大。建议先粗搜 range(2, 21) 找到大致区间,再细搜。

踩坑 2texts 参数必须传原始分词后的 list of list,不能传 corpus。否则 c_v 算不出 NPMI。

踩坑 3random_state 必须固定!gensim LDA 每次训练结果有随机性,不固定 random_state 两次跑出来的 K 评估不一样。

踩坑 4:一致性高不代表主题”可解释”。最好人工抽查 Top 词。我们见过 Coherence=0.6 但 Top 词是”今天/明天/后天”这种没意义的主题。

六、典型输出

我们的软件在 1 万条淘宝评论上的典型输出:

K Perplexity Coherence
5 -7.83 0.421
8 -7.95 0.512
10 -8.02 0.587 ← 最佳
12 -8.07 0.561
15 -8.11 0.523

困惑度一直在降,但一致性在 K=10 处达到峰值。用一致性选 K = 10


我们的 LDA 主题一致性和困惑度评价软件 把这套流程做成了一键式界面:导入 Excel 语料 → 选最大 K → 自动跑完所有评估 → 出一张双指标折线图 + 推荐 K 值。无需编程基础,3 分钟得到专业级模型评估报告