SEワンタンの独学備忘録

IT関連の独学した内容や資格試験に対する取り組みの備忘録

【機械学習】入門⑯ 誤差逆伝搬法(バックプロパゲーション)について学ぶ Python実装編


前回は誤差逆伝搬法について理論観点から学びましたので、今回はPythonによる実装をおこなっていきたいと思います。

↓↓理論編

www.wantanblog.com

事前準備

データ準備

データ自体は流用しているので、以前の記事内容の再掲になります。

アヤメの品種データ

■説明変数

0 sepal length (cm) がく片の長さ
1 petal length (cm) がく片の幅

■目的変数
3クラスに分類するという点だけ抑えておけばよいです。

0 setosa(ヒオウギアヤメ)
1 versicolor(ブルーフラッグ)
2 virginica(バージニカ)

検証を行うためにもともとあったデータを訓練データとテストデータに分割しています。

# データ準備
import numpy as npy
import matplotlib.pyplot as plt
%matplotlib inline
# 生データファイルから取り出す
sample_data = npy.load('neural_rawdata.npz')
# 入力値の設定
X=sample_data['X']
# がく片の長さの表示範囲設定
X_range0=sample_data['X_range0']
# がく片の幅の表示範囲設定
X_range1=sample_data['X_range1']
# クラス(答え)の設定
T3=sample_data['T3']

# データの並び替え
X1 = X[:50,:]
X2 = X[50:100,:]
X3 = X[100:150,:]

npy.random.shuffle(X1)
npy.random.shuffle(X2)
npy.random.shuffle(X3)

X_test=npy.r_[X1[:25,:], X2[:25,:], X3[:25,:]]
X_train=npy.r_[X1[25:50,:],X2[25:50,:],X3[25:50,:]]
T_test=npy.r_[T3[:25,:], T3[50:75,:], T3[100:125,:]]
T_train=npy.r_[T3[25:50,:],T3[75:100,:],T3[125:150,:]]

npy.savez('neural_data.npz',X_train=X_train, T_train=T_train,X_test=X_test,T_test=T_test,X_range0=X_range0,X_range1=X_range1)
データ表示

訓練データとテストデータをそれぞれグラフ上にプロットしているだけです。

import matplotlib.pyplot as plt
%matplotlib inline

# データの表示
def Show_data(x,t):
    wk,n=t.shape
    c=[[0,0,0],[.5,.5,.5],[1,1,1]]
    for i in range(n):
        plt.plot(x[t[:,i]==1,0],x[t[:,i]==1,1],linestyle='none',marker='o',markeredgecolor='black',color=c[i],alpha=0.8)
    plt.grid(True)

plt.figure(1,figsize=(8,3.7))
plt.subplot(1,2,1)
Show_data(X_train,T_train)
plt.xlim(X_range0)
plt.ylim(X_range1)
plt.title('Training Data')
plt.subplot(1,2,2)
Show_data(X_test,T_test)
plt.xlim(X_range0)
plt.ylim(X_range1)
plt.title('Test Data')

plt.show()

・出力結果

f:id:wantanBlog:20200613183233p:plain

前回実装部分

ニューラルネットワークモデルなど一部のものは以前に実装したものを流用することができます。
こちらも再掲です。

シグモイド関数
# シグモイド関数
def Sigmoid(x):
    y=1/(1+npy.exp(-x))
    return y
ニューラルネットワークモデル
# ニューラルネットワーク(wv重み、M中間層ノード数、K出力層ノード数、x入力層の入力値)
def FNN(wv, M, K, x):
    # Nデータ数 Dデータの入力次元
    N, D = x.shape
    # w 入力層⇒中間層の重み
    w = wv[:M * (D+1)]
    w = w.reshape(M,(D+1))
    # v 中間層⇒出力層の重み
    v = wv[M*(D+1):]
    v = v.reshape((K,M+1))
    # b 中間層の入力総和
    b = npy.zeros((N,M+1))
    # z 中間層の出力値
    z = npy.zeros((N,M+1))
    # a 出力層の入力総和
    a = npy.zeros((N,K))
    # y 出力層の出力値
    y = npy.zeros((N,K))
    
    # データごとに中間層、出力層の計算を行う
    for n in range(N):
        # 中間層の計算(シグモイド関数)
        for m in range(M):
            b[n,m] = npy.dot(w[m,:],npy.r_[x[n,:],1])
            z[n,m] = Sigmoid(b[n,m])
        # ダミーニューロン
        z[n,M]=1
        u=0
        # 出力層の計算()
        for k in range(K):
            a[n,k] = npy.dot(v[k,:],z[n,:])
            u = u + npy.exp(a[n,k])
        for k in range(K):
            y[n,k] = npy.exp(a[n,k])/u
    return y,a,z,b
平均交差エントロピー誤差
# 平均誤差エントロピー誤差
def CE_FNN(wv,M,K,x,t):
    N,D = x.shape
    y,a,z,b=FNN(wv,M,K,x)
    ce = -npy.dot(t.reshape(-1),npy.log(y.reshape(-1))) / N
    return ce

誤差逆伝搬法による実装

更新規則だけ再掲しておきます。

・重みwの更新規則

f:id:wantanBlog:20200613214816p:plain

・重みvの更新規則

f:id:wantanBlog:20200613214759p:plain

解析的微分の実装(dCE_FNN)

以前、最適な重み(wv)を求めるために数値微分を用いた実装を行いました。
それに対して今回は逆伝播による偏微分を行っているので解析的微分と言うことにします。

実装していきましょう。

・解析的微分

# 解析的微分
def dCE_FNN(wv,M,K,x,t):
    N,D = x.shape
    w = wv[:M*(D+1)]
    w = w.reshape(M,(D+1))
    v = wv[M*(D+1):]
    v = v.reshape((K,M+1))
    # xを入力してyを得る
    y,a,z,b = FNN(wv,M,K,x)
    dwv = npy.zeros_like(wv)
    dw = npy.zeros((M,D+1))
    dv = npy.zeros((K,M+1))
    # 中間層の誤差
    delta1 = npy.zeros(M)
    # 出力層の誤差
    delta2 = npy.zeros(K)

    # データの数だけループさせる
    for n in range(N):
        # dv 出力層のニューロン数だけループ
        for k in range(K):
            # 出力層の誤差
            delta2[k] = (y[n,k]-t[n,k])
            # vの勾配dvを求める(平均値)
            dv[k,:] = dv[k,:] + delta2[k]*z[n,:]/N
        # dw 中間層のニューロンの数だけループ
        for j in range(M):
            # 中間層の誤差
            delta1[j] = z[n,j]*(1-z[n,j])*npy.dot(v[:,j],delta2)
            # wの勾配dwを求める(平均値)
            dw[j,:] = dw[j,:] + delta1[j]*npy.r_[x[n,:],1]/N

    # dwvを生成
    dwv = npy.c_[dw.reshape((1,M*(D+1))), dv.reshape((1,K*(M+1)))]
    dwv = dwv.reshape(-1)
    return dwv

基本的には最初に示した式をPythonで実装しているだけになります。

更新規則に用いるdwとdvの対象データ数Nの数だけ求めて平均値を求めています。
戻り値はdwとdwをつなげた配列の形となります。

勾配法による最適パラメータの導出(Fit_FNN)

先に実装した解析的微分関数を用いて勾配法によるパラメータの導出を行います。

# 勾配法による最適パラメータの導出
def Fit_FNN(wv_init,M,K,x_train,t_train,x_test,t_test,n,alpha):
    wv = wv_init.copy()
    err_train =npy.zeros(n)
    err_test = npy.zeros(n)
    wv_hist = npy.zeros((n,len(wv_init)))
    for i in range(n):
        wv = wv -alpha*dCE_FNN(wv,M,K,x_train,t_train)
        err_train[i] = CE_FNN(wv,M,K,x_train,t_train)
        err_test[i] = CE_FNN(wv,M,K,x_test,t_test)
        wv_hist[i,:]=wv
    return wv, wv_hist, err_train, err_test

ここでの「n」はデータ数ではなく勾配法による更新を何回行うかのステップ数なので注意。

重みをwvとすることで、wとvの更新を一度に行っています。もちろん最適化は訓練データを使用します。
後はステップごとのエントロピー誤差を訓練データとテストデータの両方で求めることで誤差の遷移が分かります。

戻り値のwvは最終的な重みパラメータでwv_histはステップ数分の遷移データ。

分類の実施

実装した関数を使用して問題の答えを出します。
とはいっても中身の関数を変えているので最終的な出力部分の実装は、数値微分を用いた実装とほとんど変わりません。

# 境界線の表示
def show_FNN(wv,M,K):
    xn = 60
    x0 = npy.linspace(X_range0[0],X_range0[1],xn)
    x1 = npy.linspace(X_range1[0],X_range1[1],xn)
    xx0, xx1 = npy.meshgrid(x0,x1)
    x = npy.c_[npy.reshape(xx0,xn*xn,1),npy.reshape(xx1,xn*xn,1)]
    y,a,z,b = FNN(wv,M,K,x)
    plt.figure(1,figsize=(4,4))
    for ic in range(K):
        f = y[:,ic]
        f = f.reshape(xn,xn)
        f = f.T
        cont = plt.contour(xx0,xx1,f,levels=[0.5,0.7],colors=['blue','black'])
        cont.clabel(fmt='%.1f',fontsize=9)
        plt.xlim(X_range0)
        plt.ylim(X_range1)

# 勾配法のステップ数
N_step=1000
# 更新規則の学習率
alpha=0.215

M = 2
K = 3
# 重みの初期値設定
npy.random.seed(1)
WV_init = npy.random.normal(0,0.01,M*3+K*(M+1))
# 勾配法
WV,WV_hist,Err_train,Err_test=Fit_FNN(WV_init,M,K,X_train,T_train,X_test,T_test,N_step,alpha)

# グラフの表示
plt.figure(1,figsize=(12,3))
plt.subplots_adjust(wspace=0.5)

# 学習誤差の遷移表示
plt.subplot(1,3,1)
plt.plot(Err_train, 'black',label='training')
plt.plot(Err_test, 'blue', label='test')
plt.legend()

# 重みの時間遷移を表示
plt.subplot(1,3,2)
plt.plot(WV_hist[:,:M*3],'black')
plt.plot(WV_hist[:,M*3:],'orange')

# 境界線の表示
plt.subplot(1,3,3)
Show_data(X_test,T_test)
M = 2
K = 3
show_FNN(WV,M,K)
plt.show()

・出力結果

f:id:wantanBlog:20200613225829p:plain

左から「誤差の遷移」、「重みの遷移」、「テストデータに対する決定境界」の図です。

今回出力した誤差逆伝搬法と以前の数値微分による結果と比較してみます。

f:id:wantanBlog:20200613233219p:plain

使用しているデータは同じですが、訓練データとテストデータの振り分けはランダムで行っており、一度設定しなおしてしまったせいでやや結果はぶれていますが、おおむね同じ結果が得られていることが分かります。

結果は同じですが、算出スピードは数値微分よりも正確に微分式を導出して出力を行った場合の方がはるかにはやいです。
今回のニューラルネットワークモデルは2層の最小構成なので、数値微分で出力しても少し待てばいいぐらいですが、本格的な多層ネットワークモデルを構築する場合には処理速度の差がかなり顕著になるようです。

しかし、逆に言えば数値微分の方が実装というか計算式の導出は楽なので、微分して求めた結果の検証を行う際に使用することもできます。

次回はライブラリを使用していきます。

ーーーーーーーー
・今回のソース
python_dev/Neural_net3_prod.ipynb at master · wantanblog/python_dev · GitHub