はじめに
「プログラミング」学習初手『ゼロから作るDeep Learning』民のための実装攻略ノートです。『ゼロつく1』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
関数やクラスとして実装される処理の塊を細かく分解して、1つずつ処理を確認しながらゆっくりと組んでいきます。
この記事は、3.4節「ニューラルネットワークの実装」の内容になります。3層のニューラルネットワークをPythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
3.3 多次元配列の計算
要は線形代数という分野の基礎のキです。
1章のNumPy配列のところで2次元配列には触れましたが、多次元配列はそれを更に拡張(一般化)したものです。
これまで扱ってきた、要素を横に並べたものを1次元配列と呼びます(ベクトルとも呼びます)。要素を縦にも横にも並べたものを2次元配列と呼ぶのでした(行列とも呼びます)。だとすると次はこれを奥にも並べたくなりますね。縦横奥に要素を並べたものを3次元配列と呼びます。
更にこれをN次元にまで拡げたものを(3次元配列も含めて)多次元配列と言います(本では登場しません)。
多次元配列(行列)の計算については(複雑な概念なのでしっかり理解したければ)【行列の積【ゼロつく1のノート(数学)】 - からっぽのしょこ】の方で、数学上の定義とNumPyでの扱い方について確認していきましょう。まずは本を進めたいのであれば、最低限3.3.1-3項の内容を把握しておきましょう。
3.3.3 ニューラルネットワークの行列の積
入力データを$\boldsymbol{\mathrm{X}}$、重みを$\boldsymbol{\mathrm{W}}$、出力を$\boldsymbol{\mathrm{Y}}$とします。それぞれの詳細は次の通りです。
ではこれをNumPy配列の変数として作成し、np.dot()
で行列の積を計算し、出力$\boldsymbol{\mathrm{Y}}$を求めます。
# NumPyを読み込む import numpy as np # 入力データ X = np.array([1, 2]) # 重み W = np.array([[1, 3, 5], [2, 4, 6]]) # 出力を計算 Y = np.dot(X, W) print(Y)
[ 5 11 17]
出力$\boldsymbol{\mathrm{Y}}$が得られました。
要素数2の1次元配列(1×2の行列)と2×3の行列の積なので、要素数3の1次元配列(1×3の行列)となりました。
3.4 ニューラルネットワークの実装
これまでにニューラルネットワークのパーツとなる個々の処理を学びました。この節では、実践的な(3層からなる)ニューラルネットワークを実装します。
3.4.2 各層における信号伝達の実装
入力信号(データ)を$\boldsymbol{\mathrm{X}}$、第1層の重みを$\boldsymbol{\mathrm{W}}^{(1)}$、第1層のバイアスを$\boldsymbol{\mathrm{B}}^{(1)}$とします。
(突然右肩に(1)が出てくると身構えてしまうかもしれませんが、単に1層か2層か区別するためのもので数学的な意味はないです。次の変数名W1, b1
の1と同じと思えば大したことないと分かるでしょう。)逆に右下の添字は通常の表現ではありません(あまり記憶に留めない方がいいと思います)。なのでこのノートでは使いません。
それぞれ対応する変数をNumPy配列として作成します。
# NumPyを読み込む import numpy as np # 入力信号 X = np.array([1.0, 0.5]) # 重み W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]) # バイアス B1 = np.array([0.1, 0.2, 0.3])
またこの第1層の重み付き信号とバイアスの総和を$\boldsymbol{\mathrm{A}}^{(1)}$とします。$\boldsymbol{\mathrm{A}}^{(1)}$は、$\boldsymbol{\mathrm{X}}$と$\boldsymbol{\mathrm{W}}^{(1)}$行列の積に$\boldsymbol{\mathrm{B}}^{(1)}$を加えたものとなります。
$\boldsymbol{\mathrm{A}}^{(1)}$の1つ目の要素は式(3.8)になります。次元が増えた(データが増えた)ことで行列の計算となり複雑になりましたが、やっていることは式(3.4)と同じことです。
行列の積はnp.dot()
を使って計算できます。
# 重み付き信号とバイアスの和を計算:式(3.9) A1 = np.dot(X, W1) + B1 print(A1)
[0.3 0.7 1.1]
と計算できました。
この$\boldsymbol{\mathrm{A}}^{(1)}$の各要素を活性化関数$h(\cdot)$で変換した信号を$\boldsymbol{\mathrm{Z}}^{(1)}$とします。
ここでは、活性化関数としてシグモイド関数(3.2.4項)を用います。(「NameError: name 'sigmoid' is not defined」というエラーが出るなら、sigmoid()
の定義をもう一度実行する必要があります。)
# シグモイド関数による活性化 Z1 = sigmoid(A1) print(Z1)
[0.57444252 0.66818777 0.75026011]
と計算できました。
ここまでが第1層の内容になります。続いて、$\boldsymbol{\mathrm{Z}}^{(1)}$を第2層の入力として、第2層の出力を求めていきます。
第2層の重みを$\boldsymbol{\mathrm{W}}^{(2)}$、第2層のバイアスを$\boldsymbol{\mathrm{B}}^{(2)}$とします。
第2層の入力信号$\boldsymbol{\mathrm{Z}}^{(1)}$の形に応じて、それぞれ第1層と形が異なります。
# 第2層の重み W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]) # 第2層のバイアス B2 = np.array([0.1, 0.2])
第1層のときと同様に重み付き信号とバイアスの和を計算し、シグモイド関数による活性化を行い、第2層の出力$\boldsymbol{\mathrm{Z}}^{(2)}$とします。
# 重み付き信号とバイアスの和を計算:式(3.9) A2 = np.dot(Z1, W2) + B2 print(A2) # シグモイド関数による活性化 Z2 = sigmoid(A2) print(Z2)
[0.51615984 1.21402696]
[0.62624937 0.7710107 ]
となりました。これを第3層の入力とします。
これまでと同様に、第3層の重みを$\boldsymbol{\mathrm{W}}^{(3)}$、第3層のバイアスを$\boldsymbol{\mathrm{B}}^{(3)}$とします。
# 第3層の重み W3 = np.array([[0.1, 0.3], [0.2, 0.4]]) # 第3層のバイアス B3 = np.array([0.1, 0.2]) # 重み付き信号とバイアスの和を計算:式(3.9) A3 = np.dot(Z2, W3) + B3 print(A3)
[0.31682708 0.69627909]
第3層の重み付き信号とバイアスの和は
となりました。これを第3層の活性化関数に渡します。
ただし第3層の活性化関数は、入力をそのまま出力する恒等関数とします。
この関数をidentity_function()
として定義しておきます。
# 恒等関数の実装 def identity_function(x): return x
関数を作成できたら、活性化を行いましょう。(不要に思えるかもしれませんが、流れを統一しておくと後々解釈がしやすくなったり、他の活性化を使いたくなったときに(処理を加えるよりも)対応しやすくなります。)
# 恒等関数による活性化 Y = identity_function(A3) print(Y)
[0.31682708 0.69627909]
以上で最終的な出力
が得られました(めでたしめでたし)。
しかし、同じことをまた書きたくないですよね?次項でここまでの処理を関数としてまとめておきます。
3.4.3 実装のまとめ
それでは"実践的な"ニューラルネットワークを実装します。
この実装では、ニューラルネットワークで使用するパラメータをディクショナリ型として格納しておきます。ディクショナリ型としておくことで、パラメータ類を1つの変数にまとめて管理できつつ、パラメータ名(キー)を指定するとその値を取り出すことができます。
実装の前に、ディクショナリ型の扱いを復習しておきましょうか。
# 空のディクショナリを作成 dic_data = {} print(dic_data)
{}
まずは空のディクショナリを作成します。これは既にデータを持つディクショナリdic_data
があった場合に、それを空のディクショナリで上書き(初期化)すると言えます。
ここに必要なキーと値を格納していきます。
# 新たなキーに値を代入 dic_data['疲れた'] = "赤い雄牛" print(dic_data)
{'疲れた': '赤い雄牛'}
ディクショナリにキーを指定することで、いつでも対応するデータを取り出すことができます。
# キーを指定して値を取り出す living_dead = dic_data['疲れた'] print(living_dead)
赤い雄牛
翼を授けるうぅ。
あと一息!ではありませんが、この項でこの節は終わりです。そこまでで一区切りなので是非やってしまいましょう。
(ニューラルネットワークで用いる全ての)パラメータをまとめたディクショナリ型の変数を複製する関数を作成します。各パラメータの値は3.4.2項と同じものです。
# パラメータを複製する関数を定義 def init_network(): # ディクショナリを初期化(空のディクショナリを作成) network = {} # 第1層のパラメータ network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]) network['b1'] = np.array([0.1, 0.2, 0.3]) # 第2のパラメータ network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]) network['b2'] = np.array([0.1, 0.2]) # 第3のパラメータ network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]]) network['b3'] = np.array([0.1, 0.2]) return network
早速読み込んでみましょう。折角なので、変数はさっき作ったdic_data
を使いましょうか。
# パラメータ(重みとバイアス)を取得 dic_data = init_network() print(dic_data)
{'W1': array([[0.1, 0.3, 0.5],
[0.2, 0.4, 0.6]]), 'b1': array([0.1, 0.2, 0.3]), 'W2': array([[0.1, 0.4],
[0.2, 0.5],
[0.3, 0.6]]), 'b2': array([0.1, 0.2]), 'W3': array([[0.1, 0.3],
[0.2, 0.4]]), 'b3': array([0.1, 0.2])}
(何か表示が崩れてますが)必要な重みとバイアスの値が入っていますね。そして元々入っていたデータはなくなっています。
続いて、このパラメータを受け取ってニューラルネットワークの処理を行う関数を定義します。関数として定義する(この関数によって実行される)処理の流れは3.4.2項と同様です。最初にinit_network()
で作成された変数から各パラメータの値を取り出す処理を加えます。
# ニューラルネットワーク(順伝播)を定義 def forward(network, x): # ディクショナリから各パラメータを取り出す W1, W2, W3 = network['W1'], network['W2'], network['W3'] # 重み b1, b2, b3 = network['b1'], network['b2'], network['b3'] # バイアス # 第1層の処理 a1 = np.dot(x, W1) + b1 # 重み付き和の計算 z1 = sigmoid(a1) # 活性化 # 第2層の処理 a2 = np.dot(z1, W2) + b2 # 重み付き和の計算 z2 = sigmoid(a2) # 活性化 # 第3層の処理 a3 = np.dot(z2, W3) + b3 # 重み付き和の計算 y = identity_function(a3) # 活性化 return y
では使ってみましょう。
# 入力信号 X = np.array([1.0, 0.5]) # 出力 Y = forward(dic_data, X) print(Y)
[0.31682708 0.69627909]
さて、先ほどの出力と同じですか?(ちなみにこの結果は2度目で完成しました。)
パラメータの設定とニューラルネットワークの処理を関数として実装しておくことで管理がしやすくなります。例えば、第2層の重みの値を変更したくなったときに、どこを変更すればよいのか一目で分かりますね。
以上で3.4節の内容は終わりです。この節では、行列の計算と(回帰問題に用いられるタイプの)ニューラルネットワークの実装を行いました。次は、この本でメインとなる分類問題に用いられるタイプをやっていきます。
参考文献
- 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.
おわりに
それっぽい内容になってきましたね。
【次節の内容】