ML blog

IT 系のブログ

ニューラルネットワークの学習過程をアニメーションにしてみた

人工知能 (AI) や機械学習 (Machine Learning) の分野では Deep Learning が話題になっていますが、ベースとなるニューラルネットワーク(Neural Network)の学習過程を、3D CG を使ってアニメーション化してみました。 NN(ニューラルネットワーク)の説明では、入力層→隠れ層→出力層とそれぞれの間を線で結んだ図が良く出てきますが、実際にどういうタイミングでどの部分が活性化するかなど直感的に視覚で捉えられないかと思ったのが動機です。

概要

百聞は一見に如かず、まずはちょこっと再生してみてください。

youtu.be

実際の学習コードの動きを忠実に反映した動画を作ろうと思ったのですが、良いものがないか検索した所、以下ページの Python コードが簡潔だったので使わせて頂きました。本格的な学習コードと比べるとかなり簡略化されていますが、まずは学習過程を直感的に視覚化するには十分と判断しました。

iamtrask.github.io

学習コードの説明

上記ページには2つコードが掲載されていますが、使用したのは2番めの「3 Layer Neural Network」です。日本語コメントを追加したものを貼っておきます。

import numpy as np

# ユーティリティ関数が定義されています。
def nonlin(x,deriv=False):
  if(deriv==True):
    # deriv 引数が True の場合は、
    # 微分で傾きを出しています。
    return x*(1-x)

  # シグモイド関数の結果を返しています。
  # ここで x 引数の値が0~1の値に変換されます。
  # 例えば、仮に x が1億でも限りなく1に近づきますが
  # 決して1 は超えません。
  # また x 引数がマイナス10億でも0は下回りません。
  return 1/(1+np.exp(-x))
    
# 入力値(X変数)、つまり学習に使うデータです。
# 3値配列の学習データを4件使用しています。
X = np.array([[0,0,1],   # 1件目の学習データ
              [0,1,1],   # 2件目の学習データ
              [1,0,1],   # 3件目の学習データ
              [1,1,1]])  # 4件目の学習データ
                
# あるべき出力値(y変数)、つまり上記4件の入力値(学習データ)を
# NNで計算した結果としてのあるべき値です。
# 教師あり学習の教師の役目を果たすべきもので、
# このあるべき値との誤差を小さくするように機械学習を進めます。
# ラベル値と呼ばれることもあります。
y = np.array([[0],     # 1件目の学習データに対応するあるべき出力値
              [1],     # 2件目の学習データに対応するあるべき出力値
              [1],     # 3件目の学習データに対応するあるべき出力値
              [0]])    # 4件目の学習データに対応するあるべき出力値

np.random.seed(1)

# randomly initialize our weights with mean 0
# ウェイト変数(syn0,syn1)を定義して、初期化しています。
# 初期化は平均が 0 になるように、ランダムな値を与えています。
# syn0 には、入力層⇒隠れ層の間のウェイト値を格納します。
syn0 = 2*np.random.random((3,4)) - 1
# syn1 には、隠れ層⇒出力層の間のウェイト値を格納します。
syn1 = 2*np.random.random((4,1)) - 1

# 4件一組の学習データによる学習を1回のループとして、
# ループ毎に入力値から計算した出力値とあるべき値(y)との誤差
# を小さくするよう学習(ウェイトを調整)します。
# ループを回すごとに、ウェイトが調整された結果として、
# 誤差が徐々に小さくなっていき、学習が進むことになります。
# 各ループのことを Epoch と呼ぶこともあります。
# アニメーションではループ1~3回と1998~2000回目を描画し、
# 学習の進み具合を視覚化しています。
for j in xrange(60000):

  #------------------------------------------------------
  # フォワード プロパゲーション (Forward Propagation)
  #------------------------------------------------------
  # Feed forward through layers 0, 1, and 2
  # 入力層(l0変数)に入力値(X変数、学習データ)をセットします。
  # 1回のループでは4件の学習データを行列計算で一度に扱うため、
  # 4件すべてを入力層に設定しています。
  l0 = X
  # 入力層の値に入力層⇒隠れ層間のウェイトを乗算してから、
  # 足し合わせ、nonlinユーティリティ関数内のシグモイド関数にて
  # 0~1の値に変換したものを、隠れ層(l1変数)の値とします。
  l1 = nonlin(np.dot(l0,syn0))
  # 同様に、隠れ層の値に、隠れ層⇒出力層間のウェイトにを乗算してから、
  # 足し合わせ、nonlinユーティリティ関数内のシグモイド関数にて
  # 0~1の値に変換したものを、出力層(l2変数)の値とします。
  # 今時の高校生は行列は習わないようですが、ウェイトを乗算して
  # 足し合わせる部分はドット積(内積)メソッド"np.dot"を呼んでいます。
  l2 = nonlin(np.dot(l1,syn1))

  # how much did we miss the target value?
  # あるべき出力値(y変数)から出力層(l2変数)の値を引き算することにより、
  # 出力層誤差(l2_error変数)を算出しています。
  # 4件の学習データとあるべき出力値に対応して、
  # 出力層誤差の値が4つ算出されます。
  l2_error = y - l2
    
  #------------------------------------------------------
  # バック プロパゲーション (Back Propagation)
  #------------------------------------------------------
  # 表示用の出力層誤差を計算します。
  # コードでは 10000 レコード毎に出力しますが、アニメーションでは、
  # フォワード プロパゲーション終了時に毎回表示します。
  # 表示用の出力誤差は、4件の出力層誤差の絶対値の平均です。
  if (j% 10000) == 0:
    print "Error:" + str(np.mean(np.abs(l2_error)))
        
  # in what direction is the target value?
  # were we really sure? if so, don't change too much.
  # 出力層誤差(l2_error変数)と、出力層(l2変数)の値の微分値
  # (nonlinユーティリティ関数を deliv 引数を True で呼び出して取得)
  # を乗算して、出力層デルタ(l2_delta)を算出します。
  # 注:出力層の値が0または1に近づくほど、微分値は小さくなり、
  # その分、ウェイト更新に使われるデルタの値も小さくなります。
  l2_delta = l2_error*nonlin(l2,deriv=True)

  # how much did each l1 value contribute to the l2 error (according to the weights)?
  # 出力層デルタ(l2_delta)と、隠れ層⇒出力層間のウェイト(syn1)の
  # 内積を算出し、隠れ層誤差(l1_error)とします。
  l1_error = l2_delta.dot(syn1.T)
    
  # in what direction is the target l1?
  # were we really sure? if so, don't change too much.
  # 隠れ層誤差(syn1)と、隠れ層(l1変数)の値の微分値
  # (nonlinユーティリティ関数を deliv 引数を True で呼び出して取得)
  # を乗算して、隠れ層デルタ(l1_delta)を算出します。
  l1_delta = l1_error * nonlin(l1,deriv=True)

  # 隠れ層(l1変数)の値と出力層デルタ(l2_delta)の内積を算出し、
  # 隠れ層⇒出力層間のウェイト(syn1変数)に加えることで、
  # ウェイトを更新(学習)します。
  syn1 += l1.T.dot(l2_delta)
  # 入力層(l0変数)の値と隠れ層デルタ(l1_delta)の内積を算出し、
  # 入力層⇒隠れ層間のウェイト(syn0変数)に加えることで、
  # ウェイトを更新(学習)します。
  syn0 += l0.T.dot(l1_delta)

アニメーションの説明

レイアウトについて

動画の各部位の説明です。 f:id:hhok:20161107185741p:plain f:id:hhok:20161107185844p:plain f:id:hhok:20161107185851p:plain  
 
以下、処理順序に画面を説明します。


初期化


 [入力層⇒隠れ層の間のウェイト値]の初期化

まず、[入力層⇒隠れ層の間のウェイト値]を初期化します。 対応コード部位:

# ウェイト変数(syn0,syn1)を定義して、初期化しています。
# 初期化は平均が 0 になるように、ランダムな値を与えています。
# syn0 には、[入力層⇒隠れ層の間のウェイト値]を格納します。
syn0 = 2*np.random.random((3,4)) - 1

注:図ではウェイトの大小を線の直径の大小で表しています。 f:id:hhok:20161107191045p:plain

次の値が設定されます:
[[-0.16595599 0.44064899 -0.99977125 -0.39533485]
[-0.70648822 -0.81532281 -0.62747958 -0.30887855]
[-0.20646505 0.07763347 -0.16161097 0.370439 ]]

 [隠れ層⇒出力層の間のウェイト値]の初期化

次に、[隠れ層⇒出力層の間のウェイト値]を初期化します。

# syn1 には、[隠れ層⇒出力層の間のウェイト値]を格納します。
syn1 = 2*np.random.random((4,1)) - 1

f:id:hhok:20161107191147p:plain

次の値が設定されます:
[[-0.5910955 ]
[ 0.75623487]
[-0.94522481]
[ 0.34093502]]


学習の開始


学習を開始します。
ループ内のセフォワード プロパゲーションとバック プロパゲーションを繰り返すことにより、学習を進めます。

# 4件一組の学習データによる学習を1回のループとして、
# ループ毎に入力値から計算した出力値とあるべき値(y)との誤差
# を小さくするよう学習(ウェイトを調整)します。
# ループを回すごとに、ウェイトが調整された結果として、
# 誤差が徐々に小さくなっていき、学習が進むことになります。
# 各ループのことを Epoch と呼ぶこともあります。
# アニメーションではループ1~3回と1998~2000回目を描画し、
# 学習の進み具合を視覚化しています。
for j in xrange(60000):

最初のループを開始します。 f:id:hhok:20161107194604p:plain


フォワード プロパゲーション


4件の [学習レコード] を使用したフォワード プロパゲーションにより、それぞれ [出力値] を算出します。[出力値] は [あるべき出力値] と比較する形で、後で誤差の算出に使用されます。

 1番目の [学習レコード] の処理

  [入力層] の設定

1番目の [学習レコード] [0,0,1] を [入力層] に設定します。

  l0 = X

(コードでは行列演算のために4件の [学習レコード] をまとめて設定しています)
  
注:数値の大小を図形の大小で表しています。 f:id:hhok:20161107194938p:plain

  [隠れ層] の1番目ノードの設定

[入力層の値] と、[入力層⇒隠れ層間のウェイト] の内積を算出し、シグモイド関数で0~1の範囲に変換した数値を、[隠れ層] の1番目のノードの値に設定します。

  l1 = nonlin(np.dot(l0,syn0))

(コードでは行列演算により4件の [学習レコード] と [隠れ層] のすべてのノード値をまとめて計算しています)
f:id:hhok:20161107235853p:plain 検算:

> z = 0.0*-0.16595599 + 0.0*-0.70648822 + 1.0*-0.20646505
> 1/(1+np.exp(-z))
0.44856631662664037

  [隠れ層] の2番目ノードの設定

同様に、[隠れ層] の2番目ノードを設定します。 f:id:hhok:20161108010348p:plain

> z = 0.0*0.44064899 + 0.0*-0.81532281 + 1.0*0.07763347
> 1/(1+np.exp(-z))
0.51939862559049366

  [隠れ層] の3番目ノードの設定

同様に、[隠れ層] の3番目ノードを設定します。 f:id:hhok:20161108010949p:plain

> z = 0.0*-0.99977125 + 0.0*-0.62747958 + 1.0*-0.16161097
> 1/(1+np.exp(-z))
0.45968496535549458

  [隠れ層] の4番目ノードの設定

同様に、[隠れ層] の4番目ノードを設定します。 f:id:hhok:20161108011154p:plain

> z = 0.0*-0.39533485 + 0.0*-0.30887855 + 1.0*0.370439
> 1/(1+np.exp(-z))
0.5915650520492296

  [出力層] の設定

[隠れ層の値] と、[隠れ層⇒出力層間のウェイト] の内積を算出し、シグモイド関数で0~1の範囲に変換した数値を、[出力層の値] に設定します。

  l2 = nonlin(np.dot(l1,syn1))

(コードでは行列演算により4件の [学習レコード] 分をまとめて計算しています)

f:id:hhok:20161108011759p:plain

> z = 0.448566316626640*-0.5910955 + 0.519398625590494*0.75623487 + 0.459684965355495*-0.94522481 + 0.591565052049230*0.34093502 
> 1/(1+np.exp(-z))
0.47372957108332214

[あるべき出力値] である 0 に対して、[出力値] は 0.47372957108332214 なので、[出力誤差] は -0.47372957108332214 となります。

 2番目の [学習レコード] の処理

1番目の [学習レコード] と同様に、2番目の [学習レコード] を処理します。

  [入力層] の設定

2番目の [学習レコード] [0,1,1] を [入力層] に設定します。 f:id:hhok:20161108014130p:plain

  [隠れ層] の1番目ノードの設定

f:id:hhok:20161108014207p:plain

> z = 0.0*-0.16595599 + 1.0*-0.70648822 + 1.0*-0.20646505
> 1/(1+np.exp(-z))
0.28639588721102011

  [隠れ層] の2番目ノードの設定

f:id:hhok:20161108014233p:plain

> z = 0.0*0.44064899 + 1.0*-0.81532281 + 1.0*0.07763347
> 1/(1+np.exp(-z))
0.32350962799042776

  [隠れ層] の3番目ノードの設定

f:id:hhok:20161108014242p:plain

> z = 0.0*-0.99977125 + 1.0*-0.62747958 + 1.0*-0.16161097
> 1/(1+np.exp(-z))
0.3123639793175344

  [隠れ層] の4番目ノードの設定

f:id:hhok:20161108014255p:plain

> z = 0.0*-0.39533485 + 1.0*-0.30887855 + 1.0*0.370439
> 1/(1+np.exp(-z))
0.51538525402952473

  [出力層] の設定

f:id:hhok:20161108014304p:plain

> z = 0.286395887211020*-0.5910955 + 0.323509627990428*0.75623487 + 0.312363979317534*-0.94522481 + 0.515385254029525*0.34093502 
> 1/(1+np.exp(-z))
0.48895695615901025

[あるべき出力値] である 1 に対して、[出力値] は 0.48895695615901025 なので、[出力誤差] は 0.5110430438409898 となります。

 3番目の [学習レコード] の処理

同様に3番目の [学習レコード] [1,0,1] を処理すると以下の結果になります。 f:id:hhok:20161108021200p:plain

> z = 0.407956142741789*-0.5910955 + 0.626746060390834*0.75623487 + 0.238416219308552*-0.94522481 + 0.493776358949476*0.34093502 
> 1/(1+np.exp(-z))
0.54384085630684809

あるべき [出力値] である 1 に対して、[出力値] は 0.54384085630684809 なので、[出力誤差] は 0.4561591436931519 となります。

 4番目の [学習レコード] の処理

同様に4番目の [学習レコード] [1,1,1] を処理すると以下の結果になります。 f:id:hhok:20161108022544p:plain

> z = 0.253712484571769*-0.5910955 + 0.426281153143997*0.75623487 + 0.143212326822233*-0.94522481 + 0.417322537900416*0.34093502 
> 1/(1+np.exp(-z))
0.54470836902350805

[あるべき出力値] である 0 に対して、[出力値] は 0.544708369023508 なので、[出力誤差] は -0.544708369023508 となります。


バック プロパゲーション


4件の [学習レコード] を処理し、各 [出力層誤差] の計算が完了した後、ウェイトを更新するためにバック プロパゲーションを実施します。

 [表示用の出力層誤差] の計算

各 [学習レコード] 処理から計算された4件の [出力層誤差] の絶対値を取り、それらの平均値を [表示用の出力層誤差] として表示します。

np.mean(np.abs(l2_error))

f:id:hhok:20161108024122p:plain

> (abs(-0.473729571083322)+abs(0.511043043840990)+abs(0.456159143693152)+abs(-0.544708369023508)) / 4
0.496410031910243

 [出力層デルタ] の計算

各 [出力層誤差] の値に、[出力層の値] の微分値を乗算して、[出力層デルタ] を計算します。[出力層デルタ] は後で [隠れ層⇒出力層間のウェイト] に加算される形でウェイトの調整(学習)に使われます。
[出力層の値] が0または1に近く確信度が高い場合、[微分値] は小さく、逆に0または1より遠く確信度は低い場合、[微分値] は高くなります。つまり確信度が大きく [微分値] が小さいと、[微分値] を [出力層誤差] に乗算した [出力層デルタ] も小さくなり、結果としてウェイトの更新量が小さくなります。

  l2_delta = l2_error*nonlin(l2,deriv=True)

[出力層デルタ] として以下の配列が得られます:
[[-0.11810546]
[ 0.12769844]
[ 0.11316304]
[-0.13508831]]

検算:

> x = 0.473729571083322  # 1番目のレコードの出力値
> d = x*(1-x)            # 微分値を計算
> d
0.24930986456453375
> -0.473729571083322 * d # 出力誤差と微分値を乗算
-0.11810545520699767
> x = 0.488956956159010  # 2番目のレコードの出力値
> d = x*(1-x)            # 微分値を計算
> d
0.24987805118272596
> 0.511043043840990 * d  # 出力誤差と微分値を乗算
0.12769843986547497
> x = 0.543840856306848  # 3番目のレコードの出力値
> d = x*(1-x)            # 微分値を計算
> d
0.24807797931828232
> 0.456159143693152 * d  # 出力誤差と微分値を乗算
0.11316303861495514
> x = 0.544708369023508  # 4番目のレコードの出力値
> d = x*(1-x)            # 微分値を計算
> d
0.24800116173925782
> -0.544708369023508 * d # 出力誤差と微分値を乗算
-0.13508830832692637

 [隠れ層誤差] の計算

[出力層デルタ] と [隠れ層⇒出力層間のウェイト] の内積により、[隠れ層誤差] を計算します。4x1 配列の [出力層デルタ] と 1x4 配列の [隠れ層⇒出力層間のウェイト] の内積により 4x4 配列の [隠れ層誤差] が得られます。

  l1_error = l2_delta.dot(syn1.T)

[隠れ層誤差] として以下の配列が得られます:
[[ 0.0698116 -0.08931546 0.11163621 -0.04026629]
[-0.07548197 0.09657001 -0.12070373 0.04353687]
[-0.06689016 0.08557784 -0.10696451 0.03858124]
[ 0.07985009 -0.10215849 0.12768882 -0.04605634]]

検算:

> dlt = -0.11810545520699767    # 1番目の出力層デルタ
> s0 = np.array([-0.5910955,0.75623487,-0.94522481,0.34093502]) # 隠れ層⇒出力層間のウェイト
> print(dlt * s0)
[ 0.0698116  -0.08931546  0.11163621 -0.04026629]
> dlt =0.12769843986547497     # 2番目の出力層デルタ
> s0 = np.array([-0.5910955,0.75623487,-0.94522481,0.34093502]) # 隠れ層⇒出力層間のウェイト
> print(dlt * s0)
[-0.07548197  0.09657001 -0.12070373  0.04353687]
> dlt =0.11316303861495514     # 3番目の出力層デルタ
> s0 = np.array([-0.5910955,0.75623487,-0.94522481,0.34093502]) # 隠れ層⇒出力層間のウェイト
> print(dlt * s0)
[-0.06689016  0.08557784 -0.10696451  0.03858124]
> dlt =-0.13508830832692637    # 4番目の出力層デルタ
> s0 = np.array([-0.5910955,0.75623487,-0.94522481,0.34093502]) # 隠れ層⇒出力層間のウェイト
> print(dlt * s0)
[ 0.07985009 -0.10215849  0.12768882 -0.04605634]

 [隠れ層デルタ] の計算

各 [隠れ層誤差] の値に、[隠れ層の値] の微分値を乗算して、[隠れ層デルタ] を計算します。[隠れ層デルタ] は後で [入力層⇒隠れ層間のウェイト] に加算される形でウェイトの調整(学習)に使われます。

  l1_delta = l1_error * nonlin(l1,deriv=True)

[隠れ層デルタ] として以下の配列が得られます:
[[ 0.01726822 -0.02229526 0.02772761 -0.00972897]
[-0.0154265 0.02113446 -0.02592628 0.01087391]
[-0.01615584 0.02001969 -0.01942197 0.00964382]
[ 0.01511901 -0.02498445 0.01566774 -0.01119926]]

検算:

> # 隠れ層誤差
> l1err = np.array([[ 0.06981161, -0.08931547,  0.11163621, -0.04026629],
>        [-0.07548197,  0.09657001, -0.12070373,  0.04353687],
>        [-0.06689016,  0.08557784, -0.10696451,  0.03858124],
>        [ 0.07985009, -0.10215849,  0.12768882, -0.04605634]])
> # 隠れ層の値
> l1 = np.array([[ 0.44856632,  0.51939863,  0.45968497,  0.59156505],
>  [ 0.28639589,  0.32350963,  0.31236398,  0.51538526],
>  [ 0.40795614,  0.62674606,  0.23841622,  0.49377636],
>  [ 0.25371248,  0.42628115,  0.14321233,  0.41732254]])
> # 隠れ層の値の微分値を計算
> deliv = l1 * (1 - l1) 
> print(deliv)
 0.24735458  0.24962369  0.2483747   0.24161584]
 [ 0.20437328  0.21885115  0.21479272  0.24976329]
 [ 0.24152793  0.23393544  0.18157393  0.24996127]
 [ 0.18934246  0.24456553  0.12270256  0.24316444]]
> # 隠れ層誤差と隠れ層の値の微分値を乗算して隠れ層デルタを算出
> print(l1err * deliv)
[[ 0.01726822 -0.02229526  0.02772761 -0.00972897]
 [-0.0154265   0.02113446 -0.02592628  0.01087391]
 [-0.01615584  0.02001969 -0.01942197  0.00964382]
 [ 0.01511901 -0.02498445  0.01566774 -0.01119926]]

 [隠れ層⇒出力層間のウェイト] の更新

[隠れ層の値] と [出力層デルタ] の内積を算出し、 [隠れ層⇒出力層間のウェイト] に加えることで、 ウェイトを更新(学習)します。

  syn1 += l1.T.dot(l2_delta)

f:id:hhok:20161108161920p:plain

次の値が [隠れ層⇒出力層間のウェイト] に設定されます:
[[-0.59560936]
[ 0.74954163]
[-0.95199413]
[ 0.33638369]]

検算:

> # 隠れ層の値
> l1 = np.array([[ 0.44856632,  0.51939863,  0.45968497,  0.59156505],
>  [ 0.28639589,  0.32350963,  0.31236398,  0.51538526],
>  [ 0.40795614,  0.62674606,  0.23841622,  0.49377636],
>  [ 0.25371248,  0.42628115,  0.14321233,  0.41732254]])
> # 出力層デルタ
> l2delta = np.array([[-0.11810546],
>  [ 0.12769844],
>  [ 0.11316304],
>  [-0.13508831]])
> # 更新値の計算
> upd = l1.T.dot(l2delta)
> print(upd)
[[-0.00451386]
 [-0.00669325]
 [-0.00676932]
 [-0.00455133]]
> # 隠れ層⇒出力層間のウェイト
> syn1 = np.array([[-0.5910955 ],
>  [ 0.75623487],
>  [-0.94522481],
>  [ 0.34093502]])
> # 更新値を加算してウェイトを更新
> syn1 = syn1 + upd
> print(syn1)
[[-0.59560936]
 [ 0.74954162]
 [-0.95199413]
 [ 0.33638369]]

 [入力層⇒隠れ層間のウェイト] の更新

[入力層の値] と [隠れ層デルタ] の内積を算出し、 [入力層⇒隠れ層間のウェイト] に加えることで、 ウェイトを更新(学習)します。

  syn0 += l0.T.dot(l1_delta)

f:id:hhok:20161108162005p:plain

次の値が [入力層⇒隠れ層間のウェイト] 設定されます:
[[-0.16699282 0.43568423 -1.00352547 -0.3968903 ]
[-0.7067957 -0.8191728 -0.63773812 -0.3092039 ]
[-0.20566016 0.07150791 -0.16356387 0.37002849]]

検算:

> # 入力層の値
> l0 = np.array([[0, 0, 1],
>  [0, 1, 1],
>  [1, 0, 1],
>  [1, 1, 1]])
> # 隠れ層デルタ
> l1delta = np.array([[ 0.01726822, -0.02229526,  0.02772761, -0.00972897],
>  [-0.0154265,   0.02113446, -0.02592628,  0.01087391],
>  [-0.01615584,  0.02001969, -0.01942197,  0.00964382],
>  [ 0.01511901, -0.02498445,  0.01566774, -0.01119926]])
> # 更新値の計算
> upd = l0.T.dot(l1delta)
> print(upd)
[[-0.00103683 -0.00496476 -0.00375423 -0.00155544]
 [-0.00030749 -0.00384999 -0.01025854 -0.00032535]
 [ 0.00080489 -0.00612556 -0.0019529  -0.0004105 ]]
> # 入力層⇒隠れ層間のウェイト
> syn0 = np.array([[-0.16595599,  0.44064899, -0.99977125, -0.39533485],
>  [-0.70648822, -0.81532281, -0.62747958, -0.30887855],
>  [-0.20646505,  0.07763347, -0.16161097,  0.370439  ]])
> # 更新値を加算してウェイトを更新
> syn0 = syn0 + upd
> print(syn0)
[[-0.16699282  0.43568423 -1.00352548 -0.39689029]
 [-0.70679571 -0.8191728  -0.63773812 -0.3092039 ]
 [-0.20566016  0.07150791 -0.16356387  0.3700285 ]]

学習の経過


上記のフォワード プロパゲーションとバック プロパゲーションの繰り返しループにより学習を進めます。以下に各ループ終了時の状態を示します。

2回目ループの終了時

2回目ループ終了時には、下図となります。[表示用の出力層誤差] が1回目の 0.49641 から 0.49630 に減っており、ウェイトも更新されています。

f:id:hhok:20161108173742p:plain

1998回目ループの終了時

1998回目ループ終了時までジャンプした結果です。[表示用の出力層誤差] が2回目の 0.49630 から 0.02415 と大幅に減っています。また、ウェイトも大幅に更新されており、ウェイトの大小を表す線太さに反映されています。[出力値] と [あるべき出力値] を表す左端の数字と四角形の大きさが非常に近づいており、学習が進んだことが分かります。

f:id:hhok:20161108173805p:plain

1999回目ループの終了時

1999回目ループ終了時には、[表示用の出力層誤差] が1998回目の 0.02415 から 0.02414 とごく僅かながら減少しています。また、ウェイトの変化で見えるものは赤枠でかこった一部となっています。

f:id:hhok:20161108181538p:plain

2000回目ループの終了時

2000回目ループ終了時でも、[表示用の出力層誤差] が1999回目の 0.02414 から 0.02413 と僅かに減少しています。

f:id:hhok:20161108181557p:plain

おわりに

試行錯誤しながら作ってみました。至らぬ点がございましたらご容赦ください。
 
my TODO:

  1. バックプロパゲーションの部分をより詳細に可視化する方法を考えたい。
  2. 学習が進むと、ウェイト更新が小さくなる状態を記載したい。
  3. GPU 使用でもレンダリングが激遅かった。早いきれいな方法を探したい。
  4. 3DCG 生成につかった python コードを公開できるくらい整理したい。