1.6. 演習:パーセプトロン#

1.6.1. アルゴリズム#

パーセプトロンperceptron)は、ニューラルネットワークを構成する基本的なモデルの一つです。このモデルは、入力された特徴量に係数(パラメータ)を掛け合わせ、その合計が 0 以上かどうかを基準にデータを分類する、シンプルなアルゴリズムです。例えば、腫瘍のサイズを特徴量として、その腫瘍細胞が良性か悪性かを予測するケースを考えてみましょう。

パーセプトロンは、ロジスティック回帰と同様に、入力された特徴量に対して係数を掛けて合計値を計算します。ここでこの合計値を \( z \) とします。

\[ z = \beta_{1}X + \beta_{0} \]

次に、この \( z \) が 0 以上であれば「悪性(1)」、0 未満であれば「良性(0)」を出力します。数式で表すと次のようになります。

\[\begin{split} \phi (z) = \begin{cases} 1 & (z \ge 0) \\ 0 & (z < 0) \end{cases} \end{split}\]

このとき関数 \(\phi(z)\) のことをステップ関数step function)と呼びます。ロジスティック回帰では、シグモイド関数を利用して出力値が 0 と 1 の間の値に変換するのに対し、パーセプトロンはステップ関数を用いて出力値を 0 または 1 に変換しています。

また、パーセプトロンのパラメータの推定方法も特徴的です。回帰分析やロジスティック回帰では、すべてのデータを用いて損失を最小化するようにパラメータを推定します。しかし、パーセプトロンでは、まずランダムな値でパラメータを初期化し、以下の更新式に基づいてデータを 1 つずつ処理しながらパラメータを更新していきます。

\[ \beta_{1}^{\text{new}} = \beta_{1} + \eta (y - \phi(z))x_{i} \]
\[ \beta_{0}^{\text{new}} = \beta_{0} + \eta (y - \phi(z)) \]

ここで、モデルの出力値 \(\phi(z) = \phi(\beta_{1}X + \beta_{0})\) と観測値が一致していれば、\((y - \phi(z))\) は 0 となり、\(\beta_{1}\) および \(\beta_{0}\) は更新されません。一方、モデルの出力値と観測値が異なれば、\((y - \phi(z))\)\(+1\) または \(-1\) となり、新しいパラメータが計算されます。パーセプトロンは同じデータセットをシャッフルしながら何度も学習を繰り返し、パラメータが更新されなくなるまで処理を続けます。このプロセスを学習または適合と呼びます。

\(\eta\) はモデルの出力値と観測値が異なるとき、その誤差をどの程度に拡大あるいは縮小するのかを調整するパラメータです。これを学習率learning rate)と呼びます。\(\eta\) が大きければ、モデルの出力が間違ったときに、その誤差が拡大され、更新されるパラメータの値は古い値から大きく変化します。逆に、\(\eta\) が小さいと、更新されるパラメータの値は、古い値に近い値となります。最適なパラメータを見つけるためには、この \(\eta\) を適切な値に調節する必要があります。\(\beta_{1}\)\(\beta_{0}\) はデータから計算(推測)するが、\(\eta\) は人間があらかじめ与えなければなりません。このようなパラメータのことをハイパーパラメータhyperprameter)と呼びます。

なお、パーセプトロンは非常にシンプルなモデルであるため、1 本の直線や平面で分離できないようなデータには対応できません。そのような複雑なデータに対しては、複数のパーセプトロンを適切に組み合わせた多層パーセプトロン(MLP)などが用いられます。

1.6.2. 演習準備#

本節では、パーセプトロンの特徴やその限界を説明するために、scikit-learn ライブラリで人工的なサンプルデータを生成して利用します。また、本節では、次の Python ライブラリを利用します。numpy、pandas、matplotlib ライブラリはデータの読み込みや整形、可視化などに利用します。sklearn はサンプルデータセットの生成およびデータセットを訓練サブセットと検証サブセットに分けるために利用します。

import numpy as np
import pandas as pd
import matplotlib.animation
import matplotlib.pyplot as plt
import sklearn.datasets
import sklearn.model_selection
import sklearn.linear_model
import sklearn.preprocessing
import sklearn.metrics

1.6.3. 演習(パーセプトロン)#

sklearn.datasets のメソッドを利用して 2 クラス分類のサンプルデータを生成します。また、モデルの構築や可視化などの作業を簡単にするために、特徴量をあらかじめ平均 0 分散 1 となるように正規化します。

X, Y = sklearn.datasets.make_blobs(n_samples=200, n_features=2, centers=2, cluster_std=0.5, random_state=0)
X = sklearn.preprocessing.StandardScaler().fit_transform(X)
data = pd.concat([
    pd.Series(Y, name='Y'),
    pd.DataFrame(X, columns=['X1', 'X2'])
], axis='columns')

data.head()
Y X1 X2
0 1 1.364949 -0.833338
1 1 0.540478 -0.829575
2 1 1.578361 -1.054727
3 1 0.372215 -1.039384
4 0 0.380856 1.333665
Hide code cell source
fig = plt.figure()
ax = fig.add_subplot()
ax.scatter(data['X1'][data['Y'] == 0], data['X2'][data['Y'] == 0], label='0', alpha=0.5)
ax.scatter(data['X1'][data['Y'] == 1], data['X2'][data['Y'] == 1], label='1', alpha=0.5)
ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.legend()
plt.show()
../_images/69a3eb286c1c1436013e0de15d3c5c4f0582e8315020be2a934fcc0f6612e22a.png

このデータを 8:2 の割合で訓練サブセットと検証サブセットに分割します。

train_data, valid_data = sklearn.model_selection.train_test_split(data, test_size=0.2, random_state=0)

次に、このサンプルデータセットにある特徴量 X1 および X2 を利用して、Y を予測するモデルを、パーセプトロンで構築します。パーセプトロンでは、

\[ z = \beta_{2}x_{1} + \beta_{1}x_{2}s + \beta_{0} \]

を計算し、

\[\begin{split} \phi (z) = \begin{cases} 1 & (z \ge 0) \\ 0 & (z < 0) \end{cases} \end{split}\]

となるようにパラメータ \(\beta_{1}\)\(\beta_{0}\) を推定します。パーセプトロンは、前述のように、1 サンプルずつ読み込んで、パラメータを更新しています。そこで、パーセプトロンの学習過程を見るため、プログラミング言語の繰り返し構文 for を用いて、1 ステップずつ学習させるようにし、その学習の成果をアニメーションとして出力します。

なお、パーセプトロンの学習において、パラメータに初期値として小さな値を与えて、適切な学習率を設定する必要があります。しかし、その学習過程をわかりやすく可視化するために、パラメータ(model.coef_model.intercept_)に大きな初期値を与えて、また、学習が遅くなるように小さな学習率(eta0)を与えています。

model = sklearn.linear_model.Perceptron(max_iter=1, eta0=0.2, warm_start=True, shuffle=True)
model.coef_ = np.array([[-100, 10]])
model.intercept_ = 10

fig = plt.figure()
ax = fig.add_subplot()
frames = ([i for i in range(101) if i % 5 == 0]) # select 1 image per 5 images for animation

ax0 = ax.scatter(train_data['X1'][train_data['Y'] == 0], train_data['X2'][train_data['Y'] == 0], label='0', alpha=0.5)
ax1 = ax.scatter(train_data['X1'][train_data['Y'] == 1], train_data['X2'][train_data['Y'] == 1], label='1', alpha=0.5)
ax_line, = ax.plot([], [], color='#0094CD', label="Decision Boundary")
ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.legend()

def _update_step(i):
    model.fit(train_data.drop(columns=['Y']), train_data['Y'])
    w = model.coef_[0]
    b = model.intercept_
    if w[1] != 0:
        x_vals = np.linspace(train_data['X1'].min(), train_data['X1'].max(), 100)
        y_vals = -(w[0] / w[1]) * x_vals - b / w[1]
    else:
        x_vals = np.array([0, 0])
        y_vals = np.array([train_data['X2'].min(), train_data['X2'].max()])
    ax_line.set_data(x_vals, y_vals)

    return ax0, ax1, ax_line


ani = matplotlib.animation.FuncAnimation(fig, _update_step, frames=frames, interval=100, repeat=False, blit=True)

html = ani.to_jshtml()
plt.close(fig)
HTML(html)

ここで得られたモデルに対して、性能評価を行います。このサンプルデータセットでは、2 つのクラスが明らかに分離されているため、パーセプトロンのような単純なアルゴリズムでも完璧な分類が行えます。なお、分類問題を評価する指標には正解率、再現率、適合率などがあります。データセットの特徴および問題設定を考えて最適な指標を用いる必要があります。ここでは、データセットに含まれる正例(悪性)と負例(良性)のサンプルの数がほぼ同数であるため、評価指標として正解率を用います。

pred = model.predict(valid_data.drop(columns=['Y']))
acc = sklearn.metrics.accuracy_score(valid_data['Y'], pred)
print(acc)
1.0

なお、可視化が目的でなければ、次のように、Perceptron 関数に最大学習回数(max_iter)を指定して学習させることが一般的です。

model = sklearn.linear_model.Perceptron(penalty='l2', alpha=0.01, max_iter=100)
model.fit(train_data.drop(columns=['Y']), train_data['Y'])
acc = sklearn.metrics.accuracy_score(valid_data['Y'], pred)
print(acc)
1.0

1.6.4. 演習(線形分離不可能)#

平面あるいは多次元空間上に、一つの直線あるいは平面で、分類したいサンプルを明確に分離できる場合に線形分離可能linearly separable)といいます。数学的に言えば、n 次元空間上のふたつの集合を n − 1 次元の超平面で分離できることも線形分離可能と呼ぶ。逆に、分離できない場合を線形分離不可能linearly inseparable)といいます。

パーセプトロンを利用して、線形分離不可能なデータを分類してみよう。まずはそのようなデータを用意します。

X, Y = sklearn.datasets.make_gaussian_quantiles(n_samples=200, n_features=2, n_classes=2, random_state=0)
X = sklearn.preprocessing.StandardScaler().fit_transform(X)
data = pd.concat([
    pd.Series(Y, name='Y'),
    pd.DataFrame(X, columns=['X1', 'X2'])
], axis='columns')
train_data, valid_data = sklearn.model_selection.train_test_split(data, test_size=0.2, random_state=0)

data.head()
Y X1 X2
0 0 -0.277064 0.144542
1 1 -0.980677 -1.466450
2 0 -0.328820 0.362942
3 0 1.008124 0.332191
4 0 -0.802167 -0.606805
Hide code cell source
fig = plt.figure()
ax = fig.add_subplot()
ax.scatter(data['X1'][data['Y'] == 0], data['X2'][data['Y'] == 0], label='0', alpha=0.5)
ax.scatter(data['X1'][data['Y'] == 1], data['X2'][data['Y'] == 1], label='1', alpha=0.5)
ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.legend()
plt.show()
../_images/66af15b7f977e75f6c1020a3deb26ccc4f41aa44d3a3fbefa3488c2ed33a75ca.png

このデータを単純パーセプトロンで学習し、その過程を可視化します。

Hide code cell source
model = sklearn.linear_model.Perceptron(max_iter=1, eta0=0.2, warm_start=True, shuffle=True)
model.coef_ = np.array([[-100, 10]])
model.intercept_ = 10

fig = plt.figure()
ax = fig.add_subplot()
frames = ([i for i in range(101) if i % 10 == 0])

ax0 = ax.scatter(train_data['X1'][train_data['Y'] == 0], train_data['X2'][train_data['Y'] == 0], label='0', alpha=0.5)
ax1 = ax.scatter(train_data['X1'][train_data['Y'] == 1], train_data['X2'][train_data['Y'] == 1], label='1', alpha=0.5)
ax_line, = ax.plot([], [], color='#0094CD', label="Decision Boundary")
ax.set_xlabel('X1')
ax.set_ylabel('X2')
ax.legend()

def _update_step(i):
    model.fit(train_data.drop(columns=['Y']), train_data['Y'])
    w = model.coef_[0]
    b = model.intercept_
    if w[1] != 0:
        x_vals = np.linspace(train_data['X1'].min(), train_data['X1'].max(), 100)
        y_vals = -(w[0] / w[1]) * x_vals - b / w[1]
    else:
        x_vals = np.array([0, 0])
        y_vals = np.array([train_data['X2'].min(), train_data['X2'].max()])
    ax_line.set_data(x_vals, y_vals)

    return ax0, ax1, ax_line


ani = matplotlib.animation.FuncAnimation(fig, _update_step, frames=frames, interval=100, repeat=False, blit=True)

html = ani.to_jshtml()
plt.close(fig)
HTML(html)

可視化の結果から分かるように、一つのパーセプトロンは一つの直線をしか描けないので、どのように学習を進めても、線形分離不可能のデータを分離できません。この限界を克服するために、複数のパーセプトロンを繋げた多層パーセプトロンを利用します。