文章目錄
- 1.下載和緩存數據集
- 2.數據預處理
- 讀取樣本
- 預處理樣本
- 數值型特征處理
- 特征標準化的好處
- 離散值處理
- 轉換為張量表示
- 訓練
- K折交叉驗證
- 模型選擇
- 最終模型確認及結果預測
- 代碼總結
- 提交到Kaggle
房價預測比賽鏈接:https://www.kaggle.com/c/house-prices-advanced-regression-techniques
1.下載和緩存數據集
我們建立字典DATA_HUB
, 它可以將數據集名稱的字符串映射到數據集相關的二元組上, 這個二元組包含數據集的url和驗證文件完整性的sha-1密鑰。 所有類似的數據集都托管在地址為DATA_URL
的站點上(這里只托管了小一點的數據集,大的數據集還是需要去官網下載)。
import hashlib # Python的哈希庫,用于文件校驗(計算SHA1、MD5等哈希值)
import os # 操作系統接口庫 用于處理文件路徑、目錄創建等操作
import tarfile # 導入tar文件處理庫 用于解壓縮.tar、.tar.gz、.tar.bz2等歸檔文件
import zipfile # 導入zip文件處理庫 用于解壓縮.zip格式的壓縮文件
import requests # 導入HTTP請求庫 提供簡潔的API用于網絡文件下載DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'
一般情況需要自己去Kaggle將數據集下載到本地目錄。這里使用一個download
函數用來下載數據集, 將數據集緩存在本地目錄(默認情況下為../data
)中, 并返回下載文件的名稱。 如果緩存目錄中已經存在此數據集文件,并且其sha-1與存儲在DATA_HUB
中的相匹配, 我們將使用緩存的文件,以避免重復的下載。
def download(name, cache_dir=os.path.join('..', 'data')):"""下載一個DATA_HUB中的文件,返回本地文件名"""assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}"url, sha1_hash = DATA_HUB[name]os.makedirs(cache_dir, exist_ok=True)fname = os.path.join(cache_dir, url.split('/')[-1])if os.path.exists(fname):sha1 = hashlib.sha1()with open(fname, 'rb') as f:while True:data = f.read(1048576)if not data:breaksha1.update(data)if sha1.hexdigest() == sha1_hash:return fname # 命中緩存print(f'正在從{url}下載{fname}...')r = requests.get(url, stream=True, verify=True)with open(fname, 'wb') as f:f.write(r.content)return fname
競賽數據分為訓練集和測試集。 每條記錄都包括房屋的屬性和屬性值,如街道類型、施工年份、屋頂類型、地下室狀況等, 這些特征由各種數據類型組成。 例如,建筑年份由整數表示,屋頂類型由離散類別表示,其他特征由浮點數表示。 這就是現實讓事情變得復雜的地方:例如,一些數據完全丟失了,缺失值被簡單地標記為“NA”。
每套房子的價格只出現在訓練集中,我們將劃分訓練集以創建驗證集,但是在將預測結果上傳到Kaggle之后, 我們只能在官方測試集中評估我們的模型。
訓練集拆分 訓練集+驗證集 , 測試集用于評估模型。
方法1:Kaggle 比賽網站中下載數據
加入比賽后,可以在Data下載數據集。
方法2:代碼遠程下載
使用上面定義的腳本下載并緩存Kaggle房屋數據集。
import numpy as np
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2lDATA_HUB['kaggle_house_train'] = ( DATA_URL + 'kaggle_house_pred_train.csv','585e9cc93e70b39160e7921475f9bcd7d31219ce')DATA_HUB['kaggle_house_test'] = ( DATA_URL + 'kaggle_house_pred_test.csv','fa19780a7b011d9b009e8bff8e99922a8ee2eb90')train_data = pd.read_csv(download('kaggle_house_train'))
test_data = pd.read_csv(download('kaggle_house_test'))
2.數據預處理
讀取樣本
訓練數據集包括1460個樣本,每個樣本80個特征和1個標簽, 而測試數據集包含1459個樣本,每個樣本80個特征。
# 使用pandas分別加載包含訓練數據和測試數據的兩個CSV文件。
train_data = pd.read_csv(download('kaggle_house_train'))
test_data = pd.read_csv(download('kaggle_house_test'))
print(train_data.shape) #(1460,81)
print(test_data.shape) #(1459,80)
查看一下前4個樣本的前四個特征以及最后二個特征以及相應標簽(房價)
# train_data 是一個Pandas DataFrame對象 通常包含表格數據(行是樣本/記錄,列是特征/字段)
# iloc 按位置索引選擇數據的屬性,第一個參數為行選擇器,第二個參數為列選擇器
print(train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]])
在每個樣本中,第一個特征是ID, 它不攜帶任何用于預測的信息。 因此,在將數據提供給模型之前,我們將其從數據集中刪除。
# train_data.iloc[:, 1:-1] 選擇所有行,選取第二列和倒數第二列 排除訓練集的第一列(通常是ID)和最后一列(通常是標簽/目標變量)
# test_data.iloc[:, 1:] 測試集選取從第2列開始到最后一列的所有列
# 參數2接著參數1,將多個DataFrame連接成一個新的DataFrame
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
預處理樣本
數值型特征處理
- 將數據中所有缺失的值替換為相應特征的平均值。
- 為了將所有特征放在一個共同的尺度上, 我們通過將特征重新縮放到零均值和單位方差來標準化數據 x 標準 = x ? μ σ x_{標準}=\frac{x?μ}{σ} x標準?=σx?μ?。
其中 μ μ μ和 σ σ σ分別表示特征 x x x均值和標準差。 這些特征具有零均值和單位方差,即 E [ x ? μ σ ] = μ ? μ σ = 0 E[\frac{x?μ}{σ}] = \frac{μ-μ}{σ}=0 E[σx?μ?]=σμ?μ?=0和 E [ ( x ? μ ) 2 ] = ( σ 2 + μ 2 ) ? 2 μ 2 + μ 2 = σ 2 E[(x?μ)^2]=(σ^2+μ^2)?2μ^2+μ^2=σ^2 E[(x?μ)2]=(σ2+μ2)?2μ2+μ2=σ2。
all_features.dtypes
返回pandas Series
對象,包含索引index:列名 值values:對應的數據類型。
all_features.dtypes != 'object'
比較的存儲在Series中的實際值(values),返回布爾Series
布爾Series機制
在 pandas 中,當您使用一個布爾 Series 來索引另一個 Series 時,pandas 會選擇布爾 Series 中值為 True
的所有對應元素。
- 對齊索引:首先 pandas 會根據索引名稱對齊兩個 Series
- 選擇元素:然后只保留布爾 Series 中值為
True
的位置對應的元素 - 返回結果:返回一個新的 Series,包含所有被選中的元素
# numeric_features包含所有數值型特征列名的列表
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features]
是根據列名選擇all_features
的DataFrame
子集。
DataFrame.apply
會遍歷每一列,每次取一列數據作為Series傳入lambda函數進行標準化 x 標準 = x ? μ σ x_{標準}=\frac{x?μ}{σ} x標準?=σx?μ?并收集每次lambda的返回值(這里使用了Pandas的廣播機制),組合所有結果返回新的DataFrame
。
這里的均值是包含數據集和測試集的,實際情況不一定有測試集,可能只能在數據集上求均值。
# 所有數值特征的標準化
all_features[numeric_features] = all_features[numeric_features].apply(lambda x: (x - x.mean()) / (x.std()))
numeric_features
選中數值列,但有些數值列的值為NaN,最后需要將數值特征中的所有缺失值替換為0,并返回新的DataFrame。這里替換為0因為標準化后所有特征的均值為0。
# 在標準化數據之后,所有均值消失,因此我們可以將缺失值設置為0
all_features[numeric_features] = all_features[numeric_features].fillna(0)
特征標準化的好處
- 方便優化
假設預測房價有兩個特征,房間數(1-5)和房屋面積(50-300),對每個特征求梯度。發現面積方向的梯度是房間數方向梯度的幾十倍!
大梯度需要小的學習率抑制梯度爆炸,而大的梯度需要大的學習率去加速收斂,沒有一個合適的學習率可以兼容。
如果采取標準化,假設房間_std=(房間數-3)/1.5 ,面積_std =(面積-175)/125,新梯度之間差異不會太大
# 標準化變換:
房間_std = (房間數 - 3)/1.5 # 范圍[-1.33,1.33]
面積_std = (面積 - 175)/125 # 范圍[-1,1]# 新梯度:
?Loss/?w_房間 = -2(y-?)×房間_std ≈ -2(y-?)×0 (平均)
?Loss/?w_面積 = -2(y-?)×面積_std ≈ -2(y-?)×0 (平均)
→ 梯度大小比例恢復1:1
- 正則化懲罰公平
正則項 λ 2 ∣ ∣ w ∣ ∣ 2 \frac{\lambda}{2}||w||^2 2λ?∣∣w∣∣2懲罰 權重的平方和,這里超參數 λ \lambda λ控制了正則項的重要程度, λ \lambda λ設置的越大,對大的權重施加的懲罰就越重,沒辦法選出一個合適的值。
離散值處理
MSZoning的類型是一個object之類的離散值,使用one-hot編碼處理。假設這個特征(這一列)有5個不一樣的值,那我們就使用5個分量來表示。
pd.get_dummies
會處理所有字符串類型(object)或分類類型(category)的列,將其轉換為可用的數值格式(one-hot編碼),在DataFrame結構中以多列方式呈現,生成新的列 列原列名_值
,返回新的DataFrame。
參數dummy_na=True
表示將NaN視為一個獨立有效的類別,因為默認情況會忽略缺失值。
# “Dummy_na=True”將“na”(缺失值)視為有效的特征值,并為其創建指示符特征
# 處理字符串類型或分類類型
all_features = pd.get_dummies(all_features,dummy_na=True)
print(all_features.shape) #(2919,330)
可以發現此轉換之后,特征的總數量從79個增加到330個。
這里原始的列被剔除了!
print(all_features.select_dtypes(include='object').columns)
# Index(['MSZoning', 'Street', 'Alley', 'LotShape', 'LandContour', 'Utilities',
# 'LotConfig', 'LandSlope', 'Neighborhood', 'Condition1', 'Condition2',
# 'BldgType', 'HouseStyle', 'RoofStyle', 'RoofMatl', 'Exterior1st',
# 'Exterior2nd', 'MasVnrType', 'ExterQual', 'ExterCond', 'Foundation',
# 'BsmtQual', 'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2',
# 'Heating', 'HeatingQC', 'CentralAir', 'Electrical', 'KitchenQual',
# 'Functional', 'FireplaceQu', 'GarageType', 'GarageFinish', 'GarageQual',
# 'GarageCond', 'PavedDrive', 'PoolQC', 'Fence', 'MiscFeature',
# 'SaleType', 'SaleCondition'],
# dtype='object')
all_features = pd.get_dummies(all_features,dummy_na=True,dtype=np.uint8)
print(all_features.select_dtypes(include='object').columns)
# Index([], dtype='object')
大模型的解釋:
**問題:**can’t convert np.ndarray of type numpy.object_. The only supported types are: float64, float32, float16, complex64, complex128, int64, int32, int16, int8, uint64, uint32, uint16, uint8, and bool
嘗試將一個包含 numpy.object_
** **類型數據 的 NumPy 數組轉換為其他類型(如 PyTorch Tensor 或特定數值類型),但操作僅支持特定數據類型。
評論解答
get_dummies函數在pandas1.6.0版本之前返回numpy.uint8,無符號八位整數,在1.6.0版本開始更改為返回numpy.bool_,numpy布爾值。
# “Dummy_na=True”將“na”(缺失值)視為有效的特征值,并為其創建指示符特征
# 處理字符串類型或分類類型 只控制新的列的類型
all_features = pd.get_dummies(all_features,dummy_na=True,dtype=int)
print(all_features.shape) #(2919,310)
這里確實可以解決問題,但是我測試了之后發現布爾類型是不會報錯的??
轉換為張量表示
從pandas
格式中提取NumPy格式,并將其轉換為張量表示用于訓練。
all_features[:n_train]
表示前n_train行數據,只包含數據行,不包含列名,.values
表示返回NumPy數組。torch,tensor
表示將NumPy數組轉換為PyTorch
張量,并且指定格式為32位浮點數
train_data.SalePrice.values
通過列名SalePrice獲取到該列的值,輸出是一個一維數組,形狀為一維數組(n_train,)。
將其重塑數組二維數組形狀為(n_train, 1),最后轉換為PyTorch
張量。
n_train = train_data.shape[0] #獲取訓練數據集的樣本數train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float32)
test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float32)
train_labels = torch.tensor(train_data.SalePrice.values.reshape(-1, 1), dtype=torch.float32)
訓練
損失函數使用均方誤差損失 1 n ∑ i = 1 n ( y i ? y ^ i ) 2 \frac{1}{n} \sum_{i=1}^n(y_i?\hat y_i)^2 n1?∑i=1n?(yi??y^?i?)2
loss = nn.MSELoss()
我們訓練一個帶有損失平方的線性模型。 顯然線性模型很難讓我們在競賽中獲勝,但線性模型提供了一種健全性檢查, 以查看數據中是否存在有意義的信息。
如果我們在這里不能做得比隨機猜測更好,那么我們很可能存在數據處理錯誤。 如果一切順利,線性模型將作為基線(baseline)模型, 讓我們直觀地知道最好的模型有超出簡單的模型多少。
in_features = train_features.shape[1] # 特征數量def get_net():net = nn.Sequential(nn.Linear(in_features,1))return net
對于真實值減去誤差值來說,我們更關心相對誤差 y ? y ^ y \frac{y-\hat y}{y} yy?y^??,對于房價來說不同的房子價格相差較大,有10w的有100w的。例如,如果我們在俄亥俄州農村地區估計一棟房子的價格時, 假設我們的預測偏差了10萬美元, 然而那里一棟典型的房子的價值是12.5萬美元, 那么模型可能做得很糟糕。 另一方面,如果我們在加州豪宅區的預測出現同樣的10萬美元的偏差, (在那里,房價中位數超過400萬美元) 這可能是一個不錯的預測。
對數變換(log transformation)這種方法被廣泛應用于Kaggle等數據科學競賽中。我們評估模型時使用 1 n ∑ i = 1 n ( l o g ( y i ) ? l o g ( y ^ i ) ) 2 \sqrt {\frac{1}{n}\sum_{i=1}^n (log(y_i)?log(\hat y_i))^2} n1?∑i=1n?(log(yi?)?log(y^?i?))2?等價于 1 n ∑ i = 1 n ( l o g y i y ^ i ) 2 \sqrt {\frac{1}{n}\sum_{i=1}^n (log \frac {y_i}{\hat y_i})^2} n1?∑i=1n?(logy^?i?yi??)2?。這個評估值越小越好。
torch.clamp(input, min, max, *, out=None) -> Tensor
將張量中的元素限制在指定范圍,min表示下限值,max表示上限值。這里將最低值限制為1,防止出現負數和為0的情況影響對數的取值。
def log_rmse(net, features, labels):# 為了在取對數時進一步穩定該值,將小于1的值設置為1clipped_preds = torch.clamp(net(features), 1, float('inf'))rmse = torch.sqrt(loss(torch.log(clipped_preds),torch.log(labels)))return rmse.item() # 從張量中提取python標量值
這里訓練使用Adam優化器(后續會講),主要優勢是對于初始學習率不那么敏感。
- 從train_iter中按batch_size取出數據集
- 將optimizer管理的所有參數的梯度歸零,防止每一batch_size的梯度累加錯誤更新
- 前向傳播與損失計算。訓練中使用均方誤差損失,這里y和net(X)的形狀在之前的步驟已經統一
- 反向傳播計算梯度,自動微分計算梯度。
- 使用優化器更新參數。
每輪結束后評估整個訓練集的性能,將每輪結果放入train_ls
中,如果提供了測試標簽test_labels
,評估模型在測試集上的表現。
def train(net, train_features, train_labels, test_features, test_labels,num_epochs, learning_rate, weight_decay, batch_size):train_ls, test_ls = [], []train_iter = d2l.load_array((train_features, train_labels), batch_size)# 這里使用的是Adam優化算法optimizer = torch.optim.Adam(net.parameters(),lr = learning_rate,weight_decay = weight_decay)for epoch in range(num_epochs):for X, y in train_iter:# 每次返回一個(X_batch, y_batch)元組optimizer.zero_grad()l = loss(net(X), y) # 輸出形狀 net(X)=[batch_size, 1]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折交叉驗證來幫助模型選和超參數調整。
我們首先需要定義一個函數,在 K K K折交叉驗證過程中返回第 i i i折的數據。 具體地說,它選擇第 i i i個切片作為驗證數據,其余部分作為訓練數據。
注意,這并不是處理數據的最有效方法,如果我們的數據集大得多,會有其他解決辦法。
返回訓練和驗證誤差的平均值
# k總折數,i當前折數索引
def get_k_fold_data(k, i, X, y):assert k > 1 # 如果 k≤1,拋出 AssertionError#計算每折大小,整除會舍棄余數,少量樣本不被包含在任何折中fold_size = X.shape[0] // k X_train, y_train = None, Nonefor j in range(k): # slice(start, stop, step):slice對象是用于表示切片操作的特殊對象# 以下代碼相當于創建了一個切片對象:# idx = [j * fold_size : (j + 1) * fold_size]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_partelif X_train is None: # 如果不是驗證集且訓練集尚未初始化 - 第一個訓練集X_train, y_train = X_part, y_partelse: # 將其余訓練集拼接在一起# torch.cat():沿指定維度連接張量,0表示垂直堆疊X_train = torch.cat([X_train, X_part], 0)y_train = torch.cat([y_train, y_part], 0)return X_train, y_train, X_valid, y_valid
將數據集分成K份后,每次使用第i份作為驗證集,其余作為訓練集。
下面的代碼獨立訓練K次模型,每一次都創建一個全新初始化的模型,每次都有新的訓練數據與驗證數據。復用模型會使用上一次訓練后參數信息,
交叉驗證目標是評估最終模型性能,每一次訓練模型都取模型最后一輪的評估誤差。
def k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay,batch_size):train_l_sum, valid_l_sum = 0, 0for i in range(k):data = get_k_fold_data(k, i, X_train, y_train)net = get_net()# 解包為X_train, y_train, X_valid, y_validtrain_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]# K折的學習曲線通常相似,所以這里選擇只繪制第一折if i == 0:d2l.plot(list(range(1, num_epochs + 1)), # X軸:訓練輪次[1, num_epochs][train_ls, valid_ls], # Y軸數據:[訓練誤差列表, 驗證誤差列表]xlabel='epoch', # X軸標簽ylabel='rmse', # Y軸標簽(均方根誤差)xlim=[1, num_epochs], # X軸顯示范圍legend=['train', 'valid'], # 圖例說明yscale='log' # Y軸使用對數刻度)print(f'折{i + 1},訓練log rmse{float(train_ls[-1]):f}, 'f'驗證log rmse{float(valid_ls[-1]):f}')return train_l_sum / k, valid_l_sum / k
模型選擇
這里選擇了一組未調優的參數,有時一組超參數的訓練誤差可能非常低,但 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(f'{k}-折驗證: 平均訓練log rmse: {float(train_l):f}, 'f'平均驗證log rmse: {float(valid_l):f}')
代碼總結
- 初始化一組超參數
- 進行 K K K折交叉驗證,對于每一折 i i i過程執行如下的步驟
- 將訓練集劃分為k份,其中第i份作為驗證集,其余k-1份作為訓練集
- 初始化一個新的模型
- 使用當前超參數、訓練集和驗證集訓練模型(訓練該模型的參數)
- 累計訓練完成的評估誤差 - 訓練集的評估誤差與驗證集的評估誤差
- 返回折數的平均評估誤差
通過不斷調整超參數重復上述過程,選擇出最優的超參數。
最終模型確認及結果預測
- 通過之前的步驟,我們已經選擇出了一組超參數。現在使用這些超參數和全部的訓練數據來訓練最終模型。
- 將最終模型應用于測試集,將預測結果保存在CSV文件中。
在預測階段不需要知道梯度信息了,更關心內存的優化,所以通常會將預測結果從計算圖中分離datach
出來創建一個新的不含梯度信息的張量。然后將PyTorch張量轉換為NumPy數組。
將模型預測結果轉換為Pandas DataFrame中的格式,preds.reshape(1, -1)
表示將數組轉換為形狀為(1,N)
的二維數組,取出二維數組中的唯一元素(一維數組)轉換為Pandas Series對象。
def train_and_pred(train_features, test_features, train_labels, test_data,num_epochs, lr, weight_decay, batch_size):# 1. 初始化模型net = get_net() # # 2. 完整訓練(不使用驗證集)train_ls, _ = train(net, train_features, train_labels, None, None,num_epochs, lr, weight_decay, batch_size)# 3. 可視化訓練過程d2l.plot(np.arange(1, num_epochs + 1), [train_ls], xlabel='epoch',ylabel='log rmse', xlim=[1, num_epochs], yscale='log')# 4. 打印最終訓練誤差print(f'訓練log rmse:{float(train_ls[-1]):f}')# 5. 測試集預測preds = net(test_features).detach().numpy()# 6. 格式化預測結果test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0])
Kaggle提交規范
1.文件命名:通常要求固定文件名。
2.首列必須為id,次列為預測值列,列名大小寫敏感。
# 7. 創建提交文件# 將測試集id和模型預測的房價水平拼接形成新的DataFrame# Id SalePrice# 1461 181000# 1462 179500# 將DataFrame轉換為CSV格式(Kaggle標準提交格式)submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)submission.to_csv('submission.csv', # 輸出的文件名index=False # 禁止寫入行索引)
如果測試集上的預測與 K K K倍交叉驗證過程中的預測相似, 那就是時候把它們上傳到Kaggle了。 下面的代碼將生成一個名為submission.csv
的文件。
train_and_pred(train_features, test_features, train_labels, test_data,num_epochs, lr, weight_decay, batch_size)
代碼總結
import hashlib # Python的哈希庫,用于文件校驗(計算SHA1、MD5等哈希值)
import os # 操作系統接口庫 用于處理文件路徑、目錄創建等操作
import tarfile # 導入tar文件處理庫 用于解壓縮.tar、.tar.gz、.tar.bz2等歸檔文件
import zipfile # 導入zip文件處理庫 用于解壓縮.zip格式的壓縮文件
import requests # 導入HTTP請求庫 提供簡潔的API用于網絡文件下載
import numpy as np
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2lDATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'def download(name, cache_dir=os.path.join('..', 'data')):"""下載一個DATA_HUB中的文件,返回本地文件名"""assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}"url, sha1_hash = DATA_HUB[name]os.makedirs(cache_dir, exist_ok=True)fname = os.path.join(cache_dir, url.split('/')[-1])if os.path.exists(fname):sha1 = hashlib.sha1()with open(fname, 'rb') as f:while True:data = f.read(1048576)if not data:breaksha1.update(data)if sha1.hexdigest() == sha1_hash:return fname # 命中緩存print(f'正在從{url}下載{fname}...')r = requests.get(url, stream=True, verify=True)with open(fname, 'wb') as f:f.write(r.content)return fname# 數據獲取
DATA_HUB['kaggle_house_train'] = ( DATA_URL + 'kaggle_house_pred_train.csv','585e9cc93e70b39160e7921475f9bcd7d31219ce')DATA_HUB['kaggle_house_test'] = ( DATA_URL + 'kaggle_house_pred_test.csv','fa19780a7b011d9b009e8bff8e99922a8ee2eb90')train_data = pd.read_csv(download('kaggle_house_train'))
test_data = pd.read_csv(download('kaggle_house_test'))# 數據處理
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
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()))
all_features[numeric_features] = all_features[numeric_features].fillna(0)all_features = pd.get_dummies(all_features,dummy_na=True,dtype=np.uint8)
n_train = train_data.shape[0] #獲取訓練數據集的樣本數
train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float32)
test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float32)
train_labels = torch.tensor(train_data.SalePrice.values.reshape(-1, 1), dtype=torch.float32)# 模型的訓練
loss = nn.MSELoss()
in_features = train_features.shape[1] # 特征數量
# 訓練損失
def get_net():net = nn.Sequential(nn.Linear(in_features,1))return net
# 評估誤差
def log_rmse(net, features, labels):# 為了在取對數時進一步穩定該值,將小于1的值設置為1clipped_preds = torch.clamp(net(features), 1, float('inf'))rmse = torch.sqrt(loss(torch.log(clipped_preds),torch.log(labels)))return rmse.item() # 從張量中提取python標量值def train(net, train_features, train_labels, test_features, test_labels,num_epochs, learning_rate, weight_decay, batch_size):train_ls, test_ls = [], []train_iter = d2l.load_array((train_features, train_labels), batch_size)# 這里使用的是Adam優化算法optimizer = torch.optim.Adam(net.parameters(),lr = learning_rate,weight_decay = weight_decay)for epoch in range(num_epochs):for X, y in train_iter:# 每次返回一個(X_batch, y_batch)元組optimizer.zero_grad()l = loss(net(X), y) # 輸出形狀 net(X)=[batch_size, 1]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折交叉驗證
# 獲取訓練集和數據集
def get_k_fold_data(k, i, X, y):assert k > 1 # 如果 k≤1,拋出 AssertionError#計算每折大小,整除會舍棄余數,少量樣本不被包含在任何折中fold_size = X.shape[0] // k X_train, y_train = None, Nonefor j in range(k): # slice(start, stop, step):slice對象是用于表示切片操作的特殊對象# 以下代碼相當于創建了一個切片對象:# idx = [j * fold_size : (j + 1) * fold_size]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_partelif X_train is None: # 如果不是驗證集且訓練集尚未初始化 - 第一個訓練集X_train, y_train = X_part, y_partelse: # 將其余訓練集拼接在一起# torch.cat():沿指定維度連接張量,0表示垂直堆疊X_train = torch.cat([X_train, X_part], 0)y_train = torch.cat([y_train, y_part], 0)return X_train, y_train, X_valid, y_validdef k_fold(k, X_train, y_train, num_epochs, learning_rate, weight_decay,batch_size):train_l_sum, valid_l_sum = 0, 0for i in range(k):data = get_k_fold_data(k, i, X_train, y_train)net = get_net()# 解包為X_train, y_train, X_valid, y_validtrain_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]# K折的學習曲線通常相似,所以這里選擇只繪制第一折if i == 0:d2l.plot(list(range(1, num_epochs + 1)), # X軸:訓練輪次[1, num_epochs][train_ls, valid_ls], # Y軸數據:[訓練誤差列表, 驗證誤差列表]xlabel='epoch', # X軸標簽ylabel='rmse', # Y軸標簽(均方根誤差)xlim=[1, num_epochs], # X軸顯示范圍legend=['train', 'valid'], # 圖例說明yscale='log' # Y軸使用對數刻度)print(f'折{i + 1},訓練log rmse{float(train_ls[-1]):f}, 'f'驗證log rmse{float(valid_ls[-1]):f}')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(f'{k}-折驗證: 平均訓練log rmse: {float(train_l):f}, 'f'平均驗證log rmse: {float(valid_l):f}')def train_and_pred(train_features, test_features, train_labels, test_data,num_epochs, lr, weight_decay, batch_size):# 1. 初始化模型net = get_net() # # 2. 完整訓練(不使用驗證集)train_ls, _ = train(net, train_features, train_labels, None, None,num_epochs, lr, weight_decay, batch_size)# 3. 可視化訓練過程d2l.plot(np.arange(1, num_epochs + 1), [train_ls], xlabel='epoch',ylabel='log rmse', xlim=[1, num_epochs], yscale='log')# 4. 打印最終訓練誤差print(f'訓練log rmse:{float(train_ls[-1]):f}')# 5. 測試集預測preds = net(test_features).detach().numpy()# 6. 格式化預測結果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 # 禁止寫入行索引)train_and_pred(train_features, test_features, train_labels, test_data,num_epochs, lr, weight_decay, batch_size)