文章目录
房价预测比赛链接:https://www.kaggle.com/c/house-prices-advanced-regression-techniques

1.下载和缓存数据集
我们建立字典DATA_HUB
, 它可以将数据集名称的字符串映射到数据集相关的二元组上, 这个二元组包含数据集的url和验证文件完整性的sha-1密钥。 所有类似的数据集都托管在地址为DATA_URL
的站点上(这里只托管了小一点的数据集,大的数据集还是需要去官网下载)。
python
import hashlib # Python的哈希库,用于文件校验(计算SHA1、MD5等哈希值)
import os # 操作系统接口库 用于处理文件路径、目录创建等操作
import tarfile # 导入tar文件处理库 用于解压缩.tar、.tar.gz、.tar.bz2等归档文件
import zipfile # 导入zip文件处理库 用于解压缩.zip格式的压缩文件
import requests # 导入HTTP请求库 提供简洁的API用于网络文件下载
DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'
一般情况需要自己去Kaggle将数据集下载到本地目录。这里使用一个download
函数用来下载数据集, 将数据集缓存在本地目录(默认情况下为../data
)中, 并返回下载文件的名称。 如果缓存目录中已经存在此数据集文件,并且其sha-1与存储在DATA_HUB
中的相匹配, 我们将使用缓存的文件,以避免重复的下载。
python
def download(name, cache_dir=os.path.join('..', 'data')):
"""下载一个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
竞赛数据分为训练集和测试集。 每条记录都包括房屋的属性和属性值,如街道类型、施工年份、屋顶类型、地下室状况等, 这些特征由各种数据类型组成。 例如,建筑年份由整数表示,屋顶类型由离散类别表示,其他特征由浮点数表示。 这就是现实让事情变得复杂的地方:例如,一些数据完全丢失了,缺失值被简单地标记为"NA"。
每套房子的价格只出现在训练集中,我们将划分训练集以创建验证集,但是在将预测结果上传到Kaggle之后, 我们只能在官方测试集中评估我们的模型。
训练集拆分 训练集+验证集 , 测试集用于评估模型。
方法1:Kaggle 比赛网站中下载数据
加入比赛后,可以在Data下载数据集。

方法2:代码远程下载
使用上面定义的脚本下载并缓存Kaggle房屋数据集。
python
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'] = (
DATA_URL + 'kaggle_house_pred_train.csv',
'585e9cc93e70b39160e7921475f9bcd7d31219ce')
DATA_HUB['kaggle_house_test'] = (
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'))

2.数据预处理
读取样本
训练数据集包括1460个样本,每个样本80个特征和1个标签, 而测试数据集包含1459个样本,每个样本80个特征。
python
# 使用pandas分别加载包含训练数据和测试数据的两个CSV文件。
train_data = pd.read_csv(download('kaggle_house_train'))
test_data = pd.read_csv(download('kaggle_house_test'))
print(train_data.shape) #(1460,81)
print(test_data.shape) #(1459,80)
查看一下前4个样本的前四个特征以及最后二个特征以及相应标签(房价)
python
# train_data 是一个Pandas DataFrame对象 通常包含表格数据(行是样本/记录,列是特征/字段)
# iloc 按位置索引选择数据的属性,第一个参数为行选择器,第二个参数为列选择器
print(train_data.iloc[0:4, [0, 1, 2, 3, -3, -2, -1]])

在每个样本中,第一个特征是ID, 它不携带任何用于预测的信息。 因此,在将数据提供给模型之前,我们将其从数据集中删除。
python
# train_data.iloc[:, 1:-1] 选择所有行,选取第二列和倒数第二列 排除训练集的第一列(通常是ID)和最后一列(通常是标签/目标变量)
# test_data.iloc[:, 1:] 测试集选取从第2列开始到最后一列的所有列
# 参数2接着参数1,将多个DataFrame连接成一个新的DataFrame
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
预处理样本
数值型特征处理
- 将数据中所有缺失的值替换为相应特征的平均值。
- 为了将所有特征放在一个共同的尺度上, 我们通过将特征重新缩放到零均值和单位方差来标准化数据 x 标准 = x − μ σ x_{标准}=\frac{x−μ}{σ} x标准=σx−μ。
其中 μ μ μ和 σ σ σ分别表示特征 x x x均值和标准差。 这些特征具有零均值和单位方差,即 E [ x − μ σ ] = μ − μ σ = 0 E[\frac{x−μ}{σ}] = \frac{μ-μ}{σ}=0 E[σx−μ]=σμ−μ=0和 E [ ( x − μ ) 2 ] = ( σ 2 + μ 2 ) − 2 μ 2 + μ 2 = σ 2 E[(x−μ)^2]=(σ^2+μ^2)−2μ^2+μ^2=σ^2 E[(x−μ)2]=(σ2+μ2)−2μ2+μ2=σ2。
all_features.dtypes
返回pandas Series
对象,包含索引index:列名 值values:对应的数据类型。
all_features.dtypes != 'object'
比较的存储在Series中的实际值(values),返回布尔Series

布尔Series机制
在 pandas 中,当您使用一个布尔 Series 来索引另一个 Series 时,pandas 会选择布尔 Series 中值为 True
的所有对应元素。
- 对齐索引:首先 pandas 会根据索引名称对齐两个 Series
- 选择元素:然后只保留布尔 Series 中值为
True
的位置对应的元素 - 返回结果:返回一个新的 Series,包含所有被选中的元素
python
# numeric_features包含所有数值型特征列名的列表
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index
all_features[numeric_features]
是根据列名选择all_features
的DataFrame
子集。
DataFrame.apply
会遍历每一列,每次取一列数据作为Series传入lambda函数进行标准化 x 标准 = x − μ σ x_{标准}=\frac{x−μ}{σ} x标准=σx−μ并收集每次lambda的返回值(这里使用了Pandas的广播机制),组合所有结果返回新的DataFrame
。
这里的均值是包含数据集和测试集的,实际情况不一定有测试集,可能只能在数据集上求均值。
python
# 所有数值特征的标准化
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / (x.std()))
numeric_features
选中数值列,但有些数值列的值为NaN,最后需要将数值特征中的所有缺失值替换为0,并返回新的DataFrame。这里替换为0因为标准化后所有特征的均值为0。
python
# 在标准化数据之后,所有均值消失,因此我们可以将缺失值设置为0
all_features[numeric_features] = all_features[numeric_features].fillna(0)
特征标准化的好处
- 方便优化
假设预测房价有两个特征,房间数(1-5)和房屋面积(50-300),对每个特征求梯度。发现面积方向的梯度是房间数方向梯度的几十倍!
大梯度需要小的学习率抑制梯度爆炸,而大的梯度需要大的学习率去加速收敛,没有一个合适的学习率可以兼容。

如果采取标准化,假设房间_std=(房间数-3)/1.5 ,面积_std =(面积-175)/125,新梯度之间差异不会太大
python
# 标准化变换:
房间_std = (房间数 - 3)/1.5 # 范围[-1.33,1.33]
面积_std = (面积 - 175)/125 # 范围[-1,1]
# 新梯度:
∂Loss/∂w_房间 = -2(y-ŷ)×房间_std ≈ -2(y-ŷ)×0 (平均)
∂Loss/∂w_面积 = -2(y-ŷ)×面积_std ≈ -2(y-ŷ)×0 (平均)
→ 梯度大小比例恢复1:1
- 正则化惩罚公平
正则项 λ 2 ∣ ∣ w ∣ ∣ 2 \frac{\lambda}{2}||w||^2 2λ∣∣w∣∣2惩罚 权重的平方和,这里超参数 λ \lambda λ控制了正则项的重要程度, λ \lambda λ设置的越大,对大的权重施加的惩罚就越重,没办法选出一个合适的值。
离散值处理
MSZoning的类型是一个object之类的离散值,使用one-hot编码处理。假设这个特征(这一列)有5个不一样的值,那我们就使用5个分量来表示。
pd.get_dummies
会处理所有字符串类型(object)或分类类型(category)的列,将其转换为可用的数值格式(one-hot编码),在DataFrame结构中以多列方式呈现,生成新的列 列原列名_值
,返回新的DataFrame。

参数dummy_na=True
表示将NaN视为一个独立有效的类别,因为默认情况会忽略缺失值。
python
# "Dummy_na=True"将"na"(缺失值)视为有效的特征值,并为其创建指示符特征
# 处理字符串类型或分类类型
all_features = pd.get_dummies(all_features,dummy_na=True)
print(all_features.shape) #(2919,330)
可以发现此转换之后,特征的总数量从79个增加到330个。
这里原始的列被剔除了!
python
print(all_features.select_dtypes(include='object').columns)
# Index(['MSZoning', 'Street', 'Alley', 'LotShape', 'LandContour', 'Utilities',
# 'LotConfig', 'LandSlope', 'Neighborhood', 'Condition1', 'Condition2',
# 'BldgType', 'HouseStyle', 'RoofStyle', 'RoofMatl', 'Exterior1st',
# 'Exterior2nd', 'MasVnrType', 'ExterQual', 'ExterCond', 'Foundation',
# 'BsmtQual', 'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2',
# 'Heating', 'HeatingQC', 'CentralAir', 'Electrical', 'KitchenQual',
# 'Functional', 'FireplaceQu', 'GarageType', 'GarageFinish', 'GarageQual',
# 'GarageCond', 'PavedDrive', 'PoolQC', 'Fence', 'MiscFeature',
# 'SaleType', 'SaleCondition'],
# dtype='object')
all_features = pd.get_dummies(all_features,dummy_na=True,dtype=np.uint8)
print(all_features.select_dtypes(include='object').columns)
# Index([], dtype='object')
大模型的解释:


**问题:**can't convert np.ndarray of type numpy.object_. The only supported types are: float64, float32, float16, complex64, complex128, int64, int32, int16, int8, uint64, uint32, uint16, uint8, and bool
尝试将一个包含 numpy.object_
** **类型数据 的 NumPy 数组转换为其他类型(如 PyTorch Tensor 或特定数值类型),但操作仅支持特定数据类型。
评论解答
get_dummies函数在pandas1.6.0版本之前返回numpy.uint8,无符号八位整数,在1.6.0版本开始更改为返回numpy.bool_,numpy布尔值。
python
# "Dummy_na=True"将"na"(缺失值)视为有效的特征值,并为其创建指示符特征
# 处理字符串类型或分类类型 只控制新的列的类型
all_features = pd.get_dummies(all_features,dummy_na=True,dtype=int)
print(all_features.shape) #(2919,310)
这里确实可以解决问题,但是我测试了之后发现布尔类型是不会报错的??

转换为张量表示
从pandas
格式中提取NumPy格式,并将其转换为张量表示用于训练。
all_features[:n_train]
表示前n_train行数据,只包含数据行,不包含列名,.values
表示返回NumPy数组。torch,tensor
表示将NumPy数组转换为PyTorch
张量,并且指定格式为32位浮点数
train_data.SalePrice.values
通过列名SalePrice获取到该列的值,输出是一个一维数组,形状为一维数组(n_train,)。
将其重塑数组二维数组形状为(n_train, 1),最后转换为PyTorch
张量。
python
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)
训练
损失函数使用均方误差损失 1 n ∑ i = 1 n ( y i − y ^ i ) 2 \frac{1}{n} \sum_{i=1}^n(y_i−\hat y_i)^2 n1∑i=1n(yi−y^i)2
python
loss = nn.MSELoss()
我们训练一个带有损失平方的线性模型。 显然线性模型很难让我们在竞赛中获胜,但线性模型提供了一种健全性检查, 以查看数据中是否存在有意义的信息。
如果我们在这里不能做得比随机猜测更好,那么我们很可能存在数据处理错误。 如果一切顺利,线性模型将作为基线(baseline)模型, 让我们直观地知道最好的模型有超出简单的模型多少。
python
in_features = train_features.shape[1] # 特征数量
def get_net():
net = nn.Sequential(nn.Linear(in_features,1))
return net
对于真实值减去误差值来说,我们更关心相对误差 y − y ^ y \frac{y-\hat y}{y} yy−y^,对于房价来说不同的房子价格相差较大,有10w的有100w的。例如,如果我们在俄亥俄州农村地区估计一栋房子的价格时, 假设我们的预测偏差了10万美元, 然而那里一栋典型的房子的价值是12.5万美元, 那么模型可能做得很糟糕。 另一方面,如果我们在加州豪宅区的预测出现同样的10万美元的偏差, (在那里,房价中位数超过400万美元) 这可能是一个不错的预测。
对数变换(log transformation)这种方法被广泛应用于Kaggle等数据科学竞赛中。我们评估模型时使用 1 n ∑ i = 1 n ( l o g ( y i ) − l o g ( y ^ i ) ) 2 \sqrt {\frac{1}{n}\sum_{i=1}^n (log(y_i)−log(\hat y_i))^2} n1∑i=1n(log(yi)−log(y^i))2 等价于 1 n ∑ i = 1 n ( l o g y i y ^ i ) 2 \sqrt {\frac{1}{n}\sum_{i=1}^n (log \frac {y_i}{\hat y_i})^2} n1∑i=1n(logy^iyi)2 。这个评估值越小越好。
torch.clamp(input, min, max, *, out=None) -> Tensor
将张量中的元素限制在指定范围,min表示下限值,max表示上限值。这里将最低值限制为1,防止出现负数和为0的情况影响对数的取值。
python
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() # 从张量中提取python标量值
这里训练使用Adam优化器(后续会讲),主要优势是对于初始学习率不那么敏感。
- 从train_iter中按batch_size取出数据集
- 将optimizer管理的所有参数的梯度归零,防止每一batch_size的梯度累加错误更新
- 前向传播与损失计算。训练中使用均方误差损失,这里y和net(X)的形状在之前的步骤已经统一
- 反向传播计算梯度,自动微分计算梯度。
- 使用优化器更新参数。
每轮结束后评估整个训练集的性能,将每轮结果放入train_ls
中,如果提供了测试标签test_labels
,评估模型在测试集上的表现。
python
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:# 每次返回一个(X_batch, y_batch)元组
optimizer.zero_grad()
l = loss(net(X), y) # 输出形状 net(X)=[batch_size, 1]
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折交叉验证来帮助模型选和超参数调整。
我们首先需要定义一个函数,在 K K K折交叉验证过程中返回第 i i i折的数据。 具体地说,它选择第 i i i个切片作为验证数据,其余部分作为训练数据。
注意,这并不是处理数据的最有效方法,如果我们的数据集大得多,会有其他解决办法。
返回训练和验证误差的平均值
python
# k总折数,i当前折数索引
def get_k_fold_data(k, i, X, y):
assert k > 1 # 如果 k≤1,抛出 AssertionError
#计算每折大小,整除会舍弃余数,少量样本不被包含在任何折中
fold_size = X.shape[0] // k
X_train, y_train = None, None
for j in range(k):
# slice(start, stop, step):slice对象是用于表示切片操作的特殊对象
# 以下代码相当于创建了一个切片对象:
# idx = [j * fold_size : (j + 1) * fold_size]
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: # 将其余训练集拼接在一起
# torch.cat():沿指定维度连接张量,0表示垂直堆叠
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份后,每次使用第i份作为验证集,其余作为训练集。
下面的代码独立训练K次模型,每一次都创建一个全新初始化的模型,每次都有新的训练数据与验证数据。复用模型会使用上一次训练后参数信息,
交叉验证目标是评估最终模型性能,每一次训练模型都取模型最后一轮的评估误差。
python
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()
# 解包为X_train, y_train, X_valid, y_valid
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]
# K折的学习曲线通常相似,所以这里选择只绘制第一折
if i == 0:
d2l.plot(
list(range(1, num_epochs + 1)), # X轴:训练轮次[1, num_epochs]
[train_ls, valid_ls], # Y轴数据:[训练误差列表, 验证误差列表]
xlabel='epoch', # X轴标签
ylabel='rmse', # Y轴标签(均方根误差)
xlim=[1, num_epochs], # X轴显示范围
legend=['train', 'valid'], # 图例说明
yscale='log' # Y轴使用对数刻度
)
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折交叉验证的误差要高得多, 这表明模型过拟合了。 在整个训练过程中,我们希望监控训练误差和验证误差这两个数字。
python
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}')
代码总结
- 初始化一组超参数
- 进行 K K K折交叉验证,对于每一折 i i i过程执行如下的步骤
- 将训练集划分为k份,其中第i份作为验证集,其余k-1份作为训练集
- 初始化一个新的模型
- 使用当前超参数、训练集和验证集训练模型(训练该模型的参数)
- 累计训练完成的评估误差 - 训练集的评估误差与验证集的评估误差
- 返回折数的平均评估误差
通过不断调整超参数重复上述过程,选择出最优的超参数。

最终模型确认及结果预测
- 通过之前的步骤,我们已经选择出了一组超参数。现在使用这些超参数和全部的训练数据来训练最终模型。
- 将最终模型应用于测试集,将预测结果保存在CSV文件中。
在预测阶段不需要知道梯度信息了,更关心内存的优化,所以通常会将预测结果从计算图中分离datach
出来创建一个新的不含梯度信息的张量。然后将PyTorch张量转换为NumPy数组。
将模型预测结果转换为Pandas DataFrame中的格式,preds.reshape(1, -1)
表示将数组转换为形状为(1,N)
的二维数组,取出二维数组中的唯一元素(一维数组)转换为Pandas Series对象。
python
def train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size):
# 1. 初始化模型
net = get_net() #
# 2. 完整训练(不使用验证集)
train_ls, _ = train(net, train_features, train_labels, None, None,
num_epochs, lr, weight_decay, batch_size)
# 3. 可视化训练过程
d2l.plot(np.arange(1, num_epochs + 1), [train_ls], xlabel='epoch',
ylabel='log rmse', xlim=[1, num_epochs], yscale='log')
# 4. 打印最终训练误差
print(f'训练log rmse:{float(train_ls[-1]):f}')
# 5. 测试集预测
preds = net(test_features).detach().numpy()
# 6. 格式化预测结果
test_data['SalePrice'] = pd.Series(preds.reshape(1, -1)[0])
Kaggle提交规范
1.文件命名:通常要求固定文件名。
2.首列必须为id,次列为预测值列,列名大小写敏感。
python
# 7. 创建提交文件
# 将测试集id和模型预测的房价水平拼接形成新的DataFrame
# Id SalePrice
# 1461 181000
# 1462 179500
# 将DataFrame转换为CSV格式(Kaggle标准提交格式)
submission = pd.concat([test_data['Id'], test_data['SalePrice']], axis=1)
submission.to_csv(
'submission.csv', # 输出的文件名
index=False # 禁止写入行索引
)
如果测试集上的预测与 K K K倍交叉验证过程中的预测相似, 那就是时候把它们上传到Kaggle了。 下面的代码将生成一个名为submission.csv
的文件。
python
train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size)

代码总结
python
import hashlib # Python的哈希库,用于文件校验(计算SHA1、MD5等哈希值)
import os # 操作系统接口库 用于处理文件路径、目录创建等操作
import tarfile # 导入tar文件处理库 用于解压缩.tar、.tar.gz、.tar.bz2等归档文件
import zipfile # 导入zip文件处理库 用于解压缩.zip格式的压缩文件
import requests # 导入HTTP请求库 提供简洁的API用于网络文件下载
import numpy as np
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2l
DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'
def download(name, cache_dir=os.path.join('..', 'data')):
"""下载一个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
# 数据获取
DATA_HUB['kaggle_house_train'] = (
DATA_URL + 'kaggle_house_pred_train.csv',
'585e9cc93e70b39160e7921475f9bcd7d31219ce')
DATA_HUB['kaggle_house_test'] = (
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'))
# 数据处理
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))
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()))
all_features[numeric_features] = all_features[numeric_features].fillna(0)
all_features = pd.get_dummies(all_features,dummy_na=True,dtype=np.uint8)
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)
# 模型的训练
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() # 从张量中提取python标量值
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:# 每次返回一个(X_batch, y_batch)元组
optimizer.zero_grad()
l = loss(net(X), y) # 输出形状 net(X)=[batch_size, 1]
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折交叉验证
# 获取训练集和数据集
def get_k_fold_data(k, i, X, y):
assert k > 1 # 如果 k≤1,抛出 AssertionError
#计算每折大小,整除会舍弃余数,少量样本不被包含在任何折中
fold_size = X.shape[0] // k
X_train, y_train = None, None
for j in range(k):
# slice(start, stop, step):slice对象是用于表示切片操作的特殊对象
# 以下代码相当于创建了一个切片对象:
# idx = [j * fold_size : (j + 1) * fold_size]
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: # 将其余训练集拼接在一起
# torch.cat():沿指定维度连接张量,0表示垂直堆叠
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
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()
# 解包为X_train, y_train, X_valid, y_valid
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]
# K折的学习曲线通常相似,所以这里选择只绘制第一折
if i == 0:
d2l.plot(
list(range(1, num_epochs + 1)), # X轴:训练轮次[1, num_epochs]
[train_ls, valid_ls], # Y轴数据:[训练误差列表, 验证误差列表]
xlabel='epoch', # X轴标签
ylabel='rmse', # Y轴标签(均方根误差)
xlim=[1, num_epochs], # X轴显示范围
legend=['train', 'valid'], # 图例说明
yscale='log' # Y轴使用对数刻度
)
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, 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}')
def train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size):
# 1. 初始化模型
net = get_net() #
# 2. 完整训练(不使用验证集)
train_ls, _ = train(net, train_features, train_labels, None, None,
num_epochs, lr, weight_decay, batch_size)
# 3. 可视化训练过程
d2l.plot(np.arange(1, num_epochs + 1), [train_ls], xlabel='epoch',
ylabel='log rmse', xlim=[1, num_epochs], yscale='log')
# 4. 打印最终训练误差
print(f'训练log rmse:{float(train_ls[-1]):f}')
# 5. 测试集预测
preds = net(test_features).detach().numpy()
# 6. 格式化预测结果
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 # 禁止写入行索引
)
train_and_pred(train_features, test_features, train_labels, test_data,
num_epochs, lr, weight_decay, batch_size)
提交到Kaggle

