にほんごのれんしゅう

日本語として伝えるための訓練を兼ねたテクログ

勾配ブースティングを利用した教師あり形態素分析

勾配ブースティングを利用した教師あり形態素分析

辞書ファイルを必要とせず、C++用のモデル(データを入力すると、結果が返ってくるもの)が構築できるので、任意の言語、例えばRuby等でも分かち書きや品詞推定 を利用できる様になります

理論

ある文章の特定の文字と文字の間に分かち書きを行うべきかどうかの、二値分類問題として扱うことができると仮説を立てます
周辺の”文字”の分布から、そこが分かち書きを行うのに適切なのかどうかを、なんらかの方法で人間が用意したデータを用い教師あり学習で学習します

図1. 問題設定

この問題設定におけるベクトルを直列化して、スパースなベクトルとして表現すると、LIVSVM形式のフォーマットにすることができ、LightGBM, XGBoostなどで学習することが可能なフォーマットに変換できます

品詞の推定も同様の理論で行うことができ、分かち書きした結果を利用して、再び周辺の”単語”の散らばりから予想します。
単語も未知語である場合には不明ですが、周辺の既知の単語情報のみで品詞を推定することができます

前処理

githubのレポジトリには最初から学習済みのデータを用意していますが、自分で必要なデータを学習させる場合には以下のステップで行います

教師あり分かち書きなので、教師となる分かち書きの粒度を示したデータセットが必要となります

例えば、mecabの生成する分かち書きを教師データとする例を今回行います

映画.com様から取得した500MByte程度の映画のレビューデータを利用して、boosting-tree-tokenizerに学習させるデータセットを作成します

ダウンロード(20GByte程度あります)

$ cd misc
$ python3 downloader.py 

1. ポジティブデータ、ネガティブデータセットの作成

以下の様なデータを作成します

ポジティブ
[、今日は o 天気がい]
分かち書きがあり得る箇所にoが入ります
ネガティブ
[ベネチア x ンマスク]
分かち書きとしてありえないところにxが入ります

2. スパースマトリックスの作成

特徴量の作り方として、周辺の文字、単語とその距離の二つの軸で独立な特徴量とするため、まともにやろうとすると、大変高次元化して今います.
そのため、スパースマトリックスとして表現するために、まず、特徴量に対してユニークなインデックスを付与します

$ python3 wakati.py --make_sparse
$ python3 parts.py --make_sparse

付与したインデックスをもとに、スパースマトリックスを組み立てます

$ python3 wakati.py --make_sparse2 
$ python3 parts.py --make_sparse2

3. train, testデータの作成

お使いのマシンのスペックに依存しますが、trainデータで100万, testデータで10万データセットを利用する際には、この様にしまします

$ head -n 50000000 ./misc/download/dataset.txt > train
$ head -n 3000000 ./misc/download/dataset_parts.txt > train_parts
$ tail -n 10000000 ./misc/download/dataset.txt > test
$ tail -n 100000 ./misc/download/dataset_parts.txt > test_parts

LightGBMでの学習

binary-classificationでの二値分類の問題としてみなします
具体的には、ある文字と文字の間に、分かち書きすべきかどうかの確率値を計算します
50%を超えると、分かち書きを有効にし、50%未満では分かち書きを行いません

有名な勾配ブースティングライブラリにはXGBoostとLightGBMとCatBoostなどがあるのですが、スパースマトリックスの扱いやすさからLightGBMを使います

1. LightGBMのインストール

cmakeでビルドとインストールできます(要:GNU make, gcc, cmake)

$ git clone https://github.com/Microsoft/LightGBM
$ cd LightGBM
$ mkdir build
$ cd build
$ cmake ..
$ make -j16
$ sudo make install

これでlightgbmコマンドがシステムに追加されました

2. LightGBMで学習する

学習に使うパラメータを記述したconfがあるので、必要に応じでパラメータを変更して用いてください

$ lightgbm config=train.conf
$ lightgbm config=train.parts.conf

学習したモデルで分かち書き、品詞推定をしてみる

映画.comさんのレビューをランダムサンプルして適当に分かち書きしてみています

$ python3 intractive.py --test

出力はこの様になり、概ね、期待通りの分かち書きになっていることが確認できるかと思います

- 調子/に/乗り/過ぎ/て/いる/感/が/嫌い/だ/が/、/この/作品/は/見/て/よかっ/た
- はじめ/と/する/役者/さん/たち/の/演技/に/泣か/さ/れ/た/。/小林武史/の/音楽/も/素晴らしく/て/サントラ/を/買っ/て/しまっ/た/。/原作/も/読み/た
- 読ん/で/で/、/これ/を/見/た/が/とく/に/違和感/なく/見れ/た/。/多分/、/原作/は/もっと/色々命/と/の/引き換え/を/考え/て/た/気/も/する/けど/、/これ/は/これ/で/よい/。
- 映画/は/苦手/だっ/た/の/です/が/、/母/の/誘い/で/見/に/行き/まし/た/。/後半部分/が/良い/です/。/自分/で/も/なん/で/泣い/て/いる/か分から/ない/ほど/涙/が/出まし/た/。
- 目/が/真っ赤/だ/けど/大丈夫/?/!/と/言わ/れ/まし/た/.../。/今/に/なっ/て/、/あぁ/良い/映画/だっ/た/。/と/思っ/た/ので/レビュー/し/て/み/まし/た/。/命より/大切/な/もの/は/あり/ます/。
- スケジュール/が/合わ/なく/て/見/に/行け/なかっ/た/が/、/近場/の/IMAX上映館/が/今週末/から/箱割/を/大幅/に/カット/する/こと/が/わかり/、/慌て/て/見/に/行っ/た/。/IMAX上映/を/前提/と/し/た/映像/凄い/迫力/と/臨場感/だっ/た/が/、/人物/や/物語/は/平板/な/もので/、/特別/感動/も/なく/終了/。/欧州戦史/に対する/ある/程度/の/前提/知識/が/必要/だっ/た/の/かも/しれ/ない/と/思っ/て/パンフ/を/購入し/た/が/、/これ/が/凄い/情報量/だっ/た/。
- 今/に/なっ/て/、/あぁ/良い/映画/だっ/た/。/と/思っ/た/ので/レビュー/し/て/み/まし/た/。/命/より/大切なもの/は/あり/ます/。/
- 大切なもの/と/の/思い出/が/消え/た/中/で/1/人/で/生きる/なんて/つら/すぎ/ます/。/改めて/、/それ/に/気づけ/て/良かっ/た/。/周り/の/人/を/大切/に/しよう/と/思う/映画/。

品詞推定の例
実行時にオプションをつける必要があります

$ python3 intractive.py --test --hinshi

出力はこのようになります

本文/に/ネタバレ/が/あり/ます/。
本文 43 名詞-一般
に 33 助詞-格助詞-一般
ネタバレ 59 名詞-固有名詞-一般
が 33 助詞-格助詞-一般
あり 67 動詞-自立
ます 4 助動詞
。 12 記号-句点

Pure C++で記述されたモデルを得る

まだLightGBMの実験的な機能だということですが、C++で記述されたモデルを出力可能です。
具体的には、決定木の関数オブジェクトのリストを返してくれて、自分でアンサンブルを組むことができるようになっているようです
Pure C++なので、C++バインディングできるRubyなどの言語は、別途MeCab等を利用しなくても辞書がなくても、形態素解析できるようになるという感じです
(モデルをどう使うのかよくわからないのですが、知ってる人Twitter等でサンプルコード等教えていただけませんか。。。)

C++形式のモデルを出力する

traon.confの以下のコメントアウトを外すと、C++で直接コンパイルできて判別できるモデルができあがあります

convert_model=gbdt_prediction.cpp
convert_model_language=cpp

feature_index.pklのC++

pickle形式の特徴量の対応表はC++には読めないので、cppのファイルに変換します

$ python3 intractive.py --make_cpp

実装

感想とか印象とか

このネタはもともと、何か論文書けこのやろうと詰められる機運が高まってきたので、何か自然言語処理で論文になるようなネタを探していた時にふと、勾配ブースティングの性質であればできるんじゃないのかな、しかもこの場合、辞書なしでできる、という気づきがあり、やってみました
うまくいったので、なんらか形態素という人間が考える意味粒度を与えるデータセットさえあれば、傾向を一般化して、形態素解析として働きうることを示せたかと思います
私のC++力が足りないので、C++の実装がうまくいかなったですが、仮に実装したとなると、モデルのみのインプリテーションで済むので、辞書ファイル等の更新が必要なくなるという大きなメリットがあります

Twitterで報告したところ、TinySegmenter(AdaBoostでの機械学習)という似たようなアプローチを知ったのですが、数パーセントですが、こっちの方が分かち書きの性能はよかったです。あと、TinySegmenterでは品詞の推定ができませんが、これはできることが強みでしょうか

名称について

色々な名称の形態素解析器がありますが、このプロジェクトから派生した今回の一連のコードを珊瑚(sango)としたいと思います

理由は一部のサンゴが、任意の海流の流れ(とはいっても多分その海域での特徴とかあると思うんですが)を、任意の方向に整流することができます。これは、サンゴの中に複雑な微小な穴があって、穴はツリー状に内部で経路を構築しており、この穴をどこからでも海水が通ると、不思議なことに一定の方向性を持って海水が出てきます

あたかも乱雑な情報から秩序と法則性を取り出すことができる勾配ブースティングの動作に似ていることから、この名前をつけたいと考えています

ライセンス 

MIT

Alternative Implementation Of Illustration2Vec Ver2.

Alternative Implementation Of Illustration2Vec Ver2.

Alternative Illustration2Vec Ver2の概要

  • 画像をタグ等の特定の特徴量に従ってベクトル化できる
  • このベクトルとは通常画像分類で用いられるsoftmaxなどのマルチクラスではなく、softprobの(*1)問題として捉えることができる

Version2の改善、変更点

  • Ver1に比べて探索的であって割とひどいコードを修正して、わかりやすく変えました
  • 150x150の画像のサイズから、224x224にスケールアップしました
  • BatchNormalizationだけでなく、DropOutも併用してネットワークのスパース性を高めました
  • Msgpackを用いたKVSをやめて、ただのPickleで画像のシリアライズをする様になりました
  • Keras V2のインターフェースに合わせました

*1 softprobはxgboostの用語でありますが、各クラスに該当するものが、その確率値を返すものです

VGG16の転移学習

VGG16の評価モデルはよくチューニングされており、別段何かしなくても良いパフォーマンスが出せます
そのため、VGG16の評価モデルを転移学習することでそのチューニングされた良い状態を保存しつつ、softprobに該当する部分を付け足すことで、illustration2vecに該当する機能を付与させます

モデル図

毎回気持ちですみません(しかし、なんらかの活性化関数の新規のものの提案というスタイルではないので、雑でいいという思いがあります)

図1. モデルのイメージ

転移学習時のネットワーク設計

今回は過去最大の5000次元を予想するというタスクであったので、全結合層をReLuで結合していきます
前回はBatchNormalizationのみでやったのですが、これだけを用いうることに少し不安があったので、DropOutを30%のデータを入れるようにしました
ReLu, DropOutともネットワークを疎結合にする役割が期待できますので、ネットワークの意味のクラスタが獲得しやすくなると期待できます

オプティマイザと損失関数

これ系のタスクではAdamかSGDが良い成績をいつも収めることが期待されていますので、何も考えず、Adamで決め打ちです
- オプティマイザ、Adam : LearningRate 0.001 - 損失関数、BINARY CROSS ENTROPY

使ったデータセット

Safebooruさんからダウンロードさせていただきました
タグとURLと画像の三つの情報を保存し、学習用のデータセット1,500,000枚、テスト様に20,000枚用意します

データがメモリに乗り切らないときに使ったアプローチ

メモリに乗らないときは、最近いつも使うのですが、データセットを分割読み込みまします
ソフトウェア工学者にとっては一般的なアプローチですが、Epochと対応させて学習すると、なんとかスケジューリングできます(ちゃんとKerasのインターフェースの中にはgeneratorというデータセットを切り出す仕組みがありますが、私はこれを操作が複雑になりすぎると感じているであまり用いていません)

具体的には、15000枚の画像をオンメモリに保持 -> 1Epoch回す -> 別の150000枚の画像をオンメモリに保持 -> 1Epoch回す -> ...

ということを続けていきます

全てが一回のみスキャンされます

転移学習の例

前処理

make_datapairディレクトリ内のmake_datapair.pyを"--make_tag_index"オプションをつけて実行すると、特定のディレクトリの内部の画像とタグ情報に対して、よく頻出するタグ5000個に対して、タグとそのインデックスを記述します

make_dataset.pyを"--make_pair"のオプションをつけて実行することで、実行することで、タグとインデックスをもとに、画像のベクトル情報 Xとそのタグ情報をベクトル化した情報yのペアを作ります。一つの画像に対して、Piar(X,y)となる情報をpickle形式のファイルで出力します

学習

alt_i2v_V2.pyに"--train"オプションをつけて実行することで、学習します。15000枚の画像のデータセットラウンドロビンで切り替えながら学習しますが、メモリ64GByteマシンの基準でやっていますので、必要に応じて、サイズを小さくするか、ランダムサンプルに切り替えるか、Epoch数を変化させるなどすると良いでしょう

予想

alt_i2v_V2.pyに"--pred"オプションをつけて実行することで予想が可能です

評価

定量的なMSEの他に、定性的なみんなが好きそうな結果を載せておきます
結果は概ね、良好で、画像識別は本当に今や簡単になりましたね、という印象があります

KPI予想としてのIllustration2Vec

Practical Lessons from Predicting Clicks on Ads at Facebookという論文があります

趣旨としては、テキストや画像やなんらかを特徴量の集合としてみなして、CTR予想などのKPI予想問題を行う際、特徴量が極めて高次元である際、アルゴリズム(この場合、勾配ブースティングの木の出力値)を特徴量とすることで、これを元にLinear Regressionをかけることで、より実践的な速度で、より高精度で予想できるという趣旨でした

勾配ブースティングはそのアルゴリズムから残渣となる誤差を最小化する様に特徴量を選択しますが、例えば、これがIllustration2Vecの出力次元値ではどうでしょうか。アルゴリズムが自動で獲得した粒度の特徴量ではないですが、同様の圧縮された特徴量の空間と見做すことができます。(LDA的な自動でクラスタを獲得させる方法も筋が良さそうです)

データが十分に手元にないので、検証は今現在、簡単に行えそうにはありませんが、できそうなことなのでいずれチャレンジしたいと考えています

コード

こちらからダウンロード可能です

ライセンス

MIT

Pine64を10台でクラスタ組んで、Celeryで分散基盤を使って効率的に機械学習する

Pine64を10台でクラスタ組んで、Celeryで分散基盤を使って効率的に機械学習する

Pine64とRock64

Rasphberry Piを個人で5台ほど所有しているのですが、如何せん、機械学習や計算リソースが必要なプログラミングには向いていないしパワーが足りません。何よりメモリが圧倒的に足りないです
Armと呼ばれるスマートフォンに入っているようなアーキテクチャのCPUが今後来るのではないか、という期待の元[1]、10台ほどネット通販にて、購入にしてクラスタを組んでみました

PINE64-LTS(2GB)という撰品で64bitのArmアーキテクチャの製品で、一個$35程度です
最近、さらに高速で大メモリのものが出ました
ROCK64(4GB)で、これは$45程度です。ギリギリこれぐらいで遅い計算機として使えるという印象があり、やはりパワーは大事です

図1. 組んだクラスタの写真

Celeryでできる分散機械学習

コンピュータ一台ではできる機械学習のリソースが限られています。データ収集基盤については、Map Reduce, Apache Beams(Google Cloud DataFlow), Sparkなどの選択肢がありますが、学習するというものについてはそんなに多くありません
LightGBMはそのソフトウェアに分散して学習する機能がありますが、他のシステムではあまり見かけないように見受けられます
Celeryという分散タスクキューを利用することで、効率的にグリッドサーチができることや、(今回は試していませんが)アンサンブル学習ができることを示します

分散タスクキューの定義をよくわかっていないのですが、Remote Procedure Callをラップアップしてリモートのサーバの関数を透過的に利用できるのが、このライブラリの最大のメリットのように思います

データの通信に様々なシリアライズ方式を使うことができて、pickleなど、複雑でPythonのほとんどのデータ構造をサポートするのもの転送可能なので、モデルや、学習すべきnumpyのアレーなども転送可能なことから、今回、これを使いました

グリッドサーチ

機械学習には一般的に膨大なハイパーパラメータと呼ばれる多数のチューニングポイントが存在します。これらを賢く最適化するには、グリッドサーチや、それを改良したベイズ最適化や様々なアプローチが使えます
パラメータを変化させて学習、評価をしていき、最もパフォーマンスが優れいている点をあぶり出すのですが、やっていることは単純ですが、計算量がひたすらにかかります
分散コンピューティングをすることで、一台のマシンあたりの探索範囲を限定することで、効率よく、短時間て優れたパラメータを特定可能になります。

図2. グリッドサーチの探索範囲

アンサンブル学習

アンサンブル学習にもいくつか手法があります
複数の機械学習モデルの平均値をとる方法や、モデルに対して重要度を再度重み付けや、決定木をとることでアンサンブルにする方法があります

RandomForestの出力結果の平均をとり、巨大なモデルであってもコンピュータ一台がそのリソースの範囲で扱えるモデルを超える巨大なモデルとして振る舞うことも可能です

Celeryの設定とコーディング

ごちゃごちゃとCeleryを触っていたのですけど、Pythonのコードの限界というか、デコレータと呼ばれるceleryの関数の引数になっているユーザ定義関数の制御が厄介で、モンキーパッチなどの、あまり好ましくない方法で制御する必要がありました
clientとserverに機能を分けて、グリッドサーチではこのように定義しました

Celeryを動かすのに必要なソフトウェアとライブラリ

  • celery
  • rabbitmq
  • scipy
  • scikit-learn

rabbitmqにはメッセージをやり取りするのに、ユーザ名[remote]、パスワード[remote]を設定する必要があります

グリッドサーチ

Client

  • データセットを整形してServerに送信
  • 様々なパラメータを代入したモデルを構築し、学習と評価はせずにこの状態をpickleでシリアライズしてserverに送信
  • Serverで評価した探索範囲内でのベストパフォーマンスを回収
  • 各Serverの情報を統合して、最も良いパフォーマンスの結果を出力
  task_que = []
  for hostname, params in hostname_params.items():
    parameters = {
      #'n_estimators'      : [5, 10, 20, 30, 50, 100, 300],
      'max_features'      : params,
      'min_samples_split' : [3, 5, 10, 15, 20, 25, 30, 40, 50, 100],
      'min_samples_leaf'  : [1, 2, 3, 5, 10, 15, 20],
      'max_depth' : [10, 20, 25, 30]
    }
    from sklearn.ensemble import RandomForestClassifier                                               
    from sklearn.datasets import make_classification 
    from sklearn import grid_search 
    clf = grid_search.GridSearchCV(RandomForestClassifier(), parameters, n_jobs=4)
    import tasks
    tasks.write_client_memory_talbe(hostname) # ここでモンキーパッチを行なっている
    task = tasks.gridSearch.delay(X, y, clf)
    print( 'send task to', hostname )
    task_que.append( (hostname, clf, task) )
  for hostname, clf, task in task_que:
    estimator, best_param, best_score = task.get()
    print('{} best_score={}, best_param={} '.format(hostname, best_score, best_param))
    print( estimator )

Server

  • clientからデータを受け取る
  • 受け取ったデータの中のpickleからモデルを復元して学習
  • 探索範囲のパラメータの中から発見した最良の結果を返却する
  @app.task
  def gridSearch(X, y, clf): # dataとモデルを受け取って
    print('start to fit!')
    clf.fit(X, y) # 学習
    estimator = clf.best_estimator_ 
    best_param = clf.best_params_
    best_score = clf.best_score_ 
    return (etimator, best_param, best_score) # 最良のものを返却

モンキーパッチなどの実装は、コードに詳細が記述されています

コード

github.com

サンプルの実行

githubにあるものは、adultと呼ばれるデータセットでアメリカの人の学歴や人種やその他のパラメータで収入が$50kを上回るかどうかの判別問題をバンドルしています
IPアドレスがハードコードされていますが、ご利用の環境に合わせて、編集してください

Serverサイドでは、celeryをこのように起動します
自分のIPアドレスを自動で検出しますが、まれに検出に失敗するときには、ホストネームを自分のサーバのホスト名に設定します

$ celery -A tasks worker --loglevel=info

サーバサイドのプロセスが全て起動したことを確認したら、Clientからグリッドサーチの命令を送ります

 $ python3 grid_search.py --random_forest
 {'max_depth': 30, 'min_samples_split': 30, 'min_samples_leaf': 1, 'max_features': 5} 0.8591873713952274 # <-最適値なパラメータ                                                                                               
RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini', # <- 最適なパラメータを適応したモデル
            max_depth=30, max_features=5, max_leaf_nodes=None,                                     
            min_impurity_decrease=0.0, min_impurity_split=None,                                    
            min_samples_leaf=1, min_samples_split=30,                                              
            min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=1,                               
            oob_score=False, random_state=None, verbose=0,                                         
            warm_start=False) 

グリッドサーチを1台で行うのと、5分割して行うもののベンチマークの差

分散処理するサーバを一台に限定して行うと、この程度の時間がかかります  

 elapsed 32714.200119832235 <- 32715秒、つまり、9.1時間

これを5台に分散して処理すると、この程度になります。

using hostname is 192.168.15.37
send task to 192.168.15.80
send task to 192.168.15.81
send task to 192.168.15.45
send task to 192.168.15.46
send task to 192.168.15.48
{'min_samples_split': 50, 'min_samples_leaf': 2, 'max_depth': 30, 'max_features': 35} 0.8634869936427014
RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
            max_depth=30, max_features=35, max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impurity_split=None,
            min_samples_leaf=2, min_samples_split=50,
            min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=1,
            oob_score=False, random_state=None, verbose=0,
            warm_start=False)
elapsed 5459.255481719971 # <- 5460秒、つまり、1.5時間

複数回サンプルして平均を取ると正確な値になるかと思いますが、一回の施行での例です

Adultデータセットベンチマーク

データセット提供元の、UCIの詳細情報によると、エラー率に関して、様々なアルゴリズムベンチマークが記されており、今回のエラー率は十分に低く、ほぼ理想的なパフォーマンスを発揮できたことがわかるかと思います

今回GridSearchしたベンチマーク

ACCUR 86.34869936427014
ERROR 13.6513006357299

UCIによるベンチマーク

|    Algorithm               Error
| -- ----------------        -----
| 1  C4.5                    15.54
| 2  C4.5-auto               14.46
| 3  C4.5 rules              14.94
| 4  Voted ID3 (0.6)         15.64
| 5  Voted ID3 (0.8)         16.47
| 6  T2                      16.84
| 7  1R                      19.54
| 8  NBTree                  14.10
| 9  CN2                     16.00
| 10 HOODG                   14.82
| 11 FSS Naive Bayes         14.05
| 12 IDTM (Decision table)   14.46
| 13 Naive-Bayes             16.12
| 14 Nearest-neighbor (1)    21.42
| 15 Nearest-neighbor (3)    20.35
| 16 OC1                     15.04

ベンチマーク時点が古いですが、かなり余裕ですね。Armの安いマシンでの機械学習でもベンチマークを超えることはできました。

Appendix. Install Celery and Denpendencies

これが結構ややこしく、rabbitmqなどのセットアップとか結構調べました
ボリュームが多くなってしまったので、別途記載しました
github.com

まとめ

Celeryなどの分散タスクキューを使うと、機械学習の例えばグリッドサーチなどの利用において、高速化することができることがわかりました

この発想の延長線上に、Googleが作成したSibyl(シビュラ)という名のCTR, CVR予想システムがあるのですが、わかる感じの情報を拾っていくと、どうやら特徴量が2000万とかそういうレベルの予想機で、単独のマシンによる結果ではなくて、各マシンが返す結果を、Adaboostや誤差を最小化するスタッキングのアンサンブルのようなアルゴリズムで、集約しているようなので、このような膨大な特徴量が必要な予想でも、マシンの台数さえ確保すれば行えそうであると言えそうです[2]

あと、ARMのサーバを実際にいじってみて思ったんですが、とにかく重いですし、莫大な計算量が必要な機械学習にはあまり向いないかもしれないと思ったりしまた。しばらくは、x86_64アーキテクチャサーバでなんとかなるかもしれないのと、Ryzenのような多コアのCPUはうまく使えばものすごく強いので、無理にARMを採用する必要はないように思います

Celeryはモンチーパッチして今回つかったため、個人でやるにはやはりこういうアドホックが許されるPythonは便利だなと思ったのと、コードが汚くなることを許容するため、何か製品として出す場合には使えない方法だなとも思ったりしました。Pythonに変わる言語、何かないかな

参考文献

機械学習のスタックしていた案件をFacebook Prophetで3日で返済した話

機械学習のスタックしていた案件をFacebook Prophetで3日で返済した話

背景

  • 広告代理店業を行なっており、クライアント企業から予算を預かって、インターネット広告やマーケティング業をしているのだが、クライアントの予算消化の異常値を監視したい
  • 2016年半ばに外部のデータ分析専門の会社に、その日の予算消化が異常の場合、アラートを鳴らすシステムを外注開始、2016年10月に納品
  • 2017年9月半ばに進捗率が芳しくないことが判明した。終わる見込みが立たなかったので、私が解決に当たる  

(ついでに"Machine Learning: The High-Interest Credit Card of Technical Debt[2]"と呼ばれる負債化してしまう機械学習のシステムとはという評価軸があったので、これらから今回使えそうなプラクティスを取り出して適応してみたいというモチベーションがあった

さらに、開発速度感が出なかったのでUberが採用したMicroserviceのアーキテクチャの粒度に還元できるように再設計し、システムやアルゴリズム、データ収集などに対し横断的であった機械学習チームの役割を明示的に、httpでjsonで命令を受けると、jsonを返すだけのモジュールの開発に限定することで、スコープを機械学習アルゴリズムの開発・チューニングに絞った。)

全体の進捗感

  • 9/19で既存システムのサーベイランスと、問題点の洗い出し
  • 9/20でより使い勝手が良い設計へ変更、プロトタイプコーディング開始
  • 9/21でプロトタイプコーディング終了し、仕様要件の微細な差を共通認識をとり、私の手を離れて、本番実装・保守チームに移管完了(彼らはAPIのIFを付け加えるてテストを通し、デプロイし、最終的に運用に渡す)

既存システム

既存システムは個々のクライアントの予算消化を監視して、異常に多すぎる予算消化になる場合や、少なすぎる場合にアラートを鳴らすというシステムであった  

STLと呼ばれるRの周期成分分解するライブラリで、周期成分を消すとポアソン分布や正規分布に予算消化の幅が従うことから、ARIMAモデルで自己回帰で導出できるとしていたモデルで、自己回帰の予想した線からの乖離を標準偏差で表現して、異常値を定義するシステムであった。  

問題点

  • 当時、最先端とされていたAWSのデータパインプラインやそれに関するサブシステムを動員しており、できることが予算消化の異常値検知だけなはずなのだが、かなり大規模なシステムになっていた。  
  • 開発状況も、意思決定層にヒアリングしていた内容よりだいぶ未達であることがわかり、仮にこれを完成させて運用したとしても、多くの人間の労働力が消えていくと考えられ、根本的に見直す必要があった。  
  • STLを適用してARIMAで学習して異常値を検出するのに、計算時間がAWSのCPU最適化インスタンスで12時間と、スケールアウト性にも疑問があった。  
  • 弊チームは技術部に属しており、Pythonを使える人間は多くいるが、Rを十分に使える人は一人もいなく、誰も新人にアドバイスできず、放置状態であり、教育にもよくなかった。  
  • 評価指標がARIMAで導出した予想系列からの乖離をみて、標準偏差を計算しているのが、標準偏差のどの閾値でアラートを出せばいいのか不透明であった

提案システム

まず、委託した企業の調査報告書から、売り上げの時系列変化は周期性成分を消した状態では、正規分布ポアソン分布に従うとの報告を受けていたことが判明したので、この時点で信頼区間が出せるFacebook Prophetにリプレースするという選択肢が候補にあった(つまり、正規分布の信頼区間のようなものであろうという今回の仮定と一致する)。  

Prophetは時系列予想ツールで、トレンドと呼ばれる長期的な成分と、周期性成分と、イベントのように突発的な成分を表す合成関数でフィッティングすることで、信頼区間を考慮した状態で、予想できる便利なツールである  

また、Facebook Researchの一般的な検証結果で、今回のSTL + ARIMAで行なっていた学習と同等かそれ以上の結果が得られることがわかった

図1. 提案システム

メリット

  • 一つのライブラリに収まっているのでコード量も少なく済む
  • 標準技術セットのPythonで記述で可能であり、別途Rを学ぶ必要がなく、学習コストの低減と、属人化の回避が期待できる
  • STL+ARIMAで12時間かかっていた作業が10分に短縮された
  • 信頼区間という95%はカバレッジがある領域を推定するので、残り5%を異常値とすればよく、判断が容易であり、またこれを調整するのも容易
  • 雑にAWSの安いインスタンスでcronで動かせばよく、予算も、保守チームのコストも、IFの設計を行うチームへの負担も全て軽くなる

今回のアルゴリズムである、prophetは三つの系列の予想のアルゴリズムの和で表現できるとしている  

大局的なトレンドを予想する関数

a(t)は、ステップ関数のようになっており、特定の閾値では1,0を出力する関数

kは成長係数   δはa(t)の補正係数
bはオフセット係数 γは関数を継続する役割がある

周期性を予想する関数

Cnとnを推定する問題として扱える

イベントを予想する関数

Diはイベントの集合である

これらの合成関数で表現されていて、合成関数は既存の手法であるARIMAなどに比べて精度は良いようである

オプティマイザ自体は標準では準ニュートン法のソルバが利用され、非常に収束が早い  

MAPE誤差の定量的な評価では、既存の様々な手法より誤差は少ないようである  

実装

  • 基本的に前のシステムが学習に用いていたデータセットを変更させなくても、学習と予想部分だけ変更することで対応可能であることが判明したのでそのようにした。
  • 定期的に毎日、特定のcsvがgzで圧縮された状態で特定フォルダに投入されるので、それをPandasでパースして、Prophetで学習して、前日分のデータが異常値に該当すれば、jsonに出力し、該当しなければ出力しない(出力したjsonファイルは別チームがアラートシステムに投入する)

評価とチューニング

実際のデータを見ながらオーバフィットしないように注意しつつ、薄い水色の範囲(uncertainty intervals)が、特定の閾値で設定した値以上で起こる確率の範囲である。   

つまり、これを1.0にしてしまうと、今まで起こり得た系列は全て薄い水色になってしまうし、小さくしすぎても多くのアラートが飛んでしまうので、これも実用的でない。  

想定するエンドユーザから、180日分の過去のデータをもらっており、このうちの5%ぐらいは異常値としてアラート、ないし、ワーニングを飛ばして欲しいとのことであったので、そのように対応した。  

系列のトレンドはかなりこまめに変化していることがわかり、これは、アメリカの株式市場の挙動と似ていたので(ここは定性的)、株式市場のデータを用いてパラメータチューニングを行なった。  

図2. Googleの株価のtime series

図3. AMDの株価のtime series

株価にフィッティングするパラメータチューニングを行なったところ、実運用するべき異常値検知アルゴリズムに直接適応しても、悪くない結果が得られることが判明した。

トータルコストの変化

$76(c3.large) -> $9(t2.micro) 

まとめ

機械学習で結局何がやりたいのか、何ができるのか、またそのロジックはどうなっているのか、何らか簡易的なパッケージで表現可能なのか、システムとしてその選択は適切なのかというかなりヒューリスティクな判断が必要になる(と思っている)機械学習の実運用なのですが、このように適切な意思決定さえできれば一年近く進まず人月だけをひたすら溶かしていくような案件を3日で閉じることも可能になります。  

実際には多くの方のもう終わりにすべきだという強い意志と、私のやっていることを補助していただける発言でサクサク進んだところもあるので、周りの方々には本当に感謝しています。  

機械学習を通じて何らかシステムを作りたいが、どうにもこうにも進まないという企業さんもいらっしゃると思います。知見として多くの人に共有・公開することで、この件が一助になれば幸いです。

株価でチューニングに用いたコードはこちらです

Jupyter NotebookのStock.ipynbというファイルに記されています
(試行錯誤だらけで汚いです。。。)

参考論文