機器學習基礎:過擬合、欠擬合、梯度消失與爆炸相關

過擬合、欠擬合及其解決方案

  1. 過擬合、欠擬合的概念
  2. 權重衰減
  3. 丟棄法

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

訓練誤差和泛化誤差

在解釋上述現象之前,我們需要區分訓練誤差(training error)和泛化誤差(generalization error)。通俗來講,前者指模型在訓練數據集上表現出的誤差,后者指模型在任意一個測試數據樣本上表現出的誤差的期望,并常常通過測試數據集上的誤差來近似。計算訓練誤差和泛化誤差可以使用之前介紹過的損失函數,例如線性回歸用到的平方損失函數和softmax回歸用到的交叉熵損失函數。

機器學習模型應關注降低泛化誤差。

模型選擇

驗證數據集

從嚴格意義上講,測試集只能在所有超參數和模型參數選定后使用一次。不可以使用測試數據選擇模型,如調參。由于無法從訓練誤差估計泛化誤差,因此也不應只依賴訓練數據選擇模型。鑒于此,我們可以預留一部分在訓練數據集和測試數據集以外的數據來進行模型選擇。這部分數據被稱為驗證數據集,簡稱驗證集(validation set)。例如,我們可以從給定的訓練集中隨機選取一小部分作為驗證集,而將剩余部分作為真正的訓練集。

K折交叉驗證

由于驗證數據集不參與模型訓練,當訓練數據不夠用時,預留大量的驗證數據顯得太奢侈。一種改善的方法是K折交叉驗證(K-fold cross-validation)。在K折交叉驗證中,我們把原始訓練數據集分割成K個不重合的子數據集,然后我們做K次模型訓練和驗證。每一次,我們使用一個子數據集驗證模型,并使用其他K-1個子數據集來訓練模型。在這K次訓練和驗證中,每次用來驗證模型的子數據集都不同。最后,我們對這K次訓練誤差和驗證誤差分別求平均。

過擬合和欠擬合

接下來,我們將探究模型訓練中經常出現的兩類典型問題:

  • 一類是模型無法得到較低的訓練誤差,我們將這一現象稱作欠擬合(underfitting);
  • 另一類是模型的訓練誤差遠小于它在測試數據集上的誤差,我們稱該現象為過擬合(overfitting)。
    在實踐中,我們要盡可能同時應對欠擬合和過擬合。雖然有很多因素可能導致這兩種擬合問題,在這里我們重點討論兩個因素:模型復雜度和訓練數據集大小。

模型復雜度

為了解釋模型復雜度,我們以多項式函數擬合為例。給定一個由標量數據特征x和對應的標量標簽y組成的訓練數據集,多項式函數擬合的目標是找一個K階多項式函數

\hat{y} = b + \sum_{k=1}^K x^k w_k

來近似 y。在上式中,w_k是模型的權重參數,b是偏差參數。與線性回歸相同,多項式函數擬合也使用平方損失函數。特別地,一階多項式函數擬合又叫線性函數擬合。

給定訓練數據集,模型復雜度和誤差之間的關系:

Image Name

訓練數據集大小

影響欠擬合和過擬合的另一個重要因素是訓練數據集的大小。一般來說,如果訓練數據集中樣本數過少,特別是比模型參數數量(按元素計)更少時,過擬合更容易發生。此外,泛化誤差不會隨訓練數據集里樣本數量增加而增大。因此,在計算資源允許的范圍之內,我們通常希望訓練數據集大一些,特別是在模型復雜度較高時,例如層數較多的深度學習模型。

多項式擬合實驗

%matplotlib inline
import torch
import numpy as np
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l
print(torch.__version__)*/
## 初始化模型參數
n_train, n_test, true_w, true_b = 100, 100, [1.2, -3.4, 5.6], 5
features = torch.randn((n_train + n_test, 1))
poly_features = torch.cat((features, torch.pow(features, 2), torch.pow(features, 3)), 1) 
labels = (true_w[0] * poly_features[:, 0] + true_w[1] * poly_features[:, 1]
          + true_w[2] * poly_features[:, 2] + true_b)
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float)

定義模型參數

使用線性模型擬合多項式

def semilogy(x_vals, y_vals, x_label, y_label, x2_vals=None, y2_vals=None,
             legend=None, figsize=(3.5, 2.5)):
    # 用來生成圖片直接觀測 
    d2l.plt.xlabel(x_label)
    d2l.plt.ylabel(y_label)
    d2l.plt.semilogy(x_vals, y_vals)
    if x2_vals and y2_vals:
        d2l.plt.semilogy(x2_vals, y2_vals, linestyle=':')
        d2l.plt.legend(legend)
num_epochs, loss = 100, torch.nn.MSELoss()

def fit_and_plot(train_features, test_features, train_labels, test_labels):
    # 初始化網絡模型
    net = torch.nn.Linear(train_features.shape[-1], 1)
    # 通過Linear文檔可知,pytorch已經將參數初始化了,所以我們這里就不手動初始化了
    
    # 設置批量大小
    batch_size = min(10, train_labels.shape[0])    
    dataset = torch.utils.data.TensorDataset(train_features, train_labels)      # 設置數據集
    train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True) # 設置獲取數據方式
    
    optimizer = torch.optim.SGD(net.parameters(), lr=0.01)                      # 設置優化函數,使用的是隨機梯度下降優化
    # SGD函數優化參數
    train_ls, test_ls = [], []
    for _ in range(num_epochs):
        for X, y in train_iter:                                                 # 取一個批量的數據
            l = loss(net(X), y.view(-1, 1))                                     # 輸入到網絡中計算輸出,并和標簽比較求得損失函數
            optimizer.zero_grad()                                               # 梯度清零,防止梯度累加干擾優化
            l.backward()                                                        # 求梯度
            optimizer.step()                                                    # 迭代優化函數,進行參數優化
        train_labels = train_labels.view(-1, 1)
        test_labels = test_labels.view(-1, 1)
        train_ls.append(loss(net(train_features), train_labels).item())         # 將訓練損失保存到train_ls中
        test_ls.append(loss(net(test_features), test_labels).item())            # 將測試損失保存到test_ls中
    print('final epoch: train loss', train_ls[-1], 'test loss', test_ls[-1])    
    semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss',
             range(1, num_epochs + 1), test_ls, ['train', 'test'])
    print('weight:', net.weight.data,
          '\nbias:', net.bias.data)

三階函數的擬合結果(正常)

fit_and_plot(poly_features[:n_train, :], poly_features[n_train:, :], labels[:n_train], labels[n_train:])
三階多項式擬合的損失圖像

線性函數的擬合結果(欠擬合)

fit_and_plot(features[:n_train, :], features[n_train:, :], labels[:n_train], labels[n_train:])
線性函數擬合的損失圖像

訓練樣本不足時的擬合結果(過擬合)

fit_and_plot(poly_features[0:2, :], poly_features[n_train:, :], labels[0:2], labels[n_train:])
訓練樣本不足時的損失圖像

權重衰減

方法

權重衰減等價于 L_2 范數正則化(regularization)。正則化通過為模型損失函數添加懲罰項使學出的模型參數值較小,是應對過擬合的常用手段。

L2 范數正則化(regularization)

L_2范數正則化在模型原損失函數基礎上添加L_2范數懲罰項,從而得到訓練所需要最小化的函數。L_2范數懲罰項指的是模型權重參數每個元素的平方和與一個正的常數的乘積。以線性回歸中的線性回歸損失函數為例

\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范數懲罰項的新損失函數為

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

其中超參數\lambda > 0。當權重參數均為0時,懲罰項最小。當\lambda較大時,懲罰項在損失函數中的比重較大,這通常會使學到的權重參數的元素較接近0。當\lambda設為0時,懲罰項完全不起作用。上式中L_2范數平方|\boldsymbol{w}|^2展開后得到w_1^2 + w_2^2
有了L_2范數懲罰項后,在小批量隨機梯度下降中,我們將線性回歸一節中權重w_1w_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),\\ 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}

可見,L_2范數正則化令權重w_1w_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
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l

print(torch.__version__)
## 初始化模型參數
# 與前面觀察過擬合和欠擬合現象的時候相似,在這里不再解釋。
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]
## 定義L2范數懲罰項
def l2_penalty(w):
    return (w**2).sum() / 2
## 定義訓練和測試
batch_size, num_epochs, lr = 1, 100, 0.003
net, loss = d2l.linreg, d2l.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()
            d2l.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())
    d2l.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())
## 觀察過擬合
fit_and_plot(lambd=0)
## 使用權重衰減
fit_and_plot(lambd=3)
過擬合圖像

注:上面的紅線是未加懲罰項的結果,下面的紅線是添加懲罰項后的結果,很清楚看出增加懲罰項后驗證機損失顯著下降

pytorh簡潔代碼實現

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())
    d2l.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)
fit_and_plot_pytorch(3)

丟棄法

多層感知機中神經網絡圖描述了一個單隱藏層的多層感知機。其中輸入個數為4,隱藏單元個數為5,且隱藏單元h_ii=1, \ldots, 5)的計算表達式為

h_i = \phi\left(x_1 w_{1i} + x_2 w_{2i} + x_3 w_{3i} + x_4 w_{4i} + b_i\right)

這里\phi是激活函數,x_1, \ldots, x_4是輸入,隱藏單元i的權重參數為w_{1i}, \ldots, w_{4i},偏差參數為b_i。當對該隱藏層使用丟棄法時,該層的隱藏單元將有一定概率被丟棄掉。設丟棄概率為p,那么有p的概率h_i會被清零,有1-p的概率h_i會除以1-p做拉伸。丟棄概率是丟棄法的超參數。具體來說,設隨機變量\xi_i為0和1的概率分別為p1-p。使用丟棄法時我們計算新的隱藏單元h_i'

h_i' = \frac{\xi_i}{1-p} h_i

由于E(\xi_i) = 1-p,因此

E(h_i') = \frac{E(\xi_i)}{1-p}h_i = h_i

即丟棄法不改變其輸入的期望值。讓我們對之前多層感知機的神經網絡中的隱藏層使用丟棄法,一種可能的結果如圖所示,其中h_2h_5被清零。這時輸出值的計算不再依賴h_2h_5,在反向傳播時,與這兩個隱藏單元相關的權重的梯度均為0。由于在訓練中隱藏層神經元的丟棄是隨機的,即h_1, \ldots, h_5都有可能被清零,輸出層的計算無法過度依賴h_1, \ldots, h_5中的任一個,從而在訓練模型時起到正則化的作用,并可以用來應對過擬合。在測試模型時,我們為了拿到更加確定性的結果,一般不使用丟棄法

隱藏層丟棄h2和h5的神經網絡

丟棄法實現

%matplotlib inline
import torch
import torch.nn as nn
import numpy as np
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l

print(torch.__version__)
def dropout(X, drop_prob):
    X = X.float()
    assert 0 <= drop_prob <= 1
    keep_prob = 1 - drop_prob
    # 這種情況下把全部元素都丟棄
    if keep_prob == 0:
        return torch.zeros_like(X)
    mask = (torch.rand(X.shape) < keep_prob).float()
    
    return mask * X / keep_prob
X = torch.arange(16).view(2, 8)
dropout(X, 0)
dropout(X, 0.5)
dropout(X, 1.0)
# 參數的初始化
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256

W1 = torch.tensor(np.random.normal(0, 0.01, size=(num_inputs, num_hiddens1)), dtype=torch.float, requires_grad=True)
b1 = torch.zeros(num_hiddens1, requires_grad=True)
W2 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens1, num_hiddens2)), dtype=torch.float, requires_grad=True)
b2 = torch.zeros(num_hiddens2, requires_grad=True)
W3 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens2, num_outputs)), dtype=torch.float, requires_grad=True)
b3 = torch.zeros(num_outputs, requires_grad=True)

params = [W1, b1, W2, b2, W3, b3]
drop_prob1, drop_prob2 = 0.2, 0.5

def net(X, is_training=True):
    X = X.view(-1, num_inputs)
    H1 = (torch.matmul(X, W1) + b1).relu()
    if is_training:  # 只在訓練模型時使用丟棄法
        H1 = dropout(H1, drop_prob1)  # 在第一層全連接后添加丟棄層
    H2 = (torch.matmul(H1, W2) + b2).relu()
    if is_training:
        H2 = dropout(H2, drop_prob2)  # 在第二層全連接后添加丟棄層
    return torch.matmul(H2, W3) + b3
def evaluate_accuracy(data_iter, net):
    acc_sum, n = 0.0, 0
    for X, y in data_iter:
        if isinstance(net, torch.nn.Module):
            net.eval() # 評估模式, 這會關閉dropout
            acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
            net.train() # 改回訓練模式
        else: # 自定義的模型
            if('is_training' in net.__code__.co_varnames): # 如果有is_training這個參數
                # 將is_training設置成False
                acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item() 
            else:
                acc_sum += (net(X).argmax(dim=1) == y).float().sum().item() 
        n += y.shape[0]
    return acc_sum / n
num_epochs, lr, batch_size = 5, 100.0, 256  # 這里的學習率設置的很大,原因與之前相同。
loss = torch.nn.CrossEntropyLoss()
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, root='/home/kesci/input/FashionMNIST2065')
d2l.train_ch3(
    net,
    train_iter,
    test_iter,
    loss,
    num_epochs,
    batch_size,
    params,
    lr)

簡潔代碼實現

net = nn.Sequential(
        d2l.FlattenLayer(),
        nn.Linear(num_inputs, num_hiddens1),
        nn.ReLU(),
        nn.Dropout(drop_prob1),
        nn.Linear(num_hiddens1, num_hiddens2), 
        nn.ReLU(),
        nn.Dropout(drop_prob2),
        nn.Linear(num_hiddens2, 10)
        )

for param in net.parameters():
    nn.init.normal_(param, mean=0, std=0.01)
optimizer = torch.optim.SGD(net.parameters(), lr=0.5)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

梯度消失和梯度爆炸

深度模型有關數值穩定性的典型問題是消失(vanishing)和爆炸(explosion)。
梯度消失會導致模型訓練困難,對參數的優化步長過小,收效甚微,模型收斂十分緩慢
梯度爆炸會導致模型訓練困難,對參數的優化步長過大,難以收斂

\color{red}{在深層網絡中盡量避免選擇sigmoid和tanh激活函數,原因是這兩個激活函數會把元素轉換到[0, 1]和[-1, 1]之間,會加劇梯度消失的現象}

當神經網絡的層數較多時,模型的數值穩定性容易變差。

假設一個層數為L的多層感知機的第l\boldsymbol{H}^{(l)}的權重參數為\boldsymbol{W}^{(l)},輸出層\boldsymbol{H}^{(L)}的權重參數為\boldsymbol{W}^{(L)}。為了便于討論,不考慮偏差參數,且設所有隱藏層的激活函數為恒等映射(identity mapping)\phi(x) = x。給定輸入\boldsymbol{X},多層感知機的第l層的輸出\boldsymbol{H}^{(l)} = \boldsymbol{X} \boldsymbol{W}^{(1)} \boldsymbol{W}^{(2)} \ldots \boldsymbol{W}^{(l)}。此時,如果層數l較大,\boldsymbol{H}^{(l)}的計算可能會出現衰減或爆炸。舉個例子,假設輸入和所有層的權重參數都是標量,如權重參數為0.2和5,多層感知機的第30層輸出為輸入\boldsymbol{X}分別與0.2^{30} \approx 1 \times 10^{-21}(消失)和5^{30} \approx 9 \times 10^{20}(爆炸)的乘積。當層數較多時,梯度的計算也容易出現消失或爆炸。

隨機初始化模型參數

在神經網絡中,通常需要隨機初始化模型參數。下面我們來解釋這樣做的原因。

回顧多層感知機一節描述的多層感知機。為了方便解釋,假設輸出層只保留一個輸出單元o_1(刪去o_2o_3以及指向它們的箭頭),且隱藏層使用相同的激活函數。如果將每個隱藏單元的參數都初始化為相等的值,那么在正向傳播時每個隱藏單元將根據相同的輸入計算出相同的值,并傳遞至輸出層。在反向傳播中,每個隱藏單元的參數梯度值相等。因此,這些參數在使用基于梯度的優化算法迭代后值依然相等。之后的迭代也是如此。在這種情況下,無論隱藏單元有多少,隱藏層本質上只有1個隱藏單元在發揮作用。因此,正如在前面的實驗中所做的那樣,我們通常將神經網絡的模型參數,特別是權重參數,進行隨機初始化。

全聯接神經網絡

PyTorch的默認隨機初始化

隨機初始化模型參數的方法有很多。在線性回歸的簡潔實現中,我們使用torch.nn.init.normal_()使模型net的權重參數采用正態分布的隨機初始化方式。不過,PyTorch中nn.Module的模塊參數都采取了較為合理的初始化策略(不同類型的layer具體采樣的哪一種初始化方法的可參考源代碼),因此一般不用我們考慮。

Xavier隨機初始化

還有一種比較常用的隨機初始化方法叫作Xavier隨機初始化。
假設某全連接層的輸入個數為a,輸出個數為b,Xavier隨機初始化將使該層中權重參數的每個元素都隨機采樣于均勻分布

U\left(-\sqrt{\frac{6}{a+b}}, \sqrt{\frac{6}{a+b}}\right).

它的設計主要考慮到,模型參數初始化后,每層輸出的方差不該受該層輸入個數影響,且每層梯度的方差也不該受該層輸出個數影響。

考慮環境因素

協變量偏移

這里我們假設,雖然輸入的分布可能隨時間而改變,但是標記函數,即條件分布P(y∣x)不會改變。雖然這個問題容易理解,但在實踐中也容易忽視。

想想區分貓和狗的一個例子。我們的訓練數據使用的是貓和狗的真實的照片,但是在測試時,我們被要求對貓和狗的卡通圖片進行分類。

cat cat dog dog
image
image
image
image

測試數據:

cat cat dog dog
image
image
image
image

顯然,這不太可能奏效。訓練集由照片組成,而測試集只包含卡通。在一個看起來與測試集有著本質不同的數據集上進行訓練,而不考慮如何適應新的情況,這是不是一個好主意。不幸的是,這是一個非常常見的陷阱。
統計學家稱這種協變量變化是因為問題的根源在于特征分布的變化(即協變量的變化)。數學上,我們可以說P(x)改變了,但P(y∣x)保持不變。盡管它的有用性并不局限于此,當我們認為x導致y時,協變量移位通常是正確的假設。

標簽偏移

當我們認為導致偏移的是標簽P(y)上的邊緣分布的變化,但類條件分布是不變的P(x∣y)時,就會出現相反的問題。當我們認為y導致x時,標簽偏移是一個合理的假設。例如,通常我們希望根據其表現來預測診斷結果。在這種情況下,我們認為診斷引起的表現,即疾病引起的癥狀。有時標簽偏移和協變量移位假設可以同時成立。例如,當真正的標簽函數是確定的和不變的,那么協變量偏移將始終保持,包括如果標簽偏移也保持。有趣的是,當我們期望標簽偏移和協變量偏移保持時,使用來自標簽偏移假設的方法通常是有利的。這是因為這些方法傾向于操作看起來像標簽的對象,這(在深度學習中)與處理看起來像輸入的對象(在深度學習中)相比相對容易一些。

病因(要預測的診斷結果)導致 癥狀(觀察到的結果)。

訓練數據集,數據很少只包含流感p(y)的樣本。

而測試數據集有流感p(y)和流感q(y),其中不變的是流感癥狀p(x|y)。

概念偏移

另一個相關的問題出現在概念轉換中,即標簽本身的定義發生變化的情況。這聽起來很奇怪,畢竟貓就是貓。的確,貓的定義可能不會改變,但我們能不能對軟飲料也這么說呢?事實證明,如果我們周游美國,按地理位置轉移數據來源,我們會發現,即使是如圖所示的這個簡單術語的定義也會發生相當大的概念轉變。

image

美國軟飲料名稱的概念轉變
如果我們要建立一個機器翻譯系統,分布P(y∣x)可能因我們的位置而異。這個問題很難發現。另一個可取之處是P(y∣x)通常只是逐漸變化。

eg: 一個在冬季部署的物品推薦系統在夏季的物品推薦列表中出現了圣誕禮物
可以理解為在夏季的物品推薦系統與冬季相比,時間或者說季節發生了變化,導致了夏季推薦圣誕禮物的不合理的現象,這個現象是由于協變量時間發生了變化造成的。


PS:\color{red}{如果數據量足夠的情況下,確保訓練數據集和測試集中的數據取自同一個數據集,可以防止協變量偏移和標簽偏移是正確的。如果數據量很少,少到測試集中存在訓練集中未包含的標簽,就會發生標簽偏移}

應用實例:泰坦尼克號數據集訓練

%matplotlib inline
import torch
import torch.nn as nn
import numpy as np
import pandas as pd
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l
print(torch.__version__)
torch.set_default_tensor_type(torch.FloatTensor)
## 獲取和讀取數據集

# 比賽數據分為訓練數據集和測試數據集。兩個數據集都包括每棟房子的特征,如街道類型、建造年份、房頂類型、地下室狀況等特征值。這些特征值有連續的數字、離散的標簽甚至是缺失值“na”。只有訓練數據集包括了每棟房子的價格,也就是標簽。我們可以訪問比賽網頁,點擊“Data”標簽,并下載這些數據集。

# 我們將通過`pandas`庫讀入并處理數據。在導入本節需要的包前請確保已安裝`pandas`庫。
# 假設解壓后的數據位于`/home/kesci/input/houseprices2807/`目錄,它包括兩個csv文件。下面使用`pandas`讀取這兩個文件。
test_data = pd.read_csv("/home/kesci/input/houseprices2807/house-prices-advanced-regression-techniques/test.csv")
train_data = pd.read_csv("/home/kesci/input/houseprices2807/house-prices-advanced-regression-techniques/train.csv")
# 訓練數據集包括1460個樣本、80個特征和1個標簽。
#train_data.shape
# 測試數據集包括1459個樣本和80個特征。我們需要將測試數據集中每個樣本的標簽預測出來。
#test_data.shape
# 讓我們來查看前4個樣本的前4個特征、后2個特征和標簽(SalePrice):
#train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]]
# 可以看到第一個特征是Id,它能幫助模型記住每個訓練樣本,但難以推廣到測試樣本,所以我們不使用它來訓練。我們將所有的訓練數據和測試數據的79個特征按樣本連結。
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))

我們對連續數值的特征做標準化(standardization):設該特征在整個數據集上的均值為\mu,標準差為\sigma。那么,我們可以將該特征的每個值先減去\mu再除以\sigma得到標準化后的每個特征值。對于缺失的特征值,我們將其替換成該特征的均值。

## 預處理數據
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features] = all_features[numeric_features].apply(
    lambda x: (x - x.mean()) / (x.std()))
# 標準化后,每個數值特征的均值變為0,所以可以直接用0來替換缺失值
all_features[numeric_features] = all_features[numeric_features].fillna(0)
# 接下來將離散數值轉成指示特征。舉個例子,假設特征MSZoning里面有兩個不同的離散值RL和RM,那么這一步轉換將去掉MSZoning特征,并新加兩個特征MSZoning\_RL和MSZoning\_RM,其值為0或1。如果一個樣本原來在MSZoning里的值為RL,那么有MSZoning\_RL=1且MSZoning\_RM=0。
# dummy_na=True將缺失值也當作合法的特征值并為其創建指示特征
all_features = pd.get_dummies(all_features, dummy_na=True)
all_features.shape
# 可以看到這一步轉換將特征數從79增加到了331。

#最后,通過`values`屬性得到NumPy格式的數據,并轉成`Tensor`方便后面的訓練。
n_train = train_data.shape[0]
train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float)
test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float)
train_labels = torch.tensor(train_data.SalePrice.values, dtype=torch.float).view(-1, 1)
## 訓練模型
loss = torch.nn.MSELoss()

def get_net(feature_num):
    net = nn.Linear(feature_num, 1)
    for param in net.parameters():
        nn.init.normal_(param, mean=0, std=0.01)
    return net
#下面定義比賽用來評價模型的對數均方根誤差。對數均方根誤差的實現如下。
def log_rmse(net, features, labels):
    with torch.no_grad():
        # 將小于1的值設成1,使得取對數時數值更穩定
        clipped_preds = torch.max(net(features), torch.tensor(1.0))
        rmse = torch.sqrt(2 * loss(clipped_preds.log(), labels.log()).mean())
    return rmse.item()
# 下面的訓練函數跟本章中前幾節的不同在于使用了Adam優化算法。相對之前使用的小批量隨機梯度下降,它對學習率相對不那么敏感。我們將在之后的“優化算法”一章里詳細介紹它。
def train(net, train_features, train_labels, test_features, test_labels,
          num_epochs, learning_rate, weight_decay, batch_size):
    train_ls, test_ls = [], []
    dataset = torch.utils.data.TensorDataset(train_features, train_labels)
    train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True)
    # 這里使用了Adam優化算法
    optimizer = torch.optim.Adam(params=net.parameters(), lr=learning_rate, weight_decay=weight_decay) 
    net = net.float()
    for epoch in range(num_epochs):
        for X, y in train_iter:
            l = loss(net(X.float()), y.float())
            optimizer.zero_grad()
            l.backward()
            optimizer.step()
        train_ls.append(log_rmse(net, train_features, train_labels))
        if test_labels is not None:
            test_ls.append(log_rmse(net, test_features, test_labels))
    return train_ls, test_ls
## K折交叉驗證
# 我們在模型選擇、欠擬合和過擬合中介紹了$K$折交叉驗證。它將被用來選擇模型設計并調節超參數。下面實現了一個函數,它返回第`i`折交叉驗證時所需要的訓練和驗證數據。
def get_k_fold_data(k, i, X, y):
    # 返回第i折交叉驗證時所需要的訓練和驗證數據
    assert k > 1
    fold_size = X.shape[0] // k
    X_train, y_train = None, None
    for j in range(k):
        idx = slice(j * fold_size, (j + 1) * fold_size)
        X_part, y_part = X[idx, :], y[idx]
        if j == i:
            X_valid, y_valid = X_part, y_part
        elif X_train is None:
            X_train, y_train = X_part, y_part
        else:
            X_train = torch.cat((X_train, X_part), dim=0)
            y_train = torch.cat((y_train, y_part), dim=0)
    return X_train, y_train, X_valid, y_valid
# 在k折交叉驗證中我們訓練k次并返回訓練和驗證的平均誤差
def k_fold(k, X_train, y_train, num_epochs,
           learning_rate, weight_decay, batch_size):
    train_l_sum, valid_l_sum = 0, 0
    for i in range(k):
        data = get_k_fold_data(k, i, X_train, y_train)
        net = get_net(X_train.shape[1])
        train_ls, valid_ls = train(net, *data, num_epochs, learning_rate,
                                   weight_decay, batch_size)
        train_l_sum += train_ls[-1]
        valid_l_sum += valid_ls[-1]
        if i == 0:
            d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'rmse',
                         range(1, num_epochs + 1), valid_ls,
                         ['train', 'valid'])
        print('fold %d, train rmse %f, valid rmse %f' % (i, train_ls[-1], valid_ls[-1]))
    return train_l_sum / k, valid_l_sum / k
## 模型選擇
# 我們使用一組未經調優的超參數并計算交叉驗證誤差。可以改動這些超參數來盡可能減小平均測試誤差。
# 有時候你會發現一組參數的訓練誤差可以達到很低,但是在K折交叉驗證上的誤差可能反而較高。這種現象很可能是由過擬合造成的。因此,當訓練誤差降低時,我們要觀察K折交叉驗證上的誤差是否也相應降低。
k, num_epochs, lr, weight_decay, batch_size = 5, 100, 5, 0, 64
train_l, valid_l = k_fold(k, train_features, train_labels, num_epochs, lr, weight_decay, batch_size)
print('%d-fold validation: avg train rmse %f, avg valid rmse %f' % (k, train_l, valid_l))
# 預測并在Kaggle中提交結果
# 下面定義預測函數。在預測之前,我們會使用完整的訓練數據集來重新訓練模型,并將預測結果存成提交所需要的格式。
def train_and_pred(train_features, test_features, train_labels, test_data,
                   num_epochs, lr, weight_decay, batch_size):
    net = get_net(train_features.shape[1])
    train_ls, _ = train(net, train_features, train_labels, None, None,
                        num_epochs, lr, weight_decay, batch_size)
    d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'rmse')
    print('train rmse %f' % train_ls[-1])
    preds = net(test_features).detach().numpy()
    test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0])
    submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)
    submission.to_csv('./submission.csv', index=False)
    # sample_submission_data = pd.read_csv("../input/house-prices-advanced-regression-techniques/sample_submission.csv")
train_and_pred(train_features, test_features, train_labels, test_data, num_epochs, lr, weight_decay, batch_size)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372