简介:面向自然语言处理初学者与文本分析开发者,这份代码资源系统讲解如何用BERTopic进行主题建模。与传统模型相比,BERTopic借助预训练模型捕捉词语间的语义关联,通过文本嵌入、降维、聚类和主题表示等步骤,生成语义连贯且可解释的主题结果,弥补了传统主题模型忽略语义的不足。
资源共14个文件,压缩包只有906KB,包含三个Python脚本、四个CSV结果文件、三张可视化图表,以及文本示例、说明文档、依赖列表和在线运行配置等。脚本覆盖离线演示与交互式Demo,CSV保存主题词和文档分布,图片展示主题热力图与分布趋势,便于对照学习。
目前已有149人学习使用。整个资源从数据处理到参数调节都有对应代码,尤其适合想快速上手BERTopic、或需要现成实验模板的开发者。除了基础流程,还涉及主题优化、层次主题模型和动态主题模型等进阶内容,可帮助读者从入门走向实际应用。
1. 为什么是BERTopic——从LDA到预训练时代的主题建模
做文本挖掘的朋友应该都有这种体会:主题建模这个需求看着简单,真正落地却处处是坑。早些年主流方案是LDA(Latent Dirichlet Allocation),跑起来倒是快,但效果嘛,说实话有点看运气——词典里没见过的词会被直接忽略,短文本上表现尤其糟糕,而且你得提前指定主题数量,调参调到怀疑人生。
后来出现了NMF、LSA这些矩阵分解类方法,在特定场景下能用,但本质上还是在“词袋”层面上做文章,根本抓不住“苹果手机”和“iPhone”这种同义表达的联系。直到BERT这类预训练语言模型普及之后,文本表示的能力大幅提升,主题建模这个方向才算真正有了质变。
BERTopic就是在这个背景下出现的。它不搞传统的词袋统计那一套,而是先用预训练模型把每篇文档转成语义向量,再做聚类,最后用类内关键词和代表性句子来刻画每个主题。我第一次用它跑了一个几万条的电商评论数据集,跑完一看聚类结果,很多主题划分得极其准确——比如“物流慢”和“快递延误”被分到了同一类,这在LDA时代几乎不可能做到。
这篇文章我打算从原理讲到代码落地,最后再分享几个我实际踩过的坑。不管你是刚接触NLP的新手,还是已经在用LDA想换方案的老手,看完应该都能直接上手跑起来。
2. 核心思路拆解——BERTopic到底做了什么
2.1 四个核心步骤:嵌入、降维、聚类、主题表示
BERTopic的完整流程可以拆成四步,理解了这四步,后面看代码就是顺水推舟的事。
第一步是文档嵌入(Embedding)。这一步把每篇文档转换成固定维度的向量。最常用的做法是用sentence-transformer系列模型,比如all-MiniLM-L6-v2这种轻量模型,几百毫秒就能处理一篇文档,效果也很稳。当然你完全可以用别的方式生成嵌入向量——哪怕是用OpenAI的API、或者自己微调的模型,都行。BERTopic在设计上对嵌入这步很宽容,它只关心“你有向量就行”。
第二步是降维。高维向量直接聚类,效果和速度都差得远,所以一般会用UMAP把嵌入向量压缩到5维左右。这里注意,不需要太高,5维是我实测下来效果和性能比较平衡的配置。
第三步是聚类。BERTopic默认用HDBSCAN,这玩意儿最大的优点是你不用预先指定类别数量。文档密集的地方就是一个簇,疏离的地方就是噪声点。所以跑完之后你会看到-1这个标签,代表“不属于任何主题”的离群文档。
第四步是主题表示。每个簇聚好之后,BERTopic用c-TF-IDF(class-based TF-IDF)来提取每个簇的关键词——把每个簇的所有文档拼成一个大文档,再计算词频,但相比普通TF-IDF多了一步:它会把“其他所有类”视为背景语料,突出本类特有的词。这就保证了提取出来的词确实是这个主题的标志性表达,而不是全局高频的无意义词。
2.2 BERTopic和LDA的本质区别
用一句话概括:LDA是“生成式”的,它假设每篇文档由若干主题按概率混合生成;BERTopic是“判别式”的,它先看文档之间的语义距离,再根据距离抱团。
这带来的实际差异非常明显:
- LDA需要指定主题数,BERTopic自动决定主题数。
- LDA对短文本、口语化文本效果差,BERTopic因为用了预训练模型,对这类文本适应性强很多。
- LDA提取的主题词经常是“你好”“谢谢”这种高频词,BERTopic提取的词有明确的语义区分度。
- BERTopic还支持主题间的层次关系、主题演化分析、以及用LLM做主题总结,扩展性强了不是一星半点。
当然,BERTopic也不是没有代价——它需要跑模型,速度和资源开销比LDA大很多。后面我会专门讲怎么优化这块。
3. 环境准备与最小跑通示例
3.1 安装和依赖匹配
我把安装步骤放在最前面,是因为这一块我踩过太多次坑了。BERTopic的依赖链比较长,核心包括numpy、pandas、scikit-learn、umap-learn、hdbscan、sentence-transformers。如果你用的是Anaconda,建议先创建一个干净的环境:
conda create -n bertopic_env python=3.9 -y conda activate bertopic_env pip install bertopic这里有个细节:BERTopic的版本更新比较快,不同版本之间的API有差异。我写这篇文章时用的版本是0.16.x,如果你看到报错类名不存在或者参数名对不上,先检查一下版本:
pip show bertopic如果版本不一致,建议直接安装和我一致的版本,减少不必要的麻烦:
pip install bertopic==0.16.23.2 最小可运行的代码
我准备了一个非常小的示例,用sklearn自带的“20类新闻组”数据集,这数据不用另外下载,本地就能跑通全流程。
from sklearn.datasets import fetch_20newsgroups from bertopic import BERTopic # 只取其中几类新闻,降低运行时间 subset = fetch_20newsgroups(subset="all", categories=[ "sci.space", "rec.autos", "comp.graphics" ], remove=("headers", "footers", "quotes")) docs = subset.data # 初始化模型并训练 topic_model = BERTopic(verbose=True) topics, probs = topic_model.fit_transform(docs) # 查看生成的主题 topic_info = topic_model.get_topic_info() print(topic_info.head(10))get_topic_info()返回的是一个Pandas DataFrame,里面每一行对应一个主题,包含主题ID、文档数量、代表词等。你跑完之后大概率会看到类似这样的输出:
Topic Count Name Representation 0 245 3d_4_development_software_work ['3d', 'image', 'software', 'work', 'development'] -1 78 -1_unknown [] 1 121 space_launch_orbit_satellite_nasa ['space', 'launch', 'orbit', 'satellite', 'nasa']注意那个-1主题——这就是HDBSCAN认为不属于任何簇的噪声文档。如果你的数据集噪声多,-1的主题会非常大,后面我会讲怎么调整。
3.3 可视化初步观察
BERTopic自带的可视化函数是我非常喜欢的功能,处理完之后可以直接出图:
# 主题间距离图 topic_model.visualize_topics() # 主题关键词条形图 topic_model.visualize_barchart(top_n_topics=6)visualize_topics()这种交互式图表会以二维坐标展示主题之间的远近关系,鼠标悬停可以看到主题的词云和文档数量。visualize_barchart()则展示每个主题的Top关键词权重。这俩函数返回值是Plotly的Figure对象,如果你是jupyter notebook环境,直接写最后一行就能显示。
4. 核心参数详解与代码实操
4.1 嵌入模型的选择
如果文档是英文,直接用默认的all-MiniLM-L6-v2就行,轻量且效果好。但如果是中文文档,默认模型效果就一般了——它不太认识中文。
我在处理中文语料时,常用sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2或者中文专用的shibing624/text2vec-base-chinese。嵌入模型本身不一定要经过BERTopic,你可以提前算好向量,再传给模型:
from sentence_transformers import SentenceTransformer embedding_model = SentenceTransformer("shibing624/text2vec-base-chinese") embeddings = embedding_model.encode(docs, show_progress_bar=True) topic_model = BERTopic(embedding_model=embedding_model) topics, probs = topic_model.fit_transform(docs, embeddings=embeddings)这里有个经验:如果你的语料有几十万篇,一次性全部做嵌入会很费内存。建议分批量编码,或者用GPU跑encode。BERTopic的fit_transform接收预计算好的embeddings之后,会跳过内部嵌入过程,直接进入降维聚类阶段。
4.2 UMAP和HDBSCAN的关键参数
UMAP的参数里,最常用的就是n_neighbors和n_components。
n_neighbors控制局部结构和全局结构的平衡,值越小越关注局部细节,越大越关注全局结构。默认值是15,但如果你发现主题分得太碎,可以把值调大一点,比如到30或50。
from umap import UMAP umap_model = UMAP(n_neighbors=15, n_components=5, min_dist=0.0, metric="cosine", random_state=42) topic_model = BERTopic(umap_model=umap_model)min_dist控制降维后点的密集程度,0.0就是允许点之间靠得很近,这个默认配置在文本聚类里表现不错。
HDBSCAN的参数里,最核心的是min_cluster_size。这个参数的含义是“一个簇至少要有多少篇文档”,默认是10。如果你的语料量大、主题比较集中,可以提高到20甚至50;如果语料少或者主题很细碎,就降到5甚至3。
from hdbscan import HDBSCAN hdbscan_model = HDBSCAN(min_cluster_size=15, metric="euclidean", cluster_selection_method="eom") topic_model = BERTopic(hdbscan_model=hdbscan_model)cluster_selection_method有两个选项:eom(Excess of Mass)和leaf。前者倾向于得到较大且稳定的簇,后者倾向于细分的叶子簇。一般用默认的eom就行,文档量特别大且主题粒度很细时可以考虑leaf。
4.3 主题数控制
看到这里你可能会问:前面不是说BERTopic不需要指定主题数吗?没错,自动聚类确实不需要。但自动聚出来的主题经常有几十上百个,其中很多可能是某个大主题的碎片,这对实际分析很不利。
BERTopic提供了reduce_topics方法来合并相似主题:
# 先聚类,再合并 topic_model = BERTopic(verbose=True) topics, probs = topic_model.fit_transform(docs) # 合并相似主题,把主题数量控制在20个以内 reduced_model = topic_model.reduce_topics(docs, topics, nr_topics=20)这个方法内部会计算主题之间的相似度,不断合并最相似的两个主题,直到满足主题数量要求。我实际用下来的感受是:如果是做业务报告,20到30个主题比较合适;如果做探索性分析,可以先保留全部主题,再人工筛选感兴趣的簇。
4.4 自定义主题标签
跑完之后,主题名称默认是关键词1_关键词2_关键词3的格式,看起来像机器生成的,不够直观。你可以在get_topic_info()里直接改Name这一列,也可以在训练完模型后手动给它赋值:
topic_model.set_topic_labels({ 0: "太空探索", 1: "汽车工业", 2: "计算机图形学" })如果你用的是0.16版本,也可以直接修改topic_info后再用于后续展示。可视化的时候,这些自定义标签会直接显示在图上。
5. 实操过程中踩过的坑与排查方法
5.1 依赖冲突和版本不兼容
最常见的问题出现在hdbscan上,它在Windows上如果没装Microsoft C++ Build Tools,经常编译失败。解决方法是安装预编译的wheel包:
pip install hdbscan --only-binary :all:另外就是torch和sentence-transformers的CUDA版本要匹配,否则即使设置了device="cuda"也跑不起来。我在一台没有GPU的机器上跑过几万条中文语料,嵌入阶段花了将近半小时;同样数据在GPU上两三分钟就跑完了。所以如果语料规模比较大,还是建议用GPU。
5.2 主题结果碎片化严重
如果你的主题数量暴多,大部分主题只包含几篇文档,通常说明min_cluster_size设置得太小了。把这个值往上调,碎片化问题会立刻缓解。
如果调整参数后还是不行,检查一下文本预处理。BERTopic虽然不依赖分词,但过于冗长的文本也会导致向量区分度下降。我之前处理过一批用户反馈文本,IDF很高、停用词很多,聚出来的主题明显不理想。后来先做了一步轻量清洗——把纯数字、乱码符号、过短的句子过滤掉,效果立刻好了很多。
5.3 中文分词与效果优化
有朋友问过:BERTopic需不需要先做中文分词?严格来说,不需要。你直接把整句原文交给sentence-transformers做嵌入就行,分词是模型内部通过subword tokenization自行处理的。
但如果你是在c-TF-IDF阶段想显示更多有意义的关键词,可以传入自定义的向量化器,比如结合jieba分词的CountVectorizer:
import jieba from sklearn.feature_extraction.text import CountVectorizer def tokenize_zh(text): return list(jieba.cut(text)) vectorizer_model = CountVectorizer(tokenizer=tokenize_zh, ngram_range=(1, 2), stop_words="english") topic_model = BERTopic(vectorizer_model=vectorizer_model)这样提取出来的主题词会以中文词语为单位,而不是单个字或英文子词,会更符合阅读习惯。
5.4 大数据量下的性能优化
如果你要处理百万级别的文档,直接跑默认流程会非常慢,甚至内存溢出。我建议按如下顺序优化:
- 嵌入阶段用GPU,且设置批量大小,比如
embedding_model.encode(docs, batch_size=64)。 - UMAP阶段可以考虑
low_memory=True参数。 - 对聚类的输入做降采样——比如先随机抽5万篇跑一次,找到主题结构后,再通过
transform方法映射到全量数据上。 - 如果仍然太慢,可以绕开UMAP+HDBSCAN这套默认组合,用
kmeans作为聚类模型,速度会快一到两个数量级,代价是主题质量略降。
from sklearn.cluster import KMeans cluster_model = KMeans(n_clusters=30) topic_model = BERTopic(hdbscan_model=cluster_model)这种替代方案适合主题边界本来就很清晰的数据集,比如新闻分类、产品评论,实测效果不差。
6. 实操心得与扩展建议
跑了好几个项目的BERTopic之后,我自己总结了几条经验:
第一个是关于“主题数”这件事的认知。BERTopic自动聚类给出的主题数,不代表真实业务上的主题数,它更多是“文本语义空间里的自然分割”。如果你的目的是做业务分析,建议先看高亮主题(文档数量排名靠前的),从这些里面提炼业务洞察,而不是上来就要求所有主题都干净清晰。
第二个是对噪声文档的态度。-1主题这些文档别急着丢弃,它们往往代表的是长尾问题、异常情况、或者说话方式很独特的人群。我在一个客服工单项目里,恰恰是-1主题里的文档暴露了一批严重故障的早期信号。你可以单独拎出来,用关键词提取或LLM总结再处理。
第三点是关于主题粒度的调整顺序。先跑一次默认参数,看结果,再决定是调大min_cluster_size还是调小。不要一开始就追求精细,先看全局结构,再逐步细化,效率会高很多。
此外,BERTopic官方还在持续迭代,比较新的一些版本已经支持了基于GPT等大语言模型的主题总结功能。你可以把每个主题的代表性文档用LLM自动生成一句话总结,输出给非技术同事看,他们一下子就能明白你分析出了什么。
就分享到这把。这个工具看起来复杂,一旦跑通基础流程,后面不管是做舆情分析、用户反馈挖掘,还是科研文献综述,都能省下大量时间。如果看完有什么问题,欢迎评论区交流,我尽量回复。
本文还有配套的精品资源,点击获取