はじめに
この実験では、scikit-learn を使って文書を異なるカテゴリに分類する方法を示します。20 のトピックに関する約 18,000 のニュースグループ投稿を含む 20 ニュースグループデータセットを使用します。特徴をエンコードするために、単語袋アプローチと Tf-idf 重み付き文書 - 用語疎行列を使用します。また、疎行列を効率的に処理できるさまざまな分類器を示します。
VM のヒント
VM の起動が完了したら、左上隅をクリックしてノートブックタブに切り替えて、Jupyter Notebook を使った練習を行います。
場合によっては、Jupyter Notebook が読み込み終わるまで数秒待つ必要があります。Jupyter Notebook の制限により、操作の検証を自動化することはできません。
学習中に問題がある場合は、Labby にお問い合わせください。セッション後にフィードバックを提供してください。すぐに問題を解決いたします。
20 ニュースグループのテキストデータセットの読み込みとベクトル化
20newsgroups_dataset からデータを読み込む関数を定義します。このデータセットは、20 のトピックに関する約 18,000 のニュースグループ投稿から構成されており、2 つのサブセットに分割されています。1 つは訓練用、もう 1 つはテスト用です。メタデータを削除せずにデータセットを読み込み、ベクトル化します。
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 ニュースグループのデータセットを読み込み、ベクトル化します。"""
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)
単語袋文書分類器の分析
ここでは、分類器を 2 回訓練します。1 回目はメタデータ付きのテキストサンプルで、2 回目はメタデータを削除した後です。混同行列を使ってテストセット上の分類エラーを分析し、訓練済みモデルの分類関数を定義する係数を調べます。
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"混同行列 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("各クラスの上位 5 つのキーワード:")
print(top)
return ax
_ = plot_feature_effects().set_title("Average feature effect on the original data")
メタデータを削除したモデル
ここでは、scikit-learn の 20 ニュースグループデータセットローダーのremoveオプションを使って、決定に際してメタデータに依存しないテキスト分類器を訓練します。また、混同行列を使ってテストセット上の分類エラーを分析し、訓練済みモデルの分類関数を定義する係数を調べます。
(
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"混同行列 for {clf.__class__.__name__}\non filtered documents"
)
_ = plot_feature_effects().set_title("Average feature effects on filtered documents")
分類器のベンチマーク
ここでは、8 つの異なる分類モデルを使ってデータセットを訓練とテストし、各モデルの性能結果を取得します。この研究の目的は、このような多クラスのテキスト分類問題に対する異なる種類の分類器の計算量/精度のトレードオフを明らかにすることです。
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), "ロジスティック回帰"),
(RidgeClassifier(alpha=1.0, solver="sparse_cg"), "リッジ分類器"),
(KNeighborsClassifier(n_neighbors=100), "kNN"),
(RandomForestClassifier(), "ランダムフォレスト"),
## L2 ペナルティ付きの線形 SVC
(LinearSVC(C=0.1, dual=False, max_iter=1000), "線形 SVC"),
## L2 ペナルティ付きの線形 SGD
(
SGDClassifier(
loss="log_loss", alpha=1e-4, n_iter_no_change=3, early_stopping=True
),
"log-loss SGD",
),
## 最近傍重心法(つまりロッキオ分類器)
(NearestCentroid(), "最近傍重心法"),
## 疎なナイーブベイズ分類器
(ComplementNB(alpha=0.1), "補完ナイーブベイズ"),
):
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="テスト精度",
ylabel="訓練時間 (秒)",
)
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="テスト精度",
ylabel="テスト時間 (秒)",
)
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 ニュースグループのデータセットを読み込み、単語袋アプローチと Tf-idf 重み付きの文書 - 用語疎行列を使って特徴をエンコードしました。分類器を 2 回訓練しました。1 回目はメタデータ付きのテキストサンプルで、2 回目はメタデータを削除した後です。混同行列を使ってテストセット上の分類エラーを分析し、訓練済みモデルの分類関数を定義する係数を調べました。また、8 つの異なる分類モデルを使ってデータセットを訓練とテストし、各モデルの性能結果を取得しました。この研究の目的は、このような多クラスのテキスト分類問題に対する異なる種類の分類器の計算量/精度のトレードオフを明らかにすることでした。