ゼロから作るDeepLearning2

<img width="200" alt="qiita-square" src="https://images-na.ssl-images-amazon.com/images/I/512ru2i5gyL._SX352_BO1,204,203,200_.jpg">



## 3章【ニューラルネットワーク　〜多次元配列と3層NN〜】

### <font color="Green">Introduction</font>


環境はGoogle Colaboratoryを用いています。

そのためデータセットをGoogleDriveの中のMyDrive配置していることが前提になります。
必要なデータセットは[ここ](https://github.com/oreilly-japan/deep-learning-from-scratch)からダウンロード or Clone してください。

前回はNNの簡単な定義と活性化関数について学習しました。
今回は機械学習について基本的な多次元配列についてさらっと復習して、3層NNを実装していきます。
多次元配列やその他数学的な内容については筆者が数学にバックグラウンドをもつため、簡単な復習やコメントのみに終始する可能性があることをここでお詫びしておきます。

### <font color="Green">多次元配列の復習</font>

多次元配列とは行列のことです。
和に関して線形であり多重線型性をもっているものです。
数学をある程度習った人ならテンソル⊗のことだと考えても良いと思います。

それではnumpyを用いて１次元の配列から復習していきましょう。

```python
import numpy as np
A = np.array([1,2,3,4])
print(A)
np.ndim(A)
A.shape
A.shape[0]

```
配列の次元については`np.ndim()`関数で取得できます。
`shape`はインスタンス変数であり、これはタプル型になっていることに注意しましょう。
このインスタンス変数を理解するには多次元配列について同様の操作をしてみるのがいいでしょう。

```python
B = np.array([[1,2],[3,4],[5,6]])
print(B)
np.ndim(B)
B.shape
```

これは実行すると3×2型の行列になっていることがわかリます。
つまり`shape`は与えられた多次元配列の（行列の）型を表すインスタンス変数です。

また今後主に登場する多次元配列は単に行列であることが多いので、必要を迫られるまでは`shape`などのインスタンス変数も単に行列の型のことを表していると考えます。

また行列の積については`np.dot(A,B)`で求めることができます。

```python
A = np.array([1,2,3,4])
B = np.array([[1,2],[3,4],[5,6],[7,8]])
np.dot(A,B)
>>>array([50, 60])
```
行列の積については特に（実装面だけでなく現実でもそうですが）型に注意してください。

次はいよいよ行列の積を用いて基本的な3層NNを実装していきましょう。


### <font color="Green">3層NNの実装</font>
それでは行列の演算を用いて実装していきましょう。
行列が出てくるのはなぜかというと、計算の様子を簡単に表せるから以外の理由はありません。

以下では下のような簡単なNNを実装していきます。
入力層、隠れ層2層、出力層をもつようなNNになっています。

以下でこれから用いる記号を定義しておきます。
$w_{ij}^{(k)}$などの記号を導入し、これでk層目のi番目のノードからj番目のノードへの重みを表します。
$a_{i}^{j}$で　j層目のi番目のノードの値を表します。

これによるとm各信号のバイアスの和は以下の式で計算されます。

```math
a_{i}^{j} = \sum w_{ij}^{(k)} + b_{i}^j
```
つまり行列の積の形で表現すると第i層目の重み付きの信号の和というのは

```math
A^{(i)} = XW^{(i)} + B^{(i)}
```
と表現することができる。

これを具体的に実装してみよう。

まず1層目から2層目にかけての信号と重みとバイアスは以下のように実装されます。
今後もそうですが行列の型に関してだけは注意しましょう。

```python
import numpy as np

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

X  = np.array([1.0,0.5]) #1層目の入力
W1 =  np.array([[0.1,0.3,0.5],[0.2,0.4,0.6]]) #1層目の暫定の重み
B1 = np.array([0.1,0.2,0.3]) #1層目のバイアス

A1 = np.dot(X,W1) + B1
print(A1)


Z1 = sigmoid(A1)

print(Z1)


```
活性化関数にはシグモイド関数を用いました。
次には第２層から3層にかけての実装を行いましょう。


```python
W2 =  np.array([[0.1,0.4],[0.2,0.5],[0.3,0.6]]) #2層目の暫定の重み
B2 = np.array([0.1,0.2]) #2層目のバイアス

A2 = np.dot(Z1,W2) + B2
Z2 = sigmoid(A2)

print(Z2)
```
それでは最後に2層から出力層にかけての実装を行いましょう。
ここでは活性化関数というものは用いずに恒等写像で送り出します。

```python

W3 =np.array([[0.1,0.3],[0.2,0.4]]) 
B3 = np.array([0.1,0.2])

A3 = np.dot(Z2,W3) + B3
Y = A3

print(A3) #最終的な信号の出力

```

最後に今までの実装をまとめましょう。今後習慣で重みなどは大文字で、バイアスは小文字で表すことにします。
以下のコードを眺めてみてください。


```python
import numpy as np

def init_network(): #あくまで実装を簡潔に見せるための工夫。変数を初期化している。
  network = {} #空のディクショナリを用意する。
  network["W1"] = np.array([[0.1,0.3,0.5],[0.2,0.4,0.6]]) #1層目の暫定の重み
  network["b1"] =  np.array([0.1,0.2,0.3]) #1層目のバイアス
  network["W2"] = np.array([[0.1,0.4],[0.2,0.5],[0.3,0.6]]) #2層目の暫定の重み
  network["b2"] = np.array([0.1,0.2]) #2層目のバイアス
  network["W3"] = np.array([[0.1,0.3],[0.2,0.4]]) 
  network["b3"] = np.array([0.1,0.2])
  
  return network


def forward(network,x): #実質的なNNの働きをする関数。ここが今までの処理をまとめた部分になる。
  W1,W2,W3 =  network["W1"], network["W2"], network["W3"]
  b1,b2,b3 =  network["b1"], network["b2"], network["b3"]
  
  a1 = np.dot(x,W1) + b1
  z1  = sigmoid(a1)
  
  a2 = np.dot(a1,W2) + b2
  z2 = sigmoid(a2)
  
  a3 = np.dot(z2,W3) + b3
  y = a3
  
  return y


network = init_network() #初期化
x = np.array([1.0,0.5])
y = forward(network,x)

print(y)
```


## おわり

次回は出力層の設計について書いていきます。
NNの図を作りたいのですが、何か良いツールをご存知の方いたら教えてください。