在过去,我们习惯了“监督学习”——也就是先给数据打标签,再让机器去学习。但现实世界中,未标注的数据才是主流。想象一下,你手里有几万篇学术论文的摘要,没有任何分类标签,你想快速知道它们都研究了什么?哪些方向是热点?有没有什么冷门的“宝藏”课题?

这时候,无监督学习中的文本聚类主题建模就派上了大用场。

今天,我将带你从零开始,使用一个真实的案例——ArXiv上44949篇“计算与语言”领域的论文摘要,一步步构建一个文本聚类和主题建模的完整流程。我们不仅要学会如何给文档分组,还要让AI自动告诉我们每个组的“主题”是什么!

这不仅仅是一篇理论科普,更是一份可以直接上手操作的实战指南。全文干货,篇幅较长,但保证让你看得懂、学得会、用得上。


一、为什么我们需要文本聚类?

文本聚类,顾名思义,就是让机器根据文本的语义内容,自动将相似的文本归到同一个组里

比如,所有关于“猫”的文章聚成一堆,关于“狗”的聚成另一堆;关于“足球”的和“篮球”的虽然都是体育,但语义不同,也会被分开;而“意大利面”和“披萨”则会被归为“美食”这个大簇中。

这有什么用?用处太大了!

  1. 探索性数据分析:面对海量文本,你完全不知道里面有什么。聚类能帮你快速勾勒出数据全景图。

  2. 加速数据标注:人工标注太慢?先聚类,然后你只需要为每个簇打个标签,相当于一次性标注了成百上千条数据。

  3. 发现异常点:那些“无家可归”、不属于任何簇的文档,往往就是最有价值的离群点,可能是全新的研究方向,也可能是错误的数据。

  4. 支撑主题建模:我们今天的主角——主题建模,本质上就是给每个聚类出来的簇,赋予一个“可读的主题描述”。


二、实战准备:我们的数据集

为了让大家有最直观的感受,我们选用学术界最知名的预印本平台 ArXiv 上的真实数据。我们使用Hugging Face上的一个现成数据集 maartengr/arxiv_nlp,它包含了从1991年到2024年,所有 cs.CL(计算与语言) 分类下的论文摘要,一共 44,949篇

先上代码,把数据加载进来:

python

from datasets import load_dataset

# 加载数据集
dataset = load_dataset("maartengr/arxiv_nlp")["train"]

# 提取摘要、标题和年份
abstracts = dataset["Abstracts"]
titles = dataset["Titles"]
# years = dataset["Years"]  # 年份数据我们暂时不用

现在,我们手里有了近4.5万条文本数据,挑战开始。


三、文本聚类的“三步走”通用流程

文本聚类听起来玄乎,但现在的技术已经把它标准化成了一个非常清晰的三步流程。我们可以把它想象成一个“加工流水线”:

第一步:嵌入模型 —— 把人类语言(文本)翻译成机器能懂的数学语言(向量)。
第二步:降维模型 —— 把高维的数学向量压缩,去芜存菁,让聚类算法跑得更快、更好。
第三步:聚类模型 —— 根据压缩后的向量,将文档分组。

每一步都可以自由选择不同的算法,这也是现代文本处理的精髓——模块化

3.1 第一步:嵌入文档

在第一章里,我们讲过嵌入模型。简单来说,它能把一段文本变成一个固定长度的数字列表(向量)。这个向量捕捉了文本的语义信息,语义越相似的两句话,它们的向量在数学空间里离得就越近。

选择哪个嵌入模型很关键。我们参考业界公认的 MTEB排行榜,选择一个在聚类任务上表现出色,同时体积小、速度快的模型。这次我们选择 thenlper/gte-small

python

from sentence_transformers import SentenceTransformer

# 加载嵌入模型
embedding_model = SentenceTransformer("thenlper/gte-small")

# 为所有摘要生成嵌入向量
# 注意:这一步会有点耗时,因为要对4.5万篇文章进行推理
embeddings = embedding_model.encode(abstracts, show_progress_bar=True)

# 查看一下嵌入向量的维度
print(embeddings.shape)
# 输出:(44949, 384)

输出结果告诉我们,我们为44,949篇文章,每篇生成了一个包含384个数值的向量。这384个数值就是这篇文章的“数字指纹”。

3.2 第二步:嵌入向量降维

我们有了384维的数据。但高维空间对大多数聚类算法来说是个噩梦——“维度灾难”会让计算变得极其复杂,且难以找到有意义的簇。

因此,我们需要降维,把这384维的数据压缩到更低的维度(比如5维),同时尽可能地保留原始数据的全局结构。

我们选择 UMAP 算法。它比传统的PCA(主成分分析)更擅长处理非线性关系,是目前文本聚类的首选。

python

from umap import UMAP

# 初始化UMAP模型
# n_components=5: 降到5维
# min_dist=0.0: 点之间的最小距离设为0,有助于形成更紧密的簇
# metric="cosine": 使用余弦距离,非常适合处理文本向量
umap_model = UMAP(n_components=5, min_dist=0.0, metric="cosine", random_state=42)

# 对嵌入向量进行降维
reduced_embeddings = umap_model.fit_transform(embeddings)

random_state=42是为了保证结果可复现,但会牺牲一点并行计算的速度。在实际生产中,可以根据情况取舍。

现在,每个文档的表示从384维变成了5维

3.3 第三步:对降维后的向量进行聚类

终于到了分组的环节。我们选择 HDBSCAN 算法。

为什么不用经典的K-Means?因为K-Means需要你事先指定要分成几个簇,而我们根本不知道这4.5万篇文章里到底有多少个研究方向。HDBSCAN是基于密度的算法,它自己会“看着办”,自动发现密集的区域作为簇。它最大的好处是:不强制把所有点都分到一个簇里。那些游离于所有密集区域之外的点,会被标记为“离群点”(Outlier,通常用-1表示)。这对于学术论文这种可能存在大量小众方向的场景,简直完美。

python

from hdbscan import HDBSCAN

# 初始化HDBSCAN模型
# min_cluster_size=50: 一个簇最少需要包含50篇论文
# metric="euclidean": 在降维后的低维空间,通常用欧氏距离
hdbscan_model = HDBSCAN(min_cluster_size=50, metric="euclidean", cluster_selection_method="eom")

# 对降维后的向量进行聚类
clusters = hdbscan_model.fit_predict(reduced_embeddings)

# 看看我们分出了多少个簇
n_clusters = len(set(clusters)) - (1 if -1 in clusters else 0)
print(f"生成了 {n_clusters} 个簇")
# 输出:生成了 156 个簇
print(f"离群点数量: {list(clusters).count(-1)}")
# 输出:离群点数量: 14520

很有意思的结果!HDBSCAN帮我们找到了 156个 不同的研究方向,但同时也标记出了 1.4万多篇 论文为“离群点”,说明这些论文的主题比较小众,难以形成50篇以上的大规模簇。


四、检查我们的劳动成果

算法跑完了,结果怎么样?我们得亲自看看。

4.1 手动检查簇的内容

我们先看看簇0里都有啥:

python

import numpy as np

cluster = 0
print(f"簇 {cluster} 中的文档示例:")
for index in np.where(clusters == cluster)[0][:3]:
    print(abstracts[index][:200] + "... \n")

打印出来的摘要,有的关于手语翻译,有的关于手语的语言学特征。很显然,簇0的主题就是 “手语处理”

手动检查虽然准确,但156个簇挨个看过去得累死。我们需要可视化。

4.2 可视化:一图胜千言

为了能在二维平面上画出来,我们得再降一次维,这次降到2维。

python

# 为了可视化,再降维到2维
reduced_embeddings_2d = UMAP(n_components=2, min_dist=0.0, metric="cosine", random_state=42).fit_transform(embeddings)

import pandas as pd
import matplotlib.pyplot as plt

# 创建DataFrame方便绘图
df = pd.DataFrame(reduced_embeddings_2d, columns=["x", "y"])
df["title"] = titles
df["cluster"] = [str(c) for c in clusters]

# 分开离群点和非离群点
clusters_df = df.loc[df.cluster != "-1", :]
outliers_df = df.loc[df.cluster == "-1", :]

# 绘图
plt.figure(figsize=(12, 8))
# 用灰色画出离群点
plt.scatter(outliers_df.x, outliers_df.y, alpha=0.1, s=1, c="grey")
# 用彩色画出簇点
plt.scatter(clusters_df.x, clusters_df.y, c=clusters_df.cluster.astype(int), alpha=0.6, s=1, cmap="tab20b")
plt.title("ArXiv论文聚类可视化 (颜色代表不同簇)")
plt.show()

你会在图上看到一个个彩色的“小岛”,那就是算法发现的156个研究主题。灰色的背景则是那些离群点

但是,这个图只能让我们看到结构,还是不知道每个“小岛”具体是干什么的。于是,我们从文本聚类,迈入了下一个阶段——主题建模


五、从聚类到主题建模:BERTopic的魔法

文本聚类给了我们“组”,而主题建模要做的,是给每个“组”一个可读的、有意义的描述

传统的主题建模方法,如LDA(潜在狄利克雷分配),是通过统计词频来生成一组关键词。但它有个致命伤:只统计词,不理解语义。比如,“apple”这个词,它分不清是“苹果公司”还是“水果苹果”。

于是,一个集大成的神器出现了——BERTopic。它完美地结合了我们刚才做的文本聚类和传统的词袋模型,并且具有极高的模块化特性,像乐高一样,你想换哪个零件就换哪个。

BERTopic的流程分为两大部分:

第一部分:聚类 —— 这就是我们刚才做的:嵌入 -> UMAP -> HDBSCAN。
第二部分:主题表示 —— 这是它的核心创新。

5.1 核心算法:c-TF-IDF

如何为一个簇生成关键词?BERTopic使用了一种叫做 基于类的词频-逆文档频率 的算法。

  1. 合并文档:首先,把属于同一个簇的所有文档合并成一个大文档。

  2. 统计词频:统计每个簇的大文档里,每个词出现了多少次。

  3. 计算权重:对于每个词,它不仅考虑它在当前簇里出现了多少次(TF),还要考虑它在所有簇里出现的频率(IDF)。如果一个词在很多簇里都出现(比如“我们”、“研究”、“论文”),它的IDF就会很低,说明它没什么代表性;如果一个词只在一个簇里高频出现(比如“手语”、“语音识别”),它的IDF就会很高,说明它极好地代表了该簇。

这样,我们就能为每个簇(即每个主题)提取出一组得分最高的关键词。

5.2 在BERTopic中运行一切

BERTopic把整个流程封装得极其简单。我们可以直接把我们之前定义好的模型传给它:

python

from bertopic import BERTopic

topic_model = BERTopic(
    embedding_model=embedding_model,  # 复用我们之前的嵌入模型
    umap_model=umap_model,            # 复用UMAP模型
    hdbscan_model=hdbscan_model,      # 复用HDBSCAN模型
    verbose=True
).fit(abstracts, embeddings)          # 传入文本和嵌入向量

看看结果:

python

topic_info = topic_model.get_topic_info()
print(topic_info.head())

你会看到一个表格:

Topic Count Name Representation
-1 14520 -1_the_of_and_to [the, of, and, to, in, we, that...]
0 2290 0_speech_asr_recognition_end [speech, asr, recognition, end, acoustic...]
1 1403 1_medical_clinical_biomedical_patient [medical, clinical, biomedical, patient...]
2 1156 2_sentiment_aspect_analysis_reviews [sentiment, aspect, analysis, reviews...]
3 986 3_translation_nmt_machine_neural [translation, nmt, machine, neural...]
  • Topic -1:永远是这个,代表所有离群点。

  • Topic 0:关键词是 speechasr(自动语音识别), recognition... 很明显,这是语音识别方向。

  • Topic 1:关键词是 medicalclinicalbiomedical... 这是生物医学文本挖掘

  • Topic 2sentimentaspectanalysis... 这是情感分析

  • Topic 3translationnmt(神经机器翻译)... 这是机器翻译

太酷了!不到一分钟,AI就帮我们把4.5万篇论文梳理成了100多个研究主题,并用关键词标明了每个主题的内容。

我们甚至可以搜索特定的主题,比如“topic modeling”:

python

topic_model.find_topics("topic modeling")
# 输出:( [22, -1, 1, 47, 32], [0.95, 0.91, ...] )

它告诉我们,主题22最相关。我们再看看主题22的关键词:
['topic', 'topics', 'lda', 'latent', 'document', ...],没错,这就是关于“主题建模”本身的论文!甚至连BERTopic这篇论文的摘要都被分到了这个簇里。


六、高级玩法:给BERTopic换上不同的“乐高积木块”

BERTopic最强大的地方在于它的模块化。之前的关键词(Representation)都是用c-TF-IDF生成的。但我们想换换口味,比如:

6.1 用KeyBERTInspired提升语义性

c-TF-IDF只看词频统计,可能会漏掉一些重要的语义信息。KeyBERTInspired这个表示模型,会计算每个候选词和整个主题的语义相似度,然后对关键词重新排序。

python

from bertopic.representation import KeyBERTInspired

representation_model = KeyBERTInspired()
topic_model.update_topics(abstracts, representation_model=representation_model)

更新后,你会看到翻译主题的关键词从 ['translation', 'nmt', 'machine', 'neural', 'bleu'] 变成了 ['translation', 'translating', 'translate', 'translations', 'nmt']。语义上更丰富了。

6.2 用MMR去除冗余关键词

有时候关键词列表里会出现 summary 和 summarization 这种高度相似的词,造成了冗余。我们可以用最大边际相关性 来让结果更多样化。

python

from bertopic.representation import MaximalMarginalRelevance

# diversity参数控制多样性,值越大,结果越多样
representation_model = MaximalMarginalRelevance(diversity=0.3)
topic_model.update_topics(abstracts, representation_model=representation_model)

更新后,摘要主题的关键词从一堆“summary”的变体,变成了 ['summarization', 'document', 'extractive', 'rouge', 'abstractive'],涵盖了抽取式、生成式、评估指标等多个方面,信息量更大。

6.3 终极大招:用大模型生成主题标签(LLM)

关键词再好,也需要读者自己去脑补。为什么不直接让AI生成一个人类能读懂的主题标题呢?

BERTopic支持集成各种大语言模型,而且非常聪明:它不是让大模型去读所有的几万篇论文,而是只让大模型去读每个主题的前几篇最具代表性的论文前几个关键词。这样,即使有100个主题,也只需要调用大模型100次,成本极低,效率极高!

首先,用Google的FLAN-T5试试水:

python

from transformers import pipeline
from bertopic.representation import TextGeneration

prompt = """I have a topic that contains the following documents:
[DOCUMENTS]
The topic is described by the following keywords: [KEYWORDS].
Based on the documents and keywords, what is this topic about?"""

generator = pipeline("text2text-generation", model="google/flan-t5-small")
representation_model = TextGeneration(generator, prompt=prompt)
topic_model.update_topics(abstracts, representation_model=representation_model)

FLAN-T5会给主题0起名为 Speech-to-description,主题1为 Science/Tech... 感觉有点太宽泛了。

最后,请出我们的王牌选手:GPT-3.5 Turbo

python

from bertopic.representation import OpenAI
import openai

# 你需要设置你的OpenAI API Key
client = openai.OpenAI(api_key="YOUR_API_KEY")

prompt = """
I have a topic that contains the following documents:
[DOCUMENTS]
The topic is described by the following keywords: [KEYWORDS]
Based on the information above, extract a short topic label in the following format:
topic: <short topic label>
"""

representation_model = OpenAI(client, model="gpt-3.5-turbo", chat=True, prompt=prompt)
topic_model.update_topics(abstracts, representation_model=representation_model)

看看GPT-3.5交出的答卷:

  • Topic 0 (语音识别) -> Leveraging External Data for Improving Low-Resource ASR

  • Topic 1 (生物医学) -> Improved Representation Learning for Biomedical Terms Using HiPrBERT

  • Topic 2 (情感分析) -> Advancements in Aspect-Based Sentiment Analysis

  • Topic 3 (机器翻译) -> Neural Machine Translation Enhancements

  • Topic 4 (文本摘要) -> Document Summarization Techniques

这简直是专业的学术标题!信息量、准确性、可读性都达到了完美的平衡。


七、总结与展望

今天我们走完了一段从零到一的完整旅程。

  1. 我们明白了无监督学习的巨大价值:在没有标签的情况下,文本聚类和主题建模能帮助我们洞察数据的内部结构。

  2. 我们掌握了文本聚类的“三步走”标准流程:嵌入(Embedding)-> 降维(UMAP)-> 聚类(HDBSCAN)。这套流程强大且通用。

  3. 我们见识了BERTopic的魔力:它巧妙地将聚类流程与主题表示(c-TF-IDF)结合起来,让我们不仅能看到分组,还能理解每个组的含义。

  4. 我们体验了模块化的强大:通过更换不同的“乐高积木块”(KeyBERTInspired, MMR, GPT-3.5),我们可以从不同角度微调和增强主题的表示,特别是利用大模型生成高质量主题标签的能力,将主题建模的可解释性提升到了一个新的高度。

文本聚类和主题建模不是终点,而是起点。它能帮你快速理解任何大规模的文本数据集——无论是几万篇科研论文、几十万条用户评论,还是上百万份公司财报。它就像一架侦察机,在你深入敌后之前,先为你勾勒出清晰的战场地图。

本文参考:图解大模型:生成式AI原理与实战

书籍pdf免费下载地址:https://pan.baidu.com/s/1mTaUQ5czcfGpBM8KvJuS2g?pwd=un44

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐