機械学習 はじめよう

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

この記事を読むのに必要な時間:およそ 4 分

人工データの生成

これで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 回までデータを回して学習

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

著者プロフィール

中谷秀洋(なかたにしゅうよう)

サイボウズ・ラボ(株)にてWebアプリ連携や自然言語処理を中心に研究開発を行いながら,英単語タイピングゲーム iVocaをサービス提供している。

URLhttp://d.hatena.ne.jp/n_shuyo/
Twitterhttp://twitter.com/shuyo