scikit-learn でランダムフォレストによる多ラベル分類

# まえがき

本記事は[機械学習関連情報の収集と分類(構想)](http://qiita.com/suchowan/items/459062590f7134dfc138)の❷を背景としています。

例えば某企業がクラウド上の某サービスを利用して Q&A システムを構築したニュースがあったとしましょう。

そうすると[❷のローカルファイルシステムのフォルダ例](https://qiita-image-store.s3.amazonaws.com/0/144985/95d8f8f2-831c-19b8-7c67-de2973a7b0f6.png)から推察できるように、このニュースのインターネットショートカットは、

・ツール/クラウド/某サービス
・機械学習/応用/Bot・対話システム
・社会動向/企業/某企業

の少なくとも３か所に配置されねばなりません。これらの分類は排他的ではないので、いわゆる多ラベル分類です。

調べてみると Python / scikit-learn でこのような問題を扱うアルゴリズムは多様ですが、それなりに API インタフェースが統一されていて、アルゴリズムを差し替えるだけで動作する様なコードが書けるようです。

そこで、そのAPI インタフェースを確認してみようというのが本記事です。

実際のクロウル結果を扱うスクリプトは Qiita で紹介するには一般性がないため [GitHub](https://github.com/suchowan/bookmarks)で公開することとし、この記事ではインタフェースを確認するためのサンプルスクリプトのみ扱います。ランダムフォレストを選択したのも簡単のためです[^1]。データも数値の対応関係を確認するための人為的な値を用いており現実に意味のあるデータではありません。この点ご了解ください。

# scikit-learn での多クラスアルゴリズムと多ラベルアルゴリズム

scikit-learn で多クラスアルゴリズムと多ラベルアルゴリズムを扱う枠組みとしては[【翻訳】scikit-learn 0.18 User Guide 1.12. 多クラスアルゴリズムと多ラベルアルゴリズム](http://qiita.com/nazoking@github/items/9decf45d106accc6afe1)で解説されている sklearn.multiclass があります。ただし、このモジュールは問題をバイナリ分類問題に分解して扱う汎用のモジュールで、個々の分類アルゴリズムに最適化されている訳ではありません。

[決定木](https://ja.wikipedia.org/wiki/%E6%B1%BA%E5%AE%9A%E6%9C%A8)、[ランダムフォレスト](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%83%B3%E3%83%80%E3%83%A0%E3%83%95%E3%82%A9%E3%83%AC%E3%82%B9%E3%83%88)、最近傍法では多ラベル分類がそれぞれのアルゴリズム自体で実装されているので、直接それぞれのアルゴリズムを使うことになります。

# サンプルスクリプト

それではサンプルスクリプトを書いてみましょう。

```py:random-forest.py
#from sklearn.tree import DecisionTreeClassifier as classifier
from sklearn.ensemble import RandomForestClassifier as classifier
from gensim import matutils

corpus = [[(1,10),(2,20)],[(3,30),(4,40)],[(5,50),(6,60)]]
labels = [[100,500,900],[300,400,800],[200,600,700]]

dense = matutils.corpus2dense(corpus, 7)

print(dense)   #=> (*1)

print(dense.T) #=> (*2)

clf = classifier(random_state=777)
clf.fit(dense.T, labels)

for target in [[[0,10,20, 0, 0, 0, 0]], #=> (*3)
               [[0,10,20,30,40,50,60]], #=> (*4)
               [[0,10,10,0,0,0,0],      #=> (*5)
                [0,0,0,20,20,0,0],
                [0,0,0,0,0,30,30]]]:
    print(clf.predict(target))
    print(clf.predict_proba(target))
```
## classifier

コメントアウト例のようにclassifier のアルゴリズムのインポート元を変えれば、指定のアルゴリズムで分類します。

## corpus と labels

入力 corpus は３つの文書の語彙頻度を記述した sparse matrix です。
・最初の文書 - ID:1の単語を10回、ID:2の単語を20回、その他の単語は頻度 0
・次の文書   - ID:3の単語を30回、ID:4の単語を40回、その他の単語は頻度 0
・最後の文書 - ID:5の単語を50回、ID:6の単語を60回、その他の単語は頻度 0

教師データ labels はそれぞれの文書の分類を記述しています。
・最初の文書 - 最初のラベルは値100、次のラベルは値500、最後のラベルは値900
・次の文書   - 最初のラベルは値300、次のラベルは値400、最後のラベルは値800
・最後の文書 - 最初のラベルは値200、次のラベルは値600、最後のラベルは値700
つまり、
・最初のラベル - 値100、200、300 の３クラス
・次のラベル   - 値400、500、600 の３クラス
・最後のラベル - 値700、800、900 の３クラス

## sparse → dense 変換

scikit-learn が提供する分類アルゴリズムは入力として dense matrix を期待しているので、sparse matrix を dense matrix に変換せねばなりません。

それには [gensim](http://qiita.com/tags/gensim).matutils モジュールの corpus2dense が使えます。結果(*1)を見てみましょう。

```dense.
[[  0.   0.   0.]
 [ 10.   0.   0.]
 [ 20.   0.   0.]
 [  0.  30.   0.]
 [  0.  40.   0.]
 [  0.   0.  50.]
 [  0.   0.  60.]]
```
あれ？

確かにちゃんと 0 も省略しないで表現する dense matrix にはなっていますが、行が単語IDで列が文書Noですから、このままでは labels と対応関係がとれません。
よって分類アルゴリズムの入力とするには行と列を転置せねばなりません(→結果(*2))。

```dense.T
[[  0.  10.  20.   0.   0.   0.   0.]
 [  0.   0.   0.  30.  40.   0.   0.]
 [  0.   0.   0.   0.   0.  50.  60.]]
```

dense と dense.T では圧倒的に dense.T を用いるユースケースが多いと思うのですが、なぜ corpus2dense がこういう仕様になっているのかはよくわかりません。

## 分類

・分類器生成

```py:
clf = RandomForestClassifier(random_state=777)
```
ランダムフォレスト・アルゴリズムは内部で乱数を使いますから random_state を固定しないと毎回異なる結果となります。

・分類

```
print(clf.predict(target))
print(clf.predict_proba(target))
```

今回のサンプルスクリプトでは predict で分類結果を、predict_proba で根拠となった確率推算値[^2]を計算しています。

順に結果を見てみましょう。

(*3) [[0,10,20, 0, 0, 0, 0]]

```predict
[[ 100.  500.  900.]]
```

```predict_proba
[array([[ 0.8,  0.1,  0.1]]), array([[ 0.1,  0.8,  0.1]]), array([[ 0.1,  0.1,  0.8]])]
```

これは学習させたパターンのひとつですから、教師データどおりの分類になることが期待されます。

・最初のラベル - 値100:確率0.8、200:確率0.1、300:確率0.1 → 値100.
・次のラベル   - 値400:確率0.1、500:確率0.8、600:確率0.1 → 値500.
・最後のラベル - 値700:確率0.1、800:確率0.1、900:確率0.8 → 値900.

確かに教師データを再現できています。

値が出現順ではなく数値としての大きさ順に管理されていることに注意しましょう。

(*4) [[0,10,20,30,40,50,60]]


```predict
[[ 200.  600.  700.]]
```

```predict_proba
[array([[ 0.3,  0.5,  0.2]]), array([[ 0.2,  0.3,  0.5]]), array([[ 0.5,  0.2,  0.3]])]
```

・最初のラベル - 値100:確率0.3、200:確率0.5、300:確率0.2 → 値200.
・次のラベル   - 値400:確率0.2、500:確率0.3、600:確率0.5 → 値600.
・最後のラベル - 値700:確率0.5、800:確率0.2、900:確率0.3 → 値700.

(*5) [[0,10,10,0,0,0,0],[0,0,0,20,20,0,0],[0,0,0,0,0,30,30]]]

```predict
[[ 100.  500.  900.]
 [ 100.  400.  800.]
 [ 200.  600.  700.]]
```

```predict_proba
[array([[ 0.7,  0.2,  0.1],
        [ 0.4,  0.2,  0.4],
        [ 0.3,  0.5,  0.2]]),
 array([[ 0.1,  0.7,  0.2],
        [ 0.4,  0.4,  0.2],
        [ 0.2,  0.3,  0.5]]),
 array([[ 0.2,  0.1,  0.7],
        [ 0.2,  0.4,  0.4],
        [ 0.5,  0.2,  0.3]])]
```

- [0,10,10,0,0,0,0]

・最初のラベル - 値100:確率0.7、200:確率0.2、300:確率0.1 → 値100.
・次のラベル   - 値400:確率0.1、500:確率0.7、600:確率0.2 → 値500.
・最後のラベル - 値700:確率0.2、800:確率0.1、900:確率0.7 → 値900.

- [0,0,0,20,20,0,0]

・最初のラベル - 値100:確率0.4、200:確率0.2、300:確率0.4 → 値100.
・次のラベル   - 値400:確率0.4、500:確率0.4、600:確率0.2 → 値400.
・最後のラベル - 値700:確率0.2、800:確率0.4、900:確率0.4 → 値800.

- [0,0,0,0,0,30,30]

・最初のラベル - 値100:確率0.3、200:確率0.5、300:確率0.2 → 値200.
・次のラベル   - 値400:確率0.2、500:確率0.3、600:確率0.5 → 値600.
・最後のラベル - 値700:確率0.5、800:確率0.2、900:確率0.3 → 値700.

predict_proba のもっとも外側の次元がラベルNoであることに注意しましょう。

sklearn.multiclass で問題をバイナリ分類問題に分解して扱う場合、必然的にもっとも外側の繰り返しがラベルNoになります[^3]から自然な仕様だと思います。

# あとがき

こうしてみるとインタフェースとなる行列の各次元が何に対応しているかは、特に多ラベルの場合には錯綜していて分かりにくく、このようなメモを残す意味もあったかなと思います。

もし認識違いなどありましたらご指摘いただければありがたいです。

[^1]: ランダムフォレストに関してはチューニングまで検討した[scikit-learnとgensimでニュース記事を分類する](http://qiita.com/yasunori/items/31a23eb259482e4824e2)という記事があります。
[^2]: 分類アルゴリズムによっては必ずしも確率とみなせない場合もあります。例えば DecisionTree の場合「決定」されてしまうので、この値は0.か1.かの何れかになってしまうようです。
[^3]: 実際に確認したわけではありません。