Kaggle实战:预测房价
本文利用之前学习的多层感知机和深度学习相关知识,从零开始进行预测房价的实战练习
文章部分文字和代码来自《动手学深度学习》
数据集
数据集的缓存
import hashlib
import os
import tarfile
import zipfile
import requests
DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'
以下代码实现了从DATA_HUB下载数据集并缓存到本地文件中的功能。
函数定义了两个参数:name和cache_dir,其中name指定了需要下载的数据集的名称,cache_dir指定了数据集下载到本地缓存的目录。
首先,函数检查所请求的数据集是否在DATA_HUB中定义,如果没有定义则抛出一个异常。
接着,函数从DATA_HUB中获取数据集的下载链接和SHA1校验和,同时在本地创建一个缓存目录cache_dir。
然后,函数将数据集下载链接的最后一部分作为本地文件名,并将该文件名与缓存目录拼接成完整的本地文件路径fname。
如果文件fname已经存在,则使用sha1哈希算法检查文件是否已被正确下载并返回文件名。如果文件不完整或者sha1哈希值不匹配,则重新下载该文件。
如果文件fname不存在,则从链接下载数据集,并将数据集写入本地文件fname中。
最后,函数返回本地缓存文件的文件名fname。
def download(name, cache_dir=os.path.join('..', 'data')): #@save
"""下载一个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:
break
sha1.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
这是一个函数download_extract,用于从DATA_HUB下载并解压缩zip/tar文件,返回解压后的文件夹路径。具体解释如下:
name:需要下载的文件名,必须在预先定义好的DATA_HUB中存在。
folder:可选参数,解压后的文件夹名称,默认为None。
fname:通过调用download函数下载文件,并返回文件路径。
base_dir:返回fname所在的文件夹路径。
data_dir和ext:通过os.path.splitext函数,返回fname的文件名和扩展名。
fp:根据文件扩展名,使用zipfile或tarfile库打开文件,并返回文件对象。
fp.extractall:将文件解压到base_dir中。
返回解压后的文件夹路径,如果folder有值,则在文件夹路径后追加folder作为子文件夹名。
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')
elif ext in ('.tar', '.gz'):
fp = tarfile.open(fname, 'r')
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)
数据集的读取
%matplotlib inline
import numpy as np
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2l
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')
train_data = pd.read_csv(download('kaggle_house_train'))
test_data = pd.read_csv(download('kaggle_house_test'))
观察shape
print(train_data.shape)
print(test_data.shape)
删除第一个ID,不作为特征使用
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
train_data.iloc[:, 1:-1] 是选取 train_data 中除了第一列和最后一列的所有列,使用的是 pandas 库的 iloc 方法,iloc 的使用方法是通过索引选取行和列。这里的 : 表示选取所有行,1:-1 表示选取第 2 到倒数第 2 列的所有列(不包括最后一列)。
数据预处理
在开始建模之前,我们需要对数据进行预处理。 首先,我们将所有缺失的值替换为相应特征的平均值。然后,为了将所有特征放在一个共同的尺度上, 我们通过将特征重新缩放到零均值和单位方差来标准化数据。
数据标准化:
数据标准化(Normalization)是一种常见的数据预处理技术,它将数据按比例缩放,使其落入一个特定的区间,一般是[0,1]或者[-1,1]。数据标准化可以消除数据间量纲的影响,防止某些特征的权重过大而影响模型训练的结果。
常用的数据标准化方法有以下几种,我们这里使用第二种方法:
最小-最大缩放(Min-Max Scaling): 也叫离差标准化,将数据按照最大值和最小值进行缩放,公式为: x n e w = x − x m i n x m a x − x m i n x_{new}=\frac{x-x_{min}}{x_{max}-x_{min}} xnew=xmax−xminx−xmin。
Z-Score标准化:也叫标准差标准化,将数据按照均值和标准差进行缩放,公式为: x n e w = x − μ σ x_{new}=\frac{x-\mu}{\sigma} xnew=σx−μ,其中 μ \mu μ表示样本均值, σ \sigma σ表示样本标准差。
小数定标标准化(Decimal Scaling):将数据除以一个固定的数,使得最终的数据小于1,公式为: x n e w = x 1 0 j x_{new}=\frac{x}{10^j} xnew=10jx,其中 j j j表示一个正整数,一般取数据绝对值的最大值的位数。
# 若无法获得测试数据,则可根据训练数据计算均值和标准差
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.dtypes[all_features.dtypes != ‘object’].index 找到所有数值型特征的索引,这段代码会将所有数值型特征的列名组成一个Index对象返回,然后对这些特征应用标准化操作。标准化的方法是对每个特征进行零均值化和单位方差化,即将每个特征的每个值减去该特征的平均值,再除以该特征的标准差。这样可以将数据缩放到一个相似的尺度上,有利于提高模型的训练效果。
由于测试数据的标签是未知的,无法计算测试数据的均值和标准差,因此在这里使用了训练数据的均值和标准差来标准化测试数据。在标准化数据之后,由于所有的特征都被零均值化和单位方差化,因此缺失值可以被设置为0。
all_features = pd.get_dummies(all_features, dummy_na=True)
all_features.shape
这段代码中,首先使用pd.get_dummies函数对all_features中的非数值型特征进行独热编码,将其转化为数值型特征,同时将缺失值也进行编码(使用dummy_na=True)。
函数pd.get_dummies返回一个DataFrame对象,其中包含了所有的原特征以及其对应的独热编码后的特征。因此,all_features中的每个非数值型特征都被转化成了若干个数值型特征,其特征数增加了相应的数量。
all_features.shape返回all_features的形状,即行数和列数,它的值反映了编码后特征的数量。
这样做不仅让原本非数值类型的特征转变为可以训练的数值类特征,而且将特征数量扩大,有利于训练的进行
pd.get_dummies讲解
pd.get_dummies是Pandas库中的一个函数,用于将数据集中的离散特征转换成one-hot编码的形式。对于原来的数据集中的每一个离散特征,函数会创建一个新的虚拟特征,并将值转换为0或1。当原来的特征值为某个值时,新的特征的值为1,否则为0。此外,如果原来的数据集中该特征存在缺失值,函数还会创建一个新的虚拟特征来表示缺失值。返回值是一个新的DataFrame对象,其中包含one-hot编码后的所有特征。
举个例子:
假设有一个 DataFrame 如下:
Name Gender 0 Tom M 1 Lucy F 2 John M
可以使用 pd.get_dummies() 函数将分类变量(categorical variables)转换为数值变量(numerical variables):
pd.get_dummies(df)
结果为;
Name_John Name_Lucy Name_Tom Gender_F Gender_M 0 0 0 1 0 1 1 0 1 0 1 0 2 1 0 0 0 1
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)
这段代码的作用是将标准化后的训练数据和测试数据转换为 PyTorch 张量形式,并将训练数据的标签转换为 PyTorch 张量形式。
具体解释如下:
n_train = train_data.shape[0]:获取训练数据的行数,即样本数。
train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float32):将前 n_train 行的特征数据转换为 PyTorch 张量,数据类型为 float32。
test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float32):将从第 n_train 行开始的特征数据转换为 PyTorch 张量,数据类型为 float32。
train_labels = torch.tensor(train_data.SalePrice.values.reshape(-1, 1), dtype=torch.float32):将训练数据的房价标签转换为 PyTorch 张量,数据类型为 float32,并将其转换为列向量形式(因为 PyTorch 张量默认是行向量形式)。其中:train_data.SalePrice.values 返回训练数据集中房屋销售价格的值,其类型为numpy数组,.reshape(-1, 1)的作用是将该数组从一维数组转化为列向量,也就是将其变成n行1列的矩阵,其中n为数组的长度。
这样,我们就可以使用 PyTorch 构建模型并进行训练和预测。
训练
损失函数
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的值设置为1
clipped_preds = torch.clamp(net(features), 1, float('inf'))
rmse = torch.sqrt(loss(torch.log(clipped_preds),
torch.log(labels)))
return rmse.item()
这段代码定义了一个函数log_rmse,其输入包括一个神经网络net,数据特征features和标签labels。该函数计算的是对数均方根误差(log root mean squared error, log_rmse)。
在函数中,首先通过net(features)计算得到预测值preds,然后通过torch.clamp将预测值限制在1和正无穷之间。这是因为当标签值小于1时,取对数后会导致计算值不稳定,为了避免这种情况,将预测值限制在1以上。
函数解析:torch.clamp
torch.clamp 是 PyTorch 中的一个函数,用于将输入张量(Tensor)中的所有元素限制在一个指定的范围内。具体来说,它将所有小于指定下限的元素设置为该下限,所有大于指定上限的元素设置为该上限,其余元素保持不变。
然后,通过loss(torch.log(clipped_preds), torch.log(labels))计算预测值和标签值的对数均方根误差。torch.log表示对数函数,即以自然对数为底的对数。最后,通过.item()方法将结果转换为标量值并返回。
训练函数
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:
optimizer.zero_grad()
l = loss(net(X), y)
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
这是一个训练函数,它通过对训练数据集进行迭代来训练神经网络。
具体而言,该函数的输入包括:
net:神经网络模型。
train_features:训练数据集的特征,以张量形式表示。
train_labels:训练数据集的标签,以张量形式表示。
test_features:测试数据集的特征,以张量形式表示。
test_labels:测试数据集的标签,以张量形式表示。
num_epochs:迭代周期数。
learning_rate:学习率。
weight_decay:权重衰减。
batch_size:批量大小。
该函数的输出包括:
train_ls:每个迭代周期的训练损失。
test_ls:每个迭代周期的测试损失。
K折交叉验证
def get_k_fold_data(k, i, X, y):
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], 0)
y_train = torch.cat([y_train, y_part], 0)
return X_train, y_train, X_valid, y_valid
这段代码实现了 K 折交叉验证的过程,将数据集划分为 K 份,每次选择其中一份作为验证集,剩余 K-1 份作为训练集,然后将训练和验证集输入到模型中进行训练和验证,最终计算出 K 个验证结果,取平均值作为最终的验证结果。
get_k_fold_data 函数用于获取 K 折交叉验证数据集中指定折数的数据,同时返回剩余的数据作为训练集。具体来说,该函数输入参数包括 k 表示 K 折交叉验证,i 表示要获取的数据所在的折数,X 和 y 分别为输入特征和标签。函数返回值包括 X_train、y_train、X_valid 和 y_valid,分别表示剩余的训练集和指定折数的验证集。
slice是Python中的内置类型,表示一个切片对象。当我们取一个可迭代对象的一部分时,可以使用slice来指定需要取的区间。
函数的具体实现如下:
- 通过 X.shape[0] // k 计算出每一折的大小 fold_size。
- 对于 K 折交叉验证中的每一折,获取其对应的索引 idx。
- 如果当前折为指定折数 i,则将该折的数据作为验证集 X_valid 和 y_valid。
- 如果当前折不是指定折数,且 X_train 和 y_train 为空,则将该折数据赋值给 X_train 和 y_train。
- 如果当前折不是指定折数,且 X_train 和 y_train 不为空,则将该折数据追加到 X_train 和 y_train 中。
- 返回训练集 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()
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 k k 折。每次使用其中 k − 1 k-1 k−1 折数据作为训练集,剩下的一折数据作为验证集。由此可以训练出 k k k 个模型。这些模型的训练误差和验证误差分别用于评估模型在训练集和验证集上的表现。
函数中的参数含义如下:
k:
k
k
k 折交叉验证中的
k
k
k;
X_train:训练数据集的特征;
y_train:训练数据集的标签;
num_epochs:训练的轮数;
learning_rate:学习率;
weight_decay:权重衰减;
batch_size:批量大小。
函数输出的是 k k k 次训练和验证的平均训练误差和平均验证误差。在每次训练中,使用了 train 函数来训练模型。每次训练后,将训练误差和验证误差分别加到 train_l_sum 和 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(f'{k}-折验证: 平均训练log rmse: {float(train_l):f}, '
f'平均验证log rmse: {float(valid_l):f}')
训练结果
提交Kaggle
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()
# 将其重新格式化以导出到Kaggle
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)