0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

機械学習入門 第3回:ロジスティック回帰を「確率で分類する仕組み」として理解する

0
Posted at

前回は、線形回帰を使って住宅価格のような 連続値 を予測しました。

今回は、教師あり学習のもう一つの代表例である ロジスティック回帰 を扱います。名前に「回帰」と入っていますが、主な用途は分類です。たとえば「購入するか/しないか」「合格するか/しないか」「この画像は 0 か 1 か」のように、データをクラスに分けたいときに使います。

ファインマン風に言えば、ロジスティック回帰は「まず直線で点数をつけ、その点数を 0 から 1 の確率に変換して、最後にしきい値で分類する方法」です。

目次

  1. ロジスティック回帰で何をしたいのか
  2. この記事で使うデータについて
  3. 線形回帰とのつながり
  4. シグモイド関数:点数を確率に変える
  5. 仮説関数と分類のしきい値
  6. 損失関数:自信を持って外すほど大きく罰する
  7. 勾配降下法でパラメータを学習する
  8. NumPy で二値分類を実装する
  9. 正則化:重みが大きくなりすぎるのを抑える
  10. 多クラス分類への広げ方
  11. Scikit-learn でロジスティック回帰を使う
  12. ケース1:アヤメの品種を分類する
  13. ケース2:手書き数字を分類する
  14. まとめ

1. ロジスティック回帰で何をしたいのか

ロジスティック回帰は、入力された特徴量から クラス を予測するためのモデルです。

代表的な例は二分類です。

  • メールがスパムかどうか
  • 顧客が再来店するかどうか
  • 患者にある病気の疑いがあるかどうか
  • 画像に写っている数字が 0 かどうか

二分類では、片方のクラスを 1、もう片方を 0 として扱うことが多いです。

大事なのは、ロジスティック回帰がいきなりクラスを出すのではなく、まず「クラス 1 らしさ」を確率として出すことです。

たとえば顧客の再来店を予測するなら、次のように考えます。

再来店する確率 = f(来店回数, 平均購入額, 前回からの日数, ...)

この確率が 0.5 以上なら「再来店する」、0.5 未満なら「再来店しない」と判定できます。もちろん、現場では 0.5 ではなく、目的に応じて 0.7 や 0.3 など別のしきい値を使うこともあります。

具体的な数式に入る前に、この記事で使うデータを先に確認しておきましょう。

2. この記事で使うデータについて

この記事では、コードの目的に応じてデータを分けて使います。

  • NumPy でロジスティック回帰を実装する部分では、説明用の 架空の顧客データ をコード内で作ります。外部ファイルなしで、そのままコピーして動かせます。
  • Scikit-learn の実践例では、ライブラリに付属している Iris データセットdigits データセット を使います。どちらも sklearn.datasets から読み込めるため、ローカルの独自ファイルには依存しません。

手書き数字認識では、28×28 ピクセルの MNIST データセットがよく使われます。ただし、この記事では再現しやすさを優先し、Scikit-learn 付属の load_digits を使います。load_digits は 8×8 ピクセルですが、「画像をベクトルに展開して、多クラス分類する」という考え方は同じです。

3. 線形回帰とのつながり

ロジスティック回帰は、前回の線形回帰とかなり近い形から始まります。

まず、特徴量の線形結合を計算します。

$$
z=\theta^Tx
$$

これは線形回帰の予測式とほぼ同じです。

違いは、この $z$ をそのまま予測値として使わないことです。線形回帰では $z$ を価格や売上のような連続値として読めました。しかし分類では、出力を「0 から 1 の確率」として読みたい場面が多くあります。

そこで、ロジスティック回帰では $z$ を シグモイド関数 に通します。

つまり、ロジスティック回帰は「線形モデルに、確率へ変換する出口を付けたもの」と考えると理解しやすいです。

4. シグモイド関数:点数を確率に変える

線形スコア $z$ を確率らしい値に変換するのが シグモイド関数 です。英語では Sigmoid function と呼ばれ、コードでは sigmoid という名前で実装することが多いです。

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

この関数の出力は、必ず 0 より大きく 1 より小さくなります。

$$
0<g(z)<1
$$

値の動きは次のようになります。

  • $z$ が大きな正の値になるほど、$g(z)$ は 1 に近づく
  • $z$ が大きな負の値になるほど、$g(z)$ は 0 に近づく
  • $z=0$ のとき、$g(z)=0.5$ になる

コードで曲線を描くと、形がつかみやすくなります。

import numpy as np
import matplotlib.pyplot as plt


def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))


z = np.linspace(-8, 8, 200)
probability = sigmoid(z)

plt.plot(z, probability)
plt.axhline(0.5, color="gray", linestyle="--")
plt.xlabel("z")
plt.ylabel("g(z)")
plt.title("シグモイド関数")
plt.show()

この S 字カーブのおかげで、どれだけ大きい値や小さい値が入ってきても、出力を確率として扱いやすい範囲に収められます。

5. 仮説関数と分類のしきい値

シグモイド関数がわかったところで、ロジスティック回帰の予測式を見ていきます。機械学習では、この予測式を 仮説関数 と呼ぶことがあります。

$$
\hat y=h_\theta(x)=g(\theta^Tx)
=\frac{1}{1+e^{-\theta^Tx}}
$$

ここで、$\hat y$ は「クラス 1 に属する確率」として読みます。

たとえば、顧客が再来店するかどうかを予測するなら、

h_theta(x) = 0.82

は「この顧客は 82% くらいの確率で再来店しそう」と読むイメージです。

確率からクラスに変換するには、しきい値を決めます。よく使われるしきい値は 0.5 です。

$$
\hat c=
\begin{cases}
1, & h_\theta(x)\ge0.5\
0, & h_\theta(x)<0.5
\end{cases}
$$

ただし、しきい値 0.5 がいつも正解とは限りません。たとえば病気の見逃しを減らしたい場面では、少しでも疑いがあれば陽性寄りに判定するため、しきい値を低めに設定することがあります。逆に、誤検知のコストが高い場面では、しきい値を高めに設定することもあります。

予測確率としきい値の関係がわかったところで、次はそもそもこの予測がどれくらい正しいのかを測る方法を見ていきます。

6. 損失関数:自信を持って外すほど大きく罰する

分類でも、モデルの予測がどれくらい外れているかを数字で表す必要があります。

線形回帰では二乗誤差を使いました。しかしロジスティック回帰では、二分類に向いた 交差エントロピー損失 を使うのが一般的です。

1サンプルの損失は次のように書けます。

$$
L(\hat y,y)
=-y\log\hat y-(1-y)\log(1-\hat y)
$$

分けて書くと、意味が見えやすくなります。

$$
L(\hat y,y)=
\begin{cases}
-\log\hat y, & y=1\
-\log(1-\hat y), & y=0
\end{cases}
$$

つまり、

  • 本当は 1 なのに、モデルが低い確率を出すと強く罰する
  • 本当は 0 なのに、モデルが高い確率を出すと強く罰する
  • 正しいクラスに高い確率を出せていれば、損失は小さくなる

という仕組みです。

小さな数値例で見てみます。

import numpy as np
import pandas as pd


def binary_cross_entropy(y_true, y_prob):
    y_prob = np.clip(y_prob, 1e-15, 1 - 1e-15)
    return -(y_true * np.log(y_prob) + (1 - y_true) * np.log(1 - y_prob))


examples = pd.DataFrame({
    "正解ラベル": [1, 1, 0, 0],
    "予測確率": [0.9, 0.1, 0.1, 0.9],
})
examples["損失"] = binary_cross_entropy(
    examples["正解ラベル"],
    examples["予測確率"],
)

print(examples.round(3))

実行すると、1行目と3行目(正解ラベルに高い確率を出せている行)の損失は 0.105 程度、2行目と4行目(自信満々に外している行)の損失は 2.303 程度になります。

正解クラスに高い確率を出している行は損失が小さく、逆に自信満々に外している行は損失が大きくなります。ロジスティック回帰の損失は、「外れたかどうか」だけでなく「どれくらい自信を持って外したか」も見ています。

訓練データ全体では、この損失を平均した コスト関数 を最小化します。

$$
J(\theta)
=-\frac{1}{m}\sum_{i=1}^{m}
\left[
y^{(i)}\log\hat y^{(i)}
+(1-y^{(i)})\log(1-\hat y^{(i)})
\right]
$$

ここで、

$$
\hat y^{(i)}=\frac{1}{1+e^{-\theta^Tx^{(i)}}}
$$

です。線形スコアをシグモイド関数に通すことで、各サンプルの予測を確率として扱えるようになります。

7. 勾配降下法でパラメータを学習する

損失の形が決まったので、次はパラメータ $\theta$ をどう更新するかを見ます。ここでも、前回と同じく勾配降下法を使えます。

ロジスティック回帰のコスト関数を $\theta_j$ で偏微分すると、次の形になります。

$$
\frac{\partial J(\theta)}{\partial\theta_j}
=\frac{1}{m}\sum_{i=1}^{m}
\left(h_\theta(x^{(i)})-y^{(i)}\right)x_j^{(i)}
$$

見た目は線形回帰の勾配とよく似ています。ただし、中身の $h_\theta(x)$ が違います。線形回帰では直線の出力そのものを使いましたが、ロジスティック回帰ではシグモイド関数を通した確率を使います。

更新式は次のようになります。

$$
\theta_j\leftarrow\theta_j-
\frac{\alpha}{m}\sum_{i=1}^{m}
\left(h_\theta(x^{(i)})-y^{(i)}\right)x_j^{(i)}
$$

$\alpha$ は学習率です。前回と同じく、大きすぎると不安定になり、小さすぎると学習が遅くなります。

ここでも、すべてのパラメータは同時に更新します。あるパラメータだけ先に更新し、その新しい値を使って次のパラメータを更新する、という順番更新ではありません。

8. NumPy で二値分類を実装する

ここまでの式を、実際に NumPy で書いてみます。

まずは、顧客がキャンペーン後に再来店するかどうかを予測する架空データを作ります。各列の意味は次の通りです。

  • 来店回数:キャンペーン開始からこれまでの来店回数
  • 平均購入額_千円:1回あたりの平均購入額(単位は千円)
  • 前回からの日数:前回来店してからの経過日数
  • 再来店:キャンペーン後に再来店したかどうか(1: 再来店した、0: 再来店しなかった)

このうち、来店回数平均購入額_千円前回からの日数 の3つが特徴量、再来店 がラベルです。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt


customer_jp = pd.DataFrame({
    "来店回数": [1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10],
    "平均購入額_千円": [2.0, 2.5, 3.0, 3.2, 4.0, 4.3, 5.2, 5.8, 6.1, 6.5, 7.0, 8.0],
    "前回からの日数": [45, 40, 35, 30, 28, 25, 18, 15, 12, 10, 8, 5],
    "再来店": [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
})

feature_cols = ["来店回数", "平均購入額_千円", "前回からの日数"]
target_col = "再来店"

X_raw = customer_jp[feature_cols].to_numpy(dtype=float)
y = customer_jp[target_col].to_numpy(dtype=float)

print(customer_jp)

特徴量の単位が違うので、前回と同じく標準化してから学習します。ロジスティック回帰でも、特徴量スケーリングは勾配降下法を安定させるために重要です。

# 特徴量を標準化する
X_mean = X_raw.mean(axis=0)
X_std = X_raw.std(axis=0)
X_scaled = (X_raw - X_mean) / X_std

# 切片 theta_0 を扱うため、先頭に 1 の列を追加する
X = np.c_[np.ones(len(X_scaled)), X_scaled]


def sigmoid(z):
    z = np.clip(z, -500, 500)
    return 1.0 / (1.0 + np.exp(-z))


def compute_log_loss(X, y, theta):
    y_prob = sigmoid(X @ theta)
    y_prob = np.clip(y_prob, 1e-15, 1 - 1e-15)
    return -np.mean(y * np.log(y_prob) + (1 - y) * np.log(1 - y_prob))


theta = np.zeros(X.shape[1])
learning_rate = 0.1
n_iterations = 3000
m = len(y)
loss_history = []

for _ in range(n_iterations):
    y_prob = sigmoid(X @ theta)
    gradient = X.T @ (y_prob - y) / m
    theta = theta - learning_rate * gradient
    loss_history.append(compute_log_loss(X, y, theta))

coef = pd.Series(theta, index=["切片", *feature_cols])
print(coef.round(3))
print(f"最終損失: {loss_history[-1]:.4f}")

なお、sigmoid 関数の中で np.clip(z, -500, 500) を行っているのは、$z$ の絶対値が大きくなりすぎたときに np.exp がオーバーフローするのを防ぐためです。4節で描いたシンプルな sigmoid にはこの処理がありませんでしたが、学習ループの中で使う場合は、この安全対策を入れておくと安心です。

学習したモデルで、訓練データの予測確率と予測クラスを確認します。

train_probability = sigmoid(X @ theta)
train_pred = (train_probability >= 0.5).astype(int)

result = customer_jp.copy()
result["予測確率"] = np.round(train_probability, 3)
result["予測クラス"] = train_pred

print(result)
print(f"訓練データの正解率: {(train_pred == y).mean():.3f}")

新しい顧客を分類する場合も、訓練時と同じ平均・標準偏差で標準化します。

new_customer = pd.DataFrame({
    "来店回数": [5],
    "平均購入額_千円": [5.0],
    "前回からの日数": [14],
})

new_scaled = (new_customer[feature_cols].to_numpy(dtype=float) - X_mean) / X_std
new_X = np.c_[np.ones(len(new_scaled)), new_scaled]
new_probability = sigmoid(new_X @ theta)[0]
new_class = int(new_probability >= 0.5)

print(f"再来店確率: {new_probability:.3f}")
print(f"予測クラス: {new_class}")

損失曲線も確認しておきます。

plt.plot(range(1, len(loss_history) + 1), loss_history)
plt.xlabel("反復回数")
plt.ylabel("交差エントロピー損失")
plt.title("ロジスティック回帰の学習曲線")
plt.show()

損失がだんだん下がっていれば、パラメータがよりよい方向へ更新されていることがわかります。途中で激しく振動する場合は、学習率が大きすぎる可能性があります。

実際にこのコードを動かすと、学習を重ねるにつれて coef の絶対値がじわじわと大きくなっていきます。理由は、今回の架空データが「来店回数や購入額が増えるほどきれいに再来店に切り替わる」、分離しやすいデータだからです。分離しやすいデータでは、正則化のないロジスティック回帰は重みを際限なく大きくしようとする性質があります。

9. 正則化:重みが大きくなりすぎるのを抑える

二値分類の実装ができたところで、次は過学習を抑える工夫を見ます。今回のように学習データがきれいに分かれている場合、何もしないと重みがどんどん大きくなり続けてしまいます。ロジスティック回帰でも、線形回帰と同じように正則化を使えます。

L2 正則化を入れたコスト関数は、次のように書けます。

$$
J(\theta)
=-\frac{1}{m}\sum_{i=1}^{m}
\left[
y^{(i)}\log h_\theta(x^{(i)})
+(1-y^{(i)})\log(1-h_\theta(x^{(i)}))
\right]
+\frac{\lambda}{2m}\sum_{j=1}^{n}\theta_j^2
$$

最後の項が正則化項です。$\lambda$ が大きいほど、重みを小さく保とうとする力が強くなります。

通常、切片 $\theta_0$ には正則化をかけません。切片は全体の基準位置を動かす役割なので、特徴量の重みに対するペナルティとは分けて考えることが多いです。

正則化を入れた勾配は、$j\ge1$ について次のようになります。

$$
\frac{\partial J(\theta)}{\partial\theta_j}
=\frac{1}{m}\sum_{i=1}^{m}
\left(h_\theta(x^{(i)})-y^{(i)}\right)x_j^{(i)}
+\frac{\lambda}{m}\theta_j
$$

NumPy で書くなら、切片だけ正則化から外します。次のコードは、8節で作った sigmoid 関数と compute_log_loss 関数、そして標準化済みの Xy をそのまま再利用します。

def fit_logistic_regression_l2(X, y, learning_rate=0.1, n_iterations=3000, reg_strength=1.0):
    theta = np.zeros(X.shape[1])
    m = len(y)
    loss_history = []

    for _ in range(n_iterations):
        y_prob = sigmoid(X @ theta)
        regularization = reg_strength * theta / m
        regularization[0] = 0.0
        gradient = X.T @ (y_prob - y) / m + regularization
        theta = theta - learning_rate * gradient

        penalty = reg_strength * np.sum(theta[1:] ** 2) / (2 * m)
        loss_history.append(compute_log_loss(X, y, theta) + penalty)

    return theta, loss_history


theta_l2, loss_history_l2 = fit_logistic_regression_l2(X, y, reg_strength=1.0)
print(pd.Series(theta_l2, index=["切片", *feature_cols]).round(3))

Scikit-learn の LogisticRegression では、正則化の強さを C で指定します。ただし C は $\lambda$ そのものではなく、正則化の強さの逆数です。

  • C が小さいほど、正則化は強い
  • C が大きいほど、正則化は弱い

この向きは初学者がよく混乱するところなので、「C は制約をゆるめるつまみ」と覚えておくと少し楽です。

10. 多クラス分類への広げ方

ここまでは 01 の二分類を見てきました。では、アヤメの 3 品種分類や、数字の 0 から 9 までの分類のように、クラスが 3 個以上ある場合はどうすればよいでしょうか。

ロジスティック回帰を多クラス分類へ広げる代表的な考え方には、次のようなものがあります。

10.1 One-vs-Rest:OvR

OvR は、1つのクラスとそれ以外を分ける二分類器を、クラスの数だけ作る方法です。One-vs-All と呼ばれることもあります。

たとえば 0, 1, 2 の 3 クラスなら、「0 かそれ以外か」「1 かそれ以外か」「2 かそれ以外か」の 3 個の二分類器を作ります。

10.2 One-vs-One:OvO

OvO は、2つのクラスの組み合わせごとに二分類器を作る方法です。

クラスが $K$ 個ある場合、必要な分類器の数は次の通りです。

$$
\frac{K(K-1)}{2}
$$

分類器の数は増えますが、各分類器は2クラスだけを見ればよいので、問題が単純になることがあります。

10.3 Many-vs-Many:MvM

MvM は、複数のクラスをまとめて正例側、別の複数クラスを負例側に置くような分け方を、何通りか用意する考え方です。実務やライブラリでは、誤り訂正出力符号(ECOC)という枠組みで説明されることもあります。

第1段階では細かいアルゴリズム名を暗記するより、「二分類器を組み合わせれば多クラスにも対応できる」と押さえておけば十分です。

11. Scikit-learn でロジスティック回帰を使う

多クラス分類の考え方が見えたところで、実装を Scikit-learn に任せてみます。

必要なライブラリが入っていない場合は、先にインストールします。

pip install numpy pandas matplotlib scikit-learn

Scikit-learn では、ロジスティック回帰は LogisticRegression として用意されています。

from sklearn.linear_model import LogisticRegression

model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)

y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)

上のコードは呼び出し方を示す最小形です。X_trainy_train を実際に用意する完全な例は、次のアヤメ分類と手書き数字分類で示します。

よく使うメソッドは次の通りです。

メソッド 意味
fit 訓練データからパラメータを学習する
predict 予測クラスを出す
predict_proba 各クラスに属する確率を出す

max_iter は最大反復回数です。ロジスティック回帰は内部で反復計算を行うため、データによっては初期値のままだと ConvergenceWarning: lbfgs failed to converge... のような収束警告が表示されることがあります。その場合は、max_iter=1000max_iter=2000 のように増やします。

ここまでで API の使い方を確認したので、次は実際のデータセットに当てはめて、Iris の品種分類と手書き数字の分類を試してみます。

12. ケース1:アヤメの品種を分類する

まずは、Iris データセットを使ってアヤメの品種を分類します。

Iris データセットには、次の4つの特徴量があります。

特徴量 意味
sepal length (cm) がく片の長さ
sepal width (cm) がく片の幅
petal length (cm) 花びらの長さ
petal width (cm) 花びらの幅

分類したいクラスは、次の3品種です。

ラベル 品種
0 setosa
1 versicolor
2 virginica

実装は次のようになります。ここからの Xy は、8節・9節で使った顧客データではなく、新しく読み込む Iris データセットを指します。

import pandas as pd
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler


iris = load_iris(as_frame=True)
X = iris.data
y = iris.target

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42,
    stratify=y,
)

iris_model = make_pipeline(
    StandardScaler(),
    LogisticRegression(max_iter=1000),
)

iris_model.fit(X_train, y_train)
y_pred = iris_model.predict(X_test)

print(f"accuracy: {accuracy_score(y_test, y_pred):.3f}")
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred, target_names=iris.target_names))

ここでも、第2回と同じく Pipeline を使っています。StandardScaler で特徴量を標準化してから、ロジスティック回帰に渡すためです。

stratify=y は、訓練データとテストデータの中でクラスの比率が大きく崩れないようにする指定です。分類問題では、分割の仕方によって一部のクラスがテストデータにほとんど入らないことがあります。特にデータ数が少ない場合は、stratify を指定しておくと評価が安定しやすくなります。

係数も確認できます。

logistic_model = iris_model.named_steps["logisticregression"]

coef = pd.DataFrame(
    logistic_model.coef_,
    columns=iris.feature_names,
    index=iris.target_names,
)

print(coef.round(3))

多クラス分類では、クラスごとに係数の行ができます。この係数は標準化後の特徴量に対するものなので、元の単位でそのまま読むのではなく、「どの特徴量が各クラスの判定に効いていそうか」を見る手がかりとして使います。

13. ケース2:手書き数字を分類する

最後に、手書き数字認識をロジスティック回帰で試します。

手書き数字認識では、28×28 ピクセルの画像がよく使われます。28×28 の画像は、次のように 784 個の画素を持ちます。

$$
28\times28=784
$$

画像のままではロジスティック回帰に入れにくいので、2次元の画素行列を1列に並べた特徴量ベクトルに変換します。

$$
X=(x_1,x_2,\ldots,x_{784})
$$

この記事では、外部ファイルに依存しないように Scikit-learn 付属の digits データセットを使います。こちらは 8×8 ピクセルなので、1枚の画像は 64 個の特徴量になります。

まず、いくつか画像を表示して、データの形を確認します。

import matplotlib.pyplot as plt
from sklearn.datasets import load_digits


digits = load_digits()

fig, axes = plt.subplots(2, 5, figsize=(8, 3))
for ax, image, label in zip(axes.ravel(), digits.images[:10], digits.target[:10]):
    ax.imshow(image, cmap="gray_r")
    ax.set_title(f"label: {label}")
    ax.axis("off")

plt.tight_layout()
plt.show()

次に、画像を行列からベクトルへ変換し、ロジスティック回帰で分類します。ここでの Xy も、12節の Iris の Xy とは別物で、ここで新しく作り直しています。

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from sklearn.model_selection import train_test_split


X = digits.images.reshape(digits.images.shape[0], -1)
X = X / 16.0
y = digits.target

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42,
    stratify=y,
)

digits_model = LogisticRegression(max_iter=2000)
digits_model.fit(X_train, y_train)

y_pred = digits_model.predict(X_test)
target_names = [str(number) for number in digits.target_names]

print(f"test accuracy: {accuracy_score(y_test, y_pred):.3f}")
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred, target_names=target_names))

手書き数字認識でよく使われる前処理の流れは、画像を読み込み、reshape で1行1サンプルの表に変換し、画素値を最大値で割って正規化する、というものです。上のコードでも、この流れをそのまま踏襲しています。

  • digits.images.reshape(digits.images.shape[0], -1) で、画像を1行1サンプルの表に変換する
  • X / 16.0 で、画素値を 0 から 1 の範囲に近づける
  • LogisticRegression で、0 から 9 までの多クラス分類を行う

画像分類というとすぐに深層学習を思い浮かべがちですが、ロジスティック回帰でも「画素を特徴量として見て分類する」という基本の流れは体験できます。この基本形を押さえておくと、あとでニューラルネットワークに進んだときも、入力データの形を理解しやすくなります。

14. まとめ

今回の内容をまとめます。

  • ロジスティック回帰は、名前に「回帰」とあるが、主に分類問題に使うモデルである。
  • 線形スコア $z=\theta^Tx$ をシグモイド関数に通すことで、0 から 1 の確率として読める値に変換する。
  • 仮説関数 $h_\theta(x)$ は、クラス 1 に属する確率を表す。
  • 予測確率をしきい値と比べることで、最終的なクラスを決める。
  • ロジスティック回帰では、二値交差エントロピー損失を使う。
  • 勾配の形は線形回帰とよく似ているが、仮説関数の中身はシグモイド関数を通した確率である。
  • NumPy だけでも、シグモイド関数、交差エントロピー、勾配降下法を書けば二値分類を実装できる。
  • L2 正則化を入れると、重みが大きくなりすぎることを抑えられる。
  • 多クラス分類では、OvR、OvO、MvM のように二分類器を組み合わせる考え方がある。
  • Scikit-learn の LogisticRegression を使うと、Iris や手書き数字の分類を短いコードで試せる。

ロジスティック回帰は、分類モデルの入口としてとても重要です。「線形スコアを作る」「確率に変換する」「損失を測る」「勾配で更新する」という流れは、ニューラルネットワークの分類でも形を変えて何度も出てきます。ここを丁寧に押さえておくと、あとで複雑な分類モデルを学ぶときの見通しがかなりよくなります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?