
简介面向自然语言处理初学者与主题建模开发者的BERTopic教程代码包系统讲解如何利用BERT语义嵌入解决LDA等传统主题模型忽视词间语义关系的问题。压缩包共14个文件、约906KB包含3个可运行Python脚本含离线与交互式演示、4个CSV主题结果表、3张可视化图表、2个文本文件样例数据与依赖清单及说明文档结构清晰便于按需查阅。目前已有149人学习与下载参考。教程完整覆盖文本预处理、嵌入生成、UMAP降维、HDBSCAN聚类、主题表示与参数调节并延伸主题优化、可视化、层次主题模型和动态主题模型等进阶内容代码侧还给出了离线运行与交互调参的实现思路和注释。借助自带样例数据读者无需额外准备语料即可直观查看聚类分布与关键词热力图快速掌握BERTopic在真实文本上的完整应用流程。 把文本埋进主题的活儿最近一年我基本都交给BERTopic了。之前做舆情归因和用户反馈聚类用LDA一套老流程下来分词调停用词表调得人想摔键盘结果还是靠关键词硬凑主题换个语料就崩。后来试了transformer系列的语义向量配合聚类来做主题挖掘效果直接上了一个台阶文本语义相近的内容能自动聚成有解释性的主题簇而不用再抠字面词。这篇就把我用BERTopic从入门到落地踩过的坑、验证过的代码、调参心得完整整理出来。这套流程解决的实际问题简单说就是你有一堆无标注文本想自动把它们分成若干个主题并且每个主题还能给出一组让人看得懂的关键词和主题名。BERTopic不是简单把LDA换了个库而是把预训练语言模型、降维、聚类、词加权四步串联成一条流水线每一步都可以插拔替换灵活性非常高。适合做舆情分析、客服工单分类、论文综述题录分析、市场调研开放题归纳以及任何需要给无标注文本快速出结构化主题的场景。我会把从零开始跑通模型的完整代码、每个参数的作用和推荐配置、中文场景下的特殊处理、以及我在实际项目中遇到的典型报错和排查方案全部写出来。有基础的可以直接跳到第3节看完整代码新手建议顺着读每一步都能落地。1. BERTopic到底解决了什么问题1.1 传统LDA的三大硬伤LDA隐含狄利克雷分配是老牌主题模型把每篇文档看成若干个主题的混合分布每个主题看成词的混合分布。但BNG词袋表示天生的三个问题在今天的文本场景里越来越明显。第一是分词敏感。英文有天然空格中文必须切词切得不好同一个意思的词会被劈成两半主题质量断崖式下跌。第二是语义缺失。“苹果”和“iPhone”在词袋里毫无关联但语义上高度相关“价格贵”和“性价比低”字面完全不像但表达的其实是同一类用户情绪。LDA拿不到这层信息。第三是主题数不好定。LDA需要预先指定主题个数KK给大了出来一堆噪声主题K给小了两个主题硬挤一堆调K的过程既主观又耗时间。我过去做一批电商评价的归因分析用LDA定K值试了六轮每次调完都要重新跑一遍全流程半天时间基本就耗在反复调参和人工判断上了而且出来的主题还经常是“价格”“质量”“物流”这种字面词拼凑看的人得靠自己的生活经验脑补归类。1.2 BERTopic的完整工作流水线BERTopic把主题建模拆成了四个环节每个环节都能独立替换第一步把每篇文档用预训练语言模型编码成语义向量。这个过程叫embedding本质是把一段变长文本压缩成一个固定维度的稠密向量向量在空间里的距离反映了语义相似度。第二步用UMAP做降维。语义向量通常是768维或更高直接聚类在高维空间效果差、速度慢UMAP可以在尽量保持局部结构的前提下把向量压到二维到低维空间。第三步用HDBSCAN做密度聚类。它不需要预先指定簇的个数能自动把紧密的样本聚在一起同时把远离任何簇的孤立点标记为离群点这点在实际业务里非常有用——总有那么一些牛头不对马嘴的文本与其硬塞进某个主题误导判断不如让它单独流出来人工看。第四步对每个簇做c-TF-IDF加权提取这个簇区别于其他簇的高权重词作为主题的关键词方便人工定性。这套设计和传统方法最大的区别在于“语义”和“自动”。语义向量保证了相似内容哪怕用词完全不同也能被拉近距离HDBSCAN自动决定簇的个数加上离群点机制整个建模过程对人工干预的要求大幅度下降。1.3 与主流主题模型方案对比我在项目前期做过一轮方案选型横向对比过几类模型这里直接给结论。模型表示方式是否需指定主题数离群点处理语义理解可解释性适用场景LDA词袋是无弱中传统短文本、需强解释性的合规场景NMFTF-IDF矩阵分解是无弱中词汇特征明显的长文本Top2VecDoc2Vec/句向量否无较强中快速粗分类BERTopicTransformer语义向量否可控制有强高无标注文本挖掘、语义相近归因如果一个项目对可解释性要求极高、且文本用词高度规范化LDA仍然有价值毕竟它原理透明生成的主题词表可以直接拿去审计。但如果是处理真实世界的开放文本同样一段话可以有十种说法BERTopic是更省心的选择。2. 环境准备跑通第一个BERTopic模型2.1 安装依赖先说明环境我用的Python版本是3.9操作系统是Ubuntu Server显卡是RTX 3090。BERTopic本身不强制要求GPU纯CPU也能跑但做embedding时GPU能快五到十倍。安装命令简单直接pip install bertopic但这条命令默认会装PyTorch的CPU版。如果机器有NVIDIA显卡且已经装好CUDA建议先自己装GPU版PyTorch再装BERTopicpip install torch --index-url https://download.pytorch.org/whl/cu118 pip install bertopic安装过程中常见一个问题umap-learn编译时会依赖numba和llvmlite这两个包对Python版本比较敏感如果Python版本太高比如3.12可能没有匹配的wheel包导致安装失败。我的建议是直接用3.9或3.10踩坑最少。2.2 最简示例代码装完库之后先用一个最小的例子跑通全流程代码不到20行from bertopic import BERTopic from sklearn.datasets import fetch_20newsgroups # 1. 加载演示数据 docs fetch_20newsgroups(subsetall, remove(headers, footers, quotes))[data] # 2. 创建模型默认使用all-MiniLM-L6-v2做embedding topic_model BERTopic() # 3. 训练 topics, probs topic_model.fit_transform(docs) # 4. 查看生成的几个主题 topic_model.get_topic_info().head(10)这段代码里的执行过程值得说清楚第一次运行的时候BERTopic会自动去下载一个约80MB的sentence-transformer模型文件网络通畅的话一两分钟内能下完然后对所有文档做embedding再降维聚类最后提取主题词。get_topic_info()返回的DataFrame是核心产物每一行是一个主题其中Topic列是主题编号Count是包含的文档数Name是根据主题词自动拼接的名字Representation是主题关键词列表。2.3 第一次运行时模型的下载问题如果你在国内网络环境上面这段代码大概率会在下载模型那一步卡住或者直接抛超时异常。这个坑我踩过不止一次解决方法是设置HuggingFace的镜像源export HF_ENDPOINThttps://hf-mirror.com或者在代码里设置import os os.environ[HF_ENDPOINT] https://hf-mirror.com设置完再重新运行模型就能正常下载了。这里要强调一个习惯模型下载之后通常缓存在~/.cache/huggingface目录里同一台机器第二次运行一般不会重新下载所以第一次把这个下载问题解决掉后续所有实验都会顺畅很多。3. 中文文本实战接口文档主题聚类3.1 数据准备与预处理用英文示例跑通流程只能算第一步实际工作中中文才是大头。中文场景和英文有一个显著差异中文预训练模型和英文模型在分词方式、训练语料上完全不同直接默认配置往往效果一般。我以最近做的一个银行接口文档自动分类项目为例数据是从内部系统导出的接口文档摘要大概有4000多段文本每段描述一个接口的功能和用途。目标是把这些接口文档自动归类为交易类、账户类、风控类、营销类等若干主题。预处理阶段我做了三件事删除HTML标签、统一全角半角符号、删除连续的大写字母和数字串比如接口编号ID_MER_2301但不做停止词删除。BERTopic的c-TF-IDF是依赖词频差异来区分主题的保留一些常见词反而能帮助区分不同语境。中文切词也不需要单独做BERTopic内部使用的sentence-transformer模型会使用它自己的分词器完成文本切分和向量化这一步对用户是透明的。3.2 完整建模代码直接上核心代码。中文场景下需要替换embedding模型我这里用的是shibing624/text2vec-base-chinese在中文语义相似度任务上表现稳定模型容量也不算大大约400MBimport os os.environ[HF_ENDPOINT] https://hf-mirror.com import pandas as pd from sentence_transformers import SentenceTransformer from bertopic import BERTopic # 1. 准备中文文档列表 df pd.read_csv(interface_docs.csv) docs df[description].astype(str).tolist() # 2. 使用中文embedding模型 embedding_model SentenceTransformer(shibing624/text2vec-base-chinese) # 3. 配置BERTopic topic_model BERTopic( embedding_modelembedding_model, min_topic_size10, # 主题最少包含的文档数过滤噪声主题 top_n_words8, # 每个主题显示的关键词数量 calculate_probabilitiesTrue # 计算文档属于每个主题的概率 ) # 4. 训练 topics, probs topic_model.fit_transform(docs) # 5. 输出主题信息 topic_model.get_topic_info().to_csv(topics_info.csv, indexFalse)输出结果里一共有37个主题其中编号为-1的是离群点主题包含208个文档其余36个主题覆盖了剩下95%的数据。看主题词我印象深刻的是某个主题的关键词是“交易流水、查询、明细、翻页、下载”一眼就能定性是交易流水查询类接口另一个主题的关键词是“提现、到账、手续费、银行卡”就是提现服务的接口说明。这种结果的直观度比LDA用词袋跑出来的效果要清晰得多。3.3 结果可视化与主题解读BERTopic自带几类可视化代码简单但信息量很大# 主题降维分布图 topic_model.visualize_documents(docs, embeddingsembedding_model.encode(docs)) # 主题词条形图 topic_model.visualize_barchart() # 主题间相似度热力图 topic_model.visualize_heatmap()visualize_documents会输出一个交互式HTML文件每个点代表一篇文档颜色代表不同主题点与点之间的距离反映语义相似程度。做业务汇报的时候这个图非常加分可以直接在浏览器里放大、悬停查看具体文档。热力图也很有用它用余弦相似度算出主题间的相似程度。如果两个主题之间相似度数值很高说明这两个主题本质上可能该合并。我在一次跑完数据后发现“账户冻结”和“账户锁定”两个主题相似度高达0.87人工合并之后主题结构更清晰这比从头训练一次模型要省事得多。4. 参数调优与主题数控制的实战经验4.1 embedding模型怎么选BERTopic的embedding环节决定了整个模型的上限。默认配置用的是all-MiniLM-L6-v2这是一个通用英文模型维度384维速度快但中文语义理解不是它的强项。中文场景下我实测过几个模型可以给出一手使用感受。shibing624/text2vec-base-chinese是我最常用的一个针对中文优化过语义相似度任务效果稳定缺点是显存占用偏大对长文档的截断处理比较保守文档超过512个token时尾部信息会丢uer/sbert-base-chinese-nli是另一个选择基于自然语言推理任务训练出来的模型对语义对错更敏感但编码速度慢一些如果硬件资源有限可以考虑shibing624/text2vec-base-chinese-paraphrase速度更快精度略有下降。配置方式很简单只要把SentenceTransformer的模型名传给BERTopic就行。这里还有个小技巧可以用encode_kwargs{batch_size: 32, show_progress_bar: True}来优化显存和进度显示embedding_model SentenceTransformer(shibing624/text2vec-base-chinese) embedding_model.encode_kwargs {batch_size: 32, show_progress_bar: True}4.2 UMAP与HDBSCAN参数的心得UMAP和HDBSCAN是BERTopic内部默认使用的降维和聚类算法但说实话默认参数在中文长文本场景下不一定最优。UMAP的核心参数是n_neighbors控制局部结构保留程度、min_dist控制点与点之间最小距离、metric距离度量。HDBSCAN的核心参数是min_cluster_size簇的最小规模、min_samples判断核心点时的邻域样本数。实际项目里我的调优心得是n_neighbors控制在5到30之间值越小局部结构越明显主题边界越锐利但是太大容易把所有点揉成一团min_dist用0.0到0.1之间顺滑度更好可视化效果更美观但是太小会过于聚焦局部干扰聚类稳定性。HDBSCAN的min_cluster_size我通常设置成总文档数的0.5%到1%这个参数直接决定了小主题会不会被拆出去当作离群点。改造方式如下from umap import UMAP from hdbscan import HDBSCAN umap_model UMAP(n_neighbors15, n_components5, min_dist0.0, metriccosine, random_state42) hdbscan_model HDBSCAN(min_cluster_size30, metriceuclidean, cluster_selection_methodeom) topic_model BERTopic( embedding_modelembedding_model, umap_modelumap_model, hdbscan_modelhdbscan_model )顺便说明一下n_components这个参数是UMAP降维的目标维度默认是5不是必须降到2D。HDBSCAN在高维空间直接聚类效果一般降到5维左右是一个兼顾保留结构和计算效率的经验值。可视化的时候BERTopic会再单独降一次到2D所以不用担心中间维度看不到点阵分布。4.3 如何控制最终主题数量HDBSCAN自动决定主题数但自动出来的主题数不一定符合业务预期。如果自动聚成50个主题你想收成20个有三种常用的收口办法。第一种是用BERTopic的nr_topics参数这是一种粗略收敛方式原理是把小主题合并到相似度最高的大主题里topic_model BERTopic(nr_topics20)第二种是先训练再合并使用merge_topics方法传入一组你想要合并的主题编号topics, probs topic_model.fit_transform(docs) # 找到相似度极高的主题对后手动合并 topic_model.merge_topics(docs, topics_to_merge[(1, 7), (2, 9)])第三种做法是直接调大min_cluster_size让HDBSCAN在聚类阶段就把小额簇吸收到大簇里这个方式最干净因为是在源头控制不会出现“先拆散了再硬凑回来”的别扭感。我个人最推荐第三种配合热力图判断相似主题再针对性地做一次小范围合并。5. 常见报错与排查速查实际跑BERTopic的过程中真正常见的不是算法问题而是环境工程问题。整理了一份踩坑速查表按遇到频率排序现象可能原因解决办法下载模型一直卡住或超时网络无法访问HuggingFace代码开头设置os.environ[HF_ENDPOINT] https://hf-mirror.comModuleNotFoundError: No module named hdbscanhdbscan安装失败可能是llvmlite与Python版本不匹配切换到Python 3.9/3.10重装pip install hdbscan显存不足CUDA out of memoryembedding批量编码时显存爆了调小encode_kwargs里的batch_size到8或16或用CPU做embedding聚类结果大量都是-1min_cluster_size设置过大或者embedding质量差调小min_cluster_size换更合适的中文embedding模型主题词看起来和业务无关数据里有大量噪声文本或极为通用的表述检查预处理删除重复模板文本、签名、编号UMAP降维时报AttributeErrorumap和numba版本不兼容重装pip install --upgrade umap-learn numba多次运行结果完全一样需要固定随机数种子给UMAP加random_state42BERTopic里也传seed长文档主题混杂严重超过512个token的文本被截断语义丢失先做摘要或分段后再建模或用支持长文本的embedding模型在报错排查里有一条特别想说min_cluster_size是控制离群点数量的阀门。我第一次用它处理一个有一万条文本的语料时参数设成了50结果有3500条文本全部变成了离群点主题信息大量丢失根本没法用。后来我把参数调到10离群点比例掉到了8%左右而主题总数只增加了5个效果完全不一样。这个参数本质上是“一个主题最少要聚到几个人才算数”设太高就是把大多数内容拒之门外设太低又会冒出大量碎片主题需要根据数据规模反复测试。6. 更高阶的玩法本地模型加载与模型融合6.1 自定义embedding加载本地模型很多实际项目里HuggingFace的通用模型未必满足领域需求比如做法律文书分析、医疗病历挖掘通用模型对领域术语的语义理解会很弱。这时候可以加载自己微调过的本地模型BERTopic的embedding模型是直接支持传入本地路径的embedding_model SentenceTransformer(./fine_tuned_text2vec_model) # 或者直接加载一个普通的transformer模型不经过sentence-transformers from transformers import AutoTokenizer, AutoModel tokenizer AutoTokenizer.from_pretrained(./local_model) model AutoModel.from_pretrained(./local_model) def embed_texts(texts): # 自定义编码函数手动池化并归一化 encoded tokenizer(texts, paddingTrue, truncationTrue, max_length512, return_tensorspt) with torch.no_grad(): outputs model(**encoded) embeddings outputs.last_hidden_state[:, 0, :] embeddings torch.nn.functional.normalize(embeddings, p2, dim1) return embeddings.numpy() topic_model BERTopic(embedding_modelembed_texts)这里的关键是自定义embedding函数必须接收一个字符串列表、返回一个numpy二维数组形状为[文档数, 向量维度]且每个向量最好做了归一化处理。HDBSCAN用距离度量归一化后的向量更稳定不容易某一个特征值把距离计算带偏。我试过直接把涓模型编码的最后一层[CLS]向量丢进来效果和完整句向量差不多但计算复杂度低不少。6.2 模型融合的判断与实操模型融合的思路也很自然如果单一embedding模型对某种语义还不够敏感可以把两个模型对同一批文档的编码结果做拼接或加权平均让后面的聚类环节把两个视角的信息同时利用起来。通俗说就像两个人看同一张图一个关注整体轮廓一个关注局部纹理两个人给的结论综合起来肯定比单人视角更稳定。一个稳妥的融合方式是先跑通两个embedding模型各自的编码结果再验证拼接后是否提升主题内部一致性。比如把text2vec-base-chinese的768维向量和另一个模型的向量横向拼接成1536维再喂给UMAPimport numpy as np embeddings_a SentenceTransformer(shibing624/text2vec-base-chinese).encode(docs) embeddings_b SentenceTransformer(shibing624/text2vec-base-chinese-paraphrase).encode(docs) embeddings_fused np.hstack([embeddings_a, embeddings_b]) topic_model BERTopic(embedding_modellambda x: embeddings_fused[doc_index_map[x]])上面代码里按文档顺序索引即可不需要真的写成lambda直接用能返回完整二维数组的函数就行。实测下来拼接两个结构差异较大的模型效果最好比如一个基于词向量、一个基于transformer如果两个模型本身就是近亲融合带来的增益很小。6.3 交换词表与主题命名优化BERTopic默认的主题名是直接把关键词用下划线拼起来比如交易流水查询_明细_翻页可读性不算差但不够专业。给主题起名可以用LLM生成BERTopic内置了llm参数本质上是把每个主题的关键词和示例文档交给LLM让它给一个6到8个字以内的短命名from bertopic.representation import OpenAI representation_model OpenAI( modelgpt-4o-mini, prompt请根据以下主题关键词给出一个简短的中文主题名不超过6个字 ) topic_model BERTopic(embedding_modelembedding_model, representation_modelrepresentation_model)这个方法在内部还是用BERTopic自带的c-TF-IDF先得到关键词再送进LLM做概括所以不会破坏底层主题结构只是把展示层的命名优化了。注意调用第三方API时需要遵守本身的合规要求避免输入敏感数据。如果数据没法出内网也可以用本地部署的Qwen模型替代。6.4 小语料如何借力打力还有一种情况容易被忽略某些项目语料规模很小只有两三百篇文档直接跑BERTopic经常是几十篇全部堆进一个主题。两个优化方向非常有效。第一个方向是调小min_cluster_size到3到5给聚类算法更宽松的划定空间第二个方向是先生成更多样的文本表示比如用同义词替换对小文本做扩充后再编码。我试过把一批只有150篇的小语料用同义词替换扩充到500篇再重新训练效果明显提升而且因为扩充文本本身语义没变聚类结果不会跑偏。这个思路对短文本、冷启动场景非常适用可以作为自定义脚本先跑一遍再进入正式流程。写在最后实际项目里的一个建议我现在固定下来的一套配置是中长文本用text2vec-base-chinese做embeddingmin_cluster_size设为总文档数的0.5%到1%n_neighbors设为15UMAP降到5维可视化时才降到2D离群点比例控制在10%以内。这套配置在银行接口文档、用户反馈、电商评论、法律文书摘要四个场景都验证过效果稳定。最后再分享一个小建议模型训练完以后把topic_model.get_topic_info()的结果、每篇文档的主题归属、以及主题间相似度矩阵一起存下来。因为业务方一定会问“某个主题下具体有哪些文本”先准备好结果就能直接回答不用临时重跑模型。BERTopic在整个文本挖掘链条里只负责主题切分这一环但把它用扎实后续的归因分析、预警监控、报表展示都会省下大量时间。本文还有配套的精品资源点击获取