Clustering Unstructured Text with LLM Embeddings and HDBSCAN
TL;DR · AI 摘要
使用LLM嵌入和HDBSCAN聚类非结构化文本,可自动发现隐藏主题。
核心要点
- 使用Sentence Transformers生成文本嵌入,可将非结构化文本转化为语义丰富的数学表示。
- 通过UMAP降维处理嵌入,为聚类算法做准备。
- HDBSCAN算法能够自动发现文本数据中的隐藏主题,无需人工标注。
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- 文本聚类
- LLM嵌入
- Sentence Transformers
- 生成语义表示
- 降维处理
- UMAP算法
- 聚类算法
- HDBSCAN
金句 / Highlights
值得收藏与分享的关键句。
LLM可以将非结构化文本转化为语义丰富的数学表示,为后续聚类分析提供基础。
HDBSCAN是一种基于密度的聚类算法,能够自动发现文本数据中的隐藏主题,无需人工标注。
使用UMAP算法对嵌入进行降维,可以有效减少计算复杂度,提高聚类效率。
使用 LLM 嵌入和 HDBSCAN 对非结构化文本进行聚类 - MachineLearningMastery.com
使用 LLM 嵌入和 HDBSCAN 对非结构化文本进行聚类
作者:
Iván Palomares Carrascosa
于
2026 年 6 月 23 日
在
语言模型
2
分享
文章
在本文中,你将学习如何通过结合大型语言模型嵌入和 HDBSCAN(一种基于密度的聚类算法),构建一个文本聚类流水线,以自动发现未标记文本数据中的主题。
我们将涵盖的主题包括:
- 如何使用预训练的 sentence-transformers 模型为原始文档生成文本嵌入。
- 如何使用 UMAP 降低这些嵌入的维度,为聚类做准备。
- 如何应用 HDBSCAN 自动发现主题聚类并可视化结果。
使用 LLM 嵌入和 HDBSCAN 对非结构化文本进行聚类
介绍
当前生成式人工智能的时代似乎主要关注聊天界面和提示,但大型语言模型(简称 LLM)的应用范围并不局限于这些。事实上,它们最强大的下游能力之一是将原始的、混乱的、非结构化文本转换为称为嵌入的语义丰富的数学表示。一旦完成,我们就可以将这些文本表示用于各种机器学习用例,聚类也不例外。
特别是,嵌入可以与先进的基于密度的聚类技术(如 HDBSCAN)结合使用,从而在你的文本文档集合中发现隐藏的主题、模式或类别:这一切都不需要预先标记。
本文展示了如何从头开始构建一个基于文本的聚类流水线。我们将使用一个包含文本实例的免费可用数据集,以及一个用于生成嵌入的开源 LLM(即所谓的嵌入模型)。锦上添花的是,我们将使用免费且实用的现代 Python 库,这些库提供了 HDBSCAN 等聚类算法的实现。
分步操作指南
首先,让我们开始安装我们需要的关键 Python 库:
- Sentence transformers,用于从 Hugging Face 加载预训练的 LLM 以生成嵌入——你需要一个 Hugging Face API 密钥(也称为访问令牌)才能加载该模型。
- Umap-learn,用于应用一种降低嵌入维度的算法。
同样,如果你在本地 IDE 上工作而不是在云笔记本环境中,并且没有安装 scikit-learn 和 pandas,你可能也需要安装它们。
!pip install sentence-transformers umap-learn
1
!
pip
install
sentence
-
transformers
umap
learn
现在我们通过获取一些新鲜数据来开始编码部分。fetch_20newsgroups 函数可以获取一个包含分类新闻文章文本的数据集。请注意,尽管数据集包含标签,但我们将会忽略它们,因为我们假装不知道这些信息,以便根据相似性将这些数据实例聚类到组中。此外,我们将数据集采样到 150 个实例,这将足以代表我们的示例。
导入 pandas 作为 pd,从 sklearn.datasets 导入 fetch_20newsgroups # 获取一个高度针对性的数据子集(约 150-200 个文档) categories = ['sci.space', 'sci.med', 'rec.autos'] newsgroups = fetch_20newsgroups(subset='train', categories=categories, remove=('headers', 'footers', 'quotes')) # 抽样到一个具有代表性和说明性的子集 df = pd.DataFrame({'text': newsgroups.data, 'true_label': newsgroups.target}) df = df[df['text'].str.strip().str.len() > 100].sample(150, random_state=42).reset_index(drop=True) print(f"加载了 {len(df)} 个文本文档。") print("\n示例文档:") print(df['text'].iloc[0][:150] + "...")
3
4
5
6
7
8
9
10
11
12
13
14
导入
pandas
作为
pd
从
sklearn
.
datasets
fetch
_
获取一个高度针对性的数据子集(约 150-200 个文档)
categories
=
[
'sci.space'
,
'sci.med'
'rec.autos'
]
newsgroups
fetch_20newsgroups
(
subset
'train'
remove
'headers'
'footers'
'quotes'
)
抽样到一个具有代表性和说明性的子集
df
DataFrame
{
'text'
:
data
'true_label'
target
}
str
strip
len
100
sample
150
random_state
42
reset_index
drop
True
f
"加载了 {len(df)} 个文本文档。"
"\n示例文档:"
iloc
0
+
"..."
输出:
加载了 150 个文本文档。 示例文档: Okay Mr. Dyer, we're properly impressed with your philosophical skills and ability to insult people. You're a wonderful speaker and an adept politic...
加载
文本
文档
文档
Okay
Mr
Dyer
we
're properly impressed with your philosophical skills and
ability to insult people. You'
re
a
wonderful
speaker
and
an
adept
politic
下一步是从原始文本中获取嵌入。为此,我们从 Hugging Face 的 sentence-transformers 库中加载 all-MiniLM-L6-v2 模型。这是一个轻量但有效的模型,可以快速获取嵌入。
from sentence_transformers import SentenceTransformer # 加载免费的开源模型 model = SentenceTransformer('all-MiniLM-L6-v2') # 将文本文档编码为密集向量嵌入 print("生成嵌入...") embeddings = model.encode(df['text'].tolist(), show_progress_bar=True) print(f"嵌入矩阵形状: {embeddings.shape}")
sentence_transformers
SentenceTransformer
加载免费的开源模型
model
'all-MiniLM-L6-v2'
将文本文档编码为密集向量嵌入
"生成嵌入..."
embeddings
encode
tolist
show_progress_bar
"嵌入矩阵形状: {embeddings.shape}"
由于嵌入维度对于聚类目的来说最初太高,我们现在使用之前安装的同名库中的 UMAP 算法进行降维:
import umap # 将嵌入维度减少到 5,以保留足够的密度信息用于聚类 reducer = umap.UMAP(n_neighbors=15, n_components=5, min_dist=0.0, random_state=42) reduced_embeddings = reducer.fit_transform(embeddings) print(f"降维后的矩阵形状: {reduced_embeddings.shape}")
将嵌入维度减少到 5,以保留足够的密度信息用于聚类
reducer
n_neighbors
15
n_components
min_dist
0.0
reduced_embeddings
fit_transform
"降维后的矩阵形状: {reduced_embeddings.shape}"
现在,我们与新闻文章相关的数值嵌入向量仅包含五个维度(属性)。让我们看看这种紧凑的表示是否足够有意义,以通过应用 HDBSCAN 算法获得有洞察力的聚类。HDBSCAN 是一种基于密度的聚类方法:
from sklearn.cluster import HDBSCAN # 初始化 HDBSCAN # min_cluster_size=8: 我们指定每个聚类必须至少包含 8 篇文档 clusterer = HDBSCAN(min_cluster_size=8, min_samples=3, store_centers='centroid') df['cluster'] = clusterer.fit_predict(reduced_embeddings) # 统计每个聚类的实例数量 cluster_counts = df['cluster'].value_counts() print("\nCluster Distribution:") print(cluster_counts)
cluster
HDBSCAN
初始化 HDBSCAN
min_cluster_size=8: 我们指定每个聚类必须至少包含 8 篇文档
clusterer
min_cluster_size
min_samples
store_centers
'centroid'
'cluster'
fit_predict
统计每个聚类的实例数量
cluster_counts
value_counts
"\nCluster Distribution:"
重要的是,聚类结果部分受到我们为 HDBSCAN 定义的超参数设置的影响。我建议你尝试其他最小聚类大小和其他超参数的配置,以探索这如何影响结果。
结果:
Cluster Distribution: cluster 0 101 1 49 Name: count, dtype: int64
Distribution
101
49
Name
count
dtype
int64
看起来 HDBSCAN 检测到了与数据空间中高密度区域相关的两个聚类。是否也会有未被分配到这两个聚类中的噪声点?让我们检查一下:
for cluster_id in sorted(df['cluster'].unique()): if cluster_id == -1: print("\n=== CLUSTER: NOISE / UNCLASSIFIED ===") else: print(f"\n=== CLUSTER: Discovered Topic #{cluster_id} ===") # 从该聚类中获取最多 3 个样本文本 samples = df[df['cluster'] == cluster_id]['text'].head(3).tolist() for i, sample in enumerate(samples, 1): clean_sample = " ".join(sample.split())[:120] print(f" {i}. {clean_sample}...")
for
cluster_id
sorted
unique
if
==
"\n=== CLUSTER: NOISE / UNCLASSIFIED ==="
else
"\n=== CLUSTER: Discovered Topic #{cluster_id} ==="
从该聚类中获取最多 3 个样本文本
samples
head
i
enumerate
clean_sample
" "
join
split
120
" {i}. {clean_sample}..."
=== CLUSTER: Discovered Topic #0 === 1. Okay Mr. Dyer, we're properly impressed with your philosophical skills and ability to insult people. You're a wonderful ... 2. I was at an interesting seminar at work (UK's R.A.L. Space Science Dept.) on this subject, specifically on a small-scale... 3. This is the second post which seems to be blurring the distinction between real disease caused by Candida albicans and t... === CLUSTER: Discovered Topic #1 === 1. It's great that all these other cars can out-handle, out-corner, and out- accelerate an Integra. But, you've got to ask ... 2. l diamond star cars (Talon/Eclipse/Laser) put out 190 hp in the turbo models, and 195 hp in the AWD turbo models, These ... 3. Sorry for the mis-spelling, but I forgot how to spell it after my series of exams and NO-on hand reference here. Is it s...
===
Discovered
Topic
#0 ===
1.
're properly impressed with your philosophical skills and ability to insult people. You'
2.
was
at
interesting
seminar
work
UK
's R.A.L. Space Science Dept.) on this subject, specifically on a small-scale...
- 这是第二篇帖子,似乎模糊了由白色念珠菌引起的真正疾病与其他疾病之间的区别...
=== CLUSTER: 发现的主题 #1 ===
- 很高兴看到其他车型都能轻松应对弯道加速,比如Integra。但你必须问一下,像钻石星Talon/Eclipse Laser这样的车型,搭载了190马力的涡轮增压版本甚至195马力的AWD版本。
- 对不起,拼写错误,考试之后我忘了怎么拼写,这里没有手边的参考资料。
看起来样本中所有150个数据点都被分配到了两个已识别的聚类之一,这暗示了新闻文章可能很容易根据主题进行分离。
为了获得额外的见解,我们可以借助下面提供的补充代码,展示一些聚类可视化,该代码为描述每个数据点的五个现有组件的每对组合生成散点图:
import matplotlib.pyplot as plt import seaborn as sns import itertools # 创建一个包含5个降维嵌入和聚类标签的DataFrame reduced_df = pd.DataFrame(reduced_embeddings, columns=[f'UMAP_D{i+1}' for i in range(reduced_embeddings.shape[1])]) reduced_df['cluster'] = df['cluster'] # 获取5个维度的所有唯一成对组合 dim_pairs = list(itertools.combinations(reduced_df.columns[:-1], 2)) num_plots = len(dim_pairs) num_cols = 3 num_rows = (num_plots + num_cols - 1) // num_cols plt.figure(figsize=(num_cols * 5, num_rows * 4)) for i, (dim1, dim2) in enumerate(dim_pairs): plt.subplot(num_rows, num_cols, i + 1) sns.scatterplot( x=dim1, y=dim2, hue='cluster', data=reduced_df, palette='viridis', s=70, alpha=0.7, legend='full' ) plt.title(f'{dim1} vs {dim2}') plt.xlabel(dim1) plt.ylabel(dim2) plt.grid(True, linestyle='--', alpha=0.6) plt.tight_layout() plt.show()
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
matplotlib
pyplot
plt
seaborn
sns
itertools
创建一个包含5个降维嵌入和聚类标签的DataFrame
reduced_df
columns
'UMAP_D{i+1}'
range
shape
获取5个维度的所有唯一成对组合
dim_pairs
list
combinations
num_plots
num_cols
num_rows
// num_cols
figure
figsize
num_cols *
num_rows *
dim1
dim2
subplot
scatterplot
x
y
hue
palette
'viridis'
70
alpha
0.7
legend
'full'
title
'{dim1} vs {dim2}'
xlabel
ylabel
grid
linestyle
'--'
0.6
tight_layout
show
通过尝试不同的HDBSCAN配置,你可能会发现识别出的聚类数量可能与两个不同。不妨一试!
总结
一旦我们完成了基于文本的聚类流水线的构建过程,值得指出将LLM嵌入与HDBSCAN结合在一起的几个关键原因。这些原因包括,由于通过sentence-transformers获得的嵌入所固有的属性,能够保留并部分捕捉原始文本的真实语义意义和语言细微差别。此外,HDBSCAN能够自动确定最佳聚类数量,并能够检测可能为噪声或异常值的离群点,这些点可能会扭曲群体层面的统计数据。
关于此主题的更多信息
- 无结构文本数据的3种特征工程技巧
- 使用LLM嵌入在Scikit-learn中进行文档聚类
- 文本数据的7种高级特征工程技巧…
- OpenCV 中的 K-Means 聚类及其应用…
- 项目聚焦:使用…对 Stack Exchange 进行聚类
- 使用 Python 的 10 种聚类算法
/.entry