AI

深度学习实战之房价预测

动手深度学习

Posted by LXG on March 18, 2026

动手深度学习-预测房价

下载和缓存数据集



import hashlib
import os
import tarfile
import zipfile
import requests

# 数据集元信息字典:键是数据集名,值是 (下载链接, 文件sha1校验值)
#@save
DATA_HUB = dict()
# 数据集默认下载地址前缀
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'

def download(name, cache_dir=os.path.join('..', 'data')):
    """Download a file inserted into DATA_HUB, return the local filename."""
    # 1) 先确认请求的数据集名称已在 DATA_HUB 中注册
    assert name in DATA_HUB, f"{name} does not exist in {DATA_HUB}."

    # 2) 取出该数据集的下载链接和对应的 sha1 摘要(用于完整性校验)
    url, sha1_hash = DATA_HUB[name]

    # 3) 确保缓存目录存在;如果不存在则自动创建
    os.makedirs(cache_dir, exist_ok=True)

    # 4) 根据 URL 的最后一段生成本地文件名
    fname = os.path.join(cache_dir, url.split('/')[-1])

    # 5) 若本地已存在同名文件,先做 sha1 校验,避免重复下载
    if os.path.exists(fname):
        sha1 = hashlib.sha1()
        with open(fname, 'rb') as f:
            while True:
                # 每次读取 1MB,避免一次性读入大文件造成内存压力
                data = f.read(1048576)
                if not data:
                    break
                sha1.update(data)
        # 校验通过则直接返回本地路径
        if sha1.hexdigest() == sha1_hash:
            return fname

    # 6) 本地无文件或校验失败时,重新下载
    print(f'Downloading {url} to {fname}...')
    # stream=True 表示启用流式响应;这里仍通过 r.content 一次性写入
    r = requests.get(url, stream=True)
    with open(fname, 'wb') as f:
        f.write(r.content)

    # 7) 返回下载到本地后的文件路径
    return fname

def download_extract(name, folder=None):
    """Download a file inserted into DATA_HUB, extract it, and return the
    extracted folder name."""
    # 1) 先确保压缩包已下载到本地(若已缓存且校验通过则直接复用)
    fname = download(name)

    # 2) base_dir:压缩包所在目录;data_dir:去掉扩展名后的路径
    # 例如 ../data/train.zip -> base_dir=../data, data_dir=../data/train
    base_dir = os.path.dirname(fname)
    data_dir, ext = os.path.splitext(fname)

    # 3) 根据扩展名选择解压方式
    if ext == '.zip':
        fp = zipfile.ZipFile(fname, 'r')
    elif ext in ('.tar', '.gz'):
        fp = tarfile.open(fname, 'r')
    else:
        # 非 zip/tar/gz 文件不支持解压
        assert False, 'Only zip/tar files can be extracted.'

    # 4) 解压到压缩包所在目录
    fp.extractall(base_dir)

    # 5) 返回解压后的目录路径:
    #    - 若显式传入 folder,则返回 base_dir/folder
    #    - 否则默认返回去掉扩展名后的路径 data_dir
    return os.path.join(base_dir, folder) if folder else data_dir

def download_all():
    """Download all files in the DATA_HUB."""
    # 依次下载 DATA_HUB 里注册的所有数据集
    for name in DATA_HUB:
        download(name)

Kaggle

Kaggle 是一个全球知名的数据科学与机器学习平台,被 Google 收购并运营。它主要为数据科学家、AI工程师、学生提供:

  • 📊 数据集
  • 🏆 数据竞赛
  • 📓 在线编程环境(Notebook)
  • 📚 学习课程
  • 🤝 社区交流

访问和读取数据集


import numpy as np
import torch
import pandas as pd
from torch import nn
from d2l import torch as d2l

DATA_HUB['kaggle_house_train'] = (
    DATA_URL + 'kaggle_house_pred_train.csv', '585e9cc93e70b39160e7921475cb0a887b1ce1')
DATA_HUB['kaggle_house_test'] = (
    DATA_URL + 'kaggle_house_pred_test.csv', 'fa19780a7b011eabfdbf1c71eb2a93e9d8b39')

# 下载训练和测试数据集
# pd : pandas 的常用别名,提供了强大的数据处理和分析功能
train_data = pd.read_csv(download('kaggle_house_train'))
test_data = pd.read_csv(download('kaggle_house_test'))
print(train_data.shape)
print(test_data.shape)
print('训练数据集第一条数据:')
print(train_data.iloc[0])
print('测试数据集第一条数据:')
print(test_data.iloc[0])

运行结果


(1460, 81)
(1459, 80)
训练数据集第一条数据:
Id                    1
MSSubClass           60
MSZoning             RL
LotFrontage        65.0
LotArea            8450
                  ...  
MoSold                2
YrSold             2008
SaleType             WD
SaleCondition    Normal
SalePrice        208500
Name: 0, Length: 81, dtype: object

测试数据集第一条数据:
Id                 1461
MSSubClass           20
MSZoning             RH
LotFrontage        80.0
LotArea           11622
                  ...  
MiscVal               0
MoSold                6
YrSold             2010
SaleType             WD
SaleCondition    Normal
Name: 0, Length: 80, dtype: object

数据预处理

下载 Kaggle 房价数据 → 做特征工程 → 转成 PyTorch 可训练数据


# -------------------- 数据预处理 --------------------
# 1) 拼接训练集和测试集的特征列(去掉 Id 和训练集标签 SalePrice)
all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))

# 2) 对数值特征做标准化:(x - mean) / std
#    object 类型通常是类别特征,不在这一步处理
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()
)

# 3) 标准化后数值列仍可能有缺失值,统一填充为 0
all_features[numeric_features] = all_features[numeric_features].fillna(0)

# 4) 对类别特征做独热编码,dummy_na=True 会把缺失值也当作一个类别
#    dtype=np.float32 便于后续直接转为 torch.float32
all_features = pd.get_dummies(all_features, dummy_na=True, dtype=np.float32)

# 5) 按训练集行数切分回训练特征和测试特征,并转换为张量
n_train = train_data.shape[0]
train_features = torch.tensor(all_features[:n_train].to_numpy(), dtype=torch.float32)
test_features = torch.tensor(all_features[n_train:].to_numpy(), dtype=torch.float32)

# 6) 提取训练标签并转成二维张量,形状为 (样本数, 1)
train_labels = torch.tensor(
    train_data['SalePrice'].to_numpy().reshape(-1, 1),
    dtype=torch.float32
)

print('预处理后训练特征形状:', train_features.shape)
print('预处理后测试特征形状:', test_features.shape)
print('训练标签形状:', train_labels.shape)

运行结果


训练集shape : (1460, 81)
测试集shape : (1459, 80)

预处理后训练特征形状: torch.Size([1460, 330])
预处理后测试特征形状: torch.Size([1459, 330])
训练标签形状: torch.Size([1460, 1])

原始数据

数据 行数 列数
训练集 1460 81
测试集 1459 80

为什么 train 比 test 多 1 列?

  • train 有标签:SalePrice
  • test 没有标签

实际结构


train(81列):

Id + 79个特征 + SalePrice

test(80列):

Id + 79个特征

特征拼接


all_features = pd.concat((train_data.iloc[:, 1:-1], test_data.iloc[:, 1:]))

操作 含义
去掉第0列 去掉 Id
去掉最后一列 去掉 SalePrice

结果


train → 79列特征
test  → 79列特征

1460 + 1459 = 2919 行
79 列

最关键问题:为什么变成 330 维?

核心原因:One-Hot 编码爆炸

举个真实例子


Neighborhood(社区)

NAmes, CollgCr, OldTown, Edwards, ...

假设有 25 种.

One-Hot 后:25列

字段 类别数
Neighborhood 25
Exterior1st 15
RoofStyle 6
GarageType 6

独热编码后: 79列 → 330列

完整过程


原始特征:79列
        ↓
数值特征:保持不变(比如 40列)
类别特征:One-Hot(比如 39列 → 290列)
        ↓
总特征:≈ 330列

标签 shape 为什么是 (1460, 1)


原始标签: [500, 300, 400, ...]   # 一维

reshape 后: 

[[500],
 [300],
 [400]]
 

大模型预处理爆炸

维度 Kaggle表格 大模型
数据类型 表格 文本/代码
数据量 MB级 TB~PB级
预处理复杂度 ⭐⭐ ⭐⭐⭐⭐⭐
核心操作 One-Hot Tokenization
清洗难度 极高
自动化程度 复杂

👉 在很多公司:

  • ❌ 写模型的人:10%
  • ✅ 做数据的人:60%

记不住这么多API怎么办

记住模板


# 1. 读数据
train = pd.read_csv(...)

# 2. 拼接
all = pd.concat(...)

# 3. 数值处理
all[numeric] = (all[numeric] - mean) / std

# 4. 填NaN
all = all.fillna(0)

# 5. One-hot
all = pd.get_dummies(all)

# 6. 转Tensor
torch.tensor(...)

训练


# 定义训练使用的损失函数:均方误差(回归任务常用)
loss = nn.MSELoss()
# 输入特征维度,后续用于定义线性层输入大小
in_features = train_features.shape[1]

def get_net():
    # 这里使用最简单的线性回归模型:y = Wx + b
    # nn.Sequential 便于后续需要时扩展更多层
    net = nn.Sequential(
        nn.Linear(in_features, 1)
    )
    return net

def log_rmse(net, features, labels):
    # 评估指标:对数均方根误差(Kaggle 房价常用)
    # 只做前向计算,不需要梯度,使用 no_grad 可减少显存/内存开销
    with torch.no_grad():
        # 将预测值限制在 1 以上,避免对数函数输入无效
        clipped_preds = torch.clamp(net(features), 1, float('inf'))
        # RMSE = sqrt(MSE(log(pred), log(label)))
        rmse = torch.sqrt(loss(torch.log(clipped_preds), torch.log(labels)))
    # 返回 Python 标量,便于记录与打印
    return rmse.item()

def train(net, train_features, train_labels, test_features, test_labels,
          num_epochs, learning_rate, weight_decay, batch_size):
    # train_ls: 每个 epoch 的训练集 log_rmse
    # test_ls: 每个 epoch 的验证/测试集 log_rmse
    train_ls, test_ls = [], []

    # 将特征和标签打包成 TensorDataset,供 DataLoader 按批次读取
    dataset = torch.utils.data.TensorDataset(train_features, train_labels)
    # shuffle=True: 每轮训练前打乱样本顺序,有助于随机梯度下降效果
    train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True)

    # 优化器使用 Adam:对学习率较鲁棒,通常收敛更快
    # weight_decay 相当于 L2 正则化系数,用于抑制过拟合
    optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate,
                                 weight_decay=weight_decay)

    # 外层循环:epoch(完整遍历训练集一次)
    for epoch in range(num_epochs):
        # 内层循环:mini-batch 训练
        for X, y in train_iter:
            # 清空上一批次的梯度,避免梯度累加
            optimizer.zero_grad()
            # 前向传播:计算当前批次预测与损失
            l = loss(net(X), y)
            # 反向传播:自动计算各参数梯度
            l.backward()
            # 参数更新:按优化器规则更新网络权重
            optimizer.step()

        # 每个 epoch 结束后记录一次整体训练误差
        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

模型公式


y = w1*x1 + w2*x2 + ... + wn*xn + b

输入:330维
输出:1个房价

含义
in_features=330 输入特征
1 输出一个数(房价)
参数 W(330个) + b(1个)

损失函数


loss = nn.MSELoss()

预测:400万
真实:500万
误差:(400-500)^2 = 10000

K 折交叉验证


# K 折交叉验证:将训练数据划分为 k 份,轮流使用其中一份作为验证集,其余 k-1 份作为训练集
def get_k_fold_data(k, i, X, y):
    # 1) 计算每折的样本数
    fold_size = X.shape[0] // k

    # 2) 划分验证集:第 i 折
    X_valid = X[i * fold_size: (i + 1) * fold_size]
    y_valid = y[i * fold_size: (i + 1) * fold_size]

    # 3) 划分训练集:剩余 k-1 折
    X_train = torch.cat([X[:i * fold_size], X[(i + 1) * fold_size:]], dim=0)
    y_train = torch.cat([y[:i * fold_size], y[(i + 1) * fold_size:]], dim=0)

    return X_train, y_train, X_valid, y_valid

def k_fold(k, X, y, num_epochs, learning_rate, weight_decay, batch_size):
    # 记录每折的训练误差和验证误差
    train_l_sum, valid_l_sum = 0.0, 0.0

    # 依次进行 k 折交叉验证
    for i in range(k):
        # 获取第 i 折的训练集和验证集
        X_train, y_train, X_valid, y_valid = get_k_fold_data(k, i, X, y)

        # 定义新的模型实例,确保每折使用独立模型
        net = get_net()

        # 训练当前折的模型,并获取训练曲线
        train_ls, valid_ls = train(net, X_train, y_train, X_valid, y_valid,
                                   num_epochs, learning_rate, weight_decay,
                                   batch_size)

        # 累加当前折的最终训练误差和验证误差
        train_l_sum += train_ls[-1]
        valid_l_sum += valid_ls[-1]

        print(f'折 {i + 1}, 训练 log rmse {train_ls[-1]:f}, '
              f'验证 log rmse {valid_ls[-1]:f}')

    # 返回平均训练误差和平均验证误差
    return train_l_sum / k, valid_l_sum / k

把训练数据轮流当“考试题”,看看模型是不是只会背答案

模型选择


k, num_epochs, learning_rate, weight_decay, batch_size = 5, 100, 5, 0, 64
# 先进行 K 折交叉验证,用于评估当前超参数配置
train_l, valid_l, train_curve, valid_curve = k_fold(
    k, train_features, train_labels, num_epochs,
    learning_rate, weight_decay, batch_size
)
print(f'{k}-折交叉验证: 平均训练 log rmse: {train_l:f}, 平均验证 log rmse: {valid_l:f}')

# 将曲线图保存到当前脚本同目录下
current_dir = os.path.dirname(os.path.abspath(__file__))
curve_path = os.path.join(current_dir, 'kfold_training_curve.png')
plot_training_curve(train_curve, valid_curve, curve_path)

# 使用全部训练集重新训练一个“最终模型”,用于后续真正预测测试集
final_net = get_net()
final_train_ls, _ = train(
    final_net,
    train_features,
    train_labels,
    None,
    None,
    num_epochs,
    learning_rate,
    weight_decay,
    batch_size
)
print(f'最终模型训练完成,最后一个 epoch 的训练 log rmse: {final_train_ls[-1]:f}')
print_model_parameters(final_net, '最终模型参数:')

参数 含义
k=5 5折交叉验证
num_epochs=100 训练100轮
learning_rate=5 学习率(⚠️ 非常关键)
weight_decay=0 没有正则化
batch_size=64 每次训练64条

运行结果


折 1, 训练 log rmse 0.170435, 验证 log rmse 0.156715

第 1 折模型参数:
  参数名: 0.weight, 形状: (1, 330), 前5个值: [-1928.2496   3270.9465   3226.0442   6016.519     940.08795]
  参数名: 0.bias, 形状: (1,), 前5个值: [4216.395]
折 2, 训练 log rmse 0.162286, 验证 log rmse 0.191329

第 2 折模型参数:
  参数名: 0.weight, 形状: (1, 330), 前5个值: [-2326.3728  3387.953   3279.3267  5928.1333  1328.5869]
  参数名: 0.bias, 形状: (1,), 前5个值: [4211.6113]
折 3, 训练 log rmse 0.164126, 验证 log rmse 0.168309

第 3 折模型参数:
  参数名: 0.weight, 形状: (1, 330), 前5个值: [-2397.008   3649.8237  3135.434   5950.8135  1031.8002]
  参数名: 0.bias, 形状: (1,), 前5个值: [4198.4546]
折 4, 训练 log rmse 0.168370, 验证 log rmse 0.154671

第 4 折模型参数:
  参数名: 0.weight, 形状: (1, 330), 前5个值: [-2055.231    3407.2402   3447.0017   5885.6133    949.78687]
  参数名: 0.bias, 形状: (1,), 前5个值: [4231.5903]
折 5, 训练 log rmse 0.163068, 验证 log rmse 0.182890

第 5 折模型参数:
  参数名: 0.weight, 形状: (1, 330), 前5个值: [-2087.5762   4176.7007   3496.465    5896.9854    718.92566]
  参数名: 0.bias, 形状: (1,), 前5个值: [4222.353]
5-折交叉验证: 平均训练 log rmse: 0.165657, 平均验证 log rmse: 0.170783

最终模型训练完成,最后一个 epoch 的训练 log rmse: 0.162230

最终模型参数:
  参数名: 0.weight, 形状: (1, 330), 前5个值: [-2436.3774  3523.7056  3477.2534  6635.6543  1962.126 ]
  参数名: 0.bias, 形状: (1,), 前5个值: [4264.351]

折数 train valid 结论
1 0.170 0.156 👍
2 0.162 0.191 ⚠️ 偏高
3 0.164 0.168 👍
4 0.168 0.154 👍
5 0.163 0.182 ⚠️ 偏高

kfold_training_curve