《动手学深度学习》——预测房价

参考资料:

  • House Prices - Advanced Regression Techniques | Kaggle
  • 4.10. 实战Kaggle比赛:预测房价 — 动手学深度学习 2.0.0 documentation (d2l.ai)

1 数据处理

1.1 下载并导入数据集

从 Kaggle 官网将数据下载到本地,然后使用 pandas 的内置函数将数据保存到 DataFrame 中:

train_data = pd.read_csv("D:/Kaggle/house-prices-advanced-regression-techniques/train.csv")
test_data = pd.read_csv("D:/Kaggle/house-prices-advanced-regression-techniques/test.csv")

1.2 数据预处理

由于数据的特征之间在数量级和单位上可能有较大的差异,一种比较简单的处理方式是对所有数值类型的特征进行归一化

# 将训练集和测试集的特征进行拼接
all_features = pd.concat((train_data.iloc[:,1:-1], test_data.iloc[:,1:-1]))
# 使用DataFrame.dtypes会得到一个Series
# 使用Series.index就可以到我们想要的标签
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
# DataFrame.apply(self, func, axis=0, raw=False, result_type=None, args=(), **kwds)
# axis=0代表处理函数的每一列,raw=False表示把每一列作为Series传入func中
# lambda表达式相当于一个简单的匿名函数,冒号前是输入,冒号后的计算结果为输出
all_features[numeric_features] = all_features[numeric_features].apply(
    lambda x: (x-x.mean())/x.std())

在对所有数值型特征进行归一化处理后,就可以用 fillna(0) 来将所有 nan 用均值 0 0 0 代替:

all_features[numeric_features] = all_features[numeric_features].fillna(0)

然后用独热编码的方式处理非数值特征:

# dummy_na=True表示会对nan进行编码
# astype("float32")将所有值变为浮点类型,以便后续处理
all_features = pd.get_dummies(all_features, dummy_na=True).astype("float32")

1.3 构建训练集、测试集

# 获得训练集样本个数
n_train = train_data.shape[0]
train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float32)
# 这里默认了训练集的标签在训练数据的最后一列
train_labels = torch.tensor(train_data.iloc[:, -1].values.reshape(-1, 1), dtype=torch.float32)
test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float32)

2 训练&预测

这部分我们采用“自顶向下”的方式进行阐述。我们的目的是写出一个函数,它能根据训练集训练模型,并在测试集上进行预测,最后将预测写入文件中:

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')
    # 没有这句话图片可能不能正常显示
    d2l.plt.show()
    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)

观察上面的代码,共有两处需要我们实现,其一是获得模型 get_net() ,另一个是模型训练 train() 。首先看模型训练:

in_features = train_features.shape[1]
loss = nn.MSELoss()

def get_net():
    net = nn.Sequential(nn.Linear(in_features,1))
    return net

可以看出,这部分代码比较简单,我们只需要使用 nn.Sequential() 灵活设计神经网络即可。然后是模型训练:

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

上述代码中,又有两处需要我们实现,其一是用于获取小批量样本迭代器的 load_array() ,另一个是衡量误差的 log_rmse() 。首先看 load_array()

def load_array(data_arrays, batch_size, is_train=True):
    """构造一个PyTorch数据迭代器"""
    # TensorDataset相当于把所有tensor打包,传入的tensor的第0维必须相同
    # *的作用是“解压”参数列表
    dataset = data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset, batch_size, shuffle=is_train)

这部分在前面的文章已经实现过,故不在此过多解释。然后是 log_rmse()

def log_rmse(net, features, labels):
    # 为了在取对数时进一步稳定该值,将小于1的值设置为1
    # torch.clamp(input, min, max, out=None)
    # input是一个tensor,clamp()会将tensor所有小于min的值换成min,大于max的值换成max
    clipped_preds = torch.clamp(net(features), 1, float('inf'))
    rmse = torch.sqrt(loss(torch.log(clipped_preds),
                           torch.log(labels)))
    return rmse.item()

可以看出,在衡量预测结果,我们选用了对数均方误差
1 n ∑ i = 1 n ( log ⁡ y i − log ⁡ y ^ i ) 2 \sqrt{\frac1n\sum\limits_{i=1}^{n}\Big(\log y_i-\log\hat{y}_i\Big)^2} n1i=1n(logyilogy^i)2
这是因为,我们更关心相对误差 y − y ^ y \frac{y-\hat{y}}{y} yyy^ ,容易看出,对数均方误差中的 y i y_i yi y ^ \hat{y} y^ 如果扩大相同的倍数,结果将不会发生改变。

这里又产生一个问题,既然我们最终想得到一个相对误差较小的预测模型,那为什么我们在更新模型参数时,选择的损失函数是均方误差,而不是直接使用对数均方误差呢?

个人理解:因为最小化均方误差的本质是参数的极大似然估计,这是符合统计理论的。

为了训练出一个好的预测模型,我们需要选择合适的超参数,一种简单的方式是使用 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
    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

上面的代码中,需要我们实现划分训练集和验证集的 get_k_fold_data()

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_fold() 的输出来选择合适的超参数:

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}')

你可能感兴趣的:(动手学深度学习,深度学习,人工智能)