全連線神經網路學習筆記

語言: CN / TW / HK

全連線神經網路

前饋神經網路

包含的層:

  • 線性層和卷積層:這兩種層對輸入進行線性計算。層內維護著線性運算的權重
  • 啟用層:這層對資料進行非線性運算。非線性運算時可以逐元素非線性運算的,也可以是其它類習慣的非線性運算
  • 歸一化層:根據輸入的均值和方差對資料進行歸一化,使得資料的範圍在一個相對固定的範圍內
  • 池化層和視覺層:這兩種層和資料重取樣有關,包括對資料進行下采樣(就是隔幾個資料採一個數據)、上取樣(把一個數據複製出很多份)和重新排序。
  • 丟棄層:在輸入中隨機選擇一些輸出
  • 補齊層:採用迴圈補齊等方法讓資料變多

一般的簡單表示方式:

使用 torch.nn.Sequential 類搭建前饋神經網路

torch.nn.Sequential 類是由 torch.nn.Module 類派生得到的。

torch.nn.Moudle 類及其子類有以下用途。

  • 表示一個神經網路 例如 torch.nn.Sequential 類可以表示一個前饋神經網路
  • 表示神經網路的一個層 例如上次說的 torch.nn.Linear 就是表示神經網路的一個層的線性連線部分,它是一種線性層; torch.nn.ReLU 類表示逐元素計算max(·, 0),它是一種啟用層。
  • 表示損失。 torch.nn.MSELoss 類、 torch.nn.L1Loss 類、 torch.nn.SmoothL1Loss 類等等
from torch.nn import Linear, ReLU, Sequential

net = Sequential(Linear(3, 4), ReLU(), Linear(4, 2), ReLU())
print(net)

輸出:

Sequential(
  (0): Linear(in_features=3, out_features=4, bias=True)
  (1): ReLU()
  (2): Linear(in_features=4, out_features=2, bias=True)
  (3): ReLU()
)

全連線層和全連線神經網路

全連線層的意思

所謂全連線層,就是指一個神經元組成的層所有輸出和該層的所有輸入都連線,即每個輸入都會影響所有的神經元輸出

注意:一般來說兩個線性層之間可能會有一個啟用層,例如 ReLU 之類的啟用函式,這個計算是非線性的,如果,如果把它分開來看的話啟用層可能就不算全連線層,但是一般情況下我們把線性函式和啟用函式合起來看就行了。

全連線神經網路的意思

全連線神經網路是僅由全連線層組成的前饋神經網路。

全連線神經網路是一種特殊的前饋神經網路

非線性啟用

非線性啟用的必要性

如果一個神經網路沒有啟用層,所有運算都是線性計算,整個網路就是一個線性組合,這樣的神經網路就發揮不出它的優勢

啟用層分為兩大類

  1. 基於逐元素非線性運算的啟用層。對於這類啟用層,一個稱為“啟用函式”的非線性函式對張量進行逐元素運算。例如可以對張量求(x, max),或是逐元素求expit()。
  2. 多元素組合運算的啟用層,對於這類啟用層,並不是“啟用函式”對張量逐元素運算,而是利用多個元素的值聯合計算。例如對某些元素聯合起來進行softmax()運算。

逐元素啟用

分為三類:

  • S形啟用
  • 單側啟用
  • 皺縮啟用

S型啟用

S型啟用函式把(-∞, +∞)範圍的值對映到一個有限的閉區間裡。從這個意義來看,S型啟用可以有效控制輸出的範圍。但是S型啟用函式常常在輸入的絕對值比較大的時候導數為零,從而導致梯度消失

啟用函式 值域 torch.nn.Modul 的子類
\(soft z = {z \over {1+|z|}}\) (-1, 1) torch.nn.Softsign
\(expitz = {1 \over {1+exp(-z)}}\) (0, 1) torch.nn.Sigmoid
\(tanh z = {exp(z) - exp(-z) \over {exp(z) + exp(-z)}}\) (-1, 1) torch.nn.Hardtanh
\(hardtanh z = \begin{cases} 1, & z > 1 \\ z, & -1 \leq z \leq 1 \\ -1, & z > 6 \end{cases}\) [-1, 1] torch.nn.Hardtanh
\(relu6 z = \begin{cases} 0, & z < 0 \\ z, & 0 \leq z \leq 6 \\ 6, & z > 6 \end{cases}\) [0, 6] torch.nn.ReLU6

它們的影象在經過放縮和平移之後完全一樣。對自變數的的放縮和平移往往可以通過放縮和偏移之前層的權重抵消,上面這幾個函式的主要區別為輸出範圍不同。

單側啟用函式

單側啟用函式一般是把(-∞, +∞)對映到(c, +∞)(有例外),採用這種啟用函式後,比較大的值基本不變,而比較小的值就基本被拋棄了。這樣的做法能夠讓比較多的輸入有梯度,大大緩解了梯度消失的問題。但是它並不能完全將輸出控制在一個範圍內,並且會讓輸出的均值不為0,另外,這樣的函式往往是通過分段實現的,從數學意義上來看在分段點上可能沒有導數,這回引起不便。

Pytorch提供的單側啟用函式:

啟用函式 值域 torch.nn.Modul 的子類
\(relu z = \begin{cases} 0, & z < 0 \\ z, & z \geq 0 \end{cases}\) [0, +∞) torch.nn.ReLU
\(leakyrelu (z; a) = \begin{cases} az, & z < 0 \\ z, & z \geq 0 \end{cases}\) (-∞, +∞) torch.nn.LeakyReLU
torch.nn.RReLU
torch.nn.PReLU
\(threshold(z; \lambda, \nu) = \begin{cases} \nu, & z < \lambda \\ z, & z \geq \lambda \end{cases}\) \(\{\nu\}\) \(\bigcup\) [ \(\lambda, +∞\) ) torch.nn.Threshold
\(selu (z;\sigma, \alpha) = \begin{cases} \sigma\alpha(exp(z)-1), & z < 0 \\ \sigma z, & z \geq 0 \end{cases}\) [- $ \sigma\alpha $ , +∞) torch.nn.ELU
torch.nn.SELU
\(softplus (z;\beta) = {{1 \over \beta} ln(1 + exp(\beta z))}\) (0, +∞) torch.nn.Softplus
$ln expit(z) = -ln(1+exp(-z)) $ (-∞, 0) torch.nn.LogSigmoid

基於斜坡函式relu()函式的啟用層是最基本的啟用層,但是這個啟用層有一個明顯的缺陷:它對負輸入的輸出為常數,這會導致很大範圍內沒有導數,很可能會嚴重的影響權重的求解。為了解決這個問題,Leaky ReLU、PReLU和RReLU。這三種啟用在負輸入的時候還有一個小的正導數α*(0 < α < 1),起到壓縮功能,他們的區別如下:

  • Leaky ReLU:構造類例項的時候需要傳入α的值,一旦傳入,不可更改(預設為0.01)
  • RReLU:構造時需要傳入兩個引數lower和upper,α將是lower和upper間均勻分佈的隨機數,lower的值預設為1/8,upper的預設值為1/3
  • PReLU:斜率α作為一個可優化的值,將在確定權重時一併確定。並且每個元素九可以使用不同的權重值

皺縮啟用

皺縮啟用用的比較少,這裡就先不記了

網路結構的選擇

欠擬合與過擬合

欠擬合:由於網路複雜性不夠,導致網路不能很好的完成任務

過擬合:網路複雜性過大,導致網路錯誤地將噪聲帶來的影響引入到網路中。這樣在沒有見過的資料中就會引發錯誤

從左至右分別是欠擬合,正常擬合,過擬合

這兩種情況都會導致神經網路在新的資料上的效能(又稱為“泛化能力”)變差。在給特定訓練資料上進行有監督學習得到的網路,在新的資料上難免會出現差錯(generalization error)。這裡,泛化差錯可分為“偏差差錯”(bias)、“方差差錯”(variance)、“噪聲”(noise)。偏差差錯是由於網路的缺陷導致網路不能正確完成目標的差錯。一般而言,網路越複雜,偏差差錯越小。偏差差錯過大,就會出現欠擬合。方差差錯是由於訓練模型使用的資料和新的資料有一定的差別,訓練過的網路從訓練資料上學習到了在新的資料上並不滿足的性質,從而導致差錯。一般而言,對於固定的訓練資料,網路越簡單,方差差錯越小。方差差錯過大,就會出現過擬合。噪聲則是這個系統中沒有辦法消除的部分。由於方差差錯和偏差差錯隨模型複雜度的變化趨勢相反,總泛化差錯隨著模型複雜度先變小再增大。在理想情況下,應當選擇複雜度合適的模型,使得總差錯最小,這就需要在偏差差錯和方差差錯之間進行折中。如果經判斷得知當前偏差差錯過大,發生了欠擬合,則可以試圖通過增加網路層數、每層神經元個數等手段,使得網路變複雜;如果經判斷得知當前方差差錯過大,發生了過擬合,在訓練資料不變的情況下,可以試圖通過減小網路層數、每層神經元個數等手段,使得網路變簡單。

訓練集、驗證集和測試集

訓練集:用來計算權重值;

驗證集:用來判定是否出現欠擬合或者過擬合,並確定網路結構或者控制模型複雜程度的引數

測試集:用來評價最終結果

一般這三者比例是 60%、20%、20%

簡單說就是學習的資料越少學習的差錯之和也就越小

驗證曲線:在驗證集上的差錯隨著訓練資料條目數的變化稱為驗證曲線

學習曲線:在訓練集上的差錯隨著訓練資料條目數的變化稱為驗證曲線

偏差方差分析利用驗證學習曲線和學習曲線,可以判斷網路是否出現了欠擬合或過擬合

  • 如果某種結構的網路的學習曲線和驗證曲線都收斂到同一個比較大的差錯值,通過改大網路可以使得學習曲線和驗證曲線收斂到的差錯值變小,那麼在改大網路前就應該出現了高偏差差錯,出現了欠擬合。
  • 如果某種結構的網路的學習曲線和驗證曲線最終值差別較大,通過改小網路可以使得這個差別變小,那麼在改小網路前就應該出現了高方差差錯,出現了過擬合。

總結:

欠擬合 過擬合
泛化差錯主要來源 偏差差錯 方差差錯
模型複雜度 過低 過高
學習曲線和驗證曲線特徵 收斂到比較大的差錯值 兩個曲線之間差別大
解決方案 增加模型複雜度 減小模型複雜度或增大訓練集

例子:基於全連線網路的非線性迴歸

資料生成和資料集分割

import torch
torch.manual_seed(seed=0)# 固定隨機數種子,這樣生成的資料是確定的
sample_num=1000 # 生成樣本數
features=torch.rand(sample_num,2)*12-6 # 特徵資料
noises=torch.randn(sample_num)
def himmelblau(x):
    return(x[:,0] **2 + x[:,1]-11)**2 +(x[:,0] + x[:,1] **2-7)**2
hims=himmelblau(features)*0.01
labels=hims + noises # 標籤資料

train_num,validate_num,test_num=600,200,200 # 分割資料
train_mse=(noises[:train_num] **2).mean()
validate_mse=(noises[train_num:-test_num] **2).mean()
test_mse=(noises[-test_num:] **2).mean()
# MSE演算法這裡吧預測值當作0,平方的平均值就是資料的MSE
print('真實:訓練集MSE={:g},驗證集MSE={:g},測試集MSE={:g}'.format(train_mse,validate_mse,test_mse))

輸出:

真實:訓練集MSE=0.918333,驗證集MSE=0.902182,測試集MSE=0.978382

確定網路結構並訓練網路

作為開始,我們考慮3層神經網路,前2個隱含層分別有6個神經元和2個神經元,並使用邏輯函式啟用;最後一層輸出有一個神經元,沒有非線性啟用,利用 torch.nn.Sequential 類來搭建這個神經網路

import torch.nn as nn
# 指定隱含層數
hidden_features = [6, 2]
layers = [nn.Linear(2, hidden_features[0]), ]

for idx, hidden_feature in enumerate(hidden_features) :
    layers.append(nn.Sigmoid())
    next_hidden_feature = hidden_features[idx + 1] \
        if idx + 1 < len(hidden_features) else 1
    layers.append(nn.Linear(hidden_feature, next_hidden_feature))
print(layers)
net = nn.Sequential(*layers)
print(f'神經網路為{format(net)}')

# 在3.5版本開始,python對星號增加新的適用場景,即在元組、列表、集合和字典內部進行對可迭代引數直接解包,
# 這裡需要一再強調的是,這裡是在上述四個場景下才可以對可迭代引數直接解包,
# 在其他場景下進行可迭代物件的星號解包操作時不允許的。

輸出:

[Linear(in_features=2, out_features=6, bias=True), Sigmoid(), Linear(in_features=6, out_features=2, bias=True), Sigmoid(), Linear(in_features=2, out_features=1, bias=True)]
神經網路為Sequential(
  (0): Linear(in_features=2, out_features=6, bias=True)
  (1): Sigmoid()
  (2): Linear(in_features=6, out_features=2, bias=True)
  (3): Sigmoid()
  (4): Linear(in_features=2, out_features=1, bias=True)
)
import torch.optim
optimizer = torch.optim.Adam(net.parameters())

criterion = nn.MSELoss()

train_entry_num = 600 # 選擇訓練樣本數

n_iter = 100000 # 最大迭代次數
for step in range(n_iter):
    outputs = net(features)
    # 去掉所有維度為1的維度
    preds = outputs.squeeze()

    loss_train = criterion(preds[:train_entry_num], labels[:train_entry_num])
    loss_validate = criterion(preds[train_num: -test_num], labels[train_num: -test_num])

    if step % 1000 == 0:
        print('#{} 訓練集MSE = {:g},驗證集MSE={:g}'.format(step, loss_train, loss_validate))
    
    optimizer.zero_grad()
    loss_train.backward()
    optimizer.step()

print(f'訓練集MSE = {loss_train}, 驗證集MSE={loss_validate}')

輸出一部分:

#96000 訓練集MSE = 1.04245,驗證集MSE=1.06843
#97000 訓練集MSE = 1.04209,驗證集MSE=1.06864
#98000 訓練集MSE = 1.04173,驗證集MSE=1.06773
#99000 訓練集MSE = 1.04131,驗證集MSE=1.0668
訓練集MSE = 1.040966510772705, 驗證集MSE=1.0663131475448608
outputs = net(features)
preds = outputs.squeeze()
loss = criterion(preds[-test_num:], labels[-test_num:])
print(loss)

輸出:

tensor(1.0991, grad_fn=<MseLossBackward0>)