こんにちは。俺やで。
今回も前回から間が空いてしましたが、ビッグデータに対応したHiveで使える機械学習ライブラリ、 Hivemallの使い方について、書かせていただければと思います。
なお今回はQiitaのTreasure Data / Advent Calender 2015の12/3日分としても書かせていただいております。 Qiitaにリンク貼らせてもらうのも初めてで、ちょっと緊張しつつ書いてます。
今回は第3回です。第1回、第2回は以下を参照ください。 【第1回】【超入門】Hivemallで機械学習 〜Treasure Dataでロジスティック回帰編〜 【第2回】HivemallでMinhash!〜似てる記事を探し出そう。〜
今回は、Hivemall v0.4から使えるようになったRandomForestというモデルについて書こうと思います。
細かく書くと大変なことになるので、RandomForestとは何か、をエッセンスだけお伝えし、その後Hivemallの書き方についてご紹介します。 なお毎度のことながらTreasureDataでHivemallを試させていただいております。 TreasureData様、いつもお世話になります。 それではよろしくお願いします。
■1. RandomForestとは?
まずは決定木から
RandomForestは決定木のひとつです。ですから、まずは決定木の説明から。 まずはClassicな決定木(C5.0とかCARTとかCHAIDとか、1980年頃からある決定木の手法。RandomForestが提案されたのは2001年。)のアウトプットから見ていただいた方がわかりやすいと思います。「決定木」という名前そのままですが。
30歳未満か以上かで分けると、「買った人」が比較的多いクラスタと「買ってない人」が比較的多いクラスタに分かれました。 この条件が「買った人」の特徴をある程度は捉えている、ということになりそうですね。
この図だけ見てもピンと来ないかもしれませんが、もし「”商品を買った人”と”買ってない人”の違いを明らかにしろ」と言われたら、ぱっと思いつくだけでも、年齢 / 性別 / 居住地 / 家族構成 / 年収 などなどいろいろな切り口が無数にあります。 そのうえ、年齢や年収など数字で表される変数(量的変数)になると、しきい値も決めなければならず、その候補も無限です。
では、どういう条件を設定すれば、"買った人"と"買ってない人"が最も明確に分かれるのか。 ここで登場するのが機械学習であり、その一手法である決定木でありんす!
どうやって「条件」を取り出すか
「条件」の取り出し方について、ざっくり説明します。
ジニ係数という言葉を耳にしたことのある方は多いのではと思いますが、ジニ係数とは「所得格差」を定量的に表すのによく使われる数字です。 ジニ係数が大きければ所得格差が大きく、ジニ係数が小さければ所得格差は小さい、ということになります。
「格差」を定量化できれば、あとは簡単です。「格差」を表す数字が最も小さくなるような条件を探せばいいだけです。 例えば下の図を見てください。 所得が10万円の2人と100万円の2人がいます。このコミュニティは所得格差が大きいですね。 しかしこの4人を30歳未満か以上かで分けると、所得格差のない2つのコミュニティに分けることができました。
決定木では、こうやって「格差」がなるべく小さくなるような条件を取り出すわけです。
格差を表す情報量には、ジニ係数以外にも、エントロピーやカイ二乗統計量等があります。 どんな指標を使うかによっても決定木の精度は変わってきます。
RandomForestは木が森になっただけです。
ちょっと話が逸れ(るように見え)ますが、アンサンブルという機械学習の手法があります。
先ほどの例を使うと、所得が10万円か、100万円かを分類するモデルの作り方は、無数にあります。 Aさんは年齢で分けるかもしれませんし、Bさんは性別で分けるかもしれませんし、Cさんは仕事の種類で分けるかもしれません。 どんな変数を使うかがデータサイエンティストの腕の試しどころだったりするわけですが、 Aさん・Bさん・Cさんのモデルを全部組み合わせて使おう! というのがアンサンブルです。
3つのモデルのうち、2つが「この人の所得は10万円だ」、1つが「100万円だ」と分類したのであれば、多数決で「10万円」を答えに選びます。 直感的には多数決をとってしまった方が良さそうですよね。 実際、これによって精度が上がることがしばしばあります。これがアンサンブルです。
RandomForestに話を戻します。 RandomForestは、 ひとつのデータセット(学習データ)からいろんなデータセットを作って、それぞれで決定木を作って、アンサンブルしちゃおう! という、機械学習の手法です。
「いろんなデータセット」の作り方ですが、説明変数をランダムサンプリングしてモデルを作る試行をN回繰り返せばOKです。
言ってしまえば、木が森になっただけです。
■2. RandomForestで毒キノコ推定モデルを作る
それでは実際にHivemallでRandomForestを作ってみたいと思います。
使うデータはmushroom
今回使うデータはmushroomです! https://archive.ics.uci.edu/ml/datasets/Mushroom
8,124個のキノコについて、そのキノコが有毒かどうかと、そのキノコの112個の特徴について記録されているデータです。 8,124個のうち、4,208個が毒のないキノコで、残りの3,916個が有毒となっています。 112個の特徴には、例えば笠の形とか茎の色とか生息の仕方等があります。 この112個の特徴によって、そのキノコが毒キノコかどうかを推定するモデルを作ってみます!
実際のデータを見てみましょう。
1.無毒 or 2.有毒 (class) | 特徴(features) |
---|---|
1 | ["6:1","8:1","15:1","21:1","29:1","33:1","34:1","37:1","42:1"...] |
2 | ["6:1","8:1","20:1","21:1","23:1","33:1","34:1","36:1","42:1"...] |
2 | ["2:1","8:1","19:1","21:1","27:1","33:1","34:1","36:1","44:1"...] |
1行目のレコードで言うと、このキノコは無毒で、6番目の特徴(cap-shapeがsunken)と8番目の特徴(cap-surfaceがgrooves)と15番目の特徴(cap-colorがgreen)と…を持っている、ということになります。
Hiveで前処理
さきほどのデータのテーブル名をmushroomとして処理をしていきます。 [sql] select class - 1 as class ,to_dense_features(split(features,','),112) as features from mushroom [/sql]
0.無毒 or 1.有毒 (class) | 特徴(features) |
---|---|
0 | [null,null,null,null,null,null,1,null,1,null,null,null,null,null,null,1,...] |
1 | [null,null,null,null,null,null,1,null,1,null,null,null,null,null,null...] |
1 | [null,null,1,null,null,null,null,null,1,null,null,null,null,null,null...] |
class-1としていますが、HivemallのRandomForestは教師のラベルを0から始まる連番にしなければならない仕様になっているためです。 元のデータは1(食べられる)と2(毒あり)になっていたので、1を引いてあげています。
nullばっかりで非常に見づらく恐縮ですが、1行目は先ほどと同じく(配列なので0から数えて)6,8,15番目の要素に「1」が立っています。 to_dense_featuresという関数がkey:valueが要素となっている配列をkey番目の要素にvalueが入っている配列に直してくれます。 2つ目の引数はkeyの最大値を与えてあげます。 to_dense_features、めちゃ便利です。 元データに「0番目の特徴」は存在しないので取り除けばよいですが、手間だったため残してしまっています。 (すみません。)
これがHivemallでRadnomForestを作るために必要なデータの形式です!
ついにRandomForest!
やっとRandomForestまでたどり着きました。 以下のクエリでRandomForestを作ることができます!
[sql] insert into table model select train_randomforest_classifier( to_dense_features(split(features,','),112) ,class - 1 as class ,"-trees 30 -vars 11" --30個モデル(木)を作る。説明変数のサンプルサイズを11に指定。 ) from mushroom [/sql]
オプションに指定している、treesは作成するモデルの数( = 森を構成する木の数)、varsはサンプリングする説明変数の数を表しています。 ここではそれぞれ30と11を指定していますが、前者はテキトーで、後者はR言語のRandomForestのデフォルト値である、学習データの全説明変数の二乗根(√112 = 10.58...)としています。
結果は中身見てもごちゃごちゃしてて何がなんだかなので、 出力のカラムだけの記載とします。
カラム名 | 説明 |
---|---|
pred_model | 各モデル(木)の詳細 |
var_importance | 各モデル(木)の説明変数の重要度 |
oob_errors | 各モデル(木)のOOBエラー数 |
oob_tests | 各モデル(木)のOOBレコード数 |
※OOBについて詳しく書こうと思いましたが、心折れたのでまたの機会に。過学習してないかどうかを確かめるための学習データから外したサンプルのことをOOB(out-of-bag)と言います。
あとは推定(モデルのあてはめ)です。 学習データに対するスコアリングになりますが、こんな感じで書きます。 with句部分はモデリング時と同じです。クエリ的にはイケてないですが、見やすいように。 [sql] with data as ( select row_number() over() as id ,to_dense_features(split(features,','),112) as features from mushroom )
SELECT id ,rf_ensemble(predicted) as predicted --アンサンブル! FROM ( SELECT id ,tree_predict(p.pred_model, features, true) as predicted --各モデル(木)による推定 FROM data d CROSS JOIN model p --全データをすべてのモデル(木)とぶつける ) t group by id [/sql]
この結果はこんな感じで返ってきます。
id | predicted |
---|---|
1 | [1,1,[0,1]] |
2 | [0,1,[1,0]] |
10 | [0,0.93,[0.93,0.07]] |
predictは3つの要素から構成されていますが、 array[0]が、推定されたclass、 array[1]が、classがarray[0]である確率、 array[2][N]が、classがNである確率を表しています。 つまりarray[0]とarray[1]はarray[2]の中から最大のものを取り出してるだけですね。
もっと言えば、id=1は100%の確率で毒キノコ(class=1)、id=10は93%の確率で食用キノコ(class=0)ですよ!ということです。
最後にモデルの評価ですが、これもクエリ一発で出せます。 [sql] select array_sum(var_importance) as var_importance, sum(oob_errors) / sum(oob_tests) as oob_err_rate from model [/sql]
1レコード返ってくるだけです!
カラム | 値 |
---|---|
var_importance | [0,0.11797740644573695,0.15456155959612083,0.032269....] |
oob_err_rate | 0.011396011396011397 |
var_importanceには各説明変数の重要度が、oob_err_rateにはこの毒キノコ推定モデルの精度が返されています。 error_rateが1.1%なので、逆に言えば98.9%が正解というモデルができてるんですね。 var_importanceは値の大きい順に見れば、このモデルへの寄与が大きい変数順になります!
以上がHivemallでのRandomForestの作り方になります。 これだけ精度高く毒キノコであることが推定できれば、遭難してしまっても安心ですね!
データさえあればすぐにでも試せます。お試しあれ。 Hivemall万歳!
■3. 追記
バギングも大事
RandomForestとは何かを説明してきましたが、本当はバギングの説明をしなければなりません。 記事中に出てきたOOBもバギングの文脈で出てくる単語です。 ここハショってすみません。いつか書きます。いつか。
そして、申し遅れました。私ビッグデータ解析部吉村と申します。
またの機会に!