资讯详情 BERTopic主题建模实战教程:从原理到代码,告别LDA调参玄学
📅 2026/10/13 12:04:31
简介面向自然语言处理与主题建模学习者这份资源系统讲解BERTopic模型原理与实战。BERTopic融合BERT语义嵌入和传统主题模型可生成语义更连贯的主题教程覆盖文本预处理、嵌入生成、降维、聚类、主题表示等完整流程并给出参数调节与主题优化思路适合希望将语义主题模型用于文本分析的中级开发者也可用于舆情分析、文献挖掘等场景。压缩包共14个文件以Python脚本、CSV结果、PNG可视化图和Markdown说明为主包含可运行的离线与交互式演示代码、示例文本、依赖清单及输出结果压缩包仅906KB。目前已有152人学习下载。读者可借助演示代码快速复现主题分析并通过结果表格与关键词热力图、主题分布图直观理解模型输出教程还涉及层次主题模型与动态主题模型为深入应用提供参考。1. BERTopic模型教程让主题建模不再依赖调参玄学做文本主题建模的人大多被LDA折磨过预处理停用词表调了半天主题还是混成一团困惑度降下去了可结果根本没法看。BERTopic这类基于嵌入和聚类的新一代主题模型把主题发现从统计推断变成了一步向量化加聚类的流水线你能直观看到每个主题的代表性文档和关键词翻车了也能定位到具体环节。这篇教程面向想尽快在自有语料上跑出可用主题的从业者会从原理讲清楚为什么它比LDA稳再给出一套可以直接改着用的完整代码最后把最常踩的几个坑提前给你排掉。2. BERTopic的模块化流程为什么它比LDA更像工程而不是玄学2.1 BERTopic的六步流水线每个环节都能单独替换BERTopic不是单个算法而是一条装配线。官方实现把流程拆成了六个环节Embedding文本向量化、UMAP降维、HDBSCAN聚类、c-TF-IDF主题表征、主题归并与可选的自定义优化。理解这六步你才能真正掌控它而不是把它当黑匣子用。Embedding环节意思很直白把每篇文档变成一个向量。BERTopic默认用sentence-transformers里的all-MiniLM-L6-v2模型但我一般会换成中文语料更适合的paraphrase-multilingual-MiniLM-L12-v2。如果你觉得多语言模型效果不够好也可以换成用本地语料微调过的中文BERT甚至可以用text2vec或bge系列。这里有个容易被忽略的点Embedding的粒度。BERTopic默认对整篇文档做嵌入但长文档效果通常不如滑窗切段后再嵌入。官方文档里建议对长文档按句子或段落切分得到句子级嵌入后再加权合并能显著缓解一篇文档话题太多导致向量被拉平的问题。UMAP降维紧跟其后默认把高维向量压到5维。为什么需要这一步HDBSCAN对高维空间的距离计算非常敏感在原始768维向量上直接聚类基本会得到一团噪声。UMAP在这里的使命是保留局部结构的同时把维度降下来让聚类算法能区分出密集区域。需要注意UMAP是有随机性的不同的random_state会产出不同的聚类边界。这不算bug是流形学习的特性后面第五章会讲怎么用min_dist和n_neighbors控制它的行为。HDBSCAN是聚类的核心它和K-Means最大的区别在于不需要预设主题数。你不需要告诉它我要分20个主题它自己会根据密度把稠密区域挑出来剩下的点标为噪声-1类。这对真实语料太重要了——真实数据里总有垃圾文本、混合话题文档强行分进某个主题反而污染结果。HDBSCAN的min_cluster_size参数直接控制主题的最小规模设大了主题少但召回低设小了主题多但噪声多一般从10到50之间调。聚类完成后BERTopic会取出每个簇里的所有文档把它们拼成一整块虚拟文档然后跑c-TF-IDFclass-based TF-IDF。这一步把同一主题的文档中出现频率显著高于全局的词挑出来作为该主题的关键词。这个设计的巧妙之处在于它算的是主题内词频与全局词频的比值天然会把那些到处都出现的词压下去所以你会发现BERTopic给出的关键词比LDA的top words干净得多。最后一步是主题归并把关键词高度相似的主题自动合并用余弦相似度阈值控制归并力度。2.2 BERTopic与LDA的核心差异从概率推断到聚类分群LDA把文档看成词的多项式分布混合先验地假设每篇文档由若干主题按比例生成然后反推分布参数。听起来很美但落地时有三个硬伤一是主题数K要手调调一次跑一次全量训练语料大一点非常痛苦二是主题词列表里反复出现的、了、和这类高频词必须反复调停用词表三是LDA对短文本、稀疏词袋特别无力几个词凑不出共现信息主题就含糊。BERTopic换个思路它不做生成式推断而是先在语义空间里把意思接近的文档聚成簇再从簇里抽出关键词来描述这个簇是什么。这样主题数不是预设的而是数据里长出来的。你看到一个主题只有几篇文档可以判断这要么是长尾话题要么是噪声没滤干净你不需要在词表上下太多功夫因为句子嵌入已经把语义考虑进去了同义词电脑和计算机在向量空间里是靠近的这是词袋模型做不到的。另外BERTopic的文档到主题的分配是硬聚类——每篇文档要么属于一个簇要么是噪声。LDA给的是分布概率百分之三十属于A主题百分之七十属于B主题后者在工程上不好用。BERTopic还给每篇文档一个topic id配合它自带的reduce_outliers方法可以事后调整噪声文档到底归到哪个主题这在复盘阶段很实用。这里还得说一句选型的适用边界。BERTopic每一步都有独立模型任何一步不好用都有替换空间这是它最大的工程优势但也意味着它比LDA慢得多因为要跑EmbeddingGPU上快很多纯CPU处理十万级文档会明显耗时后面第三章有具体建议。如果你的场景是几百篇学术摘要分个大概就行LDA够用不必上BERTopic如果你的场景是数万条客服工单要自动分门别类且主题名要能看懂BERTopic就是更合适的选择。3. 用BERTopic跑通最小主题模型安装、代码与数据准备3.1 安装依赖与版本兼容性先装torch再装bertopic我建议用conda建一个干净的独立环境避免和已有的深度学习环境互相污染。先装CPU版或GPU版的PyTorch因为BERTopic的嵌入模型跑在torch上如果你先装bertopic再装torchpip会自动帮你拉一个torch大概率是CPU版后面想换GPU还得费劲重装。# 创建一个Python 3.10环境 conda create -n bertopic_env python3.10 -y conda activate bertopic_env # 先装CPU版torch如果想用GPU把index-url换成对应CUDA版本 pip install torch --index-url https://download.pytorch.org/whl/cpu # 再装BERTopic会自动补齐umap-learn、hdbscan、scikit-learn等依赖 pip install bertopic # 用sentence-transformers做中文嵌入需要额外装中文分词 pip install sentence-transformers jieba装好之后跑一行python -c import bertopic; print(bertopic.__version__)确认版本。这里提醒一下BERTopic的API从0.14到0.16之间有一些改动老版本用的calculate_topic_model等方法和新版本不完全一样如果你在知乎或掘金抄到旧代码跑挂了先看自己装的版本再对应调整。3.2 最小示例代码从原始文本到主题列表下面给你一段可以直接跑通的核心代码。我用的是BERTopic 0.16.x的API数据就是一个包含若干句子的list每句代表一篇文档。from bertopic import BERTopic from sentence_transformers import SentenceTransformer # 1. 准备一个面向前两个月的语料这里用几条示例代替你真实的文档列表 # 每条文档最终对应一个主题标签 docs [ 今天天气不错适合出门跑步, 显卡价格最近又涨了RTX 5090快出了, CPU和GPU的区别是什么, 打印机卡纸了怎么处理, 内存条超频遇上蓝屏怎么办, 固态硬盘和机械硬盘怎么选, 跑步的正确姿势是什么, 建议每天慢跑三十分钟, 深度学习模型训练需要多大显存, NVIDIA新驱动解决了黑屏问题, ] # 2. 加载多语言嵌入模型中文场景比默认的all-MiniLM-L6-v2友好 # 如果你显存紧张换成paraphrase-multilingual-MiniLM-L12-v2 sentence_model SentenceTransformer(shibing624/text2vec-base-chinese) # 3. 创建BERTopic实例传入自定义嵌入模型 # min_topic_size是关键参数决定最少多少篇文档能构成一个主题 topic_model BERTopic( embedding_modelsentence_model, min_topic_size2, calculate_probabilitiesTrue, # 给每篇文档输出属于各主题的概率 random_state42, # UMAP/HDBSCAN都有随机性固定它才能复现 ) # 4. 拟合并得到主题标签 topics, probs topic_model.fit_transform(docs) # 5. 直接打印出每个主题的关键词 topic_info topic_model.get_topic_info() print(topic_info.head()) # 6. 指定获取第一个主题topic_id从0开始 print(topic_model.get_topic(0))这段代码里有几个值得说道的参数。embedding_model是接口你可以传sentence-transformers的模型名也可以传一个自己封装好的callable输入是字符串列表输出是向量矩阵这种灵活性对替换成自训练模型很重要。min_topic_size2只是示例真实数据里建议先默认10再往下调——调这个值能够大大改变最终主题粒度和落地的可用性后面第四章会单独讲。get_topic_info()会返回一个DataFrame包含Topic、Count、Name、Representation四列。Topic列里的-1是噪声主题这些文档没有被分进任何主题通常会在事后用reduce_outliers处理。get_topic(0)返回的是该主题的关键词及其权重列表格式长这样[(跑步, 0.412), (锻炼, 0.283), (慢跑, 0.221), ...]如果你用的是新版BERTopic 0.16.x这里的关键词来自c-TF-IDF默认还会额外给出一个bertopic风格的黄金关键词表示新增的这步依赖于LLM初次运行会有一个下载流程。如果不想用LLM做表示不用管就行结果落在基于c-TF-IDF的表示上。3.3 数据准备和格式约定清洗到什么样才够格跑BERTopicBERTopic对输入数据的要求比LDA宽松得多但有一个死规矩必须是去重后的、基本完整的文档文本。它不做词粒度处理所以对中文来说不需要分词但是两个问题你必须在喂进去之前解决一是重复文本相同或极相似的文本会让变体数量的估算失真聚类的随机性也会被放大二是超短噪声文本比如谢谢、收到这类词它们没有任何主题意义却会在UMAP空间里挤成一团拉低其他正常簇的密度。# 常见预处理流程 import pandas as pd import re def preprocess_docs(raw_texts): # 去重保持顺序的同时去掉完全重复的文本 seen set() unique_docs [] for doc in raw_texts: text str(doc).strip() if text and text not in seen: seen.add(text) unique_docs.append(text) # 去掉极短的文本少于15个字符的通常没有主题信息 valid_docs [doc for doc in unique_docs if len(doc) 15] return valid_docs df pd.read_csv(your_corpus.csv) docs preprocess_docs(df[content].tolist()) print(f预处理后剩余 {len(docs)} 条文档)这里只做了最基础的清洗。值得注意的是不要在预处理时做停用词过滤——BERTopic的关键词提取环节已经有c-TF-IDF压高频词的能力你提前过滤反而会把一些有主题区分度的词删掉比如显卡在显卡主题里处处可见但它恰恰是硬件新闻这个主题的代表词不能当停用词去掉。对比LDA你在LDA里不滤停用词根本跑不动这算是BERTopic带来的一个解脱。4. BERTopic的关键参数与代码复现主题数、聚类密度与可视化4.1 三个必调参数min_topic_size、nr_topics、UMAP窗口BERTopic真正影响结果质量的核心参数就那么几个调对了主题清晰调错了要么碎片化要么一锅粥。每个参数背后对应一个独立的算法决策理解了决策才能调得动。min_topic_size是HDBSCAN的最小簇大小它决定的不是最终主题有几个而是多小的簇可以被接受为主题。越小主题越多是一个直觉结论但实际它把边界搞得非常敏感——设成2的时候UMAP里任何两个相近的句子都会各自成簇主题面貌会变得碎不可读设成100的时候只有最密集的大话题才会被识别出来长尾主题全部掉进噪声桶。我的经验是先用默认值10跑一遍看get_topic_info()里各主题的文档数分布。如果大量主题只有1020篇且关键词混杂就往上提到20或30如果噪声占比-1类的Count超过40%就往下调。这个调参循环本质是在粒度和召回之间找平衡没有绝对最优值只看你对噪声的容忍度。nr_topics是事后归并的参数它和min_topic_size不同它作用于聚类完成之后。如果你发现聚类结果产生了80个主题而业务上只需要20个一级主题就设nr_topics20BERTopic会基于主题间的c-TF-IDF相似度做归并。注意这个参数不是让你一开始就拍脑袋决定主题数而是让你聚类完成后按业务需要压规模它减小的方向是靠谱的反向增加不会帮你找回细节。调它的时候有个印象很深的现象nr_topics设得过后归并出来的大主题里会混进几个边界不清的词因为按相似度合并总是会把最相似但未必同一主旨的两个主题也并在一起。umap_model是最容易被忽略但影响最大的选择。BERTopic默认在8维向量上做UMAP处理实际上把高维嵌入压缩成几个维度这个动作对结果的影响比聚类算法本身更大。你可以在初始化时显式传入一个UMAP模型from umap import UMAP import numpy as np # 用余弦距离替代默认的欧氏距离 umap_model UMAP( n_neighbors15, n_components5, min_dist0.0, metriccosine, random_state42, ) # 传入BERTopic topic_model BERTopic( embedding_modelsentence_model, umap_modelumap_model, min_topic_size10, )这里metriccosine专门针对文档语义向量因为在sentence-transformer输出的向量空间里欧氏距离会受到向量模长的影响两个语义相同但模长不同的向量会被拉开而余弦距离只看方向对高频词导致的模长波动不敏感。min_dist0.0让降维后的点尽量分散对HDBSCAN找密度低谷有帮助。n_neighbors控制局部邻域大小数值越小越容易在小范围里形成密集簇越大越平滑、越容易忽略小簇。我之前在几十万条工单数据上调过n_neighbors从5调到20主题数可以从两百多降到几十个而且主题面貌完全不一样。4.2 主题可视化用代码复现看得懂的主题图调参的时候光看关键词列表不够必须看分布图。BERTopic自带的可视化基于plotly浏览器里缩放很方便。这里给出常用三种图的代码。# 1. 主题间距离图用UMAP把主题向量压到2D看一看主题之间的远近关系 fig topic_model.visualize_topics() fig.show() # 2. 主题关键词条形图查看每个主题下关键词的c-TF-IDF得分 fig topic_model.visualize_barchart(top_n_topics8) fig.show() # 3. 文档分布图把每篇文档按所属主题着色看聚类分离度 # 注意topic_id-1的噪声文档不会画在图里需要先过滤 topics_to_plot topics -1 fig topic_model.visualize_documents( docs, topicstopics, embeddingstopic_model.c_tf_idf_.toarray() if hasattr(topic_model, c_tf_idf_) else None, ) fig.show()第三个图有一个值得注意的细节visualize_documents默认会对文档嵌入再做一次UMAP降到2D但如果你传入的embeddings不对它就会在内部偷偷重新算。比较好的做法是先用model.embedding_model.embed_documents(docs)得到原始向量再把原始向量传给它。如果数据量大这里会非常吃内存建议对docs做个切片比如只画两千条。可视化之后还有一个必须做的验证动作人工抽检。把每个主题里的文档随机抽58篇出来读对标get_topic()给的关键词。如果关键词和实际文档内容对不上说明大概率是min_topic_size设太小导致聚类把两个语义不同的簇强行并到了一起或者你的嵌入模型本身在你的领域语料上表现不佳可以换个模型再跑一遍。4.3 中文语境下的嵌入模型选型在中文场景下嵌入模型的选择直接决定主题质量的上下限。默认的那个英文MiniLM模型放到中文上效果很差主题关键词经常是拼音或者乱七八糟的英文这不是代码问题是嵌入模型没有见过足够多的中文语境。我踩坑总结下来中文语料从优到次的模型选择大致是一是shibing624/text2vec-base-chinese这个模型在中文语义相似度上表现很稳且体积适中可以在CPU上跑速度也比较快二是BAAI/bge-large-zh-v1.5系列效果更好但显存占用和推理时间都要高适合离线批量处理十万级以上的数据三是paraphrase-multilingual-MiniLM-L12-v2因为是多语言模型中文效果中规中矩但对中英混排的文本有很好的兼容性。要在速度和效果之间取舍的话我的经验是先把text2vec跑通再换bge看看结果有没有明显提升如果差距不大就用更快的那个毕竟主题建模迭代实验通常要跑很多轮。5. BERTopic常见问题排查主题数失控、噪声过大、复现不稳定5.1 主题数爆炸成几百个先查min_cluster_size再查UMAP现象是聚类出来的主题数远超业务预期比如你面对几千条数据结果跑出来三四百个主题每个主题的文档只有几条。这通常不是BERTopic坏了而是你的阈值参数设得太过激进。min_topic_size设得太小比如默认可能被自己改成了2或者3那HDBSCAN就会把任何两个相近的点都当成一个簇。另一个原因是UMAP的n_neighbors太小局部结构被过度强调导致密度高低起伏不明显。解决方式分成两步第一步直接把min_topic_size提高到20看主题数是否收敛到一个合理的区间第二步把UMAP的n_neighbors从默认15提高到30并设置min_dist0.0。这两步走完再跑一次主题数通常会落到几十个的量级。# 推荐一组比较稳的初始化参数 umap_model UMAP(n_neighbors30, n_components5, min_dist0.0, metriccosine) topic_model BERTopic( embedding_modelsentence_model, umap_modelumap_model, min_topic_size20, )主题数不是越少越好。我之前处理过一组科技新闻数据主题数压到5个以下之后显卡评测和游戏本推荐会被硬并成一个主题因为它们的文档级语义分布太接近了。调整的时候既要看主题数据的分布也要看业务上需要的粒度级别最好先跑一次看自然聚类的结果再决定要不要用nr_topics归并。5.2 噪声文档-1占比太高先看数据再看参数现象是get_topic_info()里Topic-1的Count占了总文档数的40%以上。这个比例在真实语料里不能算异常因为现实文本就是有大量无主题的内容。但如果超过一半就要排查了。第一要查的是数据质量——是否清洗到位大量文本是收到好的链接这类超短无意义短语建议直接删掉第二要查的是嵌入模型和语料领域是否匹配比如用通用模型处理法律条文、医疗病历这类特殊垂直语料语义距离本来就拉不开导致聚类困难。有一个有效手段是用reduce_outliers对噪声文档做二次分类。BERTopic提供了基于主题分布或c-TF-IDF的方法把噪声文档重新分配到最近的主题里去。用它在可视化之前处理数据能把噪声比例压下去但要注意这是一种事后补救如果你的原始数据噪声占比实在太高补出来的结果也不可靠。from bertopic import BERTopic # 训练完成后调用reduce_outliers把-1噪声文档重新分配 new_topics topic_model.reduce_outliers(docs, topics, strategydistributions, threshold0.1) # 用新的分配结果重算主题 topic_model.update_topics(docs, topicsnew_topics) # 打印重新分配后的主题分布 print(topic_model.get_topic_info())strategydistributions利用的是calculate_probabilitiesTrue时保存的每篇文档主题概率如果概率矩阵里找得到高于threshold的主题就把该文档划过去。threshold越低越容易把噪声文档分走风险是分错主题。一般从0.1开始试。5.3 复现结果不一致固定随机种子只是开始这是新手最困惑的问题第四次跑同一份代码主题结果跟第一次完全不同。根源在于UMAP和HDBSCAN都有随机初始化而且BERTopic内部还不止一个随机源。你只给BERTopic(random_state42)一个种子还不够因为UMAP模型如果不显式传入它自己内部用的种子是独立的。正确的做法是把你自定义的UMAP和HDBSCAN模型都显式传入。HDBSCAN本身不是随机算法但BERTopic内部对它的调包方式可能影响结果。更保险的做法是最外层np.random.seed(42)和torch.manual_seed(42)一起设尤其是你用了自定义embedding模型或PyTorch时。如果你用的是GPUtorch的并行计算结果在不同机器上也可能有微小差异这在主题边界处容易放大成明显的聚类差异。import numpy as np import torch from bertopic import BERTopic from umap import UMAP from hdbscan import HDBSCAN np.random.seed(42) torch.manual_seed(42) umap_model UMAP(random_state42, n_neighbors15, min_dist0.0, metriccosine) hdbscan_model HDBSCAN(min_cluster_size10, prediction_dataTrue) topic_model BERTopic( embedding_modelsentence_model, umap_modelumap_model, hdbscan_modelhdbscan_model, calculate_probabilitiesTrue, random_state42, )这里的prediction_dataTrue很有必要因为后面如果用approximate_predict给新文档分配主题需要HDBSCAN保存预测数据。没有它你传给新文档时就会收到HDBSCAN的报错。总之固定随机种子能保证同一个环境下复现换环境换数据仍然可能变这是模型的特性不是bug。5.4 中文主题词全是英文或拼音嵌入模型或分词的问题效果上最容易让人愣住的是跑出来的主题关键词居然全是英文或拼音。原因有两个方向。第一是嵌入模型本身不适合中文默认的all-MiniLM-L6-v2根本不懂中文它会把中文句子映射到英文语义空间的某个角落关键词自然就变成了它认为最接近的英文词。第二是c-TF-IDF是基于词频统计的它对中文的分词敏感。BERTopic内部不强制分词但它调用的count vectorizer对中文会按字或双字切分可能切出没意义的残片比如西安变成西和安——你会看到一个主题的关键词是一堆单字这对中文场景特别不友好。解决方式模型方面直接换成中文专用模型如shibing624/text2vec-base-chinese。分词方面可以在初始化时传入一个自定义的vectorizer让它在统计词频之前先按词而非字切。前面说的jieba在这里就派上用场了。你要做一个自定义类fit时用jieba把每篇文档切成词序列transform时把词序列重新拼成空格分隔的字符串BERTopic的c-TF-IDF拿到的就是正常的中文词表而不是单字。from sklearn.feature_extraction.text import CountVectorizer import jieba def chinese_tokenizer(doc): return [word for word in jieba.cut(doc) if word.strip()] vectorizer_model CountVectorizer( tokenizerchinese_tokenizer, stop_wordsNone, ngram_range(1, 2), # 允许二元词组比如神经网络 ) topic_model BERTopic( embedding_modelsentence_model, vectorizer_modelvectorizer_model, min_topic_size10, )ngram_range(1,2)能让深度和学习在原始分词后合并成深度学习这个词组而不是两个独立词对主题关键词的可读性帮助很大。但这也意味着词表会膨胀内存吃紧的时候可以把min_df最小文档频率设成2甚至3把只在极少数文档里出现的生僻词滤掉。5.5 模型加载慢、内存爆先降embedding批大小再考虑降维度主题建模跑到Embedding这一步如果文档量很大常出现内存眼看着见涨的情况。BERTopic内部做Embedding时是一次性把所有文档向量收进内存然后交给UMAP数据量大到一定程度内存直接爆掉。我这里说的爆掉是真实的OOM报错会停在UMAP拟合那一步。解决办法有两条路。一条是控制Embedding的批大小使用你自己加载的sentence-transformers模型手动分batch处理再拼接向量矩阵。另一条是不用BERTopic内部的Embedding过程自己提前把所有文档向量算好保存为.npy文件后续实验直接加载避免每次调参都重算一遍Embedding。这一步对迭代效率的提升巨大向量化通常是整个流程里最耗时且最吃算力的环节。from sentence_transformers import SentenceTransformer import numpy as np model SentenceTransformer(shibing624/text2vec-base-chinese) # 分批嵌入每批128条避免内存峰值过高 embeddings [] for i in range(0, len(docs), 128): batch docs[i:i128] embeddings.append(model.encode(batch, show_progress_barFalse)) embeddings np.vstack(embeddings) # 保存到磁盘后续实验直接加载不用反复算 np.save(doc_embeddings.npy, embeddings)之后创建BERTopic实例时如果不想让它在内部重新计算嵌入可以直接用topic_model BERTopic(...); topic_model.fit(docs, embeddingsembeddings)。这样跑出来的结果跟让BERTopic内部算的保持一致因为传入的向量会被UMAP直接消耗。这个技巧还有一个额外的好处换了一个Embedding模型后你只需要重新算一次向量UMAP和HDBSCAN的调参就不用再重算Embedding了迭代速度翻倍。6. 用自定义嵌入模型和新文档推断让BERTopic从实验变成服务调完参数、排完坑剩下的问题是怎么把模型用到新的文本上。BERTopic本身不支持像训练好的分类器那样直接持久化到磁盘再加载预测但你可以用一个组合拳达到类似的效果保存Embedding模型和UMAP/HDBSCAN模型新文档来了以后走嵌入→降维→近似预测的流程。一个比较稳定的做法是训练好主题模型后把Embedding模型、UMAP模型、HDBSCAN模型和BERTopic实例本身全部保存下来。topic_model.save(bertopic_model)会保存整个模型目录但更稳的做法是分别保存组件然后在新环境下还原。这里有一个必须确认的点如果你要用approximate_predict给新文档分配主题HDBSCAN必须用prediction_dataTrue方式初始化否则报错找不到all_points_属性。import joblib from bertopic import BERTopic # 训练完成后保存每个组件 topic_model.save(my_bertopic_model) joblib.dump(topic_model.umap_model, umap_model.pkl) joblib.dump(topic_model.hdbscan_model, hdbscan_model.pkl) # 新环境里恢复模型 loaded_topic_model BERTopic.load(my_bertopic_model) # 预测新文档的主题先用已训练的embedding模型计算向量 new_docs [新到货的RTX显卡跑深度学习好吗] new_embeddings loaded_topic_model.embedding_model.embed_documents(new_docs) # 关键新文档向量不会重新走UMAP拟合而是用近似预测直接找最近簇 topics, probs loaded_topic_model.approximate_predict(new_docs, new_embeddings) print(topics)approximate_predict的意思是新文档向量直接映射到训练好的UMAP空间里然后再让训练好的HDBSCAN预测器找一个最近的簇。这里面有几个需要注意的坑新文档的领域不能和训练语料偏离太远否则语义向量落在UMAP空间的稀疏区域大概率会被判成噪声另外approximate_predict返回的主题编号是训练时对应的编号但它的probs和fit_transform里的概率分布含义略有差异用于置信度排序是可以的不要直接跨模型比较。这个流程跑通之后主题模型就能像一个常见的机器学习服务一样持续接收新文本。我现在做数据复盘的习惯是跑完一批新数据把新主题分布和旧主题分布做一次对比看到新主题冒出来或者旧主题消失会去翻对应的原文验证是数据问题还是业务变化。做主题建模和做其他模型一样最后一道工序永远是人工抽检。希望这篇教程能帮你把BERTopic真正用到自己的流程里走到文本数据深处时不至于两眼一抹黑那样这篇文字写得就算值了。本文还有配套的精品资源点击获取