機械学習に詳しくなりたいブログ

機械学習や数学について勉強した内容を中心に書きます。100%趣味です。記事は数学的に厳密でなかったり誤りを含んでいるかもしれません。ご指摘頂ければ幸いです。

パーセプトロン

パーセプトロンは2クラスを分類する手法です。訓練データ\mathbf x, tが与えられたとき、


  y(\mathbf x) = f(\mathbf{w}^{T} \mathbf{x}) \tag{1}

で表されるモデルを考えます。\mathbf{x}はダミー入力の定数を含みます。また、t \in \{-1,1\}とします。ここで関数f


f(a) = \begin{cases}
    1 & (a \ge 0) \\
    -1 & (a \lt 0)
  \end{cases} \tag{2}

とします。つまり、2つのクラスを±1で表し、関数fは入力が正のときに1のクラス、負のときに-1のクラスと判別するということです。訓練データをそれぞれのクラスに分類できるようなパラメータ\mathbf{w}を求めることが目的です。

モデルの定義より、正しく分類されていれば、任意のデータに対してt \mathbf{w}^{T} \mathbf{x} \ge 0となることがわかります。そこでパーセプトロンでは誤差関数を


\displaystyle E(\mathbf{w}) = -\sum_{n \in C} t_n \mathbf{w}^{T} \mathbf{x}_n \tag{3}

と定義します。Cは誤識別したデータの集合を表します。誤識別されるとt_n \mathbf{w}^{T} \mathbf{x}_n \lt 0となるので、式(3)は誤識別が多いほどE(\mathbf{w})が大きくなることがわかります。最適解は勾配法を用いて求めます。

式(3)の勾配は


\displaystyle \nabla E = - \sum_{n \in C} t_n \mathbf{x}_n \tag{4}

です。(参考:ベクトルの微分)これを用いて最急降下法のアルゴリズムを使って解を求めます。つまり、


\displaystyle \mathbf{w}^{(\tau + 1)} = \mathbf{w}^{(\tau)} + \eta \sum_{n \in C}  t_n \mathbf{x}_n \tag{5}

により、パラメータを更新していきます。求めたいのは最小値なので、式(4)の勾配を減算します。\etaは学習率と呼ばれ、1回の更新量を調整するパラメータで任意に設定します。\tauはアルゴリズムのステップ数です。全てのデータが正しく判別できた時点の\mathbf{w}を求める解とします。実際には、局所解に落ちないように確率的勾配降下法が用いられるようです。誤識別したデータの中からランダムで1つ選択し、


\displaystyle \mathbf{w}^{(\tau + 1)} =  \mathbf{w}^{(\tau)}  + \eta t_n \mathbf{x}_n \tag{6}

で更新していきます。ランダムで1つずつ更新するか、誤識別した全データで勾配を求めて更新するかどうかが違うのみです。

さてアルゴリズムは非常に単純なので実装も簡単です。適当に作った2つのデータが識別できるかどうか実験してみました。 f:id:opabinia2:20180707221742p:plain ちゃんと識別できていますね。入力を非線形変換すれば曲線の識別もできます。(参考:入力を非線形変換したパーセプトロン

今回のコードです。今回のような単純な例では確率的勾配降下法でも最急降下法でも違いはわかりません。

# パーセプトロン
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm


# 判別クラスを返す(グラフ表示用)
def f(x0, x1):
    if np.dot(w.T, np.array([x0, x1, 1.0])) > 0:
        return 0
    else:
        return 1


# 全データ数
N = 300

# 入力次元数
D = 2

# クラス数
K = 2

# 学習率
Eta = 1.0

# ランダムシードを固定
np.random.seed(0)

# 2クラス分のデータを作成
mean = np.array([[1, 2.5], [0, -2.5]])
cov = [[1.0, -0.7], [-0.7, 1.0]]
x = np.empty([D, N])
t = np.empty([N])
for i in range(N):
    # -1 , 1 のラベルをランダムで作る
    t_ = np.random.randint(K)
    t[i] = 2.0*t_-1.0
    x[:, i] = np.random.multivariate_normal(mean[t_], cov)
# 定数項をまとめて追加
x = np.vstack([x, np.ones(N)])

# 重みベクトルwを初期化 定数項があるので入力次元+1
w = np.random.uniform(-1, 1, D+1)

# 誤りがなくなるまで更新を繰り返す
while True:
    # 更新処理の順をランダムで決める
    index = np.random.permutation(np.arange(0, N))

    break_flag = True
    # 更新処理
    # 確率的勾配降下法
    for i in range(N):
        if t[index[i]] * np.dot(w.T, x[:, index[i]]) < 0:
            w += Eta * t[index[i]] * x[:, index[i]]
            break_flag = False

    # 最急降下法
    # grad = 0.0
    # for i in range(N):
    #  if t[index[i]] * np.dot(w.T, x[:, index[i]]) < 0:
    #      grad += Eta * t[index[i]] * x[:, index[i]]
    #      break_flag = False
    #  w += grad
    if break_flag:
        break

plt.xlim(-5, 5)
plt.ylim(-5, 5)

# 識別直線
x_graph = np.linspace(-5, 5, 2)
y_graph = (-w[0] * x_graph - w[2]) / w[1]
plt.plot(x_graph, y_graph, color="black")

# グラフの色分け
a, b = np.meshgrid(np.linspace(-5, 5, 1000), np.linspace(-5, 5, 1000))
vec_f = np.vectorize(f)
plt.contourf(a, b, vec_f(a, b), alpha=0.2, cmap=cm.coolwarm)

plt.scatter(x[: ,np.where(t==1)][0], x[:,np.where(t==1)][1], color="blue", alpha=0.5)
plt.scatter(x[: ,np.where(t==-1)][0], x[:,np.where(t==-1)][1], color="red", alpha=0.5)
plt.show()