テキスト分類問題その1 チュートリアル

はじめに

本チュートリアルでは「テキスト分類問題その1」コンペティションを題材にして、Pythonとscikit-learnを利用した分類器の実装方法を解説します。まず第一部で、Bag-of-featuresデータの読み込み方法と初歩的な分類器の学習手順を紹介します。第二部では、発展的な話題に触れます。なお、本チュートリアルでは学習手法の詳細には立ち入りません。

掲載コードは、Python2.7.8、scikit-learn 0.15NumPy 1.9.1SciPy 0.14.0で動作確認を行っております。scikit-learnのインストール方法は、Installing scikit-learn — scikit-learn 0.15.2 documentationを参照してください。

第一部:初歩的な分類器の作成

訓練データの読み込み

まずは、正解ラベルが与えられている「訓練データ」を読み込みます。本コンペティションで用いられているsvmlightフォーマットのデータは、scikit-learnのload_svmlight_fileにより読み込むことができます。コンペティションページより予測用データをダウンロードし、以下のコードを./text-classification-001/上で実行すると、Xに特徴量、yに正解ラベルが格納されます。

In [1]: import os
In [2]: from sklearn.datasets import load_svmlight_file
In [3]: BOW_DIR = 'bag-of-words'
In [4]: DATA_TRAIN_PATH = os.path.join(BOW_DIR, 'data-train.dat')
In [5]: N_FEATURES = 37040
In [6]: X, y = load_svmlight_file(DATA_TRAIN_PATH, n_features=N_FEATURES, dtype=int, zero_based=True)

次に読み込んだデータを、分類器学習に用いるサンプル(X_train, y_train)と、分類器の評価に用いるサンプル(X_val, y_val)に分割します。分割には、train_test_splitが利用できます。

In [7]: from sklearn.cross_validation import train_test_split
In [8]: TEST_SIZE = 0.2
In [9]: RANDOM_STATE = 0
In [10]: X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=TEST_SIZE, random_state=RANDOM_STATE)

ここでは、全体の20%を評価用としています。以下のコードにより、2,400件のサンプルが1,920件(学習用)と480件(評価用)に分割されていることが確認できます。

In [11]: X.shape, y.shape
Out[11]: ((2400, 37040), (2400,))
In [12]: X_train.shape, y_train.shape
Out[12]: ((1920, 37040), (1920,))
In [13]: X_val.shape, y_val.shape
Out[13]: ((480, 37040), (480,))

ナイーブベイズ分類器の学習と評価

初歩的な分類器であるナイーブベイズ分類器の学習手順を紹介します。scikit-learnにはいくつかのナイーブベイズの実装がありますが、ここではMultinomialNBを用います。

以下のコードで、分類器の学習を実行できます。

In [14]: from sklearn.naive_bayes import MultinomialNB
In [15]: clf = MultinomialNB(alpha=0.1)
In [16]: clf.fit(X_train, y_train)
Out[16]: MultinomialNB(alpha=0.1, class_prior=None, fit_prior=True)

ここでは、分類器のパラメータをalpha=0.1に設定しています。

次に、評価用データを用いて、分類器の性能を確認します。まず、評価用データに対する予測結果を出力します。

In [17]: y_pred = clf.predict_proba(X_val)[:, 1]

ここでは、コンペティションで指定されている通り、「ラベルが1である確率」を出力しています。

コンペティションで採用されている評価指標 (Area under the ROC curve, AUC)により、性能を評価します。roc_auc_scoreを用います。

In [18]: from sklearn.metrics import roc_auc_score
In [19]: roc_auc_score(y_val, y_pred)
Out[19]: 0.60255682394209131

評価用データに対するAUCスコアを確認できました。次に、alphaの値を変えて新しい分類器を学習してみましょう。

In [20]: clf_new = MultinomialNB(alpha=1.0)
In [21]: clf_new.fit(X_train, y_train)
Out[21]: MultinomialNB(alpha=1.0, class_prior=None, fit_prior=True)
In [22]: y_pred_new = clf_new.predict_proba(X_val)[:, 1]
In [23]: roc_auc_score(y_val, y_pred_new)
Out[23]: 0.65065187858634599

alpha=1.0にするとAUCスコアが向上することを確認できました。

予測結果の提出

alpha=1.0を採用し、予測結果を提出しましょう。これまでは評価用のデータは学習に用いませんでしたが、提出時には全てのデータ(X, y)を用いて分類器を再度学習します。

In [24]: clf_submit = MultinomialNB(alpha=1.0)
In [25]: clf_submit.fit(X, y)
Out[25]: MultinomialNB(alpha=1.0, class_prior=None, fit_prior=True)

次に、学習した分類器を用いてテストデータに対する予測結果を出力します。まずは、テストデータを読み込みます。

In [26]: DATA_TEST_PATH = os.path.join(BOW_DIR, 'data-test.dat')
In [27]: X_test, y_test_dummy = load_svmlight_file(DATA_TEST_PATH, n_features=N_FEATURES, dtype=int, zero_based=True)

テストデータに対する予測を行い、結果を出力します。

In [28]: SUBMIT_PATH = 'sample-submission-basic.dat'
In [29]: y_submit = clf_submit.predict_proba(X_test)[:, 1]
In [30]: import numpy as np
In [31]: np.savetxt(SUBMIT_PATH, y_submit, fmt='%.10f')

コンペティションページにて、sample-submission-basic.datを提出しましょう。以上で、分類器の学習と予測結果の提出が完了しました。

第二部:発展的な話題

予測性能向上のための発展的な話題に触れていきます。以下のコードにより、必要なモジュールと変数が設定されているものとします。

In [1]: import os
In [2]: import numpy as np
In [3]: from sklearn.datasets import load_svmlight_file
In [4]: from sklearn.cross_validation import train_test_split
In [5]: from sklearn.metrics import roc_auc_score
In [6]: BOW_DIR = 'bag-of-words'
In [7]: DATA_PATH = os.path.join(BOW_DIR, 'data-train.dat')
In [8]: DATA_TEST_PATH = os.path.join(BOW_DIR, 'data-test.dat')
In [9]: N_FEATURES = 37040
In [10]: TEST_SIZE = 0.2
In [11]: RANDOM_STATE = 0
In [12]: X, y = load_svmlight_file(DATA_PATH, n_features=N_FEATURES, dtype=int, zero_based=True)
In [13]: X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=TEST_SIZE, random_state=RANDOM_STATE)

様々な分類手法

ナイーブベイズ以外にも、scikit-learnでは様々な分類手法が提供されています。代表的なものに、ロジスティック回帰やSVMがあります。

L1正則化ロジスティック回帰の例 (LogisticRegression)

In [14]: from sklearn.linear_model import LogisticRegression
In [15]: clf_lr = LogisticRegression(penalty='l1')
In [16]: clf_lr.fit(X_train, y_train)
Out[16]: LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
                   intercept_scaling=1, penalty='l1', random_state=None, tol=0.0001)
In [17]: y_pred_lr = clf_lr.predict_proba(X_val)[:, 1]
In [18]: roc_auc_score(y_val, y_pred_lr)
Out[18]: 0.64002717005616505

線形SVMの例 (SVC)

In [19]: from sklearn.svm import SVC
In [20]: clf_svc = SVC(kernel = 'linear', probability = True)
In [21]: clf_svc.fit(X_train, y_train)
Out[21]: SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0, degree=3, gamma=0.0,
  kernel='linear', max_iter=-1, probability=True, random_state=None,
  shrinking=True, tol=0.001, verbose=False)
In [22]: y_pred_svc = clf_svc.predict_proba(X_val)[:, 1]
In [23]: roc_auc_score(y_val, y_pred_svc)
Out[23]: 0.71369046412133252

色々な手法を試してみましょう。

特徴抽出

新しいBag of words表現の作成

あらかじめ提供されているbag-of-words/以外の特徴量を用いることで、予測性能が向上する可能性があります。text/内のデータを用いて新しい特徴量を作成してみましょう。まずは、text/内のデータを読み込みます。

In [24]: ROOT_DIR = 'text'
In [25]: TEXT_DIR = os.path.join(ROOT_DIR, 'train')
In [26]: TEXT_TEST_DIR = os.path.join(ROOT_DIR, 'test')
In [27]: FILES_PATH = os.path.join(ROOT_DIR, 'text-files-train.dat')
In [28]: FILES_TEST_PATH = os.path.join(ROOT_DIR, 'text-files-test.dat')
In [29]: files = np.loadtxt(FILES_PATH, dtype=str)
In [30]: texts = np.array([file(os.path.join(TEXT_DIR, f), 'r').read().strip() for f in files])
In [31]: files_test = np.loadtxt(FILES_TEST_PATH, dtype=str)
In [32]: texts_test = np.array([file(os.path.join(TEXT_TEST_DIR, f), 'r').read().strip() for f in files_test])

第一部と同様に、訓練データを学習用(80%)と評価用(20%)に分割します。

In [33]: train, val, y_train, y_val = train_test_split(np.arange(texts.shape[0]), y, test_size=TEST_SIZE, random_state=RANDOM_STATE)
In [34]: texts_train = texts[train]
In [35]: texts_val = texts[val]

bag-of-words/内の特徴量は「5つ以上の文書で登場する単語のみ」を用いて作成されていました。ここでは、「2つ以上の文書で登場する単語」を用いた特徴量を作成してみましょう。CountVectorizerを用います。

In [36]: from sklearn.feature_extraction.text import CountVectorizer
In [37]: MIN_DF = 2
In [38]: count_vect = CountVectorizer(lowercase=True, min_df=MIN_DF)
In [38]: X_cnt_train = count_vect.fit_transform(texts_train)
In [40]: X_cnt_val = count_vect.transform(texts_val)
In [41]: X_cnt_test = count_vect.transform(texts_test)

CountVectorizerは単語の出現回数を特徴として抽出しますが、TF-IDF特徴として抽出するTfidfVectorizerも用意されています。

In [42]: from sklearn.feature_extraction.text import TfidfVectorizer
In [43]: tfidf_vect = TfidfVectorizer(lowercase=True, min_df=MIN_DF)
In [44]: X_tfidf_train = tfidf_vect.fit_transform(texts_train)
In [45]: X_tfidf_val = tfidf_vect.transform(texts_val)
In [46]: X_tfidf_test = tfidf_vect.transform(texts_test)

CountVectorizerTfidfVectorizerには様々なオプションが用意されています。色々な特徴抽出方法を試してみましょう。

単語の符号化について

本コンペティション用のデータでは、単語はある規則にしたがって符号化されています。たとえば、以下のように符号化されています。

  • Fly -> F86155
  • Flying -> F86155b43
  • flight -> f86155j152

ここから、以下のような対応関係が推測されます。

F ly ing
F 86155 b43

末尾のb43には何か意味がありそうです。b43の出現回数を特徴として抽出してみましょう。CountVectorizertoken_patternパラメータでは、抽出パターンを正規表現で指定することができます。

In [47]: TOKEN_PATTERN_B43 = u'(?u)\\b\\w+b43\\b'
In [48]: count_vect_b43 = CountVectorizer(lowercase=True, min_df=MIN_DF, token_pattern=TOKEN_PATTERN_B43)
In [49]: X_cnt_b43_train = count_vect_b43.fit_transform(texts_train).sum(axis=1)
In [50]: X_cnt_b43_val = count_vect_b43.transform(texts_val).sum(axis=1)
In [51]: X_cnt_b43_test = count_vect_b43.transform(texts_test).sum(axis=1)

各単語の出現回数を用いた特徴(X_cnt_train, X_cnt_val, X_cnt_test)と新しく作成した特徴量(X_cnt_b43_train, X_cnt_b43_val, X_cnt_b43_test)を結合することもできます。

In [52]: import scipy
In [53]: X_cnt_combine_b43_train = scipy.sparse.hstack([X_cnt_train, X_cnt_b43_train])
In [54]: X_cnt_combine_b43_val = scipy.sparse.hstack([X_cnt_val, X_cnt_b43_val])
In [55]: X_cnt_combine_b43_test = scipy.sparse.hstack([X_cnt_test, X_cnt_b43_test])

色々な特徴量を作ってみましょう。

クロスバリデーション

これまでは訓練データを学習用(80%)と評価用(20%)に分割して分類器の学習と性能確認を行いました。より精緻な性能確認手法に、クロスバリデーションがあります。クロスバリデーションでは、データをK分割し、「1つを評価用」「残りK-1個を学習用」に利用するプロセスをK回繰り返して、分類器の平均的な性能を確認します。scikit-learnではcross_validationが提供されています。ここでは、K=5の場合に、AUCの平均と標準偏差を出力するコードを紹介します。

In [56]: from sklearn.cross_validation import KFold
In [57]: from sklearn.naive_bayes import MultinomialNB
In [58]: kf = KFold(y.shape[0], n_folds=5, shuffle=False)
In [59]: aucs = []
In [60]: for train, val in kf:
             X_train_cv, y_train_cv = X[train], y[train]
             X_val_cv, y_val_cv = X[val], y[val]
             clf_cv = MultinomialNB(alpha=1.0)
             clf_cv.fit(X_train_cv, y_train_cv)
             y_pred_cv = clf_cv.predict_proba(X_val_cv)[:, 1]
             auc = roc_auc_score(y_val_cv, y_pred_cv)
             aucs.append(auc)
In [61]: np.mean(aucs), np.std(aucs)
Out[61]: (0.67273428817310921, 0.030980080080578294)

おわりに

本チュートリアルでは「テキスト分類問題その1」コンペティションの予測モデル構築方法を紹介しました。お気軽にコンペティションに参加してみてください。