【ラビットチャレンジ】深層学習 Day 1 レポート



# はじめに

ラビットチャレンジの「深層学習前編（day1）レポート」になります

✅　応用数学レポート　
✅　機械学習レポート
⭐️　深層学習前編（day1）レポート
深層学習前編（day2）レポート
深層学習後編（day3）レポート
深層学習後編（day4）レポート

## Section1: 入力層~中間層

### 1-1. 要点

- 最初の層を入力層 (input layer)、最後の層を出力層 (output layer)といい、その間にある層は中間層 （intermediate layer) もしくは隠れ層 (hidden layer) という。 
- よくある構造は入力層、中間層、出力層がそれぞれ一つずつあるので、合計 3 層の構造（アーキテクチャ)となっている
- 複数の中間層を持たせれば、さらに多層のニューラルネットワークとすることができる


---

### 1-2. 確認テスト

【確認テスト】 以下の図式に動物の分類の実例を入れてみよう

![スクリーンショット 2023-11-25 11.03.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223559/c5997064-4575-0227-43c8-bd73364c8c02.png)
※ 色んな要素をもとに計算して次へ繋げている



【確認テスト】 以下の数式をPythonで書け

![スクリーンショット 2023-11-25 11.00.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223559/2c0e5bc8-d4c3-0bef-8d58-be9412dbff30.png)

```py
#解答
u = np.dot(W, x)+b
```

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

```py
#解答
z = functions.relu(u)
```


### 1-3. 関連学習
※そもそも中間層の役割は何か気になったので調査してみた

- 中間層
    - 役割
        - 入力層が取り込んだ複雑なデータを選別し、学習によって扱いやすい状態に変換
        - その後、単純なデータしか扱えない出力層へ結果を渡す
        - ニューロンの数や中間層が増えるほど分析の柔軟性や結果の表現力は向上する反面、データやメモリ、円山の量は増加
    - 中間層の適切な層数は処理するデータの量や種類で決める
        - 実際に試行錯誤しながら最適化しなければならない
    - 参考記事
        - https://dl.sony.com/ja/deeplearning/about/middle_layer.html#2.1



## Section2: 活性化関数

### 2-1. 要点

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


【中間層の活性化関数】

- ステップ関数
    - しきい値を超えたら発火する関数であり、出力は常に1か0
    - パーセプトロンで利用された関数
    - 課題 : 0-1間の間を表現でき図、線形分離可能なものしか学習できなかった


```py
def step_function(x):
    if x > 0:
        return 1
    else:
        return 0
```

- シグモイド関数
    - 0~1の間を緩やかに変化する関数で、ステップ関数ではON/OFFしかない状態に対し、信号の強弱を伝えられるようになり、予想ニューラルネットワーク普及のきっかけとなった
    - 課題 : 大きな値では出力の変化が微小なため、勾配消失問題を引き起こすことがあった

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

- ReLU関数
    - 今最も使われている活性化関数
    - 勾配消失問題の回避とスパース化に貢献

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


---

### 2-2. 確認テスト

【確認テスト】 線形と非線形の違いを図に書いて簡易に説明せよ

![スクリーンショット 2023-11-25 11.11.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223559/36977bb9-136c-fd1e-abf8-921ec3d048a3.png)

※ 線形と非線形は色んな場面で使われる。今回でいえば "関数"が線形か非線形か。他には "空間"が線形か非線形か。"方程式"が線形か非線形かなどあるので注意が必要。


---

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

```py
#解答
z1 = functions.sigmoid(u)
```

### 2-3. 関連学習

※よく使用されるReLUだが派生系が色々あるので調査してみた

- Leaky ReLU
    - Leakyという形容詞の通り，ReLUの $x < 0$ 側を傾けることで，少しだけ情報をリークさせて手前の層に逆伝搬できるようにしたものである
- PReLU
    - LeakyReLUと形は同じだが，傾き係数$ a $ も学習可能なパラメータにして一般化をおこなった「パラメトリックReLU」である（通常は $a=0.25$ で初期化して学習開始）．
- GELU (Gaussian Error Linear Units)
    - Google DeepMind から発表されたランダム正則化機能も備えたReLUである．
- Swish
    - 最高性能を出す活性化関数を強化学習を用いて探索した結果として得られたReLU型の活性化関数
- Mish
    - $ f(x)= tanh(softplus(x)) $

- 参考記事
    - https://cvml-expertguide.net/terms/dl/layers/activation-function/relu-like-activation/#3_ReLU%E5%9E%8B%E3%81%AE%E6%B4%BB%E6%80%A7%E5%8C%96%E9%96%A2%E6%95%B0




## Section3: 出力層

### 3-1. 要点



- 出力層の中間層との違い
    - 値の強弱
        - 中間層:しきい値の前後で信号の強弱を調整
        - 出力層:信号の大きさはそのままに変換
    - 確率出力
        - 分類問題の場合、出力層の出力は0~1の範囲に限定し、総和を1とする必要がある
    - ※出力層と中間層で利用される活性化関数が異なる 



- 出力層の種類


|  | 回帰 | 2値分類| 多クラス分類 |
|:-----------|:------------| :------------ | :-----------|
| 活性化関数 | 恒等写像     | シグモイド関数  |ソフトマックス関数  |
| 誤差関数 | 2乗誤差     | 交差エントロピー |交差エントロピー|



---

### 3-2. 確認テスト

【確認テスト】
![スクリーンショット 2023-11-25 11.14.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223559/881b8794-047c-e29c-7fb2-b6fccf394da6.png)

- 【問】 なぜ、引き算でなく2乗するか？
    - 正確な誤差が求められないから
    - (模範回答) 引き算を行うだけでは、各ラベルでの誤差で正負両方の値が発生し、全体の誤差を正しく表すのに都合が悪い。2乗してそれぞれのラベルでの誤差を正の値になるようにする
- 【問】 上式の1/2はどういう意味を持つか
    - 計算の都合上の問題
    - (模範回答)実際にネットワークを学習する時に行う誤差逆伝播の計算で、誤差関数の微分を用いるがその際の計算式を簡単にするため。本質的な意味はない。 

---

【確認テスト】①~③の数式に該当するソースコードを示し、1行ずつ処理の説明をせよ

```py
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) #②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)) #①ソフトマックス関数の返り値
```
交差エントロピー ①~②の数式に該当するソースコードを示し、1行ずつ処理の説明をせよ

```py
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 #①&②どちらもここ
```


## Section4: 勾配降下法

### 4-1. 要点

- 深層学習の目的
    - 学習を通して誤差を最小にするネットワークを作成すること
    - 勾配降下法を利用してパラメータを最適化



- 学習率が大きすぎた場合、最小値にいつまでもたどり着かず発散してしまう
- 学習率が小さい場合発散することはないが、小さすぎると収束するまでに時間がかかってしまう

- 確率的勾配法/勾配効果法/ミニバッチ勾配降下法
    - 確率的勾配降下法(SGD)
        - ランダムに抽出したサンプルの誤差
        - メリット
            - データが冗長な場合の計算コストの軽減
            - 望まない局所極小解に収束するリスクの軽減
            - オンライン学習ができる
    - 勾配降下法
        - 全サンプルの平均誤差 
    - ミニバッチ勾配降下法
        - ランダムに分割したデータの集合に属するサンプルの平均誤差
        - メリット
            -  SGDのメリットを損なわず、計算機の計算資源を有効利用できる→CPUを利用したスレッド並列化やGPUを利用したSIMD並列化


- 数値微分のデメリット
    - 各パラメータそれぞれについて計算することが多く、順伝搬の計算を繰り返し行う必要があり負荷が大きい
    - →　誤差逆伝播法を利用する



---
### 4-2. 確認テスト

【確認テスト】 該当するソースコードを探してみよう

```py
network[key] -= learning_rate * grad[key]
```

```py
grad = backward(x, d, z1, y)
```
---

【確認テスト】　オンライン学習とは？
- 学習データが入ってくるたびに都度パラメータを更新し、学習を進めていく方法。一方、バッチ学習では一度にすべての学習データを使ってパラメータ更新を行う

---

【確認テスト】　この数式の意味を図に書いて説明せよ
- 解答は以下の図
- 重みが時刻が変化するたびに変化量をもとに更新されている


![スクリーンショット 2023-11-25 11.43.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223559/0f5dba58-a3c7-34c3-f861-d673059950df.png)


### 4-3. 実装演習

```py

import numpy as np
from common import functions
import matplotlib.pyplot as plt

def print_vec(text, vec):
    print("*** " + text + " ***")
    print(vec)
    #print("shape: " + str(x.shape))
    print("")
# サンプルとする関数
#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 = functions.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 = 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

# サンプルデータを作成
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 = functions.mean_squared_error(d, y)
    losses.append(loss)

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


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

考察

- 活性化関数を変えるだけで収束の仕方が変わることがわかった (今回は、`ReLU`と`Sigmoid`でtry)
- また、早く収束するときと少し収束するので時間がかかるパターンがあるのでなるべく早く収束するにはどうしたら良いかが重要な論点だと感じた


### 4-4. 関連学習

※最適化アルゴリズムは色々あるので調査してみた

- Adam
    - 勾配に関しても以前の情報を指数的減衰させながら伝えることで，次元量の問題に対処している
- AdaMax
    - Adam を無限次元ノルムに対応させたものが AdaMax 
- Eve
    - 対的変化が大きければ学習率を削減し，小さければ上昇させることでフラットエリアで学習を高速化できるよう Adam を改良したもの
- AMSGrad
    - Adam, RMSProp, AdaDelta, NAdam では，指数移動平均による指数減衰が使われていた．しかし，特定の標本による勾配情報が重要なとき，そのミニバッチにおける勾配情報は指数減衰によって，直ぐに消えてしまい，最適解に収束できない問題が報告されている．そこで，重要な勾配情報が直ぐに消えてしまわないように Adam を改良した手法が AMSGrad
- 参考記事
    - https://qiita.com/ZoneTsuyoshi/items/8ef6fa1e154d176e25b8




## Section5: 誤差逆伝播法

### 5-1. 要点

- 誤差逆伝播法 (Back propagation)
    - 算出された誤差を、出力層側から順に微分し、前の層前の層へと伝播
    - 最小限の計算で各パラメータでの微分値を解析的に計算する手法
    - もう少し簡単に言うと、正解値と予測値を比較することで、重みWやバイアスbを更新していく学習手法
    - 偏微分が大事。偏微分: 1つの変数に着目し、他は定数として扱う

![スクリーンショット 2023-11-25 11.48.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223559/647021ea-88ad-b117-08eb-174bfc3ad19c.png)
※計算結果(=誤差)から微分を逆算することで、不要な再帰的計算を避けて微分を算出できる


---
### 5-2. 確認テスト



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

```py
delta2 = functions.d_mean_squared_error(d, y)
```
【確認テスト】 2つの空欄に該当するソースコードを探せ

```py
delta1 = np.dot(delta2, W2.T) * functions.d_sigmoid(z1)

grad['W1'] = np.dot(x.T, delta1)
```

---


### 5-3. 関連学習
- 問い
    - PyTorch等のフレームワークにおいてBackpropagationをどうやっているか？
- 解答
    - PyTorchでは微分を自動でやってくれている
    - `Tensor.backward()`を呼び出すと逆伝播が自動で行われ`Tensor.grad`属性に計算した微分係数が記録される

```sample.py 
x = torch.tensor(1.0, requires_grad=True) 
y = x ** 2 
y.backward() 
g = x.grad

print(g)
```


# 終わりに

今回から深層学習に入り面白い内容になってきました。やはりPyTorch等のフレームワークはめちゃくちゃ便利だと改めて感じました。引き続き、次のレポートも頑張って行きたいです。