What's new in Apache Hivemall v0.6.0 (1)

この記事は、[Arm Treasure Data advent calendar](https://adventar.org/calendars/3932) ２１日目の記事です。12月19日[^1]にApacheで正式にリリースしたHivemall v0.6.0で導入された主要な機能や変更点[^changelog]、また特徴エンジニリング機能について紹介します。

[^1]: トレジャーデータのクラウドサービスでは12月11日にリリース済みです。
[^changelog]: 詳細は[ChangeLog](https://github.com/apache/incubator-hivemall/blob/master/ChangeLog.md)も参照ください。

# XGBoostのサポート

XGBoost v0.90を正式にサポートしました。単一ノードのXGBoostと異なり、複数のXGBoostインスタンスが並列で学習を行い、予測は[バギング](http://ibisforest.org/index.php?%E3%83%90%E3%82%AE%E3%83%B3%E3%82%B0)[^bagging]によってアンサンブル学習されます。Random Forestは複数の決定木でアンサンブル学習を行いますが、各木がGradient Boosting Decision Tree（GBDT)を利用してできたモデルとなりますので、Random Forestの特徴であるバギングおよび[ブートストラップ法](https://ja.wikipedia.org/wiki/%E3%83%96%E3%83%BC%E3%83%88%E3%82%B9%E3%83%88%E3%83%A9%E3%83%83%E3%83%97%E6%B3%95)とXGBoost単体の[勾配ブースティング](https://qiita.com/woody_egg/items/232e982094cd3c80b3ee)の双方を組合せたものだと思って頂ければ良いかと思います[^2]。

[^bagging]: 回帰の場合は平均、クラス分類の場合は多数決投票となります。
[^2]: 汎化性能がより高いはずなので汎化性能が求められる場合、XGBoost単体よりも精度がでる可能性があります。並列数が1ですとxgboostと同様の予測結果です。

基本的にオリジナルのXGBoost v0.90と同様のhyperparameterを利用可能[^xgboost]で、入力とする特徴ベクトルはDenseフォーマット(`array<double>`)とLibSVM形式のSparseフォーマット(`array<string>`で各要素は`index:value`)をサポートしております[^xgboost4j]。

[^xgboost]: 詳細は[こちら](http://hivemall.apache.org/userguide/binaryclass/news20b_xgboost.html#usage-and-hyperparameters)のオプションのリストをご確認ください。サンプルとしては[二値分類](http://hivemall.apache.org/userguide/binaryclass/news20b_xgboost.html)、[他クラス分類](http://hivemall.apache.org/userguide/multiclass/news20_xgboost.html)、[回帰](http://hivemall.apache.org/userguide/regression/e2006_xgboost.html)の３つをTutorialに記載しておりますが、ランキング学習のobjective functionも利用可能です。

[^xgboost4j]: DMLCが提供しているxgboost4jがScalaへの依存性を持っているため、Scalaライブラリなしでも動作するように[forkしたもの](https://github.com/myui/build-xgboost-jvm)を利用しております。また、jniの共有ライブラリが動作されるLinux環境が限られるため、共有ライブラリをstatic linkしたり、GLIBCの特定バージョンへの依存度を極力小さくしております。

学習には次のように`train_xgboost`関数を使います。目的関数`-objective`の指定は必須です[^objective]。応答変数（label）の形式はオリジナルのXGBoostの形式を踏襲しております。

[^objective]: 主に利用されるのは、reg:squarederror, binary:logistic, binary:hinge, multi:softmax, multi:softprobあたりかと思います。

```sql
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]に負である確率値が予測されます。確率値のアンサンブル学習なので、次のように平均を利用します。

```sql
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](https://github.com/komiya-atsushi/xgboost-predictor-java/)を利用して高スループットな予測を行なっております。ただし、[xgboost-predictor-java](https://github.com/komiya-atsushi/xgboost-predictor-java/)では一部のobjectiveが[サポートされておりません](https://github.com/komiya-atsushi/xgboost-predictor-java/#supported-models-objective-functions-and-api)ので、サポートされていない目的関数をご利用の場合は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](https://github.com/komiya-atsushi/xgboost-predictor-java/)ではサポートされておりません。

Hingeロスを指定したモデルで予測するときxgboostでは要素数1の配列に1.0（正）または0.0（負）を返します。アンサンブル学習で多数決をとるにはmajority_vote関数を利用します。

```sql
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`オプションで品詞情報の出力をサポートしました。

以下のクエリで品詞情報が出力されます。

```sql
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`を用意しました。

```sql
select 
  tokenize_ja("kuromojiを使った分かち書きのテストです。", 
     "normal",  -- tokenization mode
     array("kuromoji"),  -- stopwords
     stoptags_exclude(array("名詞")) -- stoptags
  );
["分かち書き","テスト"]
```

上記の場合は、全ての名詞が出力されますが、一般名詞だけ取得したい場合`stoptags_exclude(array("名詞-一般"))`と指定ください。

# 決定木の予測パスのトレース機能

決定木の予測時に次のように条件分岐に従ってリーフの値が予測結果として出力されます。

![Decision Tree Tracing](https://issues.apache.org/jira/secure/attachment/12909571/iris_trace.png)

Scikitでは[decision_path](https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html#sklearn.tree.DecisionTreeClassifier.decision_path)関数によって予測のトレーシングが可能ですが、利用するのはこれらの記事[^trace1] [^trace2]にあるとおり、出力結果の分析にやや手間がかかります。

[^trace1]: http://waakvlam.hatenadiary.jp/entry/advent181223
[^trace2]: https://qiita.com/kotaRof/items/c478a5ae4c2f70148bf4

関数の引数は次のように確認できます。optionsにはオプション、featureNamesには特徴名の配列、classNamesにはクラス名を指定することができます。

```sql
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と同様です。

```sql
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`オプションは冗長な分岐の出力を抑制します。

```sql
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](https://www.kaggle.com/c/titanic)から取得可能なデータセットを利用して、決定木やRandomForestを利用した場合に、タイタニック号の沈没事故での生死を予測するのに重要だった分岐のトップ100件を出力するクエリです。

コメントアウトしたwhere句を部分を有効にすることで、実際に生き残った場合(actual=1)に頻出した分岐、また`last_element(path) = 1`を有効にすることで生存と予測した場合の頻出分岐が出力されます。

```sql
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., 力尽きてしまったので）、この続きは記事を分けて次回にしたいと思います。