Kaggle Google QUEST Q&Aコンペに参加した記録
Kaggle Google QUEST Q&Aコンペに参加して61位(1571チーム中)でソロ銀メダルを取れました。
以下ではその記録についてまとめます。
本記事の概要
- コンペ概要
- 私の取り組み
- 興味深かった上位ソリューション
- 感想
コンペ概要
Google QUEST Q&A Labeling | Kaggle
タスク概要
- QAサイトの質問・回答に対して人が主観的にどう感じたかを予測するタスク。後述の入力データから予測対象データを予測する。
- QAサイトの質問・回答に対して人が主観的にどう感じたかを予測するタスク。後述の入力データから予測対象データを予測する。
データ
- 入力データ:stackoverflowなどのQAサイトの質問タイトル、質問文、回答文、カテゴリ、等
- 予測対象データ:質問・回答に関する人の主観的な30個の項目について、0~1の得点が与えられる。例えば、answer_helpfulという項目は回答がhelpfulであるかどうかを表しており、得点0に近ければhelpfulではなく、1に近ければhelpfulである。得点はアノテータが付けたものらしい。
- 入力データ:stackoverflowなどのQAサイトの質問タイトル、質問文、回答文、カテゴリ、等
予測性能の評価方法
スタンダードな解法
- 予測モデル:Deep learningベースの自然言語処理モデル(Bertなど)を学習させ、入力(質問タイトル、質問文、回答文)から30項目の予測対象を回帰で推定。
- 後処理:評価指標値を上げるために、予測モデルの出力を離散値とするような後処理を実施。正解値は0~1の範囲の離散値(例:0, 0.333, 0.666, ...,1.0)をとっていたため、予測値も離散値とすることでスピアマン順位相関係数が高くなる。
- 予測モデル:Deep learningベースの自然言語処理モデル(Bertなど)を学習させ、入力(質問タイトル、質問文、回答文)から30項目の予測対象を回帰で推定。
こちらのブログでわかりやすくまとめてありました。
Kaggle Google QUEST Q&A コンペ 振り返り - 機械学習 Memo φ(・ω・ )
私の解法
私の解法は以下のとおりです。流れは上記のスタンダードな解法と同じです。
コード
github.com概要
使ったライブラリ
- pytorch:kerasから乗り換えました。モデル構造や学習スキームをいじりたい凝り性の人にはpytorchおすすめ!自由度高くて使いやすい。あとgradient accumulate使いやすいのほんと良い。
- hugging face:DL系の自然言語処理ライブラリ。初めて使ったけど、かなり使いやすい。
- pytorch:kerasから乗り換えました。モデル構造や学習スキームをいじりたい凝り性の人にはpytorchおすすめ!自由度高くて使いやすい。あとgradient accumulate使いやすいのほんと良い。
モデル構造
データ前処理
- 入力:特に特別な処理をしていない。
- 出力:予測対象の値の偏りが大きかったので、ランク値にして最大ランクで割ることでスケーリングした。
- 入力:特に特別な処理をしていない。
モデルの学習
- 3epoch(最初の1epochはwarmup)。
- 学習率は1e-4。Bertは5e-5~2e-5が推奨されているらしいですが、1e-4でうまくいった。
- 損失関数は、回帰ではBinary cross entropy、分類ではcategorical cross entropy。label smoothingも使った。
- 3epoch(最初の1epochはwarmup)。
後処理
うまくいかなかったこと
- pair wiseのランク学習やってみたけど効果なかった。list wiseのランク学習であるListNetをやりたかったけど、メモリが足りなくて断念。バッチサイズをある程度大きくとれないと厳しい。ただ、ランク学習自体は面白い分野だと思うので今後活用していきたい。ランク学習に関して知りたい人は以下のサイトがおすすめ。かなり参考になりました。
ランク学習(Learning to Rank) Advent Calendar 2018 - Adventar - 初手でAlbertを試したけどまったく精度でなかった。精度出なさ過ぎて自分のコードのどこかにバグがあるのか探し回ってしまったぐらい精度が出なかった。
- Stochastic Weight Averagingは効果がなかった。でもいつか使いどころが出てくる気がする。
- pair wiseのランク学習やってみたけど効果なかった。list wiseのランク学習であるListNetをやりたかったけど、メモリが足りなくて断念。バッチサイズをある程度大きくとれないと厳しい。ただ、ランク学習自体は面白い分野だと思うので今後活用していきたい。ランク学習に関して知りたい人は以下のサイトがおすすめ。かなり参考になりました。
興味深かった上位のソリューション
入力の工夫
モデル構造の工夫
- 最期の全結合層の入力として、Bertの最終層の出力だけでなく、中間層の出力も使っていた。具体的には、中間層Aの出力([CLS])、中間層Bの出力([CLS])、…、最終層([CLS])の出力を重み付き平均し、それを全結合層の入力としていた。重み付き平均の重みは学習パラメータとし、合計が1になるようにしていた。この発想めっちゃよさげ。どこかで使いたい。
- 全結合層を2つ重ねていた。僕はBertは全結合層1つでいいものだという思い込みがあって、そこの工夫をまったくしていなかった。。
- 最期の全結合層の入力として、Bertの最終層の出力だけでなく、中間層の出力も使っていた。具体的には、中間層Aの出力([CLS])、中間層Bの出力([CLS])、…、最終層([CLS])の出力を重み付き平均し、それを全結合層の入力としていた。重み付き平均の重みは学習パラメータとし、合計が1になるようにしていた。この発想めっちゃよさげ。どこかで使いたい。
学習の工夫
- Bertの部分と全結合層で学習率を変えていた。
- Bertの部分と全結合層で学習率を変えていた。
感想
【論文メモ】Rethinking Normalization and Elimination Singularity in Neural Networks
「Rethinking Normalization and Elimination Singularity in Neural Networks」をざっと読んだのでそのメモです。
arxiv.org
github.com
論文の概要
- 画像認識に使うDeep neural network (DNN)の構造お話。
- Batch normalization (BN)はDNNでとてもよく使われる正則化層であるが、バッチサイズが小さいときに性能が落ちる。この論文ではBNの代替としてBatch-Channel Normalization (BCN)を提案している。BCNはBNより性能がよく、小さいバッチサイズでも使える。
- 検証ではBCN+Weight Standalization (WS)※がBNやGroup normalization (GN)+WSよりも良い精度を出していた。
- BCNの導出前の考察として、なぜGNやLayer Normalization (LN)がBNに劣るか、なぜWSが効くかについて、Reluによるsingularity発生という観点から述べられている。
※WS : 最近話題のBiT-Lでも使われているホットな奴です。
[1903.10520] Weight Standardization
Batch-Channel Normalization
BCNはバッチサイズが大きいときと小さいときで処理が異なる。
バッチサイズが大きいとき
入力をXとすると、BCNの出力BCN(X)は次式で表される。
要するに、BNしてからGNしているだけ。簡単。
一見冗長であるが非線形性が増したりするので意味があると論文では言及されてる。
バッチサイズが小さいとき
入力をXとすると、BCNの出力BCN(X)は次式で表される。
さっきと同じように見えるが、このBN'は普通のBNの以下の点が異なる。
普通のBN
BN'
検証結果
画像認識ではCifar10, Cifar100, ImageNet、物体検出ではCOCO、セグメンテーションではPASCALで検証されています。
BCN+WSが強いですね。物体検出とセグメンテーションではBNと比較していないのが残念。
感想
Kaggleの雲コンペ(Understanding Clouds from Satellite Images)についての記録
Kaggleの雲コンペに参加したので、実施事項や思ったことを記録しておきます。
1. 雲コンペ
衛星写真にうつった雲を種類ごとに検出して分類するタスクについて、その精度を競います。
Understanding Clouds from Satellite Images | Kaggle
特徴は以下のとおりです。
- 検出する雲の種類は4種類(fish, flower, gravel, sugar)。
- 学習用データとして衛星画像、画像中の雲の位置・種類が与えられます。テストデータとして衛星画像のみが与えられるので、雲の位置・種類を推定する必要があります。
- いわゆるSegmentationタスクにあたり、Deep learningの得意領域です。
わかりやすく説明してくれているブログがあったのでリンクを貼っておきます。
Kaggle 雲コンペ 反省録 - 機械学習 Memo φ(・ω・ )
2. 自分の解法
167位/1538チームという結果でした。
SegmentationモデルとしてはDeeplab v3+ (MobileNetV2バックボーン)を使いました。これを選んだ理由は、モデルサイズが小さい割に精度が良かったからです(PCのスペックがしょぼくて大きいモデル使えない。。)。resnet系バックボーンのUNETはGPUのメモリが足りなくてまともに使えなかったけど、Deeplab v3+ (MobileNetV2バックボーン)は使える程度に動いてくれました。ネットワーク構造も色々工夫されてて個人的に好みです笑
概要とコードは以下のとおりです。
GitHub - statsu1990/kaggle_understanding_clouds: Kaggle competition Understanding Clouds from Satellite Images
Solution overview
- ranking : 167th / 1538 teams
- score : Public 0.65691 / Private 0.65430
- method : segmentation using deep learning
- segmentation model : deeplab v3+ (backbone MobileNetV2)
https://github.com/bonlime/keras-deeplab-v3-plus - optimizer : RAdam
https://pypi.org/project/keras-rectified-adam/ - loss : binary cross entropy + dice loss
- data augmentation : h and v flip, ShiftScaleRotate, RandomBrightness
https://github.com/albumentations-team/albumentations - test time augmentation : h and v flip, h shift x2, v shift x2, rotation 180
https://github.com/qubvel/tta_wrapper - ensemble : 6 model
- segmentation model : deeplab v3+ (backbone MobileNetV2)
3. 上位の解法
全員Deep learning系のSegmentationモデルを使っていました。具体的にはUnet, FPNが多い。上位勢は当然のごとく大きなモデル使ってますね。
https://www.kaggle.com/c/understanding_cloud_organization/discussion/118080
https://www.kaggle.com/c/understanding_cloud_organization/discussion/118255
https://www.kaggle.com/c/understanding_cloud_organization/discussion/118016
気になった解法は以下とおりです。
雲の位置を検出するように事前学習
画像の白い部分(≒雲)を検出するようにモデルを事前学習した後に、指定の雲のセグメンテーションモデルを学習させる。たしかにimagenetとかの事前学習モデルを使うより良さそうですね。
https://www.kaggle.com/c/understanding_cloud_organization/discussion/118017衛星写真に写った明るい部分(太陽光?)を補正
衛星画像には太陽光?由来の明るい帯のような部分があります。本来の背景色を推定し、この部分を補正していました。すごい
https://www.kaggle.com/c/understanding_cloud_organization/discussion/118018
4. 思ったこと
Kaggleのくずし字認識(Kuzushiji recognition)コンペで15位になった
Kaggleのくずし字認識(Kuzushiji recognition)コンペで15位/293チームになりました。
https://www.kaggle.com/c/kuzushiji-recognition/leaderboard
コードをのっけておきます。
github.com
以下、記録です。
1. 記事の概要
- くずし字認識(Kuzushiji recognition)コンペについて
- 私の解法
- 感想と反省
1. くずし字認識(Kuzushiji recognition)コンペについて
今回のコンペは以下のような内容でした。
- 目的:文書画像から崩し字の位置を検出し、崩し字の種類を分類する。
- 与えられるデータ:崩し字が書かれた文書画像(3881枚)、崩し字の位置を示すバウンディングボックス、字の種類。
Kuzushiji Recognition | Kaggle
崩し字認識の難しい点は以下のとおりです。
- 画像内に文字が多数ある(0~614個)。
- 文字の大きさがばらばら。小さいもの、大きいもの、細長いもの。
- 文字の種類が多数(3422種)あり、出現回数の偏りが大きい(出現数1~24,685個)。まさに不均衡データ。
2. 私の解法
私の解法の概要は以下のとおりです。
- Centernet(HourglassNetバックボーン)で文字を検出
- Resnet baseなモデルで文字を分類
※Centernetについては原論文と神Notebookを参照してください。
[1904.07850] Objects as Points
CenterNet -Keypoint Detector- | Kaggle
最終的なプライベートリーダーボードのスコアは0.900でした。
Preprocessing
- to gray scale
- gaussian filter
- gamma correction
- ben's preprocessing
Detection
inference
2段階のcenternetを使って、以下の手順で文字のバウンディングボックスを検出します。
- ステップ1:画像を512x512にリサイズして、centernet1でバウンディングボックス1を推定。
- ステップ2:バウンディングボックス1を使って一番外側のバウンディングボックスの外側を除去。
- ステップ3:画像を512x512にリサイズして、centernet2でバウンディングボックス2を推定。
- ステップ4:バウンディングボックス1と2をアンサンブルし、最終的なバウンディングボックスを作成。
モデルアーキテクチャ
- centernet1は2つのcenternet(1スタックのhourglassnetベース)のアンサンブル
- centernet2は2つのcenternet(1スタックのhourglassnetベース)のアンサンブル
training
centernet1については以下のとおり。
- 学習データ:全データの80%を使用。(データの分割を乱数で変えて2つのモデルを作成)
- データ拡張:水平移動、輝度調整 データ拡張は過学習防止のために必須でした。
centernet2については以下のとおり。
- 学習データ:全データの80%を使用。(データの分割を乱数で変えて2つのモデルを作成)
- データ拡張:Random erasing、水平移動、輝度調整 バウンディングボックスの外側を除去した画像を入力とするため水平移動のデータ拡張の効果が薄かったため、Randome erasingが必須でした。
Classification
inference
3つのResnet baseのアンサンブルモデルを使って、以下の手順で文字ラベルを分類します。
- ステップ1:推定したバウンディングボックスを使って元の画像から文字画像をクロップし、64x64にリサイズ。
- ステップ2:3つのResnet baseのモデルで文字ラベルを分類する(テスト時augmentationは水平移動9種類)。
- ステップ3:3つのモデルの分類結果をアンサンブルし、最終的な分類結果を推定。
モデルアーキテクチャ
- Resnet base1:最終出力層の前にlog(バウンディングボックスのアスペクト比)を入力する。
- Resnet base2:Resnet base1と学習データを変えたもの。
- Resnet base3:上述のDetectionモデル、Resnet base1と2のアンサンブルモデルからpseudoラベリングした入力を学習データに加えたもの。構造はResnet base1と同じ。
training
上述のとおり学習データを変えていることとpseudoラベリングを使っていること以外は各モデルで同じ。
- 学習データ:全データの80%を使用。
- データ拡張:水平移動、回転、ズーム、Random erasing
3. 感想と反省
- 物体検出を初めてやったのですが、画像認識より応用よりでなかなか面白かったです。
- 自分なりにpipelineを組んで管理や組合せ変更をしやすいようにしてみました。使いやすかったし、作業時間を減らせたのでこれからも続けたい。
- 自宅のGTX1080でDLの学習をすべて実施したのですが、学習時間と推論時間が長すぎて試行回数を増やせなかったです。GPU増設するか。。画像処理のCPUの部分もボトルネックにならないようにもっと気を付けて実装する必要あったなーと思います。
- けっこう自前主義で、もろもろを自分で実装することが多かったのだが、公開されているものをもっと積極的に使うべきでした。自分の練習にはいいけど、本質にかける時間が足りなくなる。
- pytorchの方が色々なモデルが転がっている気がする。kerasから乗り換えるか。。
KerasでのDrop-Activationの実装と検証
KerasでDrop-Activationを実装し、性能を検証したのでその記録です。
以下の検証に関するコードはgithubにあげてあります。
github.com
1. 本記事の概要
- Deep learningの正則化手法であるDrop-Activationの概要説明
- KerasでのDrop-Activationの実装
- Cifar10でのDrop-Activationの検証
2. Drop-Activationの概要
Drop-ActivationはDeep learningの正則化手法です。原論文はこちらです。
[1811.05850] Drop-Activation: Implicit Parameter Reduction and Harmonic Regularization
Deep learningの正則化手法として有名な手法としてDropoutがあります。Dropoutは過学習対策に有効ですが、Batch Normalizationと一緒に使用するとDLの性能が悪化することが知られています。
Drop-ActivationはDropoutと似たような概念でありながら、Batch Normalizationと共存できる手法です。
Drop-Activationの理論を以下に示します。わりと簡単です。
- 前提
- 活性化関数f、活性化関数への入力x、活性化関数の出力f(x)
- 活性化関数を非活性にする確率p (0<=p<=1)
- 活性化関数f、活性化関数への入力x、活性化関数の出力f(x)
- 学習時
- 確率pに従い、活性化関数の活性or非活性を決め、活性化関数の出力を計算する。
- 活性:活性化関数の出力をf(x)とする。
- 非活性:活性化関数の出力をxとする。(活性化関数をただの線形関数にする)
- 活性:活性化関数の出力をf(x)とする。
- 確率pに従い、活性化関数の活性or非活性を決め、活性化関数の出力を計算する。
- テスト時
- 活性化関数の出力を px+(1-p)f(x) で計算する。
- 活性化関数の出力を px+(1-p)f(x) で計算する。
原論文での精度検証結果を以下に引用します。Baselineの活性化関数はReluです。Randomized Reluとの比較や、CutoutやAutoAugを使ったときの検証がされています。なかなかいいですね。
3. 実装
Kerasの自作レイヤーを作る機能を使って、Drop-Activationレイヤーを作成しました。
オリジナルのKerasレイヤーを作成する - Keras Documentation
学習時に一定の確率で非活性にする部分には既存のdropoutを活用しました。
こちらの実装も参考にしました。
GitHub - JGuillaumin/drop_activation_tf: DropActivation implementation for TensorFlow and Keras
""" Reference: drop-activation original paper : https://arxiv.org/abs/1811.05850 implementation in keras by JGuillaumin : https://github.com/JGuillaumin/drop_activation_tf """ from keras import backend as K from keras.engine.topology import Layer import tensorflow as tf class DropActivation(Layer): def __init__(self, rate=0.05, seed=None, activation_func=None, **kwargs): """ Args: rate : drop rate, seed : random seed, activation_func : function of activation function. use K.relu if is None. """ super(DropActivation, self).__init__(**kwargs) self.supports_masking = True self.RATE = rate self.SEED = seed self.ACTIVATION_FUNC = activation_func if activation_func is not None else K.relu return def call(self, inputs, training=None): if training is None: training = K.learning_phase() def oup_tensor_in_training(): # input tensor inp_tensor = tf.convert_to_tensor(inputs) # 0:drop, 1:retain mask = K.ones_like(inp_tensor, dtype=K.floatx()) mask = K.dropout(mask, level=self.RATE, seed=self.SEED) # output tensor masked = (1.0 - mask) * inp_tensor not_masked = mask * self.ACTIVATION_FUNC(inp_tensor) oup_tensor = masked + not_masked return oup_tensor def oup_tensor_in_test(): # input tensor inp_tensor = tf.convert_to_tensor(inputs) # mask = expectation value of retain rate mask = 1.0 - self.RATE # output tensor masked = (1.0 - mask) * inp_tensor not_masked = mask * self.ACTIVATION_FUNC(inp_tensor) oup_tensor = masked + not_masked return oup_tensor outputs = K.in_train_phase(oup_tensor_in_training, oup_tensor_in_test, training) return outputs def compute_output_shape(self, input_shape): return input_shape def get_config(self): config = { 'rate': self.RATE, 'seed': self.SEED, 'activation_func': self.ACTIVATION_FUNC, } base_config = super(DropActivation, self).get_config() return dict(list(base_config.items()) + list(config.items()))
4. 検証
Drop-Activationの検証をしました。検証内容は以下のとおりです。
- 目的
- Drop-Activationを適用した場合の精度を検証する。
- Drop-Activationを適用した場合の精度を検証する。
- 対象問題
- cifar10で10クラスの画像を分類する。
- cifar10で10クラスの画像を分類する。
- 画像分類モデル
- ResNet20v1 (Kerasのドキュメントで使われている構造, CIFAR-10 ResNet - Keras Documentation)
- 基本のデータ拡張(水平フリップ、画像シフト)
- ResNet20v1 (Kerasのドキュメントで使われている構造, CIFAR-10 ResNet - Keras Documentation)
- Drop-Activationの適用条件
- 基本の活性化関数: Relu
- ResNet20v1のReluすべてにDrop-Activationを適用
- 基本の活性化関数: Relu
- 比較条件
結果のまとめを以下に示します。各1回しか検証していないので、ばらつきを考慮できていないことに注意してください。
これらの結果から以下のことがわかりました。
- Drop-Activationを適用することで精度(val acc)は必ずしも良くなるわけではない。
- val lossはいずれも良くなっている。
原論文や実装の参考にしたgithubではいい結果がでているので、もっと大きなモデルだと効果が大きいのかもしれません。
GitHub - JGuillaumin/drop_activation_tf: DropActivation implementation for TensorFlow and Keras
実装も簡単なので、構造の選択肢の一つとして組み込むことは有りだと思いました。
droprate | cutout | random erasing | mixup | train acc | train loss | val acc | val loss |
---|---|---|---|---|---|---|---|
0 | - | - | - | 0.991 | 0.151 | 0.9 | 0.518 |
0.05 | - | - | - | 0.977 | 0.179 | 0.902 | 0.476 |
0.1 | - | - | - | 0.971 | 0.196 | 0.899 | 0.497 |
0 | use | - | - | 0.963 | 0.229 | 0.904 | 0.453 |
0.05 | use | - | - | 0.951 | 0.257 | 0.906 | 0.442 |
0 | - | use | - | 0.954 | 0.253 | 0.902 | 0.45 |
0.05 | - | use | - | 0.937 | 0.292 | 0.906 | 0.428 |
0 | - | - | use | 0.827 | 1.093 | 0.909 | 0.418 |
0.05 | - | - | use | 0.817 | 1.117 | 0.903 | 0.39 |
0 | use | - | use | 0.812 | 1.135 | 0.904 | 0.422 |
0.05 | use | - | use | 0.799 | 1.159 | 0.903 | 0.402 |
0 | - | use | use | 0.797 | 1.155 | 0.905 | 0.425 |
0.05 | - | use | use | 0.786 | 1.187 | 0.901 | 0.403 |
5. まとめ
- Deep learningの正則化手法であるDrop-Activationの概要を説明しました。
- Drop-ActivationをKerasで実装しました。
- cifar10でDrop-Activationの検証をしました。
- 精度が必ずしも良くなるわけではありませんでした。
- lossは必ず小さくなりました。
- 精度が必ずしも良くなるわけではありませんでした。
- 実装が簡単なので、構造の検討時にお試しで使ってみるのは良いと思います。
Deep EnsemblesでDeep Learningの不確かさを評価する
前回の記事に引き続き、Deep learningの不確かさ評価についてです。
今回は、「Simple and Scalable Predictive Uncertainty Estimation using Deep Ensembles」という論文で紹介されているアンサンブルで不確かさを評価する手法の検証を実施しました。
検証コードはgithubgithubにあげてあります。
github.com
以下、要点のみメモ。
アンサンブル学習での不確かさ評価
論文及び参考サイト
- 論文
[1612.01474] Simple and Scalable Predictive Uncertainty Estimation using Deep Ensembles - 論文の概要説明(日本語)
私たちが愛した3つのNIPS論文 | techcareer magazine | エンジニアの転職(キャリア)情報満載のWebメディア
手法
- 目的
- DLの推定値の不確かさを定量評価する。
- DLの推定値の不確かさを定量評価する。
- 手法の概要
- 通常のDLでは推定値を出力する。例えば、入力情報から土地価格を推定したければDLが土地価格を出力するようにネットワークを作る。
- 提案されているDLでは、推定値の平均と分散(または推定値の確率分布のパラメータ)を出力するようにネットワークを作る。最小化する損失関数は、推定値の確率分布の対数尤度である。※損失関数は推定値、分散、推定値の正解データから成り、分散の正解データは不要。
- ブーストラップサンプリングで元データセットからM個のデータセットを作成し、それぞれのデータセットを使ってM個のネットワークを学習する。
- M個のネットワークの推定値の平均値を最終的な推定値とする。
- M個のネットワークの混合分布の分散を最終的な分散とする。
- 通常のDLでは推定値を出力する。例えば、入力情報から土地価格を推定したければDLが土地価格を出力するようにネットワークを作る。
kerasでの実装例
ラベル0 or 1の二値分類を対象としたネットワークを作った実装例を紹介します。
問題設定は以下のとおりです。
- ネットワークの推定値μは、ラベルxの平均値であり0から1の間の値。
- ラベルxの確率分布が、定義範囲を限定した正規分布p(x : μ, ) (0≦x≦1)に従うと仮定する。このもネットワークで出力する。
- 損失関数は、定義範囲を限定した正規分布p(x : μ, ) (0≦x≦1)の対数尤度関数。
kerasでこれらを実装するためには、(1)出力が2種類のネットワーク作成、(2)自作の損失関数の使用、が必要となります。以下、実装例です。
(1) 出力が2種類のネットワーク
推定値μはsigmoid関数、はsoftplus関数で計算します。が0に近づきすぎると数値的に不安定になるため、1e-6を足しています。
def built_model(self, input_shape=None): """ model input: image shape(32, 32, 3) model output: probability of being label1, uncertainty score. Range is [0,1] and [0,1], respectively. """ # constants if input_shape is None: # assume cifar10 image input_shape = (32, 32, 3) # model structure input_img = Input(input_shape) h = input_img h = Conv2D(32, (3, 3), padding='same')(h) h = BatchNormalization()(h) h = Activation('relu')(h) h = Conv2D(32, (3, 3))(h) h = BatchNormalization()(h) h = Activation('relu')(h) h = MaxPooling2D(pool_size=(2, 2))(h) h = Conv2D(64, (3, 3), padding='same')(h) h = BatchNormalization()(h) h = Activation('relu')(h) h = Conv2D(64, (3, 3))(h) h = BatchNormalization()(h) h = Activation('relu')(h) h = MaxPooling2D(pool_size=(2, 2))(h) oup_cnn = Flatten()(h) oup_cnn = Dense(32)(oup_cnn) oup_cnn = BatchNormalization()(oup_cnn) oup_cnn = Activation('relu')(oup_cnn) # expec h_expec = Dense(1)(oup_cnn) h_expec = Activation('sigmoid')(h_expec) # var h_var = Dense(1)(oup_cnn) h_var = Activation('softplus')(h_var) h_var = Lambda(lambda x: x + 1e-6, output_shape=(1,))(h_var) oup = Concatenate(axis=-1)([h_expec, h_var]) # model self.model = Model(inputs=input_img, outputs=oup) self.model.summary() return
(2) 自作の損失関数の使用
損失関数を定義します。ここでは定義範囲を区切った正規分布を使っているので、規格化係数([0, 1]の範囲での積分値を1にする)の計算があってごちゃごちゃしています。また、が大きい値を取りやすかったので正則化項loss_reg_varを加えています。
def part_norm_dist_log_likelihood(self, y_true, y_pred): """ expec = y_pred[:,0] var = y_pred[:,1] -ln(L) = ln(I) + 0.5 * ln(var) + 0.5 * (x - expec)^2 / var """ expec = y_pred[:,0:1] var = y_pred[:,1:2] loss_var = 0.5 * K.log(var) loss_l2 = 0.5 * K.square(y_true - expec) / var I = 0.5 * (tf.math.erf((1.0 - expec) / K.sqrt(2.0 * var)) - tf.math.erf((0.0 - expec) / K.sqrt(2.0 * var))) loss_I = K.log(I) loss_reg_var = K.sqrt(var) * 16.0 # regularization of var loss = loss_I + loss_var + loss_l2 + loss_reg_var return loss
モデルの損失関数として自作損失関数を以下の方法で設定します。
self.model.compile(loss=self.part_norm_dist_log_likelihood, optimizer='nadam', metrics=['accuracy'])
検証
cifar10の犬猫画像の二値分類を対象として、上記の手法で不確かさ評価を実施してみました。
検証の設定は以下のとおりです。
- 対象は犬猫画像の二値分類問題。
- 画像データとしてcifar10を使用。
- ラベルは犬が1、猫が0。
- 上記手法で推定値の不確かさを評価。
- 推定値、分散、損失関数の設定は上述の「kerasでの実装例」のとおり。
- アンサンブルの数は10個。
- 学習に使っていないラベル(飛行機、自動車、鳥、鹿、カエル、馬、船、トラック)についても評価を実施。
- 推定値、分散、損失関数の設定は上述の「kerasでの実装例」のとおり。
※以下のグラフのuncertainty coefは、アンサンブルで求めた標準偏差です。
犬猫分類の不確かさ評価結果
犬猫の分類結果を以下に示します。推定値が1のとき犬、0のとき猫です。
推定値が0.5に近いほど推定値の標準偏差(不確かさ)が大きい傾向がわかります。
テストデータのROC曲線を下図に示します。
「threshold=y」ラベルは、犬か猫か判定する閾値を単純に推定値としてROC曲線を描いたものです。推定値>閾値であれば犬と判定されます。
「threshold=0.5+a * std」ラベルは、閾値を0.5+astd (-∞<a<∞)としてROC曲線を描いたものです。推定値>0.5+astdであれば犬と判定されます。推定値が0.5に対して不確かさを含めてどの程度余裕があるかを表しています。
結果を見ると…ほぼ同じですね。
学習に使っていないラベルでの不確かさ評価結果
学習に使っていないラベル(飛行機、自動車、鳥、鹿、カエル、馬、船、トラック)についても不確かさ評価を実施しました。
推定値の標準偏差のヒストグラム、推定値の標準偏差に対する推定値のグラフを以下に示します。
0.5付近の推定値が増えるので、ヒストグラムのピークとなる標準偏差の値が大きくなったことがわかります。しかし、標準偏差vs推定値の分布は犬猫画像と変わりなく見えます。学習に使っていないラベルは学習データのデータ分布外なので、推定値が0や1に近くても標準偏差が大きくなるかと思っていたのですが。これはDropoutを使った不確かさ評価と同じ結果ですね。
以上
Dropoutによる近似ベイズ推論でDeep Learningの不確かさを評価する
Deep learningの推定結果の不確かさってどうやって評価するのか疑問を持っていました。
Dropoutを使ったサンプリングをすることで不確かさ評価をできるということなので、kerasで実装して検証してみました。
以下の検証に関するコードはgithubにあげてあります。
github.com
1. 本記事の概要
- 記事の目的
- Dropoutによるベイズ推論と不確かさ評価の概要
- 不確かさ評価の検証
- まとめ
2. 記事の目的
- Dropoutを使ったDLの不確かさ評価の概要をつかむ。
- kerasでの実装方法の記録。
3. Dropoutによるベイズ推論と不確かさ評価の概要
Dropoutによるベイズ推論
Deep Learning(以降DL)を使うことで回帰問題や分類問題の答えを精度よく推定することができます。しかし、精度が良いと言っても線形回帰等と同じく、その推定結果はかならず不確かさを含みます。
ベイズ推論を使えば推定結果がどのようにばらつくのかがわかるので不確かさを評価できるのですが、DLの式は複雑なのでベイズ推論を適用することは難しかったとか。(詳しくはよく理解していないです笑)
以下の論文では、DLの過学習を防ぐ手法であるDropoutを使って推論(推定)を行うことが近似的にベイズ推論になっていることが証明されています。
https://arxiv.org/pdf/1506.02142.pdf
(こういう機械学習の手法が統計的・数学的にこんな意味をもっているんだぜーってやつ、めっちゃわくわくしますよね。)
詳しくは以下の日本語ブログでも詳細されています。
ニューラルネットへのベイズ推定 - Bayesian Neural Network - nykergoto’s blog
Dropoutによる近似ベイズ推論について – 戦略コンサルで働くデータサイエンティストのブログ
僕が理解した範囲で、できるだけ平易な言葉でイメージを表現しておきます。
- ベイズ推論の枠組みをDLに適用する場合、DLの各重みは一定値でなく分布を持つと考えます。これにより推定結果も分布(不確かさ)を持ちます。
- 各重みの分布から各重みを何回もサンプリングし、何回も推定を行うことで推定結果の分布を求めることができます。しかし、そもそも各重みの分布を求めるのはすごくしんどい。。
- Dropoutは各重みをランダムに0にする効果があります。通常はDropoutは学習時にしか使いませんが、Dropoutを推論時にも使うことで各重みの分布からサンプリングしていることになります。
- そのため、Dropoutを使って推定を何回も繰り返すことで、推定結果の分布(不確かさ)を求めることができます。
不確かさ評価手順
Dropoutを使うことで、以下の手順で不確かさを評価できます。
- Dropout、L2正則化を使ってDLを学習させます。
- 同じ入力xに対して複数回DLの推定を行います。このとき、学習時と同じ率のDropoutを使います。
- 推定した結果をヒストグラムにする、分散をもとめる、エントロピーを求めるなどして不確かさを評価します。ヒストグラムの裾が広いほど、分散が大きいほど、エントロピーが大きいほど不確かさが大きいと言えます。
原論文では上記のようにDropoutを使いながらサンプリングすることをモンテカルロドロップアウト(MC dropout)と呼んでいます。
分散は、「普通の分散」+「ドロップアウト率、L2正則化係数、サンプリング数等に関連した量」で求められます。詳しくは原論文に書かれています。
この不確かさ評価は、通常のDLの学習・推論とやっていることは大差ありません。異なるところは、推論時にもDropoutを使う、同じ入力に対して複数回推定を行う、というところぐらいです。そのため、keras等の既存のDLライブラリを使うことができるのでお手軽です。
ただし、複数回推定を行わないといけないので、推定に時間がかかります。
4. 不確かさ評価の検証
ここでは、DropoutによるDLの不確かさ評価の検証をしてみます。
ゴールは、不確かさ評価のイメージをつかむことです。
検証内容
検証の設定は以下のとおりです。二値分類問題で不確かさがどのようなふるまいをするか確認します。
- 対象は犬猫画像の二値分類問題。
- 画像データとしてcifar10を使用。
- モデルにはCNNを使用。構造は、入力画像→畳み込み層→…→全結合層→Dropout→全結合層→Dropout→...→出力(sigmoid)
- 犬を1、猫を0とラベルをつけてCNNを通常どおり学習させます。できあがったCNNは犬である確率を出力します。
- 画像データとしてcifar10を使用。
- 学習済みCNNで推論時にもDropoutを使い、推定値の標準偏差(原論文で提案された分散ではなく、普通の方法で算出)を計算します。また、推定値の平均も計算します。
実装
実装にはpython, kerasを使用しました。
学習は通常どおり行います。
import keras from keras.preprocessing.image import ImageDataGenerator from keras.models import Sequential from keras.layers import Dense, Dropout, Activation, Flatten, BatchNormalization from keras.layers import Conv2D, MaxPooling2D from keras import regularizers import numpy as np class ClassifierCnn: """ classify dog and cat in cifar10 """ def __init__(self): return def built_model(self, dropout_rate, l2, input_shape=None): # constants self.DROPOUT_RATE = dropout_rate //0.5 self.L2 = l2 //0.0001 if input_shape is None: # assume cifar10 image input_shape = (32, 32, 3) # model structure model = Sequential() model.add(Conv2D(32, (3, 3), padding='same', kernel_regularizer=regularizers.l2(self.L2), input_shape=input_shape)) model.add(Activation('relu')) model.add(Conv2D(32, (3, 3), kernel_regularizer=regularizers.l2(self.L2))) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Conv2D(64, (3, 3), padding='same', kernel_regularizer=regularizers.l2(self.L2))) model.add(Activation('relu')) model.add(Conv2D(64, (3, 3), kernel_regularizer=regularizers.l2(self.L2))) model.add(Activation('relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Flatten()) model.add(Dense(512, kernel_regularizer=regularizers.l2(self.L2))) model.add(Activation('relu')) model.add(Dropout(self.DROPOUT_RATE)) model.add(Dense(64, kernel_regularizer=regularizers.l2(self.L2))) model.add(Activation('relu')) model.add(Dropout(self.DROPOUT_RATE)) model.add(Dense(1, kernel_regularizer=regularizers.l2(self.L2))) model.add(Activation('sigmoid')) self.model = model self.model.summary() return def train_model(self, x_train, y_train, x_test, y_test, epochs, batch_size): # compile self.model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) # datagen datagen = ImageDataGenerator( featurewise_center=False, # set input mean to 0 over the dataset samplewise_center=False, # set each sample mean to 0 featurewise_std_normalization=False, # divide inputs by std of the dataset samplewise_std_normalization=False, # divide each input by its std zca_whitening=False, # apply ZCA whitening zca_epsilon=1e-06, # epsilon for ZCA whitening rotation_range=20, # randomly rotate images in the range (degrees, 0 to 180) # randomly shift images horizontally (fraction of total width) width_shift_range=0.1, # randomly shift images vertically (fraction of total height) height_shift_range=0.1, shear_range=0., # set range for random shear zoom_range=0.1, # set range for random zoom channel_shift_range=0., # set range for random channel shifts # set mode for filling points outside the input boundaries fill_mode='nearest', cval=0., # value used for fill_mode = "constant" horizontal_flip=True, # randomly flip images vertical_flip=False, # randomly flip images # set rescaling factor (applied before any other transformation) rescale=None, # set function that will be applied on each input preprocessing_function=None, # image data format, either "channels_first" or "channels_last" data_format=None, # fraction of images reserved for validation (strictly between 0 and 1) validation_split=0.0) datagen.fit(x_train) # fit self.model.fit_generator(datagen.flow(x_train, y_train, batch_size=batch_size), epochs=epochs, steps_per_epoch=int(x_train.shape[0] / batch_size), validation_data=(x_test, y_test), )
推論時にもDropoutを使うため、読み込んだモデルのDropout層をDropout()(training=True)と入れ替えています。
※参考:【Keras】 Kerasで訓練時だけじゃなく予測時にもドロップアウトを使う - Qiita
こうすることで推論時にもDropoutが使われるため、複数回model.predictを実行することでモンテカルロドロップアウトサンプリングができます。
from keras.layers import Dropout from keras.models import Model class MontecarloDropout: def __init__(self): return def build_model(self, model_file_path): """ keras modelからMontecarloDropoutに対応したモデルを作成 build monte carlo dropout model base on keras_model. """ model = self.__load_model(model_file_path) # change dropout layer to dropout layer that can use dropout in inference. # ドロップアウト層を推論時にもドロップアウトできるドロップアウト層に変更する。 for ily, layer in enumerate(model.layers): # input layer if ily == 0: input = layer.input h = input # is dropout layer ? if 'dropout' in layer.name: # change dropout layer h = Dropout(layer.rate)(h, training=True) else: h = layer(h) self.model = Model(input, h) return def md_predict(self, xs, sampling_num): """ predict with using monte carlo dropout sampling. return prediction average, std xs : input sample array. xs = x0, x1, x2, ... """ pre_ys = [] for ismp in range(sampling_num): pre_y = self.model.predict(xs) pre_ys.append(pre_y) pre_ys = np.array(pre_ys) # calculate ave, std pre_ave = np.average(pre_ys, axis=0) pre_std = np.std(pre_ys, axis=0) return pre_ave, pre_std
他の部分はgithubを参照してください。
github.com
検証結果
作成したモデルの精度
モデルの精度(犬猫分類の正解率)は以下のとおりになりました。MC Dropoutはアンサンブルのような効果があるので精度が良くなるかと思っていましたが、大差ないですね。
- CNN: 学習データ 92.3%, テストデータ 86.3%
- MC Dropout: 学習データ 92.2%, テストデータ 86.3%
※MC Dropoutの推定値は、入力1つに対して100回モンテカルロドロップアウトして得られた100個の推定値を平均したものです。
犬猫画像の分類結果と不確かさ
学習に使った犬猫画像の分類結果とMC Dropoutで求めた不確かさ(標準偏差)の関係をみていきます。
推定値の標準偏差に対する推定値のグラフ、推定値の標準偏差のヒストグラムを以下に示します。
推定値が0.5に近いほど標準偏差が大きく、0や1では不確かさがほぼないという結果になっています。推定値=0.5は「犬か犬以外の確率が半々」ということを表しているので、どっちかよくわからんという状況ということで不確かさが大きくなるのでしょうか。
テストデータのROC曲線を下図に示します。
「threshold=y」ラベルは、犬か猫か判定する閾値を単純に推定値としてROC曲線を描いたものです。推定値>閾値であれば犬と判定されます。
「threshold=0.5+astd」ラベルは、閾値を0.5+astd (-∞<a<∞)としてROC曲線を描いたものです。推定値>0.5+a*stdであれば犬と判定されます。推定値が0.5に対して不確かさを含めてどの程度余裕があるかを表しています。
結果を見ると…ほぼ同じですね。
同程度の推定値で、標準偏差が小さい場合と大きい場合の画像を以下に示します。標準偏差の大きい画像は人が見ても判別が難しいものであることを期待していましたが、傾向がよくわからん。。笑
学習に使っていないラベルでの不確かさ評価結果
学習に使っていないラベル(飛行機、自動車、鳥、鹿、カエル、馬、船、トラック)についても不確かさ評価を実施しました。
推定値の標準偏差のヒストグラム、推定値の標準偏差に対する推定値のグラフを以下に示します。
0.5付近の推定値が増えるので、ヒストグラムのピークとなる標準偏差の値が大きくなったことがわかります。しかし、標準偏差vs推定値の分布は犬猫画像と変わりなく見えます。学習に使っていないラベルは学習データのデータ分布外なので、推定値が0や1に近くても標準偏差が大きくなるかと思っていたのですが。。
検証のまとめ
- Dropoutを推論時に使うことで推定値がばらつくことを確認できました。
- MC Dropoutで不確かさ(標準偏差)を算出できました。
- 二値分類では推定値と不確かさの値に関連があるみたいです。推定値が0.5に近いほど不確かさが大きくなりました。
- 不確かさの解釈は難しい。。学習に使っていないラベルでは推定値が0や1に近くても不確かさが大きくなるかと思いましたが、そうではなかったです。
- ちなみに、softmaxを使って同じ検証をしてみましたが、上記(sigmoidを使った場合)と同じ結果が得られました。
6. まとめ
- DropoutがDLにおける近似ベイズ推論になっていることを軽く説明しました。
- MC Dropoutでの不確かさ評価方法を説明しました。
- kerasでのMC Dropoutの実装例を示しました。
- 二値分類にMC Dropoutを適用し、不確かさを評価できることを確認しました。しかし、その解釈は難しかったです。