获取和读取数据集
比赛数据分为训练数据集和测试数据集。两个数据集的特征值有连续的数字、离散的标签甚至是缺失值“na”。只有训练数据集包括了每栋房子的价格,也就是标签。
通过pandas
库读入并处理数据。
from torch import nn
from torch.nn import init
import torch
import numpy as np
import sys
import torchvision
from torch.utils import data
from torchvision import transforms
import pandas as pd
import matplotlib.pyplot as plt
from IPython import display
torch.set_default_tensor_type(torch.FloatTensor) # 设置默认tensor类型
使用pandas
读取这两个文件。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
bashPath = r".\house-price"
testPath = bashPath + "/test.csv"
trainPath = bashPath + "/train.csv"
# 读取数据
train_data = pd.read_csv(trainPath)
test_data = pd.read_csv(testPath)
样本的第一个特征是Id,不使用它来训练。将所有的训练数据和测试数据的79个特征按样本连结。
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
预处理数据
对连续数值的特征做标准化(standardization):设该特征在整个数据集上的均值为$\mu$,标准差为$\sigma$。那么将该特征的每个值先减去$\mu$再除以$\sigma$得到标准化后的每个特征值。对于缺失的特征值,将其替换成该特征的均值,即为0。
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index # 取出数值特征的索引
all_features[numeric_features] = all_features[numeric_features].apply( # apply函数对每个特征进行操作
lambda x: (x - x.mean()) / (x.std())) # x.std()表示标准差,x.mean()表示均值,x表示每个特征的值
all_features[numeric_features] = all_features[numeric_features].fillna(0) # 标准化后,每个数值特征的均值变为0,所以可以直接用0来替换缺失值
接下来将离散数值转成指示特征。举个例子,假设特征MSZoning里面有两个不同的离散值RL和RM,那么这一步转换将去掉MSZoning特征,并新加两个特征MSZoning_RL和MSZoning_RM,其值为0或1。如果一个样本原来在MSZoning里的值为RL,那么有MSZoning_RL=1且MSZoning_RM=0。
# dummy_na=True将缺失值也当作合法的特征值并为其创建指示特征
all_features = pd.get_dummies(all_features, dummy_na=True) # one-hot编码,get_dummies函数将类别特征转换为指示特征
# 将True和False转换为1和0
all_features = all_features.astype(np.float32)
print(all_features.shape) # (2919, 331)
可以看到这一步转换将特征数从79增加到了331。
最后,通过values
属性得到NumPy格式的数据,并转成Tensor
,放在GPU上。
n_train = train_data.shape[0] # 训练数据集的行数
train_features = torch.tensor(all_features[:n_train].values, dtype=torch.float, device=device)
test_features = torch.tensor(all_features[n_train:].values, dtype=torch.float, device=device)
train_labels = torch.tensor(train_data.SalePrice.values, dtype=torch.float, device=device).view(-1, 1)# view函数将行向量转换为列向量
训练模型
使用一个基本的线性回归模型和平方损失函数来训练模型。
loss = torch.nn.MSELoss()
def get_net():
num_inputs = train_features.shape[1]
hidden_units_1 = 81
num_outputs = 1
net = nn.Sequential(
nn.Linear(num_inputs, num_outputs)
)
# net = nn.Sequential(
# nn.Linear(num_inputs, hidden_units_1),
# nn.ReLU(),
# nn.Linear(hidden_units_1, num_outputs)
# )
# 初始化模型参数
for params in net.parameters(): # 初始化模型参数
init.normal_(params, mean=0, std=0.01) # 正态分布初始化
net = net.float() # 将模型的参数转换为float型
net.cuda() # 将模型加载到cuda上
return net
下面定义比赛用来评价模型的对数均方根误差。给定预测值$\hat y_1, \ldots, \hat y_n$和对应的真实标签$y_1,\ldots, y_n$,它的定义为
$$
\sqrt{\frac{1}{n}\sum_{i=1}^n\left(\log(y_i)-\log(\hat y_i)\right)^2}.
$$
对数均方根误差的实现如下。
def log_rmse(net, features, labels):
with torch.no_grad():
# 将小于1的值设成1,使得取对数时数值更稳定
clipped_preds = torch.max(net(features), torch.tensor(1.0))
rmse = torch.sqrt(loss(clipped_preds.log(), labels.log()))
return rmse.item()
下面的训练函数跟本章中前几节的不同在于使用了Adam优化算法。相对之前使用的小批量随机梯度下降,它对学习率相对不那么敏感。
def train(net, train_features, train_labels, test_features, test_labels, num_epochs, learning_rate, weight_decay, batch_size):
train_ls, test_ls = [], []
dataset = data.TensorDataset(train_features, train_labels)
train_iter = data.DataLoader(dataset, batch_size, shuffle=True)
optimizer = torch.optim.Adam(net.parameters(), lr = lr, weight_decay = weight_decay)
for epoch in range(1, num_epochs + 1):
for X, y in train_iter: # X是特征,y是标签
output = net(X) # 前向传播
l = loss(output, y) # 计算损失
optimizer.zero_grad() # 梯度清零,等价于net.zero_grad()
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
$K$折交叉验证
$K$折交叉验证被用来选择模型设计并调节超参数。下面实现了一个函数,它返回第i
折交叉验证时所需要的训练和验证数据。
def get_k_fold_data(k, i, X, y):
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) # slice(start, end, step)切片函数
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), dim=0)
y_train = torch.cat((y_train, y_part), dim=0)
return X_train, y_train, X_valid, y_valid
在$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
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:
semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'rmse', range(1, num_epochs + 1), valid_ls, ['train', 'valid'])
print('fold %d, train rmse %f, valid rmse %f' % (i, train_ls[-1], valid_ls[-1]))
return train_l_sum / k, valid_l_sum / k
输出:
fold 0, train rmse 0.132562, valid rmse 0.143022
fold 1, train rmse 0.126497, valid rmse 0.150703
fold 2, train rmse 0.127167, valid rmse 0.151402
fold 3, train rmse 0.131500, valid rmse 0.130405
fold 4, train rmse 0.123478, valid rmse 0.159710
(0.12824077159166336, 0.1470484495162964)
模型选择
使用一组未经调优的超参数并计算交叉验证误差。改动这些超参数来尽可能减小平均测试误差。
k = 5
lr = 5
weight_decay = 0.05 # 权重衰减参数
num_epochs = 500
batch_size = 64
train_l, valid_l = k_fold(k, train_features, train_labels, num_epochs, lr, weight_decay, batch_size)
print((train_l, valid_l))
预测并在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)
semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'rmse')
print('train rmse %f' % train_ls[-1])
preds = net(test_features).cpu().detach().numpy() # 将预测值转换为numpy数组,detach函数将tensor从计算图中分禁,cpu函数将tensor转移到内存
test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0]) # 将预测值填充到test_data的SalePrice列
submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1) # 将test_data的Id和SalePrice列拼接起来
submission.to_csv(bashPath+'/submission.csv', index=False)
设计好模型并调好超参数之后,下一步就是对测试数据集上的房屋样本做价格预测。
train_and_pred(train_features, test_features, train_labels, test_data, num_epochs, lr, weight_decay, batch_size)
输出:
train rmse 0.129388
Comments | NOTHING