5.7. Kaggle房价预测
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

现在我们已经介绍了构建和训练深度网络的一些基本工具,以及包括权重衰减和暂退法在内的正则化技术,我们准备通过参加Kaggle竞赛来将所有这些知识付诸实践。房价预测竞赛是一个很好的起点。数据是相当通用的,没有表现出需要专门模型(如音频或视频)的奇异结构。这个数据集由 De Cock (2011) 收集,涵盖了2006-2010年期间爱荷华州埃姆斯市的房价。它比Harrison和Rubinfeld(1978)著名的 波士顿房价数据集 要大得多,拥有更多的样本和特征。

在本节中,我们将详细介绍数据预处理、模型设计和超参数选择。我们希望通过动手实践的方法,你能获得一些指导你作为数据科学家职业生涯的直觉。

%matplotlib inline
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2l
%matplotlib inline
import pandas as pd
from mxnet import autograd, gluon, init, np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l

npx.set_np()
%matplotlib inline
import jax
import numpy as np
import pandas as pd
from jax import numpy as jnp
from d2l import jax as d2l
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
%matplotlib inline
import pandas as pd
import tensorflow as tf
from d2l import tensorflow as d2l

5.7.1. 下载数据

在本书中,我们将在各种下载的数据集上训练和测试模型。在这里,我们实现了两个用于下载和提取zip或tar文件的实用函数。同样,我们跳过这些实用函数的实现细节。

def download(url, folder, sha1_hash=None):
    """Download a file to folder and return the local filepath."""

def extract(filename, folder):
    """Extract a zip/tar file into folder."""

5.7.2. Kaggle

Kaggle 是一个举办机器学习竞赛的流行平台。每个竞赛都围绕一个数据集展开,许多竞赛由利益相关者赞助,他们为获胜的解决方案提供奖品。该平台通过论坛和共享代码帮助用户互动,促进了协作和竞争。虽然追逐排行榜往往会失控,研究人员会狭隘地关注预处理步骤而不是提出根本性问题,但一个能够促进竞争方法之间直接定量比较以及代码共享以便每个人都能学习到哪些方法有效、哪些无效的平台的客观性也具有巨大的价值。如果你想参加Kaggle竞赛,你首先需要注册一个账户(见 图 5.7.1)。

../_images/kaggle.png

图 5.7.1 Kaggle网站。

在房价预测竞赛页面上,如 图 5.7.2 所示,你可以找到数据集(在“Data”选项卡下),提交预测,并查看你的排名。URL就在这里

../_images/house-pricing.png

图 5.7.2 房价预测竞赛页面。

5.7.3. 访问和读取数据集

请注意,竞赛数据分为训练集和测试集。每条记录包括房屋的房产价值和诸如街道类型、建造年份、屋顶类型、地下室状况等属性。这些特征由各种数据类型组成。例如,建造年份由整数表示,屋顶类型由离散的分类分配表示,其他特征由浮点数表示。现实情况使事情变得复杂:对于某些样本,一些数据完全缺失,缺失值仅标记为“na”。每栋房屋的价格仅包含在训练集中(毕竟这是一场竞赛)。我们将需要划分训练集以创建验证集,但我们只有在向Kaggle上传预测后才能在官方测试集上评估我们的模型。图 5.7.2 中的竞赛选项卡的“Data”选项卡有下载数据的链接。

首先,我们将使用我们在 2.2节 中介绍的 pandas 来读取和处理数据。为方便起见,我们可以下载并缓存Kaggle住房数据集。如果缓存目录中已存在与此数据集对应的文件,并且其SHA-1与 sha1_hash 匹配,我们的代码将使用缓存的文件,以避免因冗余下载而堵塞您的互联网。

class KaggleHouse(d2l.DataModule):
    def __init__(self, batch_size, train=None, val=None):
        super().__init__()
        self.save_hyperparameters()
        if self.train is None:
            self.raw_train = pd.read_csv(d2l.download(
                d2l.DATA_URL + 'kaggle_house_pred_train.csv', self.root,
                sha1_hash='585e9cc93e70b39160e7921475f9bcd7d31219ce'))
            self.raw_val = pd.read_csv(d2l.download(
                d2l.DATA_URL + 'kaggle_house_pred_test.csv', self.root,
                sha1_hash='fa19780a7b011d9b009e8bff8e99922a8ee2eb90'))

训练数据集包括1460个样本,80个特征和一个标签,而验证数据包含1459个样本和80个特征。

data = KaggleHouse(batch_size=64)
print(data.raw_train.shape)
print(data.raw_val.shape)
Downloading ../data/kaggle_house_pred_train.csv from http://d2l-data.s3-accelerate.amazonaws.com/kaggle_house_pred_train.csv...
Downloading ../data/kaggle_house_pred_test.csv from http://d2l-data.s3-accelerate.amazonaws.com/kaggle_house_pred_test.csv...
(1460, 81)
(1459, 80)
data = KaggleHouse(batch_size=64)
print(data.raw_train.shape)
print(data.raw_val.shape)
Downloading ../data/kaggle_house_pred_train.csv from http://d2l-data.s3-accelerate.amazonaws.com/kaggle_house_pred_train.csv...
Downloading ../data/kaggle_house_pred_test.csv from http://d2l-data.s3-accelerate.amazonaws.com/kaggle_house_pred_test.csv...
(1460, 81)
(1459, 80)
data = KaggleHouse(batch_size=64)
print(data.raw_train.shape)
print(data.raw_val.shape)
Downloading ../data/kaggle_house_pred_train.csv from http://d2l-data.s3-accelerate.amazonaws.com/kaggle_house_pred_train.csv...
Downloading ../data/kaggle_house_pred_test.csv from http://d2l-data.s3-accelerate.amazonaws.com/kaggle_house_pred_test.csv...
(1460, 81)
(1459, 80)
data = KaggleHouse(batch_size=64)
print(data.raw_train.shape)
print(data.raw_val.shape)
Downloading ../data/kaggle_house_pred_train.csv from http://d2l-data.s3-accelerate.amazonaws.com/kaggle_house_pred_train.csv...
Downloading ../data/kaggle_house_pred_test.csv from http://d2l-data.s3-accelerate.amazonaws.com/kaggle_house_pred_test.csv...
(1460, 81)
(1459, 80)

5.7.4. 数据预处理

让我们看一下前四个样本的前四个和最后两个特征以及标签(SalePrice)。

print(data.raw_train.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
print(data.raw_train.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
print(data.raw_train.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
print(data.raw_train.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

我们可以看到,在每个样本中,第一个特征是标识符。这有助于模型确定每个训练样本。虽然这很方便,但它不携带任何用于预测的信息。因此,在将数据送入模型之前,我们将从数据集中移除它。此外,考虑到数据类型的多样性,在开始建模之前,我们需要对数据进行预处理。

让我们从数值特征开始。首先,我们采用一种启发式方法,用相应特征的均值替换所有缺失值。然后,为了将所有特征放在一个共同的尺度上,我们通过将特征重新缩放到零均值和单位方差来对数据进行标准化

(5.7.1)\[x \leftarrow \frac{x - \mu}{\sigma},\]

其中 \(\mu\)\(\sigma\) 分别表示均值和标准差。为了验证这确实将我们的特征(变量)转换为了零均值和单位方差,请注意 \(E[\frac{x-\mu}{\sigma}] = \frac{\mu - \mu}{\sigma} = 0\) 并且 \(E[(x-\mu)^2] = (\sigma^2 + \mu^2) - 2\mu^2+\mu^2 = \sigma^2\)。直观地说,我们标准化数据有两个原因。首先,它方便优化。其次,因为我们先验地不知道哪些特征是相关的,我们不希望惩罚分配给某个特征的系数比其他任何特征更多。

接下来我们处理离散值。这些包括诸如“MSZoning”之类的特征。我们用独热编码替换它们,就像我们之前将多类标签转换为向量一样(参见 4.1.1节)。例如,“MSZoning”取值为“RL”和“RM”。去掉“MSZoning”特征后,创建了两个新的指示符特征“MSZoning_RL”和“MSZoning_RM”,其值为0或1。根据独热编码,如果“MSZoning”的原始值为“RL”,则“MSZoning_RL”为1,“MSZoning_RM”为0。pandas包会自动为我们完成这项工作。

@d2l.add_to_class(KaggleHouse)
def preprocess(self):
    # Remove the ID and label columns
    label = 'SalePrice'
    features = pd.concat(
        (self.raw_train.drop(columns=['Id', label]),
         self.raw_val.drop(columns=['Id'])))
    # Standardize numerical columns
    numeric_features = features.dtypes[features.dtypes!='object'].index
    features[numeric_features] = features[numeric_features].apply(
        lambda x: (x - x.mean()) / (x.std()))
    # Replace NAN numerical features by 0
    features[numeric_features] = features[numeric_features].fillna(0)
    # Replace discrete features by one-hot encoding
    features = pd.get_dummies(features, dummy_na=True)
    # Save preprocessed features
    self.train = features[:self.raw_train.shape[0]].copy()
    self.train[label] = self.raw_train[label]
    self.val = features[self.raw_train.shape[0]:].copy()

你可以看到,这种转换将特征数量从79个增加到331个(不包括ID和标签列)。

data.preprocess()
data.train.shape
(1460, 331)
data.preprocess()
data.train.shape
(1460, 331)
data.preprocess()
data.train.shape
(1460, 331)
data.preprocess()
data.train.shape
(1460, 331)

5.7.5. 误差度量

首先,我们将训练一个带有平方损失的线性模型。不出所料,我们的线性模型不会导致一个赢得比赛的提交,但它确实提供了一个健全性检查,以查看数据中是否存在有意义的信息。如果在这里我们不能做得比随机猜测更好,那么很有可能我们有一个数据处理的bug。如果一切顺利,线性模型将作为一个基线,给我们一些关于简单模型与最佳报告模型有多接近的直觉,让我们感觉到我们应该从更复杂的模型中期望获得多少增益。

对于房价,和股价一样,我们更关心相对数量而不是绝对数量。因此,我们倾向于更关心相对误差 \(\frac{y - \hat{y}}{y}\) 而不是绝对误差 \(y - \hat{y}\)。例如,如果我们在估计俄亥俄州农村一所房屋的价格时,预测偏差了100,000美元,而那里一所典型房屋的价值是125,000美元,那么我们可能做得非常糟糕。另一方面,如果我们在加利福尼亚州洛斯阿尔托斯山出现这个数量的误差,这可能代表一个惊人准确的预测(在那里,房屋价格中位数超过400万美元)。

解决这个问题的一种方法是测量价格估计对数的差异。事实上,这也是竞赛用来评估提交质量的官方误差度量。毕竟,\(|\log y - \log \hat{y}| \leq \delta\) 的小值 \(\delta\) 会转化为 \(e^{-\delta} \leq \frac{\hat{y}}{y} \leq e^\delta\)。这导致了预测价格的对数和标签价格的对数之间的以下均方根误差:

(5.7.2)\[\sqrt{\frac{1}{n}\sum_{i=1}^n\left(\log y_i -\log \hat{y}_i\right)^2}.\]
@d2l.add_to_class(KaggleHouse)
def get_dataloader(self, train):
    label = 'SalePrice'
    data = self.train if train else self.val
    if label not in data: return
    get_tensor = lambda x: torch.tensor(x.values.astype(float),
                                      dtype=torch.float32)
    # Logarithm of prices
    tensors = (get_tensor(data.drop(columns=[label])),  # X
               torch.log(get_tensor(data[label])).reshape((-1, 1)))  # Y
    return self.get_tensorloader(tensors, train)
@d2l.add_to_class(KaggleHouse)
def get_dataloader(self, train):
    label = 'SalePrice'
    data = self.train if train else self.val
    if label not in data: return
    get_tensor = lambda x: np.array(x.values.astype(float),
                                      dtype=np.float32)
    # Logarithm of prices
    tensors = (get_tensor(data.drop(columns=[label])),  # X
               np.log(get_tensor(data[label])).reshape((-1, 1)))  # Y
    return self.get_tensorloader(tensors, train)
@d2l.add_to_class(KaggleHouse)
def get_dataloader(self, train):
    label = 'SalePrice'
    data = self.train if train else self.val
    if label not in data: return
    get_tensor = lambda x: jnp.array(x.values.astype(float),
                                      dtype=jnp.float32)
    # Logarithm of prices
    tensors = (get_tensor(data.drop(columns=[label])),  # X
               jnp.log(get_tensor(data[label])).reshape((-1, 1)))  # Y
    return self.get_tensorloader(tensors, train)
@d2l.add_to_class(KaggleHouse)
def get_dataloader(self, train):
    label = 'SalePrice'
    data = self.train if train else self.val
    if label not in data: return
    get_tensor = lambda x: tf.constant(x.values.astype(float),
                                      dtype=tf.float32)
    # Logarithm of prices
    tensors = (get_tensor(data.drop(columns=[label])),  # X
               tf.reshape(tf.math.log(get_tensor(data[label])), (-1, 1)))  # Y
    return self.get_tensorloader(tensors, train)

5.7.6. \(K\)-折交叉验证

你可能还记得,我们在 3.6.3节 中介绍了交叉验证,当时我们讨论了如何处理模型选择。我们将充分利用它来选择模型设计和调整超参数。我们首先需要一个函数,在\(K\)-折交叉验证过程中返回第 \(i^\textrm{th}\) 折的数据。它通过将第 \(i^\textrm{th}\) 段切片作为验证数据,并将其余部分作为训练数据返回。请注意,这不是处理数据的最有效方式,如果我们的数据集大得多,我们肯定会做一些更聪明的事情。但这种增加的复杂性可能会不必要地混淆我们的代码,所以由于我们问题的简单性,我们可以在这里安全地省略它。

def k_fold_data(data, k):
    rets = []
    fold_size = data.train.shape[0] // k
    for j in range(k):
        idx = range(j * fold_size, (j+1) * fold_size)
        rets.append(KaggleHouse(data.batch_size, data.train.drop(index=idx),
                                data.train.loc[idx]))
    return rets

当我们在\(K\)-折交叉验证中训练\(K\)次时,返回的是平均验证误差。

def k_fold(trainer, data, k, lr):
    val_loss, models = [], []
    for i, data_fold in enumerate(k_fold_data(data, k)):
        model = d2l.LinearRegression(lr)
        model.board.yscale='log'
        if i != 0: model.board.display = False
        trainer.fit(model, data_fold)
        val_loss.append(float(model.board.data['val_loss'][-1].y))
        models.append(model)
    print(f'average validation log mse = {sum(val_loss)/len(val_loss)}')
    return models

5.7.7. 模型选择

在这个例子中,我们选择了一组未经调整的超参数,并留给读者来改进模型。找到一个好的选择可能需要时间,这取决于一个人优化的变量数量。对于足够大的数据集和正常的超参数类型,\(K\)-折交叉验证往往对多重测试具有相当的弹性。但是,如果我们尝试了过多的选项,我们可能会发现我们的验证性能不再能代表真实误差。

trainer = d2l.Trainer(max_epochs=10)
models = k_fold(trainer, data, k=5, lr=0.01)
average validation log mse = 0.17325432986021042
../_images/output_kaggle-house-price_1852a7_88_1.svg
trainer = d2l.Trainer(max_epochs=10)
models = k_fold(trainer, data, k=5, lr=0.01)
average validation log mse = 0.12402758479118345
../_images/output_kaggle-house-price_1852a7_91_1.svg
trainer = d2l.Trainer(max_epochs=10)
models = k_fold(trainer, data, k=5, lr=0.01)
average validation log mse = 0.12551604807376862
../_images/output_kaggle-house-price_1852a7_94_1.svg
trainer = d2l.Trainer(max_epochs=10)
models = k_fold(trainer, data, k=5, lr=0.01)
average validation log mse = 0.17646737486124037
../_images/output_kaggle-house-price_1852a7_97_1.svg

请注意,有时一组超参数的训练误差数量可能非常低,即使在\(K\)-折交叉验证上的误差数量显著增加。这表明我们正在过拟合。在整个训练过程中,您需要监控这两个数字。较少的过拟合可能表明我们的数据可以支持一个更强大的模型。大规模的过拟合可能表明我们可以通过引入正则化技术来获益。

5.7.8. 在Kaggle上提交预测

现在我们知道了什么是好的超参数选择,我们可能会计算所有\(K\)个模型在测试集上的平均预测。将预测保存在csv文件中将简化将结果上传到Kaggle的过程。以下代码将生成一个名为submission.csv的文件。

preds = [model(torch.tensor(data.val.values.astype(float), dtype=torch.float32))
         for model in models]
# Taking exponentiation of predictions in the logarithm scale
ensemble_preds = torch.exp(torch.cat(preds, 1)).mean(1)
submission = pd.DataFrame({'Id':data.raw_val.Id,
                           'SalePrice':ensemble_preds.detach().numpy()})
submission.to_csv('submission.csv', index=False)
preds = [model(np.array(data.val.values.astype(float), dtype=np.float32))
         for model in models]
# Taking exponentiation of predictions in the logarithm scale
ensemble_preds = np.exp(np.concatenate(preds, 1)).mean(1)
submission = pd.DataFrame({'Id':data.raw_val.Id,
                           'SalePrice':ensemble_preds.asnumpy()})
submission.to_csv('submission.csv', index=False)
preds = [model.apply({'params': trainer.state.params},
         jnp.array(data.val.values.astype(float), dtype=jnp.float32))
         for model in models]
# Taking exponentiation of predictions in the logarithm scale
ensemble_preds = jnp.exp(jnp.concatenate(preds, 1)).mean(1)
submission = pd.DataFrame({'Id':data.raw_val.Id,
                           'SalePrice':np.asarray(ensemble_preds)})
submission.to_csv('submission.csv', index=False)
preds = [model(tf.constant(data.val.values.astype(float), dtype=tf.float32))
         for model in models]
# Taking exponentiation of predictions in the logarithm scale
ensemble_preds = tf.reduce_mean(tf.exp(tf.concat(preds, 1)), 1)
submission = pd.DataFrame({'Id':data.raw_val.Id,
                           'SalePrice':ensemble_preds.numpy()})
submission.to_csv('submission.csv', index=False)

接下来,如 图 5.7.3 所示,我们可以在Kaggle上提交我们的预测,看看它们与测试集上的实际房价(标签)相比如何。步骤非常简单:

  • 登录Kaggle网站并访问房价预测竞赛页面。

  • 点击“Submit Predictions”或“Late Submission”按钮。

  • 点击页面底部虚线框中的“Upload Submission File”按钮,并选择您希望上传的预测文件。

  • 点击页面底部的“Make Submission”按钮以查看您的结果。

../_images/kaggle-submit2.png

图 5.7.3 向Kaggle提交数据

5.7.9. 小结与讨论

真实数据通常包含不同数据类型的混合,需要进行预处理。将实值数据重新缩放到零均值和单位方差是一个很好的默认设置。用它们的均值替换缺失值也是如此。此外,将分类特征转换为指示符特征使我们能够像处理独热向量一样处理它们。当我们更关心相对误差而不是绝对误差时,我们可以测量预测对数的差异。要选择模型和调整超参数,我们可以使用\(K\)-折交叉验证。

5.7.10. 练习

  1. 将您在本节中的预测提交到Kaggle。它们的效果如何?

  2. 用均值替换缺失值总是好主意吗?提示:你能否构造一个值不是随机缺失的情况?

  3. 通过\(K\)-折交叉验证调整超参数来提高分数。

  4. 通过改进模型(例如,层数、权重衰减和暂退法)来提高分数。

  5. 如果我们不像本节中那样对连续数值特征进行标准化会发生什么?