盆暗の学習記録

データサイエンス ,エンジニアリング,ビジネスについて日々学んだことの備忘録としていく予定です。初心者であり独学なので内容には誤りが含まれる可能性が大いにあります。

note.comを読みやすくするChrome拡張機能を作った

皆さんはnote.comを使っていますか?

最近は利用者がどんどん増えていて記事数も多いため、なにか興味深い記事を見かけた際にアクセスする機会も増えているのではないでしょうか。少なくとも私はそうです。

noteはいいサービスだなと思う一方で、記事を読みづらいUIだと感じています。その理由は

  1. 記事の幅が狭いこと(620px固定)
  2. サイドバーに固定表示される目次が無いこと

の2点です。

例えば1920 x 1080のディスプレイで見ると、以下のようになってしまいます。

(例として使わせていただいた記事:https://note.com/keisemi/n/nc2c374fad8d6

短い記事だったらいいんですが、長い記事だとこのUIで読むのは辛いです。

そこで上記2点を改善するChrome拡張を作りました。

https://chrome.google.com/webstore/detail/note-toc/dddpojfjpcidbebhjijlchdkfmegoidgchrome.google.com

これを導入するとこんなUIになります。

もし同じ不満をお持ちの方がいらっしゃいましたら使ってみてください。そのうちnoteがアップデートされてこんな拡張は不要になると思いますが、それまでの間を多少は快適に過ごせるかと思います。

「150 successful machine learning models: 6 lessons learned at booking. com」を読んだ

KDD19で発表されて一時期話題になっていたBernardi et al. (2019) 150 Successful Machine Learning Models: 6 Lessons Learned at Booking.comを読みました。

これは宿泊施設を検索して予約できるウェブサイト「Booking.com」における機械学習の活用事例と、その過程で得たノウハウについての論文です。

機械学習のビジネス活用に焦点を当てていて良い論文でした。

以下にざっくり内容をまとめてメモしていきます。

機械学習はビジネスに貢献する

まず、機械学習の活用事例について。

Booking.comでは多数の機械学習モデルを利用しているのですが、例えば、

  • お客様の旅行のcontextを予測して、子供がいたらその情報も検索欄に入れるようにリマインドする(Figure 1 (a))
  • 各宿泊施設についてのレビューを要約する(Figure 1 (b))
  • 特定の都市の価格のトレンドなど有益な情報を提示する(Figure (c))

みたいなことをしているそうです。

f:id:nigimitama:20211227224734p:plain

また、Booking.comにおいては機械学習モデルはRCTによってビジネス指標で評価しているらしいのですが、機械学習は十分なビジネス貢献をしていることを述べていました。

f:id:nigimitama:20211227224744p:plain

Figure 2のBenchmark以外の各バーは機械学習の活用の各カテゴリ(model family)で、Content Curation以外はBenchmark(これらの機械学習の活用以外のプロジェクト)を上回っています。

Offline Metricsはビジネス指標とは相関しない

モデルのOffline EvaluationにおけるMetric(ROC-AUCとか)の改善がビジネス貢献につながるとは限らない、という発見について。

f:id:nigimitama:20211227224805p:plain

Figure 4は機械学習モデルの改善を行った23個のペア(46モデル)についての図です。横軸がOffline EvaluationのMetrics(classifierについてはROC-AUCでrankerについてはMean Reciprocal Rank)の相対差で、縦軸がOnline EvaluationにおけるConversion Rateの相対差になります。

これについての著者らの考察は次のようなものでした:

  1. パフォーマンスの増加によるビジネス価値の増加は逓減する
  2. 不気味の谷現象:ユーザーの行動をピッタリ予測しすぎて気味悪がられる
  3. 中間的な指標への過度な最適化:例えばクリック率を最適化するようにモデルを作っていった結果、コンバージョンにつながらない「単にクリックさせるだけ」のモデルになっていった

Booking.com内では他の領域の実験でも一貫して同様の結果が得られたそうです。

Latencyの増加がビジネスに悪影響を与える

latency(待ち時間)が増えるとビジネス指標を下げるし、逆にlatencyを下げる努力をすることでビジネスを改善できたという報告。

Figure 6は横軸がlatencyの処置群と対照群の相対差で、縦軸がConversion Rateの相対差です。図の右下の象限は程度の異なる人工的なlatencyを処置した実験のもので、左上の象限は独立した4つの実験の結果です。

f:id:nigimitama:20211227224823p:plain

機械学習は計算コストが多いためlatencyとも関わってきます。Booking.comでは計算量を下げるために、様々な工夫をしているそうで、個人的に印象的だったのは次のものでした:

  • 自社開発した線形予測エンジン: 予測時間を最小化するように徹底して調整した線形予測を自社で開発した。これはナイーブベイズ、一般化線形モデル、k-NN、Matrix Factorization modelなどの全てのモデルを内積に緩和することができる。
  • Precomputation and caching: 特徴空間が小さい時、すべての予測値を単純にkey-value storeに分散することができる。特徴空間が大きすぎるときもよくあるリクエストはメモリにキャッシュすることができる。

線形予測エンジンについてはどうやって計算量を下げているんだろう…という感じですが、論文中では詳しくは書かれていませんでした。precomputationは言われてみれば確かに当たり前なんですが徹底してるなーと感じました。

予測値のみでモデルの品質を監視する

デプロイして稼働させている機械学習モデルが変な値を予測していないかは気になるところだと思います。

しかし少なくともデプロイしてすぐには正解データ(教師ラベル)がない状態なので、予測値が正しいかどうかがわからないという問題があります。

Booking.comではこうした「単純に予測値だけを見てモデルの品質についての情報を得たい」という問題に取り組み、二値分類に関してはResponse Distribution Analysisという方法を考案して使用しているそうです。

この方法は単純に分類器の出力({0, 1}の値ではなく、スコアのほう)のヒストグラムを作り、

  • 理想的な二値分類器は0付近と1付近にピークを持つ二峰の分布になるはず(Good discriminator)
  • 二値分類なのに単峰の分布になっていたら、全然分類できていない(High Bayes error)

といった感じに診断していくというものです。

f:id:nigimitama:20211227224839p:plain

実験設計の重要性

例えば、「別の目的地をレコメンドする」という処置は、Booking.com上では「この人は目的地についてレコメンドしても邪魔にならない」と判断されたユーザーに対してしか行うことができません。そのため実験もそのようなユーザーにしか行うことができません。

このようにモデルに依存して処置が利用可能かどうかが決まる場合、処置群の一部の個体しか処置されないため、標準的なRCTの枠組みをそのまま適用すると、検出される効果を薄めてしまいます。

そこでBooking.comではTriggered AnalysisDeng & Hu, 2015)という改良版のRCTを利用しているそうです。これは処置群と対照群に割り振られた個体のうち、モデルの出力に照らして処置が利用可能だった個体のみを使用して実験を行うという枠組みです(Figure 8)。

f:id:nigimitama:20211227224852p:plain

さらに、3グループに分けて実験を行うことで、モデル導入時の効果を「latencyの増加による影響」と「モデル自体の純粋な効果」に分解できます。 2つに分けた処置群の両方ともモデルを動かすものの、結果を使うのはTreatment group 1にすることで、

  • ControlとTreatment 1の比較=モデル導入の全体的な効果
  • ControlとTreatment 2の比較=モデル導入のlatency増加による効果
  • Treatment 1とTreatment 2の比較=モデル自体の純粋な効果

という風に分解できるようになります。

f:id:nigimitama:20211227224901p:plain

新旧モデルの比較時も3群で実験を行い、両モデルで結果が異なった(Models Disagree)個体だけを分析に使うようにしているそうです。

f:id:nigimitama:20211227224920p:plain

まとめ

ざっくりいうと

  • 機械学習の活用はビジネス上の価値をもたらした
  • offline evaluationのmetricsの改善はビジネス上の利益とあまり相関しなかった
  • latency(待ち時間)が多いとビジネスに損害をもたらす。逆にlatencyを削減することで利益を増加させることができた。
  • 標準的なRCTが合わない状況もある。実験設計をしっかり行うことで「機械学習の導入の効果」から「導入によるlatency増加による負の効果」と「機械学習自体の純粋な効果」に分離することができた
  • 機械学習モデルの品質の監視のためのResponse Distribution Analysisという方法を考案し、活用した

という感じでした。

Online Evaluationの重要性と、その際の実験設計(latencyの効果とモデルの効果との分離など)の重要性がわかるよい論文でした。

参考

Deng, A., & Hu, V. (2015, February). Diluted treatment effect estimation for trigger analysis in online controlled experiments. In Proceedings of the Eighth ACM International Conference on Web Search and Data Mining (pp. 349-358). https://dl.acm.org/doi/abs/10.1145/2684822.2685307

『データ分析と意思決定理論』を読んだ

『データ分析と意思決定理論』を読みました。

この本は経済学者が書いた一般向け(非専門家向け)の本で、第1部がデータ分析、第2部が意思決定理論という構成になっています。

要点や印象的だった点などをメモしておきます。

第1部(データ分析)について

世の中のさまざまな分析・推論は信頼できない仮定に基づいている

  • 基本的に、分析で強い結論を得るためには強い仮定を置いて分析する必要がある
  • 多くの人は点推定のように強い結論を好むため、世の中の様々な分析は分析者の強い仮定の下で行われている
  • 仮定の置き方によって導かれる結論が変わることはよくあることなので(本書中で時々引用されるManskiの研究でもそういうものは少なくない)、どういう仮定を置いた分析なのかに注意する必要がある

RCTも仮定の下での分析

  • ランダム化比較試験(RCT)も(経済学にとっては)強い仮定の下の分析である
  • RCTには、「処置に対する反応は個人だけで、他のメンバーに影響を与えあうような社会的相互作用(social interaction)がない」とする仮定がある。
    • 例えば「職業訓練プログラム」を処置として扱うなら処置によって労働市場の状態が変わってしまうので仮定が満たされない
  • 実際のRCTはゴールドスタンダードと呼ばれるほどの理想とは程遠い。しかし、信頼できる区間予測を作ることはできる。

部分識別

  • 部分識別(partial identification)は効果の推定を点ではなく区間で行い、仮定を置かない状態から徐々に仮定を追加していって分析する。

f:id:nigimitama:20211226161905p:plain

f:id:nigimitama:20211226161911p:plain

(出所:奥村(2015)「部分識別とその応用: 処置効果を中心に」

第2部(意思決定理論)について

意思決定理論とは

  • 意思決定の価値(効用や厚生とよぶ)が部分的にしかわからない状況下で、意思決定者がどう振る舞うのが妥当なのかを考える分野。
    • もし厚生がすべて既知なら単にそれを最大化するようにすればよいので、不確実性が前提

用語

  • 意思決定の結果は、選択した行動と環境特性によって決まると定式化する。この環境の特性を自然状態と呼ぶ。
  • 意思決定者が起こりうると考える自然状態をすべて網羅するリストは状態空間と呼ばれ、部分的な知識を表す。
  • 行動選択から生じる結果の厚生を、行動C、自然状態sからW(C, s)と表す。これを厚生関数という。状態空間は意思決定者のもつ知識(情報)を表すのに対し、厚生関数は意思決定者の選好を表す。

意思決定基準

(1) 「支配される行動」を消去する

  • 行動CとDがあるとし、すべての自然状態のもとで W(C, s) ≧ W(D,s) のとき、DはCに支配される行動だと言われる。支配される行動は選ぶべきではない。

(2) 支配される行動以外の行動の選択肢が複数個残った場合:

  • 期待厚生基準:各自然状態が発生すると考える信念の強さ(意思決定者の主観確率)で重み付けして加重和(期待値)を計算し、その厚生の期待値が最も高くなる行動を選択するという基準。
  • マキシミン基準(maximin criterion):行動が生み出す厚生の最小値(各行動の最悪ケース)でその行動を評価し、その厚生の最小値のなかで最も大きい厚生を生む行動を選択する。
  • ミニマックス・リグレット基準(minimax regret criterion):リグレットとは各自然状態の下での最高と最悪の結果の差。起こりうるすべての自然状態にわたってその行動が生み出すリグレットの最大値を求め、その値でその行動を評価する。
  • (※常にこれを選べばいい、みたいな最高の基準はないので妥当な基準を選ぶ必要がある)

適応的分散:処置を順次施していく場合

  • いくつかの時点に分けて意思決定できる場合、対象の集団をいくつかのグループ(コホート)に分けて順次処置を施していき、前のコホートの結果に応じて後のコホートの意思決定の参考にすることができる
    • 適応的ミニマックス・リグレット(Adaptive Minimax Regret: AMR)基準:プランナーは処置の時点でわかっている処置反応のデータを使って、各コーホートミニマックス・リグレット基準を適用する。

集団による意思決定

  • 意思決定者が複数人からなり、個々人で意思決定基準が異なる場合はどうなるのか
  • 投票で意思決定する場合は社会選択理論の話になっていく(どういう選好を持つ人々がいるのかの前提によって結果は変わる。特定の前提の下での理論は例えばアローの不可能性定理、ブラックの中位投票者定理などがある)
  • 議会内での投票など、戦略的相互作用が存在する場合は、自分の選好通りに投票しないことが有利になることがあり得る
  • 投票ではなく二者間の交渉の場合、エッジワースボックスのパレート最適な配分に落ち着くことが考えられる

感想

  • 読んでいくなかで、Manskiは分析の仮定に非常に注意を払っている方だなというのが感じられて、科学者の態度として勉強になりました。
  • 部分識別の「仮定を置かない状態から徐々に強めていく」というアプローチは非常に良いと感じました。

  • 意思決定についてはバンディット問題みたいな感じもあり興味深いですね。

    • ただ、個人ならまだしも集団の意思決定となると参加者の選好も未知ですしなかなか複雑で分析も難しそうなチャレンジングな感じですね。
  • 一般向けの本ということで全体的に広く浅く書かれた本ですが、各分野に興味を持つきっかけになるという分にはいい本かなと思います。
    • 一つ文句をいうとすれば、この本は何故か縦書きになっていまして、縦書きに書かれた数式が読みにくくて仕方ありませんでした…

NGBoostの理論のまとめ

NGBoost: Natural Gradient Boosting for Probabilistic Predictionの論文を読んだのでメモしておきます。

ざっくり要約すると以下のような感じでした。

  • NGBoostは予測を点ではなく分布で予測するための機械学習アルゴリズム
  • 勾配ブースティングの枠組みで最尤推定する
  • 最適化にはニュートン法を改良したフィッシャーのスコア法をベースにしており、指数分布族以外の分布や特殊なパラメータを仮定する指数分布族でも使えるよう一般化している
  • 予測を点で推定した場合の精度はRandom Forestに匹敵するが通常の勾配ブースティングに劣るくらい

もう少し詳しい話は以下に述べていきます。

理論の概要

私の考察を交えつつ、NGBoostの理論の概要を述べます。

解きたいタスク

機械学習を使うタスクでは、予測の不確実性が評価できると嬉しい場合が結構あります。

例えば「この予測対象については訓練データにも類似する事例がないので、この予測値の信頼性は低いです」みたいな情報を予測器が述べてくれると、その予測結果を使う人間にとって便利だったりします。

そこで、予測を点ではなく分布で行いたいとします。

NGBoostでは扱う分布をパラメータで規定される分布のみに限定することで分布の推定問題をパラメータの推定問題へ緩和します。例えば予測分布が正規分布であると仮定する場合、平均μと分散σの2つのパラメータの推定問題になります。

ただし、このパラメータはxの関数です。通常の勾配ブースティングで誤差関数がMSEのときはxの条件付き平均E[y|x]を推定するわけですが、NGBoostではそれに加えてxの条件付き分散も求める、という感じです。

スコア関数

パラメータ推定のためには通常の機械学習アルゴリズムの誤差関数に相当するものを定義する必要があります。

ただし、今回は分布を推定したいので、真の分布との距離を測れるような関数を定義する必要があります。

具体的には対数尤度関数や、尤度を拡張したCRPSという関数がスコア関数にあたります。

自然勾配

勾配はパラメータを微小に変化させたときに目的関数を最も変化させる方向です。しかし、本当は分布を微小に変化させたときの目的関数を最も変化させる方向が知りたいため、NGBoostでは勾配をより一般化した自然勾配を使います。

自然勾配(natural gradient)はリーマン計量の逆行列を勾配に乗じたものです。

f:id:nigimitama:20210529145206p:plain

スコア関数が対数尤度の場合、このリーマン計量はフィッシャー情報行列(符号を反転させたヘッセ行列の期待値をとったもの)になります。

つまり、最尤推定法におけるフィッシャーのスコア法(Fisher's scoring method)に相当することを勾配ブースティングでやっているのだと思われます

ニュートン法との違い

フィッシャーのスコア法を使うメリットはなんなのか?という点について、NGBoostの論文では

確率分布が指数分布族のものであり、分布のパラメータ化をその族の自然なパラメータで行うとき、Newton-Raphson stepはnatural gradient descent stepと等しい。

という感じのことが書かれております。

NGBoostは一般化のために自然勾配を使っていますが、逆に言うと、指数分布族の分布で通常のパラメータを使うのであればニュートン法で十分なのかなと思います。

例えばXGBoostやLightGBMはニュートン法による勾配ブースティングを行っているはずなので、それらのアルゴリズムでも誤差関数を尤度関数にすればNGBoostと同じような結果が得られて分布での予測ができそうな感じがします。

実用に関する考察

NGBoostは通常の機械学習のように点推定で結果を出した場合の予測精度がscikit-learnの勾配ブースティングの実装にも劣るようなので、使用上はそこがネックになりそうな感じがします。「多少精度を落としてでも予測の不確実性が評価できるような予測を行いたい」というニーズがある状況では活きるかもしれません。

NGBoostはライブラリが公開されているのですが、XGBoostのように正則化をしたりといった精度向上のための細かなパラメータ調整ができなかったり、LightGBMのような計算の高速化がなくて大規模データへの対応がしにくかったりする欠点があります。これらの点については先述のようにLightGBMを使って最尤推定することで解決できるかもしれません。

参考

詳しい理論の解説はこちらのブログがわかりやすいかと思います。

NGBoostを読んで、実装する。 - nykergoto’s blog

pystanの環境構築で詰まったときのメモ

変なつまり方をしたのでメモ

まとめ

  • dockerのpython:3.9をベースイメージに環境を作ろうとしていたが、動かなくて困っていた
  • pystan 3.0 は gcc ≥ 9.0 が必要であるにも関わらず、python:3.9のイメージに入っているgccはver.8系だったのが原因と思われる
  • ubuntu:20.04をベースにしたら治った

症状

ドキュメントのQuick Startにあるコードを実行しようとしていたが、次のようなエラーが起こっていた

Building... This may take some time.
Messages from stanc:
Warning:
  The parameter mu has no priors.
Warning:
  The parameter tau has no priors.
Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/aiohttp/web_protocol.py", line 422, in _handle_request
    resp = await self._request_handler(request)
  File "/usr/local/lib/python3.9/site-packages/aiohttp/web_app.py", line 499, in _handle
    resp = await handler(request)
  File "/usr/local/lib/python3.9/site-packages/httpstan/views.py", line 253, in handle_show_params
    services_module = httpstan.models.import_services_extension_module(model_name)
  File "/usr/local/lib/python3.9/site-packages/httpstan/models.py", line 90, in import_services_extension_module
    module: ModuleType = importlib.util.module_from_spec(spec)  # type: ignore
  File "<frozen importlib._bootstrap>", line 565, in module_from_spec
  File "<frozen importlib._bootstrap_external>", line 1108, in create_module
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
ImportError: /root/.cache/httpstan/4.4.2/models/dnt4r2yw/stan_services_model_dnt4r2yw.cpython-39-x86_64-linux-gnu.so: undefined symbol: _ZNSt19basic_ostringstreamIcSt11char_traitsIcESaIcEEC1Ev
Traceback (most recent call last):
  File "/src/getting-started.py", line 27, in <module>
    posterior = stan.build(schools_code, data=schools_data, random_seed=1)
  File "/usr/local/lib/python3.9/site-packages/stan/model.py", line 450, in build
    return asyncio.run(go())
  File "/usr/local/lib/python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/usr/local/lib/python3.9/asyncio/base_events.py", line 642, in run_until_complete
    return future.result()
  File "/usr/local/lib/python3.9/site-packages/stan/model.py", line 439, in go
    raise RuntimeError(resp.json()["message"])
  File "/usr/local/lib/python3.9/site-packages/stan/common.py", line 24, in json
    return simdjson.loads(self.content)
  File "/usr/local/lib/python3.9/site-packages/simdjson/__init__.py", line 61, in loads
    return parser.parse(s, True)
ValueError: The JSON document has an improper structure: missing or superfluous commas, braces, missing keys, etc.

原因

おそらく、python:3.9のイメージに入っているgccが古すぎた

# gcc --version
gcc (Debian 8.3.0-6) 8.3.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

apt-get updateして新しいgccを入れようとしても「これが最新」と言われてしまい、このイメージでなんとかしようとするのは手間がかかりそうだった

対処

もっと新しいgccが入ったものを使うことにした

例えばubuntu 20.04はgcc 9.3.0がデフォルトで入っているのでこれを使うことにした

# gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

これにより問題なくpystanが動くようになった

[python]loggerの出力が重複するのを防ぐ

pythonのloggingを使うときのメモ

背景

loggerインスタンスにStreamHandlerやFileHandlerを設定する処理は使いまわしたいので、関数にしたい。

その際、単純にその処理を関数にまとめると、その関数が複数回呼ばれて同じ名前のloggerが複数回参照された場合はそのたびに毎回addHandlerStreamHandlerを設定してしまうため、ログ出力が重複する。

# 悪い例
import logging


def get_logger(name, level=logging.DEBUG):
    """loggerを設定する (悪い例)"""
    logger = logging.getLogger(name)
    logger.setLevel(level)
    # 毎回Handlerを設定してしまう場合
    ch = logging.StreamHandler()
    ch.setLevel(level)
    formatter = logging.Formatter(
        fmt="[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S"
    )
    ch.setFormatter(formatter)
    logger.addHandler(ch)
    return logger


class Horse:

    def __init__(self):
        self.logger = get_logger(self.__class__.__name__)

    def scream(self):
        self.logger.debug("ヒヒーン!")


if __name__ == "__main__":
    h1 = Horse()
    h2 = Horse()
    h2.scream()

この場合、2回__init__が呼び出されたために同じloggerに2つのStreamHandlerが設定されているため、次のように2つ重複してログが出力される

[2021-01-27 08:32:59] [Horse] [DEBUG] ヒヒーン!
[2021-01-27 08:32:59] [Horse] [DEBUG] ヒヒーン!

提案

そこでhasHandlersを使ってHandlerの存在を判定して、Handlerがなければ追加するようにすればよさそう。

import logging


def get_logger(name, level=logging.DEBUG):
    logger = logging.getLogger(name)
    logger.setLevel(level)
    if not logger.hasHandlers():
        ch = logging.StreamHandler()
        ch.setLevel(level)
        formatter = logging.Formatter(
            fmt="[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s",
            datefmt="%Y-%m-%d %H:%M:%S"
        )
        ch.setFormatter(formatter)
        logger.addHandler(ch)
    return logger


class Horse:

    def __init__(self):
        self.logger = get_logger(self.__class__.__name__)

    def scream(self):
        self.logger.debug("ヒヒーン!")


if __name__ == "__main__":
    h1 = Horse()
    h2 = Horse()
    h2.scream()
[2021-01-27 08:35:54] [Horse] [DEBUG] ヒヒーン!

このようにすれば重複したログ出力は避けられる。

LightGBMの理論のまとめ

今更ながらLightGBMの論文を読んだのでその時のメモを残しておきます。

GPUでの計算への適応など、計算機での活用に関する技術については省略しています。

要約

LightGBM

  • pre-pruning(決定木の枝をそれ以上分岐させても予測が改善しなくなったら分割を停止する剪定方法)
  • best-first(情報利得が最大の枝から順に伸ばす。pre-pruningが使える。)
  • histogram-based(連続値の特徴量をbinに離散化することで最適な分割点の探索の計算量を激減させる)

という決定木の高速化に関する既存の技術と、

  • Gradient-based One-Side Sampling(勾配の情報を活用して近似精度の良いサブサンプリングをする)
  • Exclusive Feature Bundling(one-hot encodingをしたように相互に排他的になっているスパースな特徴量をlabel encodingをした特徴量のような密な特徴量にまとめることで効率良く学習する)

というLightGBM自身が新たに提案した2つの技術を使った、

高速で精度の良い勾配ブースティング決定木。

LightGBMが使う既存の技術

pre-pruning

(この節とその次の節はほぼShi, 2007の序盤の説明をまとめたもの)

決定木のすべてのノードの不純度がゼロになるような完全に成長させきった状態にしてしまうと過学習の問題が発生する。そのため、剪定(pruning)を行ってほどよい木の大きさにする必要がある。pruningの方法は次の2種類に分けられる(Shi, 2007)。

  1. pre-pruning:木を拡張させることによる予測の改善がなくなったら(それ以上分割すると誤差が増えるなら)木の拡張を停止する。
  2. post-pruning:一度木を完全に成長させて、それから一番予測精度が良かった木のサイズを選択する。

pre-pruningのほうが無駄な計算を避けられる可能性があり効率的だが、本当に最適な木の大きさになる前に学習を停止させてしまう、「early stopping」という問題がある。

early stopping問題

決定木が各ノードで分割をする際は、一つの特徴量だけを使って情報利得を評価して分割する。そのため、複数の特徴量を組み合わせると予測精度が上がるような効果(交互作用効果)を評価することはできない。

以下の表はあるデータセットを表しており、a,bが特徴量でclassが目的変数である。a,bの両方を使うと正確に分類できるが、単体では予測に寄与しない。

このデータのように交互作用効果しかもたず単体では予測に寄与しない特徴量だけを含むデータセットでは、pre-pruningの決定木は分割を行わずに学習を終了してしまう。この問題があるためにpre-pruningはあまり評価されず、post-pruningの手法が開発された。

しかし、実世界のデータセットではこの問題はあまり起きないという報告もある。Frank(2000)やShi(2007)は数十個のオープンなデータセットを使ってpre-pruningとpost-pruningを比較し、pruningのパラメータ調整によっては両者のパフォーマンスに大きな差が生まれないことを示した。

(考察:これは私の仮説だが、parity problemのように「単体ではまったく予測に寄与しないが交互作用はある」というデータセットは実データでは稀ということかもしれない。ちなみにLightGBMはparity problemを解くことはできないが、aとbを掛け合わせた変数を追加してやれば解くことができるので特徴量の工夫で対応できないことはない)

best-first (leaf-wise) tree

標準的な決定木はdepth-firstという種類のアルゴリズムを使っているが、LightGBMが使っているのはbest-firstというタイプのものである。それぞれの特徴は次の通り。

  1. best-first:木を分岐していく各ステップで、すべての分割可能なノードの中でもっとも不純度を低下させるノードを分割する。
  2. depth-first:ノードの分割の順番は固定(通常は左の枝が先に分割される)

両者は木が最大に成長した時に得られる木は一緒だが、best-firstは最良のノードから分割していくので、pre-pruningを使うことができる。

histogram-based

GBDTの計算量の大部分は最適な分岐点の探索にある。これまでに研究されてきた分岐点の探索の高速化にはpre-sortedアルゴリズムとhistogram-basedアルゴリズムの2つがある。

  1. pre-sorted:事前に特徴量をソートしておき、すべての分割可能な点を評価して最適な分割点を探す。正確だが計算量が非常に多い。
  2. histogram-based:連続値の特徴量を離散化して少ない数(例えば256個とか)のbinにして、その中で最適な分割点を探す。binの数だけ評価すればいいので分割点の探索自体の計算量は非常に少ない。

histogram-basedアルゴリズムを使うほうが大幅に高速化ができるので、LightGBMはこちらをベースに研究している。

考察:予測精度は激減しないのか?

例えば100万レコードあって100万のユニークな値がある特徴量を256個のbinに離散化するようなことを考えると、値を減らしすぎではないか?予測精度も大きく悪化してしまうのではないか?と思ってしまう。

しかし、実証的には問題ないという報告がある。LightGBMが参照している先行研究のひとつであるLi et al.(2008)ではbinの数を65536個にしたときと256個にした時でtestデータに対するNDCGスコアの差はほとんど無かったと報告している。また、LightGBMの論文もXGBoostのexact greedy algorithm(pre-sorted algorithmのこと)を使ったモデルよりもよい精度を出している。

理論的に問題ないのかについての回答は、いまのところ私は見つけられていない。しかし、現在のところ私は2つの仮説を考えている。

  1. LightGBMのAlgorithm 1を見た感じだと、分割の探索のたびにそのノードに含まれるデータを使ってヒストグラムを作っていると思われる。そのため、1種類のbinsだけで学習するのではなく色々な分割の仕方をした多様なbinsを使って評価している。また、根ノードから伸びていった子孫のノードは含まれるサンプルサイズもある程度減っているため、最初の分割は大雑把でも続く分割は徐々に近似誤差が小さい分割ができており、仮に根ノードでの分割でbinsにまとめたことによる誤差が多かったとしても、子孫のノードでその誤差を補うことができているのかもしれない。
  2. そもそもブースティング自体が弱学習器をたくさん使って強い学習器を作る技術なので、個別の弱学習器が多少さらに弱くなったとしても、少し反復数を増やせばその損失を補えるのかもしれない。

LightGBMが新たに提案した技術

背景

histogram-basedのGBDTは、ヒストグラムの作成にO(#data × #feature)かかり、分岐点の探索にO(#bin × #feature)かかる。つまり、計算量の大部分はヒストグラムの作成が占めている。

そこでLightGBMでは、#dataを減らすためにGOSSという技術を導入し、#featureを減らすためにEFBという技術を使う。

Gradient-based One-Side Sampling(GOSS)

サンプルサイズを減らす際、もっともシンプルなのがランダムサンプリングすることである。Stochastic Gradient Boosting(Friedman, 2002)は決定木を作る際にランダムサンプリングした部分サンプルを使って学習していたが、ランダムサンプリングは近似精度がそんなに良いわけではないので、この方法は一般には予測精度を下げる。

AdaBoostをベースにしているならデータインスタンスに対する重みを持っているので、それを使って適応的にサンプリングすることができてより効果的だが、GBDTには明示的な重みはないので、この方法を直接適用することはできない。

GBDTの勾配はAdaBoostの重みに相当するもので、インスタンスの重要度の指標に使うことができる(勾配が小さい=訓練誤差が小さい=十分に予測できている=学習済み)。そこでGOSSは勾配の情報を使ってサンプリングを行う。

勾配が小さいサンプルをすべて削除するような処理にしてしまうとデータの分布が変化して予測精度に悪影響が出るため、GOSSは大きい勾配を持つインスタンスは残して、小さい勾配を持つインスタンスに対してはランダムサンプリングを行う

具体的には、GOSSはまずデータインスタンスを勾配の絶対値に従って並び替え、上位$a\times 100\%$のインスタンスを選ぶ。そして残りのデータから$b \times 100\%$のインスタンスをランダムにサンプリングする。その後、情報利得の計算の際にサンプリングされた小さい勾配のデータを定数$\frac{1-a}{b}$で増幅する(勾配が少ないインスタンスの勾配の総和がサンプリング前と同じになるように増やしている)。

これは多くの場合で近似精度がランダムサンプリングより優れている(詳しくは論文の3.2節の理論解析を参照)。

Exclusive Feature Bundling(EFB)

特徴量の次元数が大きいデータセットは、スパースであることが多い。スパースなデータは情報を損失することなく次元削減できる可能性がある。

具体的には、相互に排他的な(同時にゼロでない値をとることが無い)特徴量を1つの特徴量にまとめることで、安全に次元数を減らすことができる。

これは2つのステップからなり、まずGreedy Bundlingというアルゴリズムでは、値の衝突(conflict:同時に非ゼロの値をとること)の回数が一定の閾値以下であれば同じbundle(特徴量の束)に追加していく。つぎに、Merge Exclusive Featuresというアルゴリズムで、同じbundleに含まれる特徴量を、それぞれの値が取る範囲がかぶらないように範囲をシフトさせつつ一つの特徴量にまとめる。例えば、[0, 10)の値をとる特徴量と[0, 20)の値をとる特徴量がある場合、後者に10を足して[10, 30)にして両者をマージする。

実験結果

5つのオープンなデータセットで比較を行い、GOSSとEFBを使うことで6~21倍高速化していること、そんな高速化をしてもXGBoostでexact greedy algorithmに匹敵する(というか少し上回る)精度を出していることを示した。

また、SGBとGOSSの精度を同じサンプリング比率の下で比べた時は常にGOSSのほうが少し精度が高くなっており、「ランダムサンプリングよりも勾配の情報を使ってサンプリングしたほうがサンプリングの近似精度が良い」という理論と整合的な結果も出ている。

参考

Ke, G., Meng, Q., Finley, T., Wang, T., Chen, W., Ma, W., ... & Liu, T. Y. (2017). Lightgbm: A highly efficient gradient boosting decision tree. In Advances in neural information processing systems (pp. 3146-3154).

Friedman, J. H. (2002). Stochastic gradient boosting. Computational statistics & data analysis, 38(4), 367-378.

Frank, E. (2000). Pruning decision trees and lists (Doctoral dissertation, University of Waikato).

Shi, H. (2007). Best-first decision tree learning (Doctoral dissertation, The University of Waikato).

Li, P., Wu, Q., & Burges, C. J. (2008). Mcrank: Learning to rank using multiple classification and gradient boosting. In Advances in neural information processing systems (pp. 897-904).