文章目录
实战Kaggle比赛----预测房价
之前几节我们学习了一些训练深度网络的基本工具和网络正则化的技术(如权重衰减、暂退法等)。 本节我们将通过Kaggle比赛,将所学知识付诸实践。
Kaggle的房价预测比赛是一个很好的起点。 此数据集由Bart de Cock于2011年收集 [DeCock, 2011], 涵盖了2006-2010年期间亚利桑那州埃姆斯市的房价。 这个数据集是相当通用的,不会需要使用复杂模型架构。 它比哈里森和鲁宾菲尔德的波士顿房价 数据集要大得多,也有更多的特征。
本节我们将详细介绍数据预处理、模型设计和超参数选择。 通过亲身实践,你将获得一手经验,这些经验将有益数据科学家的职业成长。
下载和缓存数据集
在整本书中,我们将下载不同的数据集,并训练和测试模型。 这里我们实现几个函数来方便下载数据。
首先,我们建立字典DATA_HUB, 它可以将数据集名称的字符串映射到数据集相关的二元组上, 这个二元组包含数据集的url和验证文件完整性的sha-1密钥。 所有类似的数据集都托管在地址为DATA_URL的站点上。
import hashlib
import os
import tarfile
import zipfile
import requests
DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'
下面的 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}" #若名称未在DATA_HUB字典中,则中断程序,输出提示
url, sha1_hash = DATA_HUB[name] #获取DATA_HUB内的url地址和sha1_hash密钥
#创建文件路径,并使用exist_ok=True进行指定,文件夹已存在时不报错
os.makedirs(cache_dir, exist_ok=True)
fname = os.path.join(cache_dir, url.split('/')[-1]) #连接文件夹路径与名称
if os.path.exists(fname): #若该文件已经存在
"""
哈希算法又称为摘要算法,它生成的是固定长度的不可逆的混杂字符串。
不要与加密算法混为一谈,因为加密对应着解密。而哈希算法很难很难解密。
"""
sha1 = hashlib.sha1() #使用hashlib进行编码
with open(fname, 'rb') as f:
while True:
data = f.read(1048576)
if not data:
break
sha1.update(data) #使用sha1生成的字符串对数据进行编码
# 命中缓存,检查文件内容
if sha1.hexdigest() == sha1_hash: #若sha1.dexdigest()哈希编码字符串符合原定存储的字符串
return fname #返回文件指针
print(f'正在从{url}下载{fname}...')
r = requests.get(url, stream=True, verify=True) #request库发出get()请求,准备下载文件
with open(fname, 'wb') as f:
f.write(r.content)
return fname #返回文件指针
我们还需实现两个实用函数: 一个将下载并解压缩一个zip或tar文件, 另一个是将本书中使用的所有数据集从DATA_HUB下载到缓存目录中。
def download_extract(name, folder=None): #@save
"""下载并解压zip/tar文件"""
fname = download(name)
base_dir = os.path.dirname(fname) #去掉文件名,返回目录
data_dir, ext = os.path.splitext(fname) #将文件名全部路径与文件后缀名拆开为一个二元组
if ext == '.zip':
fp = zipfile.ZipFile(fname, 'r') #解压zip压缩文件
elif ext in ('.tar', '.gz'):
fp = tarfile.open(fname, 'r') #解压tar或者gz压缩文件
else:
assert False, '只有zip/tar文件可以被解压缩'
fp.extractall(base_dir) #解压文件,放到当前文件夹
return os.path.join(base_dir, folder) if folder else data_dir
def download_all(): #@save
"""下载DATA_HUB中的所有文件"""
for name in DATA_HUB:
download(name)
Kaggle简介
Kaggle是一个当今流行举办机器学习比赛的平台, 每场比赛都以至少一个数据集为中心。许多比赛有赞助方,他们为获胜的解决方案提供奖金。
该平台帮助用户通过论坛和共享代码进行互动,促进协作和竞争。虽然排行榜的追逐往往令人失去理智: 有些研究人员短视地专注于预处理步骤,而不是考虑基础性问题。 但一个客观的平台有巨大的价值:该平台促进了竞争方法之间的直接定量比较,以及代码共享。 这便于每个人都可以学习哪些方法起作用,哪些没有起作用。 如果你想参加Kaggle比赛,你首先需要注册一个账户,如下图
在房价预测比赛页面(如 图4.10.2 所示), 你在”Data”选项卡下可以找到数据集。 你可以通过下面的网址提交预测,并查看排名:
https://www.kaggle.com/c/house-prices-advanced-regression-techniques
访问和读取数据集
注意,竞赛数据分为训练集和测试集。 每条记录都包括房屋的属性值和属性,如街道类型、施工年份、屋顶类型、地下室状况等。
这些特征由各种数据类型组成。 例如,建筑年份由整数表示,屋顶类型由离散类别表示,其他特征由浮点数表示。
这就是现实让事情变得复杂的地方:例如,一些数据完全丢失了,缺失值被简单地标记为 “NA”。
每套房子的价格只出现在训练集中(毕竟这是一场比赛)。 我们将希望划分训练集以创建验证集,但是在将预测结果上传到Kaggle之后,我们只能在官方测试集中评估我们的模型。
开始之前,我们将使用pandas读入并处理数据。
import numpy as np
import pandas as pd #引入numpy, pandas数据计算处理包
import torch
from torch import nn
from d2l import torch as d2l
为方便起见,我们可以使用上面定义的脚本下载并缓存Kaggle房屋数据集。
#DATA_HUB元组的第一个元素值是数据集下载地址,第二个值是hash编码的sha1值
DATA_HUB['kaggle_house_train'] = ( #@save
DATA_URL + 'kaggle_house_pred_train.csv',
'585e9cc93e70b39160e7921475f9bcd7d31219ce')
DATA_HUB['kaggle_house_test'] = ( #@save
DATA_URL + 'kaggle_house_pred_test.csv',
'fa19780a7b011d9b009e8bff8e99922a8ee2eb90')
我们使用pandas分别加载包含训练数据和测试数据的两个CSV文件。
train_data = pd.read_csv(download('kaggle_house_train')) #使用pandas读取下载好的csv文件,或者直接下载
test_data = pd.read_csv(download('kaggle_house_test'))
训练数据集包括1460个样本,每个样本80个特征和1个标签, 而测试数据集包含1459个样本,每个样本80个特征。
print(train_data.shape)
print(test_data.shape)
(1460, 81)
(1459, 80)
让我们看看前四个和最后两个特征,以及相应标签(房价)。
train_data.iloc[: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, 这有助于模型识别每个训练样本。 虽然这很方便,但它不携带任何用于预测的信息。 因此,在将数据提供给模型之前,我们将其从数据集中删除。
all_features = pd.concat((train_data.iloc[:,1:-1], test_data.iloc[:,1:])) #将训练集与测试集的所有特征连接到一起
all_features
MSSubClass | MSZoning | LotFrontage | LotArea | Street | Alley | LotShape | LandContour | Utilities | LotConfig | ... | ScreenPorch | PoolArea | PoolQC | Fence | MiscFeature | MiscVal | MoSold | YrSold | SaleType | SaleCondition | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 60 | RL | 65.0 | 8450 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | 0 | 0 | NaN | NaN | NaN | 0 | 2 | 2008 | WD | Normal |
1 | 20 | RL | 80.0 | 9600 | Pave | NaN | Reg | Lvl | AllPub | FR2 | ... | 0 | 0 | NaN | NaN | NaN | 0 | 5 | 2007 | WD | Normal |
2 | 60 | RL | 68.0 | 11250 | Pave | NaN | IR1 | Lvl | AllPub | Inside | ... | 0 | 0 | NaN | NaN | NaN | 0 | 9 | 2008 | WD | Normal |
3 | 70 | RL | 60.0 | 9550 | Pave | NaN | IR1 | Lvl | AllPub | Corner | ... | 0 | 0 | NaN | NaN | NaN | 0 | 2 | 2006 | WD | Abnorml |
4 | 60 | RL | 84.0 | 14260 | Pave | NaN | IR1 | Lvl | AllPub | FR2 | ... | 0 | 0 | NaN | NaN | NaN | 0 | 12 | 2008 | WD | Normal |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
1454 | 160 | RM | 21.0 | 1936 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | 0 | 0 | NaN | NaN | NaN | 0 | 6 | 2006 | WD | Normal |
1455 | 160 | RM | 21.0 | 1894 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | 0 | 0 | NaN | NaN | NaN | 0 | 4 | 2006 | WD | Abnorml |
1456 | 20 | RL | 160.0 | 20000 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | 0 | 0 | NaN | NaN | NaN | 0 | 9 | 2006 | WD | Abnorml |
1457 | 85 | RL | 62.0 | 10441 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | 0 | 0 | NaN | MnPrv | Shed | 700 | 7 | 2006 | WD | Normal |
1458 | 60 | RL | 74.0 | 9627 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | 0 | 0 | NaN | NaN | NaN | 0 | 11 | 2006 | WD | Normal |
2919 rows × 79 columns
数据预处理
如上所述,我们有各种各样的数据类型。 在开始建模之前,我们需要对数据进行预处理。 首先,我们将所有缺失的值替换为相应特征的平均值。然后,为了将所有特征放在一个共同的尺度上, 我们通过将特征重新缩放到零均值和单位方差来标准化数据:
正态分布的标准化公式为:
Y
=
X
−
μ
σ
Y = \frac{X-\mu}{\sigma}
Y=σX−μ
直观地说,我们标准化数据有两个原因: 首先,它方便优化。 其次,因为我们不知道哪些特征是相关的, 所以我们不想让惩罚分配给一个特征的系数比分配给其他任何特征的系数更大。
标准正态化、缺失值填充、离散值one-hot编码
#若无法获得测试数据,则可根据训练数据计算均值和标准差
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
all_features[numeric_features] = all_features[numeric_features].fillna(0)
all_features
MSSubClass | MSZoning | LotFrontage | LotArea | Street | Alley | LotShape | LandContour | Utilities | LotConfig | ... | ScreenPorch | PoolArea | PoolQC | Fence | MiscFeature | MiscVal | MoSold | YrSold | SaleType | SaleCondition | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.067320 | RL | -0.184443 | -0.217841 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | -0.285886 | -0.063139 | NaN | NaN | NaN | -0.089577 | -1.551918 | 0.157619 | WD | Normal |
1 | -0.873466 | RL | 0.458096 | -0.072032 | Pave | NaN | Reg | Lvl | AllPub | FR2 | ... | -0.285886 | -0.063139 | NaN | NaN | NaN | -0.089577 | -0.446848 | -0.602858 | WD | Normal |
2 | 0.067320 | RL | -0.055935 | 0.137173 | Pave | NaN | IR1 | Lvl | AllPub | Inside | ... | -0.285886 | -0.063139 | NaN | NaN | NaN | -0.089577 | 1.026577 | 0.157619 | WD | Normal |
3 | 0.302516 | RL | -0.398622 | -0.078371 | Pave | NaN | IR1 | Lvl | AllPub | Corner | ... | -0.285886 | -0.063139 | NaN | NaN | NaN | -0.089577 | -1.551918 | -1.363335 | WD | Abnorml |
4 | 0.067320 | RL | 0.629439 | 0.518814 | Pave | NaN | IR1 | Lvl | AllPub | FR2 | ... | -0.285886 | -0.063139 | NaN | NaN | NaN | -0.089577 | 2.131647 | 0.157619 | WD | Normal |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
1454 | 2.419286 | RM | -2.069222 | -1.043758 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | -0.285886 | -0.063139 | NaN | NaN | NaN | -0.089577 | -0.078492 | -1.363335 | WD | Normal |
1455 | 2.419286 | RM | -2.069222 | -1.049083 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | -0.285886 | -0.063139 | NaN | NaN | NaN | -0.089577 | -0.815205 | -1.363335 | WD | Abnorml |
1456 | -0.873466 | RL | 3.884968 | 1.246594 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | -0.285886 | -0.063139 | NaN | NaN | NaN | -0.089577 | 1.026577 | -1.363335 | WD | Abnorml |
1457 | 0.655311 | RL | -0.312950 | 0.034599 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | -0.285886 | -0.063139 | NaN | MnPrv | Shed | 1.144116 | 0.289865 | -1.363335 | WD | Normal |
1458 | 0.067320 | RL | 0.201080 | -0.068608 | Pave | NaN | Reg | Lvl | AllPub | Inside | ... | -0.285886 | -0.063139 | NaN | NaN | NaN | -0.089577 | 1.763290 | -1.363335 | WD | Normal |
2919 rows × 79 columns
接下来,我们处理离散值。 这包括诸如“MSZoning”之类的特征。 我们用独热编码替换它们, 方法与前面将多类别标签转换为向量的方式相同 。 例如,“MSZoning”包含值“RL”和“Rm”。 我们将创建两个新的指示器特征“MSZoning_RL”和“MSZoning_RM”,其值为0或1。
根据独热编码,如果“MSZoning”的原始值为“RL”, 则:“MSZoning_RL”为1,“MSZoning_RM”为0。 pandas软件包会自动为我们实现这一点。
#"Dummy_na=True"将"na"(缺失值)视为有效的特征值,并为其创建指示符特征
all_features = pd.get_dummies(all_features, dummy_na=True)
all_features.shape
(2919, 331)
你可以看到,此转换会将特征的总数量从79个增加到331个。 最后,通过values属性,我们可以 从pandas格式中提取NumPy格式,并将其转换为张量表示用于训练。
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)
小栗子帮助理解
为了更好地理解预处理的步骤,现在我们可以自己测试这些预处理的具体操作。
首先生成5个样本,分别为名称和年龄
dictionary = {
'name':['China','USA','Japan','India','England'],
'age':[1,2,None,4,5]
}
dic = pd.DataFrame(dictionary) #先将字典转化为pd形式
dic
name | age | |
---|---|---|
0 | China | 1.0 |
1 | USA | 2.0 |
2 | Japan | NaN |
3 | India | 4.0 |
4 | England | 5.0 |
对所有样本的数值类型特征作正则分布标准化操作。
此时所有数值类型的均值为0,标准差为1,为缺失值填充均值0。
numeric_dic = dic.dtypes[dic.dtypes != 'object'].index #获取数值类型列下标
print(numeric_dic)
#数值数据进行标准正态分布
dic[numeric_dic] = dic[numeric_dic].apply(
lambda x:(x - x.mean())/(x.std()))
#对所有缺失值填充均值0
dic[numeric_dic] = dic[numeric_dic].fillna(0)
dic
Index(['age'], dtype='object')
name | age | |
---|---|---|
0 | China | -1.095445 |
1 | USA | -0.547723 |
2 | Japan | 0.000000 |
3 | India | 0.547723 |
4 | England | 1.095445 |
对于离散值,类似于 ‘name’ 使用热编码(one-hot)代替它们,如下
dic = pd.get_dummies(dic, dummy_na=True)
dic
age | name_China | name_England | name_India | name_Japan | name_USA | name_nan | |
---|---|---|---|---|---|---|---|
0 | -1.095445 | 1 | 0 | 0 | 0 | 0 | 0 |
1 | -0.547723 | 0 | 0 | 0 | 0 | 1 | 0 |
2 | 0.000000 | 0 | 0 | 0 | 1 | 0 | 0 |
3 | 0.547723 | 0 | 0 | 1 | 0 | 0 | 0 |
4 | 1.095445 | 0 | 1 | 0 | 0 | 0 | 0 |
以上就是对数据集预处理的小栗子讲解。
训练
首先,我们训练一个带有损失平方的线性模型。 显然线性模型很难让我们在竞赛中获胜,但线性模型提供了一种健全性检查, 以查看数据中是否存在有意义的信息。 如果我们在这里不能做得比随机猜测更好,那么我们很可能存在数据处理错误。 如果一切顺利,线性模型将作为 基线(baseline)模型, 让我们直观地知道最好的模型有超出简单的模型多少。
loss = nn.MSELoss() #定义均方损失函数
num_inputs = train_features.shape[1] #输入样本特征的个数
#定义线性神经网络
def get_net():
net = nn.Sequential(nn.Linear(num_inputs, 1))
return net
房价就像股票价格一样,我们关心的是相对数量,而不是绝对数量。 因此,我们更关心相对误差
y
−
y
h
a
t
y
\frac{y - y^{hat}}{y}
yy−yhat, 而不是绝对误差
y
−
y
h
a
t
y - y^{hat}
y−yhat
。 例如,如果我们在俄亥俄州农村地区估计一栋房子的价格时, 假设我们的预测偏差了10万美元, 然而那里一栋典型的房子的价值是12.5万美元, 那么模型可能做得很糟糕。 另一方面,如果我们在加州豪宅区的预测出现同样的10万美元的偏差,(在那里,房价中位数超过400万美元) 这可能是一个不错的预测。
解决这个问题的一种方法是用价格预测的对数来衡量差异。 事实上,这也是比赛中官方用来评价提交质量的误差指标。
即将 − δ < = ∣ y − y h a t ∣ < = δ -\delta <= |y - y^{hat}| <= \delta −δ<=∣y−yhat∣<=δ 转化为 e − δ < = ∣ y y h a t ∣ < = e δ e^{-\delta} <= |\frac{y}{y^{hat}}| <= e^{\delta} e−δ<=∣yhaty∣<=eδ。这使得预测价格的对数与真实标签价格的对数之间出现以下均方根误差:
1 n ∑ i = 1 n ( l o g y i − l o g y i h a t ) 2 \sqrt{\frac{1}{n}\sum\limits_{i=1}^{n}(log y_i - log y_{i}^{hat})^2} n1i=1∑n(logyi−logyihat)2
#定义均方根误差函数
def log_rmse(net, features, labels):
#对小于1的预测值均取最小值1
clipped_preds = torch.clamp(net(features), 1, float('inf'))
rmse = torch.sqrt(loss(torch.log(clipped_preds),
torch.log(labels))) #计算对数形式二点均方根误差
return rmse.item() #返回损失的值
与前面的部分不同,我们的训练函数将借助Adam优化器 (我们将在后面章节更详细地描述它)。 Adam优化器的主要吸引力在于它对初始学习率不那么敏感。
#训练数据集,并返回对数情况下的训练集与测试集的损失列表
def train(net, train_features, train_labels, test_features, test_labels,
num_epochs, learning_rate, weight_decay, batch_size):
#定义训练集、测试集损失列表
train_ls, test_ls = [], []
#获取数据集迭代器,没次迭代大小为batch_size
train_iter = d2l.load_array((train_features, train_labels), batch_size)
#这里使用的是Adam优化算法,参数分别为线性神经网络参数w,学习率,正则化参数
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:
optimizer.zero_grad() #清除缓存的梯度
l = loss(net(X), y) #计算损失值
l.backward() #反向传播计算梯度
optimizer.step() #更新参数w
#计算对数情况下的损失函数,追加到训练集损失列表中
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 K 折交叉验证, 它有助于模型选择和超参数调整。
我们首先需要定义一个函数,在 K K K 折交叉验证过程中返回第 i i i 折的数据。 具体地说,它选择第 i i i 个切片作为验证数据,其余部分作为训练数据。 注意,这并不是处理数据的最有效方法,如果我们的数据集大得多,会有其他解决办法。
#获取K折交叉对应的1种---训练集测试集切分方式(共有K种)
def get_k_fold_data(k, i, X, y):
assert k > 1 #K折交叉验证,其中K必须要大于1
fold_size = X.shape[0] // k #将数据集切分为K份,每份大小为
X_train, y_train = None, None #初始化训练集为空
#通过循环k次,将数据集进行切分,训练集包含样本数为 (k-1)*fold_size 份, 测试集包含样本数为 1*fold_size 份
for j in range(k):
#slice(start,end):方法可从已有数组中返回选定的元素,返回一个新数组,包含从start到end(不包含该元素)的数组元素
idx = slice(j * fold_size, (j + 1) * fold_size) #首先计算每一小份数据的下标,即(0, n)、(n, 2n)等
X_part, y_part = X[idx, :], y[idx] #取出数据集对应的样本X[0, n]、X[n, 2n]个等
#若j == i,此时该 fold_size个 数据集作为测试集数据
if j == i:
X_valid, y_valid = X_part, y_part #将此fold_size个样本作为测试数据集
#如果训练数据集还是空
elif X_train is None:
X_train, y_train = X_part, y_part #将此fold_size个样本作为训练数据集
#不是前面的两种情况,则将fold_size个样本添加到训练集中,完成(k-1) * fold_size个样本
else:
X_train = torch.cat([X_train, X_part], 0) #追加到训练集,0代表对每一列追加新的样本数据
y_train = torch.cat([y_train, y_part], 0) #列数不变,样本数增加
return X_train, y_train, X_valid, y_valid #返回训练集、测试集数据
当我们在 K K K 折交叉验证中训练 K K K 次后,返回训练和验证误差的平均值。
#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 #初始化训练集损失和、测试集损失和为0
#进行K次交叉验证,测试模型的性能
for i in range(k):
data = get_k_fold_data(k, i, X_train, y_train) #获取K切分函数返回的数据集与测试集
net = get_net() #获取神经网络
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.plot(list(range(1, num_epochs + 1)), [train_ls, valid_ls],
xlabel='epoch', ylabel='rmse', xlim=[1, num_epochs],
legend=['train', 'valid'], yscale='log')
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小批量数据大小
"""
k, num_epochs, lr, weight_decay, batch_size = 5, 100, 5, 0, 64
#调用K折交叉验证函数获取模型最终训练集与测试集的损失
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}')
折1, 训练log rmse0.170476,验证log rmse0.157046
折2, 训练log rmse0.162232,验证log rmse0.189727
折3, 训练log rmse0.163782,验证log rmse0.168296
折4, 训练log rmse0.168180,验证log rmse0.154713
折5, 训练log rmse0.163380,验证log rmse0.182908
5-折验证:平均训练log rmse: 0.165610,平均验证log rmse: 0.170538
请注意,有时一组超参数的训练误差可能非常低,但 K K K 折交叉验证的误差要高得多, 这表明模型过拟合了。 在整个训练过程中,你将希望监控训练误差和验证误差这两个数字。 较少的过拟合可能表明现有数据可以支撑一个更强大的模型, 较大的过拟合可能意味着我们可以通过正则化技术来获益。
提交 Kaggle 预测
既然我们知道应该选择什么样的超参数, 我们不妨使用所有数据对其进行训练 (而不是仅使用交叉验证中使用的数据)。
然后,我们通过这种方式获得的模型可以应用于测试集。 将预测保存在CSV文件中可以简化将结果上传到Kaggle的过程。
"""
train_features训练集的特征
test_features测试集特征
train_labels训练集的标签
test_data测试数据
num_epochs迭代次数
lr学习率
weight_decay正则化参数
batch_size批量数据集大小
"""
def train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size):
net = get_net() #定义神经网络
#训练模型,获取训练集损失
train_ls, _ = train(net, train_features, train_labels, None, None,
num_epochs, lr, weight_decay, batch_size)
#绘制出训练集的损失关于迭代次数的函数图
d2l.plot(np.arange(1, num_epochs+1), [train_ls], xlabel='epoch',
ylabel='log rmse', xlim=[1, num_epochs], yscale='log')
print(f'训练log rmse: {float(train_ls[-1]):f}')
#将网络应用于测试集,预测结果
preds = net(test_features).detach().numpy()
#将预测结果集转化为pandas数据类型,并作为test_data的列SalePrice
test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0])
#连接预测样本的id和预测值SalePrice,生成一个新的pandas列表
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)
训练log rmse: 0.162249
接下来,如下图所示, 我们可以提交预测到Kaggle上,并查看在测试集上的预测与实际房价(标签)的比较情况。 步骤非常简单:
登录Kaggle网站,访问房价预测竞赛页面。
点击“Submit Predictions”或“Late Submission”按钮(在撰写本文时,该按钮位于右侧)。
点击页面底部虚线框中的“Upload Submission File”按钮,选择你要上传的预测文件。
点击页面底部的“Make Submission”按钮,即可查看你的结果。
顺便附上我的提交与排名
小结
1、真实数据通常混合了不同的数据类型,需要进行预处理。
2、常用的预处理方法:将实值数据重新缩放为零均值和单位方法;用均值替换缺失值。
3、将类别特征转化为指标特征,可以使我们把这个特征当作一个独热向量来对待。
4、我们可以使用 K K K 折交叉验证 来选择模型并调整超参数。
5、对数 L o g Log Log 对于相对误差很有用。