statsuのblog

愛知のデータサイエンティスト。自分の活動記録。主に機械学習やその周辺に技術について学んだことを記録していく予定。

Kaggle Google QUEST Q&Aコンペに参加した記録

Kaggle Google QUEST Q&Aコンペに参加して61位(1571チーム中)でソロ銀メダルを取れました。
以下ではその記録についてまとめます。

本記事の概要

  • コンペ概要
  • 私の取り組み
  • 興味深かった上位ソリューション
  • 感想

コンペ概要

f:id:st1990:20200212230955p:plain
Google QUEST Q&A Labeling | Kaggle

  • タスク概要

    • QAサイトの質問・回答に対して人が主観的にどう感じたかを予測するタスク。後述の入力データから予測対象データを予測する。
  • データ

    • 入力データ:stackoverflowなどのQAサイトの質問タイトル、質問文、回答文、カテゴリ、等
    • 予測対象データ:質問・回答に関する人の主観的な30個の項目について、0~1の得点が与えられる。例えば、answer_helpfulという項目は回答がhelpfulであるかどうかを表しており、得点0に近ければhelpfulではなく、1に近ければhelpfulである。得点はアノテータが付けたものらしい。
  • 予測性能の評価方法

    • 各予測対象項目の得点について、全サンプルで正解と予測のスピアマン順位相関係数を算出する。これを全項目で平均したものが評価指標。
    • 評価指標がMSEではなく順位相関係数なので、予測値の大小関係さえ正確に予測できれば良い。
  • スタンダードな解法

    • 予測モデル:Deep learningベースの自然言語処理モデル(Bertなど)を学習させ、入力(質問タイトル、質問文、回答文)から30項目の予測対象を回帰で推定。
    • 後処理:評価指標値を上げるために、予測モデルの出力を離散値とするような後処理を実施。正解値は0~1の範囲の離散値(例:0, 0.333, 0.666, ...,1.0)をとっていたため、予測値も離散値とすることでスピアマン順位相関係数が高くなる。

こちらのブログでわかりやすくまとめてありました。
Kaggle Google QUEST Q&A コンペ 振り返り - 機械学習 Memo φ(・ω・ )

私の解法

私の解法は以下のとおりです。流れは上記のスタンダードな解法と同じです。

  • コード
    github.com

  • 概要

    • Bert baseのpretrainedモデルを学習させ、入力(質問タイトル、質問文、回答文)から予測対象を回帰で予測。
    • Bert baseのpretrainedモデルを学習させ、入力(質問タイトル、質問文、回答文)から予測対象をクラス分類で予測。
    • 後処理は、予測値に対してある閾値以下を0、ある閾値以上を1にし、その間は何も処理しないというもの。
  • 使ったライブラリ

    • pytorch:kerasから乗り換えました。モデル構造や学習スキームをいじりたい凝り性の人にはpytorchおすすめ!自由度高くて使いやすい。あとgradient accumulate使いやすいのほんと良い。
    • hugging face:DL系の自然言語処理ライブラリ。初めて使ったけど、かなり使いやすい。
  • モデル構造

    • 下図の2つのモデルを使いました。Model1は回帰、Model2は回帰版とクラス分類版の2つを作りました。最終的な予測値には、基本的にはModel1とModel2の回帰版の平均を使い、一部の予測項目だけはModel2のクラス分類版を使いました。

f:id:st1990:20200213002707p:plain
モデル構造

  • データ前処理

    • 入力:特に特別な処理をしていない。
    • 出力:予測対象の値の偏りが大きかったので、ランク値にして最大ランクで割ることでスケーリングした。
  • モデルの学習

    • 3epoch(最初の1epochはwarmup)。
    • 学習率は1e-4。Bertは5e-5~2e-5が推奨されているらしいですが、1e-4でうまくいった。
    • 損失関数は、回帰ではBinary cross entropy、分類ではcategorical cross entropy。label smoothingも使った。
  • 後処理

    • 予測値に対してある閾値以下を0、ある閾値以上を1にし、その間は何も処理しないというもの。CVスコアを最大にするような閾値を探索して閾値を決定した。
  • うまくいかなかったこと

    • pair wiseのランク学習やってみたけど効果なかった。list wiseのランク学習であるListNetをやりたかったけど、メモリが足りなくて断念。バッチサイズをある程度大きくとれないと厳しい。ただ、ランク学習自体は面白い分野だと思うので今後活用していきたい。ランク学習に関して知りたい人は以下のサイトがおすすめ。かなり参考になりました。
      ランク学習(Learning to Rank) Advent Calendar 2018 - Adventar
    • 初手でAlbertを試したけどまったく精度でなかった。精度出なさ過ぎて自分のコードのどこかにバグがあるのか探し回ってしまったぐらい精度が出なかった。
    • Stochastic Weight Averagingは効果がなかった。でもいつか使いどころが出てくる気がする。

興味深かった上位のソリューション

  • 入力の工夫

    • Bertの入力にスペシャトークン[category]を追加して、[CLS]+[category]+question title+...としていた。Bertの出力にカテゴリ等の情報をくっつけても精度が上がらなかったという声が多かったが、これはうまくいったらしい。最初の入力に入れ込むことで、うまいこと情報を吸い上げてくれるんかな。
  • モデル構造の工夫

    • 最期の全結合層の入力として、Bertの最終層の出力だけでなく、中間層の出力も使っていた。具体的には、中間層Aの出力([CLS])、中間層Bの出力([CLS])、…、最終層([CLS])の出力を重み付き平均し、それを全結合層の入力としていた。重み付き平均の重みは学習パラメータとし、合計が1になるようにしていた。この発想めっちゃよさげ。どこかで使いたい。
    • 全結合層を2つ重ねていた。僕はBertは全結合層1つでいいものだという思い込みがあって、そこの工夫をまったくしていなかった。。
  • 学習の工夫

    • Bertの部分と全結合層で学習率を変えていた。

感想

  • 過去2回僅差で銅メダルを逃していたので、やっとメダルとれて純粋にうれしかった。
  • 自然言語処理を今まで敬遠してたけど、初めて真剣に取り組みました。やってみるとかなり面白いし、応用範囲も広そうだから身に着けていきたい。hugging faceには日本語版Bertもあるのでなんかやってみたいな。
  • pytorchは自由度が高くてかなり良い。構造や学習の工夫を柔軟に実装できる。もっと早くkerasから乗り換えればよかった。
  • ランク学習やってみたい。論文を漁ってたら、スピアマン順位相関係数を直接最適化できそうな手法があったりして面白そう。

【論文メモ】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)は次式で表される。
BCN(X)=GN(BN(X))
要するに、BNしてからGNしているだけ。簡単。
一見冗長であるが非線形性が増したりするので意味があると論文では言及されてる。

バッチサイズが小さいとき

入力をXとすると、BCNの出力BCN(X)は次式で表される。
BCN(X)=GN(BN'(X))
さっきと同じように見えるが、このBN'は普通のBNの以下の点が異なる。

  • 普通のBN

    • 学習時:バッチ内でのXの平均、標準偏差を使って計算したμ及びσを使う。推論時に使う用のμ、σを、バッチ内Xの平均、標準偏差の指数移動平均で計算する。
  • BN'

    • 学習時:μ、σを、バッチ内Xの平均、標準偏差の指数移動平均で計算する。このμ、σを使ってBN'(X)を計算する。普通のBNでは推論時に使ってる"移動平均で計算されるμ、σ"を学習時にも使っているイメージです。アルゴリズムは以下のとおりです。

f:id:st1990:20200122001936p:plain
BCNのアルゴリズム

検証結果

画像認識ではCifar10, Cifar100, ImageNet、物体検出ではCOCO、セグメンテーションではPASCALで検証されています。
BCN+WSが強いですね。物体検出とセグメンテーションではBNと比較していないのが残念。

f:id:st1990:20200122002649p:plain
検証1
f:id:st1990:20200122002717p:plain
検証2

感想

  • BCNではBNとGNの両方を使っています。単純に考えると冗長としか思えないのに、それが効くって面白いですね。論文ではなぜ効くかをきちんと考察しています。ブラックボックスにせずに、各層がどんな役割を持っているかのイメージをきちんと持つことが大事ですね。
  • バッチサイズが小さいときのBCNのμ、σはforward時に更新されるんですね。これだとgradient accumulateするときに支障になる?僕のような雑魚GPUユーザー的には、gradient accumulateで精度が落ちない手法がうれしい。その点でGN+WSは良いね。

Kaggleの雲コンペ(Understanding Clouds from Satellite Images)についての記録

Kaggleの雲コンペに参加したので、実施事項や思ったことを記録しておきます。

1. 雲コンペ

衛星写真にうつった雲を種類ごとに検出して分類するタスクについて、その精度を競います。
Understanding Clouds from Satellite Images | Kaggle https://storage.googleapis.com/kaggle-media/competitions/MaxPlanck/Teaser_AnimationwLabels.gif

特徴は以下のとおりです。

  • 検出する雲の種類は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

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

気になった解法は以下とおりです。

4. 思ったこと

  • 大きいモデルを使えないと画像コンペでは絶対勝てない。新しいPCかクラウドを導入しよう。クラウドならGCPがいいかな。安そう。
  • Windowsでは分散処理が動かないことが多い。GPUだけでなくCPU側も処理速度のボトルネックになっている。

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なモデルで文字を分類
    f:id:st1990:20191024214148p:plain
    文字認識のフロー

※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. 本記事の概要

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)
  • 学習時
    • 確率pに従い、活性化関数の活性or非活性を決め、活性化関数の出力を計算する。
      • 活性:活性化関数の出力をf(x)とする。
      • 非活性:活性化関数の出力をxとする。(活性化関数をただの線形関数にする)
  • テスト時
    • 活性化関数の出力を px+(1-p)f(x) で計算する。

f:id:st1990:20191024134451p:plain
Drop-Activationのイメージ図(原論文より引用)

原論文での精度検証結果を以下に引用します。Baselineの活性化関数はReluです。Randomized Reluとの比較や、CutoutやAutoAugを使ったときの検証がされています。なかなかいいですね。

f:id:st1990:20191024135638p:plain
fig2-3 (原論文より引用)
f:id:st1990:20191024135718p:plain
table1 (原論文より引用)
f:id:st1990:20191024135739p:plain
table4 (原論文より引用)

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を適用した場合の精度を検証する。
  • 対象問題
    • cifar10で10クラスの画像を分類する。
  • 画像分類モデル
  • Drop-Activationの適用条件
    • 基本の活性化関数: Relu
    • ResNet20v1のReluすべてにDrop-Activationを適用
  • 比較条件
    • Drop-Activationのdrop rate (=0, 0.05, 0.1)
      • drop rate=0のとき、Drop-Activationの作用なし
    • Cutout, Random erasing, Mixupの有無

結果のまとめを以下に示します。各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

f:id:st1990:20191024143914j:plain
val_loss_acc_droprate
f:id:st1990:20191024143931j:plain
val_loss_acc_cutout
f:id:st1990:20191024143948j:plain
val_loss_acc_randomerasing
f:id:st1990:20191024144004j:plain
val_loss_acc_mixup
f:id:st1990:20191024144020j:plain
val_loss_acc_mixup_cutout
f:id:st1990:20191024144035j:plain
val_loss_acc_mixup_randomerasing

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

以下、要点のみメモ。

アンサンブル学習での不確かさ評価

論文及び参考サイト

手法

  • 目的
    • DLの推定値の不確かさを定量評価する。
  • 手法の概要
    • 通常のDLでは推定値を出力する。例えば、入力情報から土地価格を推定したければDLが土地価格を出力するようにネットワークを作る。
    • 提案されているDLでは、推定値の平均と分散(または推定値の確率分布のパラメータ)を出力するようにネットワークを作る。最小化する損失関数は、推定値の確率分布の対数尤度である。※損失関数は推定値、分散、推定値の正解データから成り、分散の正解データは不要。
    • ブーストラップサンプリングで元データセットからM個のデータセットを作成し、それぞれのデータセットを使ってM個のネットワークを学習する。
    • M個のネットワークの推定値の平均値を最終的な推定値とする。
    • M個のネットワークの混合分布の分散を最終的な分散とする。

kerasでの実装例

ラベル0 or 1の二値分類を対象としたネットワークを作った実装例を紹介します。

問題設定は以下のとおりです。

  • ネットワークの推定値μは、ラベルxの平均値であり0から1の間の値。
  • ラベルxの確率分布が、定義範囲を限定した正規分布p(x : μ, σ^2) (0≦x≦1)に従うと仮定する。このσ^2もネットワークで出力する。
  • 損失関数は、定義範囲を限定した正規分布p(x : μ, σ^2) (0≦x≦1)の対数尤度関数。

kerasでこれらを実装するためには、(1)出力が2種類のネットワーク作成、(2)自作の損失関数の使用、が必要となります。以下、実装例です。

(1) 出力が2種類のネットワーク
推定値μはsigmoid関数、σ^2はsoftplus関数で計算します。σ^2が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にする)の計算があってごちゃごちゃしています。また、σ^2が大きい値を取りやすかったので正則化項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個。
    • 学習に使っていないラベル(飛行機、自動車、鳥、鹿、カエル、馬、船、トラック)についても評価を実施。

※以下のグラフのuncertainty coefは、アンサンブルで求めた標準偏差です。

犬猫分類の不確かさ評価結果

犬猫の分類結果を以下に示します。推定値が1のとき犬、0のとき猫です。
推定値が0.5に近いほど推定値の標準偏差(不確かさ)が大きい傾向がわかります。

f:id:st1990:20190815195109p:plain
推定値の標準偏差ヒストグラム
f:id:st1990:20190815195201p:plain
推定値の標準偏差 vs 推定値 (学習データ)
f:id:st1990:20190815195242p:plain
推定値の標準偏差 vs 推定値 (テストデータ)
f:id:st1990:20190815195747p:plain
推定値の標準偏差 vs 推定値 (犬)
f:id:st1990:20190815195819p:plain
推定値の標準偏差 vs 推定値 (猫)

テストデータのROC曲線を下図に示します。
「threshold=y」ラベルは、犬か猫か判定する閾値を単純に推定値としてROC曲線を描いたものです。推定値>閾値であれば犬と判定されます。
「threshold=0.5+a * std」ラベルは、閾値を0.5+astd (-∞<a<∞)としてROC曲線を描いたものです。推定値>0.5+astdであれば犬と判定されます。推定値が0.5に対して不確かさを含めてどの程度余裕があるかを表しています。
結果を見ると…ほぼ同じですね。

f:id:st1990:20190816024535p:plain
ROC曲線(テストデータ)

学習に使っていないラベルでの不確かさ評価結果

学習に使っていないラベル(飛行機、自動車、鳥、鹿、カエル、馬、船、トラック)についても不確かさ評価を実施しました。
推定値の標準偏差ヒストグラム、推定値の標準偏差に対する推定値のグラフを以下に示します。
0.5付近の推定値が増えるので、ヒストグラムのピークとなる標準偏差の値が大きくなったことがわかります。しかし、標準偏差vs推定値の分布は犬猫画像と変わりなく見えます。学習に使っていないラベルは学習データのデータ分布外なので、推定値が0や1に近くても標準偏差が大きくなるかと思っていたのですが。これはDropoutを使った不確かさ評価と同じ結果ですね。
f:id:st1990:20190815200528p:plainf:id:st1990:20190815200510p:plainf:id:st1990:20190815195819p:plainf:id:st1990:20190815200513p:plainf:id:st1990:20190815200516p:plainf:id:st1990:20190815200522p:plainf:id:st1990:20190815200525p:plainf:id:st1990:20190815200531p:plainf:id:st1990:20190815200534p:plainf:id:st1990:20190815200539p:plainf:id:st1990:20190815200541p:plain

以上

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は犬である確率を出力します。
  • 学習済みCNNで推論時にもDropoutを使い、推定値の標準偏差(原論文で提案された分散ではなく、普通の方法で算出)を計算します。また、推定値の平均も計算します。
    • 1つの入力に対して100回モンテカルロドロップアウトサンプリングを実施。
    • 学習に使っていないラベル(飛行機、自動車、鳥、鹿、カエル、馬、船、トラック)についても評価を実施。

f:id:st1990:20190731002809p:plain
モデル構造

実装

実装には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個の推定値を平均したものです。
    f:id:st1990:20190731000006p:plain
    MC Dropoutでの推定値 vs CNNの推定値(学習データ)
    f:id:st1990:20190731000043p:plain
    MC Dropoutでの推定値 vs CNNの推定値(テストデータ)
犬猫画像の分類結果と不確かさ

学習に使った犬猫画像の分類結果とMC Dropoutで求めた不確かさ(標準偏差)の関係をみていきます。
推定値の標準偏差に対する推定値のグラフ、推定値の標準偏差ヒストグラムを以下に示します。
推定値が0.5に近いほど標準偏差が大きく、0や1では不確かさがほぼないという結果になっています。推定値=0.5は「犬か犬以外の確率が半々」ということを表しているので、どっちかよくわからんという状況ということで不確かさが大きくなるのでしょうか。

f:id:st1990:20190731000343p:plain
推定値の標準偏差 vs 推定値(学習データ)
f:id:st1990:20190731000419p:plain
推定値の標準偏差 vs 推定値(テストデータ)
f:id:st1990:20190731002322p:plain
推定値の標準偏差 vs 推定値(犬のみ)(学習データ)
f:id:st1990:20190731002353p:plain
推定値の標準偏差 vs 推定値(猫のみ)(学習データ)
f:id:st1990:20190731000437p:plain
推定値の標準偏差ヒストグラム

テストデータのROC曲線を下図に示します。
「threshold=y」ラベルは、犬か猫か判定する閾値を単純に推定値としてROC曲線を描いたものです。推定値>閾値であれば犬と判定されます。
「threshold=0.5+astd」ラベルは、閾値を0.5+astd (-∞<a<∞)としてROC曲線を描いたものです。推定値>0.5+a*stdであれば犬と判定されます。推定値が0.5に対して不確かさを含めてどの程度余裕があるかを表しています。
結果を見ると…ほぼ同じですね。

f:id:st1990:20190816021850p:plain
ROC曲線(テストデータ)

同程度の推定値で、標準偏差が小さい場合と大きい場合の画像を以下に示します。標準偏差の大きい画像は人が見ても判別が難しいものであることを期待していましたが、傾向がよくわからん。。笑

f:id:st1990:20190731001405p:plain
推定値0.48~0.52で標準偏差が小さい方から25位までの画像
f:id:st1990:20190731001542p:plain
推定値0.48~0.52で標準偏差が大きい方から25位までの画像
f:id:st1990:20190731001608p:plain
推定値0.68~0.72で標準偏差が小さい方から25位までの画像
f:id:st1990:20190731001629p:plain
推定値0.68~0.72で標準偏差が大きい方から25位までの画像
f:id:st1990:20190731001651p:plain
推定値0.78~0.82で標準偏差が小さい方から25位までの画像
f:id:st1990:20190731001708p:plain
推定値0.78~0.82で標準偏差が大きい方から25位までの画像

学習に使っていないラベルでの不確かさ評価結果

学習に使っていないラベル(飛行機、自動車、鳥、鹿、カエル、馬、船、トラック)についても不確かさ評価を実施しました。
推定値の標準偏差ヒストグラム、推定値の標準偏差に対する推定値のグラフを以下に示します。
0.5付近の推定値が増えるので、ヒストグラムのピークとなる標準偏差の値が大きくなったことがわかります。しかし、標準偏差vs推定値の分布は犬猫画像と変わりなく見えます。学習に使っていないラベルは学習データのデータ分布外なので、推定値が0や1に近くても標準偏差が大きくなるかと思っていたのですが。。

f:id:st1990:20190731002127p:plain
推定値の標準偏差ヒストグラム
f:id:st1990:20190731002513p:plain
推定値の標準偏差 vs 推定値(飛行機)
f:id:st1990:20190731002532p:plain
推定値の標準偏差 vs 推定値(自動車)
f:id:st1990:20190731002546p:plain
推定値の標準偏差 vs 推定値(鳥)
f:id:st1990:20190731002602p:plain
推定値の標準偏差 vs 推定値(鹿)
f:id:st1990:20190731002618p:plain
推定値の標準偏差 vs 推定値(カエル)
f:id:st1990:20190731002635p:plain
推定値の標準偏差 vs 推定値(馬)
f:id:st1990:20190731002657p:plain
推定値の標準偏差 vs 推定値(船)
f:id:st1990:20190731002716p:plain
推定値の標準偏差 vs 推定値(トラック)

検証のまとめ
  • Dropoutを推論時に使うことで推定値がばらつくことを確認できました。
  • MC Dropoutで不確かさ(標準偏差)を算出できました。
  • 二値分類では推定値と不確かさの値に関連があるみたいです。推定値が0.5に近いほど不確かさが大きくなりました。
  • 不確かさの解釈は難しい。。学習に使っていないラベルでは推定値が0や1に近くても不確かさが大きくなるかと思いましたが、そうではなかったです。
  • ちなみに、softmaxを使って同じ検証をしてみましたが、上記(sigmoidを使った場合)と同じ結果が得られました。

6. まとめ

  • DropoutがDLにおける近似ベイズ推論になっていることを軽く説明しました。
  • MC Dropoutでの不確かさ評価方法を説明しました。
  • kerasでのMC Dropoutの実装例を示しました。
  • 二値分類にMC Dropoutを適用し、不確かさを評価できることを確認しました。しかし、その解釈は難しかったです。