Hivemall入門 | Hadoop Advent Calendar 2016 #10

hadoop-logo

こんにちは、小澤です。 この記事はHadoop Advent Calendar 10日目のものとなります。

前回はHive2.0からの新機能であるLLAPの紹介をさせていだきました。
今回はApache Incubator入りしたHivemallについて書かせていただきます。

Hivemallとは

もともとはHiveで機械学習を行うためのライブラリです 現在では、Hive以外にもPigやSparkにも対応しており、また、機械学習の学習と予測のみでなく、関連する処理への対応も行われていますが、今回は機械学習を行う部分に焦点を当てて紹介したいと思います。

Hivemallの特徴は分散処理で機械学習を実現していることです。 その実現方法として、各ノードでの学習を行い、全体でMixすることです。 機械学習では主に、入出力として使うデータ、学習対象であるウエイト、外部から与えるパラメータの3つが登場しますが、Mixの処理では、各ノードで学習したウエイトの平均などを全体でとる処理となります。

hivemall

Hivemallの導入

HivemallはHiveのUDF(User Defined Function)として定義されています。そのため、導入はUDFを読み込むだけです。 なお、HiveにはUDF, UDAF, UDTFといったものがありますが今回はこれらをまとめてUDFと呼んでいることにします。

必要なjarファイルと関数定義のスクリプトを取得します。GithubのReleasesからhivemall-core-<バージョン番号>-with-dependencies.jarとdefine-all.hiveをダウンロードしてください。 ダウンロードができたらhiveから読み込むだけです。

add jar <jarファイルのフルパス>;
source <define-all.hiveのフルパス>;

Hivemallを使って機械学習を行う

これで使う準備が整いました。 また、気になる方はdefine-all.hiveを見ると、HiveでのUDF定義の方法が分かるかと思います。

今回は公式Githubのwikiにあるirisデータの分類の流れを追っていきたいと思います。 ただし、irisのデータはすでにテーブルとして定義されているものとして、最初のカラム定義変換などもHive内で行ってしまうことにします。 なので、テーブルはすでに

hive> select * from iris limit 5;
OK
5.1	3.5	1.4	0.2	Iris-setosa
4.9	3.0	1.4	0.2	Iris-setosa
4.7	3.2	1.3	0.2	Iris-setosa
4.6	3.1	1.5	0.2	Iris-setosa
5.0	3.6	1.4	0.2	Iris-setosa

のような形式でHive内に存在している前提とします。

また、wikiには記載されていない、細かい部分の説明や機械学習の知識がない方のためになぜそのプロセスが必要なのか、などを説明するためにいくつかの追加のプロセスも挟んでいきます。

Feature Scaling

まず最初に、特徴量のスケールの調整をします。

これはどういうことかというと、機械学習では様々なデータを並べて一つのデータの特徴とします。 今回のirisではsepalのlengthとwidth, petalのlengthとwidthがそれに該当します。また、この特徴の数は場合によって数百や数千といった量がある場合もあります。 この特徴はどのようなものを利用するかで取りうる値の範囲が変わってきます。今回は機械学習そのものの説明ではないので、詳細は割愛しますが、全ての特徴の取りうる値の範囲が統一されていた方が都合がいい場合が多くあります。 そこで特徴の値が0から1の範囲に収まるようにするなどの変換をするといったことが行われます。

変換のイメージとしては、例えば0から100の範囲の値をとるようなもので実際に観測された値が30なら0.3で50なら0.5、 取りうる範囲が0から1000であれば観測された値が300で0.3で500で0.5となるようなイメージです。 その処理を行うのがこのFeature Scalingでの作業となります。

hivemall2

まずは各特徴の取りうる範囲を調べるため、最大値・最小値を取得します。

select
  min(sepal_length), max(sepal_length),
  min(sepal_width), max(sepal_width),
  min(petal_length), max(petal_length),
  min(petal_width), max(petal_width)
from
  iris
;
...
...
OK
4.3	7.9	2.0	4.4	1.0	6.9	0.1	2.5

実際に最大値・最長値を求めてみると、特徴によってスケールに違いがあることがわかりますね。 次にこの値に基づいて特徴量のスケールを変換します。

select
  rescale(sepal_length, 4.3, 7.9),
  rescale(sepal_width, 2.0, 4.4),
  rescale(petal_length, 1.0, 6.9),
  rescale(petal_width, 0.1, 2.5)
  species
from 
  iris
limit 3
;
OK
0.22222222	0.625	0.06779661	0.041666668
0.16666667	0.41666666	0.06779661	0.041666668
0.11111111	0.5	0.050847456	0.041666668

これを踏まえて、スケーリングしつつ、Hivemallで求められるデータ構造への変換をします。

-- iris_scaledというテーブル作成してselectの結果を格納する
create table iris_scaled as 
select
  -- row_number関数をpartitionを指定せずに使うことで全体での連番を作成
  -- ただし、これはデータの規模が大きくなると重い処理になるので実用的ではない
  row_number() over() as rowid,
  species as label,
  -- 詳細はこのあと
  add_bias(array(
    concat("1:", rescale(sepal_length, 4.3, 7.9)),
    concat("2:", rescale(sepal_width, 2.0, 4.4)),
    concat("3:", rescale(petal_length, 1.0, 6.9)),
    concat("4:", rescale(petal_width, 0.1, 2.5))
  )) as features
from 
  iris
;

select * from iris_scaled limit 5;
OK
1	Iris-virginica	["1:0.44444445","2:0.41666666","3:0.69491524","4:0.7083333","0:1.0"]
2	Iris-virginica	["1:0.5277778","2:0.5833333","3:0.7457627","4:0.9166667","0:1.0"]
3	Iris-virginica	["1:0.6111111","2:0.41666666","3:0.7118644","4:0.7916667","0:1.0"]
4	Iris-virginica	["1:0.5555556","2:0.20833333","3:0.6779661","4:0.75","0:1.0"]
5	Iris-virginica	["1:0.6666667","2:0.41666666","3:0.7118644","4:0.9166667","0:1.0"]

SQL文の説明はある程度コメントに記載しておきましたが、add_bias( ... については細かく説明します。 まず一番内側のconcatについてですが、Hivemallでは、:のような形式のデータが求められます。 そこでIDと特徴をconcatでつないでその形式に変換しています。
次にそれらを配列の型で保持しておく必要があるのでarrayで一つの配列に変換しています。

なぜ特徴にIDを振る必要があるのかというと、機械学習では大量のfeatureがあるが、そのうちのほとんどは0ということがよくあります。
例えばECサイトでユーザごとの各商品の評価値を保持しているとしすると、サイト全体の商品数に対してユーザが購入や評価をする商品はほんの一部です。このような時に評価がない項目には0が入るわけです。
ユーザ数も商品数も非常に多い場合、ほとんどが0のデータを保持しておくのはもったいないので、0以外の数値があるもののみ保持しておけると便利です。0の部分は飛ばしてもいいという書き方を可能にするために、どの特徴の値が実際に入っているのかを示す値がひつようになるためIDを振る形式になっています。

最後のadd_biasという項目ですが、これは作成したテーブルの末尾にある"0:1.0"というのを追加しています。
これは機械学習でのバイアス項というものを追加する処理になっています。 詳細は割愛しますが、機械学習は、y = ax + bのような数式であわらされ、最後のbを計算するために1つ追加してると思ってもらえるといいかと思います。

Create training/test data

次に学習データとテストデータに分けます。これはなぜ必要なのでしょうか?

機械学習では学習時に与えられるデータの傾向を予想します。そのため、学習時には出てこなかった傾向があると正しい結果を出せなくなる可能性があります。
機械学習の目的の一つとしては、答えのわかっている過去のデータから状況を学習して未知のデータの答えを予測するという問題を解くことにあります。 たとえばメールのスパム分類などを考えた場合に、ユーザ自らがスパムに分類したメールの内容をもとに新たに受信したメールがスパムか否かを判定するような用途で使われます。
そこで答えの分かっているデータをあらかじめ分割して、一部を学習時に使わず答え合わせように利用することで未知のデータに対してどれくらいの結果を出せているかを判断するのです。

その処理を行うのが次の工程となります。 今回のデータは分類する対象となるvirginica, versicolor, setosaの種類ごとに綺麗に並んでいます。< これを上から順などで分割すると、学習データにしかないラベル、テストデータにしかないラベルといったものがものが出てきてしまいます。
また、今回利用する機械学習のアルゴリズムは学習結果がデータが出てくる順番に依存するなどもあるため、まずはランダムに順番を並べ替えます。 そのために、乱数を振ったカラムを追加しその値でソートします。 なお、学習データとテストデータで重複する項目が発生しないようにするため、都度乱数で選択するのではなく最初に全体に乱数を振っています。

まずは乱数を振ったテーブルの作成から見ていきます。

create table iris_shuffled as 
select 
  rand(31) as rnd, 
  * 
from
  iris_scaled
;

select * from iris_shuffed limit 5;
OK
0.7314156962376819	1	Iris-virginica	["1:0.44444445","2:0.41666666","3:0.69491524","4:0.7083333","0:1.0"]
0.39281443656790715	2	Iris-virginica	["1:0.5277778","2:0.5833333","3:0.7457627","4:0.9166667","0:1.0"]
0.7859303306509032	3	Iris-virginica	["1:0.6111111","2:0.41666666","3:0.7118644","4:0.7916667","0:1.0"]
0.6390368254140532	4	Iris-virginica	["1:0.5555556","2:0.20833333","3:0.6779661","4:0.75","0:1.0"]
0.7464921643660909	5	Iris-virginica	["1:0.6666667","2:0.41666666","3:0.7118644","4:0.9166667","0:1.0"]

次にこの乱数でソートしたデータうち80%を学習データ、20%をテストデータとするためにそれぞれを分割したテーブルを作成します。

create table train80p as 
select 
  * 
from 
  iris_shuffled
-- データは全部で150件なので、降順ソートした時に上から120件(昇順なら下から120件)を学習データとして抽出する
order by 
  rnd desc
limit 120;

create table test20p as 
select 
  * 
from 
  iris_shuffled 
-- テストデータではソートを昇順にすることで学習データには含まれていない30件を取得する
order by
  rnd asc 
limit 30;

最後にテストデータでの予測処理のために、test20pテーブルの形式を変換しています。

create table test20p_exploded as 
select
 rowid, label,
 -- <feature_id>:<value>の形式のうち、feature_idの部分のみを取り出す
 extract_feature(feature) as feature,
 -- <feature_id>:<value>の形式のうち、valueの部分のみを取り出す
 extract_weight(feature) as value
from
 test20p lateral view explode(features) t as feature
;

select * from test20p_exploded limit 10;
OK
53	Iris-versicolor	1	0.5277778
53	Iris-versicolor	2	0.375
53	Iris-versicolor	3	0.55932206
53	Iris-versicolor	4	0.5
53	Iris-versicolor	0	1.0
107	Iris-setosa	1	0.19444445
107	Iris-setosa	2	0.625
107	Iris-setosa	3	0.10169491
107	Iris-setosa	4	0.20833333
107	Iris-setosa	0	1.0

なぜこの処理が必要なのかについては後ほど解説するとして、ここではこのクエリについて補足しておきます。 from句にある lateral view explode(features) t as feature という記述についてです。

これはどういったことをしているのかというと、まずexplode関数ですが、これは配列を個々の要素に分解します。 そしてlateral viewを使うことでそれを元のテーブルにくっつける処理を行っています。

rowidが53のデータを例にとると、まず元のデータは

53	Iris-versicolor	[1:0.5277778, 2:0.375, 3:0.55932206, 4:0.5, 0:1.0]

となっています。これに対して配列になっているfeaturesをexplode関数で展開すると

1:0.5277778
2:0.375
3:0.55932206
4:0.5
0:1.0

のようになります。この結果はもとより行数が増えていので、元の1行とくっつけるためにlateral viewという指定をしてやることで

53	Iris-versicolor	1:0.5277778
53	Iris-versicolor	2:0.375
53	Iris-versicolor	3:0.55932206
53	Iris-versicolor	4:0.5
53	Iris-versicolor	0:1.0

という形式のデータが出来上がります。最後にselect句で生成されたfeatureカラムをidと値に分けています。

Training (multiclass classification)

公式のwikiではこのプロセスの前に「Define an amplified view for the training input」というのがありますが、今回はこの処理をスキップして、学習の処理を行いたいと思います。 学習の処理はこのようになります。

create table model_scw as
select
  label,
  feature,
  argmin_kld(weight, covar) as weight
from (
  select 
    train_multiclass_scw(features, label) as (label, feature, weight, covar)
  from
    train80p
) t
group by label, feature;

select * from model_scw;
OK
Iris-setosa	0	5.587689
Iris-setosa	1	-1.3656
Iris-setosa	2	0.6939353
Iris-setosa	3	-1.5793471
Iris-setosa	4	-1.9188901
Iris-versicolor	0	0.8625847
Iris-versicolor	1	0.7565306
Iris-versicolor	2	-0.48721594
Iris-versicolor	3	0.6278288
Iris-versicolor	4	0.3813299
Iris-virginica	0	0.058980703
Iris-virginica	1	1.028395
Iris-virginica	2	0.607661
Iris-virginica	3	1.1568228
Iris-virginica	4	1.8326577

train_multiclass_scwという関数が実際に学習を行う処理です。 Hivemallでは機械学習のアルゴリズムはUDFとして定義されており、特徴量や正解ラベルなどの学習に必要な値を渡してやれば学習結果を返します。 今回使用しているSCWなどのアルゴリズムがどういったものかを理解しているのが理想的ですが、関数の使い方さえわかっていれば機械学習を行うこと自体は可能になっています。

冒頭で説明した通り、Hivemallは個々のノードにあるデータを使って学習を行ったのち、それぞれが持つモデルを全体で統合して一つのモデルを作成します。 外側のselectが市の処理になっており、argmin_kldという関数でその処理を行っています。 この関数は個々のノードが学習した結果のfeatureとlabelが同じものに対しての集約関数となるためgroup byを利用しています。

最後に、学習した結果が格納されているテーブルをの中身を確認すると、ラベルととfeature idごとにwegihtとなる数値が入っているのが確認できます。

Predict

次にテストデータに対する機械学習でのラベルの予測値を求めます。

今回のデータは120件を学習に利用し、残りの30件はこの評価のプロセスのために残しておいてあります。 こうする理由は分割の際に説明した通りです。 ここでは、テスト用のデータを利用して評価を行います。

まずは、学習したモデルを利用してテストデータのラベルを予測してみます。

create or replace view predict_scw
as
select 
  rowid, 
  m.col0 as score, 
  m.col1 as label
from (
  select
     rowid, 
     maxrow(score, label) as m
  from (
    select
      t.rowid,
      m.label,
      sum(m.weight * t.value) as score
    from 
      test20p_exploded t LEFT OUTER JOIN
      model_scw m ON (t.feature = m.feature)
    group by
    t.rowid, m.label
  ) t1
  group by rowid
) t2;

select * from predict_scw limit 3;
OK
8	2.8431001589129807	Iris-virginica
12	2.855541580816479	Iris-setosa
25	3.4697314095615566	Iris-virginica

予測にはUDFを使用していません。
処理内容について、内側のselectから順に見ていきます。
まずはテストデータに含まれるfeatureと学習データのfeatureでjoinすることで、データの入力値とモデルのweightを対応付けています。 学習データとテストデータに分割した際にテストデータをexplodeを利用して展開しておいたのはこのためです。
その後、sumで結合したデータに対して、入力データの特徴と学習モデルのweightを掛け合わせたものを計算しています。 これが予測時のスコアを計算する処理となっています。

次に一つその外側のselectではmaxrowという関数を利用して、計算したスコアが最も高いラベルのものだけを抽出しています。 一番内側のselect文での結合では、モデルのテーブルの性質上すべてのラベルのすべてのweightと結合するため、スコアもテストデータの個々の値に対して、それぞれのラベルのスコアが付与されているためこの処理が必要になります。

最後に一番外側のselectで最終的な出力を出しています。 このselect文では何らかのデータにたする処理を行っているわけでなく、各カラムに名前をつけているだけとなります。
最終的な結果として、テストデータの各行に対する予測ラベルとそのスコアが出力されます。

Evaluation

これが最後のステップとなります。

実利用時とは異なり、テストデータには本来の正解ラベルもあるため、それを利用して機械学習による予測がどの程度正しい結果を出せるかを数値化できます。
その指標としてはいろいろなものがあるのですが、今回は正解率を求めています。 正解率は (正解と予測が一致したラベル数) / (データ件数) で求めることができるのでのでその計算を行うクエリとなっています。

公式のwikiでは正解した数を一度viewにしてますが、簡単なクエリのためまとめてしまいたいと思います。

select
  count(1) / 30 -- 30はテストデータの件数
from (
  select 
    t.label as actual, 
    p.label as predicted
  from 
    test20p t JOIN predict_scw p on (t.rowid = p.rowid)
) t
where actual = predicted
;
OK
0.5666666666666667

UDFなどは利用していないため、内容について解説の必要はないかと思います。 上記の正解率を計算するのみになっています。

以上で一通りのプロセスが終了となります。

Hivemallを利用した機械学習の活用について

ここまでで、機械学習を行うフローが一通り揃っているので、他のデータでの利用などもイメージしていただけたかと思います。 全体のプロセスとしては長く感じたかもしれませんが、学習と評価部分以外にどのような処理が必要になるかは実際に利用するデータに依存します。基本的には
関数に入れられる形にデータを整形 → 学習 → 評価
というプロセスを繰り返し、活用できそうなモデルが作成できればそれを実際に使っていくというフローになります。

今回見ていただいた内容では、予測の際には特殊なUDFの利用はしていませんでした。 また、モデルも特殊なフォーマットのファイルではなくテーブルとして保存されています。
そのため、予測の部分に関してはRDBなどにモデルのテーブルのデータを移すことでwebシステム上などでも利用することが可能となります。

終わりに

今回はHivemallの導入と、それを利用した機械学習のフローについて解説しました。 機械学習そのものついては深堀はしていませんが、ライブラリなどを活用しであれば複雑な数学の知識などがなくても利用してみることは可能であることがわかっていただけたかと思います。
実際には数学的な知識があったほうが有利な場面も多いことは事実ですが、まずは何か動くもを作ってその動作を確認していくのがお勧めです。

Hiveの話は今回までとなります。
明日はHiveに関連したものとしてImpalaというものについて書かせていただく予定です。
ぜひ、お楽しみに!

AWS Cloud Roadshow 2017 福岡