Spec-Zone.ru › scikit-learn

Примечание

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

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

В данном примере показано, как можно использовать API scikit-learn для кластеризации документов по темам с помощью модели «мешок слов» (Bag of Words approach).

Демонстрируются два алгоритма: KMeans и его более масштабируемый аналог MiniBatchKMeans. Кроме того, для уменьшения размерности и выявления скрытых закономерностей в данных используется анализ латентного семантического пространства.

В этом примере используются два разных векторных преобразователя текста: TfidfVectorizer и HashingVectorizer. Подробнее о преобразователях и сравнении их времени обработки см. в примере блокнота Сравнение FeatureHasher и DictVectorizer.

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

# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause

Загрузка текстовых данных

Мы загружаем данные из набора данных 20 новостных групп, который содержит около 18 000 сообщений новостных групп по 20 темам. В целях наглядности и сокращения вычислительных затрат мы выбираем подмножество всего из 4 тем, охватывающее около 3400 документов. Подробнее об области перекрытия таких тем см. пример Классификация текстовых документов с использованием разреженных признаков.

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

Мы обнаружили, что около 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.23 ± 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-средних всё ещё можно использовать, если сначала понизить размерность векторизованного пространства, чтобы сделать 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.408 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.400 ± 0.012
Completeness: 0.430 ± 0.012
V-measure: 0.414 ± 0.009
Adjusted Rand-Index: 0.315 ± 0.014
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.286 ± 0.089
Completeness: 0.326 ± 0.055
V-measure: 0.302 ± 0.074
Adjusted Rand-Index: 0.222 ± 0.106
Silhouette Coefficient: 0.025 ± 0.003

Топ-термины по кластеру

Поскольку 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 ve time does did said
Cluster 1: space launch orbit nasa shuttle earth moon like mission solar
Cluster 2: graphics thanks image program file know files looking help format
Cluster 3: god people jesus believe don bible say think religion christian

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.991 s

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

Теперь мы подгоняем и оцениваем экземпляры kmeans и minibatch_kmeans на этих данных, уменьшенных с помощью hashed-lsa:

fit_and_evaluate(kmeans, X_hashed_lsa, name="KMeans\nwith LSA on hashed vectors")
clustering done in 0.02 ± 0.01 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

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

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

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

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

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

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-средних на данных рукописных цифр

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

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

Коррекция на случайность в оценке производительности кластеризации

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

Spec-Zone.ru

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