Dropoutによる近似ベイズ推論について2

Chainer Python ディープラーニング ベイジアンモデル

以前に、Dropoutによる近似ベイズ推論に関する記事をあげました。

http://www.ie110704.net/2018/05/12/dropout%E3%81%AB%E3%82%88%E3%82%8B%E8%BF%91%E4%BC%BC%E3%83%99%E3%82%A4%E3%82%BA%E6%8E%A8%E8%AB%96%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/

上記では、ソフトマックス関数の出力値の平均を、カテゴリカル分布のパラメータと見て、その不確実性をエントロピーとして算出していました。

これについて、少し気になることがあったので、確認をしてみました。

エントロピー算出の検証

論文では、前回の記事のように具体的に計算は行っておらず、「カテゴリカル分布のパラメータが揺らぐので、それをエントロピーなり分散なりで計算すれば、深層学習の予測の不確実性を定量化できるだろう」と言っています。

この時、エントロピーを使ったとしても、定量化計算には例えば、

  • 出力ベクトル→Softmax→平均値→エントロピー
  • 出力ベクトル→平均値→Softmax→エントロピー

と算出してみると、どっちも問題なさそうな気がするのですが、どちらがより妥当なのかが疑問に思いました。

これについて、学習させるデータ、推論データ、ドロップアウトなどについて、乱数固定して、両方の結果を見比べてみました。

準備

もろもろインポートします。

import random
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
import chainer
import chainer.functions as F
import chainer.links as L
from chainer.training import extensions
from PIL import Image
from tqdm import tqdm

乱数を固定して、CPUで学習させることにします。

GPUで乱数固定する場合は、Chainerであれば、cupyで設定することになります。

random.seed(0)
np.random.seed(0)

前回同様にMNISTについてサクッと学習させます。

# データの取得
train, valid = chainer.datasets.get_mnist()

# 学習データセット・検証データセット用意
train_x, train_y = train._datasets
valid_x, valid_y = valid._datasets
train_x = train_x.reshape(len(train_x), 1, 28, 28).astype(np.float32)
train_y = train_y.astype(np.int32)
valid_x = valid_x.reshape(len(valid_x), 1, 28, 28).astype(np.float32)
valid_y = valid_y.astype(np.int32)
train_dataset = chainer.datasets.tuple_dataset.TupleDataset(train_x, train_y)
valid_dataset = chainer.datasets.tuple_dataset.TupleDataset(valid_x, valid_y)

# モデルクラス定義
class Model(chainer.Chain):
    def __init__(self):
        super(Model, self).__init__()
        with self.init_scope():
            self.conv1 = L.Convolution2D(1, 16, 3)
            self.conv2 = L.Convolution2D(16, 32, 3)
            self.fc3 = L.Linear(None, 1000)
            self.fc4 = L.Linear(1000, 1000)
            self.fc5 = L.Linear(1000, 10)

    def __call__(self, x, extract_feature=False):
        h1 = F.max_pooling_2d(F.relu(self.conv1(x)), 2)
        h2 = F.max_pooling_2d(F.relu(self.conv2(h1)), 2)
        h3 = F.dropout(F.relu(self.fc3(h2)))
        h4 = F.dropout(F.relu(self.fc4(h3)))
        y = self.fc5(h4)
        return y

# モデル、最適化手法定義
gpu = -1
model = L.Classifier(Model())
optimizer = chainer.optimizers.Adam(alpha=1e-4)
optimizer.setup(model)
if gpu >= 0:
    chainer.cuda.get_device(gpu).use()
    model.to_gpu(gpu)

# 学習
epoch_num = 10
batch_size = 1000
train_iter = chainer.iterators.SerialIterator(train_dataset, batch_size)
test_iter = chainer.iterators.SerialIterator(valid_dataset, batch_size, repeat=False, shuffle=False)
updater = chainer.training.StandardUpdater(train_iter, optimizer, device=gpu)
trainer = chainer.training.Trainer(updater, (epoch_num, 'epoch'), out='tmp_result')
trainer.extend(extensions.Evaluator(test_iter, model, device=gpu))
trainer.extend(extensions.LogReport(trigger=(1, 'epoch')))
trainer.extend(extensions.LogReport())
trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'validation/main/loss', 'main/accuracy', 'validation/main/accuracy', 'elapsed_time']))
trainer.run()

ひとまずこれで準備完了です。

出力ベクトル→Softmax→平均値→エントロピー

モデルの出力値のソフトマックス関数の値について、モンテカルロドロップアウトサンプリングの平均値をとってエントロピーを計算させてみます。

これは前回記事と同じやり方になります。

target_num = 0
sampling_num = 50

target_x = valid_x[np.where(valid_y == target_num)]

entropy = np.zeros((len(target_x)), dtype=np.float32)
for i in tqdm(range(len(target_x))):
    x = target_x[i]
    x = x[np.newaxis]
    preds = np.zeros((sampling_num, 10), dtype=np.float32)

    for j in range(sampling_num):
        with chainer.using_config('train', True):
            preds[j, :] = F.softmax(model.predictor(x), axis=1).data.squeeze()

    preds = preds.mean(axis=0)
    entropy[i] = np.sum(-preds*np.log(preds))

target_imgs = target_x.reshape(len(target_x), 28, 28)
target_imgs *= 255
target_imgs = target_imgs.astype(np.uint8)

high_entropy_imgs = target_imgs[np.argsort(entropy)[::-1][:30]]
low_entropy_imgs = target_imgs[np.argsort(entropy)[:30]]

fig, axs = plt.subplots(ncols=10, nrows=3, figsize=(20, 5))

for i, img in enumerate(low_entropy_imgs):
    img = Image.fromarray(img)
    axs[i//10, i%10].imshow(img)
    axs[i//10, i%10].axis('off')

plt.suptitle('low entropy top 30')
plt.show()

fig, axs = plt.subplots(ncols=10, nrows=3, figsize=(20, 5))

for i, img in enumerate(high_entropy_imgs):
    img = Image.fromarray(img)
    axs[i//10, i%10].imshow(img)
    axs[i//10, i%10].axis('off')

plt.suptitle('high entropy top 30')
plt.show()

結果についてはやはり前回同様、エントロピーが低いものは予測しやすい画像、エントロピーが高いものは予測しにくい画像が集まりました。

出力ベクトル→平均値→Softmax→エントロピー

次に、モデルの出力値のモンテカルロドロップアウトサンプリングの平均値について、ソフトマックス関数をとってエントロピー算出してみます。

コードを少し変更。

target_num = 0
sampling_num = 50

target_x = valid_x[np.where(valid_y == target_num)]

entropy = np.zeros((len(target_x)), dtype=np.float32)
for i in tqdm(range(len(target_x))):
    x = target_x[i]
    x = x[np.newaxis]
    preds = np.zeros((sampling_num, 10), dtype=np.float32)

    for j in range(sampling_num):
        with chainer.using_config('train', True):
            preds[j, :] = model.predictor(x).data.squeeze()

    preds = preds.mean(axis=0)[np.newaxis]
    preds = F.softmax(preds, axis=1).data.squeeze()
    entropy[i] = np.sum(-preds*np.log(preds))

target_imgs = target_x.reshape(len(target_x), 28, 28)
target_imgs *= 255
target_imgs = target_imgs.astype(np.uint8)

high_entropy_imgs = target_imgs[np.argsort(entropy)[::-1][:30]]
low_entropy_imgs = target_imgs[np.argsort(entropy)[:30]]

fig, axs = plt.subplots(ncols=10, nrows=3, figsize=(20, 5))

for i, img in enumerate(low_entropy_imgs):
    img = Image.fromarray(img)
    axs[i//10, i%10].imshow(img)
    axs[i//10, i%10].axis('off')

plt.suptitle('low entropy top 30')
plt.show()

fig, axs = plt.subplots(ncols=10, nrows=3, figsize=(20, 5))

for i, img in enumerate(high_entropy_imgs):
    img = Image.fromarray(img)
    axs[i//10, i%10].imshow(img)
    axs[i//10, i%10].axis('off')

plt.suptitle('high entropy top 30')
plt.show()

やっぱりピッタリ一致しませんでした。

ただし、傾向としては同じようなものを抽出してきている様子です。

おまけとまとめ

おまけなんですが、ドロップアウトなしの出力ベクトル→Softmax→エントロピーを計算させた結果が以下になります。

target_num = 0

target_x = valid_x[np.where(valid_y == target_num)]

entropy = np.zeros((len(target_x)), dtype=np.float32)
for i in tqdm(range(len(target_x))):
    x = target_x[i]
    x = x[np.newaxis]

    with chainer.using_config('train', False):
        preds = F.softmax(model.predictor(x), axis=1).data.squeeze()

    entropy[i] = np.sum(-preds*np.log(preds))

target_imgs = target_x.reshape(len(target_x), 28, 28)
target_imgs *= 255
target_imgs = target_imgs.astype(np.uint8)

high_entropy_imgs = target_imgs[np.argsort(entropy)[::-1][:30]]
low_entropy_imgs = target_imgs[np.argsort(entropy)[:30]]

fig, axs = plt.subplots(ncols=10, nrows=3, figsize=(20, 5))

for i, img in enumerate(low_entropy_imgs):
    img = Image.fromarray(img)
    axs[i//10, i%10].imshow(img)
    axs[i//10, i%10].axis('off')

plt.suptitle('low entropy top 30')
plt.show()

fig, axs = plt.subplots(ncols=10, nrows=3, figsize=(20, 5))

for i, img in enumerate(high_entropy_imgs):
    img = Image.fromarray(img)
    axs[i//10, i%10].imshow(img)
    axs[i//10, i%10].axis('off')

plt.suptitle('high entropy top 30')
plt.show()

やっぱり似たようなものが出てきた笑

いずれの方法も、これらの結果を能動学習に用いるなどであれば、似たような効力は得られそうな気がします。

モンテカルロドロップアウトサンプリングを導出することで、ベイズの枠組みとして考えられることは、論文で理論的に定式化していますので、モンテカルロドロップアウトサンプリングから予測分布を導出する形まで、数式的には納得がいく気がします。

また、論文著者のサイトで以下のものがあります。

- http://mlg.eng.cam.ac.uk/yarin/blog_2248.html

これのソフトマックス尤度の考えなどからすれば、やはり「出力ベクトル→Softmax→平均値→エントロピー」が今のところ一番納得できるやり方な気はするのですが、結果が同じでないということは微妙に何か違うことなので、この辺りは厳密にはどう考えればいいのかなーって思いました。

実験したソースコードは以下にまとめました。

GitHub: https://github.com/Gin04gh/datascience/blob/master/samples_deeplearning_python/dropout_bayesian_approximation_experiment.ipynb

コメント