简介:这份资源面向自然语言处理初学者与主题建模实践者,系统讲解BERTopic模型从原理到代码落地的完整路径。内容围绕BERT嵌入、UMAP降维、HDBSCAN聚类与主题表示等核心环节展开,并延伸至主题优化、可视化、层次主题模型与动态主题模型等进阶方向,帮助读者理解如何生成语义连贯、可解释性强的主题结果。压缩包共14个文件,约906KB,包含4个csv结果数据、3个py示例脚本、3个png可视化图表,以及txt、md说明文档等,覆盖数据样例、主题关键词热力图、文档主题分布与离线演示代码,便于对照运行与调参。目前已有151人学习。读者可借助示例脚本快速复现主题提取流程,结合参数调节建议与可视化输出排查聚类效果,并参考主题信息与关键词表理解模型输出结构,适合作为BERTopic入门与项目实践的参考材料。
1. BERTopic 模型教程与代码:从零把主题聚类跑通
你手上有一堆文本,可能是用户反馈、工单记录、论文摘要或者商品评论,想快速知道里面到底在聊什么。人工读当然可以,但几千条起步就不现实了。BERTopic 就是干这个的:它把 embedding 模型、降维、密度聚类和主题词提取串成一条流水线,输入一列文本,输出若干主题以及每个主题的关键词和代表文档。和 LDA 那种基于词频的玩法不同,BERTopic 先拿到句向量,再在向量空间里找簇,所以对语义相近但用词不同的文本更稳。这篇教程面向想直接上手代码的人,从环境安装到参数调优再到踩坑排查,每一步都给可复现的命令和脚本,新手能照着跑,熟手能直接拿去改自己的数据。
2. BERTopic 的流水线拆解与选型理由
2.1 四个组件各干什么
BERTopic 的默认流程是四步。第一步用 embedding 模型把每条文本编码成向量,默认是 sentence-transformers 的 all-MiniLM-L6-v2,输出 384 维。第二步用 UMAP 把高维向量降到低维,默认降到 5 维,目的是让密度聚类在低维空间里更有效。第三步用 HDBSCAN 做密度聚类,它不需要预先指定簇数量,还能把不属于任何簇的点标成 -1,也就是离群点。第四步用 c-TF-IDF 给每个簇算主题词,把簇内高频且跨簇低频的词挑出来作为主题表示。
这套组合的好处是模块可替换。embedding 模型可以换成更强的多语言模型,降维可以换 PCA,聚类可以换 KMeans,主题表示可以换 KeyBERT。选型时先问自己三个问题:文本是中文还是多语言?数据量是几百条还是几十万条?要不要在线增量更新?答案不同,组件选择就不同。
2.2 为什么默认参数不能直接上生产
默认参数在小规模英文数据上表现不错,但直接套到中文或长文本上经常翻车。all-MiniLM-L6-v2 对中文的语义区分度有限,UMAP 的 n_neighbors 默认 15 在数据量少于 500 条时会过度关注局部结构,HDBSCAN 的 min_cluster_size 默认 10 在短文本场景下会把很多本应成簇的点判成离群。所以第一步不是跑默认,而是先看数据规模和语言,再决定换哪个组件、调哪几个参数。
2.3 环境安装与最小可运行代码
先建虚拟环境,再装依赖。Python 版本建议 3.9 到 3.11,太低或太高都可能遇到编译问题。
python -m venv bertopic_env source bertopic_env/bin/activate # Windows 用 bertopic_env\Scripts\activate pip install bertopic sentence-transformers umap-learn hdbscan scikit-learn pandas装完后跑一个最小示例,确认整条链路能通。
from bertopic import BERTopic from sklearn.datasets import fetch_20newsgroups # 取一小批英文新闻做冒烟测试 docs = fetch_20newsgroups(subset='train', categories=['sci.space'], remove=('headers', 'footers', 'quotes'))['data'][:200] topic_model = BERTopic(language="english", calculate_probabilities=False, verbose=True) topics, probs = topic_model.fit_transform(docs) # 打印主题概览 print(topic_model.get_topic_info().head(10))这段代码做了三件事:加载数据、初始化 BERTopic、拟合并输出主题信息。language="english"会启用英文停用词,calculate_probabilities=False在数据量大时能省不少时间。get_topic_info()返回的表格里,Topic 列是主题编号,-1 是离群点,Count 是文档数,Name 是主题词拼接。如果 -1 占比超过 40%,说明聚类太碎,需要调 UMAP 或 HDBSCAN 参数。
2.4 中文场景的组件替换
中文文本不能直接用默认英文模型。常见做法是换成paraphrase-multilingual-MiniLM-L12-v2或者BAAI/bge-small-zh-v1.5。后者在中文语义相似度上表现更稳,但需要确认 sentence-transformers 版本支持。
from sentence_transformers import SentenceTransformer from bertopic import BERTopic from umap import UMAP from hdbscan import HDBSCAN # 换中文 embedding 模型 embedding_model = SentenceTransformer("BAAI/bge-small-zh-v1.5") # 调整 UMAP 参数,数据量小时降低 n_neighbors umap_model = UMAP(n_neighbors=10, n_components=5, min_dist=0.0, metric='cosine', random_state=42) # 调整 HDBSCAN,短文本场景降低 min_cluster_size hdbscan_model = HDBSCAN(min_cluster_size=5, metric='euclidean', cluster_selection_method='eom', prediction_data=True) topic_model = BERTopic( embedding_model=embedding_model, umap_model=umap_model, hdbscan_model=hdbscan_model, language="chinese", calculate_probabilities=False, verbose=True ) topics, probs = topic_model.fit_transform(chinese_docs)这里n_neighbors=10比默认 15 更关注局部,适合几百到几千条的数据。min_dist=0.0让 UMAP 输出更紧凑,有利于密度聚类。min_cluster_size=5是短文本的常用起点,如果主题太碎可以加到 10 或 15。metric='cosine'对句向量更合适,因为句向量的方向比绝对距离更有意义。
3. 参数调优与主题质量评估
3.1 UMAP 的三个关键参数
UMAP 决定降维后的空间结构,直接影响聚类结果。n_neighbors控制局部和全局的平衡,值越小越关注局部,值越大越关注全局结构。数据量 500 以下建议 5 到 10,500 到 5000 建议 10 到 15,超过 5000 可以试 15 到 30。n_components是降维后的维度,默认 5,降到 2 到 3 会损失信息但聚类更快,升到 10 到 15 保留更多结构但 HDBSCAN 可能变慢。min_dist控制点之间的最小距离,0.0 最紧凑,0.1 到 0.5 更松散。中文短文本我一般用n_neighbors=10, n_components=5, min_dist=0.0。
3.2 HDBSCAN 的 min_cluster_size 怎么定
min_cluster_size是最小簇大小,小于这个数的点会被判为离群。它没有万能值,取决于你希望主题多细。经验做法是先跑一遍看get_topic_info()里 -1 的占比,如果超过 50%,说明太多点被丢弃,调小min_cluster_size;如果主题数量超过文档数的十分之一,说明太碎,调大。另一个参数min_samples控制核心点的邻域大小,值越大越保守,离群点越多。短文本场景我通常先设min_cluster_size=5, min_samples=3,再根据结果微调。
3.3 主题质量怎么判断
不能只看关键词是否通顺,还要看主题一致性和区分度。常用做法是人工抽检每个主题的前 10 篇代表文档,看它们是否真的在聊同一件事。BERTopic 提供get_representative_docs()可以拿到每个主题的代表文档。另外可以算主题间的余弦相似度,如果两个主题的关键词高度重叠,说明聚类过细,需要合并或调参。
# 查看每个主题的代表文档 rep_docs = topic_model.get_representative_docs() for topic_id, docs in list(rep_docs.items())[:5]: print(f"Topic {topic_id}:") for d in docs[:2]: print(" -", d[:100]) print() # 主题间相似度矩阵 import numpy as np topic_embeddings = topic_model.topic_embeddings_ if topic_embeddings is not None: from sklearn.metrics.pairwise import cosine_similarity sim_matrix = cosine_similarity(topic_embeddings) print("主题间相似度矩阵形状:", sim_matrix.shape)get_representative_docs()返回的是每个主题最典型的文档,抽检时重点看这些。topic_embeddings_是主题向量,算余弦相似度可以快速发现冗余主题。如果两个主题相似度超过 0.8,考虑用topic_model.merge_topics()合并。
3.4 用 c-TF-IDF 调整主题词
默认的 c-TF-IDF 有时会给出太泛的词。可以通过ClassTfidfTransformer调整bm25_weighting和reduce_frequent_words。bm25_weighting=True会降低高频词的权重,reduce_frequent_words=True会进一步压制跨主题常见词。
from bertopic.vectorizers import ClassTfidfTransformer ctfidf_model = ClassTfidfTransformer(bm25_weighting=True, reduce_frequent_words=True) topic_model = BERTopic( embedding_model=embedding_model, umap_model=umap_model, hdbscan_model=hdbscan_model, ctfidf_model=ctfidf_model, language="chinese", verbose=True )这两个参数对中文尤其有用,因为中文里“我们”“可以”“这个”这类词很容易霸占主题词位置。开启后主题词会更聚焦在区分性强的词上。
4. 避坑与常见问题排查
4.1 离群点占比过高
现象:get_topic_info()里 Topic -1 的 Count 占总文档数一半以上。原因通常是min_cluster_size太大、UMAPn_neighbors太大导致局部结构被抹平,或者 embedding 模型不适合当前语言。解决:先把min_cluster_size降到 3 到 5,再把n_neighbors降到 5 到 10,如果还不行就换 embedding 模型,中文优先试BAAI/bge-small-zh-v1.5或paraphrase-multilingual-MiniLM-L12-v2。
4.2 主题数量爆炸
现象:主题数接近文档数,每个主题只有几篇文档。原因通常是min_cluster_size太小或 UMAPn_components太高。解决:把min_cluster_size调到 10 以上,n_components降到 3 到 5,同时检查数据里是否有大量重复或极短文本,这些会干扰聚类。
4.3 中文主题词全是停用词
现象:主题词里出现“的”“了”“是”“我们”等。原因是没有正确设置中文停用词或 c-TF-IDF 没有压制高频词。解决:在BERTopic初始化时传language="chinese",并开启ClassTfidfTransformer(bm25_weighting=True, reduce_frequent_words=True)。如果还不够,可以自定义停用词列表传给CountVectorizer。
from sklearn.feature_extraction.text import CountVectorizer vectorizer_model = CountVectorizer(stop_words=["的", "了", "是", "我们", "可以", "这个", "那个"], ngram_range=(1, 2)) topic_model = BERTopic( embedding_model=embedding_model, umap_model=umap_model, hdbscan_model=hdbscan_model, vectorizer_model=vectorizer_model, language="chinese", verbose=True )ngram_range=(1, 2)让主题词可以包含双字词,对中文更友好。停用词列表按你的数据补充,不用一次到位,跑一遍看结果再迭代。
4.4 拟合速度太慢
现象:几千条数据跑十几分钟甚至更久。原因通常是 embedding 模型太大、UMAPn_components太高或calculate_probabilities=True。解决:换更小的 embedding 模型,n_components降到 3 到 5,关掉calculate_probabilities。如果数据超过十万条,考虑先用 MiniBatchKMeans 做粗聚类,再对每个簇跑 BERTopic。
4.5 增量更新后主题错乱
现象:用partial_fit更新模型后,主题编号和关键词全变了。原因:BERTopic 的增量更新会重新拟合 UMAP 和 HDBSCAN,旧主题不一定保留。解决:如果必须增量,用topic_model.update_topics()只更新主题表示,不重新聚类;或者把新旧数据合并后整体重跑,保证一致性。
5. 进阶技巧:用滑动窗口和层次主题做动态分析
数据带时间戳时,静态主题模型会丢掉时间维度。BERTopic 支持topics_over_time,可以按时间窗口看主题热度变化。常见做法是把文档按天或周分组,用topics_over_time生成每个时间窗口的主题分布,再画趋势图。
import pandas as pd # 假设 docs 是文本列表,timestamps 是对应的时间戳列表 timestamps = pd.to_datetime(timestamps) topics_over_time = topic_model.topics_over_time(docs, timestamps, nr_bins=20) # 查看某个主题随时间的变化 topic_model.visualize_topics_over_time(topics_over_time, topics=[0, 1, 2])nr_bins=20把时间轴分成 20 个窗口,窗口太少看不出变化,太多会有很多空窗。如果数据量不大,建议按周或月聚合,不要按天。另一个进阶用法是层次主题,用topic_model.hierarchical_topics(docs)生成主题树,再visualize_hierarchy()看哪些主题可以合并。这对主题数量多、需要归类的场景很有用。
hierarchical_topics = topic_model.hierarchical_topics(docs) fig = topic_model.visualize_hierarchy(hierarchical_topics=hierarchical_topics)层次主题的输出是一棵树,叶子是原始主题,内部节点是合并后的父主题。如果两个主题在树上的距离很近,说明它们语义相近,可以考虑合并。我一般会先跑层次主题,再决定是否用merge_topics精简主题数量。
最后说一个我踩过的坑:不要一上来就调参,先把 embedding 模型选对。中文数据用英文模型,后面怎么调都是白费。另一个习惯是每次跑完先看 -1 占比和主题数,这两个指标正常了再去看关键词质量。希望帮到你。
本文还有配套的精品资源,点击获取