Spec-Zone.ru › scikit-learn

Примечание

Перейти в конец, чтобы скачать полный код примера или запустить его в браузере с помощью JupyterLite или Binder.

Кластеризация текстовых документов с помощью алгоритма k-средних

В этом примере показано, как использовать API scikit-learn для кластеризации документов по темам с помощью модели «мешок слов».

В примере рассматриваются два алгоритма: 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" к другим сообщениям. Мы используем параметр remove функции fetch_20newsgroups, чтобы удалить эти признаки и получить более осмысленную задачу кластеризации.

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-мера — среднее гармоническое полноты и однородности;
  • индекс Рэнда, измеряющий, насколько часто пары точек данных группируются согласованно с результатом работы алгоритма кластеризации и эталонным распределением по классам;
  • скорректированный индекс Рэнда — индекс Рэнда с поправкой на случайность, так что для случайного распределения по кластерам математическое ожидание 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 равнялась единице (проецируются на единичную сферу евклидова пространства). Это, по-видимому, важно для работы k-средних в пространстве высокой размерности.

Кроме того, извлечённые признаки можно обработать с помощью уменьшения размерности. Далее мы рассмотрим влияние этих решений на качество кластеризации.

Извлечение признаков с помощью TfidfVectorizer

Сначала мы сравним оцениватели, используя векторизатор на основе словаря вместе с нормализацией IDF, которую предоставляет TfidfVectorizer.

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.472 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

Мы видим, что ненулевыми являются около 0,7% элементов матрицы X_tfidf.

Кластеризация разреженных данных методом k-средних

Поскольку и KMeans, и MiniBatchKMeans оптимизируют невыпуклую целевую функцию, их кластеризация не обязательно будет оптимальной при заданной случайной инициализации. Более того, при работе с разреженными данными высокой размерности, например с текстом, векторизованным с помощью модели «мешок слов», 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: [ 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-средних).

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 (при идеальной кластеризации). Чем выше значение, тем лучше. Значения скорректированного индекса Рэнда, близкие к 0,0, соответствуют случайной разметке. По приведённым выше оценкам видно, что распределение по кластерам заметно лучше случайного, но общее качество, безусловно, можно улучшить.

Помните, что метки классов могут неточно отражать темы документов, поэтому метрики, использующие метки, не обязательно являются наилучшим способом оценки качества нашего конвейера кластеризации.

Уменьшение размерности с помощью 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.347 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.01 s
Homogeneity: 0.400 ± 0.014
Completeness: 0.431 ± 0.013
V-measure: 0.415 ± 0.011
Adjusted Rand-Index: 0.317 ± 0.016
Silhouette Coefficient: 0.030 ± 0.000

Мы видим, что кластеризация документов в представлении 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.03 ± 0.01 s
Homogeneity: 0.303 ± 0.117
Completeness: 0.342 ± 0.067
V-measure: 0.315 ± 0.103
Adjusted Rand-Index: 0.268 ± 0.125
Silhouette Coefficient: 0.024 ± 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.816 s

Можно заметить, что этап LSA занимает относительно много времени при обучении, особенно для хешированных векторов. Причина в том, что хешированное пространство обычно велико (в этом примере задано значение n_features=50_000). Можно попробовать уменьшить число признаков, пожертвовав этим ради увеличения доли признаков с хеш-коллизиями, как показано в блокноте с примером Сравнение FeatureHasher и DictVectorizer.

Теперь обучим и оценим экземпляры kmeans и minibatch_kmeans на этих данных, уменьшенных с помощью hashing и LSA:

fit_and_evaluate(kmeans, X_hashed_lsa, name="KMeans\nwith LSA on hashed vectors")
clustering done in 0.02 ± 0.00 s
Homogeneity: 0.396 ± 0.006
Completeness: 0.442 ± 0.009
V-measure: 0.418 ± 0.007
Adjusted Rand-Index: 0.341 ± 0.030
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.03 ± 0.01 s
Homogeneity: 0.317 ± 0.088
Completeness: 0.336 ± 0.092
V-measure: 0.326 ± 0.089
Adjusted Rand-Index: 0.272 ± 0.104
Silhouette Coefficient: 0.027 ± 0.003

Оба метода дают хорошие результаты, сопоставимые с результатами тех же моделей на традиционных векторах 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

KMeans и MiniBatchKMeans подвержены явлению, называемому проклятием размерности, при работе с наборами данных высокой размерности, например с текстовыми данными. Именно поэтому общие оценки улучшаются при использовании LSA. Уменьшение размерности данных с помощью LSA также повышает стабильность и сокращает время кластеризации, однако следует помнить, что сам этап LSA занимает много времени, особенно при использовании хешированных векторов.

Значения коэффициента силуэта лежат в диапазоне от 0 до 1. Во всех случаях мы получаем значения, близкие к 0 (хотя после использования LSA они немного улучшаются), поскольку для его определения необходимо измерять расстояния. Другие метрики оценки, такие как V-мера и скорректированный индекс Рэнда, основаны только на распределении по кластерам, а не на расстояниях. Следует учитывать, что, строго говоря, коэффициент силуэта нельзя сравнивать для пространств разной размерности из-за разных подразумеваемых понятий расстояния.

Метрики однородности, полноты и, следовательно, V-меры не имеют базового уровня для случайной разметки: это означает, что в зависимости от числа образцов, кластеров и эталонных классов случайная разметка не всегда будет давать одинаковые значения. В частности, случайная разметка не обязательно даст нулевые оценки, особенно если кластеров много. Этой проблемой можно пренебречь, если число образцов превышает тысячу, а число кластеров меньше 10, как в этом примере. При меньшем числе образцов или большем числе кластеров безопаснее использовать индекс с поправкой, например скорректированный индекс Рэнда (ARI). Демонстрацию эффекта случайной разметки см. в примере Поправка на случайность при оценке качества кластеризации.

Размеры интервалов погрешностей показывают, что для этого относительно небольшого набора данных MiniBatchKMeans менее стабилен, чем KMeans. Этот алгоритм интереснее использовать при гораздо большем числе образцов, однако качество кластеризации может немного снизиться по сравнению с традиционным алгоритмом k-средних.

Общее время выполнения скрипта: (0 минут 7,980 секунды)

Launch binder
Launch JupyterLite

Download Jupyter notebook: plot_document_clustering.ipynb

Download Python source code: plot_document_clustering.py

Download zipped: plot_document_clustering.zip

Связанные примеры

Демонстрация кластеризации рукописных цифр методом K-Means

Сравнение алгоритмов кластеризации K-Means и MiniBatchKMeans

Бикластеризация документов с помощью алгоритма спектральной бикластеризации

Классификация текстовых документов с использованием разреженных признаков

© 2007–2026 The scikit-learn developers
Licensed under the 3-clause BSD License.
https://scikit-learn.org/1.9/auto_examples/text/plot_document_clustering.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API