李沐 《动手学深度学习》 | 实战Kaggle比赛:预测房价

文章目录

房价预测比赛链接: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:]))

预处理样本

数值型特征处理
  1. 将数据中所有缺失的值替换为相应特征的平均值。
  2. 为了将所有特征放在一个共同的尺度上, 我们通过将特征重新缩放到零均值和单位方差来标准化数据 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 的所有对应元素。

  1. 对齐索引:首先 pandas 会根据索引名称对齐两个 Series
  2. 选择元素:然后只保留布尔 Series 中值为 True 的位置对应的元素
  3. 返回结果:返回一个新的 Series,包含所有被选中的元素
python 复制代码
# numeric_features包含所有数值型特征列名的列表
numeric_features = all_features.dtypes[all_features.dtypes != 'object'].index

all_features[numeric_features]是根据列名选择all_featuresDataFrame子集。

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. 方便优化

假设预测房价有两个特征,房间数(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
  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优化器(后续会讲),主要优势是对于初始学习率不那么敏感。

  1. 从train_iter中按batch_size取出数据集
  2. 将optimizer管理的所有参数的梯度归零,防止每一batch_size的梯度累加错误更新
  3. 前向传播与损失计算。训练中使用均方误差损失,这里y和net(X)的形状在之前的步骤已经统一
  4. 反向传播计算梯度,自动微分计算梯度。
  5. 使用优化器更新参数。

每轮结束后评估整个训练集的性能,将每轮结果放入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}')

代码总结

  1. 初始化一组超参数
  2. 进行 K K K折交叉验证,对于每一折 i i i过程执行如下的步骤
    1. 将训练集划分为k份,其中第i份作为验证集,其余k-1份作为训练集
    2. 初始化一个新的模型
    3. 使用当前超参数、训练集和验证集训练模型(训练该模型的参数)
    4. 累计训练完成的评估误差 - 训练集的评估误差与验证集的评估误差
  3. 返回折数的平均评估误差

通过不断调整超参数重复上述过程,选择出最优的超参数。

最终模型确认及结果预测

  1. 通过之前的步骤,我们已经选择出了一组超参数。现在使用这些超参数和全部的训练数据来训练最终模型。
  2. 将最终模型应用于测试集,将预测结果保存在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

相关推荐
BAOYUCompany9 分钟前
暴雨服务器成功交付长沙市第四医院
人工智能
文军的烹饪实验室10 分钟前
用于评估大语言模型(LLMs)能力的重要基准任务(Benchmark)
人工智能·语言模型·自然语言处理
平行绳17 分钟前
利用Coze智能体搞定小红书图文(下)
人工智能·coze
yzx99101324 分钟前
Python开发功能项目
服务器·开发语言·人工智能·python·深度学习
摘取一颗天上星️25 分钟前
外部记忆的组织艺术:集合、树、栈与队列的深度解析
深度学习·机器学习·外部记忆
测试者家园32 分钟前
接口测试不再难:智能体自动生成 Postman 集合
软件测试·人工智能·测试工具·postman·agent·智能化测试·测试开发和测试
tonydf33 分钟前
浅尝一下微软的AutoGen框架
人工智能·后端
柠檬味拥抱38 分钟前
面向大语言模型的MCP插件系统架构与能力协商机制研究
人工智能
Blossom.1181 小时前
基于深度学习的异常检测系统:原理、实现与应用
人工智能·深度学习·神经网络·目标检测·机器学习·scikit-learn·sklearn
VR最前沿1 小时前
Xsens动捕和Manus数据手套在元宇宙数字人制作中提供解决方案
大数据·人工智能·科技·机器人·自动化