Классификация текстовых документов

Beginner

This tutorial is from open-source community. Access the source code

Введение

В этом практическом занятии показано, как использовать scikit-learn для классификации текстовых документов по разным категориям. Будем использовать датасет 20 newsgroups, в котором содержится около 18 000 новостных сообщений по 20 темам. Будем использовать метод bag of words и разреженную матрицу документ-слово с весами Tf-idf для кодирования признаков. В практическом занятии также будут показаны различные классификаторы, которые могут эффективно обрабатывать разреженные матрицы.

Советы по использованию ВМ

После запуска ВМ нажмите в левом верхнем углу, чтобы переключиться на вкладку Notebook и получить доступ к Jupyter Notebook для практики.

Иногда может потребоваться подождать несколько секунд, пока Jupyter Notebook не загрузится полностью. Валидация операций не может быть автоматизирована из-за ограничений Jupyter Notebook.

Если вы сталкиваетесь с проблемами при обучении, не стесняйтесь обращаться к Labby. Оставьте отзыв после занятия, и мы оперативно решим проблему для вас.

Загрузка и векторизация текстового датасета 20 Newsgroups

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

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

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

def load_dataset(verbose=False, remove=()):
    """Загрузка и векторизация датасета 20 newsgroups."""
    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,
    )

    ## порядок меток в `target_names` может отличаться от `categories`
    target_names = data_train.target_names

    ## разделение целевого признака на тренировочный и тестовый наборы
    y_train, y_test = data_train.target, data_test.target

    ## Извлечение признаков из тренировочных данных с использованием разреженного векторизатора
    vectorizer = TfidfVectorizer(
        sublinear_tf=True, max_df=0.5, min_df=5, stop_words="english"
    )
    X_train = vectorizer.fit_transform(data_train.data)

    ## Извлечение признаков из тестовых данных с использованием того же векторизатора
    X_test = vectorizer.transform(data_test.data)

    feature_names = vectorizer.get_feature_names_out()

    if verbose:
        print(f"{len(data_train.data)} документов")
        print(f"{len(data_test.data)} документов")
        print(f"{len(target_names)} категорий")
        print(f"n_samples: {X_train.shape[0]}, n_features: {X_train.shape[1]}")
        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

X_train, X_test, y_train, y_test, feature_names, target_names = load_dataset(verbose=True)

Анализ классификатора документов в стиле bag-of-words

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

from sklearn.linear_model import RidgeClassifier
from sklearn.metrics import ConfusionMatrixDisplay
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

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 the original documents"
)

def plot_feature_effects():
    ## изученные коэффициенты, взвешенные частотой появления
    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]

    ## построение графика влияния признаков
    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")

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

Теперь мы будем использовать параметр 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"
)

_ = plot_feature_effects().set_title("Average feature effects on filtered documents")

Бенчмаркинг классификаторов

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

from sklearn.utils.extmath import density
from sklearn import metrics
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
from sklearn.linear_model import SGDClassifier
from sklearn.naive_bayes import ComplementNB
from sklearn.neighbors import KNeighborsClassifier
from sklearn.neighbors import NearestCentroid
from sklearn.ensemble import RandomForestClassifier

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))

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]))

Резюме

В этом практическом занятии показано, как использовать scikit-learn для классификации текстовых документов по разным категориям. Мы загрузили датасет 20 newsgroups и использовали подход bag of words и разреженную матрицу документ-терм с весами Tf-idf для кодирования признаков. Мы обучили классификатор дважды: один раз на текстовых примерах, включая метаданные, и один раз после удаления метаданных. Мы проанализировали ошибки классификации на тестовом наборе с использованием матрицы неточностей и изучили коэффициенты, которые определяют функцию классификации обученных моделей. Также мы обучили и протестировали датасеты с помощью восьми различных моделей классификации и получили результаты производительности для каждой модели. Цель этого исследования была подчеркнуть компромисс между вычислительной сложностью и точностью различных типов классификаторов для такой задачи многоклассовой классификации текстов.