参考资料:
- 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=1∑n(logyi−logy^i)2
这是因为,我们更关心相对误差
y
−
y
^
y
\frac{y-\hat{y}}{y}
yy−y^ ,容易看出,对数均方误差中的
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}')