BERTopic详解--主题建模利器

BERTopic:强大的主题建模工具

引子:在信息爆炸的时代,海量文本数据如潮水般涌来——从社交媒体的实时动态、学术论文的深度研究,到企业内部的业务报告,如何从纷繁复杂的文字中提炼出核心主题,成为数据分析师和研究者面临的共同挑战。传统主题建模方法(如LDA)虽曾风靡一时,却常因依赖词频统计、忽略语义关联、需预设主题数量等局限,难以满足现代文本分析的需求。

BERTopic 巧妙融合Transformer模型的深度语义理解与经典聚类算法的鲁棒性,不仅能自动发现主题数量,还能生成高度可解释、贴近人类认知的主题标签。无论是洞察用户评论中的情感倾向,还是挖掘科研文献中的前沿方向,BERTopic 像一位经验丰富的“文本侦探”,让隐藏在数据背后的故事浮出水面。

BERTopic简介

BERTopic 是一个前沿的主题建模库,它结合了基于Transformer的嵌入和聚类技术,从文本集合中提取有意义的主题。与传统的主题建模方法不同,BERTopic利用最先进的语言模型和一种新颖的基于类别的TF-IDF方法,创建连贯、可解释的主题表示。

BERTopic在几个重要方面与传统主题建模技术(如LDA)有所不同:

  1. 语义理解:BERTopic利用预训练的Transformer模型来理解文档的上下文意义,而不仅仅是词共现。
  2. 动态主题检测:主题通过基于密度的聚类形成,允许算法自然地发现主题数量。
  3. 可解释的结果:基于类别的TF-IDF方法创建了清晰、独特的主题表示,包含相关关键词。
  4. 模块化设计:流程中的每个步骤都可以定制或替换,使BERTopic极具灵活性。
  5. 丰富的可视化:内置工具用于探索和可视化主题分布、相似性和演变。

BERTopic核心工作流程

可选
文档集合
嵌入
降维
聚类
主题表示
微调
主题探索

每个文档都通过这个流程,将原始文本转换为有意义的主题:

  1. 嵌入:使用Transformer模型(默认:sentence-transformers)将文档转换为密集向量表示。
  2. 降维:这些高维向量被减少(默认:UMAP),以提高聚类效果。
  3. 聚类:文档被分组为主题(默认:HDBSCAN),包括异常值检测。
  4. 主题表示:c-TF-IDF算法为每个主题提取独特的关键词。
  5. 可选微调:可以使用各种方法(包括LLMs)来细化主题表示。

BERTopic子模块

BERTopic 非常模块化(可根据自身需求,进行灵活替换),可以在各种子模型中保持其主题生成的质量
在这里插入图片描述

嵌入(Embedding)

具体使用说明:地址

后端(Backend)描述(Description)导入路径(Import Path)最佳用途(Best For)
SentenceTransformer默认后端,使用Transformer模型生成嵌入bertopic.backend._sentencetransformers通用目的,高质量嵌入
Model2Vec高性能嵌入模型,支持快速生成嵌入bertopic.backend._model2vec快速嵌入生成
OpenAI集成OpenAI的嵌入API,提供生产级嵌入bertopic.backend._openai生产级嵌入
Cohere集成Cohere的嵌入API,提供高质量的语义表示bertopic.backend._cohere高质量的语义表示
LangChain集成LangChain的嵌入模型,方便与LangChain生态系统结合bertopic.backend._langchain与LangChain生态集成
FastEmbed轻量级嵌入解决方案,兼顾效率与简洁性bertopic.backend._fastembed效率和简洁性
MultiModal支持文本和图像的多模态嵌入,适用于多模态文档分析bertopic.backend._multimodal多模态文档分析
Flair提供词和文档级别的嵌入,适用于NLP研究bertopic.backend._flairNLP研究
Spacy集成Spacy的NLP管道,生成嵌入bertopic.backend._spacy与Spacy管道集成
Scikit-Learn使用TF-IDF或传统统计方法生成嵌入,简单易用bertopic.backend._sklearn简单统计嵌入
Gensim集成Gensim的词嵌入模型,支持经典NLP方法bertopic.backend._gensim经典NLP方法
USE通用句子编码器(Universal Sentence Encoder),专注于句子级语义bertopic.backend._use句子级语义

降维(Dimensionality Reduction)

BERTopic 的一个重要方面是输入嵌入的降维。由于嵌入通常维度很高,由于维度的诅咒,聚类变得困难。

通过将这些嵌入降低到低维空间(通常为5维),BERTopic使得聚类步骤更加有效和计算高效。

具体使用说明:地址

降维方法核心作用常用库/组件关键参数示例适用场景
UMAP非线性降维,保留局部与全局结构,简化后续聚类计算umap库(通过BERTopic的umap_model参数传入)n_neighbors=15(局部邻域大小)
n_components=5(目标维度)
min_dist=0.0(点间最小距离)
metric='cosine'(距离度量)
文本嵌入(如SentenceTransformer)、高维非线性数据,需平衡结构与效率
PCA线性降维,通过正交变换保留最大方差的主成分sklearn.decomposition.PCAn_components=5(目标维度)
svd_solver='auto'(求解器)
线性可分的高维数据(如数值特征、TF-IDF向量),追求计算效率的场景
Truncated SVD针对稀疏矩阵的线性降维,避免完整奇异值分解,适合高维稀疏数据sklearn.decomposition.TruncatedSVDn_components=5(目标维度)
random_state=42(随机种子)
稀疏矩阵(如文本的TF-IDF、CountVectorizer结果),需高效处理大规模稀疏数据
cuML UMAPGPU加速的UMAP,提升大规模数据降维速度,保留非线性结构优势from cuml.manifold import UMAP(RAPIDS库)n_neighbors=15(同CPU版)
n_components=5
metric='cosine'
大规模文本数据(如百万级文档),需要GPU加速的场景
Skip dimensionality reduction不执行降维,直接使用原始嵌入向量进行后续聚类from bertopic.dimensionality import BaseDimensionalityReduction无(跳过降维步骤)数据量小(如几千条文档)、原始嵌入维度已较低(如512维)、需保留全部信息的场景

聚类(Clustering)

在降低输入嵌入的维度后,我们需要将它们聚类成相似的嵌入组以提取我们的主题。这个聚类过程非常重要,因为我们的聚类技术越高效,我们的主题表示就越准确。

没有固定的聚类算法,在不同场景下,选择使用不同的聚类算法。

具体使用说明:地址

聚类算法核心作用常用库/组件关键参数示例适用场景
HDBSCAN处理噪声和变密度簇,无需预设簇数量,自动识别簇边界hdbscan.HDBSCANmin_cluster_size=15(最小簇大小)
metric='euclidean'(距离度量)
cluster_selection_method='eom'(簇选择策略)
文本聚类(如BERTopic默认)、含噪声的数据、未知簇数量的场景
k-Means划分成固定数量(k)的球形簇,速度快sklearn.cluster.KMeansn_clusters=10(簇数量)
init='k-means++'(初始化方法)
max_iter=300(最大迭代次数)
小规模数据、球形分布的数据(如数值特征)、需要快速聚类的场景
Agglomerative Clustering层次聚类,通过逐步合并最相似的簇形成树状结构,可指定簇数量或距离阈值sklearn.cluster.AgglomerativeClusteringn_clusters=10(簇数量)
linkage='ward'( linkage 方法,‘ward’ 最小化方差)
distance_threshold=None(若设为浮点数则按距离合并)
小数据集、需要层次结构解释的场景(如生物分类)、球形簇的细分
cuML HDBSCANGPU加速的HDBSCAN,提升大规模数据聚类速度,保留原算法优势cuml.cluster.HDBSCAN(RAPIDS库)min_cluster_size=15(同CPU版)
metric='euclidean'
cluster_selection_method='eom'
大规模文本数据(如百万级文档)、需要GPU加速的场景(需NVIDIA GPU)

向量表示(Vectorizers)

在主题建模中,主题表示的质量对于解释主题、传达结果和理解模式至关重要。

具体使用说明:地址

向量化模型(Vectorizer Model)核心作用常用库/组件关键参数示例适用场景
CountVectorizer统计文本中词项的出现频率,生成词袋模型(Bag-of-Words),作为主题关键词提取的基础sklearn.feature_extraction.text.CountVectorizerstop_words="english"(去除英文停用词)
ngram_range=(1, 2)(考虑1-2元组)
max_features=10000(限制特征数量,避免维度爆炸)
基础词频统计、需要简单词袋表示的场景(如BERTopic中主题关键词的初步提取)
TfidfVectorizer基于 TF-IDF(词频-逆文档频率) 加权,突出“重要词项”(高频且在文档中出现少的词),降低高频无关词的影响sklearn.feature_extraction.text.TfidfVectorizeruse_idf=True(启用IDF加权)
smooth_idf=True(平滑IDF,避免分母为零)
sublinear_tf=True(对词频取对数,减弱高频词 dominance)
需要强调词项重要性、避免高频无关词干扰的场景(如主题关键词的加权表示,更贴合语义)

C-TF-IDF

简介

C-TF-IDF(Class-based Term Frequency-Inverse Document Frequency,类基词频-逆文档频率)是BERTopic中用于提取主题关键词的核心算法,其设计目标是解决传统TF-IDF在主题建模中的局限性,更精准地捕捉每个主题的独特性。c-TF-IDF是传统TF-IDF(词频-逆文档频率)算法的改进版本,专门适用于主题建模。虽然常规TF-IDF计算单个文档内词语的重要性,c-TF-IDF计算整个主题内词语的重要性

c-TF-IDF背后的关键是将每个主题视为一个单独的文档,通过合并该主题内的所有文档。这种方法允许BERTopic提取表征每个主题的独特且有意义的词语。

算法公式

C-TF-IDF的权重 W x , c W_{x,c} Wx,c 由两部分相乘构成
W x , c = ∥ tf x , c ∥ ⏟ 类内词频 × log ⁡ ( 1 + A f x ) ⏟ 跨类稀有性 W_{x,c} = \underbrace{\|\text{tf}_{x,c}\|}_{\text{类内词频}} \times \underbrace{\log\left(1 + \frac{A}{f_x}\right)}_{\text{跨类稀有性}} Wx,c=类内词频 tfx,c×跨类稀有性 log(1+fxA)

  • 第一部分:词项 x x x 在类别 c c c 中的频率 ( t f x , c ) (tf_{x,c}) (tfx,c) ,反映词项在类内的“活跃程度”;
  • 第二部分:对数项,反映词项在跨类别中的“稀有性”(即词项在其他类别中出现的少,则权重越高)。

变量定义

  • ( t f x , c ) (tf_{x,c}) (tfx,c):词项 x x x 在类别 c c c 中的出现频率(例如,主题“机器学习”中“深度学习”一词的出现次数);
  • f x f_x fx :词项 x x x所有类别中的总频率(例如,“深度学习”在整个语料库(所有主题)中总共出现的次数);
  • A A A:每个类别的平均词数(例如,所有主题的平均文档长度,用于归一化跨类频率)。

公式的意义

C-TF-IDF 的设计目的是区分不同类别的独特性

  • 若词项 x x x 在类别 c c c 中频繁出现,即 tf x , c \text{tf}_{x,c} tfx,c,大,且在其他类别中很少出现,即 f x f_x fx,小,则 A f x \frac{A}{f_x} fxA 会很大,对数项 log ⁡ ( 1 + A f x ) \log\left(1 + \frac{A}{f_x}\right) log(1+fxA) 也随之增大,最终 W x , c W_{x,c} Wx,c 很高——这说明 x x x 是类别 c c c核心关键词
  • 反之,若词项 x x x 在所有类别中都频繁出现,即 f x f_x fx 大,则 A f x \frac{A}{f_x} fxA 很小,对数项趋近于 log ⁡ ( 1 ) = 0 \log(1) = 0 log(1)=0,权重 W x , c W_{x,c} Wx,c 很低——这说明 x x x 是通用词(如“的”“是”),不具备类别区分性。

与传统TF-IDF的区别

传统TF-IDF基于全局语料库计算词项重要性,而C-TF-IDF基于类别(主题) 计算,更贴合主题建模的需求:

  • 传统TF-IDF: TF-IDF ( x ) = tf x × log ⁡ ( N d f x ) \text{TF-IDF}(x) = \text{tf}_x \times \log\left(\frac{N}{df_x}\right) TF-IDF(x)=tfx×log(dfxN) N N N为文档总数, d f x df_x dfx 为含 x x x 的文档数

  • C-TF-IDF: W x , c = tf x , c × log ⁡ ( 1 + A f x ) W_{x,c} = \text{tf}_{x,c} \times \log\left(1 + \frac{A}{f_x}\right) Wx,c=tfx,c×log(1+fxA)(聚焦类别内频率与跨类稀有性)。

    简言之,C-TF-IDF 通过“类内活跃度”和“跨类稀有性”的组合,精准提取每个主题的独特关键词,是BERTopic实现“聚类→可解释主题”的核心算法之一。

代码说明

from typing import List
from sklearn.feature_extraction.text import TfidfTransformer  # 继承自 scikit-learn 的 TF-IDF 转换器
from sklearn.preprocessing import normalize  # 用于矩阵归一化
from sklearn.utils import check_array  # 用于验证输入矩阵格式
import numpy as np  # 数值计算
import scipy.sparse as sp  # 稀疏矩阵操作


class ClassTfidfTransformer(TfidfTransformer):
    """基于 scikit-learn 的 TfidfTransformer 扩展的ClassTfidfTransformer。
    
    C-TF-IDF 是一种面向多个类别的 TF-IDF 变体,通过将每个类别视为一个“超级文档”,
    计算词项在类别内的频率(TF)与跨类别的稀有性(IDF)的乘积,从而提取每个类别的核心关键词。

    Args:
        bm25_weighting: 是否使用 BM25 启发式 IDF 加权(替代标准 C-TF-IDF 公式)。
        reduce_frequent_words: 是否对归一化后的词袋矩阵取平方根,削弱高频词的影响。
        seed_words: 需要强化 IDF 权重的特定词列表(如领域关键词)。
        seed_multiplier: 强化因子(用于放大 `seed_words` 中词的 IDF 值)。
    """

    def __init__(
        self,
        bm25_weighting: bool = False,  # 是否启用 BM25 风格的 IDF 加权
        reduce_frequent_words: bool = False,  # 是否对高频词做平方根衰减
        seed_words: List[str] = None,  # 需要强化权重的种子词列表
        seed_multiplier: float = 2,  # 种子词的 IDF 强化倍数
    ):
        self.bm25_weighting = bm25_weighting  # BM25 加权开关
        self.reduce_frequent_words = reduce_frequent_words  # 高频词衰减开关
        self.seed_words = seed_words  # 种子词列表
        self.seed_multiplier = seed_multiplier  # 种子词强化倍数
        super(ClassTfidfTransformer, self).__init__()  # 调用父类构造函数

    def fit(self, X: sp.csr_matrix, multiplier: np.ndarray = None):
        """学习 IDF 向量(全局词项权重)。

        Args:
            X: 词项/token 计数矩阵(稀疏矩阵,形状为 [n_classes, n_terms])。
            multiplier: 可选的 IDF 乘数(用于调整特定词的 IDF 权重)。
        """
        # 验证输入矩阵格式(接受 csr/csc 稀疏矩阵)
        X = check_array(X, accept_sparse=("csr", "csc"))
        if not sp.issparse(X):
            X = sp.csr_matrix(X)  # 转换为 csr 稀疏矩阵
        dtype = np.float64  # 数据类型

        if self.use_idf:  # 若启用 IDF 计算
            _, n_features = X.shape  # 获取矩阵维度(n_classes, n_terms)

            # 计算词项在所有类别中的总频率(df = 词项 x 出现的类别数)
            df = np.squeeze(np.asarray(X.sum(axis=0)))  # 沿类别轴求和(得到每个词的总出现次数)

            # 计算每个类别的平均词数(作为正则化项)
            avg_nr_samples = int(X.sum(axis=1).mean())  # 沿词项轴求和后取均值

            # BM25 启发式 IDF 加权(替代标准 C-TF-IDF 公式)
            if self.bm25_weighting:
                idf = np.log(1 + ((avg_nr_samples - df + 0.5) / (df + 0.5)))
            
            # 标准 C-TF-IDF IDF 计算:log(1 + 平均类别词数 / 词项总频率)
            else:
                idf = np.log((avg_nr_samples / df) + 1)

            # 若提供了 multiplier(如种子词的强化因子),调整 IDF 值
            if multiplier is not None:
                idf = idf * multiplier

            # 构建对角矩阵(用于后续矩阵乘法,将 IDF 应用于每个词项)
            self._idf_diag = sp.diags(
                idf,
                offsets=0,  # 主对角线
                shape=(n_features, n_features),  # 方阵(n_terms × n_terms)
                format="csr",  # 保持 csr 格式
                dtype=dtype,
            )

        return self  # 返回 fitted 实例

    def transform(self, X: sp.csr_matrix):
        """将词项计数矩阵转换为 C-TF-IDF 矩阵。

        Args:
            X: 词项/ token 计数矩阵(稀疏矩阵,形状为 [n_classes, n_terms])。

        Returns:
            X: C-TF-IDF 矩阵(稀疏矩阵,形状与输入一致)。
        """
        if self.use_idf:  # 若启用 IDF
            # 第一步:L1 归一化(沿类别轴,使每个类别的词项频率总和为 1)
            X = normalize(X, axis=1, norm="l1", copy=False)

            # 第二步:若启用高频词衰减,对矩阵元素取平方根(削弱高频词权重)
            if self.reduce_frequent_words:
                X.data = np.sqrt(X.data)  # 仅修改非零元素的值

            # 第三步:乘以 IDF 对角矩阵(将 IDF 权重应用于每个词项)
            X = X * self._idf_diag

        return X  # 返回 C-TF-IDF 矩阵

代码关键逻辑说明

  1. C-TF-IDF 核心思想
    将每个类别(主题)视为一个“超级文档”,计算词项在该类别内的频率( T F TF TF)与跨类别稀有性( I D F IDF IDF)的乘积,从而突出每个类别的独特关键词。

  2. IDF 计算方式

    • 标准模式 i d f ( x ) = l o g ( 1 + A f x ) idf(x)=log(1 + \frac{A}{f_x}) idf(x)=log(1+fxA),其中 A A A 是平均类别词数, f x f_x fx 是词项 x x x 在所有类别中的总频率。

    • BM25 模式:采用 BM25 启发式公式,更适合处理稀疏数据(如短文本)。
      i d f ( x ) = l o g ( 1 + A − f x + 0.5 f x + 0.5 ) idf(x)=log(1 + \frac{A-f_x+0.5}{f_x+0.5}) idf(x)=log(1+fx+0.5Afx+0.5)

  3. 高频词衰减
    对归一化后的词袋矩阵取平方根,削弱“过于常见”的词(如停用词)的权重,使结果更聚焦于有区分度的关键词。
    ∥ t f x , c ∥ \sqrt{\begin{Vmatrix}tf_{x,c}\end{Vmatrix}} tfx,c

  4. 种子词强化
    通过 seed_wordsseed_multiplier,可以手动增强特定词(如领域关键词)的 IDF 权重,引导模型更重视这些词。

用法示例

from bertopic import BERTopic
from bertopic.vectorizers import ClassTfidfTransformer

ctfidf_model = ClassTfidfTransformer(
    bm25_weighting=True,
    reduce_frequent_words=True
)
topic_model = BERTopic(ctfidf_model=ctfidf_model)

ClassTfidfTransformer 中有两个值得探索的参数,分别是 bm25_weightingreduce_frequent_words

微调主题(Fine-tune Topics)

默认情况下,BERTopic 使用 c-TF-IDF 从分配到同一主题的文档中提取关键词。然而,这些关键词列表有时缺乏连贯性或难以解释。因此,加入了主题表示模型,可以改进默认生成的主题关键词,这些初始基于关键词的表示转换为更有意义的标签。

具体使用说明:地址

主题表示方法

方法描述何时使用要求
BaseRepresentation基础主题表示方法,直接使用聚类结果生成关键词通用场景,需要快速获取基础主题标签,BERTopic的默认主题表示方法,直接基于聚类结果提取关键词(如通过C-TF-IDF),无需额外模型无额外要求(默认方法)
TextGeneration利用大型语言模型生成自然语言主题描述当需要更自然的主题标签时,通过调用LLM(如GPT-4)生成描述性主题标签,而非仅关键词,适合需要“可读性强”的场景大型语言模型(如GPT系列)
ZeroShotClassification使用零样本分类将主题匹配到预定义标签当有明确预定义类别时,利用预训练的零样本分类模型(如DeBERTa),将主题与预定义类别匹配,适合有明确业务标签的场景。Transformer模型
KeyBERTInspired通过比较单词和代表性文档的嵌入来提取关键词通用主题标签,具有更好的语义相关性;BERTopic的核心方法之一,通过对比单词与文档嵌入的相似性提取关键词,语义相关性更强。嵌入模型
PartOfSpeech基于词性标签(如名词、形容词)提取主题当语法结构对主题理解很重要时,仅保留特定词性(如名词、形容词)作为主题关键词,适合需要语法结构清晰的场景(如学术文献主题提取)。spaCy(用于词性标注)
MaximalMarginalRelevance(MMR)选择最大化与主题相关性的多样化关键词当需要表示中的平衡多样性和相关性时嵌入模型
Cohere使用Cohere模型生成自然语言主题标签当需要高质量、可解释的主题描述时Cohere API密钥
OpenAI使用OpenAI模型生成自然语言主题标签当需要高质量、可解释的主题描述时OpenAI API密钥
LangChain利用LangChain框架进行主题表示(如调用链)当已经在堆栈中使用LangChain时LangChain包
LiteLLM通过LiteLLM统一接口调用各种LLM模型当需要灵活切换不同LLM服务时LiteLLM包
LlamaCPP通过llama.cpp使用本地LLM模型进行主题表示当需要离线主题表示时llama-cpp-python
VisualRepresentation生成多模态主题表示(融合文本和图像信息),专为多模态数据设计,可同时处理文本和图像信息,生成融合的主题表示当处理图像-文本混合数据时视觉模型(如CLIP)
补充说明:
  • Cohere/OpenAI/LangChain/LiteLLM/LlamaCPP:均属于“外部模型集成”,通过调用第三方服务或本地模型生成主题表示,满足不同部署需求(在线/离线、灵活性)。

KeyBERTInspired

该方法通过使用单词和文档嵌入为每个主题找到语义相关关键词来增强主题表示。

在用 c-TF-IDF 生成主题后,想要根据关键词/关键短语与每个主题中的文档集之间的语义关系进行一些微调,可以使用基于质心的技术来实现这一点,但这可能成本高,并且没有考虑聚类的结构。相反,可以利用 c-TF-IDF 为每个主题创建一组代表性文档,并使用这些文档作为更新的主题嵌入。然后,使用嵌入文档的相同嵌入模型来计算候选关键词与主题嵌入之间的相似度。

实现流程
Topic n
根据c-TF-IDF分数提取每个主题的前n个词
将c-TF-IDF采样的文档与主题c-TF-IDF进行比较
提取候选关键词
提取代表性文档
嵌入候选关键词
嵌入并平均文档
比较嵌入的关键词与嵌入的文档
  1. 为每个主题提取代表性文档
  2. 基于 c-TF-IDF 分数选择候选关键词
  3. 生成单词和文档的嵌入
  4. 通过平均代表性文档嵌入创建主题嵌入
  5. 计算关键词和主题嵌入之间的相似性
  6. 选择最相似的单词作为主题表示
代码说明
import numpy as np
import pandas as pd

from packaging import version
from scipy.sparse import csr_matrix
from typing import Mapping, List, Tuple, Union
from sklearn.metrics.pairwise import cosine_similarity
from bertopic.representation._base import BaseRepresentation  # 继承自BERTopic的基础表示类
from sklearn import __version__ as sklearn_version  # 获取scikit-learn版本,用于兼容性检查


class KeyBERTInspired(BaseRepresentation):
    def __init__(
            self,
            top_n_words: int = 10,
            nr_repr_docs: int = 5,
            nr_samples: int = 500,
            nr_candidate_words: int = 100,
            random_state: int = 42,
    ):
        """使用类似KeyBERT的模型微调主题表示。

        该算法遵循KeyBERT的逻辑,但进行了优化以提高推理速度。

        步骤如下:
        首先,提取每个主题的代表性文档(通过c-TF-IDF表示计算相似度);
        然后,提取候选关键词(基于c-TF-IDF分数);
        接着,嵌入候选关键词和代表性文档;
        最后,通过余弦相似度比较关键词与文档的嵌入,选出最相关的关键词。

        Args:
            top_n_words: 每个主题提取的前n个关键词数量。
            nr_repr_docs: 每个主题提取的代表性文档数量。
            nr_samples: 每个主题随机采样的候选文档数量(用于筛选代表性文档)。
            nr_candidate_words: 每个主题的候选关键词数量。
            random_state: 随机采样的种子(确保结果可复现)。
        """
        self.top_n_words = top_n_words  # 每个主题的目标关键词数量
        self.nr_repr_docs = nr_repr_docs  # 每个主题的代表性文档数量
        self.nr_samples = nr_samples  # 每个主题随机采样的候选文档数量
        self.nr_candidate_words = nr_candidate_words  # 每个主题的候选关键词数量
        self.random_state = random_state  # 随机采样种子(保证结果可复现)

    def extract_topics(
            self,
            topic_model,
            documents: pd.DataFrame,
            c_tf_idf: csr_matrix,
            topics: Mapping[str, List[Tuple[str, float]]],
            embeddings: np.ndarray = None,
    ) -> Mapping[str, List[Tuple[str, float]]]:
        """提取主题关键词。

        Args:
            topic_model: 已训练的BERTopic模型
            documents: 所有输入文档(DataFrame格式)
            c_tf_idf: 主题的c-TF-IDF表示(稀疏矩阵)
            topics: 基于c-TF-IDF计算的候选主题(字典,键为主题ID,值为关键词列表)
            embeddings: 预训练的文档嵌入(可选,若未提供则动态计算)

        Returns:
            updated_topics: 更新后的主题表示(每个主题的前n个关键词及分数)
        """
        # 1. 提取每个主题的代表性文档(通过c-TF-IDF相似度筛选)
        _, representative_docs, repr_doc_indices, _ = topic_model._extract_representative_docs(
            c_tf_idf, documents, topics, self.nr_samples, self.nr_repr_docs
        )

        # 2. 若存在预计算文档嵌入,提取代表性文档的嵌入(基于索引)
        repr_embeddings = None
        if embeddings is not None:
            repr_embeddings = [embeddings[index] for index in np.concatenate(repr_doc_indices)]

        # 3. 提取每个主题的候选关键词(基于c-TF-IDF分数)
        topics = self._extract_candidate_words(topic_model, c_tf_idf, topics)

        # 4. 计算关键词与文档的嵌入相似度,创建主题嵌入
        sim_matrix, words = self._extract_embeddings(
            topic_model, topics, representative_docs, repr_doc_indices, repr_embeddings
        )
        # 5. 基于相似度矩阵,提取每个主题的最佳关键词
        updated_topics = self._extract_top_words(words, topics, sim_matrix)

        return updated_topics

    def _extract_candidate_words(
            self,
            topic_model,
            c_tf_idf: csr_matrix,
            topics: Mapping[str, List[Tuple[str, float]]],
    ) -> Mapping[str, List[Tuple[str, float]]]:
        """为每个主题提取候选关键词(基于c-TF-IDF表示)。

        Args:
            topic_model: BERTopic模型
            c_tf_idf: 主题的c-TF-IDF表示(稀疏矩阵)
            topics: 基于c-TF-IDF计算的候选主题

        Returns:
            topics: 每个主题的候选关键词列表(前`self.nr_candidate_words`个)
        """
        labels = [int(label) for label in sorted(list(topics.keys()))]  # 获取所有主题ID(升序排列)

        # 兼容scikit-learn版本:获取特征名称(词项列表)
        if version.parse(sklearn_version) >= version.parse("1.0.0"):
            words = topic_model.vectorizer_model.get_feature_names_out()  # v1.0及以上版本
        else:
            words = topic_model.vectorizer_model.get_feature_names()  # 旧版本

        # 提取每个主题的前`self.nr_candidate_words`个词项(基于c-TF-IDF分数)
        indices = topic_model._top_n_idx_sparse(c_tf_idf, self.nr_candidate_words)  # 词项索引
        scores = topic_model._top_n_values_sparse(c_tf_idf, indices)  # 词项分数
        sorted_indices = np.argsort(scores, 1)  # 按分数升序排序索引
        indices = np.take_along_axis(indices, sorted_indices, axis=1)  # 重排索引(升序)
        scores = np.take_along_axis(scores, sorted_indices, axis=1)  # 重排分数(升序)

        # 构建每个主题的候选关键词列表(分数降序)
        topics = {
            label: [
                (words[word_index], score) if word_index is not None and score > 0 else ("", 0.00001)  # 过滤无效词项
                for word_index, score in zip(indices[index][::-1], scores[index][::-1])  # 反转数组(降序)
            ]
            for index, label in enumerate(labels)
        }
        # 只保留词项(忽略分数),截断至`self.nr_candidate_words`个
        topics = {label: list(zip(*values[: self.nr_candidate_words]))[0] for label, values in topics.items()}

        return topics

    def _extract_embeddings(
            self,
            topic_model,
            topics: Mapping[str, List[Tuple[str, float]]],
            representative_docs: List[str],
            repr_doc_indices: List[List[int]],
            repr_embeddings: np.ndarray = None,
    ) -> Union[np.ndarray, List[str]]:
        """提取代表性文档的嵌入,创建主题嵌入;再提取词项嵌入,计算词项与主题的相似度。

        Args:
            topic_model: BERTopic模型
            topics: 每个主题的候选关键词列表
            representative_docs: 代表性文档列表(扁平化)
            repr_doc_indices: 每个主题对应的代表性文档索引(二维列表)
            repr_embeddings: 代表性文档的预计算嵌入(可选)

        Returns:
            sim: 词项与主题的相似度矩阵(shape: [主题数, 词项数])
            vocab: 输入文档的全部词汇(去重)
        """
        # 1. 计算代表性文档的嵌入(若未提供预计算嵌入)
        if repr_embeddings is None:
            repr_embeddings = topic_model._extract_embeddings(representative_docs, method="document",
                                                              verbose=False)  # 动态计算文档嵌入

        # 2. 创建主题嵌入(每个主题的嵌入是其代表性文档嵌入的平均值)
        topic_embeddings = [np.mean(repr_embeddings[i[0]: i[-1] + 1], axis=0) for i in repr_doc_indices]  # 按主题分组求均值

        # 3. 提取词项嵌入并计算与主题嵌入的相似度
        vocab = list(set([word for words in topics.values() for word in words]))  # 去重后的全部候选词项
        word_embeddings = topic_model._extract_embeddings(vocab, method="document", verbose=False)  # 动态计算词项嵌入
        sim = cosine_similarity(topic_embeddings, word_embeddings)  # 余弦相似度矩阵(主题vs词项)

        return sim, vocab  # 返回相似度矩阵和词汇表

    def _extract_top_words(
            self,
            vocab: List[str],
            topics: Mapping[str, List[Tuple[str, float]]],
            sim: np.ndarray,
    ) -> Mapping[str, List[Tuple[str, float]]]:
        """基于相似度矩阵,提取每个主题的前`self.top_n_words`个关键词。

        Args:
            vocab: 输入文档的全部词汇(去重)
            topics: 每个主题的候选关键词列表
            sim: 词项与主题的相似度矩阵(shape: [主题数, 词项数])

        Returns:
            updated_topics: 更新后的主题表示(每个主题的前n个关键词及分数,按相似度降序)
        """
        labels = [int(label) for label in sorted(list(topics.keys()))]  # 获取所有主题ID(升序排列)
        updated_topics = {}
        for i, topic in enumerate(labels):
            # 定位候选词项在词汇表中的索引
            indices = [vocab.index(word) for word in topics[topic]]
            # 提取当前主题与候选词项的相似度分数
            values = sim[:, indices][i]
            # 获取相似度最高的前`self.top_n_words`个词项索引
            word_indices = [indices[index] for index in np.argsort(values)[-self.top_n_words:]]
            # 构建关键词列表(分数降序)
            updated_topics[topic] = [
                                        (vocab[index], val) for val, index in
                                        zip(np.sort(values)[-self.top_n_words:], word_indices)
                                    ][::-1]  # 反转数组(降序)

        return updated_topics

用法示例
from bertopic.representation import KeyBERTInspired
from bertopic import BERTopic

representation_model = KeyBERTInspired(
    top_n_words=10,
    nr_repr_docs=5
)

topic_model = BERTopic(representation_model=representation_model)

MaximalMarginalRelevance

最大边际相关性(Maximum Marginal Relevance, MMR)选择在相关性和多样性之间平衡的关键词,既希望关键词与主题(查询)相关,又希望关键词之间差异较大(避免重复)。

用于平衡查询与文档的相关性以及文档间的多样性的信息检索

在计算关键词权重时,通常不考虑是否已经在主题中存在相似的关键词。像“car”和“cars”这样的词本质上代表相同的信息,并且经常是冗余的。

为了减少这种冗余并提高关键词的多样性,使用最大边缘相关性(MMR)算法。MMR 考虑关键词/短语与文档的相似性,以及已选择关键词/短语的相似性。这导致选择一组在文档方面最大化其内部多样性的关键词。

算法公式

M M R = arg ⁡ max ⁡ D i ∈ R S [ λ ⋅ Sim 1 ( D i , Q ) − ( 1 − λ ) ⋅ max ⁡ D j ∈ S Sim 2 ( D i , D j ) ] MMR = \arg\max_{D_i \in R^S} \left[ \lambda \cdot \text{Sim}_1(D_i, Q) - (1-\lambda) \cdot \max_{D_j \in S} \text{Sim}_2(D_i, D_j) \right] MMR=argDiRSmax[λSim1(Di,Q)(1λ)DjSmaxSim2(Di,Dj)]

其核心思想是:

  • 既希望选出的文档与查询高度相关( S i m 1 ( D i , Q ) Sim₁(Dᵢ, Q) Sim1(Di,Q));
  • 又希望选出的文档彼此之间差异较大(避免冗余,即 max ⁡ D j ∈ S Sim 2 ( D i , D j ) \max_{D_j \in S} \text{Sim}_2(D_i, D_j) maxDjSSim2(Di,Dj))。

符号解析:

符号含义
M M R MMR MMR最大边际相关性(目标函数,需最大化)
arg ⁡ max ⁡ \arg\max argmax取得使括号内表达式最大的 D i D_i Di(即最优文档)
D i D_i Di待评估的文档(属于候选文档集 R S R^S RS
R S R^S RS候选文档集( S S S 表示文档数量, R S R^S RS 表示 S S S 个文档组成的集合)
λ \lambda λ平衡系数( 0 ≤ λ ≤ 1 0 \leq \lambda \leq 1 0λ1):控制“相关性”与“多样性”的权重
Sim 1 \text{Sim}_1 Sim1文档 D i D_i Di 与查询 Q Q Q 的相似度(如余弦相似度)
Sim 2 \text{Sim}_2 Sim2文档 D i D_i Di 与文档 D j D_j Dj 的相似度(如余弦相似度)
D j ∈ S D_j \in S DjS属于已选中文档集 S S S 的文档( S S S 是已选文档的集合)
公式逻辑:
  1. 目标:找到文档 D i D_i Di,使得: λ ⋅ Sim 1 ( D i , Q ) − ( 1 − λ ) ⋅ max ⁡ D j ∈ S Sim 2 ( D i , D j ) \lambda \cdot \text{Sim}_1(D_i, Q) - (1-\lambda) \cdot \max_{D_j \in S} \text{Sim}_2(D_i, D_j) λSim1(Di,Q)(1λ)maxDjSSim2(Di,Dj)
    取得最大值。
  2. 参数作用
    • λ \lambda λ 越大,越重视文档与查询的相关性 Sim 1 \text{Sim}_1 Sim1);
    • λ \lambda λ 越小,越重视文档间的多样性(即 max ⁡ D j ∈ S Sim 2 \max_{D_j \in S} \text{Sim}_2 maxDjSSim2 越小,文档差异越大)。
  3. 约束 D i D_i Di 必须来自候选文档集 R S R^S RS(即未选中的文档)。
实现流程
  1. 计算候选关键词与主题之间的相似性
  2. 也考虑已选关键词之间的相似性
  3. 使用多样性参数控制相关性和多样性之间的权衡
  4. 选择最大化边际相关性的关键词
代码说明
import warnings
import numpy as np
import pandas as pd
from typing import List, Mapping, Tuple
from scipy.sparse import csr_matrix
from sklearn.metrics.pairwise import cosine_similarity
from bertopic.representation._base import BaseRepresentation


class MaximalMarginalRelevance(BaseRepresentation):
    """计算候选关键词与文档之间的最大边际相关性(MMR)

    MMR同时考虑关键词/短语与文档的相似性,以及已选关键词/短语之间的相似性。
    这使得选出的关键词在保持与文档相关性的同时,最大化其内部多样性。

    Args:
        diversity: 所选关键词/短语的多样性程度。
                   取值范围在0到1之间,0表示无多样性,1表示多样性最高。
        top_n_words: 返回的关键词/短语数量

    """

    def __init__(self, diversity: float = 0.1, top_n_words: int = 10):
        self.diversity = diversity  # 多样性参数
        self.top_n_words = top_n_words  # 返回的关键词数量

    def extract_topics(
            self,
            topic_model,
            documents: pd.DataFrame,
            c_tf_idf: csr_matrix,
            topics: Mapping[str, List[Tuple[str, float]]],
    ) -> Mapping[str, List[Tuple[str, float]]]:
        """提取主题表示。

        Args:
            topic_model: BERTopic模型实例
            documents: 
            c_tf_idf: 
            topics: 通过c-TF-IDF计算出的候选主题

        Returns:
            updated_topics: 更新后的主题表示
        """
        # 检查BERTopic模型是否初始化了嵌入模型
        if topic_model.embedding_model is None:
            warnings.warn(
                "MaximalMarginalRelevance只能在BERTopic实例化时"
                "指定了`embedding_model`参数的情况下使用。"
            )
            return topics  # 无嵌入模型则直接返回原始主题

        updated_topics = {}
        # 遍历每个主题及其候选词
        for topic, topic_words in topics.items():
            # 提取候选词列表
            words = [word[0] for word in topic_words]
            # 获取每个候选词的词嵌入向量
            word_embeddings = topic_model._extract_embeddings(words, method="word", verbose=False)
            # 获取整个主题的嵌入向量(通过拼接所有候选词)
            topic_embedding = topic_model._extract_embeddings(" ".join(words), method="word", verbose=False).reshape(
                1, -1
            )
            # 使用MMR算法选择最优关键词
            topic_words = mmr(
                topic_embedding,
                word_embeddings,
                words,
                self.diversity,
                self.top_n_words,
            )
            # 更新主题表示:保留MMR选中的词及其原始权重
            updated_topics[topic] = [(word, value) for word, value in topics[topic] if word in topic_words]
        return updated_topics


def mmr(
        doc_embedding: np.ndarray,
        word_embeddings: np.ndarray,
        words: List[str],
        diversity: float = 0.1,
        top_n: int = 10,
) -> List[str]:
    """最大边际相关性算法实现。

    Args:
        doc_embedding: 文档的嵌入向量
        word_embeddings: 候选关键词/短语的嵌入向量
        words: 候选关键词/短语列表
        diversity: 所选嵌入向量的多样性。
                   取值范围在0到1之间。
        top_n: 返回的前n个结果

    Returns:
            List[str]: 选中的关键词/短语列表
    """
    # 计算词与文档的相似度,以及词与词之间的相似度
    word_doc_similarity = cosine_similarity(word_embeddings, doc_embedding)  # (n_words, 1)
    word_similarity = cosine_similarity(word_embeddings)  # (n_words, n_words)

    # 初始化:选择与文档最相似的词作为第一个关键词
    keywords_idx = [np.argmax(word_doc_similarity)]
    # 剩余候选词索引(排除已选词)
    candidates_idx = [i for i in range(len(words)) if i != keywords_idx[0]]

    # 迭代选择剩余的top_n-1个关键词
    for _ in range(top_n - 1):
        # 获取候选词与文档的相似度
        candidate_similarities = word_doc_similarity[candidates_idx, :]
        # 获取候选词与已选关键词的最大相似度
        target_similarities = np.max(word_similarity[candidates_idx][:, keywords_idx], axis=1)

        # 计算MMR得分:平衡相关性和多样性
        mmr_scores = (1 - diversity) * candidate_similarities - diversity * target_similarities.reshape(-1, 1)
        # 选择MMR得分最高的候选词
        mmr_idx = candidates_idx[np.argmax(mmr_scores)]

        # 更新已选关键词和候选词列表
        keywords_idx.append(mmr_idx)
        candidates_idx.remove(mmr_idx)

    # 返回选中的关键词列表
    return [words[idx] for idx in keywords_idx]

用法示例
from bertopic.representation import MaximalMarginalRelevance
from bertopic import BERTopic

representation_model = MaximalMarginalRelevance(
    diversity=0.3,
    top_n_words=10
)

topic_model = BERTopic(representation_model=representation_model)

Zero-Shot Classification

在某些应用场景中,文本集合中可能已经有一组候选标签,希望自动分配给部分主题。尽管我们可以使用监督式用 BERTopic 来实现这一点,但也可以使用零样本分类来为主题分配标签。

可以利用🤗 Hugging Face 中相关模型

使用预训练的 Transformer 模型将主题分类到预定义类别,而无需特定训练数据。

将模型输入通过 c-TF-IDF 生成的关键词和一组候选标签。如果对于某个主题,找到一个足够相似的标签,则将其分配。如果没有,则保留原始的 c-TF-IDF 关键词。

实现流程
  1. 取一组候选主题标签
  2. 使用 Transformer 模型将主题关键词分类到这些标签
  3. 如果主题超过最小概率阈值,则分配标签
代码说明
import pandas as pd
from transformers import pipeline  # 导入Hugging Face的管道工具(用于零样本分类)
from transformers.pipelines.base import Pipeline  
from scipy.sparse import csr_matrix  
from typing import Mapping, List, Tuple, Any  
from bertopic.representation._base import BaseRepresentation  # 继承自BERTopic的基础表示类


class ZeroShotClassification(BaseRepresentation):
    """基于候选标签的主题关键词零样本分类。

    该类通过零样本分类模型(如BART-Large-MNLI),将主题关键词与预定义的候选标签进行匹配,
    为每个主题分配最相关的标签(若概率超过阈值)。

    Args:
        candidate_topics: 预定义的候选标签列表(如["科技", "医疗", "教育"])
        model: 零样本分类模型(支持字符串或已初始化的transformers管道)
        pipeline_kwargs: 传递给transformers管道的关键字参数(如`{"multi_label": True}`启用多标签分类)
        min_prob: 分配标签的最小概率阈值(仅当预测概率≥此值时,才将标签分配给主题)
    """

    def __init__(
            self,
            candidate_topics: List[str],
            model: str = "facebook/bart-large-mnli",
            pipeline_kwargs: Mapping[str, Any] = {},
            min_prob: float = 0.8,
    ):
        """初始化零样本分类模型。

        Args:
            candidate_topics: 预定义的候选标签列表(如["科技", "医疗", "教育"])
            model: 零样本分类模型(支持字符串或已初始化的transformers管道)
            pipeline_kwargs: 传递给transformers管道的关键字参数(如`{"multi_label": True}`)
            min_prob: 分配标签的最小概率阈值(仅当预测概率≥此值时,才将标签分配给主题)
        """
        self.candidate_topics = candidate_topics  # 存储预定义的候选标签
        # 初始化零样本分类模型(支持字符串或已存在的管道对象)
        if isinstance(model, str):
            self.model = pipeline("zero-shot-classification", model=model)  # 字符串模式:自动加载模型
        elif isinstance(model, Pipeline):
            self.model = model  # 已存在的管道对象:直接使用
        else:
            raise ValueError(
                "确保传入的模型是字符串(指向Hugging Face模型)或已初始化的transformers.pipeline对象。"
            )
        self.pipeline_kwargs = pipeline_kwargs  # 存储管道关键字参数(如多标签设置)
        self.min_prob = min_prob  # 存储最小概率阈值

    def extract_topics(
            self,
            topic_model,
            documents: pd.DataFrame,
            c_tf_idf: csr_matrix,
            topics: Mapping[str, List[Tuple[str, float]]],
    ) -> Mapping[str, List[Tuple[str, float]]]:
        """提取并更新主题表示(通过零样本分类为主题分配标签)。

        Args:
            topic_model: BERTopic模型(未直接使用,保留接口兼容性)
            documents: 输入文档(未直接使用,保留接口兼容性)
            c_tf_idf: 主题的c-TF-IDF表示(未直接使用,保留接口兼容性)
            topics: 基于c-TF-IDF计算的主题关键词(字典,键为主题ID,值为关键词列表)

        Returns:
            updated_topics: 更新后的主题表示(包含零样本分类标签)
        """
        # 1. 准备主题描述(将每个主题的关键词拼接成文本,作为分类输入)
        topic_descriptions = [
            " ".join([word for word, _ in topics[topic]])  # 提取每个主题的所有关键词,拼接成字符串
            for topic in topics.keys()  # 遍历所有主题ID
        ]

        # 2. 执行零样本分类(对每个主题描述进行分类)
        classifications = self.model(topic_descriptions, self.candidate_topics, **self.pipeline_kwargs)

        # 3. 构建更新后的主题表示(包含分类标签)
        updated_topics = {}
        for topic, classification in zip(topics.keys(), classifications):  # 遍历每个主题及其分类结果
            original_topic_keywords = topics[topic]  # 获取原始主题关键词

            # 多标签分类场景(如`{"multi_label": True}`)
            if self.pipeline_kwargs.get("multi_label"):
                topic_labels = []  # 存储符合条件的标签
                for label, score in zip(classification["labels"], classification["scores"]):  # 遍历标签及其分数
                    if score > self.min_prob:  # 若分数超过阈值,则添加标签
                        topic_labels.append((label, score))

                # 若无符合条件标签,则保留原始关键词;否则补充至10个(不足时用空标签填充)
                if not topic_labels:
                    topic_labels = original_topic_keywords[:10]  # 无标签时保留原始前10个关键词
                elif len(topic_labels) < 10:
                    topic_labels += [("", 0) for _ in range(10 - len(topic_labels))]  # 补充空标签至10个

                updated_topics[topic] = topic_labels  # 更新主题表示

            # 单标签分类场景(默认)
            else:
                # 若最高分标签的概率超过阈值,则替换原始关键词;否则保留原始关键词
                if classification["scores"][0] > self.min_prob:
                    updated_topics[topic] = [(classification["labels"][0], classification["scores"][0])]  # 添加标签
                else:
                    updated_topics[topic] = original_topic_keywords  # 无标签时保留原始关键词

        return updated_topics  # 返回更新后的主题表示

用法示例
from bertopic.representation import ZeroShotClassification
from bertopic import BERTopic
 
# 定义候选主题
candidate_topics = [
    "技术和计算机",
    "政治和政府",
    "健康和医学",
    "体育和娱乐",
    "艺术和娱乐"
]

representation_model = ZeroShotClassification(
    candidate_topics,
    model="facebook/bart-large-mnli",
    min_prob=0.7,
    pipeline_kwargs={"multi_label": True}
)

topic_model = BERTopic(representation_model=representation_model)

PartOfSpeech

通过 c-TF-IDF 提取的候选主题不考虑关键词的词性,因为从所有文档中提取名词短语在计算上代价比较高。相反,可以利用 c-TF-IDF 对最能代表主题的关键词子集和文档进行词性分析。

具体方法,找到包含候选主题中通过 c-TF-IDF 计算出的关键词的文档。这些文档作为从其中 Spacy 模型可以提取每个主题的一组候选关键词的代表集。这些候选关键词首先通过 Spacy 的词性标注模块进行筛选,以查看它们是否与 DEFAULT_PATTERNS 匹配。遵循 Spacy 的基于规则的匹配。然后,所得关键词按其各自的 c-TF-IDF 值进行排序。

DEFAULT_PATTERNS = [
            [{'POS': 'ADJ'}, {'POS': 'NOUN'}],
            [{'POS': 'NOUN'}],
            [{'POS': 'ADJ'}]
]
实现流程
  1. 从c-TF-IDF提取的候选主题中,找到包含这些关键词的文档
  2. 这些候选文档作为代表集,供Spacy模型提取每个主题的候选关键词
  3. 候选关键词首先根据DEFAULT_PATTERNS或用户自定义模式进行筛选
  4. 最后根据c-TF-IDF值对结果关键词排序
代码说明
import numpy as np
import pandas as pd

import spacy
from spacy.matcher import Matcher
from spacy.language import Language

from packaging import version
from scipy.sparse import csr_matrix
from typing import List, Mapping, Tuple, Union
from sklearn import __version__ as sklearn_version
from bertopic.representation._base import BaseRepresentation


class PartOfSpeech(BaseRepresentation):
    """基于词性(Part-of-Speech)提取主题关键词的表示模型

    默认词性模式
    DEFAULT_PATTERNS = [
                [{'POS': 'ADJ'}, {'POS': 'NOUN'}],  # 形容词+名词
                [{'POS': 'NOUN'}],                  # 名词
                [{'POS': 'ADJ'}]                    # 形容词
    ]

    Args:
        model: 使用的Spacy模型
        top_n_words: 提取的前n个关键词数量
        pos_patterns: Spacy使用的词性模式,详见 https://spacy.io/usage/rule-based-matching
    """

    def __init__(
            self,
            model: Union[str, Language] = "en_core_web_sm",
            top_n_words: int = 10,
            pos_patterns: List[str] = None,
    ):
        # 初始化Spacy模型
        if isinstance(model, str):
            self.model = spacy.load(model)  # 从字符串加载模型
        elif isinstance(model, Language):
            self.model = model  # 直接使用已加载的模型
        else:
            raise ValueError(
                "请确保传入的Spacy模型是字符串(引用Spacy模型)或Spacy nlp对象"
            )

        self.top_n_words = top_n_words  # 设置要提取的关键词数量

        # 设置词性模式
        if pos_patterns is None:
            self.pos_patterns = [
                [{"POS": "ADJ"}, {"POS": "NOUN"}],  # 形容词+名词
                [{"POS": "NOUN"}],  # 单独名词
                [{"POS": "ADJ"}],  # 单独形容词
            ]
        else:
            self.pos_patterns = pos_patterns  # 使用用户自定义模式

    def extract_topics(
            self,
            topic_model,
            documents: pd.DataFrame,
            c_tf_idf: csr_matrix,
            topics: Mapping[str, List[Tuple[str, float]]],
    ) -> Mapping[str, List[Tuple[str, float]]]:
        """提取主题关键词

        Args:
            topic_model: BERTopic模型实例
            documents: 所有输入文档
            c_tf_idf: 未使用(保留参数)
            topics: 使用c-TF-IDF计算的候选主题

        Returns:
            updated_topics: 更新后的主题表示
        """
        # 创建Spacy匹配器并添加词性模式
        matcher = Matcher(self.model.vocab)
        matcher.add("Pattern", self.pos_patterns)

        candidate_topics = {}
        # 遍历每个主题及其候选关键词
        for topic, values in topics.items():
            keywords = list(zip(*values))[0]  # 提取关键词(忽略c-TF-IDF值)

            # 提取候选文档
            candidate_documents = []
            for keyword in keywords:
                # 筛选属于当前主题的文档
                selection = documents.loc[documents.Topic == topic, :]
                # 在这些文档中查找包含当前关键词的文档
                selection = selection.loc[selection.Document.str.contains(keyword, regex=False), "Document"]
                if len(selection) > 0:
                    # 每个关键词最多取2个文档
                    for document in selection[:2]:
                        candidate_documents.append(document)
            candidate_documents = list(set(candidate_documents))  # 去重

            # 从候选文档中提取关键词
            docs_pipeline = self.model.pipe(candidate_documents)  # 批量处理文档
            updated_keywords = []
            for doc in docs_pipeline:
                matches = matcher(doc)  # 匹配词性模式
                for _, start, end in matches:
                    updated_keywords.append(doc[start:end].text)  # 提取匹配的短语
            candidate_topics[topic] = list(set(updated_keywords))  # 去重后存储

        # 处理Scikit-Learn版本差异:get_feature_names在1.0版本后已弃用
        if version.parse(sklearn_version) >= version.parse("1.0.0"):
            words = list(topic_model.vectorizer_model.get_feature_names_out())
        else:
            words = list(topic_model.vectorizer_model.get_feature_names())

        # 创建词汇表索引映射
        words_lookup = dict(zip(words, range(len(words))))
        updated_topics = {topic: [] for topic in topics.keys()}  # 初始化更新后的主题

        # 为每个主题计算关键词的c-TF-IDF值并排序
        for topic, candidate_keywords in candidate_topics.items():
            # 获取候选关键词在词汇表中的索引(仅保留存在的词)
            word_indices = np.sort(
                [words_lookup.get(keyword) for keyword in candidate_keywords if keyword in words_lookup]
            )
            # 从c-TF-IDF矩阵中提取当前主题的这些关键词的值
            vals = topic_model.c_tf_idf_[:, word_indices][topic + topic_model._outliers]
            # 获取按c-TF-IDF值排序的前top_n_words个关键词
            indices = np.argsort(np.array(vals.todense().reshape(1, -1))[0])[-self.top_n_words:][::-1]
            vals = np.sort(np.array(vals.todense().reshape(1, -1))[0])[-self.top_n_words:][::-1]
            # 组合关键词和对应的c-TF-IDF值
            topic_words = [(words[word_indices[index]], val) for index, val in zip(indices, vals)]
            updated_topics[topic] = topic_words
            # 如果关键词不足top_n_words,用空字符串和0填充
            if len(updated_topics[topic]) < self.top_n_words:
                updated_topics[topic] += [("", 0) for _ in range(self.top_n_words - len(updated_topics[topic]))]

        return updated_topics

用法示例
from bertopic.representation import PartOfSpeech
from bertopic import BERTopic

self_pos_patterns = [
            [{'POS': 'ADJ'}, {'POS': 'NOUN'}],
            [{'POS': 'NOUN'}], [{'POS': 'ADJ'}]
]
representation_model = PartOfSpeech(
    model="zh_core_web_sm",
    top_n_words=10,
    pos_patterns=self_pos_patterns
)

topic_model = BERTopic(representation_model=representation_model)

基于LLM主题表示

具体使用说明:地址

实现流程

OpenAI

使用 OpenAI 的语言模型(如 GPT-4)基于代表性文档和关键词生成自然语言主题标签。

BERTopic 还支持其他 LLM 框架:

  • LlamaCPP:用于本地 LLM 集成
  • TextGeneration:使用 Hugging Face transformers
  • Cohere:与 Cohere 的 API 集成
  • LiteLLM:用于标准化 LLM API 访问
  • LangChain:与 LangChain 框架集成
  • VisualRepresentation:灵活的多模态主题建模,可以处理组合的文本和图像或仅图像

这些方法遵循与 OpenAI 实现类似的模式,但使用它们特定的依赖和模型

代码说明
import time
import openai
import pandas as pd
from tqdm import tqdm
from scipy.sparse import csr_matrix
from typing import Mapping, List, Tuple, Any, Union, Callable
from bertopic.representation._base import BaseRepresentation
from bertopic.representation._utils import (
    retry_with_exponential_backoff,
    truncate_document,
    validate_truncate_document_parameters,
)

# 默认聊天提示模板
DEFAULT_CHAT_PROMPT = """你将从给定的文档和关键词中提取一个简短的主题标签。
以下是之前创建的两个主题示例:

# 示例1
此主题的样本文本:
- 大多数文化中的传统饮食主要是以植物为基础,搭配少量肉类,但随着工业化肉类生产和工厂化养殖的兴起,肉类已成为主食。
- 肉类,尤其是牛肉,在排放方面是最糟糕的食物。
- 吃肉不会让你成为坏人,不吃肉也不会让你成为好人。

关键词:meat beef eat eating emissions steak food health processed chicken
主题:吃肉对环境的影响

# 示例2
此主题的样本文本:
- 我几周前就订购了产品,但至今仍未收到!
- 网站上说只需要几天就能送达,但我仍未收到我的产品。
- 我收到一条消息说我收到了显示器,但事实并非如此!
- 交货时间比建议的长了一个月...

关键词:deliver weeks product shipping long delivery received arrived arrive week
主题:运输和交付问题

# 你的任务
此主题的样本文本:
[DOCUMENTS]

关键词:[KEYWORDS]

根据以上信息,提取一个简短的主题标签(最多三个单词),格式如下:
topic: <topic_label>
"""

# 默认系统提示
DEFAULT_SYSTEM_PROMPT = "你是一个从文本中提取高级主题的助手。"


class OpenAI(BaseRepresentation):
    r"""使用OpenAI API生成主题标签,基于其完成或聊天完成模型。


    Args:
        client: `openai.OpenAI` 客户端
        model: 使用的OpenAI模型,默认为 `"gpt-4o-mini"`
        generator_kwargs: 传递给 `openai.Completion.create` 的参数
                          用于微调输出
        prompt: 模型中使用的提示。如果未提供提示,
                则使用 `self.default_prompt_`。
                注意:在提示中使用 `"[KEYWORDS]"` 和 `"[DOCUMENTS]"`
                来决定插入关键词和文档的位置
        system_prompt: 模型中使用的系统提示。如果未提供系统提示,
                       则使用 `self.default_system_prompt_`
        delay_in_seconds: 连续提示之间的延迟(秒)
                          以防止RateLimitErrors
        exponential_backoff: 使用随机指数退避重试请求。
                             当遇到速率限制错误时短暂休眠,
                             然后重试请求。如果遇到错误则增加休眠时间,
                             直到10次不成功的请求。
                             如果为True,则覆盖 `delay_in_seconds`
        nr_docs: 如果使用带有 `["DOCUMENTS"]` 标签的提示,
                 传递给OpenAI的文档数量
        diversity: 传递给OpenAI的文档多样性。
                   接受0到1之间的值。较高的值
                   会导致传递更多样化的文档,
                   而较低的值传递更相似的文档
        doc_length: 每个文档的最大长度。如果文档更长,
                    将被截断。如果为None,则传递整个文档
        tokenizer: 用于计算文档长度的分词器
                   * 如果tokenizer是'char',则文档被分割成
                     字符,这些字符被计数以遵守 `doc_length`
                   * 如果tokenizer是'whitespace',文档被分割成
                     由空格分隔的单词。这些单词被计数
                     并根据 `doc_length` 截断
                   * 如果tokenizer是'vectorizer',则使用内部CountVectorizer
                     对文档进行分词。这些标记被计数
                     并根据 `doc_length` 截断
                   * 如果tokenizer是可调用对象,则使用该可调用对象
                     对文档进行分词。这些标记被计数
                     并根据 `doc_length` 截断
        **kwargs: 传递给 `openai.Completion.create` 的其他参数

    """

    def __init__(
            self,
            client,
            model: str = "gpt-4o-mini",
            prompt: str = None,
            system_prompt: str = None,
            generator_kwargs: Mapping[str, Any] = {},
            delay_in_seconds: float = None,
            exponential_backoff: bool = False,
            nr_docs: int = 4,
            diversity: float = None,
            doc_length: int = None,
            tokenizer: Union[str, Callable] = None,
            **kwargs,
    ):
        self.client = client
        self.model = model

        # 设置提示词
        if prompt is None:
            self.prompt = DEFAULT_CHAT_PROMPT
        else:
            self.prompt = prompt

        # 设置系统提示词
        if system_prompt is None:
            self.system_prompt = DEFAULT_SYSTEM_PROMPT
        else:
            self.system_prompt = system_prompt

        self.default_prompt_ = DEFAULT_CHAT_PROMPT
        self.default_system_prompt_ = DEFAULT_SYSTEM_PROMPT
        self.delay_in_seconds = delay_in_seconds
        self.exponential_backoff = exponential_backoff
        self.nr_docs = nr_docs
        self.diversity = diversity
        self.doc_length = doc_length
        self.tokenizer = tokenizer
        # 验证文档截断参数
        validate_truncate_document_parameters(self.tokenizer, self.doc_length)

        self.prompts_ = []

        self.generator_kwargs = generator_kwargs
        # 处理生成参数中的模型覆盖
        if self.generator_kwargs.get("model"):
            self.model = generator_kwargs.get("model")
            del self.generator_kwargs["model"]
        if self.generator_kwargs.get("prompt"):
            del self.generator_kwargs["prompt"]
        if not self.generator_kwargs.get("stop"):
            self.generator_kwargs["stop"] = "\n"

    def extract_topics(
            self,
            topic_model,
            documents: pd.DataFrame,
            c_tf_idf: csr_matrix,
            topics: Mapping[str, List[Tuple[str, float]]],
    ) -> Mapping[str, List[Tuple[str, float]]]:
        """提取主题。

        Args:
            topic_model: BERTopic模型
            documents: 所有输入文档
            c_tf_idf: 主题的c-TF-IDF表示
            topics: 使用c-TF-IDF计算的候选主题

        Returns:
            updated_topics: 更新后的主题表示
        """
        # 提取每个主题的前n个代表性文档
        repr_docs_mappings, _, _, _ = topic_model._extract_representative_docs(
            c_tf_idf, documents, topics, 500, self.nr_docs, self.diversity
        )

        # 使用OpenAI的语言模型生成主题标签
        updated_topics = {}
        # 遍历每个主题及其代表性文档
        for topic, docs in tqdm(repr_docs_mappings.items(), disable=not topic_model.verbose):
            # 截断文档到指定长度
            truncated_docs = [truncate_document(topic_model, self.doc_length, self.tokenizer, doc) for doc in docs]
            # 创建提示词
            prompt = self._create_prompt(truncated_docs, topic, topics)
            self.prompts_.append(prompt)

            # 添加延迟以避免速率限制
            if self.delay_in_seconds:
                time.sleep(self.delay_in_seconds)

            # 构建消息列表
            messages = [
                {"role": "system", "content": self.system_prompt},
                {"role": "user", "content": prompt},
            ]
            kwargs = {
                "model": self.model,
                "messages": messages,
                **self.generator_kwargs,
            }
            # 使用指数退避或直接调用API
            if self.exponential_backoff:
                response = chat_completions_with_backoff(self.client, **kwargs)
            else:
                response = self.client.chat.completions.create(**kwargs)

            # 检查是否实际生成了内容
            # 处理OpenAI内容过滤器可能的问题(#1570)
            # 处理OpenAI返回None类型对象的问题(#2176)
            if response and hasattr(response.choices[0].message, "content"):
                label = response.choices[0].message.content.strip().replace("topic: ", "")
            else:
                label = "No label returned"

            updated_topics[topic] = [(label, 1)]

        return updated_topics

    def _create_prompt(self, docs, topic, topics):
        """创建提示词"""
        # 获取当前主题的关键词
        keywords = list(zip(*topics[topic]))[0]

        # 使用默认聊天提示
        if self.prompt == DEFAULT_CHAT_PROMPT:
            prompt = self.prompt.replace("[KEYWORDS]", ", ".join(keywords))
            prompt = self._replace_documents(prompt, docs)

        # 使用自定义提示(支持[KEYWORDS]和[DOCUMENTS]标签)
        else:
            prompt = self.prompt
            if "[KEYWORDS]" in prompt:
                prompt = prompt.replace("[KEYWORDS]", ", ".join(keywords))
            if "[DOCUMENTS]" in prompt:
                prompt = self._replace_documents(prompt, docs)

        return prompt

    @staticmethod
    def _replace_documents(prompt, docs):
        """替换提示词中的[DOCUMENTS]标签"""
        to_replace = ""
        for doc in docs:
            to_replace += f"- {doc}\n"
        prompt = prompt.replace("[DOCUMENTS]", to_replace)
        return prompt


def chat_completions_with_backoff(client, **kwargs):
    """使用指数退避机制调用OpenAI聊天完成API"""
    return retry_with_exponential_backoff(
        client.chat.completions.create,
        errors=(openai.RateLimitError,),
    )(**kwargs)

用法示例

OpenAI

import openai
from bertopic.representation import OpenAI
from bertopic import BERTopic

client = openai.OpenAI(api_key="your-api-key")
representation_model = OpenAI(
    client,
    model="gpt-4o-mini",  # 选择你要使用的模型
    delay_in_seconds=5,  # 防止速率限制
)
 
# 在 BERTopic 中使用表示模型
topic_model = BERTopic(representation_model=representation_model)

多模态模型

import glob
from bertopic.backend import MultiModalBackend
from bertopic.representation import VisualRepresentation

# 加载数据
images = list(glob.glob('photos/*.jpg'))

# 设置图像嵌入和视觉表示
## 创建图像嵌入模型
embedding_model = MultiModalBackend('clip-ViT-B-32', batch_size=32)

## 创建具有图像到文本能力的视觉表示
representation_model = {
    "Visual_Aspect": VisualRepresentation(
        image_to_text_model="nlpconnect/vit-gpt2-image-captioning"
    )
}
# 创建并拟合主题模型
## 创建具有图像能力的主题模型
topic_model = BERTopic(
    embedding_model=embedding_model,
    representation_model=representation_model,
    min_topic_size=30
)

## 仅使用图像(无文档)拟合模型
topics, probs = topic_model.fit_transform(documents=None, images=images)

链式模型主题表示

实现流程

以上所有模型都可以利用由 c-TF-IDF 生成的候选主题,以进一步微调主题表示。

默认的候选主题是有c-TF-IDF生成的,为了可以生成更强的主题模型,可以使用串联的方式,将上面的所有主题表示模型连起来。例如,我们可以使用 MaximalMarginalRelevance 来改进每个主题中的关键词,然后再将它们传递给 OpenAI

使用链式模型生成的主题效果是优于使用单个主题表示模型

用法示例
from bertopic.representation import MaximalMarginalRelevance, OpenAI, KeyBERTInspired
from bertopic import BERTopic
import openai

client = openai.OpenAI(api_key="sk-...")
openai_generator = OpenAI(client)
mmr = MaximalMarginalRelevance(diversity=0.3)
# representation_models = [mmr, openai_generator]
representation_models = {
    # "Main": [openai_generator],
    "Main": [mmr, openai_generator],
    "KeyBERT": KeyBERTInspired(),
    "MMR": MaximalMarginalRelevance(diversity=0.3),
    "OpenAI": openai_generator
}

# 使用链式调用
topic_model = BERTopic(representation_model=representation_models)

Demo示例

BERTopic通过结合基于Transformer的语言模型和传统的聚类与统计方法,以现代方式解决了这一经典问题,从而创建高质量、可解释的主题。

# 导入库:UMAP用于降维、HDBSCAN用于聚类、SentenceTransformer用于文本嵌入、CountVectorizer用于分词
from umap import UMAP
from hdbscan import HDBSCAN
from sentence_transformers import SentenceTransformer
from sklearn.feature_extraction.text import CountVectorizer

# 导入BERTopic核心类及辅助模块(主题表示优化)
from bertopic import BERTopic
from bertopic.representation import KeyBERTInspired  # 用于微调主题关键词
from bertopic.vectorizers import ClassTfidfTransformer  # 用于计算主题的Class-TF-IDF权重


# Step 1: 提取文本嵌入(将文本转换为向量表示)
embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
# 说明:"all-MiniLM-L6-v2"是轻量级预训练模型,适合快速生成文本嵌入


# Step 2: 降维(减少特征维度,保留关键信息)
umap_model = UMAP(n_neighbors=15, n_components=5, min_dist=0.0, metric='cosine')
# 参数说明:
# n_neighbors: 局部邻域大小,影响降维时对局部结构的保留程度
# n_components: 降维后的目标维度(此处设为5)
# min_dist: 降维后点之间的最小距离(0.0表示允许点重合)
# metric: 距离度量方式('cosine'余弦相似度,适合文本向量的相似性计算)


# Step 3: 聚类(对降维后的向量进行分组,形成主题簇)
hdbscan_model = HDBSCAN(min_cluster_size=15, metric='euclidean', cluster_selection_method='eom', prediction_data=True)
# 参数说明:
# min_cluster_size: 形成有效簇的最小样本数(小于此值的样本视为噪声)
# metric: 距离度量('euclidean'欧氏距离,适用于低维空间聚类)
# cluster_selection_method: 簇选择策略('eom'基于集群树结构选择最优簇)
# prediction_data: 是否存储预测所需的数据(True便于后续对新数据进行聚类)


# Step 4: 分词(将文本拆分为单词/短语,构建词汇表)
vectorizer_model = CountVectorizer(stop_words="english")
# 说明:移除英文停用词(如"a"、"the"等无意义词汇),减少噪音


# Step 5: 生成主题表示(计算每个主题的核心关键词)
ctfidf_model = ClassTfidfTransformer()
# 说明:通过"Class-TF-IDF"算法,从分词结果中提取每个主题最具代表性的关键词


# Step 6: (可选)微调主题表示(进一步提升关键词质量)
representation_model = KeyBERTInspired()
# 说明:基于KeyBERT的思想,利用上下文信息优化主题关键词的相关性和准确性


# 整合所有步骤,构建完整的BERTopic主题模型
topic_model = BERTopic(
  embedding_model=embedding_model,          # 步骤1:文本嵌入
  umap_model=umap_model,                    # 步骤2:降维
  hdbscan_model=hdbscan_model,              # 步骤3:聚类
  vectorizer_model=vectorizer_model,        # 步骤4:分词
  ctfidf_model=ctfidf_model,                # 步骤5:主题关键词提取
  representation_model=representation_model # 步骤6:(可选)关键词微调
)

结语

BERTopic的诞生,标志着主题建模从“统计驱动”迈向“语义智能”的新纪元。它不仅解决了传统方法在语义理解、主题数量预设上的痛点,更通过模块化设计赋予研究者前所未有的灵活性——无论是追求效率的轻量级嵌入(如FastEmbed),还是需要高精度理解的LLM集成(如GPT-4),皆可按需组合。

在数据驱动的决策时代,BERTopic的价值远不止于技术本身:它让舆情分析更精准、学术研究更高效、业务洞察更深刻。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

故事挺秃然

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值