使用 k 均值对文本文档进行聚类#

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

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

此示例使用两种不同的文本向量化器:TfidfVectorizerHashingVectorizer。有关向量化器的更多信息以及它们处理时间的比较,请参阅示例笔记本 FeatureHasher 和 DictVectorizer 比较

有关通过监督学习方法进行文档分析,请参阅示例脚本 使用稀疏特征对文本文档进行分类

# Author: Peter Prettenhofer <[email protected]>
#         Lars Buitinck
#         Olivier Grisel <[email protected]>
#         Arturo Amor <[email protected]>
# License: BSD 3 clause

加载文本数据#

我们从 20 个新闻组文本数据集 加载数据,该数据集包含约 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

量化聚类结果的质量#

在本节中,我们定义一个函数来使用多个指标对不同的聚类管道进行评分。

聚类算法本质上是无监督学习方法。但是,由于我们碰巧拥有此特定数据集的类别标签,因此可以使用利用此“监督”地面实况信息的评估指标来量化所得聚类的质量。此类指标的示例如下

  • 同质性,量化聚类中仅包含单个类成员的程度;

  • 完整性,量化给定类成员被分配到相同聚类的程度;

  • V 测度,完整性和同质性的调和平均值;

  • Rand 指数,衡量数据点对根据聚类算法的结果和地面实况类别分配进行一致分组的频率;

  • 调整后的 Rand 指数,一个机会调整后的 Rand 指数,使得随机聚类分配的 ARI 预期值为 0.0。

如果不知道真实标签,评估只能使用模型结果本身进行。在这种情况下,轮廓系数就派上用场了。有关如何操作的示例,请参见使用轮廓分析在 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 均值聚类#

本示例中使用了两种特征提取方法

  • TfidfVectorizer 使用内存中的词汇表(Python 字典)将最常出现的词映射到特征索引,从而计算词出现频率(稀疏)矩阵。然后使用在语料库上逐特征收集的逆文档频率 (IDF) 向量重新加权词频率。

  • HashingVectorizer 将词出现次数散列到固定维度的空间,可能存在冲突。然后将词计数向量归一化为每个向量的 l2 范数等于 1(投影到欧几里得单位球体),这对于 k 均值在高维空间中工作似乎很重要。

此外,可以使用降维对这些提取的特征进行后处理。我们将在下面探讨这些选择对聚类质量的影响。

使用 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.448 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 均值聚类稀疏数据#

由于 KMeansMiniBatchKMeans 优化非凸目标函数,因此它们的聚类不能保证对于给定的随机初始化是最佳的。更进一步,在稀疏高维数据(例如使用词袋方法向量化的文本)上,k 均值可能会将质心初始化在极其孤立的数据点上。这些数据点可能会一直保持自己的质心。

以下代码说明了之前的现象有时会导致高度不平衡的簇,具体取决于随机初始化

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: [1638  993  309  447]
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 均值的客观函数)的聚类。

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.03 s
Homogeneity: 0.354 ± 0.011
Completeness: 0.403 ± 0.005
V-measure: 0.377 ± 0.008
Adjusted Rand-Index: 0.211 ± 0.016
Silhouette Coefficient: 0.007 ± 0.000

所有这些聚类评估指标的最大值为 1.0(对于完美的聚类结果)。值越高越好。接近 0.0 的调整后的 Rand 索引值对应于随机标记。从上面的分数可以看出,簇分配确实明显高于机会水平,但总体质量肯定可以提高。

请记住,类标签可能无法准确地反映文档主题,因此使用标签的指标不一定是评估聚类管道质量的最佳指标。

使用 LSA 进行降维#

只要先将向量化空间的维度降低,就可以继续使用 n_init=1,使 k 均值更稳定。为此,我们使用 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.422 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.395 ± 0.011
Completeness: 0.425 ± 0.002
V-measure: 0.409 ± 0.005
Adjusted Rand-Index: 0.318 ± 0.022
Silhouette Coefficient: 0.030 ± 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.340 ± 0.098
Completeness: 0.367 ± 0.059
V-measure: 0.350 ± 0.083
Adjusted Rand-Index: 0.290 ± 0.125
Silhouette Coefficient: 0.026 ± 0.005

每个簇的顶级词语#

由于 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: just think don know like time ve say does good
Cluster 1: space launch orbit shuttle nasa earth moon like mission just
Cluster 2: god people jesus believe bible don say christian think religion
Cluster 3: thanks graphics image program file files know help looking 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 2.061 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.01 s
Homogeneity: 0.395 ± 0.010
Completeness: 0.446 ± 0.015
V-measure: 0.419 ± 0.012
Adjusted Rand-Index: 0.320 ± 0.013
Silhouette Coefficient: 0.031 ± 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.342 ± 0.059
Completeness: 0.364 ± 0.062
V-measure: 0.352 ± 0.060
Adjusted Rand-Index: 0.304 ± 0.053
Silhouette Coefficient: 0.027 ± 0.005

这两种方法都产生了良好的结果,与在传统 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 度量和调整后的 Rand 索引)不同,这些指标仅基于簇分配而不是距离。请注意,严格来说,不应在不同维度的空间之间比较轮廓系数,因为它们隐含的距离概念不同。

同质性、完整性和 v-measure 指标在随机标签方面没有提供基线:这意味着根据样本数量、聚类数量和真实标签类别,完全随机的标签不会始终产生相同的值。特别是,随机标签不会产生零分,尤其是在聚类数量较多时。当样本数量超过一千,聚类数量小于 10 时,这个问题可以安全地忽略,这正是本例的情况。对于较小的样本量或较多的聚类数量,使用调整后的指标(如调整后的 Rand 指数 (ARI))更安全。有关随机标签影响的演示,请参见示例 聚类性能评估中的机会调整

误差条的大小表明,对于这个相对较小的数据集,MiniBatchKMeansKMeans 稳定性差。当样本数量大得多时,使用它更有趣,但与传统的 k-means 算法相比,它可能会以聚类质量略微下降为代价。

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

相关示例

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

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

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

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

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

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

使用稀疏特征对文本文档进行分类

使用稀疏特征对文本文档进行分类

由 Sphinx-Gallery 生成的图库