目次

スパムフィルターがなぜ学習しながら精度を上げるのか。医療検査の陽性結果がなぜ「必ずしも病気とは言えない」のか。これらの問いに答えるのがベイズ統計だ。ベイズ統計は「既知の情報(事前知識)に新しい証拠を組み合わせて、信念を更新する」という推論の枠組みを提供する。

頻度主義 vs ベイズ統計

まず、統計の2つの流派の違いを押さえておこう。

頻度主義(Frequentist): 確率は「長期的な頻度」として定義される。コインを無限回投げたときに表が出る割合が0.5なら、表の確率は0.5だ。データを見て「この仮説が正しいか」を検定する。

ベイズ統計(Bayesian): 確率は「信念の度合い」として定義される。「明日雨が降る確率は70%」という文が意味を持つ。事前に持つ信念(事前確率)を新しいデータで更新して事後確率を得る。

どちらが正しいというわけではなく、問題の性質によって使い分ける。AI・機械学習ではベイズ的な考え方が多く使われる。

ベイズの定理

ベイズ統計の核心はベイズの定理だ。

P(H|E) = P(E|H) × P(H) / P(E)
記号名称意味
P(H)事前確率(Prior)データを見る前の「H が正しい」信念
`P(EH)`尤度(Likelihood)
`P(HE)`事後確率(Posterior)
P(E)周辺尤度(Evidence)証拠 E の周辺確率(正規化定数)

言葉で表すと:事後確率 ∝ 尤度 × 事前確率

新しい証拠を見るたびに、事後確率が新しい事前確率になる。これが「ベイズ更新」だ。

直感的な例:医療検査

医療検査を例にベイズの定理の威力を見てみよう。

設定:

  • ある病気の有病率: 1%(事前確率)
  • 検査の感度(病気の人が陽性になる確率): 99%
  • 検査の特異度(健康な人が陰性になる確率): 95%

検査が陽性だったとき、本当に病気である確率は?

def bayes_medical(prevalence, sensitivity, specificity):
    """
    prevalence: 有病率 P(病気)
    sensitivity: 感度 P(陽性|病気)
    specificity: 特異度 P(陰性|健康)
    """
    p_disease = prevalence
    p_healthy = 1 - prevalence

    p_positive_given_disease = sensitivity
    p_positive_given_healthy = 1 - specificity  # 偽陽性率

    # ベイズの定理
    # P(病気|陽性) = P(陽性|病気) × P(病気) / P(陽性)

    # P(陽性) = P(陽性|病気)×P(病気) + P(陽性|健康)×P(健康)
    p_positive = (
        p_positive_given_disease * p_disease +
        p_positive_given_healthy * p_healthy
    )

    p_disease_given_positive = (
        p_positive_given_disease * p_disease / p_positive
    )

    return p_disease_given_positive

result = bayes_medical(
    prevalence=0.01,    # 有病率 1%
    sensitivity=0.99,   # 感度 99%
    specificity=0.95    # 特異度 95%
)
print(f"陽性的中率: {result:.1%}")  # 約 16.7%

結果は約16.7%——99%精度の検査で陽性でも、本当に病気の確率は17%に過ぎない。

これは直感に反するが数学的には正しい。有病率が低い(事前確率が低い)ため、偽陽性の絶対数が多くなるからだ。この「検査の精度に対する過信」を「基準率の無視(Base Rate Neglect)」と呼ぶ。

スパムフィルター——ナイーブベイズ

メールのスパムフィルターはナイーブベイズ分類器の代表例だ。

import numpy as np
from collections import defaultdict

class NaiveBayesSpamFilter:
    def __init__(self):
        self.word_spam_count = defaultdict(int)
        self.word_ham_count = defaultdict(int)
        self.spam_total = 0
        self.ham_total = 0
        self.vocab = set()

    def train(self, emails):
        """emails: [(テキスト, ラベル)] のリスト"""
        for text, label in emails:
            words = text.lower().split()
            self.vocab.update(words)
            if label == 'spam':
                self.spam_total += 1
                for word in words:
                    self.word_spam_count[word] += 1
            else:
                self.ham_total += 1
                for word in words:
                    self.word_ham_count[word] += 1

    def predict(self, text):
        total = self.spam_total + self.ham_total

        # 事前確率 P(スパム), P(正常)
        log_prob_spam = np.log(self.spam_total / total)
        log_prob_ham = np.log(self.ham_total / total)

        words = text.lower().split()
        for word in words:
            if word not in self.vocab:
                continue
            # ラプラス平滑化(未知語を0にしない)
            vocab_size = len(self.vocab)

            p_word_spam = (self.word_spam_count[word] + 1) / (
                sum(self.word_spam_count.values()) + vocab_size
            )
            p_word_ham = (self.word_ham_count[word] + 1) / (
                sum(self.word_ham_count.values()) + vocab_size
            )

            log_prob_spam += np.log(p_word_spam)
            log_prob_ham += np.log(p_word_ham)

        return 'spam' if log_prob_spam > log_prob_ham else 'ham'

# 使用例
filter = NaiveBayesSpamFilter()
training_data = [
    ("無料 お金 今すぐ クリック", "spam"),
    ("会議 明日 資料 送ります", "ham"),
    ("当選 お祝い 無料 プレゼント", "spam"),
    ("プロジェクト 進捗 報告 よろしく", "ham"),
]
filter.train(training_data)
print(filter.predict("無料 クリック 当選"))  # spam
print(filter.predict("明日 会議 資料"))       # ham

「ナイーブ」と呼ばれる理由は、各単語が独立と仮定しているから(実際は相関がある)。それでも実用上は十分な精度を発揮する。

ベイズ更新——逐次的な信念の更新

ベイズ統計の強みは、データが増えるにつれて信念を更新できることだ。

コインが公正かどうかを判断する例を考えよう。

import numpy as np
import matplotlib.pyplot as plt

def beta_posterior(heads, tails, alpha_prior=1, beta_prior=1):
    """
    ベータ分布を事前分布として使ったベイズ推定
    alpha_prior, beta_prior: 事前分布のパラメータ(均一分布=1,1)
    """
    alpha_posterior = alpha_prior + heads
    beta_posterior = beta_prior + tails
    return alpha_posterior, beta_posterior

def posterior_mean(alpha, beta):
    return alpha / (alpha + beta)

# 初期状態(均一事前分布: どんな確率値も等しく可能)
alpha, beta = 1, 1
print(f"初期推定: {posterior_mean(alpha, beta):.2f}")

# コインを投げるたびに更新
observations = [1, 0, 1, 1, 0, 1, 1, 0, 1, 1]  # 1=表, 0=裏
for i, obs in enumerate(observations):
    if obs == 1:
        alpha, beta = beta_posterior(1, 0, alpha, beta)
    else:
        alpha, beta = beta_posterior(0, 1, alpha, beta)
    print(f"投げ {i+1}回後: 表率推定 = {posterior_mean(alpha, beta):.2f} "
          f"(α={alpha}, β={beta})")
初期推定: 0.50
投げ 1回後: 表率推定 = 0.67 (α=2, β=1)
投げ 2回後: 表率推定 = 0.60 (α=2, β=2)
投げ 3回後: 表率推定 = 0.67 (α=3, β=2)
...
投げ10回後: 表率推定 = 0.69 (α=8, β=4)

データが増えるほど事後確率の「幅」が狭まり、推定が確実になっていく。これがベイズ更新の直感だ。

ベイズ推論とAI

不確かさの定量化

ベイズ的ニューラルネットワーク(BNN)では、重みを点推定ではなく確率分布として扱う。これにより予測の「自信度」を定量化できる。

# 概念コード(実際は PyMC や TensorFlow Probability を使う)

# 通常のNN: 重みは固定値
# weight = 0.7

# ベイズNN: 重みは分布
# weight ~ Normal(mean=0.7, std=0.1)
# 予測時にサンプリングして不確かさを推定

# 実用的な近似: モンテカルロドロップアウト
import torch
import torch.nn as nn

class BayesianApproxNet(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim, dropout_p=0.1):
        super().__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.fc2 = nn.Linear(hidden_dim, output_dim)
        self.dropout = nn.Dropout(p=dropout_p)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = self.dropout(x)  # 推論時もドロップアウトをONにする
        return self.fc2(x)

    def predict_with_uncertainty(self, x, n_samples=50):
        """複数回予測して分散を不確かさとして使う"""
        self.train()  # ドロップアウトを有効にする
        predictions = torch.stack([self(x) for _ in range(n_samples)])
        mean = predictions.mean(0)
        std = predictions.std(0)
        return mean, std

ハイパーパラメータ最適化

ベイズ最適化(Bayesian Optimization)はハイパーパラメータ探索に使われる。過去の評価結果から「次にどこを試すべきか」を確率モデルで推定する。

# scikit-optimize ライブラリを使ったベイズ最適化の例
from skopt import gp_minimize
from skopt.space import Real, Integer

def objective(params):
    learning_rate, n_layers = params
    # ここで実際にモデルを学習して検証スコアを返す
    # (サンプルなので代替の計算を使う)
    return -(learning_rate * 10 - (learning_rate - 0.01)**2 * 1000)

result = gp_minimize(
    objective,
    dimensions=[
        Real(1e-4, 1e-1, prior='log-uniform'),  # 学習率
        Integer(1, 5)                            # 層数
    ],
    n_calls=20,          # 試行回数
    random_state=42
)
print(f"最適パラメータ: lr={result.x[0]:.4f}, layers={result.x[1]}")

事前分布の選び方

事前分布の選択がベイズ推論の結果に影響する。

分布用途特性
一様分布事前知識がない場合すべての値が等確率
ベータ分布確率値(0〜1)の推定スパムフィルターの単語確率
正規分布実数値パラメータ重みの正則化(L2=ガウス事前分布)
ラプラス分布スパースな解重みの正則化(L1=ラプラス事前分布)

正則化とベイズ統計の関係は興味深い。L2正則化は「重みはゼロ付近のガウス分布に従う」という事前分布に相当し、L1正則化は「重みはラプラス分布に従う(ゼロが多い)」という事前分布に相当する。

頻度主義の限界とベイズの強み

頻度主義が苦手なケース:

  • データが少ない(過学習しやすい)
  • 複数の仮説を比較したい
  • 予測の不確かさを表現したい

ベイズの強み:

  • 少量データでも事前知識を活かせる
  • 不確かさを確率として自然に表現
  • 新しいデータが来るたびに逐次更新できる

ベイズのデメリット:

  • 計算コストが高い(MCMC サンプリングなど)
  • 事前分布の選択が主観的
  • 大規模モデルへの適用が難しい

まとめ

ベイズ統計は「不確かさを確率で表現し、証拠が増えるほど信念を更新する」という直感的な推論の枠組みだ。

  • ベイズの定理: 事後確率 = 尤度 × 事前確率 / 周辺尤度
  • スパムフィルター: ナイーブベイズの実用的な成功例
  • 医療検査: 基準率の無視に注意——有病率が低いと陽性的中率も低い
  • ベイズ更新: データが増えるほど推定が確実に
  • 機械学習への応用: 不確かさの定量化・ベイズ最適化・正則化の統一的理解

「確率は長期的な頻度ではなく、信念の度合い」というベイズ的世界観を身に付けると、AI が「どれくらい自信を持って予測しているか」を問う視点が生まれる。

免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。