【ゼロから実装】perceptronをirisデータで実装してみた。　未経験から機械学習シリーズpt.2

## はじめに

今回は、教師あり機械学習の中でも最も基礎的なモデルである「パーセプトロン」を実装しました。
私自身、機械学習の学習を始めたばかりで、Pythonでコードを書くのも今回が初めてです。そんな初心者の私が、パーセプトロンを一から実装し、クラスとして構造化し、結果を可視化するところまで取り組んだことで、モデルの仕組みや学習の流れについて理解が深まりました。
ここでは、その過程で得られた知見や、実装の工夫・詳細について記録しておきたいと思います。

## パーセプトロンとは

パーセプトロンは、1958年にフランク・ローゼンブラットによって提案された、最も基本的なニューラルネットワークの一種です。  
複数の入力に対して重みをかけ、バイアスを足し、その合計が閾値を超えるかどうかによって出力を決定します。

## 数式による定式化

### 入力・重み・バイアス

- 入力ベクトル: $ \boldsymbol{x} = (x_1, x_2, \dots, x_n) $
- 重みベクトル: $ \boldsymbol{w} = (w_1, w_2, \dots, w_n) $
- バイアス: $ b \in \mathbb{R} $
入力ベクトル $\boldsymbol{x} = (x_1, x_2, \dots, x_n)$ は、モデルに与える複数の特徴量の集合です。  
重みベクトル $\boldsymbol{w} = (w_1, w_2, \dots, w_n)$ は、それぞれの入力成分に対応するパラメータで、学習によって調整されます。  
バイアス $b \in \mathbb{R}$ は、モデルの柔軟性を高める役割を持つ定数項です。

### 総入力（net input）

$$
z = \boldsymbol{w} \cdot \boldsymbol{x} + b = \sum_{i=1}^{n} w_i x_i + b
$$

### 出力
$$
\hat{y} =
\begin{cases}
1 & z \geq 0 \\\\
0 & z < 0
\end{cases}
$$

---

## 学習アルゴリズム

パーセプトロンは、予測が正しくなかったときにだけ重みとバイアスを更新します。  
学習は、以下の更新式に従って行われます。

### 重みとバイアスの更新式

$$
\begin{align}
\Delta w_j &= \eta \cdot (y^{(i)} - \hat{y}^{(i)}) \cdot x_j^{(i)} \\\\
\Delta b   &= \eta \cdot (y^{(i)} - \hat{y}^{(i)})
\end{align}
$$

ここで、

- $ \eta $ は学習率（0 < $\eta$ ≤ 1）
- $ y^{(i)} $ は正解ラベル
- $ \hat{y}^{(i)} $ はモデルの予測

---

## パーセプトロンが解ける問題・解けない問題

### 解ける問題

- 線形分離可能なデータ（例：Irisデータの `Setosa` vs `Versicolor` など）
このように二つのクラスを分離する線形の境界線が存在する時にはパーセプトロンを使って解くことが出来ます。
![scatter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4103169/b054e24c-694b-4e48-a65c-efc9cf05e0b4.png)
このグラフの可視化は次のコードを実行することで実現可能です。
```
import pandas as pd
import matplotlib.pyplot as plt 
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
df = pd.read_csv( url , header= None, encoding= 'utf-8')
x = df.iloc[0:100,[1,3]].values


plt.scatter(x[0:50,0],x[0:50,1],color = 'red',marker='o',label= 'setosa' )
plt.scatter(x[50:100,0],x[50:100,1],color = 'blue',marker='s',label= 'versicolor' )
plt.xlabel('sepal length [cm]')
plt.ylabel('petal length [cm]') 
plt.legend(loc = 'upper left')  #凡例
plt.show
```

### 解けない問題

- 非線形分離問題（例：XOR問題）

 非線形分離問題を解決するには、多層パーセプトロン（MLP）や非線形活性化関数を持つモデルが必要です。

## モデルの実装(初期ver)

### ライブラリとパラメータ設定

```
import numpy as np
import pandas as pd

eta  = 0.1       # 学習率
epoch_itr = 50   # 学習繰り返し回数
m = 4            # 特徴量の数
```

- NumPyとPandasを用意してデータ処理環境を整えました。

- 学習率は0.1に設定しました。

- 繰り返し学習の回数を50回に設定しました。

###  Irisデータの読み込みと前処理
```
URL = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
df = pd.read_csv(URL, header=None, encoding='utf-8')

trainingdata = df.iloc[0:100, 0:4]
ytrue = np.where(df.iloc[0:100, 4] == 'Iris-setosa', 0, 1)
```

今回はこちらのサイト
https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data
からIrisデータセットを取得しました。
その中から最初の100件のデータを抽出し、2種類の花のデータだけを扱うようにしています。
ラベルは文字列のままだと機械学習で扱いにくいため、`Iris-setosa`を0、その他の`Iris-versicolor`を1に変換しています。

### 重みとバイアスを初期化

```
weight = np.random.randn(m)
bias = float(np.random.randn(1))
```
学習を開始する前に、モデルの重みとバイアスをランダムな値で初期化しました。
重みは特徴量の数（4つ）に合わせたベクトルとして用意し、バイアスはスカラー値として初期値を設定しています。

### 学習開始！
```
for epoch in range(epoch_itr):
    error = 0
    for i in range(100):
        x = trainingdata.iloc[i]
        y = ytrue[i]
        z = weight @ x + bias
        output = 1 if z >= 0 else 0
        diff = y - output
        if diff != 0:
            error += 1
        
        weight += eta * diff * x
        bias += eta * diff
    
    print(f"epoch{epoch}, error{error}")
```
各`epoch`で全100件のサンプルを使って順に学習します。
出力は重み付き和が`0`以上なら`1`、それ未満で`0`です。
予測誤差があれば重みとバイアスを調整します。
誤分類数（`error`）を毎`epoch`表示して学習進度を確認しています。

### 完成したコード
今までの結果をまとめると次の通りです。
<details><summary>完成したコード</summary>

```
import numpy as np
import pandas as pd

eta  = 0.1# 学習率
epoch_itr = 50 # 各データに対する学習回数
m = 4 # 特徴量の数

URL = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
df =  pd.read_csv(URL , header=None ,encoding='utf-8')  
trainingdata  = df.iloc[0:100,0:4]
ans = df.iloc[0:100,4].values
ytrue = np.where(df.iloc[0:100,4].values == 'Iris-setosa',0,1)        #0と1に変換

weight = np.random.randn(m)
bias = float(np.random.randn(1))

for epoch in range(epoch_itr):
    error = 0
    for i in range(100):
        x = trainingdata.iloc[i]
        y = ytrue[i]
        z = weight@x + bias
        
        if z>=0:
            output = 1
        else:
            output = 0

        diff = y - output
        if diff != 0:
            error += 1
        
        weight += eta*diff*x
        bias += eta*diff
    
    if epoch % 1 == 0 :
        print (f"epoch{epoch},error{error}")

print(weight)
print(bias)
```
</details>

<details><summary>出力結果</summary>

```
epoch0,error3
epoch1,error3
epoch2,error2
epoch3,error1
epoch4,error0
epoch5,error0
epoch6,error0
epoch7,error0
epoch8,error0
epoch9,error0
epoch10,error0
...
0   -0.497735
1   -0.714625
2    1.432495
3    0.880663
Name: 0, dtype: float64
-0.2783979533965122
```

</details>
学習中の誤分類数の推移を見ることで、学習がうまく進んでいるかどうかを簡易的に判断できます。また、Perceptronは早期に学習が収束し、正確な分類ができるようになったことがわかります。
学習が進むにつれて誤分類数が減少し、安定したモデルが完成しています。


### クラス化による実装の整理
次に、一連の処理（学習率の設定、学習の繰り返し、重み更新など）をクラス内にまとめることで、構造が整理されて見通しがよくしました。
```
import numpy as np
import pandas as pd 

class Perceptron:

    def __init__(self, eta, epoch_itr, X, Y):
        self.eta = eta
        self.epoch_itr = epoch_itr
        self.X = X
        self.Y = Y
        self.weight = np.random.randn(X.shape[1])
        self.bias = float(np.random.randn(1))
        self.errors = np.zeros(epoch_itr)
    def fit(self):
        for epoch in range(self.epoch_itr):
            error = 0
            for i in range(len(self.X)):
                update = self.eta * (self.Y[i] - self.predict(self.X[i]))
                self.weight += update * self.X[i]
                self.bias += update
                if update != 0:
                    error += 1
            self.errors[epoch] = error   
        return self 
    
    def net_input(self, X):
        return X @ self.weight + self.bias
    
    def predict(self,X):
        return self.net_input(X)>=0
```
このようにクラス化することによって、外部から簡単に学習と予測が呼び出せるようになり、他のデータセットにも再利用可能な設計になりました。
また、`fit`, `predict`, `net_input` をメソッドとして分けることで、学習・予測・出力計算の役割が明確になりました。

### 誤分類の数の可視化
各`epoch`に対する`error`の数を可視化するコードは次の通りです。
先程のクラス化されたパーセプトロンのコードとグラフでの可視化のコードを組み合わせて作成できます。
<details><summary>詳細</summary>

```
import numpy as np
import pandas as pd 
import matplotlib.pyplot as plt 
class Perceptron:

    def __init__(self, eta, epoch_itr, X, Y):
        self.eta = eta
        self.epoch_itr = epoch_itr
        self.X = X
        self.Y = Y
        self.weight = np.random.randn(X.shape[1])
        self.bias = float(np.random.randn(1))
        self.errors = np.zeros(epoch_itr)
    def fit(self):
        for epoch in range(self.epoch_itr):
            error = 0
            for i in range(len(self.X)):
                update = self.eta * (self.Y[i] - self.predict(self.X[i]))
                self.weight += update * self.X[i]
                self.bias += update
                if update != 0:
                    error += 1
            self.errors[epoch] = error   
        return self 
    
    def net_input(self, X):
        return X @ self.weight + self.bias
    
    def predict(self,X):
        return self.net_input(X)>=0
URL = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
df =  pd.read_csv(URL , header=None ,encoding='utf-8')  
trainingdata  = df.iloc[0:100,0:4].values
ytrue = np.where(df.iloc[0:100,4].values == 'Iris-setosa',0,1)        #0と1に変換

model = Perceptron(0.9,100,trainingdata,ytrue)
model.fit()
 #moder.errorsでerrorの配列を取得できる

plt.plot(range(1,20),model.errors[1:20],color='black',marker='o')

plt.xlabel('epochs')
plt.ylabel('error counts') #軸ラベルを作成することが出来る
plt.show

```
</details>

実行結果は次のようになります。
epochが増えるにつれて誤分類は0に収束する事が分かります。
したがって、このモデルは正しく学習出来ているといえます。
![error_plot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4103169/d518a509-ba3d-4e35-b028-027a660494f3.png)

### 決定境界の可視化
特徴量が二つの時の決定領域の可視化をします。
```
URL = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
df =  pd.read_csv(URL , header=None ,encoding='utf-8')  
trainingdata  = df.iloc[0:100,[0,2]].values    #特徴量を2個に変更した
ytrue = np.where(df.iloc[0:100,4].values == 'Iris-setosa',0,1) 

model = Perceptron(0.9,100,trainingdata,ytrue)
model.fit() #モデルを訓練する
```
次に、`np.meshgrid` と `np.arange` を用いて、決定領域を評価するための座標格子を作成します。
```
URL = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
df =  pd.read_csv(URL , header=None ,encoding='utf-8')  
X  = df.iloc[0:100,[0,2]].values
x1_min , x1_max = X[:,0].min() - 1, X[:,0].max() + 1 
x2_min , x2_max = X[:,1].min() - 1, X[:,1].max() + 1

xx1 , xx2 = np.meshgrid(np.arange(x1_min,x1_max,0.02),np.arange(x2_min,x2_max,0.02))  #これで範囲内の全座標を表現することが出来る
```
これをモデルに適用するため、座標のペアについての一次元配列に変換し、モデルに適用して予測します。結果は`Z = model.predict(grid)`で`0`または`1`で保存されます。
```
grid = np.c_[xx1.ravel(),xx2.ravel()]    #座標のペアを作成し、一次元の配列にすることで訓練後のモデルに適用できるようにした。
Z = model.predict(grid)
Z = Z.reshape(xx1.shape)   #元の配列の形に戻す
```
得られた結果を可視化します。
可視化のときは`plt.contourf`を使うと便利です。分類の結果によってグリッドの色が変化します。
```
plt.contourf(xx1,xx2,Z,alpha = 0.3)
plt.scatter(X[:50,0],X[:50,1],color='red',marker='o',label = 'Setosa')
plt.scatter(X[50:100,0],X[50:100,1],color='blue',marker='s',label = 'Versicolor')  #散布図を作成できる
```
実行結果は次の通りです。
訓練データのプロットと一緒に出力するときれいに分類できていることが分かります。
![iris.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4103169/1ae35513-6099-42d1-807b-de5288033360.png)

## おわりに
今回は、パーセプトロンの基本的な動作原理からIrisデータセットを使った分類タスクにおいて実装と可視化までやりました。最初は手続き的なコードから始まり、クラスへと発展させられたのは大きい学びになったと思います。今回のコード作成を通じてpythonの知識も少し深まったので良かったです。
次回はもう1種類の基本的な機械学習であるADALINEについて実装し、ここにoutputしたいと考えています。