ラビットチャレンジ　レポート（その8　深層学習day1）

# 深層学習Day1
## ニューラルネットワークの全体像

**＜確認テスト＞**
ディープラーニングは、結局何をやろうとしているか2行以内で述べよ。
また、次の中のどの値の最適化が最終目的か、全て選べ。

  * ディープラーニングでやろうとしていることは、明示的なプログラムを用いずに、多数の中間層を持つネットワークを用いて、入力値から出力値に変換するモデルを構築すること。
  * 最終的に最適化したいのは、**重み・バイアス**の2つ。

**＜確認テスト＞**
次のネットワークを紙に書け。
入力層：2ノード1層
中間層：3ノード2層
出力層：1ノード1層

![ニューラルネットワーク.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2437549/a6a16320-65e6-43e5-6bfc-e32954981795.png)

## Section1 入力層～出力層
ニューラルネットワークの入力層とは、ニューラルネットワークへの入力データ。（＝モデル自体への入力値）
この入力値に重みをかけてバイアスを加えた値が、次の層（中間層）へと渡される。
最後の層に渡されたものが、出力値として出てくる（出力層）。
中間層や出力層では、活性化関数と呼ばれるものをかけて値を変換する（後述）。

**＜確認テスト＞**
図式に動物分類の実例を入れてみる。

![動物分類.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2437549/32a106c7-5759-f9dd-fc11-929e03ae0252.png)

**＜確認テスト＞**
この数式をpythonで書け

```python
import numpy as np
u = np.dot(x, w) + b
```

**＜確認テスト＞**
1-1のファイルから、中間層の出力を定義しているソースを抜き出せ。

```python
z = functions.relu(u)
print_vec("中間層出力", z)
```

### 実装演習
単層・単ユニットのニューラルネットワークを実装

```python
import numpy as np
from common import functions

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

# バイアス
b = 0.5

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

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

# 中間層出力
z = functions.relu(u)
```

## Section2 活性化関数
ニューラルネットワークにおいて、次の層への出力の大きさを決める非線形の関数。
入力値の値によって、次の層への信号のON/OFFや強弱を定める働きを持つ。

**＜確認テスト＞**
線形と非線形の違いを図に書いて簡易に説明せよ。

![線形と非線形.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2437549/e52b09cc-2b6f-a4b6-e87d-52112acde817.png)

グラフが直線になるものが線形、曲線になるものが非線形。

  * 中間層用の活性化関数
    * ReLU
    * Sigmoid
    * ステップ関数

  * 出力層用の活性化関数
    * Softmax
    * 恒等関数
    * Sigmoid

### 実装演習
活性化関数の実装

```python
#ReLU
def relu(x):
    return np.maximum(0, x)

#ステップ関数
def step(x):
    if x>0:
        return 1
    else:
        return 0

#Sigmoid関数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
```

**＜確認テスト＞**
配布されたソースコードより該当する箇所を抜き出せ。

```python
z = functions.sigmoid(u)
```

## Section3 出力層
モデルに入力データを入れたときの推定結果を出力する層。
分類であれば、各クラスに属する確率を出力。回帰であれば、目的変数の推定値を出力、といった具合。
この出力層の結果を用いて、モデルの精度を評価する。そのために使われる関数が誤差関数。
一般的には、以下の式で示される平均二乗誤差がよく用いられる。

```math
E_n(\boldsymbol{w}) = \frac{1}{2} \Sigma_{j=1}^{l}(y_j-d_j)^2 = \frac{1}{2}||\boldsymbol{y}-\boldsymbol{d}||^2
```
ここで、$\boldsymbol{y}$は出力結果、$\boldsymbol{d}$は正解データを表している。

**＜確認テスト＞**
なぜ引き算ではなく二乗するのか答えよ
上式の1/2はどういう意味を持つか答えよ

⇒各データでの推定結果と正解の誤差は、正負両方になり得るので、打ち消しあって誤差が小さくならないように、2乗して総和を取ることで、すべてのデータに対する誤差を正しく計算するため。
⇒1/2は誤差逆伝播する際の微分計算を簡単にするためのもので、本質的な意味はない。

**＜確認テスト＞**
①～③の数式に該当するソースコードを示し、1行ずつ処理の説明をせよ。
以下のコード中に示す。

```python
def softmax(x): #①の部分　関数の出力
    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) #分子が②、分母が③。この値が0~1に収まるため、確率とみなせる。
        return y.T

    x = x - np.max(x) # オーバーフロー対策
    return np.exp(x) / np.sum(np.exp(x))
```

**＜確認テスト＞**
①～②の数式に該当するソースコードを示し、1行ずつ処理の説明をせよ。
以下のコード中に示す。

```python
def cross_entropy_error(d, y): #①の部分。関数の出力
    if y.ndim == 1:
        d = d.reshape(1, d.size)
        y = y.reshape(1, y.size)
        
    # 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換
    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 #②の部分。
```

### 実装演習
ネットワークを作成（誤差逆伝播も含む）

```python
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])

    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
    # 中間層の活性化関数
    # ReLU関数
    z1 = functions.relu(u1)
    u2 = np.dot(z1, W2) + b2
    y = functions.softmax(u2)

    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 = functions.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) * functions.d_relu(z1)
    # b1の勾配
    grad['b1'] = np.sum(delta1, axis=0)
    #  W1の勾配
    grad['W1'] = np.dot(x.T, delta1)

    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 = functions.cross_entropy_error(d, y)

grad = backward(x, d, z1, y)
for key in ('W1', 'W2', 'b1', 'b2'):
    network[key]  -= learning_rate * grad[key]

#ここまでで重み・バイアスが更新されている。
```

## Section4 勾配降下法
深層学習における目的は、ネットワークにデータを学習させることで、誤差を最小にするネットワークを作成すること。
よって、誤差を最小にするようなパラメータを求めるために行うのが、勾配降下法。
主に以下の3種類。

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

単純な勾配降下法は、全データをモデルに与えて得られた誤差を計算する。

**＜確認テスト＞**
勾配降下法に該当するソースコードを探す。

```python
network[key] -= learning_rate * grad[key]
grad = backward(x, d, z1, y)
```

確率的勾配降下法では、ランダムに抽出したサンプルの誤差を用いて誤差を計算する。この方法のメリットは、

  * データが冗長な場合の計算コストの軽減
  * 望まない局所極小解に収束するリスクの軽減
  * オンライン学習ができる

ことが挙げられる。

**＜確認テスト＞**
オンライン学習とは何か。2行でまとめよ。

⇒モデルにその都度データを与えて学習させる学習手法。学習データが入ってくる都度パラメータを更新し、学習を進めていく。

ミニバッチ勾配降下法では、ランダムに分割したデータの集合（ミニバッチ）に属するサンプルの平均誤差を取る。
これによるメリットは、

  * 確率的勾配降下法のメリットを損なわず、計算機の計算資源を有効活用できること。

**＜確認テスト＞**
$\boldsymbol{w}^{t+1}=\boldsymbol{w}^{t}-\varepsilon \nabla E_t$
この数式の意味を図に書いて説明せよ。

⇒重み更新の数式。

![重み更新.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2437549/7f0a0513-f66e-6bd0-0031-b2deff1c972a.png)

### 実装演習
実際に、

```math
f(x,y) = x^2 + y^2
```
として、$(3.0, 4.0)$を初期位置として勾配法によりパラメータを更新してみる。

```python
import numpy as np

# 関数の定義
def function(x):
    return x[0]**2 + x[1] **2

# 数値微分
def numerical_gradient(f, x):
    h = 1e-4
    grad = np.zeros_like(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 gradienr_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x

    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad
    return x

init_x = np.array([3.0,4.0])
gradienr_descent(function, init_x, lr=0.1)
```
この結果は、

```
array([6.11110793e-10, 8.14814391e-10])
```
となり、限りなく0に近づいていることがわかる。

## Section5 誤差逆伝播法
ニューラルネットワークにおいて、各重み（パラメータ）の微分値を直接計算しようとすると、計算量が膨大になってしまう。そこで利用するのが誤差逆伝播法。
算出された誤差を、出力層側から順に微分し、前の層へと伝播させていく手法。
最小限の計算で各パラメータでの微分値を解析的に計算していく。

**＜確認テスト＞**
誤差逆伝播法では不要な再帰的処理を避けることができる。
既に行った計算結果を保持しているソースコードを抽出せよ。

```python
#  出力層でのデルタ
delta2 = functions.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) * functions.d_relu(z1)
```

**＜確認テスト＞**
2つの空欄に該当するソースコードを探せ

```python
delta2 = functions.d_mean_squared_error(d, y)
grad['W2'] = np.dot(z1.T, delta2)
```

### 実装演習
（Section3の実装演習に含まれる）