勾配ブースティングで大事なパラメータの気持ち
LightGBM
や XGBoost
などで使われている勾配ブースティングのパラメータについて、チューニングノウハウというよりもそのパラメータがどういう意味を持っているのか、に焦点をあててまとめて見ました。
各ライブラリのパラメータすべては以下から確認できます。
NOTE: 以下では lightGBM のパラメータの名前で説明しています。微妙に名前が違うものがあるので適宜読み替えてください。
勾配ブースティングについてざっくりと
一般的な決定木では木はひとつだけで、その木に対してたくさんの分割ルールを適用していきます。
勾配ブースティング木では、木をたくさん作ります。たくさん作る代わりに、一つ一つの木の分割をざっくりとしたものにします。 そして作った木すべての予測の合計を使うことで、ひとつの木では表せないような複雑な予測を可能にしています。
もうちょっとくわしく: Gradient Boosted Tree (Xgboost) の取り扱い説明書
木のパラメータ
たくさん作られるそれぞれの木をどのように作成するか、に関する制約条件についてです。
はじめに木の大きさや分割方法に関するパラメータから説明します。
max_depth
: 各木の最大の深さです。たぶん木の構造パラメータの中で一番シビアに効いてくるパラメータだと思っています(あくまで筆者の感覚ですが)。普通 3 ~ 8 ぐらいを設定します。あまり大きい値を使用すると、ひとつの木がとても大きなものになってしまいオーバーフィッティングする可能性がありますから、あまりおおきい値は設定しないほうが良いでしょう。
木はたくさん作られるのでたとえ小さい値を指定したとしても、有用な分割方法であれば次の木が分割してくれるので、まあ問題はないという印象です。
max_leaves
: 各木の末端ノードの数です。ひとつの分割だけがあるとノードは2つになり、もう一回分割が起こるとノードは3になります。というふうに分割数 + 1 だけ末端ノードは生成されます。 このノード数は max_depth と深い関係があります。というのも max_depth を指定すると、最大の末端ノード数は2 ** max_depth
で制限されるためです。(二分分割が depth だけ起こるので)
この時2 ** max_depth
以上の数のmax_leaves
を指定すると depth の指定によるノード数の上限よりも大きくなるため無意味になってしまうことに注意してください。 (lightGBM だとエラーメッセージが出ます)
以上のような理由からint(.7 * max_depth ** 2)
などを指定して depth からくる最大ノード数の 7 割だけ分割させる、とかをやったりします。min_child_samples
: 末端ノードに含まれる最小のデータ数。これを下回るような分割ルールでは分割されなくなります。 例えば 40 に設定したとすると、新しい分割を行ったあとの集合にデータ数が 40 を下回るようなルールでは分割できません。 最小の数をどのぐらいにすればよいかはデータの数に依存する部分も大きいため, データ数が大きい時は少し大きい値にすることを検討してみてください。gamma
: 分割を増やす際の目的関数の減少量の下限値です。分割を行うと、よりデータに適合できるようになるためからなず目的関数のデータへの当てはまり部分は改善します。一方で適合し過ぎるとオーバーフィットとなります。(バイアスバリアンス)
gamma
はこの当てはまり改善に対して制限をかけることに相当します。カジュアルに言うと、めっちゃロスが下がるような明確な違いで分割してもいいけど、微妙にロスが下がるような分割はゆるさないよ、というようなイメージでしょうか。
以下は使用するデータを制限することでロバストなモデルを作成しよう、という狙いのためのパラメータです。
colsample_bytree
: 木を作成する際に使用する特徴量の数を選択する割合です。まあまあ大事です。 これが 1 以下のとき、各木を作成する際に使用する特徴量をこの確率を持って選び出し、選ばれた特徴量だけを使います。
カラム数がとても多い時なんかに小さめの値を設定していることが多い印象です。個人的には0.7
あるいは0.4
をつかうことが多いです。subsamples
: 使用するデータの選択割合です。 colsample とは反対にデータの選択を行います。
つぎに木を成長させるアルゴリズムについてです。
boosting
: boosting アルゴリズムです。このパラメータはlightGBM のみ使えます。 基本は勾配ブースティングをしたいのでデフォルトのgbdt
を使うと良いでしょう。 その他にも dropout の考え方を応用して新しい木を作る際の勾配/ヘシアンの計算に今まで作った木の一部を確率的に取り出して使うdart
や, random foreset (rf
) も指定できます。random forest は stacking をする際に勾配ブースティングのモデルと相性がいいことが経験的に知られているので, 一段目のモデルとして使うことは有効かもしれません。
目的関数のパラメータ
木構造の目的関数は一般的な機械学習モデルと同様にデータへのあてはまりと、正則化の項からできています。 正確には分割を作る際にできる2つのノードに対して割り当てる予測値の値を、正則化 (l1, l2) を使って 0 に近づけるような働きをします。
目的関数は objective
で定義します。
目的関数の選択方針
目的関数の選択はターゲット変数の分布に大きく依存します。
カテゴリ変数の予測の時は普通に logloss
を使えば事足りることが多いですが回帰問題の時はちょっと工夫すると良いモデルを作れることがあります。
以下でそれぞれ述べていきます。
普通の回帰問題であれば rmse
を使います。(デフォルトの値は rmse
です).
rmse
はノイズの分布に正規分布を仮定することになります。
例えば工場で特定の場所にドリルで穴を開ける作業があり、実際に開けられた穴の場所を予測するスクがあったとします。このときドリルの位置のズレは正規分布に従うことが想定されるため、目的関数として rmse
を使うのが良いでしょう。
一方で年収やある出来事が起こる間隔など裾の広い分布に対しては gamma
を使ったり, 目的変数をログ変換して擬似的に対数正規分布に対するあてはめに変えることを検討してください。これらの分布は正規分布にならないことが多いです。
また一定期間内に起こるランダムな現象のカウントを予測する場合には poisson
を使うことを検討してみてください。
例えばサッカーの試合のゴール数などがこれに相当します。
その他
その他の目的関数に関するパラメータは主に正則化に関するものを設定することが多いです。
reg_alpha
: L1 正則化に相当するものです。デフォルトでは 0.1 ぐらいを使うことが多いです。L1正則化にあたるのであまり大きな値を使用するとかなり重要な変数以外を無視するようなモデルになってしまうため, 精度を求めている場合あまり大きくしないほうが良いでしょう。(説明変数を削減したい場合にはこの限りではありません。大きなreg_alpha
を設定して feature_importance を見て余り使われていない変数を削除する、という方法は正当な方法です。)reg_lambda
: L2 正則化に相当するものです。デフォルトでは L1 と同様に 0.1 ぐらいを指定します。L1と違い大きな値を設定してもその変数が使われないということはありませんので、特徴量の数が多い時や、徐々に木を大きくしたいすぐにオーバーフィットするような問題に対して大きく取ることが多いです。
学習時のパラメータ
学習を行うときに validation データとともに渡すパラメータです。
learing_rate
: たくさん作った木を足し合わせるときに使う重み係数です。
これを大きくするとひとつひとつの木の予測を多く使うようになるため、一般に収束するまでの木の数n_estimators
は少なくなり学習にかかる時間は短くなります。一方で分割法が雑になるため、精度とのトレードオフとなります。eval_metric
: validation データを評価する評価基準です。何も指定しないとobjective
で指定したものが使われます。例えばrmse
ならば validation set に対してもrmse
を計算します。
目的と異なるへんな評価基準を入れるとearly_stopping_round
を指定している時に学習が収束する前に止まってしまうこともあるので注意してください。early_stopping_rounds
: 指定された回数木を作っても評価基準が改善されない時に学習を途中でやめるようになります。オーバーフィットを防ぐために指定します。verbose
: 指定された回数に一度コンソールにeval_metric
を表示します。チューニングしないでね。
チューニング
チューニングは optuna
を使うとらくちんにできるのでおすすめです。
いろいろな場所で使いまわせるよう、パラメータを生成するような関数だけ切り出して定義しておくと便利だと思います。以下はその実装例です。
def get_boosting_parameter_suggestions(trial: Trial) -> dict: """ Get parameter sample for Boosting (like XGBoost, LightGBM) Args: trial(trial.Trial): Returns: dict: parameter sample generated by trial object """ return { # L2 正則化 'reg_lambda': trial.suggest_loguniform('reg_lambda', 1e-3, 1e3), # L1 正則化 'reg_alpha': trial.suggest_loguniform('reg_alpha', 1e-3, 1e3), # 弱学習木ごとに使う特徴量の割合 # 0.5 だと全体のうち半分の特徴量を最初に選んで, その範囲内で木を成長させる 'colsample_bytree': trial.suggest_loguniform('colsample_bytree', .5, 1.), # 学習データ全体のうち使用する割合 # colsample とは反対に row 方向にサンプルする 'subsample': trial.suggest_loguniform('subsample', .5, 1.), # 木の最大の深さ # たとえば 5 の時各弱学習木のぶん機は最大でも5に制限される. 'max_depth': trial.suggest_int('max_depth', low=3, high=8), # 末端ノードに含まれる最小のサンプル数 # これを下回るような分割は作れなくなるため, 大きく設定するとより全体の傾向でしか分割ができなくなる # [NOTE]: 数であるのでデータセットの大きさ依存であることに注意 'min_child_weight': trial.suggest_uniform('min_child_weight', low=.5, high=40) }
文章の埋め込みモデル: Sparse Composite Document Vectors を読んで実装してみた
自然言語処理である単語の意味情報を数値化したいという場合に単語を特定のベクトルに埋め込む(分散表現)手法として word 2 vec があります。 この word2vec と同じような発想で文章自体をベクトル化するという発想があり Doc2Vec やそのたもろもろも方法が存在しています。 今回はその中の一つである SCDV (Sparse Composite Document Vector) を実装したのでその記録です。
そもそも何者か
文章を表現するベクトルを取得する手法です。
どうやってやるか
SCDV はいくつかのフェーズに分かれています。以下では5つのフェーズに分けて説明します。 若干論文の notation と違う所があるのでそこだけ注意していただければと思います。
1. 単語の分散表現を取得する
はじめに文章全体をつかって単語の分散表現を学習します。 Word2Vec や fasttext などが有名なところですね。
以下での話をわかりやすくするため、文章と単語に関してすこし数式を定義します. まず、文章中に現れるすべての単語の数を $N$ とおきます。 そして $i \in \{1, 2, ..., N\}$ 番目の単語に対して分散表現 $w_i \in {\mathbb R}^M$ を得たとします。ここで $M$ は埋め込みベクトルの次元数を表します.
2. 単語を更に複数のクラスタに分類する
1 で取得した単語ベクトルを更に $K$ 個のクラスタに分類します。 このときクラスタリングのモデルとして混合ガウス分布をつかいます。 これによって単語ごとに、先の埋め込みベクトルとは別のクラス分だけのベクトル $p_i \in \mathbb{R}^K$ をえることが出来ます。
混合ガウス分布の学習方法については論文中では特に言及はありませんでしたが、著者の実装では「各クラスタの共分散行列が同じである」という仮定のもとで推定を行うようになっていました。これはおそらく、各クラスタの領域を同じぐらいになるような制限をかけることで各単語の負担率 $p_i$ に極端な偏りがないようにするためなのかなーと考えています。単に計算量を減らすためかもなのであくまで推定ですが。
3. 埋め込み表現とクラス表現を掛けあわせた後に idf をかける
1 で得られた単語の分散表現に 2 で得られたクラスタへの割当を表すベクトルをおのおのかけて各単語ごとに $MK$ 次元のベクトルを作ります。 そして出来上がったベクトルに単語 $i$ の idf 特徴 ${\rm idf}_i \in \mathbb{R}$ を掛けあわせます ここで得られる新しい単語-クラスタ ベクトルを $u_i \in \mathbb{R}^{MK}$ とすると以下のようになります
$$ u_i = {\rm idf}_i \left( \begin{array}{c} p_{i 1} w_i \\ p_{i 2} w_i \\ \vdots \\ p_{i k} w_i \end{array} \right) $$
ここで idf を掛け算しているのは出現回数の多い単語の影響を低くしたいからです。 とくに Step2 でクラスタに分割して次元を多くしたので、出現回数の多い単語が大きい影響度を持つ次元の割合は増えることが予想されるためこの処理は必要なんだろうな、と僕は理解しています。*1
4. 文章ごとに単語クラスタベクトルを足しあわせて正規化する
ここではじめて文章という単位が登場します(ここまでの演算には文章が関係しません)。 $j$ 番目の文章に現れる単語を $L_j$ とし、全部で $J$ 個の文章があるとします。 先ほど得られた単語クラスタベクトルを文章ごとに足しあわせて文章ベクトル $v_j \in \mathbb{R}^{MK}$ を得ます
$$ v_j = \sum_{i \in L_j} u_i $$
さらにこれを各文章ごとに正規化します。論文には After normalizing the vector
としか書かれていませんでしたが実装を見るとユークリッドノルムの意味で1になるようにしていました。
それに習うと, 正規化後のベクトルを $\hat{v_j} \in \mathbb{R}^{MK}$ とすると
$$ \hat{v_j} = \frac{v_j}{\| v_j \|_2} $$
となります。
5. ゼロに近いものを 0 に押しつぶす
4 の正規化を終えた時点でほとんどの文章ベクトルの要素が 0 に近い値になっています。Step5 では容量の圧縮や意味の鮮明化の為、ゼロに近いもののうち特定の条件を満たすものをゼロとみなします。具体的には
$$ \hat{v_i} = \begin{cases} \hat{v_i} & {\rm if} | \hat{v_i} | \ge t \\ 0 & {\rm otherwise} \end{cases} $$
とします。ここで $t$ は
$$ \begin{aligned} t = \frac{p}{100} \times \frac{ | a_{{\rm min}} | + | a_{{\rm max}} | } {2} \\ a_{{\rm min}} = \frac{1}{J} \sum_{j = 1}^{J} {\rm min}\ \hat{v_j} \\ a_{{\rm max}}= \frac{1}{J}\sum_{j = 1}^{J} {\rm max}\ \hat{v_j} \end{aligned} $$
のように定義される値です。全体の最大最小の $p$ %よりも小さかったら 0 にしましょうということみたいですね。(何故これで良いのかは特に記述がありませんでした。流石にノリで決めては無いと思うんですがよくわかりません)。
どのぐらいいいの?
定量評価のため論文中では SDCV と他の特徴量を SVM で訓練した結果が乗っています。
これを見る限りわかりやすく一番つよいですね。
実装してみる
ここまで読んで楽しそうだったので自分でも実装をやってみました。
使うのはライブドアニュースのデータセットです。 docker-compose を使うと以下のように環境が作れます。
cp project.env .env docker-compose build docker-compose up -d docker exec -it scdv-jupyter bash
SCDV の作成
作成は src/create.py
で行います。著者実装のコードでは単語の分散表現もスクラッチで学習させていましたが今回は学習済みの fasttext 特徴量を用いています。
学習済みモデルは https://qiita.com/Hironsan/items/513b9f93752ecee9e670 よりお借りしました。感謝!!
def main(): args = vars(get_arguments()) word_vec = ja_word_vector() output_dir = os.path.join(setting.PROCESSED_ROOT) n_wv_embed = word_vec.vector_size n_components = args['components'] docs, _ = livedoor_news() parsed_docs = create_parsed_document(docs) # w2v model と corpus の語彙集合を作成 vocab_model = set(k for k in word_vec.vocab.keys()) vocab_docs = set([w for doc in parsed_docs for w in doc]) out_of_vocabs = len(vocab_docs) - len(vocab_docs & vocab_model) print('out of vocabs: {out_of_vocabs}'.format(**locals())) # 使う文章に入っているものだけ学習させるため共通集合を取得してその word vector を GMM の入力にする use_words = list(vocab_docs & vocab_model) # 使う単語分だけ word vector を取得. よって shape = (n_vocabs, n_wv_embed,) use_word_vectors = np.array([word_vec[w] for w in use_words]) # 公式実装: https://github.com/dheeraj7596/SCDV/blob/master/20news/SCDV.py#L32 により tied で学習 # 共分散行列全部推定する必要が有るほど低次元ではないという判断? # -> 多分各クラスの分散を共通化することで各クラスに所属するデータ数を揃えたいとうのがお気持ちっぽい clf = GaussianMixture(n_components=n_components, covariance_type='tied', verbose=2) clf.fit(use_word_vectors) # word probs は各単語のクラスタへの割当確率なので shape = (n_vocabs, n_components,) word_probs = clf.predict_proba(use_word_vectors) # 単語ごとにクラスタへの割当確率を wv に対して掛け算する # shape = (n_vocabs, n_components, n_wv_embed) になる word_cluster_vector = use_word_vectors[:, None, :] * word_probs[:, :, None] # はじめに文章全体の idf を作成した後, use_word だけの df と left join して # 使用している単語の idf を取得 df_use = pd.DataFrame() df_use['word'] = use_words df_idf = create_idf_dataframe(parsed_docs) df_use = pd.merge(df_use, df_idf, on='word', how='left') idf = df_use['idf'].values # topic vector を計算するときに concatenation するとあるが # 単に 二次元のベクトルに変形して各 vocab に対して idf をかければ OK topic_vector = word_cluster_vector.reshape(-1, n_components * n_wv_embed) * idf[:, None] # nanで影響が出ないように 0 で埋める topic_vector[np.isnan(topic_vector)] = 0 word_to_topic = dict(zip(use_words, topic_vector)) np.save(os.path.join(output_dir, 'word_topic_vector.npy'), topic_vector) topic_vector = np.load(os.path.join(output_dir, 'word_topic_vector.npy')) n_embedding = topic_vector.shape[1] cdv_vector = create_document_vector(parsed_docs, word_to_topic, n_embedding) np.save(os.path.join(output_dir, 'raw_document_vector.npy'), cdv_vector) compressed = compress_document_vector(cdv_vector) np.save(os.path.join(output_dir, 'compressed_document_vector.npy'), compressed)
実験
著者の実験では SVM を使っていました。
普段モデリングするときには勾配ブースティング or Neural Network を使うことがおおいので、今回は lightGBM をつかってベンチマークを実装しました。
(実行するスクリプトは src/SCDV_vs_SWEM.py
にあります。)
タスクはライブドアニュースの文章から、そのカテゴリを予測する問題です。 カテゴリは全部で8個あり文章数はおおよそ7000程度です。
戦わせる特徴量は個人的に押しの論文 Baseline Needs More Love: On Simple Word-Embedding-Based Models and Associated Pooling Mechanisms で提案されている Simple Word Embedding Model (SWEM) をつかいます。
SWEM の文章 $k$ の特徴量 $z^k \in \mathbb{R}^{M}$ は以下で表されます
$$ z_j^{k} = \max_{i \in L_k} \ w_{ij}. $$
ここで $w_{ij}$ は単語 $i$ の $j$ 番目の要素であり $L_k$ は $k$ 番目の文章に含まれる単語の添字集合です. 要するに文章中の単語に対して埋め込み次元方向に max をとったものです。 得られる文章ベクトルは単語のものと同じ $M$ 次元になります。
これの n-gram version である SWEM-hier なども提案されていて文章分類などの単純なタスクにおいては CNN や LSTM をつかったリッチなモデルと匹敵、場合によっては勝つ場合もある、単純ですがあまり侮れない特徴量であったりします。
モデルはLightGBM で k=5 の Stratified Fold を行い accuracy により評価します。
結果と感想
Out of Fold の Accuracy を特徴量ごとにプロットしたのが以下のグラフです。
SCDV の圧縮を行わないバージョンがもっともよいスコアになりました。CV全てで二番目となった SWEM の max-pooling version よりも良い値となりこの特徴量の強さが伺えます。 一方 PCA で圧縮したものはあまりワークしませんでした。これはせっかく混合ガウス分布まで持ちだして拡張したことによって得られる微妙な差分が PCA によって押しつぶされてしまったことが原因と考えられます。
また SCDV は学習がおおげさになるというのも浮き彫りになりました。というのも特徴量の次元が $MK$ なので fasttext の埋め込み次元が 300, 混合ガウス分布の次元が 60 なので各文章ごとに 1800 次元もあるのです。
ですので今回のタスクのような小さい文章セット (1万以下) であっても numpy でなにも考えずに保存すると約 7GB 程度になります。また学習も SWEM に比べて体感 10 ~ 20 倍の時間がかかりました。 以上のことからメモリや計算資源に余裕が有る場合に SCDV を使いさくっとそれなりのベンチマークがほしい時は SWEM という使い分けも良いかも知れません。
まとめ
- SCDV では単語情報を $M$ 次元ベクトルから更に $K$ 次元の情報を引き出して $MK$ 次元に拡張する。そこから先は普通(たして正規化)
- livedoor ニュースのデータ・セットを用いた実験では SWEM よりも精度がよかった。が時間とメモリは要る。
*1:SWEMでは特に出現回数に関する考慮はなく精度が出ているため次元拡張の影響を打ち消すという意味合いが強いのかも
Pandas で Index を dictionary で更新したい
pandas のデータフレームで以下のようなものが有るとします。
In [1]: import pandas as pd In [2]: import numpy as np In [3]: df_train = pd.DataFrame(data=np.random.uniform(size=(3, 2)), index=['one ...: ', 'two', 'three']) In [4]: df_train Out[4]: 0 1 one 0.289881 0.649603 two 0.229427 0.811377 three 0.498204 0.779105
でこの index を例えば日本語の "いち", "に", "さん"
になおしたいなという時。愚かな方法だと index.str.replace
につらつらと書いていく方法があります.
df_train.index = df_train.index.str.replace('one', 'いち').str.replace('two', 'に')
でもなんかださいですね。dict で対応関係を記述して, それを元に変換するというふうにコードの責任分離をやりたいところです。
調べた所 pandas.DataFrame.rename
を index にたいして使えとありました。
In [5]: en_ja_map = dict(one='いち', two='に', three='さん') In [6]: df_train = df_train.rename(en_ja_map, axis='index') In [7]: df_train Out[7]: 0 1 いち 0.289881 0.649603 に 0.229427 0.811377 さん 0.498204 0.779105
おしゃれでいいですね;)
さんこう
敵対的サンプリング検出のための基準としての相互情報量 - Understanding Measures of Uncertainty for Adversarial Example Detection
Understanding Measures of Uncertainty for Adversarial Example Detection
https://arxiv.org/pdf/1803.08533.pdf
概要 (200文字程度)
敵対的サンプルを判別する基準として相互情報量 (Mutual Information) が優れていることを主張する論文. MI の推定に Dropout を用いた MC を採用している.
Mnist と隠れ層2次元 Variational Autoencoder を用いて不確実性の分布を可視化し既存の基準である softmax entropy と MI の違いをわかりやすく説明している.
この論文の新規性
予測の不確実性を意味によって2つに分離し、モデルの知識不足部分の不確実性を取り出せれば敵対的サンプルを判別できるという主張を情報量の観点から議論している点.
不確実性の基準
そもそも不確実な入力には2つの種類がある
- 知識不足による認知の不確実性
- データの偶然性による不確実性
機械学習の文脈で言うと、前者はデータが欠乏しているために、その入力データに対する事後分布が平らになってしまうような現象を指す。
後者はそもそも入力データに対して出力のばらつきが激しくて予測が無理な場合を指す。 例えば表も裏も確率 1/2 で出るコイン投げの予測モデルなどは、知識ではなくデータの生成過程自体が完全にランダム性に支配されている為に起る不確実である。
一般に用いられる不確実性を測る基準として、予測値と入力のエントロピーがある。
$$ H[p(y | x)] = - \sum_{y \in Y} p(y| x) \ln p(y | x) $$
ここで $Y$ はモデルの予測値が取る離散的な空間を表している。 この基準は先の2つの不確実性を分離していない為、不確実性がモデルの知識不足から来るのか、それともデータの分散が激しいことから来るのかに関して判断することはできない。
そこでデータ $D$ と入力 $x$ が与えられた時のモデルのパラメータ $w$ とその予測値 $y$ の相互情報量 (Mutual Information) を考える。これは以下の式で表される。
$$ I(w; y | x, D) = H[p(y|x, D)] - {\mathbb E}_{p(w|D)}[H[p(y|w,x)]] $$
A, B の相互情報量とは B の情報を得た時に減少するAの不確かさのこと、と解釈することができる(A, B は逆でも成り立つ)。
今回でいうと、ラベル $y$ が与えられた時に減る $w$ に関する不確かさである。これは即ち $w$ に関して新しく得られた情報量とみなすことができる。
相互情報量が大きいような $x$ はどういう値かということを考えてみる。 相互情報量が大きいとは上記の議論より、得られる情報量が大きいということであるから、そのラベルの値 $y$ の価値が大きい(ひとつの $y$ を得るだけでとても大きな情報(知識)を得ることができる)。一方で相互情報量が小さいというのはラベルの価値が小さい、すなわち既にその入力 $x$ に対する $w$ の不確かさが十分に小さく、ラベルを与えられてもあまり情報が増えないということを表している。
よってこの相互情報量は、先の「不確実性の種類」で言うところの知識不足による不確実性(1) に相当する量である、とみなすことができる。
この情報量は直接的に計算できないが Dropout を用いた MC をすることで近似的に求めることが可能である。具体的にはラベルの事後分布を
$$ p(y|D,x) \simeq \frac{1}{T} \sum_{i=1}^T p(y|w_i, x) := p_{MC} (y|x) $$
の用に近似することで以下のように計算することができる
$$ I(w; y | x, D) \simeq H[p_{MC}(y | D, x)] - \frac{1}{T}\sum_{i=1}^T H[ p(y|w_i, x)] $$
不確実性の基準による違い
相互情報量であれ予測値のエントロピーであれ、入力画像 $x$ の分布(多様体)から著しく離れた $\hat{x}$ が与えられると大きな値を取る。 一方で予測値のエントロピーは入力画像 $x$ の分布(多様体) の近くであっても大きな値を取る場合がある。これはそういう画像が本質的に曖昧なもの, すなわちラベルがつけにくいものであるからである。
例えば人間が見ても 1 とも 7 とも見えるような画像があるとする。これは本質的に 1 or 7 が決定的に決められない曖昧な画像であるから予測値の softmax の値は複数のラベル(今回であれば 1 と 7) の確率が高い予測となる。エントロピーはひとつのラベルにピークを持つものほど小さくなる性質を持つので1、このような一つに決められないものに対してはエントロピーは大きくなる。 一方でMIは低い値を保つ(MIは (2) を測るものであるからである)。
さらに、敵対的サンプルとは元画像の持っているクラスとは別のクラスを持つ画像を作る行為であり、普通元のクラスの予測値を小さくし、高くしたいクラスの確率を大きくするように画像を修正する。
これは予測値のエントロピーを小さくするという効果もあるが MI によって測ることができる不確実性にも影響を与える、というのもエントロピーの下界が MI に相当するので、エントロピー最小化を行ってもMIより小さい値になることは無いからである。
MI と softmax の分散
softmax の分散と MI のテイラー展開した第一項は一致するので softmax の分散は MI の近似とみなすことができる。(詳細は論文参照のこと)
実験の対象と結果
MNIST に対して latend dim = 2 の VAE を用いて潜在空間の不確実性を可視化する実験と、犬と猫の分類問題に対する敵対的サンプルを entropy と MI とで予測しその不確実性の違いの特徴を調べる実験を行っている
MI と entropy の曖昧さの捉え方の違い
潜在空間で内挿したあと decoder を通して画像にしたものと、入力画像自体を内挿した画像とを用意してそれぞれ entropy と MI の値がどうなるかを表したのが以下の Figure2. である。
上段の上の画像が潜在空間の内挿の画像で下が実空間上での内挿の画像を表している。
中段がそれらの entroy の値であり、下段が MI の値である。これを見ると entropy では潜在空間 (Latent Sapce) の内挿画像 (図の赤線) にたいしてピークを持っているのに対して MI ではピークがないことがわかる。これは潜在空間で内挿された画像は「不確実であるが本質的に曖昧で予測が曖昧なのは当然」な画像であるためデータの持つ偶然性も図ってしまう entropy は大きくなるが、モデルの知識に関する不確実性のみを測る MI ではそういう画像は曖昧で当然であるという判断になり小さいままなためである。
一方入力画像自体を内挿した場合 (Image Space) は entroy, MI ともにピークを持つ。これはそういう画像は訓練データに存在しておらず「この画像に対する知識は無い(言い換えるとモデルパラメータの分布の裾が広い)」ため、ラベルが与えられた時に得られる情報が大きいからである。
MNIST with VAE
隠れ層が2次元の VAE を用いて潜在空間上の点とその点での不確実性を可視化して、基準による違いを見る。 やり方は
- 潜在空間上で適当に点を取る
- decoder に通して画像空間上に射影して対応する画像を作成
- 作成した画像の不確実性を計算
- 以上を空間すべての点で計算して画像化する
という流れになっている。 fig1 が MI, fig4 が entropy の値を可視化したもの。どちらも白いところが不確実性が高い場所で黒くなるに連れて低い値となるようにプロットしている。色の付いている部分は MNIST の各クラスの画像の潜在空間上での値を表す。
共通する傾向として中心から遠い(画像の多様体から遠い)場所では白い(=不確実性が高い)。
一方でよーく見てみると fig4 の方は色のついたクラスの間のところが白くなっているのがわかる。これは2つのクラスの間の画像で fig2 の上段の上の画像に相当するもので「数字っぽいけど本質的に曖昧でなんとも言えない画像」に対応している。 エントロピーだと不確実性を区別しないので、クラスの間は不確実性が高くなるというわけである。
もうひとつ気になるのは Dropout で不確実性をどの程度禁じできているのか、という点である。
これに関しては Dropout による変分推論は局所的なモードしか捉えられない為に事後分布全体の近似としては上手く働いていない。2 このため decoder によって生成された画像が意味をなしていないような場合でも高い信頼度を出してしまうような領域(holes
と作者は呼んでいる)が出てしまう (figure5 がその例。 figure4 でも2時の方向に黒い holes
が確認できる。)。3
このような穴があるがゆえ、敵対的サンプルはこの脆弱性をついて攻撃することが可能である。
そこで事後分布をちゃんと表現できればこの弱点を緩和できるのでは?という観点に基づいて修正を行う。 具体的には Dropout モデルをいくつも作ってアンサンブルする。こうすることで事後分布の mode の一つしか表現できないという弱点を解消できる。4
それをやったのが以下の figure6 で figure4 と比べるとだいぶ穴が減っているのがわかる
犬猫分類
ASSIR cat and dogs データセットを用いて敵対的サンプルがどうかの判別を行い基準ごとの性質を確認する
使用するモデル
Imagenet で訓練済みの ResNet の Convolution 層を固定し Full Connection レイヤのみを fine-tuning したモデルをつかう。 Dropout は FC 層のみに適用し、 MC は 20 サンプルを抽出する。
確定的な予測は Dropout を使わないだけで重みの値は全く同じネットワークを使用する。
敵対的サンプルのアルゴリズム
- Basic Iterative Method
- Fast Gradient Method
- Momentum Iterative Method
の3つを使う。
結果と考察
各種の敵対的サンプリング作成法によって作られた画像の分類精度を AUC で測ったのが以下の表である。
DETERMINISTIC と MC の違いは予測時にも dropout を使って重みの事後分布の期待値として予測をしているかどうか、である(MCがテスト時にもdropoutを行う)。 相互情報量は dropout による近似を行わないと計算不能なため、DETERMINISTIC では N.A と記述されている。
結果を見ると MI が良い性能を出しておりまたエントロピーでは 0.50 を下回っている。 0.5 を下回る AUC は ランダムに判定するよりも性能が悪い ことになる。これは敵対的サンプルに対して過度に信頼度をおいていることを示している。
一方犬猫判定の精度においては、MCを用いたモデルの精度よりも確定的な値を用いたモデルの精度のほうが良いという結果になった。 これは Dropout の確率が高すぎたために畳み込み層の特徴の一部しか使えずに転移学習の足を引っ張る形になってしまうのが原因と考えられるので、 dropout は精度に関しては良い影響は与えられない。
議論と結論
- 敵対的サンプルに対して制約を与えずにただ不確実性だけを見れば自動的に良いロバスト性を得られることを示したのは進展である。
- Dropout だけが敵対的サンプルに対抗する手段であるとは思っていない。ただ確定的なモデルよりも攻撃するのが難しくなっただけ。
- 隠れ層の可視化でわかったことは敵対的サンプルに対抗するためには第一にモデルのロバスト性を高めて不確実性を扱えるようにすれば、結果として騙されにくいモデルが出来上がる、ということ。
読んでみた感想
VAE を用いて潜在空間を可視化し、既存の基準と提案基準の違いをわかりやすく表現していて読みやすかった。 数式が出てくる論文は気分が良い。
次に読むべき論文は
敵対的サンプル系の論文読んだことないので読みたいね、ということで実験で使っていた手法一覧です。
ADVERSARIAL EXAMPLES IN THE PHYSICAL WORLD
- basic iterative method による敵対的サンプルの生成論文
EXPLAINING AND HARNESSING ADVERSARIAL EXAMPLES
- fase gradient method による敵対的サンプルの生成論文
Boosting Adversarial Attacks with Momentum
- momentum iterative method による敵対的サンプルの生成論文