梯度消失、梯度爆炸以及Kaggle房價預測
- 梯度消失和梯度爆炸
- 考慮到環境因素的其他問題
- Kaggle房價預測
梯度消失和梯度爆炸
深度模型有關數值穩定性的典型問題是消失(vanishing)和爆炸(explosion)。
當神經網絡的層數較多時,模型的數值穩定性容易變差。
假設一個層數為
的多層感知機的第層的權重參數為,輸出層的權重參數為。為了便於討論,不考慮偏差參數,且設所有隱藏層的激活函數為恆等映射(identity mapping)。給定輸入,多層感知機的第層的輸出。此時,如果層數較大,的計算可能會出現衰減或爆炸。舉個例子,假設輸入和所有層的權重參數都是標量,如權重參數為0.2和5,多層感知機的第30層輸出為輸入分別與(消失)和
(爆炸)的乘積。當層數較多時,梯度的計算也容易出現消失或爆炸。
隨機初始化模型參數
在神經網絡中,通常需要隨機初始化模型參數。下面我們來解釋這樣做的原因。
回顧多層感知機一節描述的多層感知機。為了方便解釋,假設輸出層只保留一個輸出單元
(刪去和
以及指向它們的箭頭),且隱藏層使用相同的激活函數。如果將每個隱藏單元的參數都初始化為相等的值,那么在正向傳播時每個隱藏單元將根據相同的輸入計算出相同的值,並傳遞至輸出層。在反向傳播中,每個隱藏單元的參數梯度值相等。因此,這些參數在使用基於梯度的優化算法迭代后值依然相等。之后的迭代也是如此。在這種情況下,無論隱藏單元有多少,隱藏層本質上只有1個隱藏單元在發揮作用。因此,正如在前面的實驗中所做的那樣,我們通常將神經網絡的模型參數,特別是權重參數,進行隨機初始化。
PyTorch的默認隨機初始化
隨機初始化模型參數的方法有很多。在線性回歸的簡潔實現中,我們使用torch.nn.init.normal_()
使模型net
的權重參數采用正態分布的隨機初始化方式。不過,PyTorch中nn.Module
的模塊參數都采取了較為合理的初始化策略(不同類型的layer具體采樣的哪一種初始化方法的可參考源代碼),因此一般不用我們考慮。
Xavier隨機初始化
還有一種比較常用的隨機初始化方法叫作Xavier隨機初始化。 假設某全連接層的輸入個數為
,輸出個數為
,Xavier隨機初始化將使該層中權重參數的每個元素都隨機采樣於均勻分布
它的設計主要考慮到,模型參數初始化后,每層輸出的方差不該受該層輸入個數影響,且每層梯度的方差也不該受該層輸出個數影響。
考慮環境因素
協變量偏移
這里我們假設,雖然輸入的分布可能隨時間而改變,但是標記函數,即條件分布P(y∣x)不會改變。雖然這個問題容易理解,但在實踐中也容易忽視。
想想區分貓和狗的一個例子。我們的訓練數據使用的是貓和狗的真實的照片,但是在測試時,我們被要求對貓和狗的卡通圖片進行分類。
cat | cat | dog | dog |
---|---|---|---|
![]() |
![]() |
![]() |
![]() |
測試數據:
cat | cat | dog | dog |
---|---|---|---|
![]() |
![]() |
![]() |
![]() |
顯然,這不太可能奏效。訓練集由照片組成,而測試集只包含卡通。在一個看起來與測試集有着本質不同的數據集上進行訓練,而不考慮如何適應新的情況,這是不是一個好主意。不幸的是,這是一個非常常見的陷阱。
統計學家稱這種協變量變化是因為問題的根源在於特征分布的變化(即協變量的變化)。數學上,我們可以說P(x)改變了,但P(y∣x)保持不變。盡管它的有用性並不局限於此,當我們認為x導致y時,協變量移位通常是正確的假設。
標簽偏移
當我們認為導致偏移的是標簽P(y)上的邊緣分布的變化,但類條件分布是不變的P(x∣y)時,就會出現相反的問題。當我們認為y導致x時,標簽偏移是一個合理的假設。例如,通常我們希望根據其表現來預測診斷結果。在這種情況下,我們認為診斷引起的表現,即疾病引起的症狀。有時標簽偏移和協變量移位假設可以同時成立。例如,當真正的標簽函數是確定的和不變的,那么協變量偏移將始終保持,包括如果標簽偏移也保持。有趣的是,當我們期望標簽偏移和協變量偏移保持時,使用來自標簽偏移假設的方法通常是有利的。這是因為這些方法傾向於操作看起來像標簽的對象,這(在深度學習中)與處理看起來像輸入的對象(在深度學習中)相比相對容易一些。
病因(要預測的診斷結果)導致 症狀(觀察到的結果)。
訓練數據集,數據很少只包含流感p(y)的樣本。
而測試數據集有流感p(y)和流感q(y),其中不變的是流感症狀p(x|y)。
概念偏移
另一個相關的問題出現在概念轉換中,即標簽本身的定義發生變化的情況。這聽起來很奇怪,畢竟貓就是貓。的確,貓的定義可能不會改變,但我們能不能對軟飲料也這么說呢?事實證明,如果我們周游美國,按地理位置轉移數據來源,我們會發現,即使是如圖所示的這個簡單術語的定義也會發生相當大的概念轉變。
如果我們要建立一個機器翻譯系統,分布P(y∣x)可能因我們的位置而異。這個問題很難發現。另一個可取之處是P(y∣x)通常只是逐漸變化。
Kaggle 房價預測實戰
作為深度學習基礎篇章的總結,我們將對本章內容學以致用。下面,讓我們動手實戰一個Kaggle比賽:房價預測。本節將提供未經調優的數據的預處理、模型的設計和超參數的選擇。我們希望讀者通過動手操作、仔細觀察實驗現象、認真分析實驗結果並不斷調整方法,得到令自己滿意的結果。
%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):設該特征在整個數據集上的均值為
,標准差為。那么,我們可以將該特征的每個值先減去再除以
得到標准化后的每個特征值。對於缺失的特征值,我們將其替換成該特征的均值。
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折交叉驗證
我們在模型選擇、欠擬合和過擬合中介紹了
折交叉驗證。它將被用來選擇模型設計並調節超參數。下面實現了一個函數,它返回第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
在
折交叉驗證中我們訓練
次並返回訓練和驗證的平均誤差
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, 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")
設計好模型並調好超參數之后,下一步就是對測試數據集上的房屋樣本做價格預測。如果我們得到與交叉驗證時差不多的訓練誤差,那么這個結果很可能是理想的,可以在Kaggle上提交結果。
train_and_pred(train_features, test_features, train_labels, test_data, num_epochs, lr, weight_decay, batch_size)