2021年10月6日 星期三

機器學習筆記 : 啟動函數 (activation function)

以下是今天閱讀下面三本書中關於人工神經元啟動函數的測試筆記 : 
啟動函數 (activation function, 又稱活化函數或激活函數) 在神經網路中的角色是用來將神經元各輸入的加權總和進行非線性轉換以得到輸出訊號, 使用啟動函數的原因是為了處理常見的非線性問題. 如果不使用啟動函數, 各層神經元所執行的張量點積運算, 不論疊了多少層, 整體而言都還是線性函數, 只能處理線性問題. 而啟動函數 (非線性) 則可以將輸入轉成值域為 [-1, 1] 或 [0, 1] 的輸出, 使神經網路可以從資料中學習到非線性關係.

一個人工神經元內部功能由兩個函數模擬而成, 其一是總和函數 (summation), 負責計算各輸入的加權總和 x (wighted summation, 包含一個輸入固定為 1 的偏權值 b) , 其二是啟動函數 (activation), 將加權總和進行非線性轉換得到輸出 y, 具有兩個輸入的人工神經元結構如下圖所示 : 




感知器或神經網路常用的啟動函數有如下五個 : 


 啟動函數 說明
 Step 輸入為負時輸出為 0, 輸入為正時輸出為 1, 常用於感知器
 Sigmoid f=1/(1+e**(-x)), 值域為 [0, 1], 常用於輸出層 (二元分類)
 Tanh f=tanh(x), 值域 [-1, 1], 常用於輸出層 (二元分類)
 ReLU f=max(0, x), 值域 [0, x], 常用於隱藏層
 Softmax  f=e**x/sum(e**x), 值域 [0, 1], 常用於輸出層 (多元分類)


這五個非線性函數中, Step (步階函數) 主要用在感知器, ReLU (Rectified Linear Unit 的縮寫) 主要用在神經網路的隱藏層, 而 Sigmoid, Tanh, 與 Softmax 三個則常用於輸出層, 三者都是把輸出值壓縮在 [0, 1] 或 [-1, 1] 之間, 其中 [0, 1] 剛好可用來表示機率, 代表信心或可能性, 而 [-1, 1]  則可用於二元分類. Softmax 可說是 Sigmoid 的延伸版, 多元分類的輸出層會採用 Softmax, 而二元分類的輸出層則使用 Sigmoid 或 tanh. 

除了上面所列的五種外, 還有其他幾種啟動函數例如 elu, selu, 與 Softplus 等非線性函數, 但隱藏層最常用的是 ReLU, 因為在計算上它效能最好, 且層數再多也沒有梯度消失問題. 

下面用 Matplotlib 繪製各個啟動函數, 關於 Matplotlib 用法參考 :



1. 步階函數 (step function) : 

步階函數的數學定義如下 : 




Python 程式碼如下 : 

def step(x):
    if x > 0:
        return 1
    else:
        return 0

但此函數只能用於純量, 無法用在張量. 可傳入張量的步階函數要如下 :

def step(x):
    y=x > 0
    return y.astype(np.int)

亦可簡化為一行指令碼 : 

def step(x):
    return np.array(x > 0, dtype=np.int)

可用 Matplotlib 繪製步階函數圖形 : 


測試 1 : 繪製步階函數圖形 [看原始碼]

import numpy as np
import matplotlib.pyplot as plt

def step(x):
    return np.array(x > 0, dtype=np.int)

plt.title('Step Function')
plt.xlabel('X')
plt.ylabel('Y')
x=np.arange(-5, 5, 0.1)     # x 軸數據為 [-5, 5] 間隔 0.1 的向量
y=step(x)           # y 軸數據為 x 的步階函數值
plt.plot(x, y)
plt.show()

此例用 Numpy 的 arange() 函數產生 x 軸向量, 傳給 step_function() 產生輸出, 結果如下 :




可見步階函數在 x=0 時輸出有一個垂直的 0 到 1 變化, 就像階梯一樣. 使用步階函數作為啟動函數的人工神經元稱為感知器 (perceptron).


2. Sigmoid 函數 : 

Sigmoid 函數的數學定義如下 : 




Sigmoid 是自然指數的合成函數, 它會將輸入轉換成 0~1 之間的數, 與機率函數的值域相同, 與大腦神經元作用類似. 可用 Numpy 的 exp() 函數來實作, 程式碼如下 :

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

下面範例以 Matplotlib 繪製其圖形 :


測試 2 : 繪製 Signoid 函數圖形 [看原始碼]

import numpy as np
import matplotlib.pyplot as plt

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

plt.title('Sigmoid Function')
plt.xlabel('X')
plt.ylabel('Y')
x=np.arange(-5, 5, 0.1)
y=sigmoid_function(x)
plt.plot(x, y)
plt.show()

結果如下 : 




Sigmoid 的中文是乙狀結腸, 果然有點像 "乙" 字. 可見 Sigmoid 函數在 x=0 附近有一個近似線性的區域, 但很快地在 x>2 與 x <-2 後進入飽和區, 輸出會收斂至 0 或 1. 由於飽和區的關係, 使用 Sigmoid 的網路會有逆傳遞時梯度消失問題 (因為飽和區躺平使得梯度值太小), 層數越多梯度消失情況越嚴重, 因此無法串接太多層. 

Sigmoid 函數通常用在輸出層, 因為此函數的微分 (梯度) 最大值為 0.25, 在將誤差值反向傳遞時只要經過幾層的傳遞梯度就會消失, 因此隱藏層通常會使用 ReLU 函數. Sigmoid 的微分結果可以用 Sigmoid 本身合成 :




其推導參考下面這篇 :


下面範例繪製 Sigmoid 微分函數的圖形 : 


測試 3 : 繪製 Signoid 微分函數圖形 [看原始碼]

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    return 1/(1 + np.exp(-x))                       # 定義 Sigmoid 

def sigmoid_diff(x):                                 
    return sigmoid(x) * (1 - sigmoid(x))     # 定義 Sigmoid 的微分

plt.title('Sigmoid Function')
plt.xlabel('X')
plt.ylabel('Y')
x=np.arange(-5, 5, 0.1)
y=sigmoid_diff(x)
plt.plot(x, y)
plt.show()

結果如下 :




可見 Sigmoid 的微分 (梯度) 函數最高值為 0.25, 經過三層倒傳遞後就會變得很小了. 

解決 Sigmoid 梯度消失的辦法是先將輸入特徵先經過 feature scalling 程序將特徵映射到 Sigmoid 的非飽和區. 另外, 批次正規化法 (batch normalization, BN) 作法則不僅是在輸入前做 feature scalling, 它在每一層的輸出後面都做 feature scalling, 將特徵重新分配為平均值為 0, 標準差為 1 的分布上. 透過 BN 讓 Sigmoid 也可以用在深度神經網路上. 

Step 與 Sigmoid 的值域都是 [0, 1], 下面範例是將兩者疊在一起來比較 : 


測試 4 : Step 與 Signoid 函數圖形疊在一起 [看原始碼]

import numpy as np
import matplotlib.pyplot as plt

def step(x):
    return np.array(x > 0, dtype=np.int)

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

plt.title('Sigmoid & Step Function')
plt.xlabel('X')
plt.ylabel('Y')
x=np.arange(-5, 5, 0.1)     # X 軸資料點
y1=sigmoid(x)                  # Y 軸資料點 (Sigmoid 函數)
y2=step(x)                         # Y 軸資料點 (Step 函數)
plt.plot(x, y1, 'b-', x, y2, 'r--')     # Sigmoid (y1) 用藍實線, Step 用紅虛線繪製
plt.show()

此例用 y1 與 y2 分別表示 Sigmoid 與 Step 函數的 Y 軸數值, 呼叫 plot() 時可將個別的 X-Y 對應與格式傳入同時繪製兩條曲線, 結果如下 : 



可見 Sigmoid 可視為 Step 平滑化的一種函數, 它會隨 x 變數產生連續性的輸出 (用於神經網路), 而 Step 函數則是產生非 0 即 1 的非連續性輸出 (用於感知器). 神經網路在學習時, Sigmoid 的平滑特性具有重要意義. 


3. ReLU (Recitified Linear Unit) 函數 : 

ReLU 函數的數學定義如下 :



此函數在 x<0 時與 Step 函數一樣輸出 0, 但在 x>=0 時則輸出 x (線性), 這是 ReLU 中有 Linear 的原因, 但實際上 ReLU 是一個非線性函數, 不是線性函數. 由於 ReLU 在 x<0 時輸出 0, 會懲罰負的輸入, 因此若輸入資料含有負數就不適合使用 ReLU. 

程式碼可用 Numpy 的 np.maximum() 函數來實作 :

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

下面範例以 Matplotlib 繪製 ReLU 函數圖形 :


測試 5 : 繪製 ReLU 函數圖形 [看原始碼]

import numpy as np
import matplotlib.pyplot as plt

def relu(x):
    return np.maximum(0, x)      # 定義 ReLU 函數 (傳回與 0 比較之最大值)

plt.title('ReLU Function')
plt.xlabel('X')
plt.ylabel('Y')
x=np.arange(-5, 5, 0.1)
y=relu(x)
plt.plot(x, y)
plt.show()

結果如下 : 




在 CNN 神經網路中, ReLU 被用在卷積層與全連接層的啟動函數, 因為它比 Sigmoid 容易計算 (不需要調用指數運算), 且不會因為層數過深產生梯度消失問題而導致無法訓練 (例如 AlexNet 比 LeNet 層數還要深就是因為 AlexNet 使用了 ReLU, 而 LeNet 則使用了 Sigmoid). 


4. Softmax 函數 : 

Softmax 是機率論中的正規化指數函數, 它可將一個 n 維實數向量壓縮成另一個實數向量, 使得其每一個元素的值域在 [0, 1] 區間, 且元素之和為 1, 如同一個機率函數, 其數學定義為 : 




亦即先取輸入之自然指數, 然後除以所有輸入之自然指數之和, 這是正規化之自然指數, 參考 :


可用 Numpy 的 np.exp() 指數函數來實作程式碼 :

def softmax(x):
    return np.exp(x)/sum(np.exp(x))

下面範例以 Matplotlib 繪製 Softmax 函數圖形 :


測試 5 : 繪製 Softmax 函數圖形 [看原始碼]

import numpy as np
import matplotlib.pyplot as plt

def softmax(x):
    return np.exp(x)/sum(np.exp(x))     # 定義 Softmax 函數

plt.title('Softmax Function')
plt.xlabel('X')
plt.ylabel('Y')
x=np.arange(-5, 5, 0.1)
y=softmax(x)
plt.plot(x, y)
plt.show()

結果如下 : 




可見 Softmax 在 x=0 附近以及小於 0 區域的值非常接近 0, x > 0 以後則近乎指數上升, 似乎與 ReLU 有點類似, 但差別是 ReLU 的值域無上限, 但 Softmax 則限制在 [0, 1] 之間. Softmax 通常用在神經網路的輸出層作為多元分類之用, Softmax 的多個輸出代表多個類別 (多元), 其中機率最高的就是學習結果. 例如 :


測試 6 : 用 Softmax 進行向量空間轉換 [看原始碼]

import numpy as np

def softmax(x):
    return np.exp(x)/sum(np.exp(x))

x=np.array([1, 5, 2, 0, 6, 7])      # 輸入向量
y=softmax(x)                             # 輸出向量
print(y)
print("Sum=", sum(y))               # 輸出向量總和

結果如下 : 

[1.63793117e-03 8.94280120e-02 4.45235855e-03 6.02561205e-04
 2.43090540e-01 6.60788597e-01]
Sum= 1.0   

可見原本在 [0, 10] 之間的數被壓縮轉換為 [0, 1], 且轉換後各元素總和為 1. 

參考 :



5. 雙曲正切 tanh 函數 : 

雙曲正切函數 tanh 在 RNN 中很常用, 其數學定義式就是雙曲正弦除以雙曲餘弦 :




參考 :


可用 Numpy 的 np.tanh() 指數函數來實作程式碼 :

def tanh(x):
    return np.tanh(x)

其實也可直接呼叫 np.tanh(), 不需要自訂函數 tanh(), 此處是為了與上面範例一致. 

下面範例以 Matplotlib 繪製 tanh 函數圖形 :


測試 7 : 繪製 tanh 雙曲正切函數圖形 [看原始碼]

import numpy as np
import matplotlib.pyplot as plt

def tanh(x):
    return np.tanh(x)

plt.title('Tanh Function')
plt.xlabel('X')
plt.ylabel('Y')
x=np.arange(-5, 5, 0.1)
y=tanh(x)
plt.plot(x, y)
plt.show()

結果如下 : 



可見 tanh 有與 Sigmoid 類似的平滑曲線, 不同的是 tanh 函數的輸出包含負數, 其值域為 [-1, 1]. 相同之處是由於對兩端有飽和區, 所以也有梯度消失問題. 

沒有留言 :