この記事は、Arm Treasure Data advent calendar 21日目の記事です。12月19日1にApacheで正式にリリースしたHivemall v0.6.0で導入された主要な機能や変更点2、また特徴エンジニリング機能について紹介します。
XGBoostのサポート
XGBoost v0.90を正式にサポートしました。単一ノードのXGBoostと異なり、複数のXGBoostインスタンスが並列で学習を行い、予測はバギング3によってアンサンブル学習されます。Random Forestは複数の決定木でアンサンブル学習を行いますが、各木がGradient Boosting Decision Tree(GBDT)を利用してできたモデルとなりますので、Random Forestの特徴であるバギングおよびブートストラップ法とXGBoost単体の勾配ブースティングの双方を組合せたものだと思って頂ければ良いかと思います4。
基本的にオリジナルのXGBoost v0.90と同様のhyperparameterを利用可能5で、入力とする特徴ベクトルはDenseフォーマット(array<double>
)とLibSVM形式のSparseフォーマット(array<string>
で各要素はindex:value
)をサポートしております6。
学習には次のようにtrain_xgboost
関数を使います。目的関数-objective
の指定は必須です7。応答変数(label)の形式はオリジナルのXGBoostの形式を踏襲しております。
create table xgb_lr_model as
select
train_xgboost(features, label, '-objective binary:logistic -num_round 10 -num_early_stopping_rounds 3')
as (model_id, model)
from (
select features, label
from news20b_train
cluster by rand() -- shuffle data at random
) shuffled;
XGBoostを用いたPrediction
オリジナルのXGBoostでも同じですが、objective functionごとに予測時に出力されるものが異なります。目的関数にlogisticを指定した場合はprobabilityが出力されます。predicted[0]に正である確率、predicted[1]に負である確率値が予測されます。確率値のアンサンブル学習なので、次のように平均を利用します。
select
rowid,
array_avg(predicted) as predicted,
avg(predicted[0]) as prob
from (
select
xgboost_predict(rowid, features, model_id, model) as (rowid, predicted)
-- xgboost_batch_predict(rowid, features, model_id, model) as (rowid, predicted)
from
xgb_lr_model l
LEFT OUTER JOIN news20b_test r
) t
group by rowid;
xgboost_predictは @komiya_atsushi さんのxgboost-predictor-javaを利用して高スループットな予測を行なっております。ただし、xgboost-predictor-javaでは一部のobjectiveがサポートされておりませんので、サポートされていない目的関数をご利用の場合はxgboost4jを利用したxgboost_batch_predict関数をご利用ください。
なお、LEFT OUTER JOIN
はCROSS JOIN相当ですが、nested loop join時のouter tableを次のようにモデルに固定するHiveQLのハックになります。スループットが悪化する可能性がありますがCROSS JOINでも原理的には動作します。
for each model l {
for each test r {
predict
}
}
ヒンジロスを指定した場合の予測
Hingeロス(-objective binary:hinge
)を利用して作成したモデルはxgboost-predictor-javaではサポートされておりません。
Hingeロスを指定したモデルで予測するときxgboostでは要素数1の配列に1.0(正)または0.0(負)を返します。アンサンブル学習で多数決をとるにはmajority_vote関数を利用します。
select
rowid,
majority_vote(if(predicted[0]=1, 1, -1)) as predicted
from (
select
-- binary:hinge returns [1.0] or [0.0] for predicted
xgboost_batch_predict(rowid, features, model_id, model)
as (rowid, predicted)
from
xgb_hinge_model l
LEFT OUTER JOIN news20b_test r
) t
group by
rowid
tokenize_jaの改良
品詞情報(Part-of-Speech)の出力
Hivemallでは以前よりkuromojiを利用した形態素解析機能を tokenize_ja
関数で提供しておりましたが、トレジャーデータのお客様で品詞情報を利用したいとのご要望が多かったため、-pos
オプションで品詞情報の出力をサポートしました。
以下のクエリで品詞情報が出力されます。
WITH tmp as (
select
tokenize_ja('kuromojiを使った分かち書きのテストです。','-mode search -pos') as r
)
select
r.tokens,
r.pos,
r.tokens[0] as token0,
r.pos[0] as pos0
from
tmp;
tokens | pos | token0 | pos0 |
---|---|---|---|
["kuromoji","使う","分かち書き","テスト"] | ["名詞-一般","動詞-自立","名詞-一般","名詞-サ変接続"] | kuromoji | 名詞-一般 |
ストップタグの指定方法の改良
LuceneのKuromojiではstopTagsに結果として取得しない品詞を指定します。次のように多数の品詞の中から名詞以外をExclusiveルールで指定するのは大変です。
名詞, 名詞-一般, 名詞-固有名詞, 名詞-固有名詞-一般, 名詞-固有名詞-人名,
名詞-固有名詞-人名-一般, 名詞-固有名詞-人名-姓, 名詞-固有名詞-人名-名, 名詞-固有名詞-組織, 名詞-固有名詞-地域,
名詞-固有名詞-地域-一般, 名詞-固有名詞-地域-国, 名詞-代名詞, 名詞-代名詞-一般, 名詞-代名詞-縮約, 名詞-副詞可能,
名詞-サ変接続, 名詞-形容動詞語幹, 名詞-数, 名詞-非自立, 名詞-非自立-一般, 名詞-非自立-副詞可能,
名詞-非自立-助動詞語幹, 名詞-非自立-形容動詞語幹, 名詞-特殊, 名詞-特殊-助動詞語幹, 名詞-接尾, 名詞-接尾-一般,
名詞-接尾-人名, 名詞-接尾-地域, 名詞-接尾-サ変接続, 名詞-接尾-助動詞語幹, 名詞-接尾-形容動詞語幹, 名詞-接尾-副詞可能,
名詞-接尾-助数詞, 名詞-接尾-特殊, 名詞-接続詞的, 名詞-動詞非自立的, 名詞-引用文字列, 名詞-ナイ形容詞語幹, 接頭詞,
接頭詞-名詞接続, 接頭詞-動詞接続, 接頭詞-形容詞接続, 接頭詞-数接, 動詞, 動詞-自立, 動詞-非自立, 動詞-接尾,
形容詞, 形容詞-自立, 形容詞-非自立, 形容詞-接尾, 副詞, 副詞-一般, 副詞-助詞類接続, 連体詞, 接続詞, 助詞,
助詞-格助詞, 助詞-格助詞-一般, 助詞-格助詞-引用, 助詞-格助詞-連語, 助詞-接続助詞, 助詞-係助詞, 助詞-副助詞,
助詞-間投助詞, 助詞-並立助詞, 助詞-終助詞, 助詞-副助詞/並立助詞/終助詞, 助詞-連体化, 助詞-副詞化, 助詞-特殊,
助動詞, 感動詞, 記号, 記号-一般, 記号-読点, 記号-句点, 記号-空白, 記号-括弧開, 記号-括弧閉,
記号-アルファベット, その他, その他-間投, フィラー, 非言語音, 語断片, 未知語
そこで、特定の品詞だけ取得するためのInclusiveルール用の関数stoptags_exclude
を用意しました。
select
tokenize_ja("kuromojiを使った分かち書きのテストです。",
"normal", -- tokenization mode
array("kuromoji"), -- stopwords
stoptags_exclude(array("名詞")) -- stoptags
);
["分かち書き","テスト"]
上記の場合は、全ての名詞が出力されますが、一般名詞だけ取得したい場合stoptags_exclude(array("名詞-一般"))
と指定ください。
決定木の予測パスのトレース機能
決定木の予測時に次のように条件分岐に従ってリーフの値が予測結果として出力されます。
Scikitではdecision_path関数によって予測のトレーシングが可能ですが、利用するのはこれらの記事8 9にあるとおり、出力結果の分析にやや手間がかかります。
関数の引数は次のように確認できます。optionsにはオプション、featureNamesには特徴名の配列、classNamesにはクラス名を指定することができます。
select decision_path();
decision_path takes 3 ~ 6 arguments
usage: decision_path(string modelId, string model, array<double|string>
features [, const string options] [, optional array<string>
featureNames=null, optional array<string> classNames=null]) -
Returns a decision path for each prediction in array<string> [-c]
[-no_leaf] [-no_sumarize] [-no_verbose]
-c,--classification Predict as classification
[default: not enabled]
-no_leaf,--disable_leaf_output Show leaf value [default: not
enabled]
-no_sumarize,--disable_summarization Do not summarize decision paths
-no_verbose,--disable_verbose_output Disable verbose output [default:
verbose]
基本的な利用方法としては決定木の予測に用いるtree_predictと同様です。
SELECT
t.passengerid,
tree_predict(m.model_id, m.model, t.features, "-classification") as predicted,
decision_path(m.model_id, m.model, t.features, '-classification') as path
FROM
model_rf m
LEFT OUTER JOIN
test_rf t;
passengerid | predicted | path |
---|---|---|
892 | {"value":0,"posteriori":[0.961038961038961,0.03896103896103896]} | ["2 [0.0] = 0.0","0 [3.0] = 3.0","1 [696.0] != 107.0","7 [7.8292] <= 7.9104","1 [696.0] != 828.0","1 [696.0] != 391.0","0 [0.961038961038961, 0.03896103896103896]"] |
1309 | {"value":0,"posteriori":[0.9466666666666667,0.05333333333333334]} | ["2 [0.0] = 0.0","0 [3.0] = 3.0","1 [1306.0] != 107.0","7 [22.3583] > 12.675","1 [1306.0] != 429.0","1 [1306.0] != 65.0","6 [117.0] != 481.0","6 [117.0] != 251.0","0 [0.9466666666666667, 0.05333333333333334]"] |
各分岐の特徴の名前を指定する場合は次のようにします。なお、-no_verbose
オプションは冗長な分岐の出力を抑制します。
SELECT
t.passengerid,
tree_predict(m.model_id, m.model, t.features, "-classification") as predicted,
decision_path(m.model_id, m.model, t.features, '-classification -no_verbose',
array('pclass','name','sex','age','sibsp','parch','ticket','fare','cabin','embarked') -- feature names
) as path
FROM
model_rf_07 m
LEFT OUTER JOIN -- CROSS JOIN
test_rf_03 t
passengerid | predicted | path |
---|---|---|
1 | {"value":0,"posteriori":[0.9693251533742331,0.03067484662576687]} | ["sibsp <= 12.0","fare <= 9.8396","name != 107.0","sex != 1.0","name != 804.0","name != 286.0","0"] |
888 | {"value":1,"posteriori":[0.0,1.0]} | ["sibsp <= 12.0","fare > 9.8396","ticket != 314.0","pclass = 1.0","age <= 49.5","sex != 0.0","1"] |
重要な条件を分析する
次の例は、Kaggle titanicから取得可能なデータセットを利用して、決定木やRandomForestを利用した場合に、タイタニック号の沈没事故での生死を予測するのに重要だった分岐のトップ100件を出力するクエリです。
コメントアウトしたwhere句を部分を有効にすることで、実際に生き残った場合(actual=1)に頻出した分岐、またlast_element(path) = 1
を有効にすることで生存と予測した場合の頻出分岐が出力されます。
WITH tmp as (
SELECT
t.survived as actual,
decision_path(m.model_id, m.model, t.features, '-classification -no_verbose', array('pclass','name','sex','age','sibsp','parch','ticket','fare','cabin','embarked')) as path
FROM
model_rf_07 m
LEFT OUTER JOIN -- CROSS JOIN
test_rf_03 t
)
select
r.branch,
count(1) as cnt
from
tmp l
LATERAL VIEW explode(array_slice(path, 0, -1)) r as branch
-- where
-- actual = 1 and -- actual is survived
-- last_element(path) = 1 -- predicted is survived
group by
r.branch
order by
cnt desc
limit 100;
r.branch | cnt |
---|---|
sex != 0.0 | 29786 |
pclass != 3.0 | 18520 |
pclass = 3.0 | 7444 |
sex = 0.0 | 6494 |
embarked != 1.0 | 6175 |
ticket != 22.0 | 5560 |
... |
この例では男性か女性かどうか、搭乗クラスが3.0であったかどうかが生死を分けた条件として重要と言うことがわかります。このようにdecision_path関数は機械学習の予測結果の説明責任が求められる場合や過学習の有無の分析に有効な機能となります。
おわりに
長くなってしまったので(i.e., 力尽きてしまったので)、この続きは記事を分けて次回にしたいと思います。
-
トレジャーデータのクラウドサービスでは12月11日にリリース済みです。 ↩
-
回帰の場合は平均、クラス分類の場合は多数決投票となります。 ↩
-
汎化性能がより高いはずなので汎化性能が求められる場合、XGBoost単体よりも精度がでる可能性があります。並列数が1ですとxgboostと同様の予測結果です。 ↩
-
詳細はこちらのオプションのリストをご確認ください。サンプルとしては二値分類、他クラス分類、回帰の3つをTutorialに記載しておりますが、ランキング学習のobjective functionも利用可能です。 ↩
-
DMLCが提供しているxgboost4jがScalaへの依存性を持っているため、Scalaライブラリなしでも動作するようにforkしたものを利用しております。また、jniの共有ライブラリが動作されるLinux環境が限られるため、共有ライブラリをstatic linkしたり、GLIBCの特定バージョンへの依存度を極力小さくしております。 ↩
-
主に利用されるのは、reg:squarederror, binary:logistic, binary:hinge, multi:softmax, multi:softprobあたりかと思います。 ↩