静かなる名辞

pythonとプログラミングのこと



【python】sklearnのPipelineを使うとできること

 機械学習では、何段もの前処理をしてから最終的な分類や回帰のアルゴリズムに入力するということがよくあります。前処理にはけっこう泥臭い処理も多く、leakageの問題なども絡んできます。はっきり言って自分で書こうとすると面倒くさいです。

 こういう問題を(ある程度)解決できるのがsklearnのPipelineです。これについては、以前から「何かあるらしいな」というのは知っていましたが、実際に使ったことはありませんでした。でも、このたび使ってみたら「すげえ」となったので、こうして記事を書いている訳です。

 この記事ではPipelineのコンセプトと使い方を簡単に説明します。雰囲気は伝わるかと思いますが、細かい使い方はライブラリの公式ドキュメントを参照してください。

  • sklearn公式

sklearn.pipeline.Pipeline — scikit-learn 0.19.1 documentation

問題設定

 今回は例として、sklearnのdigits(load_digits)を対象データにして説明します。

sklearn.datasets.load_digits — scikit-learn 0.19.1 documentation

 これは0~9の数字を分類する問題で、特徴量は8*8の画像データをflattenして64次元にしたものです。このデータの分類は割と簡単な方で、直接SVMにでもかけてパラメタを追い込めばF1値にして0.95以上のスコアが得られたりするのですが、もうちょっとタチの悪いデータのつもりで扱います。

 具体的には、以下の処理をしてやることにします。

  1. RandomForestの特徴重要度を使って特徴選択
  2. PCAで累積寄与率pに次元削減
  3. SVMで分類
  4. グリッドサーチでパラメタチューニング
  5. 交差検証して性能評価

 大変そうです。でも、Pipelineを使えばすぐできます。

実装

 まず、上の1~3について、それぞれの部品を作ります。それからPipelineのインスタンスで一つにまとめます。グリッドサーチと交差検証は自分で書くことにします。

特徴選択

 これにはSelectFromModelを使うと良さそうです。分類器のfeature_importances_に基づき、重要度の高い特徴だけ残してくれます。

sklearn.feature_selection.SelectFromModel — scikit-learn 0.19.1 documentation

 何次元残すかの指定ができると使いやすかったのですが、実際はmean, medianとそれらのfloat倍、そして重要度の下限を指定できるようです。とりあえずmeanとmedianのどちらかにしてみましょう。

 分類器には今回はRandomForestを使います。つまり、次のようなコードになります。

from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.feature_selection import SelectFromModel

rfc = RFC(n_estimators=100, n_jobs=-1)
fs = SelectFromModel(rfc)

 パラメタはあとでGridsearchSVを使ってチューニングするので、今の段階で指定する必要はありません。

次元削減

 次元削減にはPCAを使います。普通にやるだけなので説明は割愛します。

from sklearn.decomposition import PCA

pca = PCA()

分類

 分類にはSVMを使います。これもインスタンスを作っておきます。

from sklearn.svm import SVC

svm = SVC()

パイプライン化

 今回の記事のキモです。パイプラインを使って上記「特徴選択」「次元削減」「分類」をすべてまとめてしまいます。

 書き方はこんな感じです。

from sklearn.pipeline import Pipeline

estimators = zip(["feature_selection", "pca", "svm"], 
                 [fs, pca, svm])
pl = Pipeline(estimators)

 とてもあっさりしていますが、これで特徴選択をし、次元削減して、分類するという一連の流れをまとめて行うインスタンスができました。

パラメタチューニング

 パラメタチューニングはGridsearchCVを使うと簡単?にできます。

sklearn.model_selection.GridSearchCV — scikit-learn 0.19.1 documentation

from sklearn.model_selection import GridSearchCV

parameters = {"feature_selection__threshold" : ["mean", "median"],
              "pca__n_components" :[0.8, 0.5],
              "svm__gamma" : [0.001, 0.01, 0.05],
              "svm__C": [1, 10]}

clf = GridSearchCV(pl, parameters)

 ちょっとパラメタ指定周りが面倒くさいですが、とにかくこうすれば後は全部自動でやってくれます。

交差検証

 これはStratifiedKFoldを使い、後は自分で書きます。クロスバリデーションをやってくれる関数もsklearnにはありますが、今回はちょっと複雑な制御(1回目のFoldでパラメタチューニングして2回目以降はそのパラメタを使いまわしたい)をするので使いません。

結果

 最終的なソースコードはこうなりました。

# coding: UTF-8

import numpy as np

from sklearn.datasets import load_digits
from sklearn.pipeline import Pipeline
from sklearn.model_selection import StratifiedKFold as SKF, GridSearchCV
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.svm import SVC
from sklearn.feature_selection import SelectFromModel
from sklearn.decomposition import PCA
from sklearn.metrics import precision_recall_fscore_support as prf

def main():
    rfc = RFC(n_estimators=100, n_jobs=-1)
    fs = SelectFromModel(rfc)
    pca = PCA()
    svm = SVC()
    estimators = zip(["feature_selection", "pca", "svm"], 
                     [fs, pca, svm])
    pl = Pipeline(estimators)
    
    parameters = {"feature_selection__threshold" : ["mean", "median"],
                  "pca__n_components" :[0.8, 0.5],
                  "svm__gamma" : [0.001, 0.01, 0.05],
                  "svm__C": [1, 10]}

    gclf = GridSearchCV(pl, parameters, n_jobs=-1, verbose=2)

    digits = load_digits()
    X = digits.data
    y = digits.target
    first_fold = True
    trues = []
    preds = []
    for train_index, test_index in SKF().split(X, y):
        if first_fold:
            gclf.fit(X[train_index], y[train_index])
            clf = gclf.best_estimator_
            first_fold = False
        clf.fit(X[train_index,], y[train_index])
        trues.append(y[test_index])
        preds.append(clf.predict(X[test_index]))

    true_labels = np.hstack(trues)
    pred_labels = np.hstack(preds)
    print("p:{0:.6f} r:{1:.6f} f1:{2:.6f}".format(
        *prf(true_labels, pred_labels, average="macro")))

if __name__ == "__main__":
    main()

 実行するとGridsearchCVのverboseがたくさん出力された後、スコアが出てきます。スコアは今回は

p:0.948840 r:0.948205 f1:0.948379

 でした。

 かなり面倒くさい処理なのに、Pipelineのおかげでシンプルに書けているのがお分かりいただけたでしょうか。

 便利なのでこれから色々使っていこうと思います。