AI実装検定に向けての勉強（数式、言語、実装）

# AI実装検定
第1回 AI実装検定　A級を受験しようと思います。

2020/03/21(土)の14：00〜15：00で実施される予定
http://kentei.ai/application/requirements

# 試験対策（数式）
検定問題のサンプルや公式教材として提供されているバンビチャレンジの動画を見たところ、数式の範囲ではとりあえずニューラルネットワークを理解できていれば問題なさそう。
なので、ニューラルネットワークの数式を計算できるようにしておく。

# ネットワーク概要
![スクリーンショット 2020-03-07 19.32.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531435/a99b6edb-be52-3a26-65f6-131883c5382d.png)
（図：draw.ioさんで描画）
一般的な2層ニューラルネットワーク（中間層が1層のみ）を考える。
入力層、中間層、出力層があり、それぞれの層でユニットは各2個ずつ、それぞれに対してバイアスもかかっていて、全結合されているとする。

# 順伝播
ニューラルネットワークで識別する際の順序。
## 入力層
まずは入力層。
ユニット数は2個なので、$ x_1 $と$ x_2 $とする。また、のちに出てくる中間層と意味を分けるため、$ x_1^{(1)} $と$ x_2^{(1)} $としておく。
これを行列で表しておく。

```math
x^{(1)} = 
\begin{bmatrix}
x_1^{(1)} \\
x_2^{(1)}
\end{bmatrix}
```

## 全結合（入力層〜中間層）
全結合後の値を$ a_1^{(1)} $と$ a_2^{(1)} $とすると、重みと入力層の積にバイアスを加える。

```math
a^{(1)} = 
\begin{bmatrix}
a_1^{(1)} \\
a_2^{(1)}
\end{bmatrix} =
\begin{bmatrix}
ω_{11}^{(1)}x_1^{(1)} + ω_{21}^{(1)}x_2^{(1)} + b_1^{(1)} \\
ω_{12}^{(1)}x_1^{(1)} + ω_{22}^{(1)}x_2^{(1)} + b_2^{(1)}
\end{bmatrix}
```

重みとバイアスはそれぞれ下記の通り。

```math
ω^{(1)} = 
\begin{bmatrix}
ω_{11}^{(1)} & ω_{21}^{(1)} \\
ω_{12}^{(1)} & ω_{22}^{(1)}
\end{bmatrix}
```
```math
b^{(1)} = 
\begin{bmatrix}
b_1^{(1)} \\
b_2^{(1)}
\end{bmatrix}
```

## 中間層
全結合したものをsigmoid関数に与えた結果を中間層とする。

```math
x^{(2)} = 
\begin{bmatrix}
x_1^{(2)} \\
x_2^{(2)}
\end{bmatrix} =
\begin{bmatrix}
sigmoid(a_1^{(1)}) \\
sigmoid(a_2^{(1)})
\end{bmatrix}
```

## 全結合（中間層〜出力層）
先ほどと同様の計算。

```math
a^{(2)} = 
\begin{bmatrix}
a_1^{(2)} \\
a_2^{(2)}
\end{bmatrix} =
\begin{bmatrix}
ω_{11}^{(2)}x_1^{(2)} + ω_{21}^{(2)}x_2^{(2)} + b_1^{(2)} \\
ω_{12}^{(2)}x_1^{(2)} + ω_{22}^{(2)}x_2^{(2)} + b_2^{(2)}
\end{bmatrix}
```

重みとバイアスはそれぞれ下記の通り。

```math
ω^{(2)} = 
\begin{bmatrix}
ω_{11}^{(2)} & ω_{21}^{(2)} \\
ω_{12}^{(2)} & ω_{22}^{(2)}
\end{bmatrix},\:
b^{(2)} = 
\begin{bmatrix}
b_1^{(2)} \\
b_2^{(2)}
\end{bmatrix}
```

## 出力層
全結合したものをsigmoid関数に与えた結果を出力層とする。

```math
y = 
\begin{bmatrix}
y_1 \\
y_2
\end{bmatrix} =
\begin{bmatrix}
sigmoid(a_1^{(2)}) \\
sigmoid(a_2^{(2)})
\end{bmatrix}
```

出力層ではyの結果から識別結果を決める。

# 逆伝播
ニューラルネットワークを学習する際は、重みとバイアスを最適なものに更新していく必要がある。
まずは上記の順伝播を実行してyを得る。

## 損失関数
$ y_1 $と$ y_2 $をそれぞれの正解データ$ t_1 $と$ t_2 $との差分を算出して、損失関数を求める。（$ y_1 $と$ y_2 $が正しい識別の結果であれば差分が0になるので損失関数も0になる）

```math
E = \frac{1}{2}\sum_{j=1} \left( y_j - t_j \right)^{2} 
```

## 更新式
重みとバイアスのそれぞれの重み更新式は下記の通り。

```math
ω_{kj}^{(l)} = ω_{kj}^{(l)} - \frac{δE}{δω_{kj}^{(l)}} \\
b_{j}^{(l)} = b_{j}^{(l)} - \frac{δE}{δb_{j}^{(l)}}
```

## 最終層(l = 2)の更新分を計算
### 重み
```math
\begin{align}
\frac{δE}{δω_{kj}^{(2)}} &= \frac{δE}{δy_{j}^{(2)}} \frac{δy_{j}^{(2)}}{δa_{j}^{(2)}} \frac{δa_{j}^{(2)}}{δω_{kj}^{(2)}} \\
&= g_{j}^{(2)}x_{k}^{(2)}
\end{align}
```
```math
\begin{align}
g_{j}^{(2)} &= \frac{δE}{δy_{j}^{(2)}} \frac{δy_{j}^{(2)}}{δa_{j}^{(2)}} \\
&= (y_j - t_j) f(a_j^{(2)})
\end{align}
```
```math
f(a_{j}^{(2)}) = \frac{δy_{j}^{(2)}}{δa_{j}^{(2)}} = (1-sigmoid(a_{j}^{(2)}))sigmoid(a_{j}^{(2)})
```

### バイアス
重みとほぼ同じ。

```math
\begin{align}
\frac{δE}{δb_{j}^{(2)}} &= \frac{δE}{δy_{j}^{(2)}} \frac{δy_{j}^{(2)}}{δa_{j}^{(2)}} \frac{δa_{j}^{(2)}}{δb_{j}^{(2)}} \\
&= g_{j}^{(2)}
\end{align}
```

## 最終層以外(l = 1)の更新分を計算
### 重み
```math
\begin{align}
\frac{δE}{δω_{kj}^{(1)}}
&= \frac{δE}{δy_{1}^{(2)}} \frac{δy_{1}^{(2)}}{δa_{1}^{(2)}} \frac{δa_{1}^{(2)}}{δx_{j}^{(2)}} \frac{δx_{j}^{(2)}}{δa_{j}^{(1)}} \frac{δa_{j}^{(1)}}{δω_{kj}^{(1)}}   + 
\frac{δE}{δy_{2}^{(2)}} \frac{δy_{2}^{(2)}}{δa_{2}^{(2)}} \frac{δa_{2}^{(2)}}{δx_{j}^{(2)}} \frac{δx_{j}^{(2)}}{δa_{j}^{(1)}} \frac{δa_{j}^{(1)}}{δω_{kj}^{(1)}} \\

&= \left( \frac{δE}{δy_{1}^{(2)}} \frac{δy_{1}^{(2)}}{δa_{1}^{(2)}} \frac{δa_{1}^{(2)}}{δx_{j}^{(2)}} + 
\frac{δE}{δy_{2}^{(2)}} \frac{δy_{2}^{(2)}}{δa_{2}^{(2)}} \frac{δa_{2}^{(2)}}{δx_{j}^{(2)}} \right) \frac{δx_{j}^{(2)}}{δa_{j}^{(1)}} \frac{δa_{j}^{(1)}}{δω_{kj}^{(1)}} \\

&= \left( g_{1}^{(2)}ω_{j1}^{(2)} + g_{2}^{(2)}ω_{j2}^{(2)} \right) f(a_j^{(1)})x_k^{(1)} \\

&= \left( \sum_{i=1} g_{i}^{(2)}ω_{ji}^{(2)} \right) f(a_j^{(1)})x_k^{(1)} \\

&= g_{j}^{(1)}x_{k}^{(1)}
\end{align}
```
```math
g_{j}^{(1)} = \left( \sum_{i=1} g_{i}^{(2)}ω_{ji}^{(2)} \right) f(a_j^{(1)})
```

### バイアス
重みとほぼ同じ。

```math
\begin{align}
\frac{δE}{δb_{j}^{(1)}}

&= \frac{δE}{δy_{1}^{(2)}} \frac{δy_{1}^{(2)}}{δa_{1}^{(2)}} \frac{δa_{1}^{(2)}}{δx_{j}^{(2)}} \frac{δx_{j}^{(2)}}{δa_{j}^{(1)}} \frac{δa_{j}^{(1)}}{δb_{j}^{(1)}}   + 
\frac{δE}{δy_{2}^{(2)}} \frac{δy_{2}^{(2)}}{δa_{2}^{(2)}} \frac{δa_{2}^{(2)}}{δx_{j}^{(2)}} \frac{δx_{j}^{(2)}}{δa_{j}^{(1)}} \frac{δa_{j}^{(1)}}{δb_{j}^{(1)}} \\

&= \left( \frac{δE}{δy_{1}^{(2)}} \frac{δy_{1}^{(2)}}{δa_{1}^{(2)}} \frac{δa_{1}^{(2)}}{δx_{j}^{(2)}} + 
\frac{δE}{δy_{2}^{(2)}} \frac{δy_{2}^{(2)}}{δa_{2}^{(2)}} \frac{δa_{2}^{(2)}}{δx_{j}^{(2)}} \right) \frac{δx_{j}^{(2)}}{δa_{j}^{(1)}} \frac{δa_{j}^{(1)}}{δb_{j}^{(1)}} \\

&= \left( g_{1}^{(2)}ω_{j1}^{(2)} + g_{2}^{(2)}ω_{j2}^{(2)} \right) f(a_j^{(1)}) \\

&= \left( \sum_{i=1} g_{i}^{(2)}ω_{ji}^{(2)} \right) f(a_j^{(1)}) \\

&= g_{j}^{(1)}
\end{align}
```

## 更新分を改めて書く
```math
ω_{kj}^{(l)} = ω_{kj}^{(l)} - \frac{δE}{δω_{kj}^{(l)}},
\: b_{j}^{(l)} = b_{j}^{(l)} - \frac{δE}{δb_{j}^{(l)}}
```

```math
\frac{δE}{δω_{kj}^{(l)}} = g_{j}^{(l)}x_{k}^{(l)},
\: \frac{δE}{δb_{j}^{(l)}} = g_{j}^{(l)}
```
```math
\begin{align}
g_j^{(l)} &= 
\left\{
\begin{array}{ll}
(y_j - t_j) f(a_j^{(l)}) & (l = 最終層) \\
\left( \sum_{i=1} g_{i}^{(l+1)}ω_{ji}^{(l+1)} \right) f(a_j^{(l)}) & (l = 最終層以外)
\end{array}
\right. \\

f(a) &= (1-sigmoid(a))sigmoid(a)
\end{align}
```

# 検定問題サンプル（気になった問題）
http://kentei.ai/introduction/subjecttrend

## AI
行列の掛け算。重み行列は下記の通り。

```math
ω^{(1)} = 
\begin{bmatrix}
ω_{11}^{(1)} & ω_{21}^{(1)} \\
ω_{12}^{(1)} & ω_{22}^{(1)}
\end{bmatrix}
```

なので、pythonのnumpyで扱う際は下記のように宣言する。（例題だと選択肢の2つ目）

```python
import numpy as np

w = np.array([2, 8], [7, 3])
```

## プログラミング
マイナスインデックスとスライスについて再確認。

```bash
>>> list = [x for x in range(10)]
>>> list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# -2は末尾から数えて2個目のインデックス
>>> list[-2]
8

# スライスは[開始インデックス:終了インデックスの1つ後]を範囲指定できる
# 省略した場合は先頭or末尾まで
>>> list[3:6]
[3, 4, 5]

# [末尾から2個目のインデックス:末尾まで]
>>> list[-2:]
[8, 9]
# 上記と同義
>>> list[len(list)-2:]
[8, 9]

# [先頭から:末尾から2個目のインデックスの1つ前]
>>> list[:-2]
[0, 1, 2, 3, 4, 5, 6, 7]
# 上記と同義
>>> list[:len(list)-2]
[0, 1, 2, 3, 4, 5, 6, 7]

# ちなみにスライス指定では範囲外でも先頭or末尾となる
>>> list[-20:]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```

## 数学
例題「主成分分析」の1問目の図がおかしいっぽいので問い合わせ中。

# 実装
実装で気になるところも少しだけだったので合わせて記載。
基本的には上記の数式をコーディングするだけ。

気になった点のみ、メモ書きとして残す。

```python:NNの実装例
import numpy as np

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

# 1層目
x1 = np.array([[1], [2]])
w1 = np.array([[0.4, 0.3], [0.7, 0.6]])
b1 = np.array([[1.0], [1.0]])
a1 = w1.dot(x1) + b1

# 2層目
x2 = sigmoid(a1)
w2 = np.array([[0.2, 0.3], [1, 0.7]])
b2 = np.array([[1.0], [1.0]])
a2 = w2.dot(x2) + b2

# 出力値と正解値
y = sigmoid(a2)
t = np.array([[1], [0]])

X = [x1, x2]
A = [a1, a2]
W = [w1, w2]
B = [b1, b2]

max_layer = len(X)

# 活性化関数の微分
def f(a):
    return (1 - sigmoid(a)) * sigmoid(a)

# 更新式の公式gの実装
def g(l, j):
    if l == (max_layer - 1):
        return (y[j] - t[j]) * f(A[l][j])
    else:
        output = 0
        m = A[l + 1].shape[0]
        for i in range(m):
            ### Wの引数は[j, i]になっていたが、おそらく[i, j]が正しい
            ### このプログラムの例ではWが(2,2)行列なのでどちらでも結果は同じ
            output += g(l + 1, i) * W[l + 1][i, j] * f(A[l][j])
        return output

# パラメーターによる誤差関数の微分
def diff_w(j, k, l):
    return g(l, j) * X[l][k]

def diff_b(j, l):
    return g(l, j)

for _ in range(100):
    for l in range(len(X)):
        for j in range(W[l].shape[0]):
            for k in range(W[l].shape[1]):
                ### 個人的にlも0始まりの方がわかりやすい
                W[l][j, k] = W[l][j, k] - diff_w(j, k, l)
            B[l][j] = B[l][j] - diff_b(j, l)
    A[0] = W[0].dot(X[0]) + B[0]
    X[1] = sigmoid(A[0])
    A[1] = W[1].dot(X[1]) + B[1]
    y = sigmoid(A[1])

## Wは元々転置した状態で初期化しているので、コーディングするときは公式と添え字が転置された状態になる
```

```python:mnist画像に対するテスト実施
import numpy as np
import random

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

## npyからload
xs = np.load('inputs.npy')
ts = np.load('true_values.npy')

for i in range(10):
    index = random.randint(0, 13007)

    x1 = xs[index].flatten().reshape(784, 1)
    w1 = np.load('w1.npy')
    w2 = np.load('w2.npy')
    b1 = np.load('b1.npy')
    b2 = np.load('b2.npy')

    a1 = w1.dot(x1) + b1
    x2 = sigmoid(a1)
    a2 = w2.dot(x2) + b2
    y = sigmoid(a2)
    ## なぜか添え字が200になっていたが、xsとインデックスを揃えるために添え字をindexにした
    t = ts[index]

    if np.argmax(y) == np.argmax(t):
        print("正解")
    else:
        print("不正解")
```

```bash:実行例
## だいたい8割〜10割の正答率になる
$ python NNmnist_exec.py
正解
正解
正解
不正解
正解
正解
正解
正解
正解
正解
```