---
# System prepended metadata

title: 'DL回歸基本功: ep.1 神經網絡'

---

## Perceptron
Perceptron(感知機)由美國學者Frank Rosenblatt在1957年提出来。這種算法作為神經網絡的一個基礎給了後續建構一個很好的架構。

### 原理

```
接受多個輸入信號，輸出一個信號。
而信號只有0或1，當訊號被送至節點的時候，會被乘以固定的權重，並加總在一起。
當總和超過門檻值，就當作1，否則為0。
```
以兩個輸入為例:
![image](https://hackmd.io/_uploads/HJ7n24lZbg.png)
x1和x2在節點上會分別乘上某個(設定過的)權重，而變成(w1x1,w2x2)兩個值，傳往y。y會先加總輸入
$$
y = w_1 x_1 + w_2 x_2
$$
，再繼續乘上權重往後傳。但圖上沒有繼續傳，y則作為輸出結束。

所以我們得到y=w1x1+w2x2的公式，而若y>=門檻值(簡稱b，因此之後換到等號左側統一使用-b)則=1，反之則為0。
所以重寫成正式的就是:
$$
y =
\begin{cases}
1, & \text{if } w_1 x_1 + w_2 x_2 > b \\
0, & \text{otherwise}
\end{cases}
$$

推廣成向量形式，多個輸入下則以矩陣形式為:
$$
Y =
\begin{cases}
1, & \text{if } \mathbf{W} \cdot \mathbf{X} - b > 0 \\
0, & \text{otherwise}
\end{cases}
$$

#### 範例: NAND gate

NAND gate的truth table如下:

| x1 | x2 | y |
|----|----|---|
| 0  |  0 | 1 |
| 0  |  1 | 1 |
| 1  |  0 | 1 |
| 1  |  1 | 0 |

很顯然我們對於perceptron的控制度在於權重W和threshold上，我們可以嘗試控制W來做出期望的邏輯運算。

什麼樣的W和b可以讓輸入輸出呈現以上分布呢?
很顯然有無限多種，但我們可以列出一個例子:
(w1,w2)=(-0.5, -0.5), b=0.7;

| w1x1 | w2x2 | b | y|
|----|----|---|---|
| 0  |  0 | 0>-0.7 |1|
| 0  |  -0.5 | -0.5>-0.7 |1|
| -0.5  |  0 | -0.5>-0.7 |1|
| -0.5  |  -0.5 | -1<-0.7 |0|

當然,(w1,w2)=(-0.2,-0.2), b=-0.3也可以。
| w1x1 | w2x2 | b | y|
|----|----|---|---|
| 0  |  0 | 0>-0.3 |1|
| 0  |  -0.2 | -0.2>-0.3 |1|
| -0.2  |  0 | -0.2>-0.3 |1|
| -0.2  |  -0.2 | -0.4<-0.3 |0|

看上去就是在"代數字"對吧? 沒錯，實際神經網絡的訓練上也相差不遠了，只是差別在於不是隨機的代，而是像玩終極密碼一樣去逼近期望的輸出。

#### 原理: 小結一下
Perceptron由W的矩陣和b(門檻值)來控制，W讓我們控制信號的重要性，側重於哪一個。而b則是調整這個節點通過的容易程度。過高的b會讓訊號都被擋掉而消失，過低則失去意義讓訊號直接往後傳。

* Exercise: Can you use perceptron to  implement XOR gate?
* Furthermore: Try to build a full-adder with perceptron NAND gate in python.

<details>
<summary  style="font-size: 12px;">Reference Ans</summary>
    
```{python}
def NAND(x1, x2, w1=-0.5, w2=-0.5, b=0.7):
    return 1 if (w1*x1+w2*x2-b)>0 else 0
    
def full_adder(x1,x2,cin):
    n0 = NAND(x1,x2)
    n1 = NAND(x1,n0)
    n2 = NAND(x2,n0)
    n3 = NAND(n1,n2)
    n4 = NAND(n3,cin)
    n5 = NAND(n3,n4)
    n6 = NAND(n4,cin)
    S = NAND(n5,n6)
    Cout = NAND(n4,n1)
    return (S, Cout)
```
</details>

### 線性和非線性
其實y=wx-b這個公式我們從國中就非常熟悉，就是平面上的一條直線，w為斜率，b則為截距。
因此perceptron的公式就是想辦法找到一條線畫上去可以把想要的點和不想要的點都分開來。
![image](https://hackmd.io/_uploads/ByxjqBe-Wx.png)
如圖，灰色區域=0，而白色=1。
但這樣的公式注定不是萬能的，他只能受到線性的約束。
如果我們需要彎曲的線來做細緻的劃分，就需要非線性的。

#### Multiple-Layer Perceptron(MLP)

在剛才的exercise中，你也許會發現，好像沒辦法找到一個W矩陣，就做出XOR gate。
因為 XOR 無法用一條線分成兩類，需多條線（多層表示）。
不過聰明的你也許會聯想到，XOR是可以用NAND gate、AND gate和OR gate做出來的。(當然也可以純用NAND)
![image](https://hackmd.io/_uploads/HyBrKIeWbe.png)
```
def XOR(x1,x2):
    S1 = NAND(x1,x2)
    S2 = OR(x1,x2)
    return AND(S1,S2)
```
當我們把每一層gate都視為一層perceptron的節點。
![image](https://hackmd.io/_uploads/SkO1cLxbZl.png)
同樣的我們就能反推去把每個箭頭上的w寫上對應的值，一組新的W就出來了。

至此，似乎我們可以用MLP來表示非線性的空間，但好像哪裡怪怪的?一堆線性怎麼可以疊出非線性的模型呢?
如果沒有非線性的激勵函數，無論疊加多少層神經網絡，最終都只是一個線性變換。
即 $W_2(W_1X) = (W_2W_1)X = W_{new}X$。

## 神經網絡

你也許會看過這張圖:
![image](https://hackmd.io/_uploads/SyxzpLlbZx.png)
請放心，這就是上面的那張圖，只是我們給他一些命名。當然神經網絡(nn)多不是只有三層。
現在我們回想一下一開始的公式:
$$
y =
\begin{cases}
1, & \text{if } w_1 x_1 + w_2 x_2 > b \\
0, & \text{otherwise}
\end{cases}
$$

其中判斷(w1x1+w2x2)-b1)>=0並賦值為1或0是用三元表達式描述的。
現在我們引入h函數(Heaviside step function)來幫我們做這件事。
即: $$
h(z) =
\begin{cases}
1, & z \ge 0 \\
0, & z < 0
\end{cases}
$$

可簡化為:
$$ y = h(\mathbf{w}\cdot \mathbf{x} - b) $$

此時，我們回看剛才三層的MLP，我們用了三層節點作出了XOR gate。
```
def XOR(x1,x2):
    S1 = NAND(x1,x2)
    S2 = OR(x1,x2)
    return AND(S1,S2)
```

現在我們能利用h(x)，也能推出他的一般式:
$$
y = h((c1(h((w1x1+w2x2)-b1))
+c2(h((m1x1+m2x2)-b2))-b3)
$$

太好了，我們終於得到了描述Perceptron和MLP的正確公式!
* perceptron
$$
y = h(\mathbf{w}\cdot \mathbf{x} - b)
$$
* MLP
$$
X^L=h^L(W^LX^{L−1}-b^L)
$$


### Activation Function(激勵函數)
h(x)就是一種激勵函數。它的用途就是把加權輸入(w1x1+w2x2)的總和，做一種分類和轉換，或者說映射。類似於考多少分，被分在了哪個班級。而具體怎麼去分，那就是看激勵函數了。

* 從公式中其實能夠看出，在節點之間，還少畫了激勵函數轉換的節點，而這個節點通常包含在了下一層的輸入之前。ex:$$c1(h((w1\cdot x1+w2\cdot x2)-b1)$$
就是先h再直接做c1的。
![image](https://hackmd.io/_uploads/rJkLGvgW-x.png)

現在我們終於發現了，MLP其實就是一個線性perceptron的遞迴型態，但是這個表達能力其實不是從堆疊線性而來。
關鍵就在於「中間插入了非線性的激勵函數 h(⋅)」。
只要激勵函數是非線性的，整體函數不管疊幾層都會變成非線性，這就是 MLP 之所以強大的根本原因。
但階躍函數h卻存在很大的問題:
![image](https://hackmd.io/_uploads/H1I2lde--x.png)
從圖上可以很直觀的看到函數樣子，仔細想一下回發現，這個函數是""**幾乎處處導數為 0，且在 x=0 不可微的**""，這使得我們可能無法在一次的"嘗試"當中去找到一個梯度去調整我們"猜數字"的策略。
後續我們會在訓練階段談到梯度下降，也就是下一次的猜法由梯度決定，但 h 的梯度永遠是 0，所以模型無法「知道往哪裡調」。

#### Sigmoid/Logistic 函数
因為階躍函數的問題，現代神經網絡引入了許多連續可微的激勵函數來解決:
Sigmoid就是一個根據Logistic regression而來的函數。
$$
\sigma(x) = \frac{1}{1 + e^{-x}}
$$
我們不需要被奇怪的公式所嚇到了。其實我們只是需要他的分布，如圖:
![image](https://hackmd.io/_uploads/HyX0Gul-Zg.png)
這個函數的輸出範圍恰好落在0到1之間，而且隨著離中心越遠，則變化越慢。這樣的曲線恰好能給出平滑的梯度:
 $$\sigma'(x) = \sigma(x) \cdot  (1- \sigma(x))$$
![image](https://hackmd.io/_uploads/ryXim_gWWl.png)
卻也帶來了致命的問題:
1. 輸出不是0對稱，而是同號(都是正的或負的)，喪失對稱性。由於輸出永遠是正值，梯度也永遠同號，造成梯度更新偏向單一方向，這使得參數收斂速度變慢，稱為「非零中心（non-zero-centered）問題」。
2. 圖上紅色區域的梯度可能會變得太小，在多次累積下梯度小到直接不見->**梯度消失問題**最後停止學習。

好處是基於Logistic regression這個統計模型，Sigmoid 仍常被用於二元分類的輸出層，因為它能自然地對應到機率。

但在深度網路的中間(隱藏)層，因為上述缺點，大多被 ReLU 系列取代。

* Exercise: $\sigma'(3)=?$
自己算一遍吧!然後想想看$\sigma'(4)$幾次方後梯度會小到幾乎消失!

#### Tanh函数
這個函數算是作為Sigmoid的替代，嘗試在中間層中解決它的缺點，儘管犧牲了自然機率的解釋性，而不適合做為輸出層。
![image](https://hackmd.io/_uploads/SyY19ueZWg.png)
1. 很顯然的變成了零對稱，並保持S型平滑的曲線。而且隱藏層的平均值還能接近0。

![image](https://hackmd.io/_uploads/SkUX0_xZ-g.png)

2. 然而"梯度消失"依然沒有解決，儘管面積變大，比sigmoid更不容易發生，但還是很容易。

#### ReLU函数
這算是深度學習領域真正的中流砥柱了，終於在它的誕生後解決了梯度消失的問題。
![image](https://hackmd.io/_uploads/S1rDKsgZZe.png)
這個函數也非常的簡單:
$$y=max(0,x)$$總之就是負的就歸零，正的才算數。像是小時候幫大人打麻將一樣，贏了吃紅，輸了也不虧。
如果直接取導，就會發現梯度保持在1，至於小於0的則同樣歸零。這就像是在許多的輸入中，只對於那些正值有反應，負值就完全不理。
因此這個函數的運算非常快，可以達到常數級(可以用C++的union和位移實現)。大幅度的降低了運算需求。
然而現實就是沒有完美的，他同樣有以下問題:
1. Dying ReLU:很直觀的就是如果數值太多負值，那整個模型就死掉了，無法啟動進一步學習。
2. **梯度爆炸**: 因為ReLU的輸出是沒有界的，訓練時若學習率設太高，可能會爆炸性增加。

梯度爆炸的問題，之後還會具體提到。
這些家族基本上都是為了解決Dying ReLU做的努力。


#### Softmax函数
$$
\text{Softmax}(z_i)=\frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}
$$
公式也不難懂，分子是第i個輸入信號的指數函數值，而分母是所有輸入函數的指數函數值之總和。本質上表示了向量中值的離散機率分布，也就是在K個類別中，屬於某樣本的機率分布。
![image](https://hackmd.io/_uploads/B15OZ3xbZx.png)
隨著輸入增加softmax越趨於1，反之則趨於0。
梯度圖:![image](https://hackmd.io/_uploads/rk4II3gW-g.png)
很顯然還是會存在梯度消失的問題。

<details>
<summary  style="font-size: 12px;">未來會提到</summary>
其梯度為一個 Jacobian 矩陣:

$$
\frac{\partial\, \text{softmax}(z)_i}{\partial z_j}
= \text{softmax}(z)_i \left( \delta_{ij} - \text{softmax}(z)_j \right)
$$
$$
J = \frac{\partial\, \mathbf{s}}{\partial \mathbf{z}}
=\begin{bmatrix}
\frac{\partial s_1}{\partial z_1} & \frac{\partial s_1}{\partial z_2} & \cdots & \frac{\partial s_1}{\partial z_K} \\
\frac{\partial s_2}{\partial z_1} & \frac{\partial s_2}{\partial z_2} & \cdots & \frac{\partial s_2}{\partial z_K} \\
\vdots & \vdots & \ddots & \vdots \\
\frac{\partial s_K}{\partial z_1} & \frac{\partial s_K}{\partial z_2} & \cdots & \frac{\partial s_K}{\partial z_K}
\end{bmatrix}= \text{diag}(\mathbf{s}) - \mathbf{s}\,\mathbf{s}^\top
$$
若搭配交叉熵損失（Cross-Entropy Loss），梯度會大幅簡化，使訓練更穩定。

</details>

結論上非常適合多分類任務，有良好的解釋性。
缺點則是因為使用 $𝑒^x$，如果輸入有大值，會造成指數爆炸，需要在資料前處理下功夫剔除極端值。


其餘激活函數將寫在[DL回歸基本功: ep.1-1 附錄:激勵函數](/dfCPzezkRDWjFkPoWXiCtw)中


如果要做更多研究: SiLU、ReLU²

[下篇: DL回歸基本功: ep.2(上) 數據驅動的學習](https://hackmd.io/@dTa8NyAfRcmx_4PYFUqMMg/By5I9neWWx)
###### 參考文獻
[1] Deep Learning from Scratch
[2] Implicit Neural Representations with Periodic Activation Functions
[3] Gaussian Error Linear Units (GELUs)