【動手學深度學習】第四章筆記:多層感知機、權重衰減、暫退法、數值穩定性和模型初始化、環境和分佈偏移

bringlu發表於2023-04-25

為了更好的閱讀體驗,請點選這裡

4.1 多層感知機

4.1.1 隱藏層

由於仿射變換中的線性是一個很強的假設,因此導致了線性模型可能會不適用。線性意味著單調假設:任何特徵的增大都會導致模型輸出的增大或者模型輸出的減小。

但是違反單調性的例子比比皆是。除此之外,分類任務中,僅依託畫素強度分類也很不合理。由於任何畫素的重要性都以複雜的方式取決於該畫素周圍的值。對於深度神經網路,用觀測資料來聯合學習隱藏層表示和應用於該表示的線性預測器。

因此可以在網路中加入隱藏層。把前 \(L-1\) 層看作表示,把最後一層看作線性預測器。這種架構通常稱為多層感知機。但是具有全連線層的多層感知機的引數開銷可能太過巨大。

用矩陣 \(\boldsymbol{X} \in \mathbb{R}^{n\times d}\) 來表示有 \(n\) 個樣本的小批次,其中每個樣本具有 \(d\) 個輸入特徵。對於具有 \(h\) 個隱藏單元的單隱藏層多層感知機,用 \(\boldsymbol{H} \in \mathbb{R}^{n\times h}\) 表示隱藏層的輸出,稱為隱藏表示(hidden representation)。在數學或程式碼中,\(\boldsymbol{H}\) 也稱為隱藏層變數(hidden layer variable)或隱藏變數(hidden variable)。不妨設當前有一個一層隱藏層以及一層輸出層的網路,由於隱藏層和輸出層都是全連線的,所以我們有隱藏層權重 \(\boldsymbol{W}^{(1)} \in \mathbb{R}^{d \times h}\) 和隱藏層偏置 \(\boldsymbol{b}^{(1)} \in \mathbb{R}^{1 \times h}\) 以及輸出層權重 \(\boldsymbol{W}^{(2)} \in \mathbb{R}^{h \times q}\) 和輸出層偏置 \(\boldsymbol{b}^{(2)} \in \mathbb{R}^{1 \times q}\)。形式上,我們按如下方式計算單隱藏層多層感知機的輸出 \(\boldsymbol{O} \in \mathbb{R}^{n \times q}\)

\[\begin{align} \boldsymbol{H} &= \boldsymbol{XW}^{(1)} + \boldsymbol{b}^{(1)} \\ \boldsymbol{O} &= \boldsymbol{HW}^{(2)} + \boldsymbol{b}^{(2)} \end{align} \]

注意,上述模型本質上和單個線性層相同。原因在於:

\[\boldsymbol{O} = (\boldsymbol{XW}^{(1)} + \boldsymbol{b}^{(1)})\boldsymbol{W}^{(2)} + \boldsymbol{b}^{(2)} = \boldsymbol{XW}^{(1)}\boldsymbol{W}^{(2)} + \boldsymbol{b}^{(1)}\boldsymbol{W}^{(2)} + \boldsymbol{b}^{(2)} \]

所以,如果要搞多層架構的話,必須在仿射變換之後對每個隱藏單元應用非線性的啟用函式(activation function)\(\sigma\)。啟用函式的輸出(例如,\(\sigma(\cdot)\))稱為啟用值(activation)。這樣就不會再退化到線性模型了,所以有:

\[\begin{align} \boldsymbol{H} &= \sigma(\boldsymbol{XW}^{(1)} + \boldsymbol{b}^{(1)}) \\ \boldsymbol{O} &= \boldsymbol{HW}^{(2)} + \boldsymbol{b}^{(2)} \end{align} \]

出於記號習慣的考量,我們定義非線性函式 \(\sigma\) 也以按行的方式作用於其輸入,即一次計算一個樣本。構建多層感知機,可以繼續堆疊這樣的隱藏層。

多層感知機可以透過隱藏神經元,捕捉到輸入之間複雜的相互作用,這些神經元依賴於每個輸入的值。我們可以很容易地設計隱藏節點來執行任意計算。而且,雖然一個單隱層網路能學習任何函式,但並不意味著我們應該嘗試使用單隱藏層網路來解決所有問題。事實上,透過使用更深(而不是更廣)的網路,可以更容易地逼近許多函式。

4.1.2 啟用函式

1. ReLU

整流線性單元(rectified linear unit, ReLU)式子如下:

\[\text{ReLU}(x) = \max(x, 0) \]

ReLU 函式透過將相應的啟用值設為 \(0\),僅保留正元素並丟棄所有負元素。

這裡程式碼在畫圖的時候很有趣,如下:

x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.relu(x)
d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))

y.detach() 那裡修改為 y.data 也可以過編譯,原因是 pyplot.plot(x,y) 函式中的 x,y 需要用兩個 numpy 型別的變數,而 tensor 變數如果有梯度/梯度追蹤,那麼無法轉換成 numpy 型別的變數。如果這裡是兩個沒有梯度/梯度追蹤的 tensor 變數,則會被型別轉化成 numpy 型別。

ReLU 函式的導數如下:

\[\frac{\mathrm{d} \text{ReLU}(x)}{\mathrm{d} x} = \begin{cases} 0,& x<0 \\ 0~或者~不可導,&x=0 \\ 1,& x>0 \end{cases} \]

當輸入值精確等於 \(0\) 時,ReLU 函式不可導。此時,預設使用左邊的導數,即當輸入為 \(0\) 時,導數為 \(0\)。原書中說,可以忽略這種情況,因為輸入可能永遠都不會是 \(0\)

使用 ReLU 的原因是其求導表現特別好:要麼讓引數消失,要麼讓引數透過。這使得最佳化表現得更好,並且ReLU減輕了困擾以往神經網路的梯度消失問題。

也有一些變體,比如引數化 ReLU(parameterized ReLU, pReLU)函式,該變體為 ReLU 新增了一個線性項,因此即使引數是負的,某些資訊仍然可以透過:

\[\text{pReLU}(x) = \max(0, x) + \alpha \min(0, x) \]

2. sigmoid 函式

對於一個定義域在 \(\mathbb{R}\) 中的輸入,sigmoid 函式將輸入變換為區間 \((0,1)\) 上的輸出。因此,sigmoid 通常稱為擠壓函式(squashing function):它將範圍 \((-\infin, \infin)\) 中的任意輸入壓縮到區間 \((0,1)\) 中的某個值:

\[\text{sigmoid}(x) = \frac{1}{1+\exp(-x)} \]

sigmoid 函式是一個平滑的、可微的閾值單元近似。

它的導數為:

\[\frac{\mathrm{d}}{\mathrm{d}x} \text{sigmoid}(x) = \frac{\exp(-x)}{(1 + \exp(-x))^2} = \text{sigmoid}(x) (1 - \text{sigmoid}(x)) \]

當輸入為 \(0\) 時,sigmoid 函式的導數達到最大值 \(0.25\); 而輸入在任一方向上越遠離 \(0\) 點時,導數越接近 \(0\)

3. tanh 函式

與 sigmoid 函式類似,tanh(雙曲正切)函式也能將其輸入壓縮轉換到區間 \((-1,1)\) 上。tanh函式的公式如下:

\[\tanh(x) = \frac{1 - \exp(-2x)}{1 + \exp(-2x)} \]

當輸入在 \(0\) 附近時,tanh 函式接近線性變換。函式的形狀類似於 sigmoid 函式,不同的是 tanh 函式關於座標系原點中心對稱。

tanh 函式的導數是:

\[\frac{\mathrm{d}}{\mathrm{d}x} \tanh(x) = 1 - \tanh^2(x) \]

當輸入接近 \(0\) 時,tanh 函式的導數接近最大值 \(1\)。輸入在任一方向上越遠離 \(0\) 點,導數越接近 \(0\)

課後題

第四道課後題有點意思,因為我沒看懂題意。在英文原書的網址 MLP 中,我發現原問題是這麼問的:

Assume that we have a nonlinearity that applies to one minibatch at a time, such as the batch normalization (Ioffe and Szegedy, 2015). What kinds of problems do you expect this to cause?

引用別人的答案:資料可能會被劇烈地拉伸或者壓縮,可能會導致分佈的偏移,並且與後面的神經元對接後可能會損失一定的特徵。

4.2 多層感知機的從零開始實現

這裡結合 PyTorch 的 optimizer 和自己的引數的方法很有趣。

num_inputs, num_outputs, num_hiddens = 784, 10, 256

W1 = nn.Parameter(torch.randn(num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))

params = [W1, b1, W2, b2]
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr)

這裡是把 nn.Parameter 類打包成一個 list 丟進 SGD 的引數中。

另外在 numpy 或者是 PyTorch 中,可以用 @ 來表示矩陣乘法。

練習(6):如果想要構建多個超引數的搜尋方法,請設計一個聰明的策略:

  1. 手調(Babysitting)
  2. 網格搜尋(Grid Search)
  3. 隨機搜尋(Random Search)

4.4 模型選擇、欠擬合和過擬合

機器學習的目標是發現模式(pattern)。更正式地說,我們的目標是發現某些模式,這些模式捕捉到了我們訓練集潛在總體的規律。如果成功做到了這點,即使是對以前從未遇到過的個體,模型也可以成功地評估風險。如何發現可以泛化的模式是機器學習的根本問題。

困難在於,當我們訓練模型時,我們只能訪問資料中的小部分樣本。當我們使用有限的樣本時,可能會遇到這樣的問題: 當收集到更多的資料時,會發現之前找到的明顯關係並不成立。

將模型在訓練資料上擬合的比在潛在分佈中更接近的現象稱為過擬合(overfitting),用於對抗過擬合的技術稱為正則化(regularization)。

4.4.1 訓練誤差和泛化誤差

訓練誤差(training error)是指模型在訓練資料集上計算得到的誤差。泛化誤差(generalization error)是指,模型應用在同樣從原始樣本的分佈中抽取的無限多資料樣本時,模型誤差的期望。問題是,我們永遠不能準確地計算出泛化誤差。這是因為無限多的資料樣本是一個虛構的物件。在實際中,我們只能透過將模型應用於一個獨立的測試集來估計泛化誤差,該測試集由隨機選取的、未曾在訓練集中出現的資料樣本構成。

假設訓練資料和測試資料都是從相同的分佈中獨立提取的。這通常被稱為獨立同分布假設(i.i.d. assumption),這意味著對資料進行取樣的過程沒有進行“記憶”。有時候我們即使輕微違背獨立同分布假設,模型仍將繼續執行得非常好。但同時有些違背獨立同分布假設的行為肯定會帶來麻煩。

一個模型是否能很好地泛化取決於很多因素。有以下幾個傾向於影響模型泛化的因素:

  1. 可調整引數的數量。當可調整引數的數量(有時稱為自由度)很大時,模型往往更容易過擬合。
  2. 引數採用的值。當權重的取值範圍較大時,模型可能更容易過擬合。
  3. 訓練樣本的數量。即使模型很簡單,也很容易過擬合只包含一兩個樣本的資料集。而過擬合一個有數百萬個樣本的資料集則需要一個極其靈活的模型。

4.4.2 模型選擇

為了確定候選模型中的最佳模型,我們通常會使用驗證集。不能依靠測試資料進行模型選擇。然而,我們也不能僅僅依靠訓練資料來選擇模型,因為我們無法估計訓練資料的泛化誤差。

解決此問題的常見做法是將我們的資料分成三份,除了訓練和測試資料集之外,還增加一個驗證資料集(validation dataset),也叫驗證集(validation set)。書中每次實驗報告的準確度都是驗證集準確度,而不是測試集準確度。

也可以用 K 折交叉驗證。原始訓練資料被分成 \(K\) 個不重疊的子集。然後執行 \(K\) 次模型訓練和驗證,每次在 \(K−1\) 個子集上進行訓練,並在剩餘的一個子集(在該輪中沒有用於訓練的子集)上進行驗證。最後,透過對 \(K\) 次實驗的結果取平均來估計訓練和驗證誤差。

但是這會帶來一個問題,不存在平均誤差方差的無偏估計。於是通常會用近似來解決。

4.4.3 欠擬合還是過擬合

訓練誤差和驗證誤差都很嚴重,但它們之間僅有一點差距。如果模型不能降低訓練誤差,這可能意味著模型過於簡單(即表達能力不足),無法捕獲試圖學習的模式。此外,由於我們的訓練和驗證誤差之間的泛化誤差很小,我們有理由相信可以用一個更復雜的模型降低訓練誤差。這種現象被稱為欠擬合(underfitting)。

另一方面,當我們的訓練誤差明顯低於驗證誤差時要小心,這表明嚴重的過擬合(overfitting)。

是否過擬合或欠擬合可能取決於模型複雜性和可用訓練資料集的大小。

4.4.4 多項式擬合

作者用擬合一個多項式的方式來作為示範。當引數量等於原本的資料分佈的引數量時擬合函式正常。但是如果模型引數量較少會發生欠擬合,而模型引數量過大會發生過擬合。

課後題(部分)

(1)多項式迴歸問題可以準確地解出嗎?

直接高斯消元,或者參考前面解方程。

(2)繪製訓練損失與模型複雜度(多項式的階數)的關係圖。觀察到了什麼?需要多少階的多項式才能將訓練損失減少到0?

animator = d2l.Animator(xlabel='degree', ylabel='loss', yscale='log', xlim=[1, max_degree], ylim=[1e-3, 1e2], legend=['train', 'test'])
for i in range(max_degree):
    p = train_parameter(poly_features[:n_train, :i], poly_features[n_train:, :i], labels[:n_train], labels[n_train:])
    animator.add(i + 1, p)

image-20230413204901138

訓練損失無法減少到 0.

資料量那個圖,取度數為 \(4\),在程式碼中體現為 \(5\)。只能說是毫無規律:

animator = d2l.Animator(xlabel='train_data', ylabel='loss', yscale='log', xlim=[100, 1000],
                       ylim=[1e-3, 1e2], legend=['train', 'test'])
for i in range(10):
    poly_features, labels = getData((i+1) * 100)
    p = train_parameter(poly_features[:n_train, :5], poly_features[n_train:, :5], labels[:n_train], labels[n_train:])
    animator.add((i + 1)*100, p)

image-20230413211648314

(3)如果不對多項式特徵 \(x^i\) 進行標準化 \((1/i!)\) ,會出現什麼問題?能用其他方法解決這個問題嗎?

如果有一個 \(x\) 大於 \(1\),那麼這個很大的 \(i\) 就會帶來很大的值,最佳化的時候可能會帶來很大的梯度值。

(4)泛化誤差可能為零嗎?

幾乎不可能。

4.5 權重衰減(weight decay)

單項式(monomial)是多項式對多變數資料的自然擴充套件,也可以說是變數的冪的乘積。給定 \(k\) 個變數,階數 \(d\)(即選 \(k\) 個自然數的加和為 \(d\)),則共有 \(\begin{pmatrix} k-1+d \\ d-1\end{pmatrix}\) 種單項式。因此對於多變數資料在多項式上的擴充套件,需要更細粒度的工具來調整函式複雜度。

4.5.1 範數與權重衰減

權重衰減是使用最廣泛的正則化技術之一,它通常也稱為 \(L_2\) 正則化。這個技術透過函式與零的距離來度量函式的複雜度。要保證權重向量比較小,最常用方法是將其範數作為懲罰項加到最小化損失的問題中,將原來的訓練目標最小化訓練標籤上的預測損失,調整為最小化預測損失和懲罰項之和

上一章種線性迴歸例子的損失為:

\[L(\boldsymbol{w}, b) = \frac{1}{n} \sum_{i=1}^n \frac{1}{2}(\boldsymbol{w}^T \boldsymbol{x}^{(i)} + b - y^{(i)})^2 \]

那麼,可以透過正則化常數 \(\lambda\) 來描述這種權衡,這是一個非負超引數,我們使用驗證資料擬合:

\[L(\boldsymbol{w}, b) + \frac{\lambda}{2} ||\boldsymbol{w}||^2 \]

原書中關於這部分函式的設計原因寫的很詳細:

對於 \(\lambda=0\),我們恢復了原來的損失函式。對於 \(\lambda>0\),我們限制 \(\|\boldsymbol{w}\|\) 的大小。這裡我們仍然除以 \(2\),當我們取一個二次函式的導數時,\(2\)\(\frac{1}{2}\) 會抵消,以確保更新表示式看起來既漂亮又簡單。為什麼在這裡我們使用平方範數而不是標準範數(即歐幾里得距離)?這樣做是為了便於計算。透過平方 \(L_2\) 範數,我們去掉平方根,留下權重向量每個分量的平方和,這使得懲罰的導數很容易計算:導數的和等於和的導數。

此外,為什麼我們首先使用 \(L_2\) 範數,而不是 \(L_1\) 範數。事實上,這個選擇在整個統計領域中都是有效的和受歡迎的。\(L_2\) 正則化線性模型構成經典的嶺迴歸(ridge regression)演算法,\(L_1\) 正則化線性迴歸是統計學中類似的基本模型,通常被稱為套索迴歸(lasso regression)。使用 \(L_2\) 範數的一個原因是它對權重向量的大分量施加了巨大的懲罰。這使得我們的學習演算法偏向於在大量特徵上均勻分佈權重的模型。在實踐中,這可能使它們對單個變數中的觀測誤差更為穩定。相比之下,\(L_1\) 懲罰會導致模型將權重集中在一小部分特徵上,而將其他權重清除為零。這稱為特徵選擇(feature selection),這可能是其他場景下需要的。

那麼 \(L_2\) 正則化迴歸的小批次隨機梯度下降更新如下式:

\[\boldsymbol{w} \leftarrow (1 - \eta \lambda) \boldsymbol{w} - \frac{\eta}{|B|} \sum_{i \in B} \boldsymbol{x}^{(i)} (\boldsymbol{w}^T \boldsymbol{x}^{(i)} + b - y^{(i)}) \]

根據之前章節,我們根據估計值與觀測值之間的差異來更新 \(\boldsymbol{w}\)。然而,我們同時也在試圖將 \(\boldsymbol{w}\) 的大小縮小到零。這就是為什麼這種方法有時被稱為權重衰減。我們僅考慮懲罰項,最佳化演算法在訓練的每一步衰減權重。與特徵選擇相比,權重衰減為我們提供了一種連續的機制來調整函式的複雜度。較小的 \(\lambda\) 值對應較少約束的 \(\boldsymbol{w}\),而較大的 \(\lambda\) 值對 \(\boldsymbol{w}\) 的約束更大。

是否對相應的偏置 \(b^2\) 進行懲罰在不同的實踐中會有所不同,在神經網路的不同層中也會有所不同。通常,網路輸出層的偏置項不會被正則化。

4.5.3 從零開始實現(有趣的 optim)

這一節中出現了很有趣的 Optimizer 的寫法,原文是給權重設定了權重衰減,而偏置沒有設定權重衰減。具體程式碼如下:

trainer = torch.optim.SGD([{"params": net[0].weight, 'weight_decay': wd},
                           {"params": net[0].bias}], lr=lr)

下面簡單寫一下 PyTorch 原文件中的有趣用法。

逐個引數最佳化方法

Optimizer 也支援逐個引數最佳化的選項。這裡傳入的不再是 Variable(或者說是 Tensor)的一個可迭代物件,取而代之的是傳入 dict 的可迭代物件(事實上,Optimizer 中也只能傳入這兩個的可迭代物件)。它們中的每一個會定義一個分離的引數組。組中應當有一個包含了引數列表的 param 鍵,其他鍵應當匹配能夠被 Optimizer 接受的引數。

當然,與此同時也仍然可以在外面寫其他預設引數,這不會覆蓋掉引數組裡面的引數。

官網中給出了這樣一個例子:

optim.SGD([
                {'params': model.base.parameters()},
                {'params': model.classifier.parameters(), 'lr': 1e-3}
            ], lr=1e-2, momentum=0.9)

這意味著 model.base 的引數會使用預設學習率 1e-2model.classifier 的引數會使用 1e-3。動量 0.9 將會應用於所有引數。

課後題

(1)繪製訓練精度和測試精度關於 \(\lambda\) 的函式圖,可以觀察到什麼?

def train_concise_many_times(max_wd):
    animator = d2l.Animator(xlabel = 'wd', ylabel = 'loss', yscale='log', xlim=[1, max_wd], legend=['train', 'test'])
    for wd in range(max_wd):
        net = nn.Sequential(nn.Linear(num_inputs, 1))
        for param in net.parameters():
            param.data.normal_()
        loss = nn.MSELoss(reduction='none')
        num_epochs, lr = 100, 0.003
        trainer = torch.optim.SGD([{"params": net[0].weight, 'weight_decay': wd},
                                  {"params": net[0].bias}], lr=lr)
        for epoch in range(num_epochs):
            for X, y in train_iter:
                trainer.zero_grad()
                l = loss(net(X), y)
                l.mean().backward()
                trainer.step()
        animator.add(wd + 1, (d2l.evaluate_loss(net, train_iter, loss), d2l.evaluate_loss(net, test_iter, loss)))
        
train_concise_many_times(10)

image-20230418112710849

可以觀察到隨著 \(\lambda\) 增加,訓練 loss 增大,測試 loss 在 \(\lambda=4\) 的時候達到最小,然後趨於平穩。

(2)使用驗證集來找到最優值 \(\lambda\),它真的是最優值嗎?

不一定,如果驗證集與測試集滿足獨立同分布,那麼最優。

(3)如果使用 \(\sum_i |w_i|\) 作為懲罰(\(L_1\) 正則化),那麼更新的公式長什麼樣子?

不妨設新的損失函式為:

\[L(\boldsymbol{w}, b) + \lambda \sum_i |w_i| \]

則,更新公式為:

\[w_i \leftarrow \begin{cases} w_i + \eta \lambda - \frac{\eta}{|B|} \left(\sum_{j \in B} \boldsymbol{x}^{(j)}(\boldsymbol{w}^T \boldsymbol{x}^{(j)} + b - y^{(j)}) \right)_i, &w_j < 0 \\ w_i - \eta \lambda - \frac{\eta}{|B|} \left(\sum_{j \in B} \boldsymbol{x}^{(j)}(\boldsymbol{w}^T \boldsymbol{x}^{(j)} + b - y^{(j)}) \right)_i, &w_j \ge 0 \end{cases} \]

(4)已知 \(\|\boldsymbol{w}\|^2 = \boldsymbol{w}^T \boldsymbol{w}\)。能找到類似的矩陣方程嗎?(提示:弗羅貝尼烏斯範數)

矩陣 \(X \in \mathbb{R}^{m \times n}\) 的 Frobenius norm 為:

\[\|X\|_F^2 = \sum_{i=1}^m \sum_{j=1}^n x_{ij}^2= tr (X^TX) \]

(5)處理過擬合的方法除了權重衰減、增加訓練資料、使用適當複雜度的模型,還有哪些?

還有 dropout,提前終止等。

(6)在貝葉斯統計中,使用先驗和似然的乘積,透過公式 \(P(w|x) \propto P(x|w)P(w)\) 得到後驗。如何得到正則化的 \(P(w)\)

這裡題目顯然有些奇怪,英文原文為:

In Bayesian statistics we use the product of prior and likelihood to arrive at a posterior via \(P(w∣x) \propto P(x∣w)P(w)\). How can you identify P(w) with regularization?

最後一句可以翻譯為如何用正則化得到 \(P(w)\),也可以翻譯為如何得到帶有正則化的 \(P(w)\)

最佳化 \(w\) 的過程本質上也是最大化後驗機率 \(P(w|x)\) 的過程,也是最小化其負對數似然的過程,即:

\[\arg \max P(w|x) \implies \arg \max P(x|w) P(w) \implies \arg \min \left(-\ln P(x | w) - \ln P(w) \right) \]

可以發現上式中 \(-\ln P(w)\) 就是正則化項了。如果要讓 \(P(w|x)\) 儘量大,那麼就應當讓正則化項 \(P(w)\) 儘量大。

這樣的話問題最好應當翻譯成:如何得到帶有與 \(P(w)\) 相關的正則化項。

4.6 暫退法(Dropout)

由於上一章的做法是假設了一個先驗,即權重的值取自均值為 0 的高斯分佈。這可能不是特別優雅。因此希望模型深度挖掘特徵,即將其權重分散到許多特徵中,而不是過於依賴少數潛在的虛假關聯。

4.6.1 重新審視過擬合

線性模型沒有考慮到特徵之間的互動作用,換言之,每一個僅為一次項,沒有交叉相乘的項。對於每個特徵,線性模型必須指定正的或負的權重,而忽略其他特徵。

泛化性和靈活性之間的這種基本權衡被描述為偏差-方差權衡(bias-variance tradeoff)。線性模型有很高的偏差:它們只能表示一小類函式。然而,這些模型的方差很低:它們在不同的隨機資料樣本上可以得出相似的結果。

神經網路並不侷限於單獨檢視每個特徵,而是學習特徵之間的互動。但即使我們有位元徵多得多的樣本,深度神經網路也有可能過擬合。

4.6.2 擾動的穩健性

經典泛化理論認為,為了縮小訓練和測試效能之間的差距,應該以簡單的模型為目標。簡單性以較小維度的形式展現。此外,引數的範數也代表了一種有用的簡單性度量。簡單性的另一個角度是平滑性,即函式不應該對其輸入的微小變化敏感。1995年,克里斯托弗·畢曉普證明了 具有輸入噪聲的訓練等價於Tikhonov正則化。這項工作用數學證實了“要求函式光滑”和“要求函式對輸入的隨機噪聲具有適應性”之間確實存在聯絡。

Srivastava提出了暫退法(Dropout)。在訓練過程中,他們建議在計算後續層之前向網路的每一層注入噪聲。因為當訓練一個有多層的深層網路時,注入噪聲只會在輸入-輸出對映上增強平滑性。這種方法之所以被稱為暫退法,因為從表面上看是在訓練過程中丟棄(drop out)一些神經元。在整個訓練過程的每一次迭代中,標準暫退法包括在計算下一層之前將當前層中的一些節點置零。

關鍵的挑戰就是如何注入這種噪聲。一種想法是以一種無偏差(unbiased)的方式注入噪聲。這樣在固定住其他層時,每一層的期望值等於沒有噪音時的值。

  1. 在畢曉普的工作中,他將高斯噪聲新增到線性模型的輸入中。在每次訓練迭代中,他將從均值為零的分佈 \(\epsilon \sim N(0, \sigma^2)\) 取樣噪聲新增到輸入 \(\boldsymbol{x}\),從而產生擾動點 \(\boldsymbol{x}' = \boldsymbol{x} + \epsilon\),預期是 \(\mathbb{E}[\boldsymbol{x}']=\boldsymbol{x}\)

  2. 在標準暫退法正則化中,透過按保留(未丟棄)的節點的分數進行規範化來消除每一層的偏差。換言之,每個中間活性值 \(h\)暫退機率 \(p\) 由隨機變數 \(h'\) 替換,如下所示:

    \[h' = \begin{cases} 0 &,\text{機率為 $p$} \\ \frac{h}{1-p} &,\text{其他情況} \end{cases} \]

    期望值保持不變,即 \(\mathbb{E}[h']=h\)

4.6.3 實踐中的暫退法

image-20230418170732248

上圖為 Dropout 前後的多層感知機。其中 Dropout 過程刪除了 \(h_2\)\(h_5\)。因此輸出的計算不依賴 \(h_2\)\(h_5\)。並且它們各自的梯度在執行反向傳播時也會消失。這樣,輸出層的計算不能過度依賴於 \(h_1, h_2, \dots, h_5\) 的任何一個元素。

通常,我們在測試時不用暫退法。給定一個訓練好的模型和一個新的樣本,我們不會丟棄任何節點,因此不需要標準化。然而也有一些例外:一些研究人員在測試時使用暫退法,用於估計神經網路預測的“不確定性”:如果透過許多不同的暫退法遮蓋後得到的預測結果都是一致的,那麼我們可以說網路發揮更穩定。

程式碼中有個技巧是,在靠近輸入層的地方設定較低的暫退機率。感性理解這個原因可能是,透過第一層後如果設定的暫退機率較高/跟之後的差不多的話,會導致透過線性層後丟掉靠近源資料的特徵,進而導致資料中的部分內容被丟棄。

nn.Dropout(dropout_probability) 會在訓練時,暫退層根據指定的暫退機率隨機丟棄上一層的輸出(相當於下一層的輸入)。在測試時,暫退層僅傳遞資料。

此外,作者推薦的暫退機率為 \([0.2, 0.5]\) 之間。

課後題

(1)如果更改第一層和第二層的暫退機率,會出現什麼問題?具體地說,如果交換這兩個層,會出現什麼問題?設計一個實驗來回答這些問題,定量描述該結果,並總結定性的結論。

# 交換前
dropout1, dropout2 = 0.2, 0.5
net = nn.Sequential(nn.Flatten(),
                   nn.Linear(784, 256),
                   nn.ReLU(),
                   nn.Dropout(dropout1),
                   nn.Linear(256, 256),
                   nn.ReLU(),
                   nn.Dropout(dropout2),
                   nn.Linear(256, 10))
def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)
    
net.apply(init_weights)

trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

image-20230419113624457

# 交換後
net = nn.Sequential(nn.Flatten(),
                   nn.Linear(784, 256),
                   nn.ReLU(),
                   nn.Dropout(dropout2),
                   nn.Linear(256, 256),
                   nn.ReLU(),
                   nn.Dropout(dropout1),
                   nn.Linear(256, 10))
def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)
    
net.apply(init_weights)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

image-20230419113732914

可以觀察到僅僅有微小的差距。交換後的收斂速度變慢了一點點,而精度幾乎沒變化。譯者在評論區說:

It may be hard to observe a huge loss/acc difference if the network is shallow and can converge quickly. As you can find in the original dropout paper (http://jmlr.org/papers/volume15/srivastava14a/srivastava14a.pdf) as well, the improvement with dropout on MNIST is less than 1%.

翻譯過來大概意思是,當模型比較小很難觀察到巨大的 loss/acc 的差別,並且它會快速收斂。原論文中也說,在 MNIST 上使用 dropout 的提升小於 1%。

(2)增加訓練輪數,並將使用暫退法和不使用暫退法時獲得的結果進行比較。

此處增大訓練輪數到 20 輪。當每層中間有 Dropout 層時,程式碼如下:

num_epochs = 20
net = nn.Sequential(nn.Flatten(),
                   nn.Linear(784, 256),
                   nn.ReLU(),
                   nn.Dropout(dropout1),
                   nn.Linear(256, 256),
                   nn.ReLU(),
                   nn.Dropout(dropout2),
                   nn.Linear(256, 10))
def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)
    
net.apply(init_weights)

trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

image-20230419142625353

去掉中間的 Dropout 層時,程式碼如下:

num_epochs = 20
net = nn.Sequential(nn.Flatten(),
                   nn.Linear(784, 256),
                   nn.ReLU(),
                   nn.Linear(256, 256),
                   nn.ReLU(),
                   nn.Linear(256, 10))
def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)
    
net.apply(init_weights)

trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

連續測了幾次都發現,影像出現了突然的 loss/acc 爆炸。剛開始以為是 Kaggle 伺服器的問題,再測了幾把發現是真的會出這個問題。

image-20230419143126036

除去突然的 loss/acc 爆炸外,對比兩張圖,可以發現去掉 Dropout 層的時候擬合得更快。理論上講,帶有 dropout 的應當減少更多的泛化誤差,但是這幾張圖片中可能確實有一點,但是實在看不太出來。

(3)當使用或不使用暫退法時,每個隱藏層中啟用值的方差是多少?繪製一個曲線圖,以展示這兩個模型的每個隱藏層中啟用值的方差是如何隨時間變化的。

num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256

dropout1, dropout2 = 0.2, 0.5

class Net(nn.Module):
    def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2, is_training=True):
        super(Net, self).__init__()
        self.num_inputs = num_inputs
        self.training = is_training
        self.lin1 = nn.Linear(num_inputs, num_hiddens1)
        self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
        self.lin3 = nn.Linear(num_hiddens2, num_outputs)
        self.relu = nn.ReLU()
    
    def forward(self, X):
        H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs))))
        if self.training == True:
            H1 = dropout_layer(H1, dropout1)
        d1 = torch.var(H1)
        H2 = self.relu(self.lin2(H1))
        if self.training == True:
            H2 = dropout_layer(H2, dropout2)
        d2 = torch.var(H2)
        out = self.lin3(H2)
        return out, d1, d2

def train_epoch_with_variance(net, train_iter, loss, updater):
    """The training loop defined in Chapter 3.
    Defined in :numref:`sec_softmax_scratch`"""
    # Set the model to training mode
    if isinstance(net, torch.nn.Module):
        net.train()
    # Sum of training loss, sum of training accuracy, no. of examples
    metric = d2l.Accumulator(3)
    for X, y in train_iter:
        # Compute gradients and update parameters
        y_hat, v1, v2 = net(X)
        l = loss(y_hat, y)
        if isinstance(updater, torch.optim.Optimizer):
            # Using PyTorch in-built optimizer & loss criterion
            updater.zero_grad()
            l.mean().backward()
            updater.step()
        else:
            # Using custom built optimizer & loss criterion
            l.sum().backward()
            updater(X.shape[0])
        metric.add(v1 * (y.numel() - 1), v2 * (y.numel() - 1), y.numel())
    # Return training loss and training accuracy
    return metric[0] / metric[2], metric[1] / metric[2]

def train_with_varience(net, train_iter, test_iter, loss, num_epochs, updater):
    """Train a model (defined in Chapter 3).
    Defined in :numref:`sec_softmax_scratch`"""
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
                        legend=['variance 1', 'variance 2'])
    for epoch in range(num_epochs):
        train_metrics = train_epoch_with_variance(net, train_iter, loss, updater)
        print(train_metrics)
        animator.add(epoch + 1, train_metrics)
    print(train_metrics)

num_epochs, lr, batch_size = 20, 0.5, 256
loss = nn.CrossEntropyLoss(reduction = 'none')
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)

net_with_dropout = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2, True)
net_with_dropout.apply(init_weights)
train_with_varience(net_with_dropout, train_iter, test_iter, loss, num_epochs, trainer)

net_without_dropout = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2, False)
net_without_dropout.apply(init_weights)
train_with_varience(net_without_dropout, train_iter, test_iter, loss, num_epochs, trainer)

image-20230419153212076image-20230419153222486

上圖(左圖)為使用暫退法的第一層方差與第二層方差,下圖(右圖)為不使用暫退法的第一層與第二層方差。可以發現加入 Dropout 層後,方差明顯變小。

(4)為什麼在測試時通常不使用暫退法?

儘量將全部函式都應用上來,暫退法本質上是給模型加噪聲,測試的時候反而會影響效果。

(5)以本節中的模型為例,比較使用暫退法和權重衰減的效果。如果同時使用暫退法和權重衰減,會發生什麼情況?結果是累加的嗎?收益是否減少(或者說更糟)?它們互相抵消了嗎?

wd = 0

trainer = torch.optim.SGD([
    {"params": net[1].weight, "weight_decay": wd},
    {"params": net[4].weight, "weight_decay": wd},
    {"params": net[7].weight, "weight_decay": wd},
    {"params": net[1].bias},
    {"params": net[4].bias},
    {"params": net[7].bias},
], lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

image-20230419162942632image-20230419164052915image-20230419163350478image-20230419163823029

圖 1 是 wd=0 的情況下,即不採用權重衰減僅採用暫退法的曲線。圖 2 是 \(10^{-4}\) 的情況下的曲線,圖 3 為 \(10^{-3}\) 的情況下的曲線,可以發現準確率有所降低,訓練損失函式也沒有降下去,最後還有奇怪的凸起。圖 4 為 \(10^{-2}\) 情況下的曲線,可以發現完全沒擬合。再大的情況甚至曲線都不會出現在這張圖上了。

由上面幾張圖可以簡單得出結論,超過 \(10^{-3}\) 的情況幾乎無法選擇,\(10^{-4}\) 目前來看情況最好,但是和 \(0\) 的情況比起來,收斂速度更慢,除此之外難以說明哪個更優。

(6)如果我們將暫退法應用到權重矩陣的各個權重,而不是啟用值,會發生什麼?

只使用暫退法修改前兩個線性層的權重。

dropout1, dropout2 = 0.2, 0.5

class Net(nn.Module):
    def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2, is_training=True):
        super(Net, self).__init__()
        self.num_inputs = num_inputs
        self.training = is_training
        self.lin1 = nn.Linear(num_inputs, num_hiddens1)
        self.lin2 = nn.Linear(num_hiddens1, num_hiddens2)
        self.lin3 = nn.Linear(num_hiddens2, num_outputs)
        self.relu = nn.ReLU()
    
    def forward(self, X):
        W1 = dropout_layer(self.lin1.weight, dropout1)
        W2 = dropout_layer(self.lin2.weight, dropout2)
        H1 = self.relu(torch.matmul(X.reshape((-1, self.num_inputs)), W1.t()) + self.lin1.bias)
        H2 = self.relu(torch.matmul(H1, W2.t()) + self.lin2.bias)
        out = self.lin3(H2)
        return out
    
net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)

num_epochs, lr, batch_size = 20, 0.5, 256
loss = nn.CrossEntropyLoss(reduction = 'none')
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
trainer = torch.optim.SGD(net.parameters(), lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

image-20230419170915750

訓練速度更慢了,且幾乎顯然不如對啟用值應用暫退法。

(7)請提出另一個在每一層注入隨機噪聲的技術,該技術優於標準暫退法。

評論區有老哥說對每一層輸出的啟用值加了個高斯噪聲(這不就是書中提的 Bishop 的做法嗎),但是由於原本的損失以及精度太好了,所以啥都看不出來。我懶得做,估計我也做不出來。

4.7 前向傳播、反向傳播和計算圖

中間作為示例的式子就跳過吧,就附個圖好了,剩下的部分太長了=。=

image-20230419212355288

前向傳播(forward propagation或forward pass)指的是:按順序(從輸入層到輸出層)計算和儲存神經網路中每層的結果。

反向傳播(backward propagation或backpropagation)指的是計算神經網路引數梯度的方法。簡言之,該方法根據微積分中的鏈式規則,按相反的順序從輸出層到輸入層遍歷網路。該演算法儲存了計算某些引數梯度時所需的任何中間變數(偏導數)。

對於前向傳播,我們沿著依賴的方向遍歷計算圖並計算其路徑上的所有變數。然後將這些用於反向傳播,其中計算順序與計算圖的相反。

因此,在訓練神經網路時,在初始化模型引數後,我們交替使用前向傳播和反向傳播,利用反向傳播給出的梯度來更新模型引數。注意,反向傳播重複利用前向傳播中儲存的中間值,以避免重複計算。帶來的影響之一是我們需要保留中間值,直到反向傳播完成。這也是訓練比單純的預測需要更多的記憶體(視訊記憶體)的原因之一。此外,這些中間值的大小與網路層的數量和批次的大小大致成正比。因此,使用更大的批次來訓練更深層次的網路更容易導致記憶體不足(out of memory)錯誤。

課後題

(1)假設一些標量函式 \(\boldsymbol{X}\) 的輸入 \(\boldsymbol{X}\)\(n \times m\) 矩陣。\(f\) 相對於 \(\boldsymbol{X}\) 的梯度的維數是多少?

顯然是 \(n \times m\) 的。可以理解為 \(f = x_{11} \oplus x_{12} \oplus \cdots \oplus x_{nm}\),那麼 \(f\)\(\boldsymbol{X}\) 中的每一個元素都有偏導數。

(2)向本節中描述的模型的隱藏層新增偏置項(不需要在正則化項中包含偏置項)

畫計算圖——不畫(懶得拍照了)

推導反向傳播方程:

\[\frac{\partial J}{\partial \boldsymbol{b}^{(1)}} = \frac{\partial J}{\partial \boldsymbol{h}} \frac{\partial \boldsymbol{h}}{\partial \boldsymbol{b}^{(1)}} = \frac{\partial J}{\partial \boldsymbol{h}} \odot \phi'(\boldsymbol{z}+\boldsymbol{b}^{(i)}) \\ \frac{\partial J}{\partial \boldsymbol{b}^{(2)}} = \frac{\partial J}{\partial L} \frac{\partial L}{\partial \boldsymbol{b}^{(2)}} = \frac{\partial L}{\partial\boldsymbol{b}^{(2)}} \]

上面的推導可能有一些問題,希望您們如果和我推的不一樣可以在評論區討論一下。

(3)計算本節所描述的模型用於訓練和預測的記憶體空間。

首先假定所有的變數統一使用 float32,即一個浮點數 \(4\) 個位元組。其次,訓練時由於需要中間變數,因此所需空間是全部中間變數所佔空間之和,除此之外還有梯度的存在。而預測時除了輸入 \(\boldsymbol{x}\)、輸出 \(y\)、目標函式 \(J\) 這三者(除引數以外)必須要儲存外,剩下的僅需要統計佔最大空間的變數即可。

如下表所示(這裡有些定義的空間大小您需要回去翻一下原書):

變數/引數 特徵個數
\(\boldsymbol{x}\) \(d\)
\(\boldsymbol{z}\) \(h\)
\(\boldsymbol{h}\) \(h\)
\(\boldsymbol{o}\) \(q\)
\(y\) 1
\(L\) 1
\(s\) 1
\(J\) 1
\(\boldsymbol{W}^{(1)}\) \(h \times d\)
\(\boldsymbol{W}^{(2)}\) \(q \times h\)
\(\frac{\partial \boldsymbol{z}}{\partial \boldsymbol{x}}\) \(h \times d\)
\(\frac{\partial \boldsymbol{z}}{\partial \boldsymbol{W}^{(1)}}\) \(h \times d\)
\(\frac{\partial \boldsymbol{h}}{\partial \boldsymbol{z}}\) \(h\)
\(\frac{\partial \boldsymbol{o}}{\partial \boldsymbol{h}}\) \(q \times h\)
\(\frac{\partial \boldsymbol{o}}{\partial \boldsymbol{W}^{(2)}}\) \(q \times h\)
\(\frac{\partial L}{\partial \boldsymbol{o}}\) \(q\)
\(\frac{\partial L}{\partial y}\) 1
\(\frac{\partial s}{\partial \boldsymbol{W}^{(1)}}\) \(h \times d\)
\(\frac{\partial s}{\partial \boldsymbol{W}^{(2)}}\) \(q \times h\)
\(\frac{\partial J}{\partial s}\) 1
\(\frac{\partial J}{\partial y}\) 1

因此,訓練時,除梯度外,所佔空間為 \(4 \times (d + 2h + q + 4 + h \times d + q \times h)\) 位元組。

而梯度如果不最佳化的話,足足佔了 \(4 \times 3 \times (1 + h \times d + q \times h)\) 位元組。

因此共佔了 \(4 \times (d + 2h + q + 7 + 4 \times h \times d + 4 \times q \times h)\) 位元組。

預測時所佔空間為 \(4 \times \left(d + 2 + h \times d + q \times h + \max (1, h, q) \right)\) 位元組。

(4)假設想計算二階導數。計算圖會發生什麼變化?預計計算需要多長時間?

在使用 autograd 計算一階導數時,讓 create_graph=True 這樣就可以對一階導再求導了。

顯而易得的,計算圖中的節點數量會增加,因為梯度也變成節點進入計算圖中,假設上一次節點數為 \(n\),那麼二階導的計算圖中的節點個數變為 \(2n+1\),預計計算時間會變二倍。

(5)假設計算圖對當前 GPU 來說太大了。(5.a)請嘗試把它劃分到多個 GPU 上。(5.b)這與小批次訓練相比,有哪些優點和缺點?

可以參考知乎PyTorch 81. 模型並行 Model Parallel 將模型並行到多個 GPU 的做法。

比如現在有一個包含2個 Linear layers 的模型,我們想在2塊 GPU 上 run 它,辦法可以是在每塊 GPU 上放置1個 Linear layer,並且把得到的中間結果在 GPU 之間移動。程式碼可以是這樣子:

import torch
import torch.nn as nn
import torch.optim as optim


class ToyModel(nn.Module):
   def __init__(self):
       super(ToyModel, self).__init__()
       self.net1 = torch.nn.Linear(10, 10).to('cuda:0')
       self.relu = torch.nn.ReLU()
       self.net2 = torch.nn.Linear(10, 5).to('cuda:1')

   def forward(self, x):
       x = self.relu(self.net1(x.to('cuda:0')))
       return self.net2(x.to('cuda:1'))

注意,上述 ToyModel 看起來與在單個 GPU 上的實現方式非常相似,除了四個 to(device) 的呼叫,將 Linear layer 和張量放在適當的裝置上。這是該模型中唯一需要改變的地方。backward() 和 torch.optim 將自動處理梯度問題,就像模型是在一個 GPU 上一樣。

你只需要確保在呼叫損失函式時,標籤和輸出是在同一個裝置上。像下面這樣:

model = ToyModel()
loss_fn = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.001)

optimizer.zero_grad()
outputs = model(torch.randn(20, 10))
labels = torch.randn(20, 5).to('cuda:1')
loss_fn(outputs, labels).backward()
optimizer.step()

這裡應該把標籤 labels 放在 \(1\) 號 GPU 上面,因為模型的輸出就在 \(1\) 號 GPU 上。

這樣做可以增大資料的規模,本質上和小批次訓練一樣,但是這樣可以增大批次的大小。而訓練速度略有降低,這是由於資料在不同的 GPU 上覆制而導致的。

4.8 數值穩定性和模型初始化

初始化方案的選擇在神經網路學習中起著舉足輕重的作用,它對保持數值穩定性至關重要。此外,這些初始化方案的選擇可以與非線性啟用函式的選擇有趣的結合在一起。我們選擇哪個函式以及如何初始化引數可以決定最佳化演算法收斂的速度有多快。糟糕選擇可能會導致我們在訓練時遇到梯度爆炸或梯度消失。

4.8.1 梯度消失和梯度爆炸

在鏈式法則中,梯度容易受到數值下溢問題的影響。當將太多的機率乘在一起時,這些問題經常會出現。在處理機率時,一個常見的技巧是切換到對數空間,即將數值表示的壓力從尾數轉移到指數。不幸的是,上面的問題更為嚴重:引數矩陣可能具有各種各樣的特徵值。他們可能很小,也可能很大;他們的乘積可能非常大,也可能非常小。

不穩定梯度帶來的風險不止在於數值表示,也威脅到我們最佳化演算法的穩定性。我們可能面臨一些問題。要麼是梯度爆炸(gradient exploding)問題:引數更新過大,破壞了模型的穩定收斂;要麼是梯度消失(gradient vanishing)問題:引數更新過小,在每次更新時幾乎不會移動,導致模型無法學習。

1. 梯度消失

sigmoid 函式以前很流行。由於早期的人工神經網路受到生物神經網路的啟發,神經元要麼完全啟用要麼完全不啟用(就像生物神經元)的想法很有吸引力。

當sigmoid函式的輸入很大或是很小時,它的梯度都會消失。此外,當反向傳播透過許多層時,除非我們在剛剛好的地方,這些地方sigmoid函式的輸入接近於零,否則整個乘積的梯度可能會消失。當我們的網路有很多層時,除非我們很小心,否則在某一層可能會切斷梯度。事實上,這個問題曾經困擾著深度網路的訓練。因此,更穩定的ReLU系列函式已經成為從業者的預設選擇。

2. 梯度爆炸

多層神經網路通常存在像懸崖一樣斜率較大的區域。這是由於幾個較大的權重相乘導致的。書上舉的例子是一百個服從 \(N(0,1)\)\(4\times 4\) 矩陣相乘,當遇到斜率較大的懸崖結構時,梯度更新會很大程度地改變引數值,通常會完全跳過這類懸崖結構,使得引數彈射得非常遠,可能導致之前做了無用功。

3. 打破對稱性

神經網路設計中的另一個問題是其引數化所固有的對稱性。書中舉了一個一層兩個隱藏單元的多層感知機,其中兩個隱藏單元在前向傳播過程中採用相同的輸入和引數。這會導致回傳時有相同的梯度,進而導致無論怎麼迭代引數都對稱。雖然小批次隨機梯度下降不會打破這種對稱性,但暫退法正則化可以。

4.8.2 引數初始化

解決(或至少減輕)上述問題的一種方法是進行引數初始化, 最佳化期間的注意和適當的正則化也可以進一步提高穩定性。

1. 預設初始化

如果我們不指定初始化方法, 框架將使用預設的隨機初始化方法。例如在 PyTorch 中,線性層的權重和偏置被初始化為 \(U(-\sqrt{k}, \sqrt{k})\),其中 \(k=\frac{1}{\text{in_features}}\)

2. Xavier 初始化

原書假設沒有非線性的全連線層輸出 \(o_i\) 的分佈。對於該層 \(n_{\text{in}}\) 輸入 \(x_j\) 以及相關權重 \(w_{ij}\),輸出由下式給出:

\[o_i = \sum_{j=1}^{n_{\text{in}}} w_{ij} x_j \]

不妨假設 \(\mathbb{E}[w_{ij}] = 0, \text{Var}[w_{ij}] = \sigma^2\),而 \(\mathbb{E}[x_j] = 0, \text{Var}[x_j] = \gamma^2\),且這些都互相獨立。

則可以計算出 \(o_i\) 的期望和方差:

\[\begin{align} \mathbb{E}[o_i] &= \sum_{j=1}^{n_{\text{in}}} \mathbb{E}[w_{ij} x_j] \\ &= \sum_{j=1}^{n_{\text{in}}} \mathbb{E}[w_{ij}] \mathbb{E}[x_j] \\ &= 0 \\ \text{Var}[o_i] &= \mathbb{E}[o_i^2] - (\mathbb{E}[o_i])^2 \\ &= \mathbb{E}[w_{ij}^2 x_j^2] - 0 \\ &= \mathbb{E}[w_{ij}^2] \mathbb{E}[x_j^2] \\ &= n_{\text{in}} \sigma^2 \gamma^2 \end{align} \]

如果要使方差不變的方法是設定 \(n_{\text{in}} \sigma^2=1\)。然而,反向傳播的過程中,也有類似的問題。如果要使反向傳播梯度的方差不變,需要設定 \(n_{\text{out}} \sigma^2=1\),但是由於一般來說全連線層維度都會有所變化,即 \(n_{\text{in}} \not = n_{\text{out}}\),因此幾乎無法同時滿足上述兩個條件。

於是 Xavier 初始化提出,要滿足這個條件:

\[\frac{1}{2}(n_{\text{in}} + n_{\text{out}}) \sigma^2=1 \iff \sigma = \sqrt{\frac{2}{n_{\text{in}} + n_{\text{out}}}} \]

通常,Xavier 初始化從均值為 \(0\),方差 \(\sigma^2 = \frac{2}{n_{\text{in}} + n_{\text{out}}}\) 的高斯分佈中抽樣權重。也可以改成從均勻分佈中抽取權重,那麼 Xavier 初始化的均勻分佈為:

\[U \left(-\sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}}, \sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}} \right) \]

儘管“不存在非線性”的假設在神經網路中很難實現,但是 Xavier 初始化方法在實踐中比較有效。

練習題

(1)除了多層感知機的排列對稱性之外,還能設計出其他神經網路可能會表現出對稱性且需要被打破的情況嗎?

顯然存在,如 CNN 等。

(2)我們是否可以將線性迴歸或 softmax 迴歸中的所有權重引數初始化為相同的值?

可以,但是並不推薦。原因在於最開始這一步會顯著體現出對稱性,如果輸入資料也具有對稱性,那麼反向傳播後引數對稱性幾乎無法改變。

(3)在相關資料中查詢兩個矩陣乘積特徵值的解析解。這對確保梯度條件合適有什麼啟示?

最大特徵值與最小特徵值可以決定矩陣的條件數,如果相差太大,那麼條件數就也會太大,稍有擾動就會出現巨大差別。

(4)如果我們知道某些項是發散的,我們能在事後修正嗎?

參考 LARS 的原論文,可以對每一層使用不同的學習率來修正這一問題。

4.9 環境和分佈偏移

機器學習的許多應用中都存在類似的問題: 透過將基於模型的決策引入環境,可能會導致破壞模型的後果。

4.9.1 分佈偏移的型別

在一個經典的情景中,假設訓練資料是從某個分佈 \(p_S(\boldsymbol{x}, y)\) 中取樣的, 但是測試資料將包含從不同分佈 \(p_T(\boldsymbol{x}, y)\) 中抽取的未標記樣本。 一個清醒的現實是:如果沒有任何關於 \(p_S(\boldsymbol{x})\) 和 $$p_T(\boldsymbol{x}, y)$$ 之間相互關係的假設, 學習到一個分類器是不可能的。

幸運的是,在對未來我們的資料可能發生變化的一些限制性假設下,有些演算法可以檢測這種偏移,甚至可以動態調整,以提高原始分類器的精度。

1. 協變數偏移

協變數偏移是指輸入的分佈改變了,但是標籤函式(即條件分佈 \(P(y|\boldsymbol{x})\))沒有改變。之所以命名為協變數偏移是因為協變數(特徵)分佈的變化。比如原書中給的例子:

訓練時使用下列寫實貓狗影像:

image-20230420194829696

測試時使用下列卡通貓狗影像:

image-20230420194856351

訓練集由真實照片組成,而測試集只包含卡通圖片。假設在一個與測試集的特徵有著本質不同的資料集上進行訓練,如果沒有方法來適應新的領域,可能會有麻煩。

2. 標籤偏移

標籤偏移(label shift)描述了與協變數偏移相反的問題。這裡我們假設標籤邊緣機率 \(P(y)\) 可以改變, 但是類別條件分佈 \(P(\boldsymbol{x} | y)\) 在不同的領域之間保持不變。當我們認為 \(y\) 導致 \(\boldsymbol{x}\) 時,標籤偏移是一個合理的假設。在另一些情況下,標籤偏移和協變數偏移假設可以同時成立。例如,當標籤是確定的,即使 \(y\) 導致 \(\boldsymbol{x}\),協變數偏移假設也會得到滿足。有趣的是,在這些情況下,使用基於標籤偏移假設的方法通常是有利的。這是因為這些方法傾向於包含看起來像標籤(通常是低維)的物件,而不是像輸入(通常是高維的)物件。

3. 概念偏移

概念偏移(concept shift): 當標籤的定義發生變化時,就會出現這種問題。類別會隨著不同時間、不同地理位置的用法而發生變化。例如地瓜這個概念南北方有巨大差異,這是地理位置的差異。再比如中國從文言文到白話文的過程中所有的詞伴隨著時間流逝意思已經有巨大變化。因此,最好可以利用在時間或空間上逐漸發生偏移的知識。

4.9.3 經驗風險與實際風險

1. 經驗風險與實際風險

訓練資料 \(\{(\boldsymbol{x}_1, y_1), \dots, (\boldsymbol{x}_n, y_n)\}\) 的特徵和相關的標籤經過迭代,在每一個小批次之後更新模型 \(f\) 的引數。為了簡單起見,我們不考慮正則化,因此極大地降低了訓練損失:

\[\min_f \frac{1}{n} \sum_{i=1}^n l(f(\boldsymbol{x}_i), y_i) \]

其中 \(l\) 是損失函式,用來度量:給定標籤 \(y_i\),預測 \(f(\boldsymbol{x}_i)\) 的“糟糕程度”。統計學家將上面式子為經驗風險經驗風險(empirical risk)是為了近似真實風險(true risk),整個訓練資料上的平均損失,即從其真實分佈 \(P(\boldsymbol{x}, y)\) 中抽取的所有資料的總體損失的期望值:

\[\mathbb{E}_{P(\boldsymbol{x}, y)} [ l(f(\boldsymbol{x}), y) ] = \iint l(f(\boldsymbol{x}), y) P(\boldsymbol{x}, y) \mathrm{d} \boldsymbol{x} \mathrm{d}y \]

然而在實踐中,我們通常無法獲得總體資料。因此可以最小化經驗風險來近似最小化真實風險。

2. 協變數偏移糾正

假設對於帶標籤的資料 \((\boldsymbol{x}_i, y_i)\),我們要評估 \(P(y|\boldsymbol{x})\)。然而觀測值 \(\boldsymbol{x}_i\) 是從某些源分佈 \(q(\boldsymbol{x})\) 中得出的,而不是從目標分佈 \(p(\boldsymbol{x})\) 中得出的。幸運的是,依賴性假設意味著條件分佈保持不變,即:\(p(y|\boldsymbol{x}) = q(y|\boldsymbol{x})\)。如果源分佈 \(q(\boldsymbol{x})\) 是“錯誤的”,我們可以透過在真實風險的計算中,使用以下簡單的恆等式來進行糾正:

\[\iint l(f(\boldsymbol{x}), y) p(y|\boldsymbol{x})p(\boldsymbol{x}) \mathrm{d}\boldsymbol{x} \mathrm{d}y = \iint l(f(\boldsymbol{x}), y) q(y|\boldsymbol{x}) q(\boldsymbol{x}) \frac{p(\boldsymbol{x})}{q(\boldsymbol{x})} \mathrm{d}\boldsymbol{x} \mathrm{d}y \]

換句話說,我們需要根據資料來自正確分佈與來自錯誤分佈的機率之比,來重新衡量每個資料樣本的權重:

\[\beta_i \overset{\text{def}}{=}\frac{p(\boldsymbol{x}_i)}{q(\boldsymbol{x}_i)} \]

將權重 \(\beta_i\) 代入到每個資料樣本 \((\boldsymbol{x}_i, y_i)\) 中,我們可以使用”加權經驗風險最小化“來訓練模型:

\[\min_f \frac{1}{n} \sum_{i=1}^n \beta_i l(f(\boldsymbol{x}_i), y_i) \]

由於不知道這個比率 \(\beta_i\),我們需要估計它。有許多方法都可以用,包括一些花哨的運算元理論方法,試圖直接使用最小范數或最大熵原理重新校準期望運算元。對於任意一種這樣的方法,我們都需要從兩個分佈中抽取樣本:“真實”的分佈 \(p\),透過訪問測試資料獲取;訓練集 \(q\),透過人工合成的很容易獲得。請注意,我們只需要特徵 \(\boldsymbol{x} \sim p(\boldsymbol{x})\),不需要訪問標籤 \(y \sim p(y)\)

在這種情況下,有一種非常有效的方法可以得到幾乎與原始方法一樣好的結果:邏輯斯蒂迴歸(logistic regression)。這是用於二元分類的 softmax 迴歸的一個特例。綜上所述,我們學習了一個分類器來區分從 \(p(\boldsymbol{x})\) 抽取的資料和從 \(q(\boldsymbol{x})\) 抽取的資料。如果無法區分這兩個分佈,則意味著相關的樣本可能來自這兩個分佈中的任何一個。此外,任何可以很好區分的樣本都應該相應地顯著增加或減少權重。

為了簡單起見,假設我們分別從 \(p(\boldsymbol{x})\)\(q(\boldsymbol{x})\) 兩個分佈中抽取相同數量的樣本。現在用 \(z\) 標籤表示:從 \(p\) 抽取的資料為 \(1\),從 \(q\) 抽取的資料為 \(−1\)。然後,混合資料集中的機率由下式給出:

\[P(z=1|\boldsymbol{x}) = \frac{p(\boldsymbol{x})}{p(\boldsymbol{x}) + q(\boldsymbol{x})} \implies \frac{P(z=1|\boldsymbol{x})}{P(z=-1|\boldsymbol{x})} = \frac{p(\boldsymbol{x})}{q(\boldsymbol{x})} \]

因此,如果使用 logistic 迴歸方法,其中 \(P(z=1|\boldsymbol{x}) = \frac{1}{1 + \exp(-h(\boldsymbol{x}))}\)(ℎ是一個引數化函式),則很自然有:

\[\beta_i = \frac{p(\boldsymbol{x})}{q(\boldsymbol{x})} = \frac{P(z=1|\boldsymbol{x})}{P(z=-1|\boldsymbol{x})} = \frac{\frac{1}{1 + \exp(-h(\boldsymbol{x}_i))}}{1 - \frac{1}{1 + \exp(-h(\boldsymbol{x}_i))}} = \exp (h(\boldsymbol{x}_i)) \]

在得到這個式子之後,接下來就只剩下兩個問題了。第一個問題是關於區分來自兩個分佈的資料;第二個問題是關於加權經驗風險的最小化問題。問題二里,要對其中的項加權 \(\beta_i\)

現在,我們來看一下完整的協變數偏移糾正演算法。假設我們有一個訓練集 \(\{(\boldsymbol{x}_1, y_1), \dots, (\boldsymbol{x}_n, y_n)\}\) 和一個未標記的測試集 \(\{\boldsymbol{u}_1, \dots, \boldsymbol{u}_m \}\)。對於協變數偏移,我們假設 \(\boldsymbol{x}_i (1 \le i \le n)\) 來自某個源分佈,\(\boldsymbol{u}_i\) 來自目標分佈。以下是糾正協變數偏移的典型演算法:

  1. 生成一個二元分類訓練集:\(\{(\boldsymbol{x}_1, -1), \dots, (\boldsymbol{x}_n, -1), (\boldsymbol{u}_1, 1), \dots, (\boldsymbol{u}_m, 1)\}\)
  2. 用邏輯斯蒂迴歸訓練二元分類器得到函式 \(h\)
  3. 使用 \(\beta_i = \exp(h(\boldsymbol{x}_i))\) 或更好的 \(\beta_i = \min (\exp(h(\boldsymbol{x}_i)), c)\)\(c\) 為常量)對訓練資料進行加權。
  4. 使用權重 \(\beta_i\) 進行經驗風險最小化中 \(\{(\boldsymbol{x}_1, y_1), \dots, (\boldsymbol{x}_n, y_n)\}\) 的訓練。

請注意,上述演算法依賴於一個重要的假設:需要目標分佈(例如,測試分佈)中的每個資料樣本在訓練時出現的機率非零。如果我們找到 \(p(\boldsymbol{x}) > 0\)\(q(\boldsymbol{x})=0\) 的點,那麼相應的重要性權重會是無窮大。

3. 標籤偏移糾正

原書這段寫的非常奇怪,很難看懂。寫一點一家之言。

假設我們處理的是 \(k\) 個類別的分類任務。使用與上文中相同符號,\(q\)\(p\) 中分別是源分佈(例如訓練時的分佈)和目標分佈(例如測試時的分佈)。假設標籤的分佈隨時間變化:\(q(y) \not = p(y)\),但類別條件分佈保持不變:\(q(\boldsymbol{x}|y)=p(\boldsymbol{x}|y)\)。如果源分佈 \(q(y)\) 是“錯誤的”,我們可以根據之前定義的真實風險中的恆等式進行更正:

\[\iint l(f(\boldsymbol{x}), y) p(\boldsymbol{x} | y) p(y) \mathrm{d}\boldsymbol{x} \mathrm{d} y = \iint l(f(\boldsymbol{x}), y) q(\boldsymbol{x}|y) q(y) \frac{p(y)}{q(y)} \mathrm{d} \boldsymbol{x} \mathrm{d}y \]

這裡,重要性權重將對應於標籤似然比率:

\[\beta_i \overset{\text{def}}{=} \frac{p(y_i)}{q(y_i)} \]

標籤偏移的一個好處是,如果我們在源分佈上有一個相當好的模型,那麼我們可以得到對這些權重的一致估計,而不需要處理周邊的其他維度。在深度學習中,輸入往往是高維物件(如影像),而標籤通常是低維(如類別)。因此處理標籤偏移會容易一些。

為了估計目標標籤分佈 \(p(y)\),我們首先採用效能相當好的現成的分類器(通常基於訓練資料,即 \(q\) 對應的資料進行訓練),並使用驗證集(也來自訓練分佈)計算其混淆矩陣。混淆矩陣 \(\boldsymbol{C}\) 是一個 \(k \times k\) 矩陣,其中每列對應於標籤類別,每行對應於模型的預測類別。每個單元格的值 \(c_{ij}\) 是驗證集中真實標籤為 \(j\),而模型預測為 \(i\) 的樣本數量所佔的比例。這個矩陣可以理解為在源分佈上當真實標籤為 \(j\) 發生時,模型預測為 \(i\) 的條件機率 \(P_q(i|j)\)。由於標籤偏移僅僅是標籤的邊緣機率 \(P(y)\) 改變,因此這個式子在目標分佈上的條件機率 \(P_p(i | j)\) 與在源分佈上的條件機率 \(P_q(i|j)\) 相等。

現在,我們不能直接計算目標資料上的混淆矩陣,因為我們無法看到真實環境下的樣本的標籤,除非我們再搭建一個複雜的實時標註流程。然而,我們所能做的是將所有模型在測試時的預測取平均數,得到平均模型輸出 \(\mu(\hat{\boldsymbol{y}}) \in \mathbb{R}^k\),其中第 \(i\) 個元素 \(\mu (\hat{y}_i)\) 是我們模型預測測試集中 \(i\) 的總預測分數,也可以理解為在目標分佈下模型預測為 \(i\) 類別的機率之和。

結果表明,如果我們的分類器一開始就相當準確,並且目標資料只包含我們以前見過的類別,以及如果標籤偏移假設成立(這裡最強的假設),我們就可以透過求解一個簡單的線性系統來估計測試集的標籤分佈:

\[\boldsymbol{C} p(\boldsymbol{y}) = \mu(\hat{\boldsymbol{y}}) \]

因為作為一個估計,\(\sum_{j=1}^k c_{ij} p(y_j) = \mu (\hat{y}_i)\) 對所有 \(1 \le i \le k\) 成立,其中 \(p(y_j)\)\(k\) 維標籤分佈向量 \(p(\boldsymbol{y})\) 的第 \(j\) 個元素。如果我們的分類器一開始就足夠精確,那麼混淆矩陣 \(\boldsymbol{C}\) 的對角線元素將會比較大,因此是可逆的, 進而我們可以得到一個解 \(p(\boldsymbol{y}) = \boldsymbol{C}^{-1} \mu(\hat{\boldsymbol{y}})\)

這個式子原書的跳步非常嚴重,所以展開了寫一下。

\[\begin{align} \sum_{j=1}^k c_{ij} p(y_j) &= \sum_{j=1}^k P_q(\text{model predict}=i|\text{true label}=j) P_p(\text{true label}=j) \\ &= \sum_{j=1}^k P_p(\text{model predict}=i|\text{true label}=j) P_p(\text{true label}=j) \\ &= \sum_{j=1}^k P_p(\text{model predict}=i,\text{true label}=j) \\ &= P_p(\text{model predict}=i) = \mu (\hat{y}_i) \end{align} \]

因此,上式成立。

因為我們觀測源資料上的標籤,所以很容易估計分佈 \(q(y)\)。那麼對於標籤為 \(y_i\) 的任何訓練樣本 \(i\),我們可以使用我們估計的 \(p(y_i) / q(y_i)\) 比率來計算權重 \(\beta_i\),並將其代入加權經驗風險最小化的式子中。

4. 概念偏移糾正

概念偏移很難用原則性的方式解決。除了從零開始收集新標籤和訓練,別無妙方。幸運的是,在實踐中極端的偏移是罕見的,通常情況下,概念的變化總是緩慢的。在這種情況下,我們可以使用與訓練網路相同的方法,使其適應資料的變化。換言之,我們使用新資料更新現有的網路權重,而不是從頭開始訓練。

4.9.4 學習問題的分類法

1. 批次學習

批次學習(batch learning)中,我們可以訪問一組訓練特徵和標籤 \(\{ (\boldsymbol{x}_1, y_1), \dots, (\boldsymbol{x}_n, y_n) \}\),我們使用這些特性和標籤訓練 \(f(\boldsymbol{x})\)。然後,我們部署此模型來對來自同一分佈的新資料 \((\boldsymbol{x},y)\) 進行評分。

2. 線上學習

除了“批次”地學習,我們還可以單個“線上”學習資料 \((\boldsymbol{x}_i, y_i)\)。更具體地說,我們首先觀測到 \(\boldsymbol{x}_i\),然後我們得出一個估計值 \(f(\boldsymbol{x}_i)\),只有當我們做到這一點後,我們才觀測到 \(y_i\)。然後根據我們的決定,我們會得到獎勵或損失。

線上學習(online learning)中,我們有以下的迴圈。在這個迴圈中,給定新的觀測結果,我們會不斷地改進我們的模型。

\[\text{模型 } f_t \rightarrow \text{資料 } \boldsymbol{x}_t \rightarrow \text{估計 } f_t(\boldsymbol{x}_t) \rightarrow \text{觀測 } y_t \rightarrow \text{損失 } l(y_t, f_t(\boldsymbol{x}_t)) \rightarrow \text{模型 } f_{t+1} \]

3. 老虎只因

老虎只因(bandits)是上述問題的一個特例。雖然在大多數學習問題中,我們有一個連續引數化的函式 \(f\)(例如,一個深度網路)。但在一個老虎只因問題中,我們只有有限數量的手臂可以拉動。也就是說,我們可以採取的行動是有限的。對於這個更簡單的問題,可以獲得更強的最優性理論保證。

4. 控制

在很多情況下,環境會記住我們所做的事。不一定是以一種對抗的方式,但它會記住,而且它的反應將取決於之前發生的事情。許多這樣的演算法形成了一個環境模型,在這個模型中,他們的行為使得他們的決策看起來不那麼隨機。近年來,控制理論也被用於自動調整超引數,以獲得更好的解構和重建質量,提高生成文字的多樣性和生成影像的重建質量。

5. 強化學習

強化學習(reinforcement learning)強調如何基於環境而行動,以取得最大化的預期利益。

6. 考慮到環境

上述不同情況之間的一個關鍵區別是:在靜止環境中可能一直有效的相同策略,在環境能夠改變的情況下可能不會始終有效。環境變化的速度和方式在很大程度上決定了我們可以採用的演算法型別。

4.10 Kaggle:預測房價

使用 Kaggle 跑程式碼的時候發現,to_csv() 函式會出問題,我也改不動,因此本章派生實驗跳過。

相關文章