使用 k-means 对文本文档进行聚类#

本例展示了如何使用 scikit-learn API,通过词袋模型方法对文档按主题进行聚类。

演示了两种算法,即 KMeans 及其更具可扩展性的变体 MiniBatchKMeans。此外,还使用潜在语义分析来降低维度并发现数据中的潜在模式。

本例使用两种不同的文本向量化器:TfidfVectorizerHashingVectorizer。有关向量化器的更多信息以及它们处理时间的比较,请参阅示例笔记本 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_20newsgroupsremove 参数来剔除这些特征,从而得到一个更合理的聚类问题。

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 对稀疏数据进行聚类#

由于 KMeansMiniBatchKMeans 都优化非凸目标函数,它们的聚类结果不保证在给定随机初始化下是最佳的。更进一步,在像使用词袋方法向量化的文本数据这样的稀疏高维数据上,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%

使用单次初始化意味着 KMeansMiniBatchKMeans 的处理时间都将减少。

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 降维数据上的 kmeansminibatch_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()
plot document clustering

KMeansMiniBatchKMeans 在高维数据集(例如文本数据)上会遇到维度灾难现象。这就是为什么在使用 LSA 后整体分数会提高的原因。使用 LSA 降维数据还可以提高稳定性和缩短聚类时间,但请记住 LSA 步骤本身需要很长时间,特别是对于哈希向量。

轮廓系数定义在 0 到 1 之间。在所有情况下,我们都获得了接近 0 的值(即使在使用 LSA 后它们有所改善),因为其定义要求测量距离,这与 V-measure 和 Adjusted Rand Index 等其他评估指标不同,后者仅基于聚类分配而非距离。请注意,严格来说,由于它们所隐含的距离概念不同,不应在不同维度的空间之间比较轮廓系数。

同质性、完整性以及 V-measure 指标没有提供随机标记的基线:这意味着根据样本数量、聚类数量和真实类别数量,完全随机的标记不总是会产生相同的值。特别是,随机标记不会产生零分,尤其当聚类数量很大时。当样本数量超过一千且聚类数量小于 10 时(本例即是如此),这个问题可以安全地忽略。对于较小的样本量或较多的聚类数量,使用调整后的指数(如调整兰德指数 (ARI))更安全。有关随机标记效果的演示,请参阅示例 聚类性能评估中的机会调整

误差条的大小表明,对于这个相对较小的数据集,MiniBatchKMeans 不如 KMeans 稳定。当样本数量大得多时,使用它更有意义,但与传统 k-means 算法相比,它可能会导致聚类质量略有下降。

脚本总运行时间: (0 分钟 7.550 秒)

相关示例

手写数字数据的 K-Means 聚类演示

手写数字数据的 K-Means 聚类演示

K-Means 和 MiniBatchKMeans 聚类算法的比较

K-Means 和 MiniBatchKMeans 聚类算法的比较

使用谱协同聚类算法对文档进行双聚类

使用谱协同聚类算法对文档进行双聚类

聚类性能评估中的机会调整

聚类性能评估中的机会调整

由 Sphinx-Gallery 生成的画廊