ロジスティック回帰の理論と実装をわかりやすく解説

ロジスティック回帰(Logistic Regression)は、二値分類問題を解くための最も基本的な統計モデルです。名前に「回帰」とありますが、分類モデルであり、入力変数から0から1の確率値を予測します。

線形回帰が連続値を予測するのに対し、ロジスティック回帰はシグモイド関数を用いて出力を確率に変換し、「ある事象が起こる確率」を予測します。

本記事の内容

  • シグモイド関数とロジスティック回帰の回帰式
  • 最尤推定によるパラメータ学習
  • 勾配降下法による最適化
  • Pythonでのスクラッチ実装と決定境界の可視化

前提知識

この記事を読む前に、以下の概念を押さえておくと理解が深まります。

  • 重回帰分析の基礎
  • 最尤推定法の考え方

ロジスティック回帰の回帰式

シグモイド関数

ロジスティック回帰の核となるのはシグモイド関数(ロジスティック関数)です。

$$ \sigma(z) = \frac{1}{1 + e^{-z}} $$

シグモイド関数の性質:

  • 値域は $(0, 1)$ であり、確率として解釈可能
  • $z = 0$ のとき $\sigma(0) = 0.5$
  • $z \to \infty$ のとき $\sigma(z) \to 1$、$z \to -\infty$ のとき $\sigma(z) \to 0$

ロジスティック回帰モデル

入力 $\bm{x} = (x_1, x_2, \dots, x_p)^T$ に対し、クラス1に属する確率を以下でモデル化します。

$$ p(y=1|\bm{x}) = \sigma(\bm{w}^T\bm{x} + b) = \frac{1}{1 + \exp(-(\bm{w}^T\bm{x} + b))} $$

ここで $\bm{w} \in \mathbb{R}^p$ は重みベクトル、$b$ はバイアスです。

対数オッズとの関係

ロジスティック回帰では、対数オッズ(log-odds)が線形関数になっています。

$$ \ln\frac{p}{1-p} = \bm{w}^T\bm{x} + b $$

$p/(1-p)$ はオッズと呼ばれ、「事象が起こる確率と起こらない確率の比」を表します。

最尤推定によるパラメータ学習

$n$ 個のデータ $\{(\bm{x}_i, y_i)\}_{i=1}^{n}$($y_i \in \{0, 1\}$)が与えられたとき、尤度関数は、

$$ L(\bm{w}, b) = \prod_{i=1}^{n} p_i^{y_i}(1 – p_i)^{1-y_i} $$

ここで $p_i = \sigma(\bm{w}^T\bm{x}_i + b)$ です。対数尤度は、

$$ \ell(\bm{w}, b) = \sum_{i=1}^{n} [y_i \ln p_i + (1 – y_i)\ln(1 – p_i)] $$

これは交差エントロピー損失(binary cross-entropy)の負値に相当します。

$$ L_{\text{BCE}} = -\frac{1}{n}\sum_{i=1}^{n}[y_i\ln p_i + (1-y_i)\ln(1-p_i)] $$

勾配の導出

対数尤度を $\bm{w}$ で微分すると、

$$ \begin{align} \frac{\partial \ell}{\partial \bm{w}} &= \sum_{i=1}^{n} (y_i – p_i)\bm{x}_i \end{align} $$

この導出にはシグモイド関数の微分の性質を利用しています。

$$ \sigma'(z) = \sigma(z)(1 – \sigma(z)) $$

勾配降下法による更新式:

$$ \bm{w} \leftarrow \bm{w} + \eta \sum_{i=1}^{n}(y_i – p_i)\bm{x}_i $$

$$ b \leftarrow b + \eta \sum_{i=1}^{n}(y_i – p_i) $$

ここで $\eta$ は学習率です。

重回帰分析との違い

線形回帰 ロジスティック回帰
目的変数 連続値(身長、価格など) 確率値(0-1)
活性化関数 なし(恒等関数) シグモイド関数
損失関数 MSE 交差エントロピー
最適化 解析解あり 勾配降下法(解析解なし)

Pythonでの実装

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

np.random.seed(42)

# --- シグモイド関数 ---
def sigmoid(z):
    return 1 / (1 + np.exp(-np.clip(z, -500, 500)))

# --- ロジスティック回帰のスクラッチ実装 ---
class LogisticRegressionScratch:
    def __init__(self, lr=0.01, n_iter=1000):
        self.lr = lr
        self.n_iter = n_iter

    def fit(self, X, y):
        n, p = X.shape
        self.w = np.zeros(p)
        self.b = 0.0
        self.losses = []

        for _ in range(self.n_iter):
            z = X @ self.w + self.b
            p_hat = sigmoid(z)

            # 交差エントロピー損失
            loss = -np.mean(y * np.log(p_hat + 1e-8) + (1 - y) * np.log(1 - p_hat + 1e-8))
            self.losses.append(loss)

            # 勾配の計算
            error = y - p_hat
            grad_w = -(1 / n) * X.T @ error
            grad_b = -(1 / n) * error.sum()

            # パラメータの更新
            self.w -= self.lr * grad_w
            self.b -= self.lr * grad_b

        return self

    def predict_proba(self, X):
        return sigmoid(X @ self.w + self.b)

    def predict(self, X, threshold=0.5):
        return (self.predict_proba(X) >= threshold).astype(int)

# --- データ生成 ---
X, y = make_classification(n_samples=300, n_features=2, n_redundant=0,
                            n_informative=2, random_state=42, n_clusters_per_class=1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# --- 学習 ---
model = LogisticRegressionScratch(lr=0.1, n_iter=500)
model.fit(X_train, y_train)

y_pred = model.predict(X_test)
print(f"テスト精度: {accuracy_score(y_test, y_pred):.4f}")

# --- 可視化 ---
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# シグモイド関数
ax0 = axes[0]
z = np.linspace(-8, 8, 200)
ax0.plot(z, sigmoid(z), 'b-', linewidth=2)
ax0.axhline(y=0.5, color='gray', linestyle='--', alpha=0.5)
ax0.axvline(x=0, color='gray', linestyle='--', alpha=0.5)
ax0.set_xlabel('z')
ax0.set_ylabel('$\\sigma(z)$')
ax0.set_title('Sigmoid Function')
ax0.grid(True, alpha=0.3)

# 決定境界
ax1 = axes[1]
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),
                      np.linspace(y_min, y_max, 200))
Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)

ax1.contourf(xx, yy, Z, levels=50, cmap='RdBu_r', alpha=0.6)
ax1.scatter(X_test[y_test == 0, 0], X_test[y_test == 0, 1],
            c='blue', s=30, label='Class 0')
ax1.scatter(X_test[y_test == 1, 0], X_test[y_test == 1, 1],
            c='red', s=30, label='Class 1')
ax1.contour(xx, yy, Z, levels=[0.5], colors='black', linewidths=2)
ax1.set_xlabel('$x_1$')
ax1.set_ylabel('$x_2$')
ax1.set_title('Decision Boundary')
ax1.legend()
ax1.grid(True, alpha=0.3)

# 学習曲線
ax2 = axes[2]
ax2.plot(model.losses, 'b-')
ax2.set_xlabel('Iteration')
ax2.set_ylabel('Cross-Entropy Loss')
ax2.set_title('Training Loss')
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

まとめ

本記事では、ロジスティック回帰の理論と実装について解説しました。

  • ロジスティック回帰はシグモイド関数を用いて入力を確率に変換する二値分類モデル
  • 対数オッズが入力の線形関数になるという解釈ができる
  • 最尤推定により交差エントロピー損失を最小化してパラメータを学習する
  • 勾配 $\nabla_{\bm{w}} \ell = \sum(y_i – p_i)\bm{x}_i$ は直感的に解釈しやすい形をしている

次のステップとして、以下の記事も参考にしてください。