🧠

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

2024/04/13に公開

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


今回はscikit-learnに実装されているscikit-learnを用いていきます。

ライブラリのインポート

import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.metrics import accuracy_score            # 正解率
from sklearn.linear_model import LogisticRegression   # ロジスティック回帰
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split  # データセットの分割
from matplotlib.colors import ListedColormap

ロジスティック回帰モデルによる学習を行うため、データセットを用意します。

データセットの準備

データセットの準備を行います。

# Irisデータセットをロード
iris = datasets.load_iris()

# データセットの確認
print(iris)

Irisデータセットの準備

# 特徴量の取得
# 150個のデータの2, 3行目(花びらの長さと花びらの幅)を抽出
X = iris.data[:, [2, 3]]

# 目的変数の取得
y = iris.target
# 目的変数のunique値(以後正解ラベルという)を確認する
print('Class labels:', np.unique(y))
実行結果
Class labels: [0 1 2]

訓練データセットとテストデータセットに分割する

# データセットの分割
X_train, X_test, y_train, y_test =\
    train_test_split(X, y,
                     test_size    = 0.3,    # 全体の30%をテストデータとする
                     random_state = 1,      # 乱数を固定
                     stratify     = y       # 層化抽出を実行
                    )
# 層化抽出を指定したため、正解ラベルのバランスを維持してデータセットを分割できる
print('Labels counts in y:', np.bincount(y))
print('Labels counts in y_train:', np.bincount(y_train))
print('Labels counts in y_test:', np.bincount(y_test))
実行結果
Labels counts in y: [50 50 50]
Labels counts in y_train: [35 35 35]
Labels counts in y_test: [15 15 15]

訓練データセットを標準化する

# インスタンス化
sc = StandardScaler()

# 学習し、特徴量毎の平均値と標準偏差を推定する
sc.fit(X_train)

# 推定した平均値と標準偏差を用いて標準化を行う
X_train_std = sc.transform(X_train)
X_test_std  = sc.transform(X_test)

決定境界を可視化するための関数の実装

def plot_decision_regions(X, y, classifier, test_idx = None, resolution = 0.02):

    """マーカーとカラーマップの準備"""
    markers = ('o', 's', '^', 'v', '<')
    colors  = ('#3F9877',                  # ジェードグリーン
               '#003F8E',                  # インクブルー
               '#EA5506',                  # 赤橙
               'gray',
               'cyan'
              )
    cmap    = ListedColormap(colors[: len(np.unique(y))])

    """グリッドポイント(格子点)の生成"""    
    # x軸方向の最小値、最大値を定義
    x_min = X[:, 0].min() - 1
    x_max = X[:, 0].max() + 1
    # y軸方向の最小値、最大値を定義
    y_min = X[:, 1].min() - 1
    y_max = X[:, 1].max() + 1

    # 格子点の生成
    xx, yy = np.meshgrid(np.arange(x_min, x_max, resolution),
                         np.arange(y_min, y_max, resolution)
                        )
    # 確認用
    #print(xx1)
    #print(xx2)

    """各特徴量を1次元配列に変換(ravel())して予測を実行"""
    # つまり2つの特徴量から0と予測された格子点と1と予測された値が格子点ごとにlabに格納される
    lab = classifier.predict(np.array([xx.ravel(), yy.ravel()]).T)

    # 確認用
    #print(np.array([xx1.ravel(), xx2.ravel()]).T)
    #print(lab) # 格子点の並びで0,1が格納されているが、この時点では1次元配列なので変換が必要

    """予測結果の元のグリッドポイント(格子点)のデータサイズに変換"""
    lab = lab.reshape(xx.shape)

    """グリッドポイントの等高線のプロット"""
    plt.contourf(xx, yy, lab,
                 alpha = 0.3,                    # 透過度を指定
                 cmap  = cmap
                 )
    plt.xlim(xx.min(), xx.max())
    plt.ylim(yy.min(), yy.max())

    """決定領域のプロット"""
    # クラスごとに訓練データをセット
    for idx, cl in enumerate(np.unique(y)):
        plt.scatter(x         = X[y == cl, 0],
                    y         = X[y == cl, 1],
                    alpha     = 0.8,
                    c         = colors[idx],
                    marker    = markers[idx],
                    label     = f'Class {cl}',
                    edgecolor = 'black'
                    )
        
    """テストデータ点を目立たせる(点を〇で表示)"""
    if test_idx:    # ここはbool(test_idx)と同義。つまりTrueを返す
        # すべてのデータ点を描画
        X_test = X[test_idx, :]
        y_test = y[test_idx]

        plt.scatter(X_test[:, 0],
                    X_test[:, 1],
                    c         = 'none',
                    edgecolor = 'black',
                    alpha     = 1.0,
                    linewidth = 1,
                    marker    = 'o',
                    s         = 100,
                    label     = 'Test set'
                   )

ロジスティック回帰モデルによる学習を行う

scikit-learnで実装されているロジスティック回帰モデルを用いていきます。このモデルには多クラス分類の設定も標準でサポートされいます。今回は3つのクラスに分類できるデータセットを用いて、Part3のように2値分類ではなく、多クラス分類の訓練を行います。
また、最適化アルゴリズムはこれまで(フルバッチ)勾配降下法や確率的勾配降下法について学んできましたが、scikit-learnは他にもいろいろな種類の最適化アルゴリズムを実装しています。今回はその中から記憶制限BFGS法(lbfgs)を選択します。
また、多クラス分類手法としてovrmultinomialの2種類をそれぞれ指定してみたいと思います。

[参考資料]

# 訓練データとテストデータの特徴量を行方向に結合
X_combined_std = np.vstack((X_train_std, X_test_std))
# 訓練データとテストデータのクラスラベルを結合
y_combined     = np.hstack((y_train, y_test))

multi_class = 'ovr'のとき

# インスタンス生成
lr_ovr = LogisticRegression(C           = 100.0,
                            solver      = 'lbfgs',    # 最適化アルゴリズムとして確率的勾配法(SGD)ではなくlbgsを指定
                            multi_class = 'ovr'
                           )

# 学習を実施
lr_ovr.fit(X_train_std, y_train)

決定領域を可視化

# 決定領域をプロット
plot_decision_regions(X          = X_combined_std,
                      y          = y_combined,
                      classifier = lr_ovr,
                      test_idx   = range(105, 150)
                     )

plt.xlabel('Petal length [standardized]')
plt.ylabel('Petal width [standardized]')
plt.legend(loc = 'upper left')
plt.tight_layout()

# グラフを保存
plt.savefig('fig3-7-1.png')

plt.show()

multi_class = 'multinomial'のとき

# インスタンス生成
lr_multinomial = LogisticRegression(C           = 100.0,
                                    solver      = 'lbfgs',    # 最適化アルゴリズムとして確率的勾配法(SGD)ではなくlbgsを指定
                                    multi_class = 'multinomial'
                                   )

# 学習を実施
lr_multinomial.fit(X_train_std, y_train)

決定領域を可視化

# 決定領域をプロット
plot_decision_regions(X          = X_combined_std,
                      y          = y_combined,
                      classifier = lr_multinomial,
                      test_idx   = range(105, 150)
                     )

plt.xlabel('Petal length [standardized]')
plt.ylabel('Petal width [standardized]')
plt.legend(loc = 'upper left')
plt.tight_layout()

# グラフを保存
plt.savefig('fig3-7-2.png')

plt.show()

訓練データが特定のクラスに所属する確率を求める

次に、predict_probaメソッドを用いて、訓練データが特定のクラスに属する確率を計算します。

# multi_class = 'ovr'のとき
lr_ovr.predict_proba(X_test_std[:3, :])
実行結果
array([[3.81527885e-09, 1.44792866e-01, 8.55207131e-01],
       [8.34020679e-01, 1.65979321e-01, 3.25737138e-13],
       [8.48831425e-01, 1.51168575e-01, 2.62277619e-14]])
# multi_class = 'multinomial'のとき
lr_multinomial.predict_proba(X_test_std[:3, :])
実行結果
array([[1.52213484e-12, 3.85303417e-04, 9.99614697e-01],
       [9.93560717e-01, 6.43928295e-03, 1.14112016e-15],
       [9.98655228e-01, 1.34477208e-03, 1.76178271e-17]])

predict_probaの結果を用いてクラスラベルの予測値を取得る方法

NumPyのargmax関数を用いて各行で最も大きい列を特定すると、クラスラベルの予測値を取得することができます。

# multi_class = 'ovr'のとき
lr_ovr.predict_proba(X_test_std[:3, :]).argmax(axis = 1)
実行結果
array([2, 0, 0])

※0 = Iris-Setosa, 1 = Iris-Versicolor, 2 = Iris-Virginica

# multi_class = 'multinomial'のとき
lr_multinomial.predict_proba(X_test_std[:3, :]).argmax(axis = 1)
実行結果
array([2, 0, 0])

単一データを予測するときの注意点

最後に、predictが受け取る引数は2次元配列でなければならないため、単一データ(一行のデータ)を入れる場合はNumPyのreshapeメソッドを用いて次元を追加する必要があります。

[参考資料]

# reshape前のデータ確認
print(f"X_test_std[0, :] : {X_test_std[0, :]}")
print(f"X_test_std[0, :].shape : {X_test_std[0, :].shape}")

# reshape後のデータ確認
print(f"X_test_std[0, :].reshape(1, -1) : {X_test_std[0, :].reshape(1, -1)}")
print(f"X_test_std[0, :].reshape(1, -1).shape : {X_test_std[0, :].reshape(1, -1).shape}")

print(f"\n lr_ovr.predict(X_test_std[0, :].reshape(1, -1)) : {lr_ovr.predict(X_test_std[0, :].reshape(1, -1))}")
実行結果
X_test_std[0, :] : [0.89820289 1.44587881]
X_test_std[0, :].shape : (2,)
X_test_std[0, :].reshape(1, -1) : [[0.89820289 1.44587881]]
X_test_std[0, :].reshape(1, -1).shape : (1, 2)

 lr_ovr.predict(X_test_std[0, :].reshape(1, -1)) : [2]

参考文献

Discussion