0
0

単純パーセプトロンの学習をPythonで実装する

Last updated at Posted at 2024-07-05

はじめに

先日、自己学習のために単純パーセプトロンの学習をPythonを用いて実装したので、その備忘録として記事に残します。今回は「2クラス分類において、線形分離可能かを判定するコードの実装」をゴールとしました。

パーセプトロンの学習方法

はじめに単純パーセプトロンの学習について簡単におさらいします。
まず、任意の入力パターン$x_i (i=1,2, \cdots ,n)$と、重み$w_i (i=0,1, \cdots ,m)$を用いて以下の識別関数$g(x)$を用意します。

g(x) = w_0 + w_1x_1 + w_2x_2 +  \cdots + w_{m-1}x_{n-1} + w_mx_n 

これを使って入力パターン$x$のクラス分類を行います。「ある入力に対して出力される$g(x)$の値が一定以上ならクラスAに、そうでなければクラスBに属す」などといった振る舞いをします。例えば2クラス分類の場合、関数$g(x)$の出力によって次のようにクラスを決定できます(分類クラスは$c_0, c_1$)。

\left\{
    \begin{array}{ll}
    g(x) > 0 & \Rightarrow  x \in c_0\\
    g(x) < 0 & \Rightarrow  x \in c_1\\
    \end{array}
\right.

$0$を決定境界として、それ以上であればクラス$c_0$に、そうでなければクラス$c_1$に属すると判断できます。これを実現するために、入力データすべてにおいて、識別関数の出力が正しくなるように重み$w_i$を決定することが、パーセプトロンの学習です。

実装

先んじて、今回実装したコードを記載します。簡単な解説は後述。

環境

実装環境は以下の通りです。データセットはsk-learnのirisデータセットを使用しました。
・Windows 11 Pro
・Python 3.12.3
・irisデータセット

実装コード

TrainPerceptron.py
# ===== import =====
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris # irisデータ読み込み

# ===== 変数定義 =====
iris = load_iris()  # irisデータ
epochs = 100        # エポック数
LearningRate = 0.5  # 学習率
dim = 5             # 入力次元数
weights = [0]*dim   # 重みベクトル


# ===== irisデータを整形する関数 =====
# --- 出力:irisデータのデータフレーム(df) ---
def DataFormat():
    df = pd.DataFrame(iris.data, columns=iris.feature_names)
    df['target'] = iris.target  # taeget列を生成
    df['dim'] = 1               # 次元を合わせる
    return df

# ===== 重みベクトルを初期化(-1から1のランダム)する関数 =====
def WeightsIni(weights):
    for i in range(len(weights)):
        weights[i] = np.random.default_rng().uniform(-1, 1)
    print('weights_ini:', weights, '\n')
    return weights

# ===== パーセプトロンの学習を行う関数 =====
# --- 入力:2つのクラス, df ---
def TrainPerceptron(DataA, DataB, df):
    print('START:', DataA, ',', DataB)
    # --- 変数定義 ---
    dfX = pd.concat([df[df['target'] == DataA], df[df['target'] == DataB]])
    dfX.loc[df['target'] == DataA, 'target'] = 1   # ダミー変数
    dfX.loc[df['target'] == DataB, 'target'] = -1  # ダミー変数
    dfX = dfX.reset_index(drop=True)               # インデックスを振り直す

    # --- 重みの初期化 ---
    global weights  # 重みベクトル
    weights = WeightsIni(weights)    

    # --- 学習の実行 ---
    for epoch in range(epochs):
        MissCount = 0 # 誤識別した回数
        for i in range(len(dfX)):
            x = dfX.iloc[i].drop('target').values # 説明変数
            target = dfX.at[i, 'target']          # 目的変数

            # 識別関数による計算
            ans = DiscriminationFunc(x, weights, target)
            
            # 重みベクトルの更新
            if ans < 0:
                MissCount += 1
                for j in range(dim):
                    weights[j] += LearningRate * x[j] * target
        
        # 正しく識別できたら終了
        if MissCount == 0:
            break
        
    # --- 結果出力 ---
    if MissCount == 0:
        print('DONE: linear separable')
    else:
        print('DONE: NOT linear separable')
    print('weights_end', weights)
    print('miss', MissCount)
    print('epoch:', epoch)
    return 0

# ===== 識別関数 =====
# --- 入力:特徴ベクトルx, 重みベクトル, 学習パターンの属するクラス ---
# --- 出力:正負 ---
def DiscriminationFunc(xweights, target):
    ans = 0
    for i in range(len(x)):
        ans += weights[i]*x[i]
    if ans*target > 0:
        return 1  # 正識別した
    else:
        return -1 # 誤識別した  

# ===== main関数 =====
def main():
    # データ整形
    df = DataFormat() 

    # パーセプトロンの学習
    print('0: setosa, 1: versicolor, 2: virginica\n')
    TrainPerceptron(0, 1, df)

if __name__ == '__main__':
    main()

各関数のふるまい

データ整形

TrainPerceptron.py
# ===== import =====
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris # irisデータ読み込み

# ===== irisデータを整形する関数 =====
# --- 出力:irisデータのデータフレーム(df) ---
def DataFormat():
    df = pd.DataFrame(iris.data, columns=iris.feature_names)
    df['target'] = iris.target  # taeget列を生成
    df['dim'] = 1               # 次元を合わせる(*1)
    return df

sk-learnのirisデータをデータフレームに格納しています。重みベクトルとデータの要素数を合わせるために、(*1)の行を追加しました。以下の式変形において$x_0$に該当します。

\begin{align}
g(x) = w_0 + w_1x_1 + w_2x_2 +  \cdots + w_{m-1}x_{n-1} + w_mx_n  \\
    = w_0x_0 + w_1x_1 + w_2x_2 +  \cdots + w_{n-1}x_{n-1} + w_nx_n
\end{align}

識別関数

TrainPerceptron.py
# ===== 識別関数 =====
# --- 入力:特徴ベクトルx, 重みベクトル, 学習パターンの属するクラス ---
# --- 出力:正負 ---
def DiscriminationFunc(x, weights, target):
    ans = 0
    for i in range(len(x)):
        ans += weights[i]*x[i]
    if ans*target > 0:
        return 1  # 正識別した
    else:
        return -1 # 誤識別した 

先述した通りの識別関数を、for文を回して計算し、変数ansに格納しています。
ここで、分類クラスを表す変数targetは1 or -1を取る質的な変数です。ansの正負がtargetの正負と一致していれば正しく識別したと判断します。

学習

TrainPerceptron.py
# ===== パーセプトロンの学習を行う関数 =====
# --- 入力:2つのクラス, df ---
def TrainPerceptron(DataA, DataB, df):
    '''
    省略
    '''
    # --- 学習の実行 ---
    for epoch in range(epochs):
        MissCount = 0 # 誤識別した回数
        for i in range(len(dfX)):
            x = dfX.iloc[i].drop('target').values # 説明変数
            target = dfX.at[i, 'target']          # 目的変数

            # 識別関数による計算
            ans = DiscriminationFunc(x, target, weights)
            
            # 重みベクトルの更新
            if ans < 0:
                MissCount += 1
                for j in range(dim):
                    weights[j] += LearningRate * x[j] * target
        
        # 正しく識別できたら終了
        if MissCount == 0:
            break
        
    # --- 結果出力 ---
    '''
    省略
    '''
    return 0

対象の学習データを格納したデータフレームを作成し、その学習データの数だけfor文を回して、少しづつ識別関数の重みを修正します。データに対して誤識別しなくなった場合、その時点で学習を終了します。
識別関数によって-1が返ってきた(誤識別した)ときに重みベクトルを更新します。更新するときの式は以下のようになります。

\left\{
    \begin{array}{ll}
    w' = w + η*x (クラスc_0をc_1と識別したとき)\\
    w' = w - η*x (クラスc_1をc_0と識別したとき)\\
    \end{array}
\right.

正負が異なるだけなので、1か-1を取る変数targetを計算に使用しました。もとより多クラスへの拡張は考えていないから、これが楽かなあ。

結果

0: setosa, 1: versicolor, 2: virginica

START: 0 , 1
weights_ini: [0.9538111946133632, 0.3363357385833583, 0.39864598153337094, -0.115127847711751, -0.012107471414915416]

DONE: linear separable
weights_end [0.5538111946133628, 2.286335738583358, -3.851354018466629, -1.8151278477117507, 0.4878925285850846]
miss 0
epoch: 5

ためしにsetosaとversicolorとで分類を行ったら、線形分離可能と判定されて終了しました。ちゃんと重みが更新されていますね。

まとめ

今回はじめての記事執筆でした。何かしらの参考になれば幸いです。

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