※本コラムは、以前に個人ブログとして公開していた内容を、加筆・再構成のうえ掲載しております。技術的な内容は執筆当時のものであり、現在とは異なる場合がございます。
こんにちは。Anagraftの伊藤です。
深層学習は機械学習の中でも強力な手法ですが、一般的に、予測や分類などのタスクにおいて「確信度」や「不確実性」を示すベイズ的なアウトプットを得ることは難しいという課題があります。
今回は、深層学習で汎化性能を保つための工夫の一つであるDropoutを使って推論を行うことが、近似的にベイズ推論になっているという論文を紹介します。実はかなりシンプルな方法で実現でき、私自身も実務で使うことで「予測が難しい傾向にある教師データはこれらです」といったインサイトを得られた、強力な方法です。皆さんの学習の役に立てば幸いです。
なお、元記事の執筆は2018年で、コードは深層学習ライブラリのChainerで書かれていました。Chainerは2019年12月にメンテナンスフェーズへ移行し、開発元のPreferred Networksが研究開発基盤をPyTorchへ移行すると発表しました。そこで本コラムのコード例は現在広く使われているPyTorchに書き換えています(手法そのものは元記事と同じです)。
論文は下記になります。
この論文は、Dropoutを適用して学習した深層学習が、ディープなガウス過程における近似ベイズ推論として解釈・定式化できることを理論的に示しています。
少し整理します。学習データ\( \textbf{X}, \textbf{Y} \)が与えられたとき、ニューラルネットワークの重み\( {\boldsymbol \omega} \)の事後分布\( p({\boldsymbol \omega}|\textbf{X}, \textbf{Y}) \)を直接求めるのは困難です。そこで、これを近似する分布\( q({\boldsymbol \omega}) \)を考えます。論文は、この近似分布\( q({\boldsymbol \omega}) \)からの重みのサンプリングが、Dropoutによってネットワークのユニットをランダムに0にすることと同じ意味になることを示しました。
そのうえで、出力\( \textbf{y} \)の予測分布は、Dropoutを適用したまま推論を複数回繰り返し、その平均をとることで近似できます。論文ではこれをMonte Carlo dropout(モンテカルロ・ドロップアウト、以下MC Dropout)と呼んでいます。
\( p(\textbf{y}|\textbf{x}, \textbf{X}, \textbf{Y}) \approx \displaystyle\frac{1}{T}\sum_{t=1}^{T} p(\textbf{y}|\textbf{x}, {\boldsymbol \omega}_t) \)
ここで\( T \)はサンプリング回数、\( {\boldsymbol \omega}_t \)は\( t \)回目のDropoutで得られた重みです。通常、Dropoutは推論時には無効にしますが、MC Dropoutでは推論時もDropoutを有効にしたまま\( T \)回推論する、というのがポイントです。
予測分布の不確実性(予測しにくさ)を表す指標としては、論文では分散やエントロピーの利用が提案されています。本コラムでは、このうちエントロピーを使って、各画像の予測しにくさを定量化してみます。エントロピーは確率分布の予測しにくさを表す指標で、確率分布が一様分布に近いほど(どのラベルとも判断がつかないほど)大きくなります。
それでは実際に、Dropoutを適用して深層学習モデルを学習し、Dropoutを適用したまま推論を繰り返して予測分布を作成してみます。論文と同様に、MNIST画像分類タスクで実験してみます。
元記事ではChainerで実装していましたが、Chainerは2019年12月にメンテナンスフェーズへ移行し、開発リソースがPyTorchへ移管されました。そこで本コラムのコードはPyTorchに書き換えています。MC Dropoutの考え方(推論時もDropoutを有効にして複数回推論する)は変わりません。
まずは必要なライブラリの読み込みとMNISTデータの準備です。
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from tqdm import tqdm
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
transform = transforms.ToTensor()
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
valid_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=1000, shuffle=True)
valid_loader = DataLoader(valid_dataset, batch_size=1000, shuffle=False)
len(train_dataset), len(valid_dataset) # (60000, 10000)
モデルのアーキテクチャは、元記事と同じく畳み込みを少し加えた簡単なCNNにします。全結合層のあとにDropoutを入れている点がポイントです。
class Model(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 16, 3)
self.conv2 = nn.Conv2d(16, 32, 3)
self.fc3 = nn.Linear(32 * 5 * 5, 1000)
self.fc4 = nn.Linear(1000, 1000)
self.fc5 = nn.Linear(1000, 10)
self.dropout = nn.Dropout(p=0.5)
def forward(self, x):
h1 = F.max_pool2d(F.relu(self.conv1(x)), 2)
h2 = F.max_pool2d(F.relu(self.conv2(h1)), 2)
h2 = h2.flatten(1)
h3 = self.dropout(F.relu(self.fc3(h2)))
h4 = self.dropout(F.relu(self.fc4(h3)))
y = self.fc5(h4)
return y
モデルを学習させます。
model = Model().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
criterion = nn.CrossEntropyLoss()
epoch_num = 10
for epoch in range(epoch_num):
model.train()
for x, y in train_loader:
x, y = x.to(device), y.to(device)
optimizer.zero_grad()
loss = criterion(model(x), y)
loss.backward()
optimizer.step()
# 検証
model.eval()
correct = 0
with torch.no_grad():
for x, y in valid_loader:
x, y = x.to(device), y.to(device)
correct += (model(x).argmax(1) == y).sum().item()
print(f'epoch {epoch + 1}: valid accuracy = {correct / len(valid_dataset):.4f}')
10エポックほど学習させると、検証データで98%程度の精度になります(環境や乱数により多少前後します)。
続いて、本題のMC Dropoutによる予測分布を作成します。通常、PyTorchではmodel.eval()でDropoutを無効にして推論しますが、ここでは意図的にDropoutを有効な状態(model.train())にして推論を繰り返します(今回のモデルには含まれませんが、BatchNormなどtrain/evalで挙動が変わる層がある場合は、それらはeval()のままDropout層だけを有効化する必要があります)。1回の推論で各クラスの予測確率ベクトル(softmax出力)が1つ得られ、これをサンプリング回数だけ繰り返して平均することで、「結局どのラベルにどれだけ振り分けられたのか」という予測分布を近似できます。
1枚の画像について推論を繰り返したとき、すべてのラベルに均等に振り分けられれば(一様分布に近ければ)、その画像はどのラベルか判断がつかない、つまり予測しにくい画像ということになります。逆に、予測しやすい画像は、どんなDropoutパターンでも予測ラベルが特定の値に集中します。この予測しにくさを、エントロピーで定量化します。
以下は、バリデーションデータの中からtarget_numのラベルに絞ってエントロピーを算出し、予測しやすい画像と予測しにくい画像をプロットするコードです。
def plot_entropy_examples(target_num, sampling_num=50):
# 対象ラベルの画像だけ抽出
valid_x = valid_dataset.data.float().unsqueeze(1) / 255.0 # (N, 1, 28, 28)
valid_y = valid_dataset.targets
target_x = valid_x[valid_y == target_num].to(device)
# MC Dropout:推論時もDropoutを有効にする
model.train()
entropy = np.zeros(len(target_x), dtype=np.float32)
with torch.no_grad():
for i in tqdm(range(len(target_x))):
x = target_x[i].unsqueeze(0)
preds = np.zeros((sampling_num, 10), dtype=np.float32)
for j in range(sampling_num):
preds[j] = F.softmax(model(x), dim=1).cpu().numpy().squeeze()
preds = preds.mean(axis=0)
entropy[i] = np.sum(-preds * np.log(preds + 1e-12))
target_imgs = (target_x.cpu().numpy().reshape(-1, 28, 28) * 255).astype(np.uint8)
low_entropy_imgs = target_imgs[np.argsort(entropy)[:30]]
high_entropy_imgs = target_imgs[np.argsort(entropy)[::-1][:30]]
for title, imgs in [('low entropy top 30', low_entropy_imgs),
('high entropy top 30', high_entropy_imgs)]:
fig, axs = plt.subplots(ncols=10, nrows=3, figsize=(20, 5))
for i, img in enumerate(imgs):
axs[i // 10, i % 10].imshow(img, cmap='gray')
axs[i // 10, i % 10].axis('off')
plt.suptitle(title)
plt.show()
plot_entropy_examples(target_num=0) # 試しに0の画像で検証

予想通りの結果になりました。バリデーションの中から0の画像で、MC Dropoutで得られた予測分布のエントロピーが低かったTOP30、高かったTOP30を表示しています。エントロピーが低いものは予測がしやすい画像なので、とても綺麗にお手本のように書かれた0が集まりました。逆にエントロピーが高いものは予測がしにくく、他のラベルと間違えやすい画像なので、形がいびつだったり汚い字が集まっています。
これを他の数字でも実行した結果が以下です。

1の画像では、エントロピーが低いものはただ真っ直ぐに線が引かれているだけで、間違いようがなさそうです。エントロピーが高いものは、字がかすれていたり、線が太すぎたりして予測を間違えやすい傾向にあるようです。

2の画像では、エントロピーが高いものはかなりひどく、人でも読めなさそうなものも見受けられます。

3の画像では、エントロピーが低いものはとても綺麗にバランスの取れたお手本のような3が集まりました。エントロピーが高いものは、読めないことはなさそうですが、やはりバランスが悪い字が多いです。






4以降も同様の傾向で、エントロピーが低いものはバランスが良く綺麗な字、高いものは崩れた字が集まります。
このように、Dropoutを入れるだけで様々な深層学習のネットワークアーキテクチャに適用でき、結果も見ていて面白いです。欠点があるとすれば、1つの入力につきサンプリング回数だけ推論を繰り返すため、予測に少し時間を要することです。また、予測がしにくいデータは教えてくれますが、「なぜ予測しにくいのか」「どうすれば間違えにくくなるのか」は、結果を見て自分で考察していく必要があります。
このような方法の応用例として、物体検出のモデルに適用し、予測確率が高そうなバウンディングボックスを重ねて可視化する、以下のような論文も出ています。
この章では、私個人のちょっとした疑問に対する実験をやってみます。
論文は予測の平均・分散・エントロピーといった指標には触れていますが、本記事で試すような実装上の計算順序の違いまでは主題にしていません。このとき、エントロピーを使うにしても、計算の順序として例えば次の2通りが考えられます。
どちらも問題なさそうな気がしますが、どちらがより妥当なのかが疑問に思いました。そこで、学習・予測データやDropoutなどの乱数を固定したうえで、両方の結果を見比べてみます。
1つ目は前章と同じ、出力ベクトル → Softmax → 平均 → エントロピーのパターンです(前章のplot_entropy_examplesがこれにあたります)。MC Dropoutサンプリングごとにsoftmaxをとり、その平均からエントロピーを計算しています。

2つ目は、出力ベクトル → 平均 → Softmax → エントロピーのパターンです。MC Dropoutサンプリングの出力ベクトル(softmax前)の平均をとってから、softmaxをかけてエントロピーを算出します。コードは、サンプリング部分を以下のように変更します。
# パターン2:出力ベクトルの平均をとってからSoftmax
for j in range(sampling_num):
preds[j] = model(x).cpu().numpy().squeeze() # softmax前の出力
mean_logits = preds.mean(axis=0)
prob = F.softmax(torch.from_numpy(mean_logits), dim=0).numpy()
entropy[i] = np.sum(-prob * np.log(prob + 1e-12))

結果は、やはり完全一致はしませんが、傾向としては同じようなものになりました。
さらに、Dropoutなし(通常の推論)で 出力ベクトル → Softmax → エントロピー を計算してみると、結果は以下のようになります。
# Dropoutなし(model.eval())で1回だけ推論
model.eval()
with torch.no_grad():
prob = F.softmax(model(x), dim=1).cpu().numpy().squeeze()
entropy[i] = np.sum(-prob * np.log(prob + 1e-12))

こちらも傾向は同じになりました。ということは、例えば目的が能動学習(予測しにくいデータを優先的にラベル付けする手法)に用いるなどであれば、いずれの方法でも似たような効力が得られそうな気がします。
とはいえ、MC Dropoutサンプリングを導出することでベイズの枠組みとして考えられることは論文で理論的に定式化されていますので、サンプリングから予測分布を導出する形まで、数式的にはMC Dropoutが最も納得のいく方法だと思います。
元記事の執筆以降も、深層学習における不確実性推定(Uncertainty Quantification)は活発に研究されています。実務での位置づけを整理します。
手軽に試せるMC Dropoutから始めて、要求される精度やコストに応じてアンサンブルやEvidential系へ広げていく、という使い分けが現在の実務的な流れです。
今回は、モンテカルロ・ドロップアウト(MC Dropout)を用いたベイズ深層学習について解説しました。通常の深層学習では確信度や不確実性をアウトプットすることが難しいという課題がありますが、MC Dropoutを取り入れることで、ベイズ的な不確実性を推定する方法を学びました。
本記事でも触れたとおり、予測の不確実さを表すことで、深層学習の予測がどの程度外しうるか、不得意なデータはどんなものか、能動学習でピックアップすべきデータはどれか、など様々な発展のアイデアが考えられます。
やり方自体はシンプルですので、ぜひ皆さんも身近な場面で応用して試してみてください。