🤵♂️ 个人主页:@艾派森的个人主页
✍🏻作者简介:Python学习者
🐋 希望大家多多支持,我们一起进步!😄
如果文章对你有帮助的话,
欢迎评论 💬点赞👍🏻 收藏 📂加关注+
目录
2.2 语义向量生成:基于 Sentence-BERT 的文本编码
三、BERTopic 相比传统主题建模方法(如 LDA)的优势
一、引言:为什么我们需要 BERTopic?
在文本挖掘和自然语言处理的众多任务中,主题建模(Topic Modeling)一直是一个核心技术。无论是从大量新闻中发现热点议题,还是从用户评论中提炼关注焦点,主题建模都扮演着重要角色。然而,传统的主题建模方法,尤其是经典的 LDA(Latent Dirichlet Allocation),却在如今信息爆炸、文本碎片化的时代面临越来越多的挑战。
📉 传统方法遇到的困境:
-
语义理解能力弱:LDA 基于词袋模型(Bag-of-Words),无法理解上下文含义,难以识别“汽车”与“车辆”、“AI”与“人工智能”的同义关系;
-
短文本效果差:在如微博、知乎、评论区等短文本环境中,LDA 因词频稀疏往往难以建模出清晰的主题;
-
主题一致性差:生成的关键词往往关联性弱,主题解释困难;
-
缺乏现代语义建模能力:面对今天以大模型为代表的 NLP 飞跃,LDA 的建模能力显得相对“过时”。
🚀 BERTopic 的出现,正是为了解决这些问题。
BERTopic(Bidirectional Encoder Representations for Topic Modeling)是一种结合 Transformer 预训练模型(如 BERT)与无监督聚类算法的现代主题建模方法。它不仅具备强大的上下文理解能力,还支持短文本建模、多语言处理和主题时间演变分析,逐步成为 NLP 应用中极具潜力的主题挖掘工具。
更重要的是,BERTopic 不仅是一种算法,更是一个集成完备、使用简单的开源工具包,极大降低了主题建模的门槛。通过少量代码,我们就可以高效地从大量文本中提取出清晰、可解释的主题结构。
那么,BERTopic 背后的原理是什么?它与传统方法相比有哪些显著优势?我们又该如何在真实数据中使用它呢?
本文将带你从原理到实战,全面解锁 BERTopic 的强大能力!
二、BERTopic 核心原理解析
在了解 BERTopic 如何运行之前,我们先来看它的整体工作流程:
文本输入 → 向量编码 → 降维 → 聚类 → 关键词抽取 → 主题建模
这个流程串联起了多个 NLP 和机器学习组件,也正是 BERTopic 与传统方法最大的不同。下面我们逐步拆解每个关键步骤。
2.1 整体流程一览图
2.2 语义向量生成:基于 Sentence-BERT 的文本编码
BERTopic 使用 Sentence-BERT(SBERT) 或其他语义向量模型(如 text2vec、mpnet)对每条文本进行编码,获取语义嵌入(embedding)。相比词袋模型,这一步带来了几大好处:
-
能理解上下文含义("苹果手机" vs "苹果水果")
-
适用于短文本,嵌入向量稳定、信息丰富
-
支持多语言,包括中文(可使用如
text2vec-base-chinese
)
💡 Tips:SBERT 是 BERT 的轻量级句子对比版本,专为“句子级语义表示”优化,非常适合主题建模场景。
2.3 降维:UMAP 让高维语义空间更清晰
Transformer 编码后的向量通常有 768 维或更多,这对聚类和可视化都不太友好。为此,BERTopic 使用 UMAP(Uniform Manifold Approximation and Projection) 进行降维处理。
-
保持语义结构相对完整
-
聚类效果更稳,支持可视化
-
比 t-SNE 更快、更稳定,适合实际项目
2.4 聚类:HDBSCAN 自动发现主题簇
接下来,BERTopic 将降维后的文本嵌入输入给 HDBSCAN(Hierarchical Density-Based Spatial Clustering) 进行无监督聚类。HDBSCAN 的优势包括:
-
自动确定聚类数量(不像 KMeans 需要预设)
-
能检测“噪声点”并排除无效文本
-
能适应密度不均的真实语料(如评论、社交文本)
每个聚类簇,即被认为是一个潜在的“主题”。
2.5 主题提取:c-TF-IDF 关键词抽取算法
聚类完毕后,我们需要为每个“主题”提取代表性的关键词。此时,BERTopic 使用改进版的 class-based TF-IDF(简称 c-TF-IDF) 来做关键词提取:
-
类比将每个主题视为一篇“巨文档”(把所有属于该主题的文本合并)
-
对不同主题之间的词语差异性进行放大
-
提取出更具代表性、可解释性的关键词
相比传统 TF-IDF,c-TF-IDF 更关注“在本主题中高频,而在其他主题中低频”的词语,也就是最能代表这个主题特征的词。
✅ 总结一下:BERTopic 的五个关键词是——
嵌入(Embedding)→ 降维(UMAP)→ 聚类(HDBSCAN)→ 抽词(c-TF-IDF)→ 构建主题模型
这样一套流程,既保留了深度语义信息,又兼顾了可解释性与可视化能力,是当前语义主题建模中的一大利器。
三、BERTopic 相比传统主题建模方法(如 LDA)的优势
对比维度 | 传统LDA等方法 | BERTopic |
---|---|---|
语义建模能力 | 基于词袋模型(Bag-of-Words),无法捕捉上下文语义 | 利用 BERT 等预训练模型,能理解上下文和同义词 |
适用于短文本 | 表现较差,词频稀疏问题严重 | 表现优异,能准确捕捉短文本中的语义信息 |
主题一致性 | 主题关键词可能无明显语义连贯性 | 提取的关键词语义一致性更高 |
多语言支持 | 支持有限,需手动适配 | 可直接用多语种 BERT 处理 |
时间建模 | 不支持时间变化 | 支持分析主题随时间演变 |
可扩展性 | 需指定主题数K,扩展性差 | HDBSCAN自动决定聚类数,灵活适应不同数据集 |
可视化工具 | 通常需额外开发 | 内置多种可视化功能,交互性强 |
四、实战案例
4.1前置准备
Python环境:3.9
需要下载的包:bertopic、sentence_transformers
pip install bertopic
pip install sentence_transformers
数据集为新闻数据集,包含新闻内容(文本内容)、新闻类型(一般做主题建模没有这个变量,这里是为了方便对比主题分类结果)、时间(有时间变量的话可以分析主题随时间演变趋势)
4.2加载数据
导入第三方库并加载数据集
# 导入工具包
import numpy as np
from bertopic import BERTopic
from sentence_transformers import SentenceTransformer
from umap import UMAP
from hdbscan import HDBSCAN
from sklearn.feature_extraction.text import CountVectorizer
import pandas as pd
import re
import jieba
# 加载数据
data = pd.read_excel("data_news_year.xlsx")
data
有八个新闻类别,每个各100条数据
4.2中文分词
# 自定义中文分词的函数
def chinese_word_cut(mytext):
jieba.load_userdict('自定义词典.txt') # 这里你可以添加jieba库识别不了的网络新词,避免将一些新词拆开
jieba.initialize() # 初始化jieba
# 文本预处理 :去除一些无用的字符只提取出中文出来
new_data = re.findall('[\u4e00-\u9fa5]+', mytext, re.S)
new_data = " ".join(new_data)
# 文本分词
seg_list_exact = jieba.lcut(new_data)
result_list = []
# 读取停用词库
with open('stopwords.txt', encoding='utf-8') as f: # 可根据需要打开停用词库,然后加上不想显示的词语
con = f.readlines()
stop_words = set()
for i in con:
i = i.replace("\n", "") # 去掉读取每一行数据的\n
stop_words.add(i)
# 去除停用词并且去除单字
for word in seg_list_exact:
if word not in stop_words and len(word) > 1:
result_list.append(word)
return ' '.join(result_list)
# 调用分词函数
docs = data['content'].apply(chinese_word_cut).tolist()
print('文本条数: ', len(docs))
print('预览第一条: ', docs[0])
4.3构建模型
# 1. 词向量模型
model_path = "paraphrase-multilingual-MiniLM-L12-v2"
embedding_model = SentenceTransformer(model_path)
embeddings = embedding_model.encode(docs) # 生成嵌入向量
print('向量shape:', embeddings.shape)
这一步如果你能正确运行,也就是会程序自动在huggingface下载模型
如果因为网络等问题不能正常运行,可以把模型下载到本地再运行
https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/tree/main
在这个网址中下载,注意需要科学上网!如果不会下载可进我的QQ粉丝群下载
如果是运行本地下载的模型,只需要将模型文件的路径填写即可,绝对路径和相对路径均可,我这里用的是相对路径,也就是代码和模型文件在同一级目录
# 1. 词向量模型,同时加载本地训练好的词向量
#https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/tree/main
model_path = "./paraphrase-multilingual-MiniLM-L12-v2"
embedding_model = SentenceTransformer(model_path)
embeddings = embedding_model.encode(docs) # 生成嵌入向量
print('向量shape:', embeddings.shape)
接着准备UMAP、HDBSCAN、 CountVectorizer模型
# 2. 创建UMAP降维模型
umap_model = UMAP()
# 3. 创建HDBSCAN聚类模型
hdbscan_model = HDBSCAN()
# 4. 创建CountVectorizer模型
# 停用词
with open('stopwords.txt', "r", encoding='utf-8') as file:
lines = file.readlines()
stopwords = [line.strip() for line in lines]
vectorizer_model = CountVectorizer(stop_words=stopwords)
注意我这里初始化的模型都是使用默认参数,后期你可以自己调整参数进行优化
UMAP模型的参数如下:
class UMAP(
n_neighbors: int = 15,
n_components: int = 2,
metric: str = "euclidean",
metric_kwds: Any | None = None,
output_metric: str = "euclidean",
output_metric_kwds: Any | None = None,
n_epochs: Any | None = None,
learning_rate: float = 1,
init: str = "spectral",
min_dist: float = 0.1,
spread: float = 1,
low_memory: bool = True,
n_jobs: int = -1,
set_op_mix_ratio: float = 1,
local_connectivity: float = 1,
repulsion_strength: float = 1,
negative_sample_rate: int = 5,
transform_queue_size: float = 4,
a: Any | None = None,
b: Any | None = None,
random_state: Any | None = None,
angular_rp_forest: bool = False,
target_n_neighbors: int = -1,
target_metric: str = "categorical",
target_metric_kwds: Any | None = None,
target_weight: float = 0.5,
transform_seed: int = 42,
transform_mode: str = "embedding",
force_approximation_algorithm: bool = False,
verbose: bool = False,
tqdm_kwds: Any | None = None,
unique: bool = False,
densmap: bool = False,
dens_lambda: float = 2,
dens_frac: float = 0.3,
dens_var_shift: float = 0.1,
output_dens: bool = False,
disconnection_distance: Any | None = None,
precomputed_knn: Any = (None, None, None)
)
HDBSCAN模型参数如下:
class HDBSCAN(
min_cluster_size: int = 5,
min_samples: Any | None = None,
cluster_selection_epsilon: float = 0,
max_cluster_size: int = 0,
metric: str = "euclidean",
alpha: float = 1,
p: Any | None = None,
algorithm: str = "best",
leaf_size: int = 40,
memory: Memory = Memory(None, verbose=0),
approx_min_span_tree: bool = True,
gen_min_span_tree: bool = False,
core_dist_n_jobs: int = 4,
cluster_selection_method: str = "eom",
allow_single_cluster: bool = False,
prediction_data: bool = False,
branch_detection_data: bool = False,
match_reference_implementation: bool = False,
cluster_selection_epsilon_max: float = float('inf'),
**kwargs: Any
)
如果说你也不知道这些参数都表示什么意思,你可以鼠标放在模型名称上,然后就会弹出模型参数介绍的框框,往下翻还有每个模型的解释,只不过是英文的。注意我使用的代码编辑器是VS code,这个软件有这个功能。
这里我列举几个最常用可能也相对关键的参数吧
# 2. 创建UMAP降维模型
umap_model = UMAP(
n_neighbors=15, # 控制局部邻域的大小
n_components=5, # 降维后的维度
min_dist=0.05, # 控制点与点之间的最小距离 min_dist=0.0 可能导致数据点过度重叠,建议调整为 0.05 以提升可视化清晰度。
metric='cosine', # 距离度量方式
random_state=42 # 随机种子,确保结果可复现
)
# 3. 创建HDBSCAN聚类模型
hdbscan_model = HDBSCAN(
min_cluster_size=10, # 控制最小簇的大小
min_samples=5, # 控制簇的核心点数量
gen_min_span_tree=True
)
4.4训练模型
BERTopic模型中有一个nr_topics的参数,也就是指定主题的个数,默认就是不指定个数。
# 5. 正式创建BERTopic模型
topic_model = BERTopic(
# nr_topics=8, # 主题数,可以不指定
embedding_model=embedding_model,
vectorizer_model=vectorizer_model,
umap_model=umap_model,
hdbscan_model=hdbscan_model
)
# 6.查看主题
topics, probs = topic_model.fit_transform(docs, embeddings=embeddings) #传入训练好的词向量
topic_info = topic_model.get_topic_info()
topic_info
没指定个数就有20几个主题,下面是我指定8个就生成8个主题
可以使用下面代码把主题聚类结果保存为excel文件
# 保存聚类主题信息
topic_docs = topic_model.get_document_info(docs)
topic_docs.to_excel('聚类结果.xlsx',index=False)
4.5结果可视化
注意下面代码生成的都是html交互式的可视化文件,打开即可
# 结果可视化
#1 主题分布可视化
fig1 = topic_model.visualize_topics()
fig1.write_html("1.topics.html")
#2 主题相似度可视化
fig2 =topic_model.visualize_heatmap()
fig2.write_html("2.simi_heatmap.html")
# 3 文档散点图可视化
reduced_embeddings = UMAP().fit_transform(embeddings)
fig3 = topic_model.visualize_documents(docs, reduced_embeddings=reduced_embeddings)
fig3.write_html("3.docsvisual.html")
#4 主题关键词可视化
fig4 = topic_model.visualize_barchart()
fig4.write_html("4.keywords.html")
注意,这里为啥只显示8个主题的图,是因为该可视化函数中top_n_topics默认是8,这里你可以自行修改参数
#5 层级可视化
fig5= topic_model.visualize_hierarchy()
fig5.write_html("5.Hierarchical.html")
这是层次聚类的结果可视化图,从上往下看,可以看出主题18/8/11/6是属于同一个主题,此时我们可以去看聚类结果的excel文件来验证这几个主题是不是属于同一个主题,或者从主题关键词来判断。
可以发现主题18/8/11/6讲的都是房产相关的内容,可以归为一类。以此类推,主题2/9/13讲的是股票相关的内容,主题16/1讲的是游戏相关的内容,主题3/12讲的是电影娱乐相关的内容,主题7/0/21讲的是教育相关的内容,主题10/19讲的是科技相关的内容,主题20/15/4讲的是彩票相关的内容,主题17/5/14讲的是体育相关的内容。最终也就对应上原始的八个主题。分析到这一步,你可以根据层次聚类的可视化结果,判断出最优的主题数,然后再去前面的BERTopic模型中修改nr_topics的参数,指定主题数。
#6.动态主题模型
timestamps = [int(line) for line in data['year']] # 提取出时间列数据
topics_over_time = topic_model.topics_over_time(docs, timestamps)
fig6 = topic_model.visualize_topics_over_time(topics_over_time)
fig6.write_html("6.visualize_topics_over_time.html")
做这个图的前提是数据集中有时间变量,结果图中可以通过滑轮进行滚动查看各主题的颜色。
4.6番外篇
如果想进行主题优化,同时减少离群值,可以将4.4训练模型的代码改为下面的代码
# 主题优化
# 该模型会将冗余的词进行替换,生成更具有多样性的主题词。
from bertopic.representation import MaximalMarginalRelevance # 导入
representation_model = MaximalMarginalRelevance(diversity=0.3) # 创建mmr模型
topic_model = BERTopic(
embedding_model=embedding_model,
vectorizer_model=vectorizer_model,
umap_model=umap_model,
hdbscan_model=hdbscan_model,
representation_model=representation_model # 传入模型
)
# 如何减少离群值-reduce outliers函数
# 调用函数
topics, probs = topic_model.fit_transform(docs, embeddings=embeddings)
new_topics = topic_model.reduce_outliers(docs, topics, probabilities=probs, strategy="probabilities", threshold=0.5)
# 应用更新
topic_model.update_topics(docs, topics=new_topics, vectorizer_model=vectorizer_model)
topic_info = topic_model.get_topic_info()
topic_info
使用上面优化的代码可能未必比没优化的效果好,具体怎么选由你自己来决定。
源代码
# 导入工具包
import numpy as np
from bertopic import BERTopic
from sentence_transformers import SentenceTransformer
from umap import UMAP
from hdbscan import HDBSCAN
from sklearn.feature_extraction.text import CountVectorizer
import pandas as pd
import re
import jieba
# 加载数据
data = pd.read_excel("data_news_year.xlsx")
data
data['type'].value_counts()
# 自定义中文分词的函数
def chinese_word_cut(mytext):
jieba.load_userdict('自定义词典.txt') # 这里你可以添加jieba库识别不了的网络新词,避免将一些新词拆开
jieba.initialize() # 初始化jieba
# 文本预处理 :去除一些无用的字符只提取出中文出来
new_data = re.findall('[\u4e00-\u9fa5]+', mytext, re.S)
new_data = " ".join(new_data)
# 文本分词
seg_list_exact = jieba.lcut(new_data)
result_list = []
# 读取停用词库
with open('stopwords.txt', encoding='utf-8') as f: # 可根据需要打开停用词库,然后加上不想显示的词语
con = f.readlines()
stop_words = set()
for i in con:
i = i.replace("\n", "") # 去掉读取每一行数据的\n
stop_words.add(i)
# 去除停用词并且去除单字
for word in seg_list_exact:
if word not in stop_words and len(word) > 1:
result_list.append(word)
return ' '.join(result_list)
# 调用分词函数
docs = data['content'].apply(chinese_word_cut).tolist()
print('文本条数: ', len(docs))
print('预览第一条: ', docs[0])
# 1. 词向量模型
model_path = "paraphrase-multilingual-MiniLM-L12-v2"
embedding_model = SentenceTransformer(model_path)
embeddings = embedding_model.encode(docs) # 生成嵌入向量
print('向量shape:', embeddings.shape)
# 1. 词向量模型,同时加载本地训练好的词向量
#https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2/tree/main
model_path = "./paraphrase-multilingual-MiniLM-L12-v2"
embedding_model = SentenceTransformer(model_path)
embeddings = embedding_model.encode(docs) # 生成嵌入向量
print('向量shape:', embeddings.shape)
# 2. 创建UMAP降维模型
umap_model = UMAP()
# 3. 创建HDBSCAN聚类模型
hdbscan_model = HDBSCAN()
# 4. 创建CountVectorizer模型
# 停用词
with open('stopwords.txt', "r", encoding='utf-8') as file:
lines = file.readlines()
stopwords = [line.strip() for line in lines]
vectorizer_model = CountVectorizer(stop_words=stopwords)
# 5. 正式创建BERTopic模型
topic_model = BERTopic(
# nr_topics=8, # 主题数,可以不指定
embedding_model=embedding_model,
vectorizer_model=vectorizer_model,
umap_model=umap_model,
hdbscan_model=hdbscan_model
)
# 6.查看主题
topics, probs = topic_model.fit_transform(docs, embeddings=embeddings) #传入训练好的词向量
topic_info = topic_model.get_topic_info()
topic_info
# 保存聚类主题信息
topic_docs = topic_model.get_document_info(docs)
topic_docs.to_excel('聚类结果.xlsx',index=False)
# 结果可视化
#6.1 主题分布可视化
fig1 = topic_model.visualize_topics()
fig1.write_html("1.topics.html")
#6.2 主题相似度可视化
fig2 =topic_model.visualize_heatmap()
fig2.write_html("2.simi_heatmap.html")
# 6.3 文档散点图可视化
reduced_embeddings = UMAP().fit_transform(embeddings)
fig3 = topic_model.visualize_documents(docs, reduced_embeddings=reduced_embeddings)
fig3.write_html("3.docsvisual.html")
#6.4 主题关键词可视化
fig4 = topic_model.visualize_barchart()
fig4.write_html("4.keywords.html")
#6.5 层级可视化
fig5= topic_model.visualize_hierarchy()
fig5.write_html("5.Hierarchical.html")
#6.6.动态主题模型
timestamps = [int(line) for line in data['year']] # 提取出时间列数据
topics_over_time = topic_model.topics_over_time(docs, timestamps)
fig6 = topic_model.visualize_topics_over_time(topics_over_time)
fig6.write_html("6.visualize_topics_over_time.html")
资料获取,更多粉丝福利,关注下方公众号获取