初めに
本記事ではニューラルネットワークについて、以下の5つの項目をまとめています。
- 入力層~中間層
- 活性化関数
- 出力層
- 勾配降下法
- 誤差逆伝搬法
ニューラルネットワークについて
ニューラルネットワークは、人間の脳内にある神経細胞(ニューロン)とそのつながり、いわゆる神経回路網を人工ニューロンという数式的なモデルで表現したものです。最近では、人工知能(AI)領域がブームになっていますが、ニューラルネットワークは機械学習や深層学習(ディープラーニング)などを学ぶ際に知っておくべき基本的な仕組みです。ニューラルネットワークは、入力層、出力層、隠れ層から構成され、層と層の間には、ニューロン同士のつながりの強さを示す重み「w」があります。
重み「w」とは?
-> 人間の脳の中にあるニューロンは電気信号として情報伝達を行います。その際にシナプスの結合強度によって、情報の伝わりやすさが変わってきます。この結合強度を、人工ニューロンでは重みwで表現しています。
ひとつひとつの人工ニューロンは単純な仕組みですが、それを多数組み合わせる事で複雑な関数近似を行う事ができるためノーフリーランチにならないように気を付ける必要があります。
以下の記事も合わせて確認。
万能近似定理は活性化関数を持つようなモデルを使用することでどんな関数でも近似できるのではないか?という定理です。
#入力層〜中間層
入力層とは
人工ニューロンが最初に情報を受け取るのが入力層です。生物のニューロンは電気信号によって情報を受け取るのに対して、人工ニューロンは数値で受け取ります。例えば、手書きで「0」と書かれた画像を人工ニューロンに認識させる場合、入力層では画像の1ピクセルを入力値として受け取ります。入力層で受け取った情報は、ニューロン同士の結合の強度に応じて、優先順位が決定される仕組みになっています。
中間層とは
入力層から情報を受け継ぎ、さまざまな計算を行うのが中間層です。中間層(隠れ層)が多いほど複雑な分析ができ、中間層が3層以上あるニューラルネットワークをディープラーニングと呼びます。中間層は、入力層が取り込んだ複雑なデータを選別し、学習によって扱いやすい状態に変換します。その後、単純なデータしか扱えない出力層へ結果を渡します。ニューロンの数や中間層が増えるほど分析の柔軟性や結果の表現力は向上する反面、データやメモリ、演算の量は増加します。
##確認テスト
###確認テスト1
ディープラーニングは、結局何をやろうとしているか2行以内で述べよ。
-> 明示的なプログラムの代わりに多数の中間層を持つニューラルネットワークを用いて、入力値から目的とする出力値に変換する数学モデルを構築すること。
また、次の中のどの値の最適化が最終目的か。
-> 重み[w], バイアス[b]
###確認テスト2
次のネットワークを紙にかけ。
入力層:2ノード1層
中間層:3ノード2層
出力層:1ノード1層
###確認テスト3
入力層~中間層の図式に動物分類の実例を入れる。
###確認テスト4
次の数式をPythonで書け。
\begin{aligned}
u &=w_{1} x_{1}+w_{2} x_{2}+w_{3} x_{3}+w_{4} x_{4}+b \\
&=W x+b
\end{aligned}
import numpy as np
u = np.dot(x, W) + b
###確認テスト5
「1_1_forward_propagation.ipynb」から中間層の出力を定義しているソースを抜き出せ。
z2 = functions.relu(u2)
#活性化関数
活性化関数(Activation function)とは
あるニューロンから次のニューロンへと出力する際に、あらゆる入力値を別の数値に変換して出力する関数です。複数のニューロンから、あるニューロンへの入力は、全結合などの線形変換(線形写像)処理によって1つの数値にまとめられます。活性化関数は、その数値を次のニューロンに「どのように出力するか」が定義されたものです。この活性化の変換は、非線形変換(Non-Linear transformation、非線形写像)である必要があります。この時、出力層の活性化関数は次のニューロンに伝播するわけではないので、出力層の活性化関数には「恒等関数」と呼ばれる線形変換が用いられることもあります。
出力層においてよく使われる主な活性化関数
- ステップ関数
- 分類問題(二値)の場合は「シグモイド関数」
- 分類問題(多クラス)の場合は「ソフトマックス(Softmax)関数」
- ReLU関数
- 回帰問題の場合は「(活性化関数なし)」もしくは(前述した)「恒等関数」
ステップ関数
閾値を超えたら発火する関数で、出力は常に1か0になります。 パーセプトロンで利用された関数で、特徴は、0-1間の間を表現できず、線形分離可能なものしか学習ができません。
f(x)= \begin{cases}1 & (x \geq 0) \\ 0 & (x<0)\end{cases}
import numpy as np
import matplotlib.pyplot as plt
def step(x):
return np.where( x > 0, 1, 0)
x = np.array([range(-2000, 2000)])
x = x/100
y = step(x)
plt.scatter(x, y, marker='.')
plt.show()
シグモイド関数(Sigmoid function)
入力値を0.0~1.0の範囲の数値に変換して出力する関数です。
ニューラルネットワークの基礎モデル「パーセプトロン」では、「ステップ関数」という活性化関数が用いられていました。しかし、「バックプロパゲーション」が登場してからは「シグモイド関数」が活性化関数として使われるようになりました。さらに、最近のディープニューラルネットワークでは「ReLU」がよく使われるようになっています。バックプロパゲーションの処理過程では、「損失」を微分することになります。その際の、損失の計算項目の一つである「予測値」は、活性化関数を通して計算されます。そのため、微分できないステップ関数の代わりに、微分可能な「シグモイド関数」が活性化関数として採用されたのです(損失関数には交差エントロピー)。
シグモイド関数
$$
f(x)=\frac{1}{1+e^{-x}}
$$
def sigmoid(x):
return 1.0 / (1.0 + np.exp(-x))
シグモイド関数の導関数
$$
f^{\prime}(x)=f(x)(1-f(x))
$$
def der_sigmoid(x):
return sigmoid(x) * (1.0 - sigmoid(x))
シグモイド関数の実装
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
x = np.array([range(-1000, 1000)])
x = x/100
y = sigmoid(x)
plt.scatter(x, y, marker='.')
plt.show()
ソフトマックス関数(Softmax function)
複数の出力値の合計が1.0(=100%)になるように変換して出力する関数です。各出力値の範囲は0.0~1.0となります。ソフトマックス関数は、主に分類問題における出力層の活性化関数として用いられます(損失関数にはシグモイド関数同様(基本的に)交差エントロピー)。
ソフトマックス関数
$$
y_{i}=\frac{e^{x_{i}}}{\sum_{k=1}^{n} e^{x_{k}}} \quad(i=1,2, \cdots, n)
$$
def softmax(x):
if (x.ndim == 1):
x = x[None,:] # ベクトル形状なら行列形状に変換
return np.exp(x) / np.sum(np.exp(x), axis=1, keepdims=True)
# 入力(x)と出力(y)の例
x = np.array([[1,0,0], [0,1,0], [0,0,1]])
y = softmax(x)
print(y)
ソフトマックス関数の導関数
$$
\frac{\partial y_{i}}{\partial x_{j}}= \begin{cases}y_{i}\left(1-y_{j}\right), & i=j \ -y_{j} y_{i}, & i \neq j\end{cases}
$$
def der_softmax(x):
y = softmax(x) # ソフトマックス関数の出力
jcb = - y[:,:,None] * y[:,None,:] # ヤコビ行列を計算(i≠jの場合)
iy, ix = np.diag_indices_from(jcb[0]) # 対角要素のインデックスを取得
jcb[:,iy,ix] = y * (1.0 - y) # 対角要素値を修正(i=jの場合)
return jcb # 微分係数の行列(ヤコビ行列)を出力
der_y = der_softmax(x)
print(der_y)
ソフトマックス関数の実装
def softmax_func(x): # ソフトマックス関数の定義
exp_x = np.exp(x)
sum_exp_x = np.sum(exp_x)
y = exp_x / sum_exp_x
return y
x = np.arange(-5.0, 5.0, 0.1) # 区間を-5~5 まで、描画制度を 0.1 刻みに設定
y = softmax_func(x) # ソフトマックス関数をコール
plt.xlabel("x") # x軸のラベルを設定
plt.ylabel("y") # y軸のラベルを設定
plt.plot(x, y)
ReLU関数
関数への入力値が0以下の場合には出力値が常に0、入力値が0より上の場合には出力値が入力値と同じ値となる関数です。ディープニューラルネットワークでは、層が深くなるにつれ勾配が消えてしまう勾配消失問題が発生しました。勾配が消えていく理由は、シグモイド関数の微分係数の最大値が0.25(範囲:0.0~0.25)であり、そのシグモイド関数を重ねれば重ねるほど勾配の値は小さくなっていくからです。よって、微分係数の最大値が1.0(範囲:0.0か1.0)である「ReLU」が使われるようになりました。
ReLU関数
f(x)= \begin{cases}0, & x \leq 0 \\ x, & x>0\end{cases}
ReLU関数の導関数
f^{\prime}(x)= \begin{cases}0, & x \leq 0 \\ 1, & x>0\end{cases}
import numpy as np
import matplotlib.pyplot as plt
def relu(x):
return np.maximum(0, x)
x = np.array([range(-1000, 1000)])
x = x/100
y = relu(x)
plt.scatter(x, y, marker='.')
plt.show()
恒等関数(Identity function)
あらゆる入力値を、全く同じ数値に変換して出力する関数です。線形関数と言っても、活性化関数の場合は基本的に入力と出力が「同じ値」となる直線を指します。
恒等関数
$$
f(x)=x
$$
def identity(x):
return x
恒等関数の実装
x = np.arange(-5.0, 5.0, 0.1) # 区間を-5~5 まで、描画制度を 0.1 刻みに設定
y = koutou_func(x) # 恒等関数をコール
plt.xlabel("x") # x軸のラベルを設定
plt.ylabel("y") # y軸のラベルを設定
plt.plot(x, y)
##確認テスト
###確認テスト1
線形と非線形の違いを簡潔に説明せよ。
線形の特徴
加法性
$$
f\left(x_{1}+x_{2}\right)=f\left(x_{1}\right)+f\left(x_{2}\right)
$$
斉次性
$$
f(a x)=a f(x)
$$
の二つの性質をもつ写像を線形写像 (線形関数) といいます。
以下のように書くこともできます。
$$
f\left(a x_{1}+b x_{2}\right)=a f\left(x_{1}\right)+b f\left(x_{2}\right)
$$
一変数関数で線形性を持つ関数(aは定数)。
$$
f(x)=a x
$$
二変数関数における線形関数
$$
f(x, y)=a x+b y
$$
非線形の特徴
非線形な関数は加法性、斉次性を満たさない。
以下のような一変数関数は全て非線形
$$
x^{2}, \frac{1}{x}, \sin x, \log x
$$
先ほどの二変数関数(線形)における線形関数が以下であれば非線形となる。
$$
x^{2} 、 y^{2} 、 x y
$$
###確認テスト2
「1_1_forward_propagation.ipynb」から活性化関数を使っている箇所を抜き出せ。
# 1層の総出力
z1 = functions.relu(u1)
# 2層の総出力
z2 = functions.relu(u2)
#出力層
出力層とは
入力層と中間層で重みをかけ、活性化関数で処理された値が示されるのが出力層です。たとえば、手書きで書かれた「0」の画像について、何が書かれているのか判断した結果が出力されます。
なお、出力層で得られた結果を教師データと照合し、出力層から入力層に向けて誤差の修正や調整を行う方法を「誤差逆伝播法」とよびます。これにより、多くの中間層をもつ複雑なニューラルネットワークでも、より適切な学習を行うことが可能です。
- 2乗誤差
$$
E_{n}(w)=\frac{1}{2} \sum_{i=1}^{I}\left(y_{i}-d_{i}\right)^{2}
$$
def mean_squared_error(y, d):
return np.mean(np.square(y-d)) / 2
- クロスエントロピー
$$
E_{n}(w)=-\sum_{i=1}^{I} d_{i} \log y_{i}
$$
def cross_entropy_error(d, y):
if y.ndim == 1:
d = d.reshape(1, d.size)
y = y.reshape(1, y.size)
if d.size == y.size:
d = d.argmax(axis=1)
batch_size = y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), d] + 1e-7) / batch_size
##確認テスト
###確認テスト1
今回用いた誤差関数に平均2乗誤差を用いたが、なぜ予測と目的変数を引き算だけでなく2乗するのか?
-> 引き算のみでは、各ラベルごとの誤差で正負両方の値が発生し、全体の誤差を正しく表すのに都合が悪い。そのため2乗して各ラベルでの誤差を正の値になるようにする。
平均2乗誤差はなぜ1/2しているのか?
-> 実際にネットワークを学習するときに行う誤差逆伝搬の計算では誤差関数の微分を用いる。その際の計算式を簡単にするために1/2している。
###確認テスト2
ソフトマックス関数の数式に該当するコードについて1行ずつ処理の説明をせよ。
ソフトマックス関数
$$
f(\boldsymbol{i}, \boldsymbol{u})=\frac{e^{u_{i}}}{\sum_{k=1}^{K} e^{u_{k}}}
$$
# ソフトマックス関数
def softmax(x):
if x.ndim == 2: # ミニバッチとしてデータを扱うときに用いる
x = x.T # xを転置してデータ構造を整える
x = x - np.max(x, axis=0) # オーバーフロー対策とプログラムの動きの安定
y = np.exp(x) / np.sum(np.exp(x), axis=0) # ソフトマックス関数の計算
return y.T # yを転置して出力データの構造を整える
x = x - np.max(x) # オーバーフロー対策とプログラムを安定化
return np.exp(x) / np.sum(np.exp(x)) # ソフトマックス関数の計算部分
###確認テスト3
クロスエントロピーの数式に該当するソースコードを示し、1行ずつ処理の説明をせよ。
クロスエントロピー
$$
E_{n}(\mathrm{w})=-\sum_{i=1}^{I} d_{i} \log y_{i}
$$
def cross_entropy_error(d, y):
if y.ndim == 1: # 1次元の場合
d = d.reshape(1, d.size) # (1, 全要素数)のベクトルに変形
y = y.reshape(1, y.size) # (1, 全要素数)のベクトルに変形
# 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換
if d.size == y.size:
d = d.argmax(axis=1) # argmaxで最大値のインデックスを取得
batch_size = y.shape[0] # この場合は行の形状なので1
return -np.sum(np.log(y[np.arange(batch_size), d] + 1e-7) / batch_size
# 「/ batch_size」までが上記の式に該当(左記の部分は使い勝手を良くするために追記)
# np.arangeでバッチサイズ分取り出して対数関数に与えている。
# 「1e-7」:対数関数は-∞に落ちることを回避するために極めて小さい値を与える。
#勾配降下法
モデル関数に対してコストが最小になるようにパラメータを少しづつ変化させ、トレーニングに適合したパラメータを算出するアルゴリズムです。
- 勾配降下法
最小化したい関数がある場合にその関数の勾配を求め、その勾配を元に関数の最小値を探索する手法です。
$$
W^{(t+1)}=W^{t}-\epsilon \Delta E
$$
$$
\nabla E=\frac{\partial E}{\partial W}=\left[\frac{\partial E}{\partial w_{1}} \ldots \frac{\partial E}{\partial w_{M}}\right]
$$
- バッチ勾配降下法(最急降下法)
バッチ勾配降下法はパラメータの更新のたびに全ての訓練データで勾配を計算します。バッチ勾配降下法により100件や1000件程度のデータであれば全てのデータを同時に計算しても問題はありません。しかし、訓練データの量が増加するにつれて計算を行うのが難しくなるため、その場合はミニバッチ勾配降下法や確率的勾配降下法を利用します。
- 確率的勾配降下法(SGD)
確率的勾配降下法は訓練データの中からひとつのデータを取り出し、そのデータから計算した勾配を用いてパラメータを更新していく手法です。毎回の勾配の更新がひとつのデータから行われるので計算量が少なく済むという利点がある一方で、外れ値などの影響を受けやすくなるため毎回のパラメータの更新が安定しないという問題がある点は無視できません。
$$
W^{(t+1)}=W^{(t)}-\epsilon \nabla E_{n}
$$
$$
\nabla E=\frac{\partial E}{\partial W}=\left[\frac{\partial E}{\partial w_{1}} \cdots \frac{\partial E}{\partial w_{M}}\right]
$$
SGDの実装
class SGD:
def __init__(self, learning_rate=0.01):
self.learning_rate = learning_rate
def update(self, params, grad):
for key in params.keys():
params[key] -= self.learning_rate * grad[key]
- ミニバッチ勾配降下法
ミニバッチ勾配降下法は一度に利用する訓練データの量がバッチ勾配降下法と比較して少なくなっていることから、ミニバッチと命名されており、訓練データの中からいくつかのデータを取り出し、そのデータで計算した勾配に基づいてパラメータを更新する手法です。バッチ勾配降下法と比較して計算に必要なメモリ量が少ないこと、後ほど紹介する確率的勾配降下方と比較して外れ値の影響を受けにくく学習が比較的安定して進むという利点があります。
$$
W^{(t+1)}=W^{(t)}-\epsilon \nabla E_{t}
$$
$$
E_{t}=\frac{1}{N_{t}} \sum_{n \in D_{t}} E_{n}
$$
$$
N_{t}=\left|D_{t}\right|
$$
ミニバッチ勾配降下法は、ランダムに分割したデータの集合(ミニバッチ)Dtに属するサンプルの平均誤差です。確率的勾配降下法のメリットを損なわずに、計算機の計算資源を有効活用できます。
-> CPUを利用したスレッド並列化やGPUを利用したSIMD並列化をすることができます。
##確認テスト
###確認テスト1
「1_3_stochastic_gradient_descent.ipynb」から勾配降下法の下記数式に該当するコードを抜き出せ。
$$
\mathrm{w}^{(\mathrm{t}+1)}=\mathrm{w}^{(\mathrm{t})}-\varepsilon \nabla E
$$
network[key] -= learning_rate * grad[key]
###確認テスト2
オンライン学習とは何か?
-> 学習データが入ってくるたびに都度パラメータを更新し、学習を進めていく方法。一方、バッチ学習では一度にすべての学習データを使ってパラメータ更新を行う。
###確認テスト3
下記の数式の意味を図に書いて説明せよ。
$$
w^{t+1}=w^{t}-\epsilon \Delta E
$$
1エポックのごとに重みを修正・学習することで、より高い精度で予測が可能となる。
#誤差逆伝播法
誤差逆伝播法は、重みを更新する際にする損失関数の各パラメータに対して行う勾配を求める計算を効率化する手法です。誤差逆伝播法では、算出された誤差を出力層側から順に微分し、前の層へと伝播させていきます。最小限の計算で各パラメータの数値微分を解析的に計算することができます。
###誤差逆伝播法の実装
誤差逆伝播法の実装
##確認テスト
###確認テスト1
誤差逆伝播法では不要な再帰的処理を避ける事が出来る(計算量を減らすことができる)。
「1_3_stochastic_gradient_descent.ipynb」から既に行った計算結果を保持しているソースコードを抽出せよ。
# 誤差逆伝播
def backward(x, d, z1, y):
# print("\n##### 誤差逆伝播開始 #####")
grad = {}
W1, W2 = network['W1'], network['W2']
b1, b2 = network['b1'], network['b2']
# 出力層でのデルタ(誤差関数を微分)
delta2 = functions.d_mean_squared_error(d, y)
# b2の勾配
grad['b2'] = np.sum(delta2, axis=0)
# W2の勾配
grad['W2'] = np.dot(z1.T, delta2)
# 中間層でのデルタ
#delta1 = np.dot(delta2, W2.T) * functions.d_relu(z1)
# 試してみよう
delta1 = np.dot(delta2, W2.T) * functions.d_sigmoid(z1)
delta1 = delta1[np.newaxis, :]
# b1の勾配
grad['b1'] = np.sum(delta1, axis=0)
x = x[np.newaxis, :]
# W1の勾配
grad['W1'] = np.dot(x.T, delta1)
# print_vec("偏微分_重み1", grad["W1"])
# print_vec("偏微分_重み2", grad["W2"])
# print_vec("偏微分_バイアス1", grad["b1"])
# print_vec("偏微分_バイアス2", grad["b2"])
return grad
###確認テスト2
以下の2つの数式に該当するソースコードを探せ。
$$
\frac{\partial E}{\partial y} \frac{\partial y}{\partial u}
$$
delta1 = np.dot(delta2, W2.T) * functions.d_sigmoid(z1)
$$
\frac{\partial E}{\partial y} \frac{\partial y}{\partial u} \frac{\partial u}{\partial w_{j i}^{(2)}}
$$
grad['W1'] = np.dot(x.T, delta1)
#参考文献