※本コラムは、以前に個人ブログとして公開していた内容を、加筆・再構成のうえ掲載しております。技術的な内容は執筆当時のものであり、現在とは異なる場合がございます。
こんにちは。Anagraftの伊藤です。
ベイズ推論では、機械学習や統計学における従来の確率的手法に加え、不確実性を取り入れることで、より柔軟なモデル構築が可能になります。ベイズニューラルネットワーク(BNN)は、その考え方をニューラルネットワークに適用し、不確実性を考慮した確率的なモデルとして扱うものです。
今回は、ベイズ推論とベイズニューラルネットワークの基本的な概念から、実際のPythonコードを用いた実装例までを紹介します。題材として、Kaggleにある毒キノコ・食用キノコの分類データセットを使ってみます。
元記事の執筆は2018年で、当時はEdwardという確率的プログラミングライブラリを使って実装していました。しかしEdwardはその後開発が終了し、Edward2を経てTensorFlow Probabilityに統合されました。本コラムでは、現在ベイズモデリングで広く使われているPyMCを用いてコードを書き直しています(詳しくは本文と最新動向で触れます)。
まずはベイズ推論の基本的な考え方に触れます。
ベイズ推論では、観測データの集合\( \mathcal{D} \)と未知のパラメータ\( \theta \)について、モデル\( p(\mathcal{D},\theta)=p(\mathcal{D}|\theta)p(\theta) \)を構築し、次の事後分布を求めます。
\( p(\theta|\mathcal{D})=\displaystyle\frac{p(\mathcal{D}|\theta)p(\theta)}{p(\mathcal{D})}=\displaystyle\frac{p(\mathcal{D}|\theta)p(\theta)}{\int p(\mathcal{D}|\theta)p(\theta)d\theta} \)
これを解析的または近似的に求めます。共役な事前分布を仮定すれば事後分布は解析的に求まりますが、そうでない場合は分母の\( \int p(\mathcal{D}|\theta)p(\theta)d\theta \)の計算が非常に困難になるため、代わりにマルコフ連鎖モンテカルロ法や変分推論を使って近似的に事後分布を求めます。
これらを踏まえて、ベイズニューラルネットワークの考え方に移ります。ベイズニューラルネットワークでは、ニューラルネットワークのパラメータ(重みとバイアス)\( {\boldsymbol w} \)がある確率分布に従うと仮定し、その事後分布をベイズ推論で求めます。
\( {\boldsymbol w} \sim N({\boldsymbol 0}, {\boldsymbol I}) \)
\( p(y|{\boldsymbol x},{\boldsymbol w}) = \text{Categorical}\Bigl(\text{softmax}(f({\boldsymbol x},{\boldsymbol w}))\Bigr) \)
\( f(\cdot) : \text{ニューラルネットワークの出力} \)
ニューラルネットワークにベイズを適用する話は歴史的に古くからあり、メリット・デメリットは、ベイズ推論で一般的に言われることと同等です。
ベイズモデルを実装するには、確率的プログラミングライブラリを使うのが一般的です。確率変数や事前分布を宣言的に記述し、MCMCや変分推論といった推論アルゴリズムを呼び出すだけで事後分布を求められます。
元記事ではEdwardというライブラリを使っていました。Edwardは計算にTensorFlowを用いる確率的プログラミングライブラリで、当時は「今後TensorFlowに統合される予定」と紹介していました。実際にその後、EdwardはEdward2を経てTensorFlow Probabilityに統合され、独立したライブラリとしての開発は終了しました。本コラムでは、現在ベイズモデリングで広く使われているPyMCを使います。
2026年時点で、Pythonでベイズモデリングを行う主なライブラリには、PyMC、NumPyro、Pyro、TensorFlow Probability(TFP)などがあります。今回使うPyMCは、Pythonらしい直感的な書き方で確率モデルを記述でき、MCMC(NUTS)や変分推論(ADVI)にも対応しているため、入門にも実務にも使いやすいライブラリです。
例えば、観測データから正規分布の平均\( \mu \)の事後分布を変分推論で求めるコードは、PyMCでは以下のように書けます。
import numpy as np
import pymc as pm
x_train = np.random.randint(10, 20, size=30) # 10〜20の観測値が30個
with pm.Model() as model:
mu = pm.Normal('mu', mu=0.0, sigma=10.0) # 事前分布
x = pm.Normal('x', mu=mu, sigma=1.0, observed=x_train) # 尤度
approx = pm.fit(n=10000, method='advi', random_seed=0) # 変分推論(ADVI)
trace = approx.sample(1000)
print(trace.posterior['mu'].mean().item()) # 14.7 付近
確率変数をpm.Normalなどで宣言し、observedに観測データを渡して、pm.fit(変分推論)やpm.sample(MCMC)で事後分布を求める、という流れです。
それでは、実際にベイズニューラルネットワークを実装してみます。題材は、Kaggleにある毒キノコの分類用データセットです。
食用キノコか毒キノコかがラベル付けされているデータセットです。KaggleからダウンロードしたCSVをdata/に配置した前提で、mushrooms.csvを見てみると、次のようになっています。
import pandas as pd
data = pd.read_csv('./data/mushrooms.csv')
data.head()

一番左のclass列が、pなら毒(poisonous)、eなら食用(edible)です。その他、傘の形・傘の表面・傘の色・匂いなどが格納されている、面白いデータセットです。
カラムの値はすべてカテゴリ(文字列)なので、ダミー変数化します。目的変数(class)を分けて、残りを説明変数とします。今回は、毒キノコである確率を予測する二値分類として扱います。
元記事ではDataFrameのNumPy変換にas_matrix()を使っていましたが、このメソッドは削除されているため、現在はto_numpy()を使います。
import numpy as np
# 目的変数:p(毒)を1、e(食用)を0に
y = (data['class'] == 'p').astype(int).to_numpy()
# 説明変数:class以外のカテゴリ列をダミー変数化
X = pd.get_dummies(data.drop(columns='class')).astype(np.float32).to_numpy()
print(X.shape) # (8124, 117)
from sklearn.model_selection import train_test_split
train_x, test_x, train_y, test_y = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=0)
in_size = train_x.shape[1]
せっかくなので、まず通常の(点推定の)モデルと、ベイズニューラルネットワークの両方を実装し、実装と結果の違いを確認してみましょう。今回は隠れ層のないシンプルな構成にします。隠れ層がなく出力をソフトマックス(二値ならシグモイド)に通す構成は、数学的にはロジスティック回帰と同等です。
通常の(点推定の)モデル
まずは点推定のモデルです。隠れ層のないニューラルネットワーク、すなわちロジスティック回帰をscikit-learnで学習させてみます。
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
clf = LogisticRegression(max_iter=1000)
clf.fit(train_x, train_y)
print('train accuracy:', accuracy_score(train_y, clf.predict(train_x)))
print('test accuracy :', accuracy_score(test_y, clf.predict(test_x)))
キノコのデータは特徴が分かりやすいため、点推定のモデルでもほぼ100%に近い高い精度で分類できます(精度は環境や乱数により多少変動します)。predict_probaを使えばクラスの確率も得られますが、点推定では「重みは一つの値」として求まるため、重みそのものの不確実性(推定のばらつき)は表現されません。
ベイズニューラルネットワーク
次に、同じ構成をベイズニューラルネットワークとしてPyMCで実装します。重み\( {\boldsymbol w} \)とバイアス\( b \)に正規分布の事前分布を置き、変分推論(ADVI)で事後分布を求めます。
import pymc as pm
with pm.Model() as bnn:
# 重みとバイアスに事前分布を置く
w = pm.Normal('w', mu=0.0, sigma=1.0, shape=in_size)
b = pm.Normal('b', mu=0.0, sigma=1.0)
# 線形結合 → シグモイドで毒である確率
logit = pm.math.dot(train_x, w) + b
y_obs = pm.Bernoulli('y_obs', logit_p=logit, observed=train_y)
# 変分推論(ADVI)で事後分布を近似
approx = pm.fit(n=30000, method='advi', random_seed=0)
trace = approx.sample(500) # 事後分布から500セットの重みをサンプリング
学習できたら、テストデータで精度を確認します。ベイズニューラルネットワークでは、重みが確率分布になっているため、同じ入力に対しても予測がばらつきます。そこで、事後分布から重みを複数サンプリングして予測を行い、その平均をとって最終的な予測とします。
# 事後分布からサンプリングした重みで予測(不確実性込み)
post_w = trace.posterior['w'].values.reshape(-1, in_size) # (サンプル数, in_size)
post_b = trace.posterior['b'].values.reshape(-1) # (サンプル数,)
# 各テストサンプルについて、サンプリングした重みごとに毒である確率を計算
logits = test_x @ post_w.T + post_b # (テスト件数, サンプル数)
probs = 1.0 / (1.0 + np.exp(-logits)) # シグモイド
pred_mean = probs.mean(axis=1) # 事後平均(毒である確率)
pred_label = (pred_mean > 0.5).astype(int)
pred_std = probs.std(axis=1) # 事後サンプル間のばらつき(予測の不確実性)
print('test accuracy:', (pred_label == test_y).mean())
ベイズニューラルネットワークでも、点推定のモデルと同様に高い精度で分類できました(精度は環境・乱数・分割により多少変動します)。重要なのは、ベイズの場合は予測が確率分布として得られる点です。pred_meanは毒である予測確率の事後平均で、pred_std(事後サンプル間のばらつき)を見ると、モデルがどれだけ自信を持って判定しているかが分かります。例えば、pred_meanが0.5付近だったり、pred_stdが大きいサンプルは、モデルが判断に迷っているデータということになります。これが、点推定にはないベイズニューラルネットワークの強みです。
キノコのデータは特徴がはっきりしているため、序盤から精度が高く、点推定とベイズで大きな精度差は出ませんでした。ただ、データが少ない場合や、判断が難しいデータに対しては、ベイズの「不確実性を表現できる」という性質が効いてきます。元記事では隠れ層のない構成で試しましたが、隠れ層を入れた本格的なベイズニューラルネットワークも、同じ考え方で実装できます(PyMC公式ギャラリーに例があります)。
元記事の執筆以降、ベイズモデリングを取り巻くライブラリと、不確実性推定の実務的な位置づけは大きく変わりました。ポイントを整理します。
今回は、ベイズニューラルネットワークの基本的な考え方と、PyMCを用いた毒キノコ分類の実装例を紹介しました。
ベイズニューラルネットワークでは、モデルの予測を不確実性とともに表現できます。点推定のモデルが「これは毒です」と一つの答えを返すのに対し、ベイズのモデルは「90%の確率で毒です」「判定が際どいです」といった確信度まで示せます。実務への適用において、この不確実性の表現は、やれることの幅を広げてくれるものだと思います。
元記事で使ったEdwardは役目を終えましたが、ベイズニューラルネットワークやベイズ深層学習という考え方自体は、PyMCやNumPyroといった現代のライブラリに引き継がれ、むしろ扱いやすくなっています。この記事が、ベイズ推論とベイズニューラルネットワークの基本を理解する一助となれば幸いです。