SQLだけで機械学習するぞ-Hivemall入門-

こんにちは。onunuです。
IESHILでエンジニアをしています。主にSQLを描くのがお仕事です。
以前はマッハバイト(旧ジョブセンス)で開発していたのですが、[SQLで座標平面を描く能力](https://qiita.com/onunu/items/66b3c92f8fefb05c94ae)を評価してもらい、座標上の2点間の距離計算とかいっぱいする不動産のサービスに異動になりました。地球は球なので大変です。

さてさて、本記事はLivesense -自- Advent Calendar 2017のために書かれています。
テーマは ***自*** 、ということなので、自分の仕事領域であるSQLに関して書こうと思います。

# Hivemallとは
![hivemall](http://hivemall.incubator.apache.org/images/hivemall_bg.png "Hivemall logo")
[Hivemall](http://hivemall.incubator.apache.org/)はHadoop/Spark上でSQLクエリを用いて機械学習を行うためのライブラリです。
実際にはHadoop/Spark用のデータウェアハウスであるHive上で動作するUDF(User Defined Function)として実装されています。
![hivemall layers](http://hivemall.incubator.apache.org/userguide/resources/images/techstack.png "hivemall layers")
ユーザーはHive上のデータに対してSQLのみで機械学習を行うことができます。
Hivemallにはロジスティック回帰等の回帰分析やランダムフォレストによるクラス分類など、さまざまな機械学習アルゴリズムが梱包されていて、本格的な機械学習をすぐに導入できるのが強みです。


# Getting Start!
## データセットの準備
ではさっそく始めてみましょう。
Hadoop, Hive, Hivemallのセットアップとかは省きます(TresureDataでやってしまったので・・・)。
では、[Kaggle](https://www.kaggle.com/)のチュートリアルとして名高い[titanicのデータセット](https://www.kaggle.com/c/titanic)を使ってランダムフォレストによる2値分類を行なってみましょう。
ざっくり説明しますと、titanic号の乗客の情報から生き残ったかどうかを学習させ、予測しましょう、ということですね。Kaggleで提供されるtestデータには正解が記載されていないので、trainデータの一部をtestデータとして学習用のデータからは省くこととします。

元のデータは以下のような形式です。

| passengerid | survived | pclass | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked |
|--:|--:|--:|:--|:--|--:|--:|--:|:--|--:|--:|:--|
| 1 | 0 | 3 | Braund, Mr. Owen Harris | male | 22 | 1 | 0 | A/5 21171 | 7.25 | 0 | S |
| 3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26 | 0 | 0 | STON/O2. 3101282 | 7.925 | 0 | S |

## 説明変数を数値に変更する
機械学習する上では文字列は扱えないので数値に変換します。

```sql
SELECT
  QUANTIFY(true,
    passengerid, survived, pclass, name, sex, age, sibsp, parch, ticket, fare, cabin, embarked
  ) AS (passengerid, survived, pclass, name, sex, age, sibsp, parch, ticket, fare, cabin, embarked)
FROM
  titanic
```

`QUANTIFY()`は文字列情報を数値に変換してくれる関数です。これを使うととても早いですね。結果は以下のようになりました。

| passengerid | survived | pclass | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked |
|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| 358 | 0 | 2 | 0 | 0 | 38 | 0 | 0 | 0 | 13 | 0 | 0 |
| 359 | 1 | 3 | 1 | 0 | 0 | 0 | 0 | 1 | 7.8792 | 0 | 1 |
| 360 | 1 | 3 | 2 | 0 | 0 | 0 | 0 | 2 | 7.8792 | 0 | 1 |


## 説明変数を配列にまとめる
ここで学習用データとtest用データを分けておきます。
`passengerid` が800未満を学習用、800以上をtest用として分けることにしました。
説明変数を配列にまとめるのはこのあとで使うHivemallの関数が引数を配列で取るための配慮です。

```sql
WITH quantified AS (
  SELECT
    QUANTIFY(true,
      passengerid, survived, pclass, name, sex, age, sibsp, parch, ticket, fare, cabin, embarked
    ) AS (passengerid, survived, pclass, name, sex, age, sibsp, parch, ticket, fare, cabin, embarked)
  FROM
    titanic
), train AS (
  SELECT
    passengerid,
    ARRAY(pclass, name, sex, age, sibsp, parch, ticket, fare, cabin, embarked) AS features,
    survived
  FROM
    quantified
  WHERE
    passengerid < 800
), test AS (
  SELECT
    passengerid,
    ARRAY(pclass, name, sex, age, sibsp, parch, ticket, fare, cabin, embarked) AS features,
    survived AS survived_correct
  FROM
    quantified
  WHERE
    passengerid >= 800
)
```

結果は以下の通りです。

| passengerid | features | survived |
|--:|:--|--:|
| 1 | [3.0, 0.0, 0.0, 22.0, 1.0, 0.0, 0.0, 7.25, 0.0, 0.0] | 0 |
| 3 | [3.0, 1.0, 1.0, 26.0, 0.0, 0.0, 1.0, 7.925, 0.0, 0.0] | 1 |

SQL長くなっちゃうのでここからは適切に分けていきますね。

## 学習する
いよいよ学習です。`TRAIN_RANDOMFOREST_CLASSIFIER()`を使ってクラス分類を行います。
`TRAIN_RANDOMFOREST_CLASSIFIER()` 関数では`-trees`で決定木の数、`-attrs`では各説明変数が[Q:量的]か[C:質的]かどうかを明示する必要があります。

```sql
-- table:model
SELECT
  TRAIN_RANDOMFOREST_CLASSIFIER(features, survived, "-trees 500 -attrs C,C,C,Q,Q,Q,C,Q,C,C")
FROM
  train
```

結果は以下のようになりました。これだけだと何がなんだかよくわかりませんね。このmodelを使って予測を次のステップで行います。

| model_id | model_type | pred_model | var_importance | oob_errors | oob_tests |
|:--|--:|:--|:--|--:|--:|
| 7a3f3ae9-3862-47ca-b9f7-cfc37c4e37da | -3 | 省略 | [0.09576779301680655, 1.2733288209825033, 0.3358146372035047, 0.6587979973279458, 0.03684900957628229, 0.31999999999999984, 0.8978685276906975, 1.9327439756054603, 0.0, 0.04884845185461929] | 0 | 0 |
| 7bccf92b-fb0c-4fa8-92d7-49390a5dfbb5 | -3 | 省略 | [0.14843401059181255, 1.1773166948759979, 0.15179900993895604, 0.14748706533080252, 0.06508875739644956, 0.08341309362133248, 0.5143381765613078, 0.8223971522757472, 0.0, 0.7444881277704621] | 0 | 0 |

ちなみにここでvar_importanceを集計すれば、どの説明変数が重要度の高い説明変数なのか計算することができます。


## 予測する
上記で作成したmodelを用いてtestデータを予測してみましょう。

```sql
-- table:predicted_raw
SELECT
  t.passengerid,
  TREE_PREDICT(p.model_id, p.model_type, p.pred_model, t.features, true) AS predicted
FROM
  model AS p
  LEFT JOIN test AS t
```

| passengerid | predicted |
|:--|:--|
|800|0|
|801|0|


model x testデータの数だけレコードが作成されました。
この時点ではまだ予測の途中です。
続いてこの生予測データを予測として成立するよう集計します。

## 集計する

```sql
-- table: predicted_reduced
SELECT
  passengerid,
  RF_ENSEMBLE(predicted) AS predicted
FROM
  predicted_raw
GROUP BY
  passengerid
```

| passengerid | predicted |
|--:|--:|
|800|[0,0.676,[0.676,0.324]]|
|801|[0,0.994,[0.994,0.006]]|

`RF_ENSEMBLE()`は学習結果をまとめるための関数です。
先ほどの予測結果はmodelの数だけレコードがある状態なので集計します。
ENSEMBLEってフランス語なんですね。


## 集計結果を見やすくする

```sql
-- table: predicted
SELECT
  passengerid,
  predicted.label,
  predicted.probability,
  predicted.probabilities
FROM
  predicted_reduced
```

| passengerid | label | probability | probabilities |
|--:|--:|--:|--:|
|800|0|0.982|[0.982,0.018]|
|801|1|0.986|[0.014,0.986]|

`label`は分類結果で、`probability`は分類結果の信頼性ですね。
`probabilities`は分類結果以外のものも含めた確率の配列です。合計すると1になります。

## testデータの成否をたしかめる
先ほど作成していたtestデータを再度結合し、成否を確かめます。
最後のクエリなので、全てご覧ください。

```sql
WITH quantified AS (
  SELECT
    QUANTIFY(true,
      passengerid, survived, pclass, name, sex, age, sibsp, parch, ticket, fare, cabin, embarked
    ) AS (passengerid, survived, pclass, name, sex, age, sibsp, parch, ticket, fare, cabin, embarked)
  FROM
    titanic
), train AS (
  SELECT
    passengerid,
    ARRAY(pclass, name, sex, age, sibsp, parch, ticket, fare, cabin, embarked) AS features,
    survived
  FROM
    quantified
  WHERE
    passengerid < 800
), test AS (
  SELECT
    passengerid,
    ARRAY(pclass, name, sex, age, sibsp, parch, ticket, fare, cabin, embarked) AS features,
    survived AS survived_correct
  FROM
    quantified
  WHERE
    passengerid >= 800
), model AS (
  SELECT
    TRAIN_RANDOMFOREST_CLASSIFIER(features, survived, "-trees 500 -attrs C,C,C,Q,Q,Q,C,Q,C,C")
  FROM
    train
), predicted_raw AS (
  SELECT
    t.passengerid,
    TREE_PREDICT(p.model_id, p.model_type, p.pred_model, t.features, true) AS predicted
  FROM
    model AS p
    LEFT JOIN test AS t
), predicted_reduced AS (
  SELECT
    passengerid,
    RF_ENSEMBLE(predicted) AS predicted
  FROM
    predicted_raw
  GROUP BY
    passengerid
), predicted AS (
  SELECT
    passengerid,
    predicted.label,
    predicted.probability,
    predicted.probabilities
  FROM
    predicted_reduced
)
SELECT
  t.passengerid,
  t.survived_correct,
  p.label AS predict,
  p.probability,
  p.probabilities
FROM
  predicted AS p
  INNER JOIN test AS t ON p.passengerid = t.passengerid
```

| passengerid | survived_correct | predict | probability | probabilities |
|--:|--:|--:|--:|:--|
| 800 | 0 | 0 | 0.69 | [0.69, 0.31] |
| 801 | 0 | 0 | 0.996 | [0.996, 0.004] |
| 802 | 1 | 1 | 0.982 | [0.018, 0.982] |
| 805 | 1 | 0 | 0.992 | [0.992, 0.008] |
| 806 | 0 | 0 | 0.942 | [0.942, 0.058] |
| 808 | 0 | 1 | 0.686 | [0.314, 0.686] |

うーん、あんまりあってない!!(全部載せると大変なので一部抜粋)
自信満々に間違えてるやつとかいるので、要チューニングです。
まあ説明変数やtreeの数を精査していないので、仕方ないかもしれません。
機械学習のチューニングはやはりそれ相応の技術が必要ですね。

# まとめ
Hivemallを用いた機械学習のクエリを紹介しました。
最終的にSQL60行ちょっとで機械学習を実装できているので、そこそこ簡単、と言えるのではないでしょうか？

実際のプロダクトで使いどころとしては、色々なデータがある中で機械学習をプロトタイプで導入する時などでしょうか？
TresureDataで利用できるHiveではデフォルトでHivemallが利用できる環境になっています。
もしTresureDataを利用していれば、上記のようにとりあえず簡単に機械学習をおこなって、ビジネスチャンスを伺うのもいいかもしれません。

それではぼくはここらへんで失礼しましょう。今年もみなさんお疲れ様でした！

# 参考
- [Apache Hivemall](http://hivemall.incubator.apache.org/)
- [Kaggle Titanic tutorial](http://hivemall.incubator.apache.org/userguide/binaryclass/titanic_rf.html)
- [Hivemall Getting Started](https://datumstudio.jp/blog/hivemall-getting-started)