🧠

【Python】パーセプトロンの実装と決定境界の可視化について

2024/02/28に公開

はじめに

この記事は株式会社インプレスの「Python機械学習プログラミング Pytorch&scilit-learn編」を読んで、私が学習したことをまとめています。
今回は2章1節と2節のパーセプトロンと学習の収束を読んで学んだことをまとめていきます。


パーセプトロンの学習アルゴリズムをPythonで実装し、その後Irisデータセットを用いて、アヤメの花の品種を分類するための訓練を行います。また、訓練したデータセットの決定境界をcontourf()関数を用いて可視化していきます。

【リンク紹介】
【一覧】Python機械学習プログラミング Pytorch&scilit-learn編
これまで書いたシリーズ記事一覧

ライブラリのインポート

まずはライブラリをインポートします。
※ここにはコード実行に必要なライブラリしか載せていませんので注意してください

import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap

パーセプトロンの実装

まずはパーセプトロンの実装を行います。なお、理論や詳しい内容についての記述は割愛します。より詳細を学びたいという人は記事の最後に参考文献のリンクを張っておくので、そちらを読んでください。

class Perceptron:
    """
    パラメータ
    ----------------------------------------------------
    eta          : float.学習率
    n_iter       : int.訓練データの訓練回数
    random_state : int.重みを初期化するための乱数シード
    ----------------------------------------------------

    データ属性
    ----------------------------------------------------
    w_      : 1次元配列.適合後の重み
    b_      : スカラー.適合後のバイアスユニット
    errors_ : リスト.各エポックでの誤分類(更新)の数
    ----------------------------------------------------
    """

    def __init__(self, eta = 0.01, n_iter = 50, random_state = 1):
        self.eta          = eta
        self.n_iter       = n_iter
        self.random_state = random_state

    def fit(self, X, y):
        """
        パラメータ
        -----------------------------------------------------------------------------------------------
        X : {配列のようなデータ構造}. shape = [n_examples(訓練データの個数), n_features(特徴量の個数)]
        y : {配列のようなデータ構造}. shape = [n_examples(訓練データの個数)]
        -----------------------------------------------------------------------------------------------
        """

        rgen         = np.random.RandomState(self.random_state)
        # 重みself.w_を(Xの列の数)次元の実数ベクトルに初期化
        self.w_      = rgen.normal(loc   = 0.0,
                                   scale = 0.01,
                                   size  = X.shape[1]             # Xの列の数で指定
                                  )
        # バイアスユニットself.b_を0に初期化
        self.b_      = np.float_(0.)
        # 誤分類を格納する
        self.errors_ = []                                         # エポック数に対する誤分類をプロットする際に使用可能

        for _ in range(self.n_iter):                              # 訓練データを繰り返し処理
            errors = 0
            for xi, target in zip(X, y):                          # 重みとバイアスユニットを更新
                update = self.eta * (target - self.predict(xi))   # 学習率*(正解値 - 予測値)
                self.w_ += update * xi                            # 更新値(重み)の計算
                self.b_ += update                                 # 更新値(バイアスユニット)の計算
                errors  += int(update != 0.0)                     # 誤差を追加
            self.errors_.append(errors)
        return self

    # 総入力(net input)を定義
    def net_input(self, X):
        return np.dot(X, self.w_) + self.b_

    # 決定関数を定義
    def predict(self, X):
        return np.where(self.net_input(X) >= 0.0, 1, 0)

[memo]
numpy.random.RandomState.normal(loc = 0.0, scale = 1.0, size = none)
正規分布からランダムなサンプルを抽出する.
loc:分布の平均
scale:分布の標準偏差
size:出力される形状

Irisデータセットでパーセプトロンモデルの訓練

次に、実装したパーセプトロンモデルを用いて訓練しますが、後に描画する訓練したモデルの決定境界を可視化しやすくするために、特徴量を「がく片の長さ」と「花びらの長さ」の2つのみとします。また、品種クラスは「Setosa」と「Versicolor」の2つの品種のみとします。

s = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
print('From URL:', s)

# IrisデータセットをDaraFrameオブジェクトに直接読み込む
df = pd.read_csv(s,
                 header   = None,
                 encoding = 'utf-8')
# 確認用
#df.tail()

散布図を用いてデータを可視化する

がく片の長さと花びらの長さを抽出し、散布図を用いてデータを可視化していきます。

# 1-100行目の1, 3行目(がく片の長さ、花びらの長さ)の抽出 ※今回扱う特徴量は2つ
X = df.iloc[0 : 100, [0, 2]].values

# 1-100行目の目的変数の抽出
y = df.iloc[0 : 100, 4].values
# Iris-setosaを0, Iris-versicolorを1に変換
y = np.where(y == 'Iris-setosa', 0, 1)


# 品種setosaのプロット(緑の●)
plt.scatter(x      = X[:50, 0],
            y      = X[:50, 1],
            color  = '#3F9877',
            marker = 'o',
            label  = 'Setosa')
# 品種Versicolorのプロット(青の■)
plt.scatter(x      = X[50 : 100, 0],
            y      = X[50 : 100, 1],
            color  = '#003F8E',
            marker = 's',
            label  = 'Versicolor')

# 軸のラベルの設定
plt.xlabel('Sepal length [cm]')
plt.ylabel('Petal length [cm]')
# 凡例の設定
plt.legend(loc = 'upper left')

# グラフを保存
plt.savefig('fig2-6.png')

plt.show()

パーセプトロンのアルゴリズムを用いて訓練する

# パーセプトロンのインスタンス化
ppn = Perceptron(eta = 0.1, n_iter = 10)

# 学習
ppn.fit(X, y)

# エポックと誤分類の関係を表す折れ線グラフを描画
plt.plot(range(1, len(ppn.errors_) + 1),
         ppn.errors_,
         marker = 'o'
         )
# 軸ラベルの設定
plt.xlabel('Epochs')
plt.ylabel('Number of updates')

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

plt.show()

2次元のデータセットの決定境界を可視化するための関数を実装する

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'
                   )

決定境界を可視化

# 決定領域のプロット
plot_decision_regions(X, y, classifier = ppn)

# 軸ラベルの設定
plt.xlabel('Sepal length [cm]')
plt.ylabel('Peral length [cm]')

# 凡例の設定
plt.legend(loc = 'upper left')

# グラフを保存
plt.savefig('fig2-8.png')

plt.show()

参考文献

\bf{\textcolor{red}{記事が役に立った方は「いいね」を押していただけると、すごく喜びます \ 笑}}
ご協力のほどよろしくお願いします。

Discussion