機械学習 はじめよう

第17回パーセプトロンを実装してみよう

少し間が空いてしまいましたが、今回は実践編。第15回で紹介したパーセプトロンを実装してみましょう。

環境はこれまでと同じくPython/numpy/matplotlibを用います。インストールなどの準備は第6回を参照してください。

パーセプトロンの復習

第15回で紹介したパーセプトロンの学習アルゴリズムをもう一度簡単に振り返っておきましょう。

2次元平面上のデータ点(xn,yn)(n=1,…,N)に正解ラベルtn∈{+1,-1}が与えられているとします。パーセプトロンは、次の予測式の結果がすべて正解になるようにパラメータa, b, cを決めるものでした。

そのようなパラメータは、次の手順で求めることができます。

データの中からランダムに1点(xn,yn)を取り出し、f(x,y)に代入すると、現在のパラメータを用いた予測値として+1または-1が得られます。それが正解tnと一致する場合は何もせず、一致しなかった場合、次のようにパラメータを更新します。

続いてまた別のデータ点をランダムに取って、この操作を繰り返します。

すべてのデータ点について「予測→外れたら更新」を実行したら、また最初から繰り返します。予測が外れる点が無くなったら学習終了です。

ここまで、2次元空間のデータについて説明してきましたが、一般のD次元空間でも基本は同じです。

D次元空間の点xnに対して、M次元空間への特徴関数(写像)φ(x)とすると、予測関数とパラメータの更新式はそれぞれ次のようになります。

これらを使って、上の説明と全く同じようにパラメータを更新します。φ(x,y)=(x,y,1), w=(a,b,c)とおくと、先ほどの2次元の場合に一致します。

パラメータベクトルwの次元は、元の空間の次元Dではなく、特徴空間の次元Mになる点に注意してください。

パーセプトロンの実装

それではパーセプトロンを実装していきますが、まずはどのように実装していくのがよいのかを考えてみます。

一般にプログラムを書くとき、最初に初期化などの準備処理をきちんと記述してから、中核となる部分に取りかかるというのが通常の順番になります。

ところがこういったアルゴリズムのとき、教科書や論文の説明に擬似コードが添えられていれば親切でいいのですが、そうでない場合は中核となる数式が記述されているだけで、準備処理については「読めばわかるよね?」という感じで特に解説されてません。

しかし慣れてないとそのあたりがわかりませんから、説明を読んでも実装できないということが起こります。

そこで今回はまず中核となるところを実装して、それを動かすのに必要な準備処理を後から用意するという順番で実装しましょう。そうすれば、他のアルゴリズムを実装するときにも今回の手順が参考になるかもしれませんからね。

さて、まずは上の更新式を実装します。見ての通りとても簡単な式ですから実装も簡単です。

ここでは簡単のためデータ点が2次元空間上にある場合を実装するのですが、せっかくですから、一般式のほうで実装することにしましょう。

というわけで、まずは特徴関数φを定義します。

import numpy as np

# データ点を特徴ベクトルに変換
def phi(x, y):
  return np.array([x, y, 1])

φの引数のデータ点をベクトルで与える場合は、そのベクトルの末尾に1をつけ加えた新しいベクトルを返すという処理を書くことになりますが、それにはnumpyのconcatenate関数が使えます。今回はこちらの方法は使いませんが、覚えておくと便利でしょう。

# 末尾に定数項にあたる 1 を付け加えるバージョン
# (今回はこちらは使いません)
def phi(x):
  return np.concatenate((x, [1]))

次は予測とパラメータの更新を書きます。数式を素直に実装するとこうなります。

# 予測
predict = np.sign((w * phi(x_n, y_n)).sum())

# 予測が不正解なら、パラメータを更新する
if predict != t_n:
  w += t_n * phi(x_n, y_n)

こうしてみると、xn, yn, tnそしてwが必要ということがわかります。

その中で一番簡単なのはwですから、これをまず指定しましょう。このパーセプトロンのパラメータは0で初期化するのでしたね。

w = np.zeros(3)  # 3次の 0 ベクトル

データは2次元空間ですが、特徴関数φの返り値は3次元ですから、wも3次元であることに注意してください。

次にデータ点(xn,yn)や正解tnですが、これはN個ある中からランダムに取ってくるのでしたね。Nまでの乱数を取ればいいでしょうか。

いえ、その方法では「すべてのデータ点について評価したら、また最初から繰り返す」ことができません。最初にランダムな順番を決めてしまって、後はそれにしたがって一周させるのがいいでしょう。

Pythonで0からN-1までの配列を作るにはrange関数を、それをバラバラに並べ替えるにはrandom.shuffle関数を用います。

import random

w = np.zeros(3)  # 3次の 0 ベクトル

list = range(N)
random.shuffle(list)

for n in list:
  x_n = X[n]
  y_n = Y[n]
  t_n = T[n]
  # 予測
  predict = np.sign((w * phi(x_n, y_n)).sum())

  # 予測が不正解なら、パラメータを更新する
  if predict != t_n:
    w += t_n * phi(x_n, y_n)

さらに「予測が外れる点が無くなったら学習終了」というのも必要です。これは予測を外した回数を数えておけば簡単に実現できます。

while True:
  list = range(N)
  random.shuffle(list)

  misses = 0 # 予測を外した回数
  for n in list:
    x_n, y_n = X[n, :]
    t_n = T[n]

    # 予測
    predict = np.sign((w * phi(x_n, y_n)).sum())

    # 予測が不正解なら、パラメータを更新する
    if predict != t_n:
      w += t_n * phi(x_n, y_n)
      misses += 1

  # 予測が外れる点が無くなったら学習終了(ループを抜ける)
  if misses == 0:
      break

人工データの生成

これでxn, yn, tn, wは解決しましたが、X, Y, TとNという未定義の変数が増えてしまいました。X, Yはデータ点、Tはその正解、Nはその個数ですから、いずれも学習データを表す変数です。

そこで何でもいいから適当なデータを取ってきて……いえいえ、パーセプトロンの場合、そういうわけにはいきません。というのも、パーセプトロンは線形分離可能な問題しか解けないからでしたね(詳しくは連載第15回参照⁠⁠。

しかし、現実のデータでは完全に線形分離可能なものはなかなかありません。そこで自分で作成することにしましょう。

2次元空間上の点(x,y)をランダムに100個生成し、それが適当な分離平面(直線)よりも上にある場合は正解ラベルとしてt=+1を、下にある場合はt=-1を振ることにします。

そういったデータ{(xn,yn)}とT={tn}を生成するためのスクリプトは次のようになります。

# データ点の個数
N = 100

# ランダムな N×2 行列を生成 = 2次元空間上のランダムな点 N 個
X = np.random.randn(N, 2)

def h(x, y):
  return 5 * x + 3 * y - 1  #  適当に決めた真の分離平面 5x + 3y = 1

T = np.array([ 1 if h(x, y) > 0 else -1 for x, y in X])

ランダムなデータ点の生成には、正規乱数を生成するnp.random.randnを使っています。この方法は、主に原点付近に集まりつつ、周りにもいい感じに散らばったデータ点が手軽に生成できるのでおすすめです。

このように乱数を使って人工データを作ることはとても一般的なのですが、実行する度に異なるデータになると困る場合も多いでしょう。その場合には、乱数のシードとして適当な値を与えます。それによって同じ乱数が得られ、つまりデータも毎回同じとなります。

# データ点のために乱数列を固定(シードに 0 を与える場合)
np.random.seed(0)

プログラムの途中でまた「毎回違う乱数」が欲しくなった場合は、引数なしでnp.random.seedを呼び出しなおしてください。

シードを0にした場合のデータ点の分布は次のようになります。t=+1である点は赤、t=-1である点は青で表しています。

画像

ところでここで書いた生成方法だと2次元のデータ点を一つの変数Xに入れているため、データ点(xn,yn)を取り出すところをすこし修正しなければなりません。

  # 修正前
  x_n = X[n]
  y_n = Y[n]
  # 修正後
  x_n, y_n = X[n,:]

最初からデータ構造をどのように実装するか設計しておけば、このような手戻りをなくすことができます。しかし、そのようにあらかじめ先を見越して考えることは経験がないと難しいでしょう。

今回の手順のように、まずは使いやすいように変数を割り当てておき、次に初期化しやすいように変数を定義・生成して、必要に応じてすりあわせて修正していくというのも、一つのやり方だと思いますよ。

最後に、データ点の散布図と求めた分離平面を描くコードです。

# 図を描くための準備
seq = np.arange(-3, 3, 0.02)
xlist, ylist = np.meshgrid(seq, seq)
zlist = [np.sign((w * phi(x, y)).sum()) for x, y in zip(xlist, ylist)]

# 分離平面と散布図を描画
plt.pcolor(xlist, ylist, zlist, alpha=0.2, edgecolors='white')
plt.plot(X[T== 1,0], X[T== 1,1], 'o', color='red')
plt.plot(X[T==-1,0], X[T==-1,1], 'o', color='blue')
plt.show()

これらのコードの部品をつなげた、パーセプトロンのコードの完成形は次のようになります。

import numpy as np
import matplotlib.pyplot as plt
import random

# データ点の個数
N = 100

# データ点のために乱数列を固定
np.random.seed(0)

# ランダムな N×2 行列を生成 = 2次元空間上のランダムな点 N 個
X = np.random.randn(N, 2)

def h(x, y):
  return 5 * x + 3 * y - 1  #  真の分離平面 5x + 3y = 1

T = np.array([ 1 if h(x, y) > 0 else -1 for x, y in X])

# 特徴関数
def phi(x, y):
  return np.array([x, y, 1])

w = np.zeros(3)  # パラメータを初期化(3次の 0 ベクトル)

np.random.seed() # 乱数を初期化
while True:
  list = range(N)
  random.shuffle(list)

  misses = 0 # 予測を外した回数
  for n in list:
    x_n, y_n = X[n, :]
    t_n = T[n]

    # 予測
    predict = np.sign((w * phi(x_n, y_n)).sum())

    # 予測が不正解なら、パラメータを更新する
    if predict != t_n:
      w += t_n * phi(x_n, y_n)
      misses += 1

  # 予測が外れる点が無くなったら学習終了(ループを抜ける)
  if misses == 0:
    break

# 図を描くための準備
seq = np.arange(-3, 3, 0.02)
xlist, ylist = np.meshgrid(seq, seq)
zlist = [np.sign((w * phi(x, y)).sum()) for x, y in zip(xlist, ylist)]

# 分離平面と散布図を描画
plt.pcolor(xlist, ylist, zlist, alpha=0.2, edgecolors='white')
plt.plot(X[T== 1,0], X[T== 1,1], 'o', color='red')
plt.plot(X[T==-1,0], X[T==-1,1], 'o', color='blue')
plt.show()

パーセプトロンの役割

このコードを実行すると、生成したデータ点から分離平面を学習し、それを描画します。

画像

このように「データをきれいに分ける一本の線」を見つけるのがパーセプトロンの目的です。

そんな直線があることくらいグラフを見れば一目でわかってしまうのに、何がうれしいの?と思ってしまうかもしれませんが、実はそんなに簡単な問題ではありません。

ここでは2次元のデータを使っていますからグラフが描けますが、実際にはグラフなどとても描けないもっと高い次元、例えば「1,000次元空間の999次元平面」を見つけることを考えてみてください。そのような場合でもパーセプトロンを使うことで、目に見えない分離平面でも自動的に見つけてくれるのです。

パーセプトロンの感じをつかむためにも、今回のコードでいろいろ条件を変えて実行してみるといいでしょう。例えば分離平面を少し変えてみたり、データ点の個数を増減したり、少し頑張って次元を増やしてみたり。

中でも、パーセプトロンに線形分離不可能なデータを与えるとどうなるのかは興味深いですよね。分離平面を定義する関数h(x,y)を二次関数などにすれば簡単に実験できます。

ただしその場合は、パーセプトロンの学習を何回繰り返しても終了条件を満たすことはありませんので、例えば1,000回など適当な回数で打ち切るようにしておくといいでしょう。

具体的には、上記のコードのwhile Trueのところを、次のよう書き換えるだけで済みます。

#while True:
for i in xrange(1000): # 1000 回までデータを回して学習

次回はロジスティック回帰を紹介します。この連載で扱う最後のモデルになる予定です。お楽しみに。

おすすめ記事

記事・ニュース一覧