🧠

【Python】ロジスティック回帰を使ってクラスの確率を予測するモデルの構築Part2

2024/03/24に公開

はじめに

この記事は株式会社インプレスの「Python機械学習プログラミング Pytorch&scilit-learn編」を読んで、私が学習したことをまとめています。リンク一覧はこちらから。今回は3章3節の「ロジスティック回帰を使ってクラスの確率を予測するモデルの構築」を読んで学んだことをまとめていきます。
※まとめている中で、思った以上にボリュームがあったので、細かく記事を分けています。今回は「3.3.2 ロジスティック損失関数を使ってモデルの重みを学習する」を読んで学んだことをまとめています。
Part1の「3.3.1 ロジスティック回帰と条件付き確率」をまとめた記事こちらからどうぞ。
また、用語などの定義については【Python】ADALINE(フルバッチ勾配降下法)と学習の収束にまとめていますので、こちらをご確認ください。


前回はロジスティック回帰アルゴリズムにおいて、活性化関数として与えられるシグモイド関数\sigma_3と、シグモイド関数をふまえた閾値関数の再定義を行いました。

今回はADALINEのときと同様に、モデルのパラメータを学習させるための損失関数L_2として対数尤度関数(log-likelihood function)を定義します※(正確にはL_2とは、この対数尤度関数にマイナスをつけたもののことを指します)。ADALINEの平均二乗誤差による損失関数L_1と明確に区別するために添え字を使って決定関数\sigmaと同様に定義します。

定義

\boldsymbol{x}^{(j)}が与えられており、かつ\boldsymbol{w}, bも与えられているとします。このとき、\boldsymbol{x}^{(j)}に対応する正解値y^{(j)}となる条件付き確率はp(y^{(j)} | x^{(j)}, \boldsymbol{w}, b)と表せます。ただし、(1 \leq j \leq n)かつnは自然数とします。
ここで、各jに対する条件付き確率は互いに独立であると仮定し、各条件下においてy^{(1)}, y^{(2)}, \cdots, y^{(j)}, \cdots, y^{(n)}が同時に起こる確率は

\begin{alignat*}{2} p((y^{(1)}, y^{(2)}, \cdots, y^{(j)}, \cdots, y^{(n)}) | (\boldsymbol{x}^{(1)}, \boldsymbol{x}^{(2)}, \cdots, \boldsymbol{x}^{(j)}, \cdots, \boldsymbol{x}^{(n)}); \boldsymbol{w},b) &= p(y^{(1)} | \boldsymbol{x}^{(1)}; \boldsymbol{w},b) \times p(y^{(2)} | \boldsymbol{x}^{(2)}; \boldsymbol{w},b) \times \cdots \times p(y^{(n)} | \boldsymbol{x}^{(n)}; \boldsymbol{w},b) \\ &= \prod_{j}^{n} p(y^{(j)} | \boldsymbol{x}^{(j)}; \boldsymbol{w},b) \end{alignat*}

ここで、p(y^{(j)} | \boldsymbol{x}^{(j)}; \boldsymbol{w},b)について、

y^{(j)} \in \{ 0, 1 \}

であり、また【Python】ロジスティック回帰を使ってクラスの確率を予測するモデルの構築Part1より

p(y^{(j)} | \boldsymbol{x}^{(j)}; \boldsymbol{w},b) = \begin{cases} \sigma_{3} (z^{(j)}) & (y^{(j)} = 1) \\ 1 - \sigma_{3} (z^{(j)}) & (y^{(j)} = 0) \\ \end{cases}

と表すことができます。このとき定義より0 \leq \sigma_{3} (z^{(j)}) \leq 1は明らかです。以上のことからy^{(j)}はベルヌーイ分布に従うため、

p(y^{(j)} | \boldsymbol{x}^{(j)}; \boldsymbol{w},b) = (\sigma_{3} (z^{(j)}))^{y^{(j)}} (1 - \sigma_{3} (z^{(j)}))^{1 - y^{(j)}}

が成り立ちます。ゆえに

\begin{alignat*}{2} \prod_{j}^{n} p(y^{(j)} | \boldsymbol{x}^{(j)}; \boldsymbol{w},b) &= \prod_{j}^{n} (\sigma_{3} (z^{(j)}))^{y^{(j)}} (1 - \sigma_{3} (z^{(j)}))^{1 - y^{(j)}} \end{alignat*}

が成り立ちます。

[参考資料]

ここで、\prod_{j}^{n} p(y^{(j)} | \boldsymbol{x}^{(j)}; \boldsymbol{w},b)の対数を取ったものを対数尤度関数といい、この式にマイナスを付与した式

- \log \prod_{j}^{n} p(y^{(j)} | \boldsymbol{x}^{(j)}; \boldsymbol{w},b)

損失関数L_2とします。

損失関数の定義は以上になりますが、上の形のままだと扱いづらいので最後に式の変形を行います。尤度関数ではなく、なぜ対数を取った対数尤度関数を用いているのかといえば、やはり積の計算を和に変換してしまう対数の便利な性質が理由です。計算が圧倒的に楽になります。また、マイナスを付与する理由は、対数尤度関数は最大化することによってパラメータの最適化を行うため、他の損失関数と同様に最小化で最適化を行うために付与しています。

それでは式変形を以下に記します。

\begin{alignat*}{2} L_2 (\boldsymbol{w}, b) &= - \log \prod_{j}^{n} p(y^{(j)} | \boldsymbol{x}^{(j)}; \boldsymbol{w},b) \\ &= - \log \prod_{j}^{n} (\sigma_{3} (z^{(j)}))^{y^{(j)}} (1 - \sigma_{3} (z^{(j)}))^{1 - y^{(j)}} \\ &= - \log \{ (\sigma_{3} (z^{(1)}))^{y^{(1)}} (1 - \sigma_{3} (z^{(1)}))^{1 - y^{(1)}} \times (\sigma_{3} (z^{(2)}))^{y^{(2)}} (1 - \sigma_{3} (z^{(2)}))^{1 - y^{(2)}} \times \cdots \times (\sigma_{3} (z^{(n)}))^{y^{(n)}} (1 - \sigma_{3} (z^{(n)}))^{1 - y^{(n)}} \} \\ &= - \{ \log (\sigma_{3} (z^{(1)}))^{y^{(1)}} (1 - \sigma_{3} (z^{(1)}))^{1 - y^{(1)}} + \log (\sigma_{3} (z^{(2)}))^{y^{(2)}} (1 - \sigma_{3} (z^{(2)}))^{1 - y^{(2)}} + \cdots + \log (\sigma_{3} (z^{(n)}))^{y^{(n)}} (1 - \sigma_{3} (z^{(n)}))^{1 - y^{(n)}} \} \\ &= - \sum_{j = 1}^{n} \left[ \log (\sigma_{3} (z^{(j)}))^{y^{(j)}} (1 - \sigma_{3} (z^{(j)}))^{1 - y^{(j)}} \right] \\ &= - \sum_{j = 1}^{n} \left[ \log (\sigma_{3} (z^{(j)}))^{y^{(j)}} + \log (1 - \sigma_{3} (z^{(j)}))^{1 - y^{(j)}} \right] \\ &= - \sum_{j = 1}^{n} \left[ y^{(j)} \log (\sigma_{3} (z^{(j)})) + (1 - y^{(j)}) \log (1 - \sigma_{3} (z^{(j)})) \right] \\ &= \sum_{j = 1}^{n} \left[ - y^{(j)} \log (\sigma_{3} (z^{(j)})) - (1 - y^{(j)}) \log (1 - \sigma_{3} (z^{(j)})) \right] \\ \end{alignat*}

損失関数L_2 (\boldsymbol{w}, b)のグラフの描画

最後にロジスティック回帰の損失関数の値を具体的に示すグラフを描画していきます。

import matplotlib.pyplot as plt
import numpy as np

# シグモイド関数を定義
def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))

# y = 1 の損失値を計算する関数
def loss_1(z):
    return - np.log(sigmoid(z))

# y = 0 の損失値を計算する関数
def loss_0(z):
    return - np.log(1 - sigmoid(z))
# 0.1間隔で-10以上10未満のデータを生成
z = np.arange(-10, 10, 0.1)


sigma_z = sigmoid(z)                # シグモイド関数をインスタンス化
c1      = [loss_1(x) for x in z]    # y = 1の損失値を計算する関数をインスタンス化
c0      = [loss_0(x) for x in z]    # y = 0の損失値を計算する関数をインスタンス化

# 結果をプロット
plt.plot(sigma_z, c1, 
         label     = 'L(w, b) if y = 1')
plt.plot(sigma_z, c0, 
         label     = 'L(w, b) if y = 0', 
         linestyle = '--')

plt.ylim(0.0, 5.1)
plt.xlim([0, 1])
plt.xlabel('$\sigma(z)$')
plt.ylabel('$L_2(w, b)$')
plt.legend(loc = 'best')

# グラフを保存
plt.savefig('3-4.png')

plt.show()

このグラフは次のように読むことができます。

  • クラス1であると正しく予測できた場合
    \sigma_3 (z)の値が高い(=クラス1である確率が高い)ほど、損失関数の値も0に近づく。

  • クラス0であると正しく予測できた場合
    \sigma_3 (z)の値が低い(=クラス0である確率が高い)ほど、損失関数の値も0に近づく。

これまでの議論の流れを追えており、かつグラフの読み取りが難なくできる人ならば自明ですが、慣れていない人はそれぞれ個の情報をつなぎ合わせることは意外と難しいものです。参考に【Python】ロジスティック回帰を使ってクラスの確率を予測するモデルの構築Part1の際に描画したシグモイド関数も下図のとおり掲載しておきますので、損失関数との関係を整理するのに役立ててください。

参考文献

Discussion