【Python】パーセプトロンモデルを利用した線形分類の実装

早稲田大学本庄高等学院所属でプログラミング系の活動をしているアイスです。
# 概要
　パーセプトロンを用いた学習アルゴリズムをPythonで実装します。Irisの花のデータセットから、「がく片の太さ」と「花びらの太さ」を取り出し、計100個のデータを描画、その分布を元に決定境界を学習させる。
　結果として、次のようなグラフが描画されることを目標とすることにする。

![iris.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/f27f162c-177e-f941-833c-7f618a0cbf41.png)

　上の画像は、Google Colaboratory を利用して描画したグラフです。このグラフを描画するまでの過程を説明する。

# データセットの取得
　scikit-learnはimportせず、インターネット上のmachine-learning-databasesから直接データを取得。

```python:main.py
import numpy as np
import pandas as pd

# machine-learning-databasesからデータを取得
df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data', header=None)
'''
irisデータセット(150, 4)
- がく片の長さ(cm)
- がく片の太さ(cm)
- 花びらの長さ(cm)
- 花びらの太さ(cm)
- タグ（Iris-Setosa, Iris-Versicolour, Iris-Virginica)
  0:50が "Iris-Setosa", 50:100が "Iris-Versicolur", 100:150が "Iris-Virginca"
'''
# がく片の太さ(1)と花びらの太さ(3)を取得
# shape : (100, 2)
x = df.iloc[:100, [1, 3]].values
# タグを取得
# shape : (100, 1)
y = df.iloc[:100, 4].values
# クラスラベルを変換（Iris-Setoraを-1、Iris-Versicolourを１）
y = np.where(y == 'Iris-setosa', -1, 1)
```
　Google Colaboratory を利用しているメリットとしては、Numpyなどのライブラリを追加で準備する必要がないためである。VS code などを使う場合は、Anacondaを別でダウンロードして、環境を構築したりなど、初学者にとって面倒かつ、挫折する原因にもなり得るので、Google Colaboratory を使うことをお勧めしている。

### 各種関数：

+ [pd.read_csv](https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html?highlight=read_csv#pandas.read_csv)
+ [pd.DataFrame.iloc](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.iloc.html?highlight=iloc#pandas.DataFrame.iloc)
+ [np.where](https://numpy.org/doc/stable/reference/generated/numpy.where.html#numpy.where)

# Irisデータの描画
　後の分類実装を可視化するために、Irisのデータセットをグラフにプロットし、感覚的に確認できるようにする。

```python:main.py
import matplotlib.pyplot as plt

# Setora を青色のoで表示
plt.scatter(x[:50, 0], x[:50, 1], color='blue', marker='o', label='Setora')
# Versicolur を赤色のxで表示 
plt.scatter(x[50:100, 0], x[50:100, 1], color='red', marker='x', label='Versicolur')
# ラベルの設定
plt.xlabel('sepal width [cm]')
plt.ylabel('petal width [cm]')
plt.legend(loc='upper left')
# 表示
plt.show()
```

結果：
![iris1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/027507cb-382f-7e2d-25fa-e64ebe4afe70.png)
　上の図のように点がプロットされた。青色のoがSetoraを表し、赤色のxがVersicolurを表している。x軸が「がく片の幅」、y軸が「花びらの幅」を表している。
グラフに描画するとSetoraとVersicolurが明らかに分類されていることが感覚的に理解できるようになった。これを機械に分類させるために、パーセプトロンの分類器のモデルを作成し、学習させる。

### 各種関数：

+ [plt.scatter](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.scatter.html?highlight=scatter#matplotlib.pyplot.scatter)
+ [plt.xlabel](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.xlabel.html?highlight=xlabel#matplotlib.pyplot.xlabel)
+ [plt.ylabel](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.ylabel.html?highlight=ylabel#matplotlib.pyplot.ylabel)
+ [plt.legend](https://matplotlib.org/api/legend_api.html?highlight=legend#module-matplotlib.legend)
+ [plt.show](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.show.html?highlight=show#matplotlib.pyplot.show)

# パーセプトロンの分類器の実装
　以下に示すのはパーセプトロンの概念図です。

![02_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/fa1fb3ff-8f0b-011c-23da-c487b4771f54.png)
（Sebastian Raschka『達人データサイエンティストによる理論と実装　Python機械学習プログラミング』福島真太郎監訳、クイープ訳、インプレス出版、2016年）より引用
　m個の要素に対してデータxと重みwを組み合わせる。

```math
1, x_1, x_2, …, x_m
```
```math
w_0, w_1, w_2, …, w_m
```

その総和はステップ関数（活性化関数）に渡され、誤差が計算される。この一連の流れを何回か繰り返すことで誤差を減らし、重みが更新する。

```python:main.py
class Perceptron():
    def __init__(self, eta=0.1, n_iter=10):
        self.eta = eta
        self.n_itr = n_iter
    
    def fit(self, x, y):
        self.w_ = np.zeros(1 + x.shape[1])
        self.errors = []
        print(self.w_)

        for _ in range(self.n_itr):
            error = 0
            for i, target in zip(x, y):
                update = self.eta * (target - self.step(i))
                self.w_[1:] += update * i
                self.w_[0] += update
                error += int(update != 0.0)
            self.errors_.append(error)
    
    def net_input(self, x):
        return np.dot(x, self.w_[1:]) + self.w_[0]

    def step(self, x):
        return np.where(self.net_input(x) >= 0.0, 1, -1)

```

　オブジェクト指向に基づいてPerceprotonクラスを定義する。後にインスタンスを作成して、fit関数から使用すると、

fit関数
　↓
step関数
　↓
net_input関数
　↓
step関数
　↓
fit関数

の順で処理される。fit関数では、エポック数(n_itr)回の処理を繰り返し、誤差を小さくしていく。その過程で、targetと予測値の差を取り、それに学習係数(eta)をかけた値を重みを随時更新、errorを更新していく。step関数では、net_input関数で内積をとったものをstep関数で処理し、予測値を計算していく。targetoと予測値の差に学習係数をかけたものを関数で処理し、重みを更新する。
　この処理を繰り返します。

### 各種関数：

+ [np.zeros](https://numpy.org/doc/stable/reference/generated/numpy.zeros.html?highlight=zeros#numpy.zeros)
+ [np.ndarray.shape](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.shape.html?highlight=shape#numpy.ndarray.shape)
+ [np.dot](https://numpy.org/doc/stable/reference/generated/numpy.dot.html?highlight=dot#numpy.dot)

# 誤差の収束の確認
　上で実装したモデルで本当に分類が学習されているのかを確認するために、errorsに格納されている誤差が収束していく様子をグラフに描画して確認する。

```python:check.py
# インスタンス化
ch = Perceptron(eta=0.1, n_iter=10)
# モデルに学習させる
ch.fit(x, y)
# 誤差を描画
plt.plot(range(1, len(ch.errors) + 1), ch.errors, marker='v')
plt.xlabel('epochs')
plt.ylabel('errors')
plt.show()
```

結果：
![iris2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/11dab210-17fb-7962-3305-9bcb04da45bb.png)
　x軸がエポック数、y軸が誤差を表している。上図のように、誤差はすぐに収束し、２回目以降は既に収束していることが分かった。

### 各種関数：

+ [plt.plot](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html?highlight=plot#matplotlib.pyplot.plot)

# 決定境界の描画
　カラーマップを事前準備し、その後、格子点の座標を用意し、等高線を色分けしながら表示する。

```python:main.py
from matplotlib.colors import ListedColormap

def plot_decision_regions(X, y, classifier, resolution=0.01):
  # マーカーと色の準備
  # 2種類(1か-1)で十分
  markers = ('s', 'x')
  colors = ('red', 'blue')
  mp = ListedColormap(colors[:len(np.unique(y))])
  # 「がく片の太さ」の最大値と最小値を取得。
  # 描画に余裕を持たせるために+1と-1
  x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
  # 「花びらの太さ」の最大値と最小値を取得
  # 描画に余裕を持たせるために+1と-1
  x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
  # 格子点の座標をresolution(0.01)ごとに取得
  xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution), np.arange(x2_min, x2_max, resolution))
  # パーセプトロンの分類器を使って、zにデータを格納
  z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
  z = z.reshape(xx1.shape)
  # 格子点とデータをもとに線をプロット
  plt.contourf(xx1, xx2, z, alpha=0.1)
  plt.xlim(xx1.min(), xx1.max())
  plt.ylim(xx2.min(), xx2.max())

  for i, cl in enumerate(np.unique(y)):
    plt.scatter(x=X[y == cl, 0], y=X[y == cl, 1], alpha=0.8, color=camp(i), marker=markers[i], label=cl)
```

　markers, colorsは点をプロットする際のマーカーとプロットを事前に準備する。今回取り扱うデータは、Setora(-1)とVersicolur(1)の二種類だけなので、マーカーと色も２種類で十分。

　matplotlibからListedColormapを読み込む。np.unique(y)では、yの要素は２種類（1と-1）しかないので、mpには二つの色が格納される。

　x1_min, x1_maxは「がく片の太さ」の最大値と最小値を取得し、グラフのx軸の幅を決めます。x2_min, x2_maxも同様に「花びらの太さ」の最大値と最小値を取得し、y軸の幅を決める。グラフの端を見やすくするために、それぞれ+1か-1している。

　xx1、xx2はそれぞれx軸とy軸の格子点を表している。上で求めた最小値と最大値を両端に置き、resolution(0.01)ごとの間隔で格子点を設ける。この時使用するmeshgrid関数に対応させるため、それぞれをndarrayの型に統一し、処理を行う。

　classfierインスタンスからpredict関数を呼び出し、zにデータを格納、それらのデータをcontourf関数を使って等高線として色分けして表示する。この時のalphaは色味を調整するものなので、適当でよい。最後に、xlimとylim関数でグラフの両端を設定して、等高線の表示は完了である。

　for文内は、「がく片の太さ」と「花びらの太さ」をそれぞれプロットしている。最初に準備したcolorとmarkerから要素を取り出してきて表示している。labelはそれぞれ、-1がSetora、1がVercicolurを表している。
![iris.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/e4eb3137-ecf8-5423-2eb2-394fd80bb4b8.png)
　始めに設定した目標のグラフが表示されたことが確認できた。

### 各種関数：

+ [matplotlib.colors.LinkedColormap](https://matplotlib.org/api/_as_gen/matplotlib.colors.ListedColormap.html?highlight=listedcolormap#matplotlib.colors.ListedColormap)
+ [np.meshgrid](https://numpy.org/doc/stable/reference/generated/numpy.meshgrid.html?highlight=meshgrid#numpy.meshgrid)
+ [np.arange](https://numpy.org/doc/stable/reference/generated/numpy.arange.html?highlight=arange#numpy.arange)
+ [np.array](https://numpy.org/doc/stable/reference/generated/numpy.array.html?highlight=array#numpy.array)
+ [np.ndarray.ravel](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.ravel.html?highlight=ravel#numpy.ndarray.ravel)
+ [np.ndarray.reshape](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.reshape.html?highlight=reshape#numpy.ndarray.reshape)
+ [plt.countourf](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.contourf.html?highlight=contourf#matplotlib.pyplot.contourf)
+ [plt.xlim](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.xlim.html?highlight=xlim#matplotlib.pyplot.xlim)
+ [plt.ylim](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.ylim.html?highlight=ylim#matplotlib.pyplot.ylim)

# まとめ
　今回は、Pythonを用いて、irisのデータをパーセプトロンと呼ばれる機械学習の手法を用いて分類した。データから、「がく片の太さ」と「花びらの太さ」を抽出し、それぞれの特徴を学習させ、その結果をグラフに表示させるところまですることが出来た。

# 感想
　関数の種類を見て貰ったらわかるが、パーセプトロンの仕組み自体は、一度理解してしまえば実装もさほど難しくなく、簡単にまとめることができる。その一方で、描画に関しては、格子点、ラベル、両端、色などを全て設定しなければいけないため、手間がかかり、挫折の原因として大きいのではないかと思った。本記事は初学者向けに書いたため、そのような挫折をしないことを「させない」ことを目標に書いた。そのため、Google Colaboratoryを使い環境構築を簡単にしたり、関数の説明を載せて調べる手間を省くなどした。
　自分としても学んだことを整理できたので、よかったと思う。

## 参考文献

+ [pandas documentation](https://pandas.pydata.org/docs/index.html)
+ [numpy documentation](https://numpy.org/doc/)
+ [matplotlib documentation](https://matplotlib.org/contents.html)
+ [[機械学習]iriデータセットを用いてscikit-learnの様々な分類アルゴリズムを試してみた](https://qiita.com/ao_log/items/fe9bd42fd249c2a7ee7a)
+ Sebastian Raschka『達人データサイエンティストによる理論と実装　Python機械学習プログラミング』福島真太郎監訳、クイープ訳、インプレス出版、2016年