注意
转到末尾 下载完整示例代码。或通过 JupyterLite 或 Binder 在浏览器中运行此示例
使用 k-means 对文本文档进行聚类#
本例展示了如何使用 scikit-learn API,通过词袋模型方法对文档按主题进行聚类。
演示了两种算法,即 KMeans
及其更具可扩展性的变体 MiniBatchKMeans
。此外,还使用潜在语义分析来降低维度并发现数据中的潜在模式。
本例使用两种不同的文本向量化器:TfidfVectorizer
和 HashingVectorizer
。有关向量化器的更多信息以及它们处理时间的比较,请参阅示例笔记本 FeatureHasher 和 DictVectorizer 比较。
有关通过监督学习方法进行文档分析的示例脚本,请参阅 使用稀疏特征对文本文档进行分类。
# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause
加载文本数据#
我们从 20 newsgroups 文本数据集 加载数据,该数据集包含大约 18,000 条新闻组帖子,涉及 20 个主题。为了说明目的并降低计算成本,我们仅选择 4 个主题的子集,约 3,400 篇文档。有关这些主题重叠的直观理解,请参阅示例 使用稀疏特征对文本文档进行分类。
请注意,默认情况下,文本样本包含一些消息元数据,例如 "headers"
(标题)、"footers"
(签名)和 "quotes"
(引用其他帖子)。我们使用 fetch_20newsgroups
的 remove
参数来剔除这些特征,从而得到一个更合理的聚类问题。
import numpy as np
from sklearn.datasets import fetch_20newsgroups
categories = [
"alt.atheism",
"talk.religion.misc",
"comp.graphics",
"sci.space",
]
dataset = fetch_20newsgroups(
remove=("headers", "footers", "quotes"),
subset="all",
categories=categories,
shuffle=True,
random_state=42,
)
labels = dataset.target
unique_labels, category_sizes = np.unique(labels, return_counts=True)
true_k = unique_labels.shape[0]
print(f"{len(dataset.data)} documents - {true_k} categories")
3387 documents - 4 categories
量化聚类结果的质量#
在本节中,我们定义一个函数,使用多种指标对不同的聚类管道进行评分。
聚类算法本质上是无监督学习方法。然而,由于我们恰好拥有此特定数据集的类别标签,因此可以使用利用此“监督”真实信息来量化所得聚类质量的评估指标。此类指标的示例如下:
同质性(homogeneity),量化了聚类中包含单一类别成员的程度;
完整性(completeness),量化了给定类别的成员被分配到相同聚类的程度;
V-measure,完整性和同质性的调和平均值;
兰德指数(Rand-Index),衡量数据点对根据聚类算法结果和真实类别分配一致分组的频率;
调整兰德指数(Adjusted Rand-Index),一种经过机会调整的兰德指数,使得随机聚类分配的 ARI 期望值为 0.0。
如果真实标签未知,则只能使用模型结果本身进行评估。在这种情况下,轮廓系数(Silhouette Coefficient)会很有用。有关如何操作的示例,请参阅 使用 KMeans 聚类上的轮廓分析选择聚类数量。
更多参考,请参阅 聚类性能评估。
from collections import defaultdict
from time import time
from sklearn import metrics
evaluations = []
evaluations_std = []
def fit_and_evaluate(km, X, name=None, n_runs=5):
name = km.__class__.__name__ if name is None else name
train_times = []
scores = defaultdict(list)
for seed in range(n_runs):
km.set_params(random_state=seed)
t0 = time()
km.fit(X)
train_times.append(time() - t0)
scores["Homogeneity"].append(metrics.homogeneity_score(labels, km.labels_))
scores["Completeness"].append(metrics.completeness_score(labels, km.labels_))
scores["V-measure"].append(metrics.v_measure_score(labels, km.labels_))
scores["Adjusted Rand-Index"].append(
metrics.adjusted_rand_score(labels, km.labels_)
)
scores["Silhouette Coefficient"].append(
metrics.silhouette_score(X, km.labels_, sample_size=2000)
)
train_times = np.asarray(train_times)
print(f"clustering done in {train_times.mean():.2f} ± {train_times.std():.2f} s ")
evaluation = {
"estimator": name,
"train_time": train_times.mean(),
}
evaluation_std = {
"estimator": name,
"train_time": train_times.std(),
}
for score_name, score_values in scores.items():
mean_score, std_score = np.mean(score_values), np.std(score_values)
print(f"{score_name}: {mean_score:.3f} ± {std_score:.3f}")
evaluation[score_name] = mean_score
evaluation_std[score_name] = std_score
evaluations.append(evaluation)
evaluations_std.append(evaluation_std)
文本特征上的 K-means 聚类#
本示例使用两种特征提取方法
TfidfVectorizer
使用内存词汇表(Python 字典)将最频繁的词映射到特征索引,从而计算词频(稀疏)矩阵。然后使用语料库中按特征收集的逆文档频率 (IDF) 向量对词频进行重新加权。HashingVectorizer
将词频哈希到一个固定维度的空间,可能存在冲突。然后将词计数向量归一化,使其 l2 范数等于一(投影到欧几里得单位球体),这对于 k-means 在高维空间中工作似乎很重要。
此外,可以使用降维对这些提取的特征进行后处理。我们将在下文中探讨这些选择对聚类质量的影响。
使用 TfidfVectorizer 进行特征提取#
我们首先使用字典向量化器以及 TfidfVectorizer
提供的 IDF 归一化来评估估计器。
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(
max_df=0.5,
min_df=5,
stop_words="english",
)
t0 = time()
X_tfidf = vectorizer.fit_transform(dataset.data)
print(f"vectorization done in {time() - t0:.3f} s")
print(f"n_samples: {X_tfidf.shape[0]}, n_features: {X_tfidf.shape[1]}")
vectorization done in 0.374 s
n_samples: 3387, n_features: 7929
在忽略出现在超过 50% 文档中的术语(由 max_df=0.5
设置)和至少 5 个文档中不存在的术语(由 min_df=5
设置)之后,生成的唯一术语数量 n_features
约为 8,000。我们还可以将 X_tfidf
矩阵的稀疏度量化为非零条目分数除以总元素数。
print(f"{X_tfidf.nnz / np.prod(X_tfidf.shape):.3f}")
0.007
我们发现 X_tfidf
矩阵中大约 0.7% 的条目是非零的。
使用 k-means 对稀疏数据进行聚类#
由于 KMeans
和 MiniBatchKMeans
都优化非凸目标函数,它们的聚类结果不保证在给定随机初始化下是最佳的。更进一步,在像使用词袋方法向量化的文本数据这样的稀疏高维数据上,k-means 可能会在极其孤立的数据点上初始化质心。这些数据点可能一直保持自己的质心。
以下代码说明了前述现象有时如何导致高度不平衡的聚类,具体取决于随机初始化
from sklearn.cluster import KMeans
for seed in range(5):
kmeans = KMeans(
n_clusters=true_k,
max_iter=100,
n_init=1,
random_state=seed,
).fit(X_tfidf)
cluster_ids, cluster_sizes = np.unique(kmeans.labels_, return_counts=True)
print(f"Number of elements assigned to each cluster: {cluster_sizes}")
print()
print(
"True number of documents in each category according to the class labels: "
f"{category_sizes}"
)
Number of elements assigned to each cluster: [ 481 675 1785 446]
Number of elements assigned to each cluster: [1689 638 480 580]
Number of elements assigned to each cluster: [ 1 1 1 3384]
Number of elements assigned to each cluster: [1887 311 332 857]
Number of elements assigned to each cluster: [ 291 673 1771 652]
True number of documents in each category according to the class labels: [799 973 987 628]
为了避免这个问题,一种可能性是增加独立随机初始化 n_init
的运行次数。在这种情况下,选择惯性(k-means 的目标函数)最佳的聚类。
kmeans = KMeans(
n_clusters=true_k,
max_iter=100,
n_init=5,
)
fit_and_evaluate(kmeans, X_tfidf, name="KMeans\non tf-idf vectors")
clustering done in 0.20 ± 0.06 s
Homogeneity: 0.349 ± 0.010
Completeness: 0.398 ± 0.009
V-measure: 0.372 ± 0.009
Adjusted Rand-Index: 0.203 ± 0.017
Silhouette Coefficient: 0.007 ± 0.000
所有这些聚类评估指标的最大值为 1.0(表示完美的聚类结果)。值越高越好。调整兰德指数值接近 0.0 对应于随机标记。从上面的分数可以看出,聚类分配确实远高于随机水平,但整体质量肯定可以提高。
请记住,类别标签可能无法准确反映文档主题,因此使用标签的指标不一定是评估我们聚类管道质量的最佳指标。
使用 LSA 执行降维#
只要首先降低向量化空间的维度以使 k-means 更稳定,仍然可以使用 n_init=1
。为此,我们使用 TruncatedSVD
,它适用于词计数/tf-idf 矩阵。由于 SVD 结果未归一化,我们重新进行归一化以改善 KMeans
结果。在信息检索和文本挖掘文献中,使用 SVD 降低 TF-IDF 文档向量的维度通常被称为潜在语义分析 (LSA)。
from sklearn.decomposition import TruncatedSVD
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import Normalizer
lsa = make_pipeline(TruncatedSVD(n_components=100), Normalizer(copy=False))
t0 = time()
X_lsa = lsa.fit_transform(X_tfidf)
explained_variance = lsa[0].explained_variance_ratio_.sum()
print(f"LSA done in {time() - t0:.3f} s")
print(f"Explained variance of the SVD step: {explained_variance * 100:.1f}%")
LSA done in 0.344 s
Explained variance of the SVD step: 18.4%
使用单次初始化意味着 KMeans
和 MiniBatchKMeans
的处理时间都将减少。
kmeans = KMeans(
n_clusters=true_k,
max_iter=100,
n_init=1,
)
fit_and_evaluate(kmeans, X_lsa, name="KMeans\nwith LSA on tf-idf vectors")
clustering done in 0.02 ± 0.00 s
Homogeneity: 0.398 ± 0.011
Completeness: 0.427 ± 0.025
V-measure: 0.412 ± 0.017
Adjusted Rand-Index: 0.321 ± 0.018
Silhouette Coefficient: 0.029 ± 0.001
我们可以观察到,对文档的 LSA 表示进行聚类明显更快(既因为 n_init=1
,也因为 LSA 特征空间的维度小得多)。此外,所有聚类评估指标都有所改善。我们使用 MiniBatchKMeans
重复实验。
from sklearn.cluster import MiniBatchKMeans
minibatch_kmeans = MiniBatchKMeans(
n_clusters=true_k,
n_init=1,
init_size=1000,
batch_size=1000,
)
fit_and_evaluate(
minibatch_kmeans,
X_lsa,
name="MiniBatchKMeans\nwith LSA on tf-idf vectors",
)
clustering done in 0.02 ± 0.00 s
Homogeneity: 0.333 ± 0.084
Completeness: 0.351 ± 0.066
V-measure: 0.341 ± 0.076
Adjusted Rand-Index: 0.290 ± 0.060
Silhouette Coefficient: 0.026 ± 0.004
每个聚类的热门词项#
由于 TfidfVectorizer
可以反转,我们可以识别聚类中心,这提供了每个聚类中最具影响力的词的直观理解。有关与每个目标类别最具预测性的词的比较,请参阅示例脚本 使用稀疏特征对文本文档进行分类。
original_space_centroids = lsa[0].inverse_transform(kmeans.cluster_centers_)
order_centroids = original_space_centroids.argsort()[:, ::-1]
terms = vectorizer.get_feature_names_out()
for i in range(true_k):
print(f"Cluster {i}: ", end="")
for ind in order_centroids[i, :10]:
print(f"{terms[ind]} ", end="")
print()
Cluster 0: thanks graphics image know files file program looking software does
Cluster 1: space launch orbit nasa shuttle earth moon like mission just
Cluster 2: god jesus bible believe christian faith people say does christians
Cluster 3: think don people just say know like time did does
HashingVectorizer#
可以使用 HashingVectorizer
实例进行另一种向量化,它不提供 IDF 加权,因为这是一个无状态模型(fit 方法什么也不做)。当需要 IDF 加权时,可以通过将 HashingVectorizer
的输出通过管道连接到 TfidfTransformer
实例来添加。在这种情况下,我们还将 LSA 添加到管道中,以减少哈希向量空间的维度和稀疏性。
from sklearn.feature_extraction.text import HashingVectorizer, TfidfTransformer
lsa_vectorizer = make_pipeline(
HashingVectorizer(stop_words="english", n_features=50_000),
TfidfTransformer(),
TruncatedSVD(n_components=100, random_state=0),
Normalizer(copy=False),
)
t0 = time()
X_hashed_lsa = lsa_vectorizer.fit_transform(dataset.data)
print(f"vectorization done in {time() - t0:.3f} s")
vectorization done in 1.703 s
可以观察到 LSA 步骤需要相对较长的时间来拟合,特别是对于哈希向量。原因是哈希空间通常很大(在本例中设置为 n_features=50_000
)。可以尝试降低特征数量,但代价是哈希冲突的特征比例会增加,如示例笔记本 FeatureHasher 和 DictVectorizer 比较 所示。
现在我们拟合和评估在这个哈希-LSA 降维数据上的 kmeans
和 minibatch_kmeans
实例
fit_and_evaluate(kmeans, X_hashed_lsa, name="KMeans\nwith LSA on hashed vectors")
clustering done in 0.02 ± 0.00 s
Homogeneity: 0.390 ± 0.008
Completeness: 0.436 ± 0.012
V-measure: 0.411 ± 0.010
Adjusted Rand-Index: 0.322 ± 0.014
Silhouette Coefficient: 0.030 ± 0.001
fit_and_evaluate(
minibatch_kmeans,
X_hashed_lsa,
name="MiniBatchKMeans\nwith LSA on hashed vectors",
)
clustering done in 0.02 ± 0.00 s
Homogeneity: 0.359 ± 0.053
Completeness: 0.385 ± 0.059
V-measure: 0.371 ± 0.055
Adjusted Rand-Index: 0.315 ± 0.046
Silhouette Coefficient: 0.029 ± 0.002
这两种方法都得到了良好的结果,与在传统 LSA 向量(不带哈希)上运行相同模型的结果相似。
聚类评估总结#
import matplotlib.pyplot as plt
import pandas as pd
fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(16, 6), sharey=True)
df = pd.DataFrame(evaluations[::-1]).set_index("estimator")
df_std = pd.DataFrame(evaluations_std[::-1]).set_index("estimator")
df.drop(
["train_time"],
axis="columns",
).plot.barh(ax=ax0, xerr=df_std)
ax0.set_xlabel("Clustering scores")
ax0.set_ylabel("")
df["train_time"].plot.barh(ax=ax1, xerr=df_std["train_time"])
ax1.set_xlabel("Clustering time (s)")
plt.tight_layout()

KMeans
和 MiniBatchKMeans
在高维数据集(例如文本数据)上会遇到维度灾难现象。这就是为什么在使用 LSA 后整体分数会提高的原因。使用 LSA 降维数据还可以提高稳定性和缩短聚类时间,但请记住 LSA 步骤本身需要很长时间,特别是对于哈希向量。
轮廓系数定义在 0 到 1 之间。在所有情况下,我们都获得了接近 0 的值(即使在使用 LSA 后它们有所改善),因为其定义要求测量距离,这与 V-measure 和 Adjusted Rand Index 等其他评估指标不同,后者仅基于聚类分配而非距离。请注意,严格来说,由于它们所隐含的距离概念不同,不应在不同维度的空间之间比较轮廓系数。
同质性、完整性以及 V-measure 指标没有提供随机标记的基线:这意味着根据样本数量、聚类数量和真实类别数量,完全随机的标记不总是会产生相同的值。特别是,随机标记不会产生零分,尤其当聚类数量很大时。当样本数量超过一千且聚类数量小于 10 时(本例即是如此),这个问题可以安全地忽略。对于较小的样本量或较多的聚类数量,使用调整后的指数(如调整兰德指数 (ARI))更安全。有关随机标记效果的演示,请参阅示例 聚类性能评估中的机会调整。
误差条的大小表明,对于这个相对较小的数据集,MiniBatchKMeans
不如 KMeans
稳定。当样本数量大得多时,使用它更有意义,但与传统 k-means 算法相比,它可能会导致聚类质量略有下降。
脚本总运行时间: (0 分钟 7.550 秒)
相关示例