ロジスティック回帰とは
ロジスティック回帰は、主に分類問題に用いられる統計学・機械学習の手法です。入力変数(特徴量)から出力ラベル(クラス)への所属確率を予測するため、結果を確率として解釈できます。特に、二値分類(例:スパムか否か、病気の有無など)でよく使われますが、適切な拡張により複数クラスに対応することも可能です。
基本的な考え方
- 目的:入力データに基づいて、対象が特定のクラスに属する確率を出力する。
- 特徴:線形モデルの枠組みを用いつつ、出力を0〜1の範囲に収めるために非線形のシグモイド関数を適用する。
- 応用例:医療診断、マーケティング分析、金融リスク評価など。
数学的な背景
シグモイド関数
ロジスティック回帰では、シグモイド関数(またはロジスティック関数)を用いて、実数値の線形結合を確率に変換します。
シグモイド関数の定義は次の通りです。
$$\sigma(z) = \frac{1}{1 + e^{-z}}$$
- z は線形結合の結果(後述)で、シグモイド関数は入力を「S字状」の曲線に写像し、常に0から1の間の値を返します。
モデルの表現
- 線形結合 入力ベクトル \(\mathbf{x} = (x_1, x_2, \dots, x_n)\) に対し、パラメータ \(\beta = (\beta_0, \beta_1, \dots, \beta_n)\) を用いて以下のように線形結合を行います。 $$z = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \dots + \beta_n x_n$$
- 確率の計算 線形結合 z にシグモイド関数を適用して、クラス1に属する確率を求めます。 $$P(y = 1 \mid \mathbf{x}) = \sigma(z) = \frac{1}{1 + e^{-z}}$$ これにより、出力は確率として解釈可能となります。
コスト関数
ロジスティック回帰では、モデルのパラメータを最適化するために交差エントロピー誤差(対数損失)をコスト関数として採用します。
サンプル数を m とすると、コスト関数 J(\beta) は次のように定義されます。
$$J(\beta) = -\frac{1}{m} \sum_{i=1}^{m} \left[ y^{(i)} \log\left(\sigma(z^{(i)})\right) + (1 – y^{(i)}) \log\left(1 – \sigma(z^{(i)})\right) \right]$$
- \(y^{(i)}\) は正解ラベル(0または1)
- \(\sigma(z^{(i)})\) はサンプル i に対する予測確率
この損失関数の値を最小化することで、最も適切なパラメータ \(\beta\) を求めます。
パラメータ推定
- 最適化手法:一般に、勾配降下法や確率的勾配降下法(SGD)を用いてコスト関数を最小化し、パラメータを学習します。
- 収束条件:損失関数の変化が小さくなった時点、またはあらかじめ決めた反復回数に達した場合に学習を終了します。
利点と注意点
利点
- 解釈性が高い:各特徴量の重み(\(\beta_i\))から、特徴が分類に与える影響が理解できます。
- 確率出力:出力が0〜1の範囲に収まり、確率として解釈できるため、意思決定に役立ちます。
- 計算効率が良い:パラメータが少ない場合、学習や推論が高速です。
注意点
- 線形決定境界:基本的には線形な境界しか表現できないため、複雑なデータに対しては性能が劣る場合があります。
- 特徴量の前処理:入力データのスケールが大きく異なる場合、学習に悪影響を及ぼすことがあるため、標準化などの前処理が重要です。
- 多重共線性:特徴量間の相関が強いと、パラメータの推定が不安定になる可能性があります。
多項ロジスティック回帰
複数クラスを識別する必要がある場合、多項ロジスティック回帰(ソフトマックス回帰)が用いられます。
- 仕組み:各クラスに対して線形結合を計算し、ソフトマックス関数によって各クラスに属する確率を出力します。
- ソフトマックス関数の定義: $$P(y = k \mid \mathbf{x}) = \frac{e^{z_k}}{\sum_{j} e^{z_j}}$$ ここで、\(z_k\) はクラス k に対する線形結合の結果です。
ロジスティック回帰モデルの実装
では、ここまで学習した理論をプログラムに落とし込んでみましょう。ここではGoogle colaboratoryの環境で実行していきます。
必要なライブラリをインポートしておきます。
from pathlib import Path
import numpy as np
from sklearn.datasets import fetch_openml
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
DATA_DIR = "/content/drive/MyDrive" #ここにデータがダウンロードされます
pathlib.Path
: ファイルやディレクトリのパスをオブジェクト指向で操作するために使用します。numpy
(asnp
): 高速な数値計算、特に多次元配列(ベクトル、行列など)の操作を行うために使用します。sklearn.datasets.fetch_openml
: OpenML.orgから機械学習用データセットをダウンロードするために使用します。sklearn.metrics.accuracy_score
: 機械学習モデルの分類精度(正解率)を計算するために使用します。sklearn.model_selection.train_test_split
: データセットを訓練用とテスト用に分割するために使用します。sklearn.utils.shuffle
: データセットや配列の要素をランダムに並び替える(シャッフルする)ために使用します。
シグモイド関数の実装
def sigmoid_test(x):
return 1 / (1 + np.exp(- x))
def sigmoid(x):
return 0.5 * (1 + np.tanh(x / 2))
本来であればsigmoid_testのように書くのが公式通りですがこれを機会で実行するとOverflowという現象が起こってしまいます(桁が小さくなりすぎて値が消失してしまう現象が起こる)。
np.tanh
: NumPyライブラリに含まれる双曲線正接関数(ハイパボリックタンジェント)を計算する関数です。- 数式:
tanh(x) = (e^x - e^-x) / (e^x + e^-x)
で定義されます。 - 特徴:
- 入力値
x
に対して、出力値は必ず -1から1の範囲 に収まります。 - 形状がシグモイド関数 (
1 / (1 + exp(-x))
) に似ており、同じく活性化関数などとして利用されます。
- 入力値
- コードでの利用:
np.tanh(x * 0.5) * 0.5 + 0.5
という計算を行うことで、出力範囲を 0から1の範囲 に調整し、標準的なシグモイド関数と同じ出力範囲になるようにしています。- コメントにあるように、
np.exp(-x)
を直接計算するとx
の値によっては指数関数の計算でオーバーフロー(非常に大きな値になりすぎて計算できなくなること)が発生する可能性があります。np.tanh
は内部的に数値的な安定性を考慮した実装になっているため、オーバーフローのリスクを低減できます
データセットの設定と重みづけ
# ORのデータセット
x_train_or = np.array([[0, 1], [1, 0], [0, 0], [1, 1]])
t_train_or = np.array([[1], [1], [0], [1]])
x_valid_or, t_valid_or = x_train_or, t_train_or
x_test_or, t_test_or = x_train_or, t_train_or
# 重み (入力の次元数: 2, 出力の次元数: 1)
W_or = np.random.uniform(low=-0.08, high=0.08, size=(2, 1)).astype("float32")
b_or = np.zeros(shape=(1,)).astype("float32")
- 目的: ORゲートを機械学習モデルで学習させるための準備。
- ライブラリ: 数値計算ライブラリ NumPy (
np
) を使用。 - データセット:
x_train_or
: ORゲートの入力データ(全4パターン)。t_train_or
:x_train_or
に対応する正解出力。x_valid_or
,t_valid_or
: 検証用データ(訓練データと同じ)。x_test_or
,t_test_or
: テスト用データ(訓練データと同じ)。
- モデルパラメータ初期化:
W_or
: 重み行列(入力次元2、出力次元1)。-0.08から0.08の範囲の乱数で初期化。b_or
: バイアス項(出力次元1)。ゼロで初期化。- データ型: 全てのNumPy配列を
float32
(32ビット浮動小数点数)に指定。
train関数とvalid関数を実装
# Numpyを用いてLogの計算結果が0になるのを防ぐ
def np_log(x):
return np.log(np.clip(a=x, a_min=1e-10, a_max=1e1000))
def train_or(x, t, eps=1.0):
"""
:param x: np.ndarray, 入力データ, shape=(batch_size, 入力の次元数)
:param t: np.ndarray, 教師ラベル, shape=(batch_size, 出力の次元数)
:param eps: float, 学習率
"""
global W_or, b_or
batch_size = x.shape[0]
# 順伝播
y = sigmoid(np.matmul(x, W_or) + b_or) # shape: (batch_size, 出力の次元数)
# 逆伝播
cost = (-t * np_log(y) - (1 - t) * np_log(1 - y)).mean()
delta = y - t # shape: (batch_size, 出力の次元数)
# パラメータの更新
dW = np.matmul(x.T, delta) / batch_size # shape: (入力の次元数, 出力の次元数)
db = np.matmul(np.ones(shape=(batch_size,)), delta) / batch_size # shape: (出力の次元数,)
W_or -= eps * dW
b_or -= eps * db
return cost
def valid_or(x, t):
y = sigmoid(np.matmul(x, W_or) + b_or)
cost = (-t * np_log(y) - (1 - t) * np_log(1 - y)).mean()
return cost, y
np_log(x)
関数:- NumPyの
log
関数のカスタム版。 - 入力
x
の値がゼロや非常に小さい値になるのを防ぐため、計算前に値を一定範囲内(1e-10
~1e1000
)にクリップ(制限)する。 - 目的:
log(0)
によるエラーや計算の不安定性を回避するため。
- NumPyの
train_or(x, t, eps=1.0)
関数:- ORゲートモデルの1ステップ分の訓練を実行する関数。
- 入力
x
と正解t
を受け取り、モデルのパラメータ (W_or
,b_or
) を更新する。 - 処理フロー:
- 順伝播: 入力
x
と現在のW_or
,b_or
から、シグモイド関数 (sigmoid
) を使って予測値y
を計算。 - 損失計算: 予測値
y
と正解t
から交差エントロピー誤差 (cost
) を計算(np_log
を使用)。 - 逆伝播 (勾配計算): 損失を最小化するための
W_or
とb_or
の勾配 (dW
,db
) を計算。 - パラメータ更新: 計算した勾配と学習率
eps
を使ってW_or
とb_or
を更新(勾配降下法)。
- 順伝播: 入力
- 計算された損失
cost
を返す。 - グローバル変数
W_or
,b_or
を直接変更する。
valid_or(x, t)
関数:- 現在のモデルの性能を検証する関数(パラメータは更新しない)。
- 入力
x
と正解t
を受け取る。 - 処理フロー:
- 順伝播: 入力
x
と現在のW_or
,b_or
から予測値y
を計算。 - 損失計算: 予測値
y
と正解t
から交差エントロピー誤差cost
を計算。
- 順伝播: 入力
- 計算された損失
cost
とモデルの出力y
を返す。
For文で学習を行わせる
for epoch in range(1000):
# online学習
x_train_or, t_train_or = shuffle(x_train_or, t_train_or)
for x, t in zip(x_train_or, t_train_or):
cost = train_or(x[None, :], t[None, :])
cost, y_pred = valid_or(x_valid_or, t_valid_or)
print(y_pred)
- 目的: 定義済みの
train_or
関数とvalid_or
関数を使って、ORゲートモデルを実際に訓練するループ。 - ループ:
for epoch in range(1000):
で、訓練プロセスを 1000回繰り返す(1000エポック)。
- エポック内の処理:
- データシャッフル:
x_train_or, t_train_or = shuffle(x_train_or, t_train_or)
: 各エポックの開始時に、訓練データの順番をランダムに入れ替える(shuffle
関数の実装が必要)。学習の偏りを防ぐため。
- オンライン学習 (1サンプルずつの学習):
for x, t in zip(x_train_or, t_train_or):
: シャッフルされた訓練データから、入力x
と正解t
のペアを1つずつ取り出すループ。cost = train_or(x[None, :], t[None, :])
: 取り出した1つのデータサンプルでtrain_or
関数を呼び出し、モデルの重みとバイアスを更新する。x[None, :]
はx
を1サンプルだけのバッチ(2次元配列)に変換する。
- 検証:
cost, y_pred = valid_or(x_valid_or, t_valid_or)
: 1エポック(全ての訓練データを1回使用)が終了した後、検証データを使って現在のモデルの損失 (cost
) と予測値 (y_pred
) を計算する。パラメータ更新はしない。
- データシャッフル:
- 最終結果の表示:
print(y_pred)
: 1000エポックの訓練が全て完了した後の、最後の検証ステップで得られた予測値 (y_pred
) を表示する。これは、訓練されたモデルが検証データ(ORゲートの全入力パターン)に対してどのような出力をするかを示す。
コード全体のまとめ
from pathlib import Path
import numpy as np
from sklearn.datasets import fetch_openml
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
DATA_DIR = "/content/drive/MyDrive"
def sigmoid(x):
return 0.5 * (1 + np.tanh(x / 2))
x_train_or = np.array([[0, 1], [1, 0], [0, 0], [1, 1]])
t_train_or = np.array([[1], [1], [0], [1]])
x_valid_or, t_valid_or = x_train_or, t_train_or
x_test_or, t_test_or = x_train_or, t_train_or
W_or = np.random.uniform(low=-0.08, high=0.08, size=(2, 1)).astype("float32")
b_or = np.zeros(shape=(1,)).astype("float32")
def np_log(x):
return np.log(np.clip(a=x, a_min=1e-10, a_max=1e1000))
def train_or(x, t, eps=1.0):
global W_or, b_or
batch_size = x.shape[0]
y = sigmoid(np.matmul(x, W_or) + b_or)
cost = (-t * np_log(y) - (1 - t) * np_log(1 - y)).mean()
delta = y - t
dW = np.matmul(x.T, delta) / batch_size
db = np.matmul(np.ones(shape=(batch_size,)), delta) / batch_size
W_or -= eps * dW
b_or -= eps * db
return cost
def valid_or(x, t):
y = sigmoid(np.matmul(x, W_or) + b_or)
cost = (-t * np_log(y) - (1 - t) * np_log(1 - y)).mean()
return cost, y
for epoch in range(1000):
x_train_or, t_train_or = shuffle(x_train_or, t_train_or)
for x, t in zip(x_train_or, t_train_or):
cost = train_or(x[None, :], t[None, :])
cost, y_pred = valid_or(x_valid_or, t_valid_or)
print(y_pred)
コードの説明
- ライブラリのインポートと初期設定:
numpy
などの必要なライブラリをインポートします。- 乱数シードを設定して結果の再現性を確保します。
- データディレクトリのパスを設定します(このコードでは直接使用されていませんが、設定されています)。
- シグモイド関数:
sigmoid
関数を定義します。これはロジスティック回帰の活性化関数として使われ、入力を0から1の間の確率値に変換します。ここでは数値的安定性のためにtanh
を使って実装されています。
- データセットの準備:
- ORゲートの入力 (
x_train_or
) と対応する正しい出力 (t_train_or
) を定義します。この例では、訓練データ、検証データ、テストデータはすべて同じものを使用しています。
- ORゲートの入力 (
- モデルパラメータの初期化:
- ロジスティック回帰モデルの重み (
W_or
) とバイアス (b_or
) をランダムな値とゼロで初期化します。
- ロジスティック回帰モデルの重み (
- 補助関数:
np_log
関数を定義します。これは対数計算時に引数がゼロや非常に小さい値になるのを防ぐためのものです。
- 学習関数 (
train_or
):- 入力データ (
x
) と教師ラベル (t
) を受け取り、モデルの予測 (y
) を計算します(順伝播)。 - 予測と教師ラベルから損失(交差エントロピー誤差)を計算します。
- 誤差に基づいて勾配 (
dW
,db
) を計算します(逆伝播)。 - 計算された勾配と学習率 (
eps
) を使って、重み (W_or
) とバイアス (b_or
) を更新します。 - 計算されたコスト(損失)を返します。
- 入力データ (
- 検証関数 (
valid_or
):- 入力データ (
x
) と教師ラベル (t
) を受け取り、現在のモデルパラメータを使って予測 (y
) と損失 (cost
) を計算します。パラメータの更新は行いません。 - 計算されたコストと予測値を返します。
- 入力データ (
- 学習ループ:
- 指定されたエポック数(ここでは1000回)だけ学習を繰り返します。
- 各エポックの開始時に訓練データをシャッフルします。
- 訓練データを1つずつ(オンライン学習)
train_or
関数に渡し、モデルのパラメータを更新します。 - 各エポックの終わりに
valid_or
関数を使って検証データに対する損失と予測値を計算します(ただし、このループ内ではcost
とy_pred
は毎エポック上書きされます)。
- 結果の表示:
- 学習ループが終了した後、最後に計算された検証データに対する予測値 (
y_pred
) を表示します。これにより、学習後のモデルがORゲートの各入力に対してどのような確率を出力するかがわかります。
- 学習ループが終了した後、最後に計算された検証データに対する予測値 (
おまけ:多クラス分類用のソフトマックス回帰の実装
先ほどまでのロジスティック回帰分析では2値分類しかできないため複数のクラスに分類したい際にはソフトマックス回帰を用いる必要があります。
$$\mathrm{softmax}({\bf x})_k = \frac{\exp({\bf x}_k)}{\sum^K_{k’=1} \exp({\bf x}_{k’})} \hspace{10mm} \text{for} \, k=1,\ldots, K$$
\({\bf x}\)の要素の中に大きなものがある場合,発散してしまうため,下にあるように普通,
$${\bf x} \leftarrow {\bf x} – (\bf{x}_{max})$$
としてからsoftmax関数にかけることで,オーバーフローを防ぐ。
def softmax(x):
x -= x.max(axis=1, keepdims=True) # expのunderflow & overflowを防ぐ
x_exp = np.exp(x)
return x_exp / np.sum(x_exp, axis=1, keepdims=True)
- 目的: ソフトマックス関数を計算する。これは、入力された数値ベクトル(スコア)を、合計が1になる確率分布に変換するためによく使われる(特に出力層で)。
- 入力: NumPy配列
x
。通常、各行が1つのサンプル、各列がそのサンプルの各クラスに対するスコアを表す(例:shape=(batch_size, num_classes)
)。 - 処理フロー:
- オーバーフロー/アンダーフロー対策:
x -= x.max(axis=1, keepdims=True)
: 各行(サンプルごと)の最大値を、その行の全ての要素から引く。- 理由: 指数関数
np.exp()
の計算時に、非常に大きな値(オーバーフロー)や非常に小さな値(アンダーフロー)が発生するのを防ぐため。この操作はソフトマックス関数の結果を変えない。
- 指数関数:
x_exp = np.exp(x)
: 値を引いた後のx
の各要素に対して、指数関数exp
を適用する。
- 正規化:
return x_exp / np.sum(x_exp, axis=1, keepdims=True)
: 指数関数を適用した結果 (x_exp
) を、各行(サンプルごと)の合計値で割る。axis=1
: 行ごとに合計を計算。keepdims=True
: 合計計算後も配列の次元数を維持し、ブロードキャストによる割り算を可能にする。
- オーバーフロー/アンダーフロー対策:
- 出力: 入力
x
と同じ形状のNumPy配列。各行の要素の合計は1になり、各要素は0から1の間の値(確率)となる。
コメント