唯物是真 @Scaled_Wurm

プログラミング(主にPython2.7)とか機械学習とか

pythonの機械学習ライブラリscikit-learnの紹介

scikit-learn(sklearn)の日本語の入門記事があんまりないなーと思って書きました。
どちらかっていうとよく使う機能の紹介的な感じです。
英語が読める方は公式のチュートリアルがおすすめです。

scikit-learnとは?

scikit-learnはオープンソース機械学習ライブラリで、分類回帰クラスタリングなどの機能が実装されています。
また様々な評価尺度クロスバリデーション、パラメータのグリッドサーチなどの痒いところに手が届く機能もあります。

インストール

scikit-learnの他にもnumpyとかscipyとかが必要です。
Windows 64 bit版の人は以下のURLに色々なインストーラーがおいてあるのでおすすめ

その他の人は以下のURLを見てapt-getなりMacportsなりでインストールしてください。

他にもmatplotlibを入れておくとグラフがかけるので嬉しいです

サンプルデータセットの読み込み

sklearnにはIrisなどのトイデータセットサンプルデータの自動生成などの機能もあります。

トイデータセットの一つのdigits(数字)のデータを使ってみます
以下のサンプルコードを実行すると次のような画像が得られます
これは0のデータのうちの一つです
f:id:sucrose:20130523134818p:plain

>>> from sklearn.datasets import load_digits
>>> digits = load_digits()
>>> print digits.data.shape
(1797, 64)
>>> import pylab as pl 
>>> pl.gray() 
>>> pl.matshow(digits.images[0]) 
>>> pl.show() 
http://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_digits.html

基本的に機械学習ではデータ一つ一つをベクトルで表現します
この例では縦横が8×8の大きさなので64次元のベクトルになっています
データ全体ではデータ数×64次元の行列の形になっています。

>>> digits.data[0]
array([  0.,   0.,   5.,  13.,   9.,   1.,   0.,   0.,   0.,   0.,  13.,
        15.,  10.,  15.,   5.,   0.,   0.,   3.,  15.,   2.,   0.,  11.,
         8.,   0.,   0.,   4.,  12.,   0.,   0.,   8.,   8.,   0.,   0.,
         5.,   8.,   0.,   0.,   9.,   8.,   0.,   0.,   4.,  11.,   0.,
         1.,  12.,   7.,   0.,   0.,   2.,  14.,   5.,  10.,  12.,   0.,
         0.,   0.,   0.,   6.,  13.,  10.,   0.,   0.,   0.])

線形SVMによる二値分類

二値分類では入力されたデータを正例負例の2つのクラス(ラベル)に分類します。
とりあえずdigitsの中から0(負例)と1(正例)の2つのデータだけを使って二値分類します

いわゆる線形分類器の一つである線形(Linear)SVMを使います。
他にもたくさんの分類器が実装されています。
以下の名前ではSVMではなくSVCとなっていますが、Classification(分類)の頭文字のCです。

データをトレーニング用とテスト用に分けて、トレーニングデータで訓練したモデルでテストデータを予測してみます。
データの分割にはtrain_test_split関数を使います(デフォルトではデータをトレーニング:テスト = 3:1に分割します)

# -*- coding: utf-8 -*-
from sklearn.datasets import load_digits
from sklearn.cross_validation import train_test_split
from sklearn.svm import LinearSVC

#データの読み込み
digits = load_digits(2)

#トレーニングデータとテストデータに分割
data_train, data_test, label_train, label_test = train_test_split(digits.data, digits.target)

#分類器にパラメータを与える
estimator = LinearSVC(C=1.0)

#トレーニングデータで学習する
estimator.fit(data_train, label_train)

#テストデータの予測をする
label_predict = estimator.predict(data_test)

label_predictには以下のように推定されたラベル(今回は0か1)が入っています

array([0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1,
       1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1,
       0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1,
       1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1])

今回はLinearSVCを使っていますが、他の分類器や回帰などを使う場合でも"分類器の名前(パラメータ=値)"の形でEstimator(推定器)を作ってfit関数などにデータを入力として与えて、同様に結果を得ます。

分類結果の評価

分類器で得られた推定結果がテストデータとどれぐらい一致しているかでモデルの評価を行います。
評価にはいろいろな評価尺度が使われていて、以下でいくつか紹介します。

Confusion Matrix

だいたいの評価関数は以下のソースコードのように、真のラベルと推定されたラベルを引数に与えれば、結果を計算してくれます。

from sklearn.metrics import confusion_matrix
print confusion_matrix(label_test, label_predict)
array([[43,  0],
       [ 0, 47]])

これは尺度というよりはどの正解ラベルのデータをどのラベルに何個分類したかという表です。
対角線上が正解した数(1を1、0を0と分類)となっていて、その他が間違いの数(1を0、0を1と分類)になっています

今回は簡単な例なので、すべてが正解しています。
例が簡単すぎたので、以下の説明は公式の例を使っていきます。

以下の公式の例のように多クラス(二値よりも多い)分類の場合でも使えます

>>> from sklearn.metrics import confusion_matrix
>>> y_true = [2, 0, 2, 2, 0, 1]
>>> y_pred = [0, 0, 2, 2, 0, 2]
>>> confusion_matrix(y_true, y_pred)
array([[2, 0, 0],
       [0, 0, 1],
       [1, 0, 2]])
http://scikit-learn.org/dev/modules/generated/sklearn.metrics.confusion_matrix.html

Accuracy (正解率)

>>> from sklearn.metrics import accuracy_score
>>> y_pred = [0, 2, 1, 3]
>>> y_true = [0, 1, 2, 3]
>>> accuracy_score(y_true, y_pred)
0.5
http://scikit-learn.org/stable/modules/model_evaluation.html#accuracy-score

Classification Report

この関数はPrecisionRecallF値とsupport(正解ラベルのデータの数)を教えてくれます
Precision、Recall、F値は評価に非常によく使われています。
Precision、Recall、F値の説明については以下のURLを参照

>>> from sklearn.metrics import classification_report
>>> y_true = [0, 1, 2, 2, 0]
>>> y_pred = [0, 0, 2, 2, 0]
>>> target_names = ['class 0', 'class 1', 'class 2']
>>> print(classification_report(y_true, y_pred, target_names=target_names))
             precision    recall  f1-score   support

    class 0       0.67      1.00      0.80         2
    class 1       0.00      0.00      0.00         1
    class 2       1.00      1.00      1.00         2

avg / total       0.67      0.80      0.72         5
http://scikit-learn.org/stable/modules/generated/sklearn.metrics.classification_report.html

表の形ではない出力が欲しい場合にはprecision_recall_fscore_support関数で得られます

二値分類では以下のようになります

>>> from sklearn.metrics import precision_recall_fscore_support
>>> y_pred = [0, 1, 0, 0]
>>> y_true = [0, 1, 0, 1]
>>> p, r, f, s = precision_recall_fscore_support(y_true, y_pred, beta=0.5)
>>> p  
array([ 0.66...,  1.        ])
>>> r
array([ 1. ,  0.5])
>>> f  
array([ 0.71...,  0.83...])
>>> s  
array([2, 2]...)
http://scikit-learn.org/dev/modules/generated/sklearn.metrics.precision_recall_fscore_support.html

Precision Recall curve

PrecisionとRecallはトレード・オフの関係にあるため、単一の値では評価しづらく、その関係を図示することがあります
この曲線の下側の面積(AUC)をAverage Precisionというらしいです

f:id:sucrose:20130523180326p:plain

以下のソースコードは長くなっていますが、基本的にはprecision_recall_curve関数に正解ラベルと推定したラベルの確率(あるいは決定境界からの距離)を与えて、その結果をグラフにすればいいだけです
この計算の時には今までのような推定したラベルの結果ではなくラベルの確率などを使って正例と負例の境界の閾値を変えながらPrecision、Recallを求めています。
以下では確率の推定であるpredict_proba関数を使っていますが、SVMでは代わりに決定境界からの距離であるdecision_function関数が使えます

import random
import pylab as pl
import numpy as np
from sklearn import svm, datasets
from sklearn.metrics import precision_recall_curve
from sklearn.metrics import auc

# import some data to play with
iris = datasets.load_iris()
X = iris.data
y = iris.target
X, y = X[y != 2], y[y != 2]  # Keep also 2 classes (0 and 1)
n_samples, n_features = X.shape
p = range(n_samples)  # Shuffle samples
random.seed(0)
random.shuffle(p)
X, y = X[p], y[p]
half = int(n_samples / 2)

# Add noisy features
np.random.seed(0)
X = np.c_[X, np.random.randn(n_samples, 200 * n_features)]

# Run classifier
classifier = svm.SVC(kernel='linear', probability=True)
probas_ = classifier.fit(X[:half], y[:half]).predict_proba(X[half:])

# Compute Precision-Recall and plot curve
precision, recall, thresholds = precision_recall_curve(y[half:], probas_[:, 1])
area = auc(recall, precision)
print "Area Under Curve: %0.2f" % area

pl.clf()
pl.plot(recall, precision, label='Precision-Recall curve')
pl.xlabel('Recall')
pl.ylabel('Precision')
pl.ylim([0.0, 1.05])
pl.xlim([0.0, 1.0])
pl.title('Precision-Recall example: AUC=%0.2f' % area)
pl.legend(loc="lower left")
pl.show()
http://scikit-learn.sourceforge.net/stable/auto_examples/plot_precision_recall.html

ROC Curve

Precision Recall Curve以外にもROC Curveというものが使われることがあります
これは敏感度(sensitivity)特異度(specificity)に対して、縦軸が敏感度、横軸が偽陽性率(1 - 特異度)のカーブを描いたものです。
ちなみに敏感度は正例のRecall、特異度は負例のRecallと等しくなります。
これも曲線の下側の面積(AUC)が評価に使われることがあります

f:id:sucrose:20130523181716p:plain

import numpy as np
import pylab as pl
from sklearn import svm, datasets
from sklearn.utils import shuffle
from sklearn.metrics import roc_curve, auc

random_state = np.random.RandomState(0)

# Import some data to play with
iris = datasets.load_iris()
X = iris.data
y = iris.target

# Make it a binary classification problem by removing the third class
X, y = X[y != 2], y[y != 2]
n_samples, n_features = X.shape

# Add noisy features to make the problem harder
X = np.c_[X, random_state.randn(n_samples, 200 * n_features)]

# shuffle and split training and test sets
X, y = shuffle(X, y, random_state=random_state)
half = int(n_samples / 2)
X_train, X_test = X[:half], X[half:]
y_train, y_test = y[:half], y[half:]

# Run classifier
classifier = svm.SVC(kernel='linear', probability=True)
probas_ = classifier.fit(X_train, y_train).predict_proba(X_test)

# Compute ROC curve and area the curve
fpr, tpr, thresholds = roc_curve(y_test, probas_[:, 1])
roc_auc = auc(fpr, tpr)
print "Area under the ROC curve : %f" % roc_auc

# Plot ROC curve
pl.clf()
pl.plot(fpr, tpr, label='ROC curve (area = %0.2f)' % roc_auc)
pl.plot([0, 1], [0, 1], 'k--')
pl.xlim([0.0, 1.0])
pl.ylim([0.0, 1.0])
pl.xlabel('False Positive Rate')
pl.ylabel('True Positive Rate')
pl.title('Receiver operating characteristic example')
pl.legend(loc="lower right")
pl.show()
http://scikit-learn.org/stable/auto_examples/plot_roc.html

クロスバリデーション(交差検定)

上のほうで二値分類を試すときにデータをトレーニング用とテスト用に分解しました。
しかし、データを分けるとそれぞれに使えるデータが少なくなってしまいます。
そこで、クロスバリデーションという手法が使われることがあります。

クロスバリデーションではデータをいくつかに分割して、1個をテスト用、残りをトレーニング用に使ってスコアの計算をします。
このとき分けられたデータのすべてがテストに選ばれるようにくりかえし評価を行い、そのスコアの平均を使って評価をします。
データをランダムにK個に等分する方法をK-Fold Cross Validationといいます。
またクラスごとのデータの比率を保ったままK個に等分する場合は Stratified K-Fold Cross Validation といいます。
これらは以下のようにイテレータとして実装されています。

また変わり種の方法として、以下のLeaveOneLabelOut(この呼び方はsklearnで初めて見ました)があります
これはデータをあらかじめ与えた分割用のラベルを使って分ける方法で、分け方に自然な分割があるとき、例えばデータが年ごとに分かれていて、それぞれの年で分割してクロスバリデーションしたい場合などに使えます。

複雑なことをやるには上記のイテレータを使った方がいいのですが、単純な用途では以下のcross_val_score関数を使うと、もっと簡単に書けます

>>> clf = svm.SVC(kernel='linear', C=1)
>>> scores = cross_validation.cross_val_score(
...    clf, iris.data, iris.target, cv=5)
...
>>> scores                                            
array([ 1.  ...,  0.96...,  0.9 ...,  0.96...,  1.        ])
http://scikit-learn.org/stable/modules/cross_validation.html#computing-cross-validated-metrics

この関数はそれぞれのクロスバリデーションごとの評価値のarrayを返してくれます
クロスバリデーションの方法として、上で紹介したようないくつかの方法を指定することができますし、また評価関数も指定出来ます(ただしfloatを返す評価関数しか使えないですが)
更にこの関数は並列化の機能も実装されていて、引数に並列数を指定するだけでクロスバリデーション内のそれぞれの実行を並列に行うことができます。

グリッドサーチ

分類器などのモデルにはいくつか指定しなければならないパラメータがあり、これらは結果に大きな影響を与えます。
適切なパラメータを選ぶのによく使われるのがグリッドサーチという方法で、これはいくつかのパラメータの組み合わせを実際に試して評価関数を計算し、スコアがよかったパラメータを選ぶというものです
本来はトレーニング用とパラメータチューニング用、テスト用にデータを分けるのが適切なやり方ですが、クロスバリデーションでトレーニングとテストを分けた後に、トレーニング側だけを使ってパラメータチューニング用のクロスバリデーションをすることもあるみたいです。

これも引数に並列数を指定するだけで、並列化できます。

>>> from sklearn import svm, grid_search, datasets
>>> iris = datasets.load_iris()
>>> parameters = {'kernel':('linear', 'rbf'), 'C':[1, 10]}
>>> svr = svm.SVC()
>>> clf = grid_search.GridSearchCV(svr, parameters)
>>> clf.fit(iris.data, iris.target)
http://scikit-learn.org/stable/modules/generated/sklearn.grid_search.GridSearchCV.html

パラメータごとの結果は以下のようにgrid_scores_メンバ変数に格納されています

[({'C': 1, 'kernel': 'linear'},
  0.97999999999999998,
  array([ 0.96,  0.98,  1.  ])),
 ({'C': 1, 'kernel': 'rbf'},
  0.96666666666666667,
  array([ 0.96,  0.96,  0.98])),
 ({'C': 10, 'kernel': 'linear'},
  0.96666666666666667,
  array([ 0.96,  0.98,  0.96])),
 ({'C': 10, 'kernel': 'rbf'},
  0.96666666666666667,
  array([ 0.96,  0.98,  0.96]))]

不均衡データ

ラベルごとのデータ数が大きくアンバランスなデータだと学習がうまくいかないときがあります。
たとえば正例:負例=1:100とかだったりすると、十分なデータがあっても訓練したモデルはすべてを負例に分類してしまったりします。
こういうときはクラスに対する重み(LinearSVCならclass_weight)を変えたりresample関数を使ってトレーニングデータ内の比率が1:1に近くなるようにアンダーサンプリングやオーバーサンプリングをしたりすると結果がよくなることがあります。

特徴量の抽出

分類器のモデルの入力(データのベクトルによる表現)をどうやって作るかという話です。

↑のURLを見るといろいろな機能があります。
以前、一部の機能を以下の記事で解説しました

まとめ

紹介しきれていないですがscikit-learnは多くの手法が実装されていて便利です。
定型的な処理はライブラリを使ったほうがバグが入らないと思うので、もっと積極的にライブラリを使っていきましょう