Spec-Zone.ru › scikit-learn

Примечание

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

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

В этом примере показано, как scikit-learn можно использовать для классификации документов по темам с помощью подхода «мешок слов». В примере используется разреженная матрица «документ–термин» с весами Tf-idf для кодирования признаков, а также демонстрируются различные классификаторы, способные эффективно работать с разреженными матрицами.

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

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

Загрузка и векторизация текстового набора данных 20 newsgroups

Определим функцию для загрузки данных из текстового набора данных 20 newsgroups, который содержит около 18 000 публикаций в группах новостей на 20 темах и разделён на две части: одну для обучения (или разработки), другую — для тестирования (или оценки производительности). По умолчанию текстовые примеры содержат метаданные сообщений, например 'headers', 'footers' (подписи) и 'quotes' в других публикациях. Поэтому функция fetch_20newsgroups принимает параметр с именем remove, позволяющий попытаться удалить такие данные, которые могут сделать задачу классификации «слишком простой». Для этого используются простые эвристики, которые не являются ни идеальными, ни общепринятыми, поэтому по умолчанию эта возможность отключена.

from time import time

from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer

categories = [
    "alt.atheism",
    "talk.religion.misc",
    "comp.graphics",
    "sci.space",
]


def size_mb(docs):
    return sum(len(s.encode("utf-8")) for s in docs) / 1e6


def load_dataset(verbose=False, remove=()):
    """Load and vectorize the 20 newsgroups dataset."""

    data_train = fetch_20newsgroups(
        subset="train",
        categories=categories,
        shuffle=True,
        random_state=42,
        remove=remove,
    )

    data_test = fetch_20newsgroups(
        subset="test",
        categories=categories,
        shuffle=True,
        random_state=42,
        remove=remove,
    )

    # order of labels in `target_names` can be different from `categories`
    target_names = data_train.target_names

    # split target in a training set and a test set
    y_train, y_test = data_train.target, data_test.target

    # Extracting features from the training data using a sparse vectorizer
    t0 = time()
    vectorizer = TfidfVectorizer(
        sublinear_tf=True, max_df=0.5, min_df=5, stop_words="english"
    )
    X_train = vectorizer.fit_transform(data_train.data)
    duration_train = time() - t0

    # Extracting features from the test data using the same vectorizer
    t0 = time()
    X_test = vectorizer.transform(data_test.data)
    duration_test = time() - t0

    feature_names = vectorizer.get_feature_names_out()

    if verbose:
        # compute size of loaded data
        data_train_size_mb = size_mb(data_train.data)
        data_test_size_mb = size_mb(data_test.data)

        print(
            f"{len(data_train.data)} documents - "
            f"{data_train_size_mb:.2f}MB (training set)"
        )
        print(f"{len(data_test.data)} documents - {data_test_size_mb:.2f}MB (test set)")
        print(f"{len(target_names)} categories")
        print(
            f"vectorize training done in {duration_train:.3f}s "
            f"at {data_train_size_mb / duration_train:.3f}MB/s"
        )
        print(f"n_samples: {X_train.shape[0]}, n_features: {X_train.shape[1]}")
        print(
            f"vectorize testing done in {duration_test:.3f}s "
            f"at {data_test_size_mb / duration_test:.3f}MB/s"
        )
        print(f"n_samples: {X_test.shape[0]}, n_features: {X_test.shape[1]}")

    return X_train, X_test, y_train, y_test, feature_names, target_names

Анализ классификатора документов на основе мешка слов

Теперь мы дважды обучим классификатор: сначала на текстовых примерах с метаданными, а затем — после их удаления. В обоих случаях проанализируем ошибки классификации на тестовой выборке с помощью матрицы ошибок и изучим коэффициенты, определяющие функцию классификации обученных моделей.

Модель без удаления метаданных

Для начала воспользуемся пользовательской функцией load_dataset, чтобы загрузить данные, не удаляя метаданные.

X_train, X_test, y_train, y_test, feature_names, target_names = load_dataset(
    verbose=True
)
2034 documents - 3.98MB (training set)
1353 documents - 2.87MB (test set)
4 categories
vectorize training done in 0.433s at 9.191MB/s
n_samples: 2034, n_features: 7831
vectorize testing done in 0.267s at 10.750MB/s
n_samples: 1353, n_features: 7831

Наша первая модель — экземпляр класса RidgeClassifier. Это линейная модель классификации, использующая среднеквадратичную ошибку для целевых значений, закодированных как {-1, 1}, по одному значению для каждого возможного класса. В отличие от LogisticRegression, RidgeClassifier не выдаёт вероятностных прогнозов (у него нет метода predict_proba), но часто обучается быстрее.

from sklearn.linear_model import RidgeClassifier

clf = RidgeClassifier(tol=1e-2, solver="sparse_cg")
clf.fit(X_train, y_train)
pred = clf.predict(X_test)

Построим матрицу ошибок этого классификатора, чтобы проверить, есть ли закономерность в ошибках классификации.

import matplotlib.pyplot as plt

from sklearn.metrics import ConfusionMatrixDisplay

fig, ax = plt.subplots(figsize=(10, 5))
ConfusionMatrixDisplay.from_predictions(y_test, pred, ax=ax)
ax.xaxis.set_ticklabels(target_names)
ax.yaxis.set_ticklabels(target_names)
_ = ax.set_title(
    f"Confusion Matrix for {clf.__class__.__name__}\non the original documents"
)
Confusion Matrix for RidgeClassifier on the original documents

Матрица ошибок показывает, что документы класса alt.atheism часто ошибочно относятся к классу talk.religion.misc и наоборот. Это ожидаемо, поскольку темы семантически связаны.

Мы также видим, что некоторые документы класса sci.space могут быть ошибочно классифицированы как comp.graphics, тогда как обратная ошибка встречается гораздо реже. Чтобы разобраться в этой асимметрии, потребуется вручную изучить документы, классифицированные неправильно. Возможно, словарь темы космоса более специфичен, чем словарь компьютерной графики.

Чтобы лучше понять, как классификатор принимает решения, изучим слова с наибольшим средним влиянием на признаки:

import numpy as np
import pandas as pd


def plot_feature_effects():
    # learned coefficients weighted by frequency of appearance
    average_feature_effects = clf.coef_ * np.asarray(X_train.mean(axis=0)).ravel()

    for i, label in enumerate(target_names):
        top5 = np.argsort(average_feature_effects[i])[-5:][::-1]
        if i == 0:
            top = pd.DataFrame(feature_names[top5], columns=[label])
            top_indices = top5
        else:
            top[label] = feature_names[top5]
            top_indices = np.concatenate((top_indices, top5), axis=None)
    top_indices = np.unique(top_indices)
    predictive_words = feature_names[top_indices]

    # plot feature effects
    bar_size = 0.25
    padding = 0.75
    y_locs = np.arange(len(top_indices)) * (4 * bar_size + padding)

    fig, ax = plt.subplots(figsize=(10, 8))
    for i, label in enumerate(target_names):
        ax.barh(
            y_locs + (i - 2) * bar_size,
            average_feature_effects[i, top_indices],
            height=bar_size,
            label=label,
        )
    ax.set(
        yticks=y_locs,
        yticklabels=predictive_words,
        ylim=[
            0 - 4 * bar_size,
            len(top_indices) * (4 * bar_size + padding) - 4 * bar_size,
        ],
    )
    ax.legend(loc="lower right")

    print("top 5 keywords per class:")
    print(top)

    return ax


_ = plot_feature_effects().set_title("Average feature effect on the original data")
Average feature effect on the original data
top 5 keywords per class:
  alt.atheism comp.graphics sci.space talk.religion.misc
0       keith      graphics     space          christian
1         god    university      nasa                com
2    atheists        thanks     orbit                god
3      people          does      moon           morality
4     caltech         image    access             people

Видно, что наиболее предсказательные слова часто сильно положительно связаны с одним классом и отрицательно — со всеми остальными. Большинство таких положительных связей достаточно легко интерпретировать. Однако некоторые слова, например "god" и "people", положительно связаны как с "talk.misc.religion", так и с "alt.atheism", поскольку у этих двух классов, как и следовало ожидать, есть общий словарь. Но обратите внимание: есть и слова, например "christian" и "morality", которые положительно связаны только с "talk.misc.religion". Кроме того, в этой версии набора данных слово "caltech" входит в число наиболее предсказательных признаков для класса атеизма из-за загрязнения набора данных метаданными, например адресами электронной почты отправителей предыдущих сообщений в обсуждении, как показано ниже:

data_train = fetch_20newsgroups(
    subset="train", categories=categories, shuffle=True, random_state=42
)

for doc in data_train.data:
    if "caltech" in doc:
        print(doc)
        break
From: livesey@solntze.wpd.sgi.com (Jon Livesey)
Subject: Re: Morality? (was Re: <Political Atheists?)
Organization: sgi
Lines: 93
Distribution: world
NNTP-Posting-Host: solntze.wpd.sgi.com

In article <1qlettINN8oi@gap.caltech.edu>, keith@cco.caltech.edu (Keith Allan Schneider) writes:
|> livesey@solntze.wpd.sgi.com (Jon Livesey) writes:
|>
|> >>>Explain to me
|> >>>how instinctive acts can be moral acts, and I am happy to listen.
|> >>For example, if it were instinctive not to murder...
|> >
|> >Then not murdering would have no moral significance, since there
|> >would be nothing voluntary about it.
|>
|> See, there you go again, saying that a moral act is only significant
|> if it is "voluntary."  Why do you think this?

If you force me to do something, am I morally responsible for it?

|>
|> And anyway, humans have the ability to disregard some of their instincts.

Well, make up your mind.    Is it to be "instinctive not to murder"
or not?

|>
|> >>So, only intelligent beings can be moral, even if the bahavior of other
|> >>beings mimics theirs?
|> >
|> >You are starting to get the point.  Mimicry is not necessarily the
|> >same as the action being imitated.  A Parrot saying "Pretty Polly"
|> >isn't necessarily commenting on the pulchritude of Polly.
|>
|> You are attaching too many things to the term "moral," I think.
|> Let's try this:  is it "good" that animals of the same species
|> don't kill each other.  Or, do you think this is right?

It's not even correct.    Animals of the same species do kill
one another.

|>
|> Or do you think that animals are machines, and that nothing they do
|> is either right nor wrong?

Sigh.   I wonder how many times we have been round this loop.

I think that instinctive bahaviour has no moral significance.
I am quite prepared to believe that higher animals, such as
primates, have the beginnings of a moral sense, since they seem
to exhibit self-awareness.

|>
|>
|> >>Animals of the same species could kill each other arbitarily, but
|> >>they don't.
|> >
|> >They do.  I and other posters have given you many examples of exactly
|> >this, but you seem to have a very short memory.
|>
|> Those weren't arbitrary killings.  They were slayings related to some
|> sort of mating ritual or whatnot.

So what?     Are you trying to say that some killing in animals
has a moral significance and some does not?   Is this your
natural morality>


|>
|> >>Are you trying to say that this isn't an act of morality because
|> >>most animals aren't intelligent enough to think like we do?
|> >
|> >I'm saying:
|> >    "There must be the possibility that the organism - it's not
|> >    just people we are talking about - can consider alternatives."
|> >
|> >It's right there in the posting you are replying to.
|>
|> Yes it was, but I still don't understand your distinctions.  What
|> do you mean by "consider?"  Can a small child be moral?  How about
|> a gorilla?  A dolphin?  A platypus?  Where is the line drawn?  Does
|> the being need to be self aware?

Are you blind?   What do you think that this sentence means?

        "There must be the possibility that the organism - it's not
        just people we are talking about - can consider alternatives."

What would that imply?

|>
|> What *do* you call the mechanism which seems to prevent animals of
|> the same species from (arbitrarily) killing each other?  Don't
|> you find the fact that they don't at all significant?

I find the fact that they do to be significant.

jon.

Такие заголовки, подписи в конце сообщений (а также цитируемые метаданные из предыдущих сообщений) можно считать побочной информацией, которая искусственно раскрывает группу новостей, позволяя определить её зарегистрированных участников. В идеале наш текстовый классификатор должен учиться только на «основном содержимом» каждого текстового документа, а не полагаться на раскрытую личность авторов.

Модель с удалением метаданных

Параметр remove загрузчика набора данных 20 newsgroups в scikit-learn позволяет эвристически попытаться отфильтровать часть нежелательных метаданных, искусственно упрощающих задачу классификации. Следует учитывать, что такая фильтрация текста далека от совершенства.

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

(
    X_train,
    X_test,
    y_train,
    y_test,
    feature_names,
    target_names,
) = load_dataset(remove=("headers", "footers", "quotes"))

clf = RidgeClassifier(tol=1e-2, solver="sparse_cg")
clf.fit(X_train, y_train)
pred = clf.predict(X_test)

fig, ax = plt.subplots(figsize=(10, 5))
ConfusionMatrixDisplay.from_predictions(y_test, pred, ax=ax)
ax.xaxis.set_ticklabels(target_names)
ax.yaxis.set_ticklabels(target_names)
_ = ax.set_title(
    f"Confusion Matrix for {clf.__class__.__name__}\non filtered documents"
)
Confusion Matrix for RidgeClassifier on filtered documents

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

_ = plot_feature_effects().set_title("Average feature effects on filtered documents")
Average feature effects on filtered documents
top 5 keywords per class:
  alt.atheism comp.graphics sci.space talk.religion.misc
0         don      graphics     space                god
1      people          file      like          christian
2         say        thanks      nasa              jesus
3    religion         image     orbit         christians
4        post          does    launch              wrong

В следующем разделе мы оставим набор данных без метаданных, чтобы сравнить несколько классификаторов.

Сравнение производительности классификаторов

Scikit-learn предоставляет множество различных алгоритмов классификации. В этом разделе мы обучим несколько таких классификаторов на одной и той же задаче классификации текста и измерим как качество их обобщения (точность на тестовой выборке), так и вычислительную производительность (скорость) — и на этапе обучения, и на этапе тестирования. Для этого определим следующие вспомогательные средства для сравнения производительности:

from sklearn import metrics
from sklearn.utils.extmath import density


def benchmark(clf, custom_name=False):
    print("_" * 80)
    print("Training: ")
    print(clf)
    t0 = time()
    clf.fit(X_train, y_train)
    train_time = time() - t0
    print(f"train time: {train_time:.3}s")

    t0 = time()
    pred = clf.predict(X_test)
    test_time = time() - t0
    print(f"test time:  {test_time:.3}s")

    score = metrics.accuracy_score(y_test, pred)
    print(f"accuracy:   {score:.3}")

    if hasattr(clf, "coef_"):
        print(f"dimensionality: {clf.coef_.shape[1]}")
        print(f"density: {density(clf.coef_)}")
        print()

    print()
    if custom_name:
        clf_descr = str(custom_name)
    else:
        clf_descr = clf.__class__.__name__
    return clf_descr, score, train_time, test_time

Теперь обучим и протестируем наборы данных с помощью 8 различных моделей классификации и получим показатели производительности для каждой модели. Цель этого исследования — показать компромисс между вычислительными затратами и точностью для разных типов классификаторов в такой задаче многоклассовой классификации текста.

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

from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression, SGDClassifier
from sklearn.naive_bayes import ComplementNB
from sklearn.neighbors import KNeighborsClassifier, NearestCentroid
from sklearn.svm import LinearSVC

results = []
for clf, name in (
    (LogisticRegression(C=5, max_iter=1000), "Logistic Regression"),
    (RidgeClassifier(alpha=1.0, solver="sparse_cg"), "Ridge Classifier"),
    (KNeighborsClassifier(n_neighbors=100), "kNN"),
    (RandomForestClassifier(), "Random Forest"),
    # L2 penalty Linear SVC
    (LinearSVC(C=0.1, dual=False, max_iter=1000), "Linear SVC"),
    # L2 penalty Linear SGD
    (
        SGDClassifier(
            loss="log_loss", alpha=1e-4, n_iter_no_change=3, early_stopping=True
        ),
        "log-loss SGD",
    ),
    # NearestCentroid (aka Rocchio classifier)
    (NearestCentroid(), "NearestCentroid"),
    # Sparse naive Bayes classifier
    (ComplementNB(alpha=0.1), "Complement naive Bayes"),
):
    print("=" * 80)
    print(name)
    results.append(benchmark(clf, name))
================================================================================
Logistic Regression
________________________________________________________________________________
Training:
LogisticRegression(C=5, max_iter=1000)
train time: 0.2s
test time:  0.000716s
accuracy:   0.772
dimensionality: 5316
density: 1.0


================================================================================
Ridge Classifier
________________________________________________________________________________
Training:
RidgeClassifier(solver='sparse_cg')
train time: 0.0315s
test time:  0.000848s
accuracy:   0.76
dimensionality: 5316
density: 1.0


================================================================================
kNN
________________________________________________________________________________
Training:
KNeighborsClassifier(n_neighbors=100)
train time: 0.00124s
test time:  0.0422s
accuracy:   0.752

================================================================================
Random Forest
________________________________________________________________________________
Training:
RandomForestClassifier()
train time: 1.69s
test time:  0.0672s
accuracy:   0.7

================================================================================
Linear SVC
________________________________________________________________________________
Training:
LinearSVC(C=0.1, dual=False)
train time: 0.0332s
test time:  0.000769s
accuracy:   0.752
dimensionality: 5316
density: 1.0


================================================================================
log-loss SGD
________________________________________________________________________________
Training:
SGDClassifier(early_stopping=True, loss='log_loss', n_iter_no_change=3)
train time: 0.0397s
test time:  0.000719s
accuracy:   0.764
dimensionality: 5316
density: 1.0


================================================================================
NearestCentroid
________________________________________________________________________________
Training:
NearestCentroid()
train time: 0.125s
test time:  0.00167s
accuracy:   0.748

================================================================================
Complement naive Bayes
________________________________________________________________________________
Training:
ComplementNB(alpha=0.1)
train time: 0.00246s
test time:  0.000577s
accuracy:   0.779

Точность, время обучения и время тестирования для каждого классификатора

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

indices = np.arange(len(results))

results = [[x[i] for x in results] for i in range(4)]

clf_names, score, training_time, test_time = results
training_time = np.array(training_time)
test_time = np.array(test_time)

fig, ax1 = plt.subplots(figsize=(10, 8))
ax1.scatter(score, training_time, s=60)
ax1.set(
    title="Score-training time trade-off",
    yscale="log",
    xlabel="test accuracy",
    ylabel="training time (s)",
)
fig, ax2 = plt.subplots(figsize=(10, 8))
ax2.scatter(score, test_time, s=60)
ax2.set(
    title="Score-test time trade-off",
    yscale="log",
    xlabel="test accuracy",
    ylabel="test time (s)",
)

for i, txt in enumerate(clf_names):
    ax1.annotate(txt, (score[i], training_time[i]))
    ax2.annotate(txt, (score[i], test_time[i]))
  • Score-training time trade-off
  • Score-test time trade-off

Наивный байесовский классификатор обеспечивает наилучший компромисс между оценкой и временем обучения и тестирования, тогда как случайный лес медленно обучается, требует больших затрат времени на предсказание и сравнительно неточен. Это ожидаемо: в задачах прогнозирования с большим числом измерений линейные модели часто подходят лучше, поскольку при размерности пространства признаков от 10 000 и выше большинство задач становится линейно разделимым.

Различия в скорости обучения и точности линейных моделей можно объяснить выбором функции потерь, которую они оптимизируют, и типом используемой регуляризации. Имейте в виду, что некоторые линейные модели с той же функцией потерь, но другим решателем или настройками регуляризации, могут обучаться за другое время и показывать иную точность на тестовой выборке. На втором графике видно, что после обучения все линейные модели имеют примерно одинаковую скорость предсказания. Это ожидаемо, поскольку все они реализуют одну и ту же функцию предсказания.

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

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

Launch binder
Launch JupyterLite

Download Jupyter notebook: plot_document_classification_20newsgroups.ipynb

Download Python source code: plot_document_classification_20newsgroups.py

Download zipped: plot_document_classification_20newsgroups.zip

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

Пример конвейера извлечения признаков из текста и оценки

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

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

Сравнение FeatureHasher и DictVectorizer

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

Spec-Zone.ru

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