3層ニューラルネットワーク実装

 書籍「ゼロから作るDeepLearning」で紹介されている3層ニューラルネットワークをRubyで実装してみました。

www.oreilly.co.jp

サンプルコード全体

 まずは実装したコード全体を掲載します。

# 3層ニューラルネットワーク実装
# 各層のニューロン数は下記の通り
#  第0層(入力層):2
#  第1層(隠れ層):3
#  第2層(隠れ層):2
#  第3層(出力層):2

require 'numo/narray'

# 活性化関数としてシグモイド関数を使う
def sigmoid(x)
  1 / (1 + Numo::DFloat::Math.exp(-x))
end

# 出力層の活性化関数として恒等関数を使う
def identity_function(x)
  x
end

# 重みとバイアスの初期化
def init_network
  network = {}
  network['w1'] = Numo::DFloat[[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]
  network['b1'] = Numo::DFloat[0.1, 0.2, 0.3]
  network['w2'] = Numo::DFloat[[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]
  network['b2'] = Numo::DFloat[0.1, 0.2]
  network['w3'] = Numo::DFloat[[0.1, 0.3], [0.2, 0.4]]
  network['b3'] = Numo::DFloat[0.1, 0.2]
  network
end

# 入力から出力までの処理
def forward(network, x)
  w1 = network['w1']
  w2 = network['w2']
  w3 = network['w3']
  b1 = network['b1']
  b2 = network['b2']
  b3 = network['b3']

  a1 = x.dot(w1) + b1
  z1 = sigmoid(a1)
  a2 = z1.dot(w2) + b2
  z2 = sigmoid(a2)
  a3 = z2.dot(w3) + b3
  identity_function(a3)
end

network = init_network     # 重みとバイアスのデータを用意
x = Numo::DFloat[1.0, 0.5] # 入力層として2つのニューロンを用意
y = forward(network, x)    # 入力層、重み、バイアスのデータを渡して出力層のデータを取得
puts y.inspect             # 出力層のデータを表示

重みとバイアスのデータの用意

 まずは各層の重みとバイアスのデータを用意します。今回のニューロン数の構成としては下記のようになります。

  • 第0層(入力層):2
  • 第1層(隠れ層1):3
  • 第2層(隠れ層2):2
  • 第3層(出力層):2

 init_newtorkメソッドで上記構成の第1層〜第3層の各層への処理に使うデータを作成しています。w1, w2, w3 はそれぞれ第1層、第2層、第3層への重みの値で、b1, b2, b3 はバイアスの値になります。Numo::DFloatの配列として用意します。

def init_network
  network = {}
  network['w1'] = Numo::DFloat[[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]
  network['b1'] = Numo::DFloat[0.1, 0.2, 0.3]
  network['w2'] = Numo::DFloat[[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]
  network['b2'] = Numo::DFloat[0.1, 0.2]
  network['w3'] = Numo::DFloat[[0.1, 0.3], [0.2, 0.4]]
  network['b3'] = Numo::DFloat[0.1, 0.2]
  network
end

入力層のデータ

 入力層(第0層)のニューロン数は2なので、Numo::DFloatの配列を下記のように用意します。

x = Numo::DFloat[1.0, 0.5]

活性化関数

 単純パーセプトロンでは活性化関数としてステップ関数(ある閾値を界に出力が切り替わる関数)が使われますが、ニューラルネットワーク(多層パーセプトロン)では活性化関数としては入力に対して連続的に出力が変化する(グラフにすると滑らかな曲線になる)関数が使われるということで、今回はシグモイド関数を用いています。

def sigmoid(x)
  1 / (1 + Numo::DFloat::Math.exp(-x))
end

 Ruby標準のMathモジュールのexpメソッドは引数に配列を渡すことができませんが、Numo::DFloatのMathモジュールのexpメソッドは配列に対応しているので、そちらを使用しています。

Module: Numo::DFloat::Math — Documentation by YARD 0.9.8

恒等関数

 出力層の活性化関数は解く問題の性質によって選択するようですが、今回はひとまずのサンプル実装ということで恒等関数を使用しています。入力値をそのまま返しています。

def identity_function(x)
  x
end

各層の伝達処理

 各層の伝達処理では、前の層のニューロンからの入力値に重みを掛け合わせ、バイアスを加算したものをシグモイド関数で処理します。例えば第0層(入力層)から第1層(隠れ層1)への伝達処理は下記のようになっています。

  a1 = x.dot(w1) + b1
  z1 = sigmoid(a1)

 第0層からの入力値 x と、第1層へ伝達する際の重みの値 w1 はいずれもNumo::DFloatの配列として作成されているので、dotメソッドで内積を計算し、第1層へ伝達する際のバイアス値 b1 を加算しています。そしてそれをシグモイド関数で処理し、第1層の値としてz1を得ています。これを後続の層でも繰り返しますが、出力層の場合だけシグモイド関数の代わりに恒等関数を用います。

Class: Numo::NArray — Documentation by YARD 0.9.8

  a2 = z1.dot(w2) + b2
  z2 = sigmoid(a2)
  a3 = z2.dot(w3) + b3
  identity_function(a3)

スクリプト実行結果

 上記のスクリプトを実行すると下記のような出力になります。出力層のニューロンの値として、2つの要素を持つNumo::DFloatの配列が出力されます。

$ ruby three_layer_newral_network.rb 
Numo::DFloat#shape=[2]
[0.316827, 0.696279]

 コードは下記にも公開しました。

github.com