1. 程式人生 > 實用技巧 >(pytorch-深度學習系列)pytorch避免過擬合-權重衰減的實現-學習筆記

(pytorch-深度學習系列)pytorch避免過擬合-權重衰減的實現-學習筆記

pytorch避免過擬合-權重衰減的實現

首先學習基本的概念背景

L0範數是指向量中非0的元素的個數;(L0範數難優化求解)
L1範數是指向量中各個元素絕對值之和;
L2範數是指向量各元素的平方和然後求平方根。
權重衰減等價於 L2範數正則化(regularization)。正則化通過為模型損失函式新增懲罰項使學出的模型引數值較小,是應對過擬合的常用手段。

對於線性迴歸損失函式

$$ \ell(w_1, w_2, b) = \frac{1}{n} \sum_{i=1}^n \frac{1}{2}\left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y{(i)}\right)

2 $$

其中$w_1, w_2$是權重引數,$b$是偏差引數,樣本$i$的輸入為$x_1^{(i)}, x_2{(i)}$,標籤為$y{(i)}$,樣本數為$n$。將權重引數用向量$\boldsymbol{w} = [w_1, w_2]$表示,帶有$L_2$範數懲罰項的新損失函式為

$$\begin{aligned}\ell(w_1, w_2, b) + \frac{\lambda}{2n} |\boldsymbol{w}|^2,\end{aligned}$$

其中超引數$\lambda > 0$。當權重引數均為0時,懲罰項最小。當$\lambda$較大時,懲罰項在損失函式中的比重較大,這通常會使學到的權重引數的元素較接近0。當$\lambda$設為0時,懲罰項完全不起作用。上式中$L_2$範數平方$|\boldsymbol{w}|2$展開後得到$w_1

2 + w_2^2$。有了$L_2$範數懲罰項後,在小批量隨機梯度下降中,我們將線性迴歸中權重$w_1$和$w_2$的迭代方式更改為

$$ \begin{aligned} w_1 &\leftarrow \left(1- \frac{\eta\lambda}{|\mathcal{B}|} \right)w_1 - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}x_1^{(i)} \left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y^{(i)}\right),\end{aligned}$$

$$ \begin{aligned}\ w_2 &\leftarrow \left(1- \frac{\eta\lambda}{|\mathcal{B}|} \right)w_2 - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}x_2^{(i)} \left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y^{(i)}\right). \end{aligned} $$

注:
原線性迴歸的$w_1$和$w_2$的迭代方式為
$$ \begin{aligned} w_1 &\leftarrow w_1 - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \frac{ \partial \ell^{(i)}(w_1, w_2, b) }{\partial w_1} = w_1 - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}x_1^{(i)} \left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y^{(i)}\right), \end{aligned} $$
$$\begin{aligned}\ w_2 &\leftarrow w_2 - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \frac{ \partial \ell^{(i)}(w_1, w_2, b) }{\partial w_2} = w_2 - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}x_2^{(i)} \left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y^{(i)}\right), \end{aligned} $$
$$\begin{aligned}\ b &\leftarrow b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \frac{ \partial \ell^{(i)}(w_1, w_2, b) }{\partial b} = b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}\left(x_1^{(i)} w_1 + x_2^{(i)} w_2 + b - y^{(i)}\right). \end{aligned} $$

在上式中,$|\mathcal{B}|$ 代表每個小批量中的樣本個數(批量大小,batch size),$\eta$ 稱作學習率(learning rate)並取正數。這裡的批量大小和學習率的值是人為設定的,並不是通過模型訓練學出的,因此叫作超引數(hyperparameter)。我們通常所說的“調參”指的正是調節超引數,例如通過反覆試錯來找到超引數合適的值。在少數情況下,超引數也可以通過模型訓練學出。

可見,$L_2$範數正則化令權重$w_1$和$w_2$先自乘小於1的數,再減去不含懲罰項的梯度。因此,$L_2$範數正則化又叫權重衰減。權重衰減通過懲罰絕對值較大的模型引數為需要學習的模型增加了限制,這可能對過擬合有效。實際場景中,我們有時也在懲罰項中新增偏差元素的平方和。

設定一個過擬合問題

以高維線性迴歸為例來引入一個過擬合問題,並使用權重衰減來應對過擬合。設資料樣本特徵的維度為$p$。對於訓練資料集和測試資料集中特徵為$x_1, x_2, \ldots, x_p$的任一樣本,我們使用如下的線性函式來生成該樣本的標籤:

$$ y = 0.05 + \sum_{i = 1}^p 0.01x_i + \epsilon $$
其中噪聲項$\epsilon$服從均值為0、標準差為0.01的正態分佈。
為了較容易地觀察過擬合,我們考慮高維線性迴歸問題,如設維度$p=200$;同時,我們特意把訓練資料集的樣本數設低,如20。

%matplotlib inline
import torch
import torch.nn as nn
import numpy as np

n_train, n_test, num_inputs = 20, 100, 200
true_w, true_b = torch.ones(num_inputs, 1) * 0.01, 0.05

features = torch.randn((n_train + n_test, num_inputs))
labels = torch.matmul(features, true_w) + true_b
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float)
train_features, test_features = features[:n_train, :], features[n_train:, :]
train_labels, test_labels = labels[:n_train], labels[n_train:]

這裡就定義好了線性迴歸問題,現在開始設定模型進行線性迴歸求解:

隨機初始化模型引數的函式:

def init_params():
    w = torch.randn((num_inputs, 1), requires_grad=True)
    b = torch.zeros(1, requires_grad=True)
    return [w, b]

定義$L_2$範數懲罰項:

def l2_penalty(w):
    return (w**2).sum() / 2

定義訓練模型需要的函式

def linreg(X, w, b):
    return torch.mm(X, w) + b

def squared_loss(y_hat, y): 
    # 注意這裡返回的是向量, 另外, pytorch裡的MSELoss並沒有除以 2
    return ((y_hat - y.view(y_hat.size())) ** 2) / 2
    
def sgd(params, lr, batch_size):
    # 為了和原書保持一致,這裡除以了batch_size,但是應該是不用除的,因為一般用PyTorch計算loss時就預設已經
    # 沿batch維求了平均了。
    for param in params:
        param.data -= lr * param.grad / batch_size # 注意這裡更改param時用的是param.data

def semilogy(x_vals, y_vals, x_label, y_label, x2_vals=None, y2_vals=None,
             legend=None, figsize=(3.5, 2.5)):
    set_figsize(figsize)
    plt.xlabel(x_label)
    plt.ylabel(y_label)
    plt.semilogy(x_vals, y_vals)
    if x2_vals and y2_vals:
        plt.semilogy(x2_vals, y2_vals, linestyle=':')
        plt.legend(legend)
    # plt.show()

訓練模型:

batch_size, num_epochs, lr = 1, 100, 0.003
net, loss = linreg, squared_loss

dataset = torch.utils.data.TensorDataset(train_features, train_labels)
train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True)

def fit_and_plot(lambd):
    w, b = init_params()
    train_ls, test_ls = [], []
    for _ in range(num_epochs):
        for X, y in train_iter:
            # 添加了L2範數懲罰項
            l = loss(net(X, w, b), y) + lambd * l2_penalty(w)
            l = l.sum()
            
            if w.grad is not None:
                w.grad.data.zero_()
                b.grad.data.zero_()
            l.backward()
            sgd([w, b], lr, batch_size)
        train_ls.append(loss(net(train_features, w, b), train_labels).mean().item())
        test_ls.append(loss(net(test_features, w, b), test_labels).mean().item())
    semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss',
                 range(1, num_epochs + 1), test_ls, ['train', 'test'])
    print('L2 norm of w:', w.norm().item())

訓練並測試高維線性迴歸模型。當lambd設為0時,我們沒有使用權重衰減。結果訓練誤差遠小於測試集上的誤差。這是典型的過擬合現象。

fit_and_plot(lambd=0)

使用權重衰減

fit_and_plot(lambd=3)

你會發現訓練誤差雖然有所提高,但測試集上的誤差有所下降。

可以直接在構造優化器例項時通過weight_decay引數來指定權重衰減超引數預設下,PyTorch會對權重和偏差同時衰減。我們可以分別對權重和偏差構造優化器例項,從而只對權重衰減。
修改上面的訓練程式碼:

def fit_and_plot_pytorch(wd):
    # 對權重引數衰減。權重名稱一般是以weight結尾
    net = nn.Linear(num_inputs, 1)
    nn.init.normal_(net.weight, mean=0, std=1)
    nn.init.normal_(net.bias, mean=0, std=1)
    optimizer_w = torch.optim.SGD(params=[net.weight], lr=lr, weight_decay=wd) # 對權重引數衰減
    optimizer_b = torch.optim.SGD(params=[net.bias], lr=lr)  # 不對偏差引數衰減
    
    train_ls, test_ls = [], []
    for _ in range(num_epochs):
        for X, y in train_iter:
            l = loss(net(X), y).mean()
            optimizer_w.zero_grad()
            optimizer_b.zero_grad()
            
            l.backward()
            
            # 對兩個optimizer例項分別呼叫step函式,從而分別更新權重和偏差
            optimizer_w.step()
            optimizer_b.step()
        train_ls.append(loss(net(train_features), train_labels).mean().item())
        test_ls.append(loss(net(test_features), test_labels).mean().item())
    semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss',
                 range(1, num_epochs + 1), test_ls, ['train', 'test'])
    print('L2 norm of w:', net.weight.data.norm().item())

通過設定不同的衰減權重:

fit_and_plot_pytorch(0) #labmda=0,不衰減
fit_and_plot_pytorch(3) #labmda=3,衰減