主题模型 本节摘要:LDA 把文档看作话题的混合,把话题看作词上的分布;BERTopic 让文档在嵌入空间里聚类,把簇当作话题。目标相同,分解方式不同。你手上有一万张客服工单、五万篇新闻或二十万条推文,需要在不读的前提下知道这批材料在讲什么——没有标注类别,甚至连有几类都不知道。主题模型在无监督下回答这个问题:喂语料,取回一小组连贯话题,并给每篇文档一个在这些话题上的分布。两大算法族主导。LDA(2003)用贝叶斯推断把每篇文档建模为隐话题的混合、每个话题建模为词上的分布,至今在生产里仍用于需要混合归属和可解释词级概率的场景。
本节摘要:LDA 把文档看作话题的混合,把话题看作词上的分布;BERTopic 让文档在嵌入空间里聚类,把簇当作话题。目标相同,分解方式不同。你手上有一万张客服工单、五万篇新闻或二十万条推文,需要在不读的前提下知道这批材料在讲什么——没有标注类别,甚至连有几类都不知道。主题模型在无监督下回答这个问题:喂语料,取回一小组连贯话题,并给每篇文档一个在这些话题上的分布。两大算法族主导。LDA(2003)用贝叶斯推断把每篇文档建模为隐话题的混合、每个话题建模为词上的分布,至今在生产里仍用于需要混合归属和可解释词级概率的场景。BERTopic(2020)用 BERT 编码文档、UMAP 降维、HDBSCAN 聚类、再用类内 TF-IDF 抽话题词,在短文本、社交媒体以及语义相似比词重叠更重要的任何场合都胜出——代价是一篇文档只属一个话题,对长文是个限制。本节为两者建立直觉,并点明给定语料时该选哪个。
对应原课程:Phase 5 · Lesson 15 ·
topic-modeling(原英文phases/05-nlp-foundations-to-advanced/15-topic-modeling/docs/en.md)。前置依赖:第 02 节(词袋与 TF-IDF)、第 03 节(Word2Vec)。
阅读完本节,你应当能够:
你有一万张客服工单、五万篇新闻或二十万条推文。需要在不读的前提下知道这批材料在讲什么。你没有标注类别,甚至不知道有几类。
主题模型在无监督下回答这个问题:给语料,取回一小组连贯话题,并给每篇文档一个在这些话题上的分布。两大算法族主导:
LDA 的生成式故事。 每个话题是词上的分布,每篇文档是话题的混合。要在文档里生成一个词,先从文档的混合里抽一个话题,再从该话题的词分布里抽一个词。推断反过来:给定观测到的词,反推每篇文档的话题分布和每个话题的词分布。坍缩 Gibbs 采样或变分贝叶斯做这套数学。
LDA 的两个关键输出:
doc_topic:矩阵 (n_docs, n_topics),每行求和为 1(文档的话题混合)。topic_word:矩阵 (n_topics, vocab_size),每行求和为 1(话题的词分布)。BERTopic 流水线。 先用句子编码器(如 all-MiniLM-L6-v2)把每篇文档编码成 384 维向量;BERT 嵌入对聚类维度太高,用 UMAP 降到约 5 维;再用 HDBSCAN 聚类,密度法产出变长簇和一个「离群」标签;最后对每个簇,在其文档上算类内 TF-IDF 抽出最高词。
输出是「一篇文档一个话题」(外加 -1 离群标签);可选地通过 HDBSCAN 的概率向量得到软归属。
from sklearn.feature_extraction.text import CountVectorizer from sklearn.decomposition import LatentDirichletAllocation import numpy as np def fit_lda(documents, n_topics=5, max_features=1000): cv = CountVectorizer( max_features=max_features, stop_words="english", min_df=2, max_df=0.9, ) X = cv.fit_transform(documents) lda = LatentDirichletAllocation( n_components=n_topics, random_state=42, max_iter=50, learning_method="online", ) doc_topic = lda.fit_transform(X) feature_names = cv.get_feature_names_out() return lda, cv, doc_topic, feature_names def print_top_words(lda, feature_names, n_top=10): for idx, topic in enumerate(lda.components_): top_idx = np.argsort(-topic)[:n_top] words = [feature_names[i] for i in top_idx] print(f"topic {idx}: {' '.join(words)}")
注意:去停用词;min_df、max_df 过滤罕见词与无处不在的词;用 CountVectorizer(不是 TfidfVectorizer),因为 LDA 要的是原始计数。
from bertopic import BERTopic topic_model = BERTopic( embedding_model="sentence-transformers/all-MiniLM-L6-v2", min_topic_size=15, verbose=True, ) topics, probs = topic_model.fit_transform(documents) info = topic_model.get_topic_info() print(info.head(20)) valid_topics = info[info["Topic"] != -1]["Topic"].tolist() for topic_id in valid_topics[:5]: print(f"topic {topic_id}: {topic_model.get_topic(topic_id)[:10]}")
Topic != -1 的过滤丢掉 BERTopic 的离群桶(HDBSCAN 聚不进去的文档)。min_topic_size 控制 HDBSCAN 的最小簇规模;BERTopic 库默认 10,本例为本节规模显式设 15。语料超过一万篇时,提到 50 或 100。
两种方法都输出话题词。问题在于这些词连不连贯。
gensim.models.CoherenceModel 配 coherence="c_v"。| 情形 | 选 |
|---|---|
| 短文本(推文、评论、标题) | BERTopic |
| 长文档、话题混合 | LDA |
| 无 GPU / 算力受限 | LDA 或 NMF |
| 需要文档级多话题分布 | LDA |
| 用 LLM 给话题打标签 | BERTopic(原生支持) |
| 资源受限的边缘部署 | LDA |
| 最大化语义连贯 | BERTopic |
最大的实战考量是文档长度。BERT 嵌入会截断;LDA 的计数对任意长度都工作。文档比嵌入模型上下文长时,要么分块再聚合,要么用 LDA。
2026 的栈:
gensim.models.LdaModel:经典生产 LDA,成熟、久经考验。sklearn.decomposition.LatentDirichletAllocation:实验用的简易 LDA。保存为 outputs/skill-topic-picker.md:
--- name: topic-picker description: Pick LDA or BERTopic for a corpus. Specify library, knobs, evaluation. version: 1.0.0 phase: 5 lesson: 15 tags: [nlp, topic-modeling] --- Given a corpus description (document count, avg length, domain, language, compute budget), output: 1. Algorithm. LDA / NMF / BERTopic / Top2Vec / FASTopic. One-sentence reason. 2. Configuration. Number of topics: `recommended = max(5, round(sqrt(n_docs)))`, clamped to 200 for corpora under 40,000 docs; permit >200 only when the corpus is genuinely large (>40k) and note the increased compute cost. `min_df` / `max_df` filters and embedding model for neural approaches also belong here. 3. Evaluation. Topic coherence (c_v) via `gensim.models.CoherenceModel`, topic diversity, and a 20-sample human read. 4. Failure mode to probe. For LDA, "junk topics" absorbing stopwords and frequent terms. For BERTopic, the -1 outlier cluster swallowing ambiguous documents. Refuse BERTopic on documents longer than the embedding model's context window without a chunking strategy. Refuse LDA on very short text (tweets, reviews under 10 tokens) as coherence collapses. Flag any n_topics choice below 5 as likely wrong; flag >200 on corpora under 40k docs as likely over-splitting.
doc_topic(行和为 1)与 topic_word(行和为 1),天然支持混合归属。min_topic_size 随语料调:BERTopic 默认 10,过万文档提到 50~100。下一节,我们换到「生成」一侧——进入「Transformer 前的文本生成」,看 n-gram、RNN/LSTM、seq2seq 这些前 Transformer 时代的方法如何一个 token 一个 token 地吐出文本。