単純パーセプトロンの学習を可視化してみる

結局DLって何やってるのよ、の復習、の前段

東大松尾研のDL研修を受け、 今までのコピペプログラマから脱却し、 数式から学んで自分でDLのライブラリが書けるレベルになったぜ!

…と思いいざ改めて書こうとするとキーボードが全く動かない。あるある。

で、復習がてらまずは単純パーセプトロン。 DLまでは遠いな…。

単純パーセプトロンって何よ

詳しい記事は ここ とか ここ とか ここ に譲るとして、 ようは、

  1. 線形分離可能な2値問題を解く
  2. それぞれの説明変数(とバイアス)に重み付けをしてきれいに分離する線形式を見つけ出す
  3. 学習は間違えた場合間違えた分だけ線の位置を修正する
  4. 間違えなくなったら学習を終える

です。

でも実際どんな風に正解に近づいているのよ… ってのを可視化するのが今回の記事。

書く

データがなくちゃ始まらないので、 とりあえず生成。

巷では2軸でやってばっかりなので、 私は3軸。差別化バンザイ。

答えとなる境界線は下記式とする。 (特に意味はない)

w0 * x0 + w1 * x1 + w2 * x2 + b = 0

w0 = 5

w1 = -3

w2 = 2

b = 1

データはとりあえず1000個ほど、1,0のラベルで、 それぞれ500個ずつ用意。

import numpy as np
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

N = 1000

w0 = 5
w1 = -3
w2 = 2
b = 1

# ラベル1
x0_rand_posi = stable_rand.rand(int(N/2))
x1_rand_posi = stable_rand.rand(int(N/2))
x2_rand_posi = -(w0/w2)*x0_rand_posi - (w1/w2)*x1_rand_posi -b/w2 + stable_rand.rand(int(N/2))

# ラベル0
x0_rand_nega = stable_rand.rand(int(N/2))
x1_rand_nega = stable_rand.rand(int(N/2))
x2_rand_nega = -(w0/w2)*x0_rand_nega - (w1/w2)*x1_rand_nega -b/w2 - stable_rand.rand(int(N/2))

train_x = np.array([np.append(x0_rand_posi,x0_rand_nega,axis=0),np.append(x1_rand_posi,x1_rand_nega,axis=0),np.append(x2_rand_posi,x2_rand_nega,axis=0)])
train_y = np.append(np.ones(int(N/2)),np.zeros(int(N/2)))

fig = plt.figure()
ax = Axes3D(fig)
ax.set_xlabel("x0")
ax.set_ylabel("x1")
ax.set_zlabel("x2")
ax.set_xlim3d(0, 1)
ax.set_ylim3d(0, 1)
ax.set_zlim3d(-3, 1)
ax.plot(train_x[0,:int(N/2)],train_x[1,:int(N/2)],train_x[2,:int(N/2)],"o",ms=4, mew=0.5,color="#00ff00")
ax.plot(train_x[0,int(N/2):],train_x[1,int(N/2):],train_x[2,int(N/2):],"o",ms=4, mew=0.5,color="#ff0000")
ax.view_init(19, 97)
plt.show()

f:id:kazuhitogo:20181025191515p:plain

それっぽいのが出来ました。 緑がラベル1で赤がラベル0ですね。

こっからガシガシ単純パーセプトロンをコーディングします。

# 重みとバイアス初期化
# 乱暴に全部1
w = np.ones(3)
b = np.ones(1)

# ステップ関数
def step(x):
    return 1 * (x > 0)
# 予測(単にwx+b)
def predict(w,x,b):
    return step(np.dot(w, x) + b)    


# 学習の推移を見るには履歴を残す必要があるので重みとバイアス記録用リスト
w_hist = []
w_hist.append(w)
b_hist = []
b_hist.append(b)

# 学習率
eps = .01

# オンライン学習するための、データを与える順番をランダムに
# (これやらないとずっとラベルが1のデータを500回与えた後、ラベル0のデータを500回…となる)
np.random.seed(4)
lerning_order = np.random.choice(np.arange(N),N,replace=False) 

# 学習
# 全部完璧に分類するまで実行する
while True:
    fin_flag = True
    for i in lerning_order:
        # 合ってたらdelta_wは0,間違ってたら間違った分☓学習率だけdelta_wを計算
        # delta_bも同様
        delta_w = eps * (train_y[i] - predict(w,train_x[:,i],b)) * train_x[:,i]
        delta_b = eps * (train_y[i] - predict(w,train_x[:,i],b))
        if all(delta_w == np.zeros(3)) and all(delta_b == np.zeros(1)):
            pass
        else:
            # 重みとバイアスを修正したときのみ履歴に突っ込む
            w_hist.append(w + delta_w)
            b_hist.append(b + delta_b)
            w += delta_w
            b += delta_b
        # 一回でも誤り判定があればTrueになる
        fin_flag *= all(delta_w == 0) * (delta_b == 0)
    if fin_flag:
        break

# 学習を終えたら境界直線を引く
# x2 = -(w0/w2)*x0 - (w1/w2)*x1 - b/w2 = 0を履歴順に出しているだけ。
x0_boundary = np.arange(0, 2)
x1_boundary = np.arange(0, 2)
    
x0_boundary, x1_boundary = np.meshgrid(x0_boundary, x1_boundary)
x2_boundary = []
for i in range(len(w_hist)):
    x2_boundary.append(-(w_hist[i][0]/w_hist[i][2])*x0_boundary - (w_hist[i][1]/w_hist[i][2])*x1_boundary - b_hist[i]/w_hist[i][2])

さて、こっから可視化

# gif動画として保存
def update_fig(i):
    ax.cla()
    ax.plot(train_x[0,:int(N/2)],train_x[1,:int(N/2)],train_x[2,:int(N/2)],"o",ms=4, mew=0.5,color="#00ff00")
    ax.plot(train_x[0,int(N/2):],train_x[1,int(N/2):],train_x[2,int(N/2):],"o",ms=4, mew=0.5,color="#ff0000")
    ax.plot_surface(x0_boundary,x1_boundary,x2_boundary[i])
    ax.view_init(33, 123)
    ax.set_title('train count = ' + str(i))
    ax.set_xlim3d(0, 1)
    ax.set_ylim3d(0, 1)
    ax.set_zlim3d(-3, 1)
    ax.set_xlabel("x0")
    ax.set_ylabel("x1")
    ax.set_zlabel("x2")
    

fig = plt.figure(figsize = (6, 6))
ax = Axes3D(fig)
anim = animation.FuncAnimation(fig,update_fig,interval=10,frames=len(w_hist))
anim.save('simple_perceptron_3d.gif',writer='imagemagick')

https://lh3.googleusercontent.com/U8mC5HEluW4wVtdZ3lpqCT8QtRrTUA-L9-JxqAeG_ikTbPoDJSn7JLDHrI51CFONn7_nm3PSA1xcuo9Gcc_jb0txX7sM4ruXZOZ0jnIE9xnZRPuogcrkZldTgIWXzSj4CKCw4LZOlAQ=s600-no

なんか…ヌルヌル… 落ち着きそうで… 落ち着かない…。

train_countはepochやイテレーションの回数ではなく、 wやbが上書きされた回数ですのであしからず。

732回書き換えて漸く終わりです。

これもjulia vs pythonやってみたいですね。

ちなみに↓で復習してます。

ざっつおーるせんきゅー。

2018/10/26追記

ソースコードgithubにうp

github.com