0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ラビットチャレンジレポート3 深層学習Day1

Posted at

0. ニューラルネットワークの全体像

image.png

確認テスト

  • ディープラーニングは、結局何をやろうとしてるか?

入力値の重み付き和を次の中間層の入力値にすることを繰り返し行うことで、線形分離不能なものやより複雑なモデルを作成できるようにすること。

最適化の対象は各層の重みである。

識別モデル

開発には3つのアプローチが存在

  • 生成モデル(生成も識別に応用できる)
     データのクラス条件付き密度を推定する
     $p(C_k|\boldsymbol{x})・p(C_k)$を推定(各クラスの生起確率)
  • 識別モデル(確率的識別モデル)
     事後確率を推論し、それをもとに識別結果を得る
     $p(\boldsymbol{x}|C_k)$を推定(データがクラスに属する確率)
  • 識別関数(決定的識別モデル)
     入力データから識別結果を一気に得る
     入力値$\boldsymbol{x}$を直接クラスに写像する関数$f(\boldsymbol{x})$を推定(データの属するクラスの情報のみ)

ニューラルネットワーク

識別モデルを考える
入力層-中間層-出力層で構成される。
重みとバイアスの最適化することが目的となる。
ニューラルネットワークの対象とする問題としては

  • 回帰
  • 分類

がある。
一般に4層以上の中間層を持つニューラルネットワークを深層ニューラルネットワークと呼ばれ、
チャットボットや音声解釈などより複雑な問題を解くことに利用される。

1. 入力層~中間層

1.1 要約

入力を$x_i$、重みを$w_i$、バイアスを$b$、総入力を$u$、出力を$z$、活性化関数を$f$としたときに、
総入力$u$は、
$$
\begin{align}
u&=w_1x_1+w_2x_2+w_3x_3+w_4x_4+b\
&=\boldsymbol{W}\boldsymbol{x}+b
\end{align}
$$
ここで、$\boldsymbol{W}$、$\boldsymbol{x}$は
$$
\boldsymbol{W}=(w_1,w_2,w_3,w_4)^T\
\boldsymbol{x}=(x_1,x_2,x_3,x_4)^T
$$
を表す。
そして、出力$z$(中間層からの出力)は、
$$
\begin{align}
z&=f(u)\
&=f(\boldsymbol{W}\boldsymbol{x}+b)
\end{align}
$$
と計算できる。

image.png

1.2 実装

単層・単ユニット

def print_vec(text, vec):
    print("*** " + text + " ***")
    print(vec)
    #print("shape: " + str(x.shape))
    print("")
# 順伝搬
import numpy as np

def relu(x):
    return np.maximum(0, x)

# 重み:W
W = np.array([[0.1], [0.2]])

# バイアス:b
b = 0.5

# 入力:x
x = np.array([2, 3])

# 総入力:u
u = np.dot(x, W) + b

print_vec('重み',W)
print_vec('バイアス',b)
print_vec('入力',x)
print_vec('総入力',u)

image.png

単層・複数ユニット

# 重み
W = np.array([
    [0.1, 0.2, 0.3], 
    [0.2, 0.3, 0.4], 
    [0.3, 0.4, 0.5],
    [0.4, 0.5, 0.6]
])

print_vec("重み", W)

# バイアス
b = np.array([0.1, 0.2, 0.3])
print_vec("バイアス", b)

# 入力値
x = np.array([1.0, 5.0, 2.0, -1.0])
print_vec("入力", x)


#  総入力
u = np.dot(x, W) + b
print_vec("総入力", u)

image.png

1.3 確認テスト

動物の分類の例で入力層を書いてみる。

image.png

$$
\begin{align}
u&=w_1x_1+w_2x_2+w_3x_3+w_4x_4+b\
&=\boldsymbol{W}\boldsymbol{x}+b
\end{align}
$$
の部分をpythonのコードで書くと、

u=np.dot(x,W)+b

である。
ソースコードの中で中間層の出力を表しているのは以下の部分である。

z = functions.sigmoid(u)

2. 活性化関数

中間層用の活性化関数

  • ReLU関数
  • シグモイド関数
  • ステップ関数

ステップ関数

しきい値を超えたら発火する関数であり、1か0の値をとる。

h(x)= \left\{
\begin{array}{ll}
0 & (x \leq 0) \\
1 & (x > 0)
\end{array}
\right.

シグモイド関数

ニューラルネットワークでよく用いられる活性化関数。
ステップ関数と比較して滑らかであり、0から1の連続した実数値をとる。

h(x)=\frac{1}{1+\exp{(-x)}}

ReLU関数

入力が0を超えていれば、そのまま入力を出力し、0以下ならば0を出力する。
勾配消失問題の回避とスパース化に貢献することで良い結果をもたらしている。

h(x)= \left\{
\begin{array}{ll}
0 & (x \leq 0) \\
x & (x > 0)
\end{array}
\right.

2.2 実装

まず、各活性化関数の実装と出力の確認を行う。

import matplotlib.pyplot as plt

def step_function(x):
    return np.array(x > 0, dtype=np.int)

x = np.arange(-5,5,0.1)
y = step_function(x)

plt.plot(x, y);

image.png

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

x = np.arange(-5,5,0.1)
y = sigmoid(x)

plt.plot(x, y);

image.png

def relu(x):
    return np.maximum(0, x)

x = np.arange(-5,5,0.1)
y = relu(x)

plt.plot(x, y);

image.png

単層・単ユニット

# 順伝搬
import numpy as np

def relu(x):
    return np.maximum(0, x)

# 重み:W
W = np.array([[0.1], [0.2]])

# バイアス:b
b = 0.5

# 入力:x
x = np.array([2, 3])

# 総入力:u
u = np.dot(x, W) + b

# 中間層出力:z
z = relu(x) # 確認テスト解

print_vec('重み',W)
print_vec('バイアス',b)
print_vec('入力',x)
print_vec('総入力',u)
print_vec('中間層出力',z)

image.png

単層・複数ユニット

# 重み
W = np.array([
    [0.1, 0.2, 0.3], 
    [0.2, 0.3, 0.4], 
    [0.3, 0.4, 0.5],
    [0.4, 0.5, 0.6]
])

print_vec("重み", W)

# バイアス
b = np.array([0.1, 0.2, 0.3])
print_vec("バイアス", b)

# 入力値
x = np.array([1.0, 5.0, 2.0, -1.0])
print_vec("入力", x)


#  総入力
u = np.dot(x, W) + b
print_vec("総入力", u)

# 中間層出力
z = sigmoid(u) # 確認テスト解
print_vec("中間層出力", z)

image.png

3層・複数ユニット

def init_network():
    print("##### ネットワークの初期化 #####")
    network = {}
    
    #試してみよう
    #_各パラメータのshapeを表示
    #_ネットワークの初期値ランダム生成
    
    network['W1'] = np.array([
        [0.1, 0.3, 0.5],
        [0.2, 0.4, 0.6]
    ])
    network['W2'] = np.array([
        [0.1, 0.4],
        [0.2, 0.5],
        [0.3, 0.6]
    ])
    network['W3'] = np.array([
        [0.1, 0.3],
        [0.2, 0.4]
    ])
    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['b2'] = np.array([0.1, 0.2])
    network['b3'] = np.array([1, 2])

    print_vec("重み1", network['W1'] )
    print_vec("重み2", network['W2'] )
    print_vec("重み3", network['W3'] )
    print_vec("バイアス1", network['b1'] )
    print_vec("バイアス2", network['b2'] )
    print_vec("バイアス3", network['b3'] )

    return network

# プロセスを作成
# x:入力値
def forward(network, x):
    
    print("##### 順伝播開始 #####")

    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    
    # 1層の総入力
    u1 = np.dot(x, W1) + b1
    
    # 1層の総出力
    z1 = relu(u1) # 確認テスト解
    
    # 2層の総入力
    u2 = np.dot(z1, W2) + b2
    
    # 2層の総出力
    z2 = relu(u2) # 確認テスト解

    # 出力層の総入力
    u3 = np.dot(z2, W3) + b3
    
    # 出力層の総出力
    y = u3
    
    print("総入力1", u1)
    print("中間層出力1", z1)
    print("総入力2", u2)
    print("出力1", z1)
    print("出力合計: " + str(np.sum(z1)))

    return y, z1, z2

# 入力値
x = np.array([1., 2.])
print("入力", x)

# ネットワークの初期化
network =  init_network()

y, z1, z2 = forward(network, x)

image.png

2.3 確認テスト

線形・非線形の違いは?

線形は直線で表せる関数であり、非線形は曲線や角をもつもの、値がとびとびであるような形を表す関数のことである。3つの活性化関数は非線形関数である。

ソースコードのなかで活性化関数によって変換された部分については、「# 確認テスト解」とコード内に記入した。

3. 出力層

3.1 要約

誤差

出力の値と訓練データ(答え)との誤差を利用して学習を行う。
例えばこの誤差の計算に二乗誤差を使用する場合は、誤差関数は以下の式となる。

E_n(\boldsymbol{w})=\frac{1}{2}\sum_{i=1}^f(y_i-d_i)^2

分類問題の場合はクロスエントロピー誤差
回帰問題の場合は平均二乗誤差を用いる。
ここで、クロスエントロピーは以下の式で定義される誤差関数である。

E_n(\boldsymbol{w})=-\sum_{i=1}^fd_i\log{y_i}

活性化関数

中間層では、しきい値の前後で信号の強弱を調整するが、
出力層では、信号の大きさはそのままに変換する。
また、分類問題では出力層の出力は総和が1になる必要がある。

出力層で用いる活性化関数

  • 回帰の場合は恒等写像
  • 二値分類の場合はシグモイド関数
  • 多クラス分類の場合はソフトマックス関数

ソフトマックス関数は、

f(i,u)=\frac{e^{u_i}}{\sum_{k=1}^{K}e^{u_k}}

で表される関数である。

3.2 実装

誤差関数

# 平均二乗誤差
def mean_squared_error(d, y):
    return np.mean(np.square(d - y)) / 2
# クロスエントロピー
def cross_entropy_error(d, y): # 左辺に該当 dは正解ラベル、yは重みwのときの予測値
    if y.ndim == 1: # 値が1つの時は、ベクトルに変形
        d = d.reshape(1, d.size)
        y = y.reshape(1, y.size)
        if d.size == y.size:
            # 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換
            d = d.argmax(axis=1)
    batch_size = y.shape[0] # バッチサイズ
    # 右辺に該当
    # 正解ラベルに対応するyの成分を取り出す
    return -np.sum(np.log(y[np.arange(batch_size), d] + 1e-7)) / batch_size

活性化関数(ソフトマックス関数)

以下のように変形したものを実装する。

f(i,u)=\frac{e^{u_i+C}}{\sum_{k=1}^{K}e^{u_k+C}}

ここで$C$はオーバーフロー対策であり、入力信号の最大値を用いるのが一般的である。

# ソフトマックス関数
def softmax(x): # f(i,u)に該当:入力xはベクトルであり、i番目の成分がuiに当たる
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0) # オーバーフロー対策
        y = np.exp(x) / np.sum(np.exp(x), axis=0) # 分母分子はここで計算
        return y.T
    
    x = x - np.max(x) # オーバーフロー対策
    return np.exp(x) / np.sum(np.exp(x)) # 分母分子はここで計算

ネットワーク

多クラス分類

ソフトマックス関数の出力のから、4番目の要素の確率が最も高い。
これは、正解ラベルと一致する。

def init_network():
    print("##### ネットワークの初期化 #####")

    #試してみよう
    #_各パラメータのshapeを表示
    #_ネットワークの初期値ランダム生成

    network = {}
    network['W1'] = np.array([
        [0.1, 0.3, 0.5],
        [0.2, 0.4, 0.6]
    ])
    network['W2'] = np.array([
        [0.1, 0.4, 0.7, 1.0],
        [0.2, 0.5, 0.8, 1.1],
        [0.3, 0.6, 0.9, 1.2]
    ])
    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['b2'] = np.array([0.1, 0.2, 0.3, 0.4])
    
    print_vec("重み1", network['W1'] )
    print_vec("重み2", network['W2'] )
    print_vec("バイアス1", network['b1'] )
    print_vec("バイアス2", network['b2'] )

    return network

# プロセスを作成
# x:入力値
def forward(network, x):
    
    print("##### 順伝播開始 #####")
    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']
    
    # 1層の総入力
    u1 = np.dot(x, W1) + b1

    # 1層の総出力
    z1 = relu(u1)

    # 2層の総入力
    u2 = np.dot(z1, W2) + b2
    
    # 出力値
    y = softmax(u2)
    
    print_vec("総入力1", u1)
    print_vec("中間層出力1", z1)
    print_vec("総入力2", u2)
    print_vec("出力1", y)
    print("出力合計: " + str(np.sum(y)))
        
    return y, z1

## 事前データ
# 入力値
x = np.array([1., 2.])

# 目標出力
d = np.array([0, 0, 0, 1])

# ネットワークの初期化
network =  init_network()

# 出力
y, z1 = forward(network, x)

# 誤差
loss = cross_entropy_error(d, y)

## 表示
print("\n##### 結果表示 #####")
print_vec("出力", y)
print_vec("訓練データ", d)
print_vec("誤差",  loss)

image.png

回帰

def init_network():
    print("##### ネットワークの初期化 #####")

    network = {}
    network['W1'] = np.array([
        [0.1, 0.3, 0.5],
        [0.2, 0.4, 0.6]
    ])
    network['W2'] = np.array([
        [0.1, 0.4],
        [0.2, 0.5],
        [0.3, 0.6]
    ])
    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['b2'] = np.array([0.1, 0.2])
    
    print_vec("重み1", network['W1'] )
    print_vec("重み2", network['W2'] )
    print_vec("バイアス1", network['b1'] )
    print_vec("バイアス2", network['b2'] )

    return network

# プロセスを作成
def forward(network, x):
    print("##### 順伝播開始 #####")
    
    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']
    # 隠れ層の総入力
    u1 = np.dot(x, W1) + b1
    # 隠れ層の総出力
    z1 = relu(u1)
    # 出力層の総入力
    u2 = np.dot(z1, W2) + b2
    # 出力層の総出力
    y = u2
    
    print_vec("総入力1", u1)
    print_vec("中間層出力1", z1)
    print_vec("総入力2", u2)
    print_vec("出力1", y)
    print("出力合計: " + str(np.sum(z1)))
    
    return y, z1

# 入力値
x = np.array([1., 2.])
network =  init_network()
y, z1 = forward(network, x)
# 目標出力
d = np.array([2., 4.])
# 誤差
loss = mean_squared_error(d, y)
## 表示
print("\n##### 結果表示 #####")
print_vec("中間層出力", z1)
print_vec("出力", y)
print_vec("訓練データ", d)
print_vec("誤差",  loss)

image.png

3.3 確認テスト

平均二乗誤差が引き算ではなく二乗するのか?

引き算であると、すべて足したときに0となってしまうから。絶対値であると微分ができないため二乗したものを用いる。

1/2はどういう意味をもつか?

微分した際に出てくる2と掛け合わせて1として、計算を簡単にする。

ソフトマックス関数・クロスエントロピーの説明はコード内に記した。

4. 勾配降下法

4.1 要約

パラメータの最適化を行い、誤差を最小化するネットワークを作成する。
最適化の手法として以下のものがある。

  • 勾配降下法
  • 確率的勾配降下法
  • ミニバッチ勾配降下法

勾配降下法

学習率を$\epsilon$としたとき、勾配降下法は以下の式で表される。

\boldsymbol{w}^{(t+1)} = \boldsymbol{w}^{(t)} - \epsilon \nabla E
\nabla E=\frac{\partial E}{\partial \boldsymbol{w}}=\biggr[\frac{\partial E}{\partial w_1} \cdots \frac{\partial E}{\partial w_m} \biggl]

image-2.pngimage.png
image.png

学習率が大きい場合、最小値にいつもでも収束せずに発散してしまう。
逆に学習率が小さい場合、収束するまでに時間がかかってしまう。
学習率の決定、収束性向上のための以下のようなアルゴリズムが考えられている。

  • Momentum
  • AdaGrad
  • Adadelta
  • Adam

確率的勾配降下法(SGD)

ランダムに抽出したサンプルの誤差を利用した勾配降下法

\boldsymbol{w}^{(t+1)} = \boldsymbol{w}^{(t)} - \epsilon \nabla E_n
  • 計算コストの削減
  • 局所解に収束するリスクの軽減
  • オンライン学習が可能(データが入ってくるたびに都度パラメータの更新を行う)

などのメリットがある。

ミニバッチ勾配降下法

ランダムに抽出したデータの集合(ミニバッチ)$D_t$に属するサンプルの平均誤差

\boldsymbol{w}^{(t+1)} = \boldsymbol{w}^{(t)} - \epsilon \nabla E_t
E_t=\frac{1}{N_t}\sum_{n\in D_t}E_n\\
N_t=|D_t|

計算機の計算資源を有効利用できる

4.2 実装

勾配広報のイメージを示す。

from mpl_toolkits.mplot3d import Axes3D

def function2(x):
    return x[0]**2+x[1]**2

x = np.linspace(-2, 2, 101)
y = np.linspace(-2, 2, 101)
X, Y = np.meshgrid(x, y)

z = function2(np.array([X, Y]))

fig = plt.figure()
ax = Axes3D(fig)
ax.plot_surface(X,Y,z,cmap='plasma');

適当な初期値から始めて、値が中心へと収束していく様子を示す。

def numerical_gradient(f, x):
    h = 1e-4
    grad = np.zeros_like(x) # xと同じ形状の配列
    
    for idx in range(x.size):
        tmp_val = x[idx]
        
        # f(x+h)の計算
        x[idx] = tmp_val + h
        fxh1 = f(x)
        
        # f(x-h)の計算
        x[idx] = tmp_val - h
        fxh2 = f(x)
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        x[idx] = tmp_val # 値をもとに戻す
    
    return grad

def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append(x.copy())

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)

_, x_hist = gradient_descent(function2, init_x=np.array([-2.0,1.5]), lr=0.1, step_num=15)

x = np.linspace(-2, 2, 21)
y = np.linspace(-2, 2, 21)

X, Y = np.meshgrid(x, y)
X = X.flatten()
Y = Y.flatten()

grad = np.array([numerical_gradient(function2, np.array([x0, x1])) for x0, x1 in zip(X,Y)]).T


plt.quiver(X, Y, -grad[0], -grad[1],  angles="xy",color="#666666");
plt.plot(x_hist[:,0], x_hist[:,1], 'o');

image.png

簡単な分類問題で、分類誤差からネットワークの重みの更新を行う。

def d_relu(x):
    return np.where( x > 0, 1, 0)

def d_sigmoid_with_loss(d, y):
    return y - d

# ウェイトとバイアスを設定
# ネートワークを作成
def init_network():
    print("##### ネットワークの初期化 #####")

    network = {}
    network['W1'] = np.array([
        [0.1, 0.3, 0.5],
        [0.2, 0.4, 0.6]
    ])

    network['W2'] = np.array([
        [0.1, 0.4],
        [0.2, 0.5],
        [0.3, 0.6]
    ])

    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['b2'] = np.array([0.1, 0.2])
    
    print_vec("重み1", network['W1'])
    print_vec("重み2", network['W2'])
    print_vec("バイアス1", network['b1'])
    print_vec("バイアス2", network['b2'])

    return network

# 順伝播
def forward(network, x):
    print("##### 順伝播開始 #####")

    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']
    
    u1 = np.dot(x, W1) + b1
    z1 = relu(u1)
    u2 = np.dot(z1, W2) + b2
    y = softmax(u2)
    
    print_vec("総入力1", u1)
    print_vec("中間層出力1", z1)
    print_vec("総入力2", u2)
    print_vec("出力1", y)
    print("出力合計: " + str(np.sum(y)))

    return y, z1

# 誤差逆伝播
def backward(x, d, z1, y):
    print("\n##### 誤差逆伝播開始 #####")

    grad = {}

    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']
    #  出力層でのデルタ
    delta2 = d_sigmoid_with_loss(d, y)
    #  b2の勾配
    grad['b2'] = np.sum(delta2, axis=0)
    #  W2の勾配
    grad['W2'] = np.dot(z1.T, delta2)
    #  中間層でのデルタ
    delta1 = np.dot(delta2, W2.T) * d_relu(z1)
    # b1の勾配
    grad['b1'] = np.sum(delta1, axis=0)
    #  W1の勾配
    grad['W1'] = np.dot(x.T, delta1)
        
    print_vec("偏微分_dE/du2", delta2)
    print_vec("偏微分_dE/du2", delta1)

    print_vec("偏微分_重み1", grad["W1"])
    print_vec("偏微分_重み2", grad["W2"])
    print_vec("偏微分_バイアス1", grad["b1"])
    print_vec("偏微分_バイアス2", grad["b2"])

    return grad
    
# 訓練データ
x = np.array([[1.0, 5.0]])
# 目標出力
d = np.array([[0, 1]])
#  学習率
learning_rate = 0.01
network =  init_network()
y, z1 = forward(network, x)

# 誤差
loss = cross_entropy_error(d, y)

grad = backward(x, d, z1, y)
for key in ('W1', 'W2', 'b1', 'b2'):
    network[key]  -= learning_rate * grad[key] # 確認テスト解

print("##### 結果表示 #####")    


print("##### 更新後パラメータ #####") 
print_vec("重み1", network['W1'])
print_vec("重み2", network['W2'])
print_vec("バイアス1", network['b1'])
print_vec("バイアス2", network['b2'])

image.png

image.png

重み1は若干増加、重み2は若干減少した。
正解に近づけるために各重みがどのような効果を持っているかが解釈できる可能性がある。

4.3 確認テスト

オンライン学習とは何か

新しいデータが与えられるたびに学習をし直して、モデルの更新を行っていく学習方法。

勾配降下法によって重みが更新される部分は、ソースコードに「確認テスト解」と記した部分である。

5. 誤差逆伝搬法

5.1 要約

誤差勾配の計算

  • 数値微分のデメリット
    各パラメータについて計算するために、順伝搬の計算を繰り返し行う必要があり負荷が多い

誤差逆伝搬法

算出された誤差を、出力層側から順に微分し、前の層へと伝搬させる方法。
誤差関数

E(\boldsymbol{y})=\frac{1}{2}\sum_{j=1}^J(y_j-d_j)^2=\frac{1}{2}|\boldsymbol{y}-\boldsymbol{d}|^2

出力層の活性化関数を恒等写像

\boldsymbol{y}=\boldsymbol{u}^{(l)}

総入力

\boldsymbol{u}^{(l)}=\boldsymbol{w}^{(l)}\boldsymbol{z}^{(l-1)}+\boldsymbol{b}^{(l)}

とする。
勾配は、

\frac{\partial E}{\partial w_{ji}^{(2)}}=\frac{\partial E}{\partial \boldsymbol{y}}\frac{\partial \boldsymbol{y}}{\partial \boldsymbol{u}}\frac{\partial \boldsymbol{u}}{\partial w_{ji}^{(2)}}
``
と書ける。
ここで、

```math
\frac{\partial E(\boldsymbol{y})}{\partial \boldsymbol{y}}=\frac{\partial}{\partial \boldsymbol{y}}\frac{1}{2}|\boldsymbol{y}-\boldsymbol{d}|^2=\boldsymbol{y}-\boldsymbol{d}
\frac{\partial \boldsymbol{y}(\boldsymbol{u})}{\partial \boldsymbol{u}}=1
\frac{\partial \boldsymbol{u}(\boldsymbol{w})}{\partial w_{ji}}=\frac{\partial}{\partial w_{ji}}(\boldsymbol{w}^{(l)}\boldsymbol{z}^{(l-1)}+\boldsymbol{b}^{(l)})=\begin{bmatrix}
0\\
\vdots\\
z_i\\
\vdots\\
0
\end{bmatrix}
``
と計算できるので、

```math
\frac{\partial E}{\partial w_{ji}^{(2)}}=(\boldsymbol{y}-\boldsymbol{d})\begin{bmatrix}
0\\
\vdots\\
z_i\\
\vdots\\
0
\end{bmatrix}=(y_j-d_j)z_i

となる。

image.png

5.2 実装

仮の予測モデルを使って、勾配降下法を実装する。
回帰モデルであり、2層構造である。
「backward」部分で勾配の計算を行っている。

import numpy as np

def d_sigmoid(x):
    dx = (1.0 - sigmoid(x)) * sigmoid(x)
    return dx

def d_mean_squared_error(d, y):
    if type(d) == np.ndarray:
        batch_size = d.shape[0]
        dx = (y - d)/batch_size
    else:
        dx = y - d
    return dx

# サンプルとする関数
#yの値を予想するAI
def f(x):
    y = 3 * x[0] + 2 * x[1]
    return y

# 初期設定
def init_network():
    # print("##### ネットワークの初期化 #####")
    network = {}
    nodesNum = 10
    network['W1'] = np.random.randn(2, nodesNum)
    network['W2'] = np.random.randn(nodesNum)
    network['b1'] = np.random.randn(nodesNum)
    network['b2'] = np.random.randn()

    # print_vec("重み1", network['W1'])
    # print_vec("重み2", network['W2'])
    # print_vec("バイアス1", network['b1'])
    # print_vec("バイアス2", network['b2'])

    return network

# 順伝播
def forward(network, x):
    # print("##### 順伝播開始 #####")
    
    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']
    u1 = np.dot(x, W1) + b1
    z1 = relu(u1)
    
    ## 試してみよう
    #z1 = functions.sigmoid(u1)
    
    u2 = np.dot(z1, W2) + b2
    y = u2

    # print_vec("総入力1", u1)
    # print_vec("中間層出力1", z1)
    # print_vec("総入力2", u2)
    # print_vec("出力1", y)
    # print("出力合計: " + str(np.sum(y)))    
    
    return z1, y

# 誤差逆伝播
def backward(x, d, z1, y):
    # print("\n##### 誤差逆伝播開始 #####")    

    grad = {}
    
    W1, W2 = network['W1'], network['W2']
    b1, b2 = network['b1'], network['b2']

    # 出力層でのデルタ
    delta2 = 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) * 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

# サンプルデータを作成
data_sets_size = 100000
data_sets = [0 for i in range(data_sets_size)]

for i in range(data_sets_size):
    data_sets[i] = {}
    # ランダムな値を設定
    data_sets[i]['x'] = np.random.rand(2)
    
    ## 試してみよう_入力値の設定
    # data_sets[i]['x'] = np.random.rand(2) * 10 -5 # -5〜5のランダム数値
    
    # 目標出力を設定
    data_sets[i]['d'] = f(data_sets[i]['x'])
    
losses = []
# 学習率
learning_rate = 0.07

# 抽出数
epoch = 1000

# パラメータの初期化
network = init_network()
# データのランダム抽出
random_datasets = np.random.choice(data_sets, epoch)

# 勾配降下の繰り返し
for dataset in random_datasets:
    x, d = dataset['x'], dataset['d']
    z1, y = forward(network, x)
    grad = backward(x, d, z1, y)
    # パラメータに勾配適用
    for key in ('W1', 'W2', 'b1', 'b2'):
        network[key]  -= learning_rate * grad[key]

    # 誤差
    loss = mean_squared_error(d, y)
    losses.append(loss)

print("##### 結果表示 #####")    
lists = range(epoch)


plt.plot(lists, losses, '.')
# グラフの表示
plt.show()

image.png

各エポックでの誤差をプロットしたものを示す。
数十エポックで誤差が0付近になっているので確認できる。

5.3 補足

各レイヤでの誤差逆伝播を求める。

加算

$z=x+y$について考える。
微分は

\frac{\partial z}{\partial x}=1\\
\frac{\partial z}{\partial y}=1

最終的に求める値を$L$とすると連鎖率より、

\frac{\partial L}{\partial x}=\frac{\partial L}{\partial z}\frac{\partial z}{\partial x}=\frac{\partial L}{\partial z}・1\\
\frac{\partial L}{\partial y}=\frac{\partial L}{\partial z}\frac{\partial z}{\partial y}=\frac{\partial L}{\partial z}・1

のように上流から伝わってきた微分の値$\frac{\partial L}{\partial z}$を、そのまま次のノードへ流すだけとなる。

乗算

$z=xy$について考える。
微分は

\frac{\partial z}{\partial x}=y\\
\frac{\partial z}{\partial y}=x

最終的に求める値を$L$とすると連鎖率より、

\frac{\partial L}{\partial x}=\frac{\partial L}{\partial z}\frac{\partial z}{\partial x}=\frac{\partial L}{\partial z}・y\\
\frac{\partial L}{\partial y}=\frac{\partial L}{\partial z}\frac{\partial z}{\partial y}=\frac{\partial L}{\partial z}・x

となり、上流の値に順伝播の際の入力信号をひっくり返した値を乗算して下流へ流す。

ReLU

ReLUは、

y= \left\{
\begin{array}{ll}
0 & (x \leq 0) \\
x & (x > 0)
\end{array}
\right.

と表され、この式の微分は

\frac{\partial y}{\partial x}= \left\{
\begin{array}{ll}
0 & (x \leq 0) \\
1 & (x > 0)
\end{array}
\right.

となり、順伝播時の入力が0より大きければ逆伝播は上流の値をそのまま下流に流し、
0以下であれば下流への信号はそこでストップする。

Sigmoid

シグモイド関数は、

y=\frac{1}{1+\exp{(-x)}}

微分は、

\frac{\partial y}{\partial x}=\frac{-\exp{(-x)}}{(1+\exp{(-x)})^2}=\frac{1}{1+\exp{(-x)}}\biggr(\frac{-1+1-\exp{(-x)}}{1+\exp{(-x)}} \biggl)=y(1-y)

と計算できる。

Softmax-With-Loss

学習時のみ使用するため、交差エントロピー誤差の計算も同時に行う。
推論時は、スコアの大きさを比較すればよいのでSoftmaxレイヤは必要ない。

入力を$\boldsymbol{a}=(a_1,\cdots.a_n)$としたとき、ソフトマックス関数の出力は、

y_k = \frac{\exp{a_k}}{\sum_{i=1}^n\exp{a_i}}

教師ラベルを$\boldsymbol{t}=(t_1,\cdots.t_n)$とすると、交差エントロピー誤差は、

L=-\sum_{k}t_k\log{y_k}

次に、$L$の$y_{k'}$による微分を考えると、

\frac{\partial L}{\partial y_{k'}}=-\frac{t_{k'}}{y_{k'}}

ここで$S=\sum_{i=1}^n\exp{a_i}$とすれば、

\begin{align}
\frac{\partial L}{\partial S}&=\sum_{k=1}^n\frac{\partial L}{\partial y_k}\frac{\partial y_k}{\partial (\frac{1}{S})}\frac{\partial (\frac{1}{S})}{\partial S}\\
&=-\sum_{k=1}^n\frac{t_{k}}{y_{k}}\exp{a_k}(-\frac{1}{S^2})\\
&=\sum_{k=1}^n\frac{t_{k}}{y_{k}}Sy_k(\frac{1}{S^2})\\
&=\sum_{k=1}^n\frac{t_k}{S}\\
&=\frac{1}{S}
\end{align}

また、

\begin{align}
\frac{\partial L}{\partial \exp{a_k}}&=\frac{\partial L}{\partial y_k}\frac{\partial y_k}{\partial \exp{a_k}}\\
&=-\frac{t_{k}}{y_{k}}\frac{1}{S}\\
&=-\frac{t_{k}}{\exp{a_k}}
\end{align}

そして、

\begin{align}
\frac{\partial L}{\partial a_k}&=\frac{\partial L}{\partial \exp{a_k}}\frac{\partial \exp{a_k}}{\partial a_k}+\frac{\partial L}{\partial S}\frac{\partial S}{\partial a_k}\\
&=\biggr(\frac{1}{S}-\frac{t_{k}}{\exp{a_k}} \biggl)\exp{a_k}\\
&=(y_k-t_k)
\end{align}

SoftmaxwithLossレイヤの逆伝播は、

\frac{\partial L}{\partial \boldsymbol{a}}=\boldsymbol{y}-\boldsymbol{t}

バッチ処理の場合は

\frac{\partial L}{\partial \boldsymbol{a}}=\frac{1}{N}\sum_{i=1}^N(\boldsymbol{y_i}-\boldsymbol{t_i})

となる。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?