深層学習(python、入力層～誤差逆転伝播法)

※以下、個人的な勉強のためのレポートです。
※間違い多々あると存じますが、現在の理解レベルのスナップショットのようなものです。
※勉強のためWebサイトや書籍からとても参考になったものを引用させていただいております。
http://ai999.careers/rabbit/
※書籍「ゼロからつくるDeep Learning」を参考にさせていただいております。

# 深層学習という手法
深層学習(ディープラーニング)は、人工知能における機械学習アルゴリズムの一つである。よって深層学習も、データを学習し、予測を行う「回帰」と、データの特長量からカテゴライズを行う「分類」が主たる使用目的である。「回帰」も「分類」も従来の機械学習で扱っていた対象であるので、すべての問題に深層学習を適用する必要はない。しかしながら、従来の機械学習が、次元の増加に苦しむ一方で、深層学習は多次元の扱いに追従する仕組みを持つことが特徴である。階層を多層化し、出力の誤差を修正していく、誤差を最小化するパラメータ(重みwとバイアスb)を発見する仕組みが深層学習にはある。

# Python、numpyによる行列演算
深層学習に限らず、機械学習について勉強していると、その中身は線形代数を記述言語として説明がなされている。
https://www.hellocybernetics.tech/entry/2016/09/30/003155
よって、深層学習のアルゴリズムを実装するに際しては、プログラミング言語で線形代数を扱える必要がある。
https://www.sejuku.net/blog/75248
https://pythondatascience.plavox.info/numpy/%E8%A1%8C%E5%88%97%E3%82%92%E6%93%8D%E4%BD%9C%E3%81%99%E3%82%8B
http://matsunag.hatenablog.com/entry/20161115/1479201919
http://kaisk.hatenadiary.com/entry/2014/09/20/185553

## numpuyによる行列の作成方法

```math
\begin{pmatrix}
1 & 2 \\
\end{pmatrix}
```
は、

```
import numpy as np
np_arr1 = np.array([1, 2])
print(np_arr1)
[1 2]
```

```math
\begin{pmatrix}
1 & 2 \\
3 & 4 
\end{pmatrix}
```
は、

```
import numpy as np
np_arr1 = np.array([[1,2],
                    [3,4]])
print(np_arr1)
[[1 2]
 [3 4]]
```

```math
\begin{pmatrix}
1 & 2 & 3\\
4 & 5 & 6 
\end{pmatrix}
```
は、

```
import numpy as np
np_arr1 = np.array([[1,2,3],
                    [4,5,6]])
print(np_arr1)
```

縦ベクトル

```math
\begin{pmatrix}
1\\
2\\
3 
\end{pmatrix}
```
は、直接作成するすべはないらくし、

```
import numpy as np
a = np.array([1,2,3])
a1 = a[:,np.newaxis]
print(a1)
[[1]
 [2]
 [3]]
```

この記述は、意味がわかりにくい。
https://qiita.com/rtok/items/10f803a226892a760d75
よってもしくは、

```
import numpy as np
a = np.array([1,2,3])
a1 = np.c_[a]
print(a1)
[[1]
 [2]
 [3]]

転地は、

```
import numpy as np
np_arr1 = np.array([[1,2,3],
                    [4,5,6]])
print(np_arr1)
print(np_arr1.T)
[[1 2 3]
 [4 5 6]]
[[1 4]
 [2 5]
 [3 6]]
```

## 行列の演算
### 和、差
```
a = np.array([1, 2])
b = np.array([4, 3])
print('和',a+b)
print('差',a-b)
```
### 積
```
a = np.array([1, 2])
b = np.array([4, 3])
print('積',a*b)
積 [4 6]
```
### 内積
```
a = np.array([1, 2])
b = np.array([4, 3])
内積 10
print('内積',np.dot(a, b))
```
### 固有値分解
```
import numpy as np
import numpy.linalg as LA
np_arr1 = np.array([[8,1],
                    [4,5]])
print(np_arr1)
print(LA.eig(np_arr1))
[[8 1]
 [4 5]]
(array([9., 4.]), array([[ 0.70710678, -0.24253563],
       [ 0.70710678,  0.9701425 ]]))
固有値9、4
固有ベクトル[ 0.70710678, -0.24253563]、[ 0.70710678,  0.9701425 ]
```

(注)
http://www.geisya.or.jp/~mwm48961/linear_algebra/eigenvalue2.htm
参照ページの答えは以下。
固有値λ1=4，固有ベクトル(1,1)
固有値λ2=9，固有ベクトル(1,-4)

最初は、ん？と思いましたが、固有ベクトルは一つだけではなく、それと同じ方向を向いた、長さの違う無数のベクトルが当てはまる。逆方向を向いているベクトルも固有ベクトルであるhttps://eman-physics.net/math/linear09.html

それと、
```
(array([9., 4.]), array([[ 0.70710678, -0.24253563],
       [ 0.70710678,  0.9701425 ]]))
```

の見方は、
固有値9に対応する固有ベクトルは (0.70710678 0.70710678)、
固有値4に対応する固有ベクトルは (-0.24253563 0.9701425)。
出力される2×2行列を、一列目、2列目として見る。

```math
固有値9、固有ベクトルt
\begin{pmatrix}
0.70710678 \\
0.70710678
\end{pmatrix}
(tは任意定数、t≠0)
```
(0.70710678 0.70710678)を定数倍(例えば×1.414213…)、すれば、(1,1)になる。

```math
固有値9、固有ベクトルt
\begin{pmatrix}
-0.24253563 \\
0.9701425
\end{pmatrix}
(tは任意定数、t≠0)
```
(-0.24253563 0.9701425)を定数倍(例えば×-4.123105…)、すれば、(1,-4)になる。
※逆方向も固有ベクトル

## 連立一次方程式の解法
行列は、もともと連立方程式の効率的な解き方の研究から生まれたものであるといわれている。機械学習では、非常に多次元のパラメータを計算せねばならず、pythonおよびnumpyはその仕組みを備えている。
https://nagi-blog28.com/liner-algebra
https://org-technology.com/posts/solving-linear-equations-LU.html

```math
x+y=3 \\
2x+5y=9
```
これを、行列の形にして、pythonで解かせる。

```math
\begin{pmatrix}
1 & 1 \\
2 & 5
\end{pmatrix}
\begin{pmatrix}
x \\
y
\end{pmatrix}
=
\begin{pmatrix}
3 \\
9
\end{pmatrix}
```

```
import numpy as np
A = np.array([[1,1],
              [2,5]])
b = np.array([3, 9])
x = np.linalg.solve(A, b)
print(x)
[2. 1.]
```

# 入力層から中間層
深層学習を構成するノード(パーセプトロン)のモデル図
![NN概念図.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405299/e7fb4624-6202-4683-3af4-79d251f64e2c.jpeg)

このモデルを数式化すると、

```math
u=w1x1+w2x2+w3x3+w4x4+b
```
```math
w1…w4を行列W=\begin{pmatrix}
w1 \\
w2 \\
w3 \\
w4
\end{pmatrix}
```

とし、

```math
x1…x4を行列X=\begin{pmatrix}
x1 \\
x2 \\
x3 \\
x4
\end{pmatrix}
```

とすれば、

```math
u=w1x1+w2x2+w3x3+w4x4+b \\
=WX+b
```

と記述することができる。
行列(ベクトル)を用いることで、モデルの記述が簡便になる。
線形代数には行列を変数のように扱う手法である。解きたい問題のモデルを構築し、モデルをを数式化し、数式を数学を用いてよりシンプルな形に変形し、シンプルなモデルを構築、そのモデルを実装するという工学的なアプローチに資する。

先の数式のpythonでの実装は、
```
u=np.dot(X,W)+b
```
となる。

# 活性化関数
ある入力をAと判定するか、Bと判定するかという問題を分類問題という。先に示したパーセプトロンの場合、得られた一次関数から、ある領域はA、他の領域はBというような直線で境界を定めるような分類しか行えない。しかし現実の分類問題は、直線領域で分類できるような問題だけではない。

入力信号の総和を出力信号に変換する関数を**活性化関数**という。パーセプトロンの場合、ある閾値を境界として出力が変化するステップ関数を活性化関数に使用しているため、線形分離可能な問題への適用に限られてしまう。そこで、非線形な問題にも対処できるようにする工夫が必要となる。

そのアプローチが、
①パーセプトロンの出力関数に非線形な活性化関数を導入すること
②パーセプトロンを多層化すること
である。

DNNで使用される「非線形な活性化関数」の代表例として、シグモイド関数、ReLU関数、ハイポバリックタンジェントなどがあげられる。

```math
シグモイド関数 \\
f(x)=\frac{1}{1+e^{-x}} \\
```

```math
ReLU関数 \\
f(x) = \left\{
\begin{array}{ll}
x & (x \geq 0) \\
0 & (x \lt 0)
\end{array}
\right. \\
入力が0を超えていればその入力をそのまま出力し、\\
0以下ならば0を出力する。
```

```math
ハイパボリックタンジェント(双曲線正接)
f(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}}
```
層を多層にすることにより、
第n層のnx番ノードから出力信号を受け取った受け取った信号を受け取った第n+1層のn+1x番ノード
第n層のnx番ノードから出力信号を受け取った受け取った信号を受け取った第n+1層のn+1x+1番ノード
第n層のnx+1番ノードから出力信号を受け取った受け取った信号を受け取った第n+1層のn+1x番ノード
第n層のnx+1番ノードから出力信号を受け取った受け取った信号を受け取った第n+1層のn+1x+1番ノード
それぞれで、「どの入力ノードから受け取った」出力が「次層のどの入力ノード」へ入力されたかで、異なる出力を得ることができ線形でない問題への対応が可能でとなる。
http://yaju3d.hatenablog.jp/entry/2017/05/31/235745
されに、それぞれの出力に活性化関数が使用され、層ごとに重みwやバイアスbが加味されていく。活性化関数が非線形であるがゆえに、単純な累乗計算になってしまう「隠れ層のないネットワーク」の問題を回避できる。

# 出力層
出力層の役割
中間層までは人間にはわからない値であるが、最終的な出力を行う出力層は人間が知覚できるものにする必要があり、見せ方が大切になる。
具体的には、
分類問題の場合、Aである確率、Bである確率、Cである確率・・・
→出力関数にはソフトマックス関数を使用
回帰問題の場合、予測値
→出力関数には恒等関数を使用

DNNは入力データを学習していき、重みとバイアスの最適値を調整していくことが目的となる。
よって、出力値と正解値との誤差を少なくするために、出力値と正解値との誤差を出力する「誤差関数」を導入する。
「誤差値」ではなく「誤差関数」を採用する理由は、学習の過程に後述する「微分」「偏微分」を用いるためである。

## 誤差関数

産出した出力と正解の誤差
誤差を最小化させていくのがDNNの目標、よって誤差関数は重要な項目である。

誤差関数の有名なものに、二乗誤差がある。
二乗誤差の公式の理由
・なぜ二乗
⇒値を正にするため
・なぜ1/2
⇒微分を簡単にするため

## 出力層の活性化関数
⇒中間層の活性化関数は、閾値の前後で信号の強弱を調整
⇒出力層の活性化関数は、信号の大きさ(比率)はそのままに変換、人間がわかるように
⇒確率出力を行う分類問題の場合、出力層の出力は0～1の範囲に限定し、
総和を1とする必要があるよって、活性化関数が異なる

代表的な出力層の活性化関数には、
・ソフトマックス関数
・恒等写像
・シグモイド関数
があげられる。

タスクが回帰の場合、活性化関数は恒等写像、二乗誤差
二値分類、シグモイド関数、交差エントロピー
多値分類、ソフトマックス関数、交差エントロピー
⇒この組み合わせは計算の相性が良い

ソフトマックス関数のコード
ベクトル前提※二次元、今回は多次元には使えない形で実装

```
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)
        return y.T

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

二乗誤差のコード

```
def mean_squared_error(d, y):
    return np.mean(np.square(d - y)) / 2
```

交差エントロピーのコード

```
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
```
※微小な値を足し合わせることで0にならないようにしている1e-7
※delta = 1e-7 というのは、「0.0000001」という数字を表現する一つの方法が「1e-7」で、指数表示と呼ばれている。1*10-7（1かける10の-7乗）。

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

(再掲)DNNの目的
学習を通じて、誤差を最小にするネットワークを作成すること
→誤差E(W)を最小化するパラメータwの発見

〇誤差∇E
誤差をパラメータwで微分したものを∇Eとする

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

〇学習率ε
学習率が大きすぎると発散してしまう。
かといって、学習率が小さすぎると収束するまでに時間がかかる。
ここに、大域的極小解の問題がある。

勾配降下法の学習率決定、収束性向上のためのアルゴリズムがあり、代表的なものに以下がある。
・Momentum
・AdaGrad
・Adadelta
・Adam

誤差関数の値をより小さくする方向に、重みWやバイアスbを更新し、次の周回(エポック)に反映する。

確率的勾配降下法(SGT)のメリット
勾配降下法は全データ、確率的勾配降下法は全データについてサンプルを抽出
データが冗長な場合の計算コストの軽減
望まない局所極小解に就職するリスクの軽減※かならず0ではない
オンライン学習ができる
→オンライン学習とは、
例えば、facebookのユーザ登録などで、新規ユーザのみの情報で学習するということができる。
自分でサービスを作ったときにメリットとなり、オンライン学習とは学習データが入ってくるたびにその都度、新たに入ってきたデータのみを使って学習を行うものになる。
https://dev.classmethod.jp/machine-learning/online-batch-learning/

(具体的なメリット)
・1回の学習あたりのコストが低い
・1回の学習あたり1件のデータしか使わないので非常に軽いです
・学習データを全て蓄えておく必要がない
・これも1件のデータしか使わない故、可能。
　※ただし、データがないと同じ結果の再現もできないので注意
・ユーザ行動の変化にすぐに対応できる
　※例えば季節性のイベントなどに即座に対応可能な仕組みが実現可能
　※突発的な盛り上がりに対しても、その話題がホットなうちに対応可能
(具体的なデメリット)
・外れ値などノイズの影響を受けやすい
・バッチ学習であれば事前にデータを確認して対処していたなどの状況を拾ってしまう可能性がある
・最新のデータの影響を受けやすい
　※「ユーザ行動の変化にすぐに対応できる」の裏返しであるが、新しいパターンのみを拾ってしまい、これまでできていたことができなくなっているという状況が起こりやすくなる
　※Deep Learningの文脈ではバッチ学習と異なり、都度目的関数の形が変わるので局所解に落ちづらいという性質もあると言われている

# 誤差逆転伝播法

## 数値微分と解析微分
<数値微分>
関数の描くグラフ上でとても近接した二点を結ぶ直線をとり，微分値の近似値とする方法
https://gihyo.jp/dev/serial/01/java-calculation/0069
<解析的な微分>
一方、「極限」の考え方を用いて、導関数を求める方法を解析的な部分という

具体的な計算を行う数値微分では計算リソースが大きいので、連鎖律を用いて、算出された誤差を、出力層側から順に微分し、前の層、前の層へと伝播、最小限の計算で各パラメータの微分値を解析的に計算する手法を「誤差逆転伝播法」という。

誤差関数の微分
出力における活性化関数の微分に誤差関数の微分をかける
ソフトマックスと交差エントロピーの複合関数
シグモイドと交差エントロピーの複合関数
をどうせ掛け合わせるなら実装段階で用意しておく方が便利

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

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

# ソフトマックスとクロスエントロピーの複合導関数
def d_softmax_with_loss(d, y):
    batch_size = d.shape[0]
    if d.size == y.size: # 教師データがone-hot-vectorの場合
        dx = (y - d) / batch_size
    else:
        dx = y.copy()
        dx[np.arange(batch_size), d] -= 1
        dx = dx / batch_size
    return dx

# シグモイドとクロスエントロピーの複合導関数
def d_sigmoid_with_loss(d, y):
    return y - d
```

誤差伝播法の実装の考え方

```
∂E/∂y
delta2 = functions.d_mean_squared_error(d, y)
∂E/∂y ∂u/∂u
delta2 = functions.d_mean_squared_error(d, y)
∂E/∂y ∂u/∂u ∂u/∂w(2)
grad['W1'] = np.dot(x.T, delta1)
```