動手學習pytorch(5)--梯度消失、梯度爆炸


梯度消失、梯度爆炸以及Kaggle房價預測

  1. 梯度消失和梯度爆炸
  2. 考慮到環境因素的其他問題
  3. Kaggle房價預測
 

梯度消失和梯度爆炸

深度模型有關數值穩定性的典型問題是消失(vanishing)和爆炸(explosion)。

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

假設一個層數為

的多層感知機的第的權重參數為,輸出層的權重參數為。為了便於討論,不考慮偏差參數,且設所有隱藏層的激活函數為恆等映射(identity mapping)。給定輸入,多層感知機的第層的輸出。此時,如果層數較大,的計算可能會出現衰減或爆炸。舉個例子,假設輸入和所有層的權重參數都是標量,如權重參數為0.2和5,多層感知機的第30層輸出為輸入分別與(消失)和

(爆炸)的乘積。當層數較多時,梯度的計算也容易出現消失或爆炸。

隨機初始化模型參數

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

回顧多層感知機一節描述的多層感知機。為了方便解釋,假設輸出層只保留一個輸出單元

(刪去

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

Image Name

PyTorch的默認隨機初始化

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

Xavier隨機初始化

還有一種比較常用的隨機初始化方法叫作Xavier隨機初始化。 假設某全連接層的輸入個數為

,輸出個數為

,Xavier隨機初始化將使該層中權重參數的每個元素都隨機采樣於均勻分布

 

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

 

考慮環境因素

協變量偏移

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

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

cat cat dog dog
Image Name Image Name Image Name Image Name

測試數據:

cat cat dog dog
Image Name Image Name Image Name Image Name
 

顯然,這不太可能奏效。訓練集由照片組成,而測試集只包含卡通。在一個看起來與測試集有着本質不同的數據集上進行訓練,而不考慮如何適應新的情況,這是不是一個好主意。不幸的是,這是一個非常常見的陷阱。

統計學家稱這種協變量變化是因為問題的根源在於特征分布的變化(即協變量的變化)。數學上,我們可以說P(x)改變了,但P(y∣x)保持不變。盡管它的有用性並不局限於此,當我們認為x導致y時,協變量移位通常是正確的假設。

標簽偏移

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

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

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

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

概念偏移

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

Image Name

美國軟飲料名稱的概念轉變

如果我們要建立一個機器翻譯系統,分布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) 
 
1.3.0
 

獲取和讀取數據集

比賽數據分為訓練數據集和測試數據集。兩個數據集都包括每棟房子的特征,如街道類型、建造年份、房頂類型、地下室狀況等特征值。這些特征值有連續的數字、離散的標簽甚至是缺失值“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 
 
(1460, 81)
 

測試數據集包括1459個樣本和80個特征。我們需要將測試數據集中每個樣本的標簽預測出來。

 
test_data.shape 
 
(1459, 80)
 

讓我們來查看前4個樣本的前4個特征、后2個特征和標簽(SalePrice):

 
train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]] 
 
  Id MSSubClass MSZoning LotFrontage SaleType SaleCondition SalePrice
0 1 60 RL 65.0 WD Normal 208500
1 2 20 RL 80.0 WD Normal 181500
2 3 60 RL 68.0 WD Normal 223500
3 4 70 RL 60.0 WD Abnorml 140000
 

可以看到第一個特征是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 
 
(2919, 331)
 

可以看到這一步轉換將特征數從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)) 
 
fold 0, train rmse 0.241365, valid rmse 0.223083
fold 1, train rmse 0.229118, valid rmse 0.267488
fold 2, train rmse 0.232072, valid rmse 0.237995
fold 3, train rmse 0.238050, valid rmse 0.218671
fold 4, train rmse 0.231004, valid rmse 0.259185
5-fold validation: avg train rmse 0.234322, avg valid rmse 0.241284
 

預測並在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) 
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM