使用k-means聚类文本文档#

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

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

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

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

# Authors: The scikit-learn developers
# SPDX-License-Identifier: 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-measure,完整性和同质性的调和平均值;

  • 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-means聚类#

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

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

  • HashingVectorizer将单词出现次数散列到固定维度的空间中,可能存在冲突。然后将单词计数向量标准化,使其每个向量的l2范数等于1(投影到欧几里得单位球面上),这对于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.402 s
n_samples: 3387, n_features: 7929

在忽略出现在超过50%文档中的词语(由max_df=0.5设置)和至少不出现在5个文档中的词语(由min_df=5设置)之后,生成的唯一词语数量n_features大约为8000个。我们可以额外地将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.05 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(对于完美的聚类结果)。值越高越好。调整后的Rand指数接近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.382 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.393 ± 0.014
Completeness: 0.428 ± 0.014
V-measure: 0.410 ± 0.013
Adjusted Rand-Index: 0.312 ± 0.025
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.339 ± 0.098
Completeness: 0.384 ± 0.080
V-measure: 0.357 ± 0.089
Adjusted Rand-Index: 0.299 ± 0.131
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: space nasa shuttle launch station program think sci like just
Cluster 1: just like time think don know ve does new good
Cluster 2: god people don think jesus bible say believe religion christian
Cluster 3: thanks graphics image file program files know help looking format

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.871 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.387 ± 0.011
Completeness: 0.429 ± 0.017
V-measure: 0.407 ± 0.013
Adjusted Rand-Index: 0.328 ± 0.023
Silhouette Coefficient: 0.029 ± 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.357 ± 0.043
Completeness: 0.378 ± 0.046
V-measure: 0.367 ± 0.043
Adjusted Rand-Index: 0.322 ± 0.030
Silhouette Coefficient: 0.028 ± 0.004

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

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

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

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

相关示例

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

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

比较 K-Means 和 MiniBatchKMeans 聚类算法

比较 K-Means 和 MiniBatchKMeans 聚类算法

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

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

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

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

由 Sphinx-Gallery 生成的图库