Classificação de Documentos de Texto

Beginner

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

Introdução

Este laboratório demonstra como usar o scikit-learn para classificar documentos de texto em diferentes categorias. Usaremos o conjunto de dados 20 newsgroups, que contém cerca de 18.000 mensagens de grupos de notícias sobre 20 tópicos. Usaremos uma abordagem de "bag of words" e uma matriz esparsa de termos-documento ponderada por Tf-idf para codificar as características. O laboratório também demonstrará vários classificadores que podem lidar eficientemente com matrizes esparsas.

Dicas da Máquina Virtual

Após o arranque da máquina virtual, clique no canto superior esquerdo para mudar para a aba Notebook para aceder ao Jupyter Notebook para a prática.

Às vezes, pode ser necessário esperar alguns segundos para o Jupyter Notebook terminar de carregar. A validação das operações não pode ser automatizada devido a limitações no Jupyter Notebook.

Se tiver problemas durante o aprendizado, não hesite em contactar o Labby. Forneça feedback após a sessão e resolveremos o problema rapidamente para si.

Carregamento e Vectorização do Conjunto de Dados 20 Newsgroups

Definimos uma função para carregar dados do conjunto de dados 20newsgroups, que compreende cerca de 18.000 mensagens de grupos de notícias sobre 20 tópicos divididos em dois subconjuntos: um para treino e outro para teste. Carregaremos e vectorizaremos o conjunto de dados sem remover os metadados.

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=()):
    """Carregar e vectorizar o conjunto de dados 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,
    )

    ## a ordem das etiquetas em `target_names` pode ser diferente de `categories`
    target_names = data_train.target_names

    ## dividir o alvo em um conjunto de treino e um conjunto de teste
    y_train, y_test = data_train.target, data_test.target

    ## Extraindo características dos dados de treino usando um vectorizador esparso
    vectorizer = TfidfVectorizer(
        sublinear_tf=True, max_df=0.5, min_df=5, stop_words="english"
    )
    X_train = vectorizer.fit_transform(data_train.data)

    ## Extraindo características dos dados de teste usando o mesmo vectorizador
    X_test = vectorizer.transform(data_test.data)

    feature_names = vectorizer.get_feature_names_out()

    if verbose:
        print(f"{len(data_train.data)} documentos")
        print(f"{len(data_test.data)} documentos")
        print(f"{len(target_names)} categorias")
        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)

Análise de um Classificador de Documentos Bag-of-Words

Agora, treinaremos um classificador duas vezes, uma vez nos exemplos de texto incluindo metadados e outra após remover os metadados. Analisaremos os erros de classificação num conjunto de teste usando uma matriz de confusão e inspecionaremos os coeficientes que definem a função de classificação dos modelos treinados.

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"Matriz de Confusão para {clf.__class__.__name__}\nos documentos originais"
)

def plot_feature_effects():
    ## coeficientes aprendidos ponderados pela frequência de ocorrência
    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]

    ## plotar os efeitos das características
    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("5 palavras-chave principais por classe:")
    print(top)

    return ax

_ = plot_feature_effects().set_title("Efeito médio das características nos dados originais")

Modelo com Remoção de Metadados

Agora, usaremos a opção remove do carregador de dados 20 newsgroups no scikit-learn para treinar um classificador de texto que não dependa demasiado dos metadados para tomar suas decisões. Também analisaremos os erros de classificação em um conjunto de teste usando uma matriz de confusão e inspecionaremos os coeficientes que definem a função de classificação dos modelos treinados.

(
    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"Matriz de Confusão para {clf.__class__.__name__}\nos documentos filtrados"
)

_ = plot_feature_effects().set_title("Efeitos médios das características nos documentos filtrados")

Comparação de Classificadores

Agora, treinaremos e testaremos os conjuntos de dados com oito modelos de classificação diferentes e obteremos os resultados de desempenho para cada modelo. O objetivo deste estudo é destacar os trade-offs entre computação e precisão de diferentes tipos de classificadores para um problema de classificação de texto multiclasse.

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), "Regressão Logística"),
    (RidgeClassifier(alpha=1.0, solver="sparse_cg"), "Classificador Ridge"),
    (KNeighborsClassifier(n_neighbors=100), "kNN"),
    (RandomForestClassifier(), "Floresta Aleatória"),
    ## Linear SVC com penalidade L2
    (LinearSVC(C=0.1, dual=False, max_iter=1000), "Linear SVC"),
    ## Linear SGD com penalidade L2
    (
        SGDClassifier(
            loss="log_loss", alpha=1e-4, n_iter_no_change=3, early_stopping=True
        ),
        "SGD com log-loss",
    ),
    ## NearestCentroid (também conhecido como classificador Rocchio)
    (NearestCentroid(), "NearestCentroid"),
    ## Classificador Naive Bayes esparso
    (ComplementNB(alpha=0.1), "Naive Bayes Complementar"),
):
    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="Trade-off entre Precisão e Tempo de Treinamento",
    yscale="log",
    xlabel="Precisão no teste",
    ylabel="Tempo de treinamento (s)",
)
fig, ax2 = plt.subplots(figsize=(10, 8))
ax2.scatter(score, test_time, s=60)
ax2.set(
    title="Trade-off entre Precisão e Tempo de Teste",
    yscale="log",
    xlabel="Precisão no teste",
    ylabel="Tempo de teste (s)",
)

for i, txt in enumerate(clf_names):
    ax1.annotate(txt, (score[i], training_time[i]))
    ax2.annotate(txt, (score[i], test_time[i]))

Resumo

Este laboratório demonstrou como usar o scikit-learn para classificar documentos de texto em diferentes categorias. Carregamos o conjunto de dados 20 newsgroups e utilizamos uma abordagem de "bag of words" e uma matriz esparsa de termos-documento ponderada por Tf-idf para codificar as características. Treinamos um classificador duas vezes: uma vez nos exemplos de texto incluindo metadados e outra após a remoção dos metadados. Analisamos os erros de classificação em um conjunto de teste usando uma matriz de confusão e inspecionamos os coeficientes que definem a função de classificação dos modelos treinados. Também treinamos e testamos os conjuntos de dados com oito modelos de classificação diferentes e obtivemos resultados de desempenho para cada modelo. O objetivo deste estudo foi destacar os trade-offs entre tempo de computação e precisão de diferentes tipos de classificadores para este problema de classificação de texto multiclasse.