機械学習： パーセプトロン の Python コードを1行1行理解してデータ分類してみた

初期の機械学習アルゴリズムに、 パーセプトロン とADALINEというのがあるらしいので、今回はまずパーセプトロンを実装していきます。パーセプトロンは、しきい値によってデータを区別する単純な学習規則です。

Python機械学習プログラミング（←[Amazonのリンクはこちら](https://www.amazon.co.jp/%E7%AC%AC3%E7%89%88-Python%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-%E9%81%94%E4%BA%BA%E3%83%87%E3%83%BC%E3%82%BF%E3%82%B5%E3%82%A4%E3%82%A8%E3%83%B3%E3%83%86%E3%82%A3%E3%82%B9%E3%83%88%E3%81%AB%E3%82%88%E3%82%8B%E7%90%86%E8%AB%96%E3%81%A8%E5%AE%9F%E8%B7%B5-impress-gear/dp/4295010073/?_encoding=UTF8&pd_rd_w=ptznS&pf_rd_p=76664ffb-7b25-48c6-a4a1-bd795b0f767b&pf_rd_r=P99YG1APHNHV0HA9NSP2&pd_rd_r=758ba9d0-5ee4-4733-9642-65700b92128e&pd_rd_wg=QWmus&ref_=pd_gw_ci_mcx_mi)）を読みながら進めているのですが、それだけだとコード1行1行完璧に理解できていないので、めちゃくちゃ調べながらコード中にコメントを書き込んでいきたいと思います。（本記事はあくまでコードの理解を深めるものなので、計算や背景などに関する詳しい部分はこの本を読んで確認してください。）

## パーセプトロン

まず、パーセプトロンという学習規則についてです。
下の表のようなデータセットを何かしらの基準で分類していくことを考えます。

データテーブルの例

| |特徴量１ |特徴量２ |... |
|---|---|---|---|
|データ１ |x1_1 |x1_2 |... |
|データ２ |x2_1 |x2_2 |... |
|データ３ |x3_1 |x3_2 |... |
|： |：	|：	|...|

パーセプトロンはもうほとんど使われていないようですが、元祖機械学習アルゴリズムということで勉強しておいた方が後の機械学習の理解に役立つでしょう。

データの分類においてパーセプトロンは簡単に言えば、しきい値（nより大きいか小さいか）でAかBかを区別してみて、それが合ってたらOK、間違ってたら修正するというのを繰り返すものです。

じゃあ何を修正するの？という話なのですが、訓練データにはそれぞれ**重み**というのが掛け算でかかっています。もっというと、訓練データの持つ特徴量それぞれに重みがかかっています。この重みを修正するのです。

ちなみに重みは全ての訓練データ共通です。

w = (w1, w2, w3, …)

しきい値がどんな値であろうとこの重みで訓練データを大きくしたり小さくしたりできるので、しきい値を正しくくぐり抜けられるように重みを修正していくのです。

そして何をしきい値に通すの？というと、その一つのデータが持つそれぞれの特徴量×重みを全て足したものです。この計算をするのが、下の図でいう総入力関数です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388259/b4c6cbcc-fb2e-4e5b-b88e-a33b5709be07.png)

**ポイントは、答え合わせをするのはあくまでしきい値に通した後であり、予測ラベルさえ合ってれば重みが少々的外れでも何も言われないというところです。**

## Pythonで実装

筆者は以下のPythonコードをGoogleColaboratoryで実行しました。GoogleColaboratoryは環境構築が必要ないので便利です。

訓練データもネットから引っ張ってくるので用意する必要がありません！

早速ですが以下がパーセプトロンのプログラミングになります。

今回やることは、**2種類の花合計100枚を機械学習によって種類を区別し、結果を表示させる**ことです。

### パーセプトロン の実装

~~~python
# データの格納と操作に便利なNumPyパッケージの多次元配列を用いる
import numpy as np

# パーセプトロンオブジェクトを定義していく
class Perceptron(object):
    
    # コンストラクタ
    def __init__(self, eta=0.01, n_iter=50, random_state=1):
    # selfはインスタンス自身を表す
        # etaなどの各パラメータの説明はコードの下部に記載
        self.eta = eta
        self.n_iter = n_iter
        self.random_state = random_state

    #訓練データに適合させるメソッド
    def fit(self, X, y):
        
        # NumPyの乱数生成器rgenを生成
        rgen = np.random.RandomState(self.random_state)
        # 重みを標準偏差0.01の正規分布中の小さな乱数で初期化
        self.w_ = rgen.normal(loc=0.0, scale=0.01, size=1 + X.shape[1])
        self.errors_ = []

        # 訓練回数分だけ繰り返すfor文
        for _ in range(self.n_iter):
            # 誤分類の個数を0で初期化
            errors = 0
            # 訓練データを一つずつ処理していく
            for xi, target in zip(X, y):
                # 重みの更新　predictメソッドでクラスラベルを予測
                # 学習率×(答えと予測の差)×この訓練データ
                update = self.eta * (target - self.predict(xi))
                # 重みw_1以降の更新
                self.w_[1:] += update * xi
                # 更新w_0の更新
                self.w_[0] += update
                # 更新する重みが0でない、つまり予測が答えとずれていた場合
                # →誤分類にカウントされる
                errors += int(update != 0.0)
            # 今回の誤分類の個数を格納
            self.errors_.append(errors)
        return self

    def net_input(self, X):
        # 総入力を計算
        return np.dot(X, self.w_[1:]) + self.w_[0]

    def predict(self, X):
        # 1ステップ後のクラスラベルを返す
        return np.where(self.net_input(X) >= 0.0, 1, -1)
~~~

**パーセプトロンの分類器（コード全体）**

|パラメータ  |型  |値  |
|---|---|---|
|eta  |float  |学習率（0.0 < eta <= 1.0）  |
|n_iter  |int  |訓練データの訓練回数  |
|random_state  |int  |重みを初期化するための乱数シード  |

|属性 |型  |値  |
|---|---|---|
|w_  |1次元配列  |適合後の重み |
|errors_  |リスト |各エポックでの誤分類の数 |

**def fit(self, X, y)**

|パラメータ  |型  |値  |
|---|---|---|
|X  |データフレーム shape：[n_examples, n_features]  |訓練データ |
|y  |データフレーム shape：[n_examples] |目的変数  |

**戻り値**
self : object

かなりコメントが多くなってしまいましたが、かなり理解できました。

### 訓練データセットを用意

では実行するための訓練データを用意します。
GoogleColaboratoryでは上記のコードブロックの下に次のコードブロックを追加します。

~~~python
import os
import pandas as pd

#Irisデータセットを使う（ネット環境必要）
s = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
print('URL:', s)

df = pd.read_csv(s,
                 header=None,
                 encoding='utf-8')

#確認のため最後の5行だけ出力してみる
df.tail()
~~~

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388259/3cd113b4-4773-2641-2195-da46617dc56f.png)

次に

1. Iris-setosaの50枚の花とIris-versicolorの50枚の花に対応する100個のクラスラベルを抽出（6行目）
2. これらを1(versicolor)と-1(setosa)の2つのクラスラベルに変換し、ベクトルyに代入（9行目）
3. 100個の訓練データそれぞれの1列目（がく片の長さ）と3列目（花びらの長さ）を特徴量行列Xに代入（12行目）
4. この2つの特徴量軸で散布図に表す

を実行します。

~~~python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np

# 1-100行目の目的変数の抽出
y = df.iloc[0:100, 4].values

# Iris-setosaを-1、それ以外（Iris-versicolor）を1に変換
y = np.where(y == 'Iris-setosa', -1, 1)

# 1-100行目の1、3列目を抽出
X = df.iloc[0:100, [0, 2]].values

# setosaは赤の〇
plt.scatter(X[:50, 0], X[:50, 1],
            color='red', marker='o', label='setosa')
# versicolorは青の✕
plt.scatter(X[50:100, 0], X[50:100, 1],
            color='blue', marker='x', label='versicolor')

plt.xlabel('sepal length [cm]')
plt.ylabel('petal length [cm]')
plt.legend(loc='upper left')

# 散布図を表示
plt.show()
~~~

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388259/7d3a6069-19f3-8dde-3dd6-a3b84bbd26fc.png)
↑この散布図のsetosaとversicolorの境界線を手書きするのは簡単そうですね。それをパソコンにやらせてみましょう。

ここからがパーセプトロンのアルゴリズムの出番です。いざ訓練！！

### パーセプトロン に通して訓練

訓練しながら、ちゃんと境界線ができたのかをチェックするために各エポックの誤分類の個数もプロットしていきます。

~~~python
# パーセプトロンのオブジェクトの生成（インスタンス化）
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.show()
~~~

ちなみに**エポック**とは、何度も繰り返すパーセプトロン処理それぞれのことを指します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388259/2e3cc6fb-1130-2606-86d2-bde4f0e11428.png)
このグラフから、6回目のエポックでパーセプトロンは既に収束していることが分かります。
ということはこの時点で訓練データを完全に境界線で分類できるようになっているはずです。

### 図で確認

では可視化していきましょう。

~~~python
from matplotlib.colors import ListedColormap


def plot_decision_regions(X, y, classifier, resolution=0.02):

    # マーカーとカラーマップの準備
    markers = ('s', 'x', 'o', '^', 'v')
    colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
    cmap = ListedColormap(colors[:len(np.unique(y))])

    # 決定領域のプロット
    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, resolution),
                           np.arange(x2_min, x2_max, resolution))
    # 各特徴量を1次元配列に変換して予測を実行
    Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
    # 予測結果を元のグリッドポイントのデータサイズに変換
    Z = Z.reshape(xx1.shape)
    # グリッドポイントの等高線のプロット
    plt.contourf(xx1, xx2, Z, alpha=0.3, cmap=cmap)
    # 軸の範囲の設定
    plt.xlim(xx1.min(), xx1.max())
    plt.ylim(xx2.min(), xx2.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=cl, 
                    edgecolor='black')
~~~

続けてさらに次のコードで図を完成させます。

~~~python
plot_decision_regions(X, y, classifier=ppn)
plt.xlabel('sepal length [cm]')
plt.ylabel('petal length [cm]')
plt.legend(loc='upper left')

plt.show()
~~~

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388259/d83a75f4-5c17-dda7-9153-5bc2c61698de.png)
おー、本当にできてますね！

↓次回はADALINEを実装していきます。

https://qiita.com/haruki-lo-shelon/items/bca685ed13bcfe82dab3