はじめに
先日、自己学習のために単純パーセプトロンの学習を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データセット
実装コード
# ===== 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(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 # 誤識別した
# ===== main関数 =====
def main():
# データ整形
df = DataFormat()
# パーセプトロンの学習
print('0: setosa, 1: versicolor, 2: virginica\n')
TrainPerceptron(0, 1, df)
if __name__ == '__main__':
main()
各関数のふるまい
データ整形
# ===== 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}
識別関数
# ===== 識別関数 =====
# --- 入力:特徴ベクトル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の正負と一致していれば正しく識別したと判断します。
学習
# ===== パーセプトロンの学習を行う関数 =====
# --- 入力: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とで分類を行ったら、線形分離可能と判定されて終了しました。ちゃんと重みが更新されていますね。
まとめ
今回はじめての記事執筆でした。何かしらの参考になれば幸いです。