話題の記事

いまさら聞けない?scikit-learnのキホン

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、小澤です。

今回は、scikit-learn入門として、機械学習を使ったシステム構築の流れを見てみましょう。 機械学習というと複雑な数式などを駆使して難しいプログラムを実装するイメージがあるかもしれませんが、 ライブラリを利用するだけであれば簡単であることがわかるかと思います。

機械学習の種類

機械学習には様々な種類のものがあります。 ここでは、分類方法として以下のようにしています。

  • 教師あり学習
  • 教師なし学習
  • その他(半教師あり学習、強化学習など)

教師あり学習

教師あり学習ではデータと正解ラベルの2つの情報が渡されます。 大量のデータから「このデータの正解はこのラベルであった」というパターンを見つけ出して、正解ラベルのないデータに対してもそれを予測するものになります。

教師あり学習はさらに正解ラベルの種類によって回帰と分類に分けられます。

回帰は、正解となる値が連続した数値となるものです。 たとえば、ビールの売り上げは気温に影響すると言われています。 この情報を元に実際の売り上げと気温のデータを利用して機械学習を行うことで、天気予報から得た翌日の気温から売り上げを予測することが可能になります。

分類は、正解となる値が数値ではない場合に利用するものとなります。 これは、ユーザの行動から次の契約更新時に「継続する」「解約する」、や画像に写っているものが「ねこ」「いぬ」「うさぎ」のどれか、などいくつかの項目のうちどれかが正解となるものです。

Deep Learningも教師あり学習の一種であるフィードフォワード型のニューラルネットワークから発展したものです。 (※ニューラルネットワークにも様々な種類がありますが、ここでは割愛します)

教師なし学習

教師なし学習は正解ラベルのないデータに対して、その性質に規則性を発見するような手法になります。

正解ラベルが必要ない分、教師あり学習よりもデータを揃えるコストは低くなりますが、どのような結果が望ましいかを人間が指定することはできません。 たとえば、教師なし学習の代表的なものの1つであるクラスタリングでは、データをいくつかのクラスタに分類します。 この時のスラスタ数は手法によって人間が設定するものもありますが、個々のクラスタにはどのような性質を持つかは人間が判断することになります。 この結果は当初期待していたような基準での分類になるとは限りませんし、人間にとっては理解できない基準での分類になっていることもあります。

教師あり学習を行う前にデータの特徴を掴んでおくなどで利用するなど、それ自体をそのまま使うのではなく次のステップへつなげるための事前準備として利用されることも多くあります。

教師なし学習にはこの他にも有名どころとして、次元削減と呼ばれる手法に利用されたりします。

その他

教師あり/なしの分類に入らない手法も存在します。

例えば、強化学習は明確な教師データではないが行動した結果に応じて報酬が与えられることで、徐々により良い行動を行うようになっていくというものです。 強化学習はDeepLearningとの組み合わせで盛り上がっている領域でもあり、最近では「その他」には含まず単独で教師あり/なしと並んで項目にあげられることも多くなっています。

機械学習の流れ

最初に、一連の流れを確認しておきます。 下の図は、教師あり学習を想定したものとなります。 機械学習の種類によって異なる点もあるため、以降は教師あり学習に絞って話を進めていきます。

mllib

機械学習では大まかに「学習」と「予測」のフローに分けられます。 学習では、データとその正解ラベルからパターンを学習します。 パターンを学習するというのは例えば

latex-image-1

のような形式で表されます。 これは先ほどのビールの例ですと、yが売り上げ、xが気温や季節、天候などといった観測されたデータになります。 各wの値を適切に設定してやることでxに具体的な値を入れた時のyの値が実際の売り上げに近くなるように、wを調整していくのが学習で実際に行っている処理になります。

学習を行った結果の各wの値はモデルとして保存され、次の予測で利用されます。

予測は、学習によって決められたwの値を元に、実際の売り上げが未知の情報に対するxを入れた時にどうなるかを求めます。 これは過去の売り上げを元に天気予報から明日の売り上げを予測するといったプロセスになります。

次に、この2つのプロセスの詳細を順に見ていきます。

学習

データ収集・前処理

学習のプロセスではまず、データの収集・前処理から始まります。 機械学習は大量のデータからそのパターンを見つけ出すという処理を行うため、それに十分なデータを準備してやる必要があります。

データを集めるに際して、注意点がいくつかあります。

まず、データであればどのようなものでもいいわけではありません。 ビールの例だと、気温などは影響があるかもしれませんが、ビール会社社員の飼っている猫がその日鳴いた回数など何の関係もないデータを入れても意味がありません。 また、先ほどの「ビール会社社員の飼っている猫がその日鳴いた回数」などは仮関連があったとしても、その日になっていないと計測できないため予測の際に利用することができません。 予測するのに必要なデータかつ取集可能なものである必要があります。

また、「収集せずともデータならすでにたくさんある」という方もいるかもしれません。 上記のように、どのようなものを予測したいかによってそれに必要な情報も異なるため、その場合でも今あるデータとしてどのようなものがあるのか、や足りない情報はないか、などの確認が必要になる場合があります。

教師あり学習の場合、データとともに正解ラベルをどのようにつけるかも考えなければなりません。 ビールの売り上げのように、すでにわかっている場合もあれば、口コミ情報がポジティブな内容かネガティブな内容かのように人間が別途付与してやる必要がある場合もあります。 機械学習で必要なデータ量を考えると、人手で正解ラベルを付与するのは非常にコストのかかる作業となります。 また、人間が付与する必要がある場合は、作業者によってラベルのつけ方に違いが生じる場合もあるかもしれませんのでそういった考慮も必要になります。

企業などでは内外に散らばる多くのデータを一箇所に集めるところから作業が始まる場合もあり、こちらも非常に大掛かりな作業となることが予想されます。

さて、こうして収集されたデータが必ずしやすぐに分析に使える形式になっているとは限りません。

外れ値や欠損値のようなものをどのように扱うかや、口コミデータなど文字列の場合は表記ゆれの解消、ETL処理などが必要になります。 また、詳細は割愛しますが、特徴量のスケーリングやBoWへの変換なども必要になります。

学習・評価

さて、こうしてデータが整ってようやく機械学習が可能になります。 このプロセスまでたどり着いた方はおめでとうございます!これで全体作業の8割は終了しました!

学習を行うに際しては、アルゴリズム選択・特徴選択・パラメータチューニングが主なプロセスとなります。 こちらに関してはこの後scikit-learnの具体例で行っていくので詳細はそちらで記載します。

モデルを作成したら、学習を行ったモデルがどのくらいうまく予想できるのかを評価します。 このプロセスもこの後scikit-learnの具体例で行っていくので詳細はそちらで記載します。

予測

次に予測の流れを見ていきます。 こちらはそれほど難しいことはなく、入力されたデータに対して学習時と同じように特徴抽出を行い、作成したモデルから予測値を求めるだけです。 オフラインでの予測もありますが、システムに組み込んでオンラインで予測する際などにはAPIなどにしておくと便利でしょう。

機械学習ではモデルがシステムに組み込まれて、定常的に予測が可能になってもそれで終わりではありません。

まず、第1に学習時の評価が良かったからといってそれがシステムの利用者に受け入れられるとは限りません。 オフラインでの定量的な評価とは別に実際の利用者がどのような評価をするかも重要になってきます。

また、この際機械学習では細かい制御ができないことが問題になる場合もあります。機械学習ではいわゆる「常識的な」判断ができないため、例えばテレビを買った人にテレビをレコメンドするなど間違えても許容されるようなものと、差別的な意味合いを含むような間違いもどちらも等しく1件の間違いとして扱われます。 特に、予測時にはどのようなデータが入力されるかわからないので、学習時の評価では出てこなかったような予測ミスがあるかもしれません。

次に、モデルの鮮度についてです。 人間の行動は日々何らかの基準によって変わっていきます。 これによってモデル作成時に利用したデータだけでは対応しきれず、徐々にうまく予測できなくなっていくといった状況になることがあります。 機械学習を利用したシステムを導入したことがそのトリガーとなっていた、なんてこともあり得るため、一度導入しておしまいではなく、常に現状を計測してモデルを更新していく必要があります。

scikit-learnで機械学習をしてみる

さて、ではいよいよ実際にやってみましょう。

環境構築

まずは環境構築です。

pip install sklearn

でも導入可能ですが、scikit-learn自体やnumpyなどの依存ライブラリでC言語のコンパイルが必要だったりするのでここではAnacondaを利用することにします。

今回はPython3用のAnaconda3-4.2.0を利用します。

今回使うデータセット

今回はscikit-learnに付属のdigitsを利用します。 これは8x8の0から9の手書き文字画像となっています。64個の特徴それぞれが各ピクセルの値を表していて、そのデータからどの文字であるかを予測するものになります。 オリジナルのデータセットはUCIにあります。

実際の画像データは以下のようになっています。

download

これを出力するにはRecognizing hand-written digitsを参照してください。

まずはざっくりとデータを確認しましょう。

最初にデータセットを読み込みます

# datasetを読み込む
from sklearn.datasets import load_digits

# load_digitsの引数でクラス数を指定
# 2なら0と1, 3なら0と1と2が書かれたデータのみに絞られる
# 最大は10で0から9となる
digits = load_digits(10)

次にデータの中身を確認してみましょう

# dataにデータが入ってる
print(digits.data)

[[  0.   0.   5. ...,   0.   0.   0.]
 [  0.   0.   0. ...,  10.   0.   0.]
 [  0.   0.   0. ...,  16.   9.   0.]
 ..., 
 [  0.   0.   1. ...,   6.   0.   0.]
 [  0.   0.   2. ...,  12.   0.   0.]
 [  0.   0.  10. ...,  12.   1.   0.]]
# 正解ラベルはtargetに入っている
print(digits.target)

[0 1 2 ..., 8 9 8]
# データの形
# データ1件あたり、8x8=64の特徴が配列(numpyのndarray)となっていて
# データ件数が1797件分ある
print(digits.data.shape)

(1797, 64)

データの分割

次に、データを学習用とテスト用に分割します。 機械学習はこれから入力される未知のデータに対する予測ができることを目的としていますが、 通常未知のデータは手に入らないので、すでに正解のわかっているデータを分割し、一部を答え合わせに利用することで未知のデータへの予測を擬似的に実現します。

なぜそのようなことをするかというと、機械学習は「過学習」という学習データの性質を表すことのみに特化してしまう可能性があるからです。 通常、機械学習を行う際に利用するデータにはノイズが乗ってます。 また、結果に影響する全ての情報を取得できるわけではないため、未知のデータに対して100%完全に予測することはできません。 そのため、与えられた学習データのノイズなどを含めて全てをデータの性質として利用してしまうと、それだけに特化したモデルが出来上がるというわけです。 詳細は割愛しますが、そういったことを防ぐために正則化や特徴選択、次元削減などを利用し100%正確ではないけど、本質的な性質に基づいて予測を行えるようにする必要があります。

評価時に学習に利用したのと別なデータで予測性能を測るのは、学習データのみでは過学習してしまっていても気づけないという問題があるためです。

データの分け方にいくつかのパターンがあります。

  • 学習データ、テストデータに分ける
    • 学習データで学習を行い、テストデータでの予測性能を評価します
  • 学習データ、検証データ、テストデータに分ける
    • 様々なアルゴリズム、パラメータで試行錯誤する際に学習データで学習し、検証データで評価したのち、もっとも性能の良かったものを最後にテストデータで評価し検証データに対しても過学習していないか確認する
  • クロスバリデーション
    • データをn個(5個とか10個とか)に分割して、そのうちn-1個を学習に、残りの1個をテストに利用するというのをn回繰り返します。データの件数が少なく可能な限り無駄にしたくない時などに特に有効です

今回は学習用とテスト用の2つに分割します。 処理内容自体は特に難しいことのない普通のpythonコードです

# 今回は1500件を学習データ、残りの297件をテストデータにする
train_X = digits.data[:1500]
train_y = digits.target[:1500]

test_X = digits.data[1500:]
test_y = digits.target[1500:]

学習してみる

さて、いよいよ実際に学習を行います。 まずはLogistic Regressionという手法で試しにやってみます。

from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
# fit関数で学習を行う
lr.fit(train_X, train_y)

# predict関数で予測を行う
pred = lr.predict(test_X) 
print(pred)

array([3, 7, 4, 6, 3, 1, 3, 9, 1, 7, 6, 8, 4, 3, 1, 4, 0, 5, 3, 6, 9, 6, 3,
       7, 5, 4, 4, 7, 2, 8, 2, 2, 5, 7, 9, 5, 4, 8, 8, 4, 9, 0, 8, 9, 8, 0,
       1, 2, 3, 4, 5, 6, 8, 1, 9, 0, 1, 2, 3, 4, 5, 6, 9, 0, 1, 2, 3, 4, 5,
       6, 7, 1, 7, 4, 9, 1, 5, 6, 5, 0, 9, 8, 1, 8, 4, 1, 7, 7, 1, 5, 1, 6,
       0, 2, 2, 1, 8, 2, 0, 1, 2, 6, 8, 7, 7, 7, 3, 4, 6, 6, 6, 9, 9, 1, 5,
       0, 9, 5, 2, 8, 0, 1, 7, 6, 3, 2, 1, 7, 9, 6, 3, 1, 3, 9, 1, 8, 6, 8,
       4, 3, 1, 4, 0, 5, 3, 6, 3, 6, 1, 7, 5, 4, 4, 7, 2, 2, 5, 7, 3, 1, 9,
       4, 1, 0, 8, 9, 8, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 8, 4, 5, 6,
       7, 8, 9, 0, 8, 2, 8, 4, 5, 6, 7, 8, 9, 0, 9, 5, 5, 6, 5, 0, 9, 8, 9,
       8, 4, 1, 7, 7, 3, 5, 1, 0, 0, 2, 2, 7, 8, 2, 0, 1, 2, 6, 8, 8, 7, 5,
       8, 4, 6, 6, 6, 4, 9, 1, 5, 0, 9, 5, 2, 8, 2, 0, 0, 8, 7, 6, 3, 2, 1,
       7, 4, 6, 3, 1, 3, 9, 1, 7, 6, 8, 4, 5, 1, 4, 0, 5, 3, 6, 9, 6, 8, 7,
       5, 4, 4, 7, 2, 8, 2, 2, 5, 7, 9, 5, 4, 8, 8, 4, 9, 0, 8, 9, 8])

さて、テストデータに対する予測値が出力されましたが、これだけではよくわかりませんね。 そこでどのくらい正解したのかを出してみましょう。 今回はconfusion matrixを見てみることにします。これをどのように見るかの前にまずはやってみましょう

from sklearn.metrics import confusion_matrix
confusion_matrix(test_y, pred, labels=digits.target_names)

array([[25,  0,  0,  0,  1,  0,  1,  0,  0,  0],
       [ 0, 26,  0,  2,  0,  0,  0,  0,  3,  0],
       [ 0,  0, 27,  0,  0,  0,  0,  0,  0,  0],
       [ 0,  1,  0, 19,  0,  2,  0,  2,  6,  0],
       [ 0,  0,  0,  0, 30,  0,  0,  0,  0,  3],
       [ 0,  2,  0,  0,  0, 28,  0,  0,  0,  0],
       [ 0,  0,  0,  0,  0,  0, 30,  0,  0,  0],
       [ 0,  1,  0,  0,  0,  0,  0, 27,  2,  0],
       [ 0,  2,  0,  0,  0,  0,  0,  0, 26,  0],
       [ 0,  2,  0,  2,  0,  0,  0,  1,  0, 26]])

縦軸が正解、横軸が予測値として、そこに該当するデータ件数が出力されます。 対角成分が正解した値の数です。

例えば正解ラベルが0のものであれば、25個は0と予測し、4と6と予測したものが1つずつあったことになります。 確かに4や6は丸っこく書くと0に見えるような書き方になってしまいますね。

より実践的に

さて、ここまでで、機械学習を使ったプログラムが実装できました。 最後により実践的な内容として、pipelineを使ってみましょう。

今回は、Logistic RegressionではなくSVMを利用しています。 また、機械学習には手法によって様々なパラメータがあり、状況によってパラメータの適切な値が異なります。 Grid Searchを利用したパラメータチューニングをしています(※ 今回はかなり荒い探索です。より細かくして本来は2段階でやったり、ベイズ推定したりしたほうがいいです)。 ここでは解説しませんが、実際のベストなパラメータなどを知りたい場合は、clfの中身から確認できます。

import numpy as np

from sklearn.datasets import load_digits
from sklearn.pipeline import Pipeline
from sklearn.grid_search import GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.metrics import confusion_matrix, classification_report
from sklearn.externals import joblib

digits = load_digits(10)

train_X = digits.data[:1500]
train_y = digits.target[:1500]

test_X = digits.data[1500:]
test_y = digits.target[1500:]

# Pipelineを作成
# データの正規化とSVMを定義
pipeline = Pipeline([
        ('standard_scaler', StandardScaler()),
        ('svm', SVC())])

# パラメータの探索範囲を指定
# Grid Search用のパラメータは本来であればもっと細かくやったほうがいい
params = {
    'svm__C' : np.logspace(0, 2, 5),
    'svm__gamma' : np.logspace(-3, 0, 5)
}

# Grid Searchを行う
clf = GridSearchCV(pipeline, params)
clf.fit(train_X, train_y)

pred = clf.predict(test_X)

# 結果のレポーティング
print(classification_report(test_y, pred))
print(confusion_matrix(test_y, pred))

# モデルの保存
# APIなどで利用する際はjoblib.loadで保存したモデルを読み込んで、入力されたデータに対してpredictを行えば良い
joblib.dump(clf, 'clf.pkl') 

             precision    recall  f1-score   support

          0       1.00      0.96      0.98        27
          1       0.94      1.00      0.97        31
          2       1.00      1.00      1.00        27
          3       1.00      0.67      0.80        30
          4       0.97      0.91      0.94        33
          5       0.88      1.00      0.94        30
          6       1.00      1.00      1.00        30
          7       0.94      1.00      0.97        30
          8       0.76      0.93      0.84        28
          9       0.90      0.87      0.89        31

avg / total       0.94      0.93      0.93       297

[[26  0  0  0  1  0  0  0  0  0]
 [ 0 31  0  0  0  0  0  0  0  0]
 [ 0  0 27  0  0  0  0  0  0  0]
 [ 0  0  0 20  0  3  0  0  7  0]
 [ 0  0  0  0 30  0  0  0  0  3]
 [ 0  0  0  0  0 30  0  0  0  0]
 [ 0  0  0  0  0  0 30  0  0  0]
 [ 0  0  0  0  0  0  0 30  0  0]
 [ 0  2  0  0  0  0  0  0 26  0]
 [ 0  0  0  0  0  1  0  2  1 27]]

レポーティング結果にある、precision, recall, f1-scoreなどは今回解説していませんが、興味がある方は調べてみてください。

終わりに

今回はscikit-learnと機械学習の基本的な部分の解説をしました。 本来であれば、まだまだ知っておかないとうまくいかない知識などが多くありますが、ライブラリを使うだけであれば比較的簡単に利用できることがわかっていただけたかと思います。