kaggle新手入门房价预测:Pytorch代码-超详细基础讲解,保证你看完也会!(网络搭建与训练篇)

部署运行你感兴趣的模型镜像

在上一篇文章我们已经完全的处理好了数据,数据特征工程的处理需要经验并且也是高手和新手的区别之一,现在我们进入网络搭建,依旧有很多高阶技巧等着大家学习。

代码解析依旧分三个步骤:有什么用,为什么要用它,语法如何使用?

数据集下载地址:

通过网盘分享的文件:home-data-for-ml-course
链接: https://pan.baidu.com/s/1jzsYgKlT7FLoKQ6COBNyDw 提取码: 6688 
 

第一章,数据转化与打包

# 分离回训练集和测试集
X = all_data[:len(y)]
X_test = all_data[len(y):]

# 特征缩放
scaler = StandardScaler()
X = scaler.fit_transform(X)
X_test = scaler.transform(X_test)

# 转换为PyTorch张量
X_tensor = torch.tensor(X, dtype=torch.float32)
y_tensor = torch.tensor(y.values, dtype=torch.float32).view(-1, 1)
X_test_tensor = torch.tensor(X_test, dtype=torch.float32)

# 创建训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(X_tensor, y_tensor, test_size=0.2, random_state=42)

# 创建DataLoader
batch_size = 64
train_dataset = TensorDataset(X_train, y_train)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)

val_dataset = TensorDataset(X_val, y_val)
val_loader = DataLoader(val_dataset, batch_size=batch_size)

1. 分离回训练集和测试集
X = all_data[:len(y)]
X_test = all_data[len(y):]

它在做什么?

这段代码将我们之前精心处理过的、合并在一起的 all_data DataFrame,重新一分为二

X:处理后的训练集特征

X_test:处理后的测试集特征

为什么要这么做?


我们在前面将训练集和测试集合并,是为了确保所有的数据预处理步骤(如缺失值填充、特征工程、One-Hot编码等)都遵循完全一致的标准。

现在,所有的预处理工作都已完成。我们需要将它们分开,因为它们的用途不同了:

训练集 (X) 将与它的答案(y,即房价)配对,用来训练我们的模型。

测试集 (X_test) 是模型没有见过的未知数据,将用来检验模型的预测能力。

语法讲解
这里的语法是利用了Pandas的切片功能。

len(y):y 是我们最早从原始训练集中分离出来的目标变量(房价),所以 y 的长度 len(y) 正好就是原始训练集的行数。

all_data[:len(y)]: 这表示“从 all_data 的第0行开始,取到第 len(y) - 1 行”。这部分不多不少,正好是属于原来训练集的数据。

all_data[len(y):]: 这表示“从 all_data 的第 len(y) 行开始,一直取到最后一行”。这部分就是属于原来测试集的数据。

2. 特征缩放 (Feature Scaling)


scaler = StandardScaler()
X = scaler.fit_transform(X)
X_test = scaler.transform(X_test)

它在做什么?

这段代码使用StandardScaler对所有特征数据进行标准化处理。标准化后,每一列特征的平均值都会变为0,标准差变为1

为什么要这么做?
核心思想:把所有特征放在同一“起跑线”上。

在我们的数据中,不同特征的数值范围(尺度)可能差异巨大。比如,TotalSF(总面积)可能在1000到5000之间,而TotalBath(总浴室数)可能在1到5之间。

如果不进行缩放,数值范围大的特征(如TotalSF)在模型训练(尤其是使用梯度下降算法时)中会占据主导地位,导致模型“偏心”,过度关注这些大数值特征,而忽略小数值特征的作用。

特征缩放后,所有特征都被拉到了一个相似的、更小的范围内,这会带来两大好处:

提升模型性能:模型可以公平地对待每一个特征。

加快训练速度:有助于算法更快地收敛到最优解。

语法讲解

这里有一个极其重要的知识点,是面试和实践中的关键:

scaler = StandardScaler(): 创建一个标准化处理器。

X = scaler.fit_transform(X):

.fit_transform() 对训练集 X 执行两步操作:

fit (拟合):scaler 在 X 上进行“学习”,计算出每一列的平均值和标准差。

transform (转换):使用刚刚学到的平均值和标准差,对 X 的每一列数据进行标准化转换。

X_test = scaler.transform(X_test):

.transform() 只对测试集 X_test 执行一步操作:

transform (转换):使用从训练集X学来的平均值和标准差,去转换 X_test。

类比理解:

fit​ 是学习数学公式(如 y=2x+1)

transform​ 是套用公式计算结果

测试集不能重新学公式,只能用训练集学到的公式

tips:为什么测试集只用 transform 而不是 fit_transform

因为测试集是模拟的未来未知数据。在现实世界中,我们无法提前知道未来数据的分布。因此,所有的数据处理标准(包括缩放用的平均值和标准差)都必须仅从训练数据中学习。用测试集的数据来fit,是一种数据泄露(Data Leakage),会导致对模型性能的评估过于乐观而不真实。

3. 转换为PyTorch张量 (Converting to PyTorch Tensors)


X_tensor = torch.tensor(X, dtype=torch.float32)
y_tensor = torch.tensor(y.values, dtype=torch.float32).view(-1, 1)
X_test_tensor = torch.tensor(X_test, dtype=torch.float32)

它在做什么?

这段代码将我们处理好的数据(目前是Pandas或NumPy格式)转换成PyTorch框架专用的数据格式——张量(Tensor)

为什么要这么做?
框架要求:PyTorch中的所有运算都是基于Tensor的。模型只能“吃”Tensor格式的数据。

两大能力:Tensor可以看作是NumPy数组的加强版,它有两个核心优势:

GPU加速:可以将Tensor轻松地移到GPU上进行计算,利用GPU成千上万个核心进行并行计算,极大地加速深度学习模型的训练。

自动求导:Tensor能够自动追踪所有基于它的运算,构建一个计算图。这是实现反向传播(Backpropagation)和梯度下降,从而让神经网络能够学习。

语法讲解
torch.tensor(...): PyTorch中用于创建张量的主要函数。

dtype=torch.float32: 显式地指定张量的数据类型为32位浮点数。这是深度学习中最常用的数据类型。

y.values: y 是一个Pandas Series,.values 会将其底层的NumPy数组提取出来。

.view(-1, 1): 这是一个非常重要的形状重塑操作。

原始的y_tensor是一个一维向量,形状类似于 (1458,)。

但PyTorch中,损失函数等通常期望目标张量的形状是 (样本数, 输出特征数)。对于我们这个任务,输出特征只有1个(房价)。

.view(-1, 1) 会将这个一维向量变成一个列向量,形状变为 (1458, 1)。这里的 -1 是一个占位符,意思是“PyTorch你帮我自动计算这一维应该有多少行”。

至此,数据已经万事俱备!它们被清理、转换、创造、缩放,并最终封装成了PyTorch能够处理的Tensor格式。下一步,就是将这些准备好的 X_tensor, y_tensor, X_test_tensor 投入到神经网络模型中.

4.划分训练集与验证集


X_train, X_val, y_train, y_val = train_test_split(X_tensor, y_tensor, test_size=0.2, random_state=42)

它在做什么?


这行代码从我们准备好的完整训练数据(X_tensor, y_tensor)中,切分出一小部分(20%)作为验证集(Validation Set)。

训练集 (Training Set): X_train, y_train (占80%)

验证集 (Validation Set): X_val, y_val (占20%)

为什么要这么做?
这是模型训练中一个至关重要的步骤,目的是为了监控和防止模型“过拟合”(Overfitting)。

过拟合指的是模型过于“死记硬背”训练集的数据,导致它在训练集上表现完美,但一遇到没见过的新数据(比如验证集或未来的真实数据)就表现很差。

我们的策略是:让模型只在训练集上学习。在每个学习阶段(epoch)结束后,我们都在验证集上进行一次“模拟考试”,看看模型在“陌生题目”上的表现如何。

如果模型在训练集和验证集上表现都越来越好,说明它在有效地学习。

如果模型在训练集上表现持续变好,但在验证集上的表现开始停滞甚至变差,这就是一个过拟合信号。我们就知道应该停止训练或采取其他措施了。

X_test_tensor 去哪了? 我们之前准备的测试集是“高考卷”,在整个训练和调优过程中都不能碰,只有当我们对模型满意后,才用它来进行最后一次。

语法讲解
train_test_split(...): 这是Scikit-learn库(需要from sklearn.model_selection import train_test_split)提供的一个便捷函数,用于划分数据。

test_size=0.2: 指定了验证集所占的比例为20%。

random_state=42: 这是随机种子。设置一个固定的数字可以保证每次运行代码时,划分出的训练集和验证集都是完全一样的。这对于保证实验的可复现性至关重要,否则每次训练的模型都会因为初始数据不同而有差异,不便于比较和调试。

5.创建数据加载器

# 创建DataLoader
batch_size = 64
train_dataset = TensorDataset(X_train, y_train)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)

val_dataset = TensorDataset(X_val, y_val)
val_loader = DataLoader(val_dataset, batch_size=batch_size)

在做什么?
这段代码为训练集和验证集分别创建了对应的数据加载器(DataLoader)。你可以把它想象成一个智能的“数据传送带”。

TensorDataset: 把特征(X)和标签(y)配对在一起,形成一个标准的数据集。

DataLoader: 在TensorDataset的基础上,创建了一个可以迭代的对象。在模型训练时,我们就可以从这个DataLoader中方便地、一小批一小批地(in batches)取出数据。

为什么要这么做?

核心思想:使用小批量(Mini-batch)进行训练。

我们不一次性把所有数据都丢给模型,而是分成一小批一小批地“喂”给它。batch_size = 64 就意味着每次喂64个样本。这样做有几个巨大的好处:

内存效率高:对于非常大的数据集,一次性加载到内存(尤其是显存)中可能会导致内存溢出。分批加载完美地解决了这个问题。

训练过程更稳定:每次更新模型权重时,是基于一小批数据的平均梯度,这比只用一个样本(随机梯度下降)要稳定,又比用全部样本(批量梯度下降)计算得快。

加速收敛:小批量梯度下降通常能让模型更快地找到最优解,并且有助于跳出“局部最优”的陷阱,从而可能找到更好的全局最优解。

语法讲解
batch_size = 64: 定义了每一批数据的大小。这是一个超参数,可以根据你的显存大小和模型需求进行调整。

TensorDataset(X_train, y_train): 将训练的特征和标签打包。

DataLoader(...): 数据加载器的核心。

shuffle=True: 这是训练集加载器中一个重要的参数看,它会在每个训练周期(epoch)开始前,将数据集的顺序完全打乱。这可以防止模型学习到数据的排列顺序,增强其泛化能力,是避免过拟合的重要手段。

为什么验证集加载器没有shuffle=True? 因为验证集只用于评估,我们只需要计算它上面的总体性能指标(如平均损失),打乱顺序与否对最终结果没有影响,所以没有必要打乱,可以节省一点点计算时间。

第二章,构建PyTorch神经网络模型与实例化

#5. 构建PyTorch神经网络模型


class EnhancedHousePriceNet(nn.Module):
    def __init__(self, input_features):
        super(EnhancedHousePriceNet, self).__init__()
        self.layer1 = nn.Linear(input_features, 512)
        self.bn1 = nn.BatchNorm1d(512)
        self.layer2 = nn.Linear(512, 256)
        self.bn2 = nn.BatchNorm1d(256)
        self.layer3 = nn.Linear(256, 128)
        self.bn3 = nn.BatchNorm1d(128)
        self.layer4 = nn.Linear(128, 64)
        self.output_layer = nn.Linear(64, 1)
        
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(p=0.3)

    def forward(self, x):
        x = self.relu(self.bn1(self.layer1(x)))
        x = self.dropout(x)
        x = self.relu(self.bn2(self.layer2(x)))
        x = self.dropout(x)
        x = self.relu(self.bn3(self.layer3(x)))
        x = self.dropout(x)
        x = self.relu(self.layer4(x))
        x = self.output_layer(x)
        return x

详解各个模块:


super(...):这行是必须的,用于调用父类(nn.Module)的构造函数,完成一些必要的内部初始化。

nn.Linear(input_features, 512) (线性层)

这是神经网络最核心的层,也叫全连接层。它通过一系列的矩阵乘法和加法,将输入数据进行线性变换。

input_features 是输入数据的特征数量(即我们处理好的数据有多少列),512 是这一层输出的特征数量(即该层有512个神经元)。

我们的网络结构像一个漏斗:input_features -> 512 -> 256 -> 128 -> 64 -> 1。数据在其中流动时,信息被不断地提炼和压缩。

nn.ReLU() (激活函数)

这是网络的“开关”。如果没有激活函数,即使堆叠再多线性层,整个网络也只能学习线性关系,能力非常有限。

ReLU (Rectified Linear Unit) 的作用是引入非线性:它会把所有小于0的输入值都变成0,而大于等于0的值保持不变 (output = max(0, input))。这使得网络能够学习和拟合非常复杂的模式。

nn.BatchNorm1d(512) (批量归一化)

这是网络的“稳定器”。在训练过程中,数据流经每一层后,其分布可能会发生剧烈变化,导致训练不稳定、速度变慢。

批量归一化会在每一小批数据通过该层后,强制将其重新“拉回”到一个标准的分布上(类似均值为0,方差为1)。这能极大地加速模型收敛并提高训练的稳定性。

nn.Dropout(p=0.3) (随机失活)

这是网络的“防作弊”或“防过拟合”机制。

在训练时,Dropout会以p=0.3(即30%)的概率,随机地将前一层传来的某些神经元的输出暂时置为0。

这好比一个团队在训练时,总有成员随机轮休。这会迫使其他成员必须学会独立工作,不能过度依赖某一个“学霸”成员。这能有效地防止网络“死记硬背”训练数据,从而提升模型的泛化能力。

详解数据流动路径
我们来追踪一下数据 x 的旅程:

x = self.relu(self.bn1(self.layer1(x)))

首先,x 进入 layer1 进行线性变换。

然后,结果进入 bn1 进行批量归一化。

最后,归一化的结果通过 relu 激活函数。

x = self.dropout(x)

激活后的结果,经过 dropout 层进行随机失活处理。

这个 线性层 -> 批量归一化 -> 激活函数 -> Dropout 的组合模式,是现代深度神经网络中非常经典和有效的“标准模块”,它被重复了三次。

x = self.relu(self.layer4(x))

数据经过第四个隐藏层,并激活。

x = self.output_layer(x)

最后,数据流经输出层,得到一个单一的输出值(因为output_layer的输出维度是1)。

注意:在输出层之后,我们没有使用任何激活函数。对于回归任务(预测一个连续的数值,如房价),房价可以是任何数值,我们不希望像ReLU那样限制它必须大于0。

实例化模型:

# 实例化模型
input_dim = X.shape[1]
model = EnhancedHousePriceNet(input_features=input_dim)
print("\nModel architecture:")
print(model)

# 打印可训练参数数量
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Trainable parameters: {total_params}")


input_dim = X.shape[1]
model = EnhancedHousePriceNet(input_features=input_dim)

它在做什么?
这两行代码正式地创建(或称“实例化”)了我们之前定义的神经网络模型。

input_dim = X.shape[1]:

X 是我们处理好的训练集特征数据(一个NumPy数组或Pandas DataFrame)。

.shape 会返回数据的维度,例如 (1166, 251),表示有1166行(样本)和251列(特征)。

.shape[1] 则会取出第二个数字,即特征的数量 251。

这一步是动态地获取我们最终输入模型的特征数量。

model = EnhancedHousePriceNet(input_features=input_dim):

EnhancedHousePriceNet(...) 这部分就是在调用我们之前定义的那个类的构造函数 __init__。

我们把刚刚计算出的 input_dim (例如251) 作为参数 input_features 传递进去。

这样,模型的第一层 nn.Linear(input_features, 512) 就被正确地初始化为 nn.Linear(251, 512)。

执行完毕后,变量 model 就指向了一个在内存中被创建出来的、结构完整、随时可以接收数据的神经网络对象(实例)。

为什么要这么做?
class EnhancedHousePriceNet(...) 只是一个设计蓝图,它描述了模型“应该”长什么样。而实例化则是根据这份蓝图,实际地在计算机内存中把这个模型“建造”出来。没有这个“实体”,我们就无法用它来训练或预测。

动态获取 input_dim 是一个非常好的编程习惯,它让我们的代码更具鲁棒性。未来如果我们调整了特征工程的步骤,导致最终特征数量发生变化,这段代码无需任何修改就能自动适应。

print("\nModel architecture:")
print(model)
total_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Trainable parameters: {total_params}")

它在做什么?
这部分代码是对我们刚刚创建好的模型实体进行一次可视化:

print(model): 打印出模型的完整结构。

sum(...): 计算并打印出模型中所有可训练参数的总数量。

为什么要这么做?
验证结构 (print(model)): 这是一个重要理智检查。通过打印出的结构,我们可以直观地确认:

所有的层都按照我们在 __init__ 中定义的顺序被创建了吗?

每一层的输入和输出维度都正确连接了吗?

我们有没有遗漏或写错什么层?
这能帮助我们在训练开始前就发现结构上的错误。

了解模型复杂度 (total_params):

参数指的是神经网络中的权重(weights)和偏置(biases),这些是模型在训练过程中需要学习和调整的东西。

可训练参数的总数是衡量一个模型复杂度和容量的关键指标。

一个拥有几百万参数的模型远比一个只有几千参数的模型复杂。这个数字能给我们一个直观的感受:我们的模型有多“大”?它需要多少计算资源来训练?它是否存在过拟合的风险?

语法讲解
print(model): 因为我们的 model 对象继承自 nn.Module,PyTorch为它提供了一个非常友好的打印格式,会自动展示所有子模块的结构。

sum(p.numel() for p in model.parameters() if p.requires_grad):

这行代码虽然看起来复杂,但逻辑很清晰,我们把它拆解开:

model.parameters(): 这个方法会返回一个包含模型所有参数(权重矩阵、偏置向量等)的迭代器。

for p in ...: 我们遍历每一个参数 p(p 本身是一个Tensor)

if p.requires_grad: 我们只关心那些需要被训练(即需要计算梯度)的参数。

p.numel(): .numel() 是 "number of elements" 的缩写,它会返回这个参数张量 p 中所有元素(数字)的总数。例如,一个[10, 5]的权重矩阵,其.numel()就是50。

sum(...): 最后,将所有参数的元素数量加起来,得到整个模型的可训练参数总和。

第3章,训练模型

loss_fn = nn.SmoothL1Loss(beta=0.6)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)

# 添加学习率调度器
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
    optimizer, mode='min', factor=0.5, patience=10
)

# 训练参数
epochs = 1000
train_losses = []
val_losses = []

# 早停设置
best_val_loss = float('inf')
patience = 30
no_improve_count = 0

print("\nTraining started...")

定义损失函数
loss_fn = nn.SmoothL1Loss(beta=0.6)
它在做什么?
这一行定义了损失函数(Loss Function)。损失函数的作用是衡量模型预测值与真实值之间的差距。这个差距(或称为“损失”、“误差”)越小,说明模型预测得越准。整个训练过程的目标,就是想尽一切办法让这个损失值变得尽可能小。

为什么要用 SmoothL1Loss?
这是一个比普通损失函数更高级、更鲁棒的选择。

我们常见的回归损失函数有 nn.L1Loss(计算绝对误差 |y_pred - y_true|)和 nn.MSELoss(计算均方误差 (y_pred - y_true)²)。

L1Loss 的优点是对异常值不那么敏感,但其在零点附近梯度不平滑。

MSELoss 的优点是处处平滑,利于模型学习,但它会放大异常值的误差(因为平方的存在),可能会被个别离谱的数据带偏。

SmoothL1Loss 结合了两者的优点:当误差较小(小于参数beta)时,它的行为像MSELoss,平滑稳定;当误差较大时,它的行为像L1Loss,能有效抵抗异常值的干扰。对于房价预测这类容易出现异常值的数据集,这是一个非常好的选择。

optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)

它在做什么?
这一行定义了优化器(Optimizer)。如果说损失函数告诉我们“模型错得有多离谱”,那么优化器就是那个根据这个“离谱程度”,来具体指导模型如何调整自身参数(权重和偏置)以求下次做得更好的算法。

为什么要用 Adam?
Adam (Adaptive Moment Estimation) 是目前深度学习领域最常用、也最稳健的优化器之一。它结合了其他几种优化器的优点,能够自适应地为不同参数调整学习率,通常能让模型更快、更稳定地收敛。

model.parameters(): 告诉优化器,它需要更新的是 model 中的所有可训练参数。

lr=0.001: lr 代表学习率(Learning Rate),这是训练中最重要的超参数。它决定了模型每次更新参数时“步子”迈多大。步子太大容易“跨过”最优点,步子太小则训练速度过慢。0.001 是一个非常常见的、安全的初始值。

weight_decay=1e-5: 这是在优化器中加入了 L2正则化。它通过给损失函数增加一个关于参数大小的惩罚项,来抑制模型参数变得过大,是一种非常有效的防止过拟合的技术。

 添加学习率调度器

scheduler = optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=10 )

它在做什么?

这一行定义了一个学习率调度器(Learning Rate Scheduler),它可以在训练过程中动态地调整学习率

为什么要这么做?

在训练初期,我们希望学习率大一些,让模型能快速学习;但在训练后期,当模型接近最优解时,我们就需要调小学习率,让它能进行“精雕细琢”的微调,而不是在最优点附近来回“震荡”。

ReduceLROnPlateau 的策略是:“当学习遇到瓶颈时,降低学习率”

mode='min': 它会监控一个指标,当这个指标不再下降时,就触发调整。我们通常用它来监控验证集损失(validation loss)

patience=10: 耐心值。如果连续10个周期(epochs),被监控的指标都没有改善,调度器就会失去耐心。

factor=0.5: 失去耐心后,它会将当前的学习率乘以0.5(即学习率减半)。

epochs = 1000 train_losses = [] val_losses = [] # 早停设置 best_val_loss = float('inf') patience = 30 no_improve_count = 0

它在做什么?

这部分设定了训练的总时长,并为早停(Early Stopping)机制做好了准备。

为什么要这么做?

epochs = 1000: 设定了训练最多进行1000个周期。一个周期(epoch)代表模型完整地看过一遍所有的训练数据。

train_losses, val_losses: 创建两个空列表,用于在每个周期结束后,记录下训练集和验证集的平均损失。这对于后续将训练过程可视化(画出学习曲线)至关重要。

早停机制:这是另一个强大的防止过拟合的策略。即使我们设置了1000个周期,但如果模型在验证集上的表现已经连续很多次没有提升了,再继续训练下去不仅浪费时间,还可能让模型在训练集上过拟合得更严重。

best_val_loss = float('inf'): 用一个变量来记录迄今为止见过的最低的验证集损失,初始值为无穷大。

 patience = 30: 早停的耐心值

 no_improve_count = 0: 一个计数器。

工作流程(将在训练循环中实现):每个周期结束后,如果当前的验证集损失没有比 best_val_loss 更低,no_improve_count 就加1。如果更低了,就更新 best_val_loss 并将 no_improve_count 重置为0。一旦 no_improve_count 达到了30,就意味着模型已经连续30个周期没有在验证集上取得进步了,此时我们就可以提前终止训练。

for epoch in range(epochs):
    model.train()
    batch_train_loss = 0.0
    for inputs, targets in train_loader:
        # 前向传播
        outputs = model(inputs)
        loss = loss_fn(outputs, targets)
        
        # 反向传播和优化
        optimizer.zero_grad()
        loss.backward()
        
        # 梯度裁剪
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        
        optimizer.step()
        
        batch_train_loss += loss.item() * inputs.size(0)
    
    avg_train_loss = batch_train_loss / len(train_loader.dataset)
    train_losses.append(avg_train_loss)
    
    # 验证
    model.eval()
    batch_val_loss = 0.0
    with torch.no_grad():
        for inputs, targets in val_loader:
            outputs = model(inputs)
            loss = loss_fn(outputs, targets)
            batch_val_loss += loss.item() * inputs.size(0)
            
    avg_val_loss = batch_val_loss / len(val_loader.dataset)
    val_losses.append(avg_val_loss)

代码的最外层是一个 for 循环,它会迭代 epochs 次(比如我们之前设定的1000次)。一个 周期 (Epoch) 指的是模型完整地学习一遍所有训练数据。这个外层循环就是整个训练过程的总指挥。

model.train(): 这行代码将模型设置为训练模式。这非常重要,因为它会告诉模型中的某些特殊层(如 Dropout 和 BatchNorm1d)要开始正常工作了(例如,Dropout层会开始随机丢弃神经元)。

for inputs, targets in train_loader:: 这个循环会从我们之前创建的 train_loader 中,一小批一小批地取出数据。inputs 是一批特征,targets 是对应的一批真实标签。

对于从train_loader中取出的每一批数据,模型都会执行以下一套完整的“学习动作”:

前向传播 (Forward Pass)

outputs = model(inputs)

loss = loss_fn(outputs, targets)

数据 inputs 被送入模型,模型根据当前的参数给出一个预测 outputs。然后,损失函数 loss_fn 会计算这个预测与真实值 targets 之间的差距,得到损失值 loss。

清空旧梯度

optimizer.zero_grad()

在计算新的梯度之前,必须先把上一次计算的梯度清除掉。因为PyTorch默认会累积梯度,如果不清零,梯度会越积越大,导致错误的更新。

反向传播 (Backpropagation)

loss.backward()

这是PyTorch最神奇的地方。调用 .backward() 后,它会自动计算出损失值 loss 相对于模型中每一个可训练参数的梯度(即导数)。这个梯度指明了参数应该朝哪个方向调整,才能让损失变得更小。

梯度裁剪 (Gradient Clipping)

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

这是一个可选但非常有用的“安全措施”。有时在训练中,梯度可能会变得异常大(称为梯度爆炸),导致训练极其不稳定。这行代码会检查所有参数的梯度范数(可以理解为梯度的总体大小),如果超过了设定的阈值 max_norm=1.0,就会按比例将它们“裁剪”回这个阈值内,保证了训练的稳定性。

更新权重 (Update Weights)

optimizer.step()

优化器 optimizer 会根据刚刚计算好(并裁剪好)的梯度,来更新模型的所有参数。这就是模型真正“学习”的一步。

阶段二:验证模式 (model.eval())

这是模型进行“模拟考试”的阶段,用来检验学习成果。

    model.eval()

    batch_val_loss = 0.0

    with torch.no_grad():

        for inputs, targets in val_loader:

            # ... 核心验证步骤 ...

model.eval(): 这行代码将模型切换到评估模式。这同样非常重要,它会告诉 Dropout 和 BatchNorm1d 等层改变行为模式(例如,Dropout层会停止丢弃神经元,让整个网络都参与预测)。

with torch.no_grad(): 这是一个上下文管理器,它包裹下的所有代码都不会进行梯度计算。这样做有两个巨大的好处:

节省内存:不计算梯度会大大减少内存占用。

加快速度:省去了大量的计算,使得验证过程更快。 因为在验证时,我们只关心模型的表现,不需要更新参数,所以完全不需要梯度。

核心验证步骤

验证循环比训练循环简单得多,它只做预测和计分,不学习:

            outputs = model(inputs)

            loss = loss_fn(outputs, targets)

            batch_val_loss += loss.item() * inputs.size(0)

它只是把验证数据 val_loader 送入模型,计算预测值和损失,然后把损失累加起来,用于计算整个验证集的平均损失。

    # 更新学习率调度器
    scheduler.step(avg_val_loss)
    
    # 获取当前学习率
    current_lr = optimizer.param_groups[0]['lr']
    
    # 输出训练进度
    if (epoch + 1) % 20 == 0 or epoch == 0:
        print(f'Epoch [{epoch+1}/{epochs}], LR: {current_lr:.6f}, '
              f'Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}')
    
    # 早停机制
    if avg_val_loss < best_val_loss:
        best_val_loss = avg_val_loss
        no_improve_count = 0
        torch.save(model.state_dict(), 'best_model.pth')
        print(f"Validation loss improved to {best_val_loss:.4f}, model saved")
    else:
        no_improve_count += 1
        if no_improve_count >= patience:
            print(f"Early stopping: no improvement for {patience} consecutive epochs")
            break

print("Training completed.")

# 加载最佳模型
model.load_state_dict(torch.load('best_model.pth'))
print("Loaded best model")



# 绘制损失曲线
plt.figure(figsize=(10, 6))
plt.plot(train_losses, label='Training Loss')
plt.plot(val_losses, label='Validation Loss')
plt.title('Training and Validation Loss Curve')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
plt.savefig('loss_curve.png', dpi=300)
plt.show()

# 在验证集上评估
model.eval()
with torch.no_grad():
    val_preds = model(X_val)
    val_mse = loss_fn(val_preds, y_val).item()
    print(f"Final validation loss: {val_mse:.6f}")

    # 预测值逆变换回原始价格尺度
    original_val_preds = np.expm1(val_preds.numpy().squeeze())
    original_y_val = np.expm1(y_val.numpy().squeeze())
    
    # 计算相对误差百分比
    relative_errors = np.abs(original_val_preds - original_y_val) / original_y_val * 100
    mean_relative_error = np.mean(relative_errors)
    print(f"Mean relative error percentage: {mean_relative_error:.2f}%")

# 绘制预测值与实际值对比图
plt.figure(figsize=(10, 8))
plt.scatter(original_y_val, original_val_preds, alpha=0.3)
plt.plot([min(original_y_val), max(original_y_val)], 
         [min(original_y_val), max(original_y_val)], 
         color='red', linestyle='--')
plt.xlabel('Actual Price')
plt.ylabel('Predicted Price')
plt.title('Actual vs. Predicted House Prices')
plt.grid(True)
plt.savefig('price_comparison.png', dpi=300)
plt.show()



model.eval()
with torch.no_grad():
    predictions_log = model(X_test_tensor).squeeze().numpy()

# 逆变换回原始价格尺度
predictions = np.expm1(predictions_log)

# 创建提交文件
submission = pd.DataFrame({'Id': test_ids, 'SalePrice': predictions})
submission.to_csv('submission.csv', index=False)

print("\nPredictions completed. 'submission.csv' file generated.")
print("Submission file preview:")
print(submission.head())

1. 训练循环的收尾工作 (在for epoch...循环内部)

这部分代码在每个周期(epoch)的验证阶段之后执行,负责管理、汇报和决策。

# 更新学习率调度器
    scheduler.step(avg_val_loss)
    
    # 获取当前学习率
    current_lr = optimizer.param_groups[0]['lr']
    
    # 输出训练进度
    if (epoch + 1) % 20 == 0 or epoch == 0:
        print(f'Epoch [{epoch+1}/{epochs}], LR: {current_lr:.6f}, '
              f'Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}')
    
    # 早停机制
    if avg_val_loss < best_val_loss:
        best_val_loss = avg_val_loss
        no_improve_count = 0
        torch.save(model.state_dict(), 'best_model.pth')
        # print(f"...") # (Optional print statement)
    else:
        no_improve_count += 1
        if no_improve_count >= patience:
            print(f"Early stopping: no improvement for {patience} consecutive epochs")
            break

print("Training completed.")

它在做什么?

这个模块是训练循环的“智能大脑”,负责:

策略调整:根据本轮的验证损失 avg_val_loss,让学习率调度器 scheduler 决定是否要降低学习率。

进度汇报:定期(每20个周期)打印出关键指标,让我们能实时监控训练状态。

择优保存与早停:判断模型性能是否提升,如果提升了就保存当前模型为“最佳模型”;如果连续多次没有提升,就提前终止训练。

为什么要这么做?
动态调整:scheduler.step(avg_val_loss) 是实现动态学习率的关键。我们把“模拟考试”的成绩单交给它,它会根据我们预设的策略(如果连续10次没进步,学习率减半)来调整学习方法。

避免信息轰炸:如果每个周期都打印一次进度,屏幕会被刷爆。if (epoch + 1) % 20 == 0 通过取余数运算,实现了每20次才汇报一次,让输出更清爽。

保存状态:torch.save(...) 是这里最关键的一步。训练过程可能会有起伏,模型在第500个周期的表现可能比第520个周期还好。我们只在验证损失创下新低的时候才保存模型的状态(参数),确保我们最后使用的是模型整个生涯中的巅峰状态,而不是它“退休”时的状态。

节省时间与资源:早停机制 Early Stopping 避免了无意义的训练。当模型已经无法在验证集上取得进步时,再训练下去不仅浪费时间,还可能导致过拟合。果断停止是明智之举。

2. 评估与可视化 (Evaluation & Visualization)

训练结束后,我们用数据和图表来全方位地评估我们的模型。

# 加载最佳模型
model.load_state_dict(torch.load('best_model.pth'))
print("Loaded best model")

# 绘制损失曲线
plt.figure(...)
plt.plot(train_losses, ...)
plt.plot(val_losses, ...)
# ... (plotting code) ...

# 在验证集上评估
model.eval()
with torch.no_grad():
    # ... (evaluation code) ...
    original_val_preds = np.expm1(val_preds.numpy().squeeze())
    original_y_val = np.expm1(y_val.numpy().squeeze())
    # ... (calculate metrics) ...

# 绘制预测值与实际值对比图
plt.figure(...)
plt.scatter(original_y_val, original_val_preds, ...)
# ... (plotting code) ...

它在做什么?
加载最佳模型:从硬盘加载刚才保存的 best_model.pth 文件,确保我们使用的是性能最好的模型参数。

绘制损失曲线:将记录下来的 train_losses 和 val_losses 画成折线图,直观地展示训练过程。

最终评估:在验证集上进行一次最终的性能评估,并计算一些比“损失值”更易于人类理解的指标。

绘制对比图:画出“预测房价”与“真实房价”的散点图,来观察模型的预测能力。

 为什么要这么做?
损失曲线是“心电图”:这张图是诊断模型训练健康状况最重要的工具。理想情况下,两条线都平稳下降并最终收敛。如果训练损失很低但验证损失很高(两条线分叉),就是典型的过拟合。

逆变换回真实价格:我们的模型预测的是 log(1+price),这个值对我们来说不直观。np.expm1() 是 np.log1p() 的逆运算,它可以将预测值和真实值都转换回我们熟悉的美元价格,这样才能计算有实际意义的误差。

可视化预测效果:散点图能非常直观地展示模型的预测效果。如果所有的点都紧密地分布在 y=x 这条红色的对角线周围,说明模型的预测非常精准。

3. 生成提交文件 (Generating Submission File)
这是我们参加Kaggle竞赛的最后一步,生成需要提交给官方的结果文件。

model.eval()
with torch.no_grad():
    predictions_log = model(X_test_tensor).squeeze().numpy()

predictions = np.expm1(predictions_log)

submission = pd.DataFrame({'Id': test_ids, 'SalePrice': predictions})
submission.to_csv('submission.csv', index=False)

它在做什么?
进行最终预测:用我们最好的模型,对从未见过的测试集 X_test_tensor 进行预测。

逆变换结果:同样,将对数尺度的预测结果转换回真实的价格。

创建并保存文件:按照Kaggle要求的格式(Id 和 SalePrice 两列),创建一个DataFrame,并将其保存为 submission.csv 文件。

 为什么要这么做?
这是整个项目的最终交付成果。每一个步骤都至关重要,以确保生成的文件格式正确无误。

model.eval() with torch.no_grad() 是预测时的标准操作。

.squeeze().numpy(): 这是将PyTorch的Tensor变回NumPy数组的标准流程,以便于后续处理。

pd.DataFrame({'Id': test_ids, ...}): 这里用到了我们项目一开始就小心翼翼保存下来的 test_ids,现在它终于派上了用场,用来将每个预测价格与它的房屋ID对应起来。

index=False: 在保存CSV时,这个参数可以防止Pandas将自己的行号(0, 1, 2...)写入文件,确保文件格式纯净,符合提交要求。

您可能感兴趣的与本文相关的镜像

PyTorch 2.5

PyTorch 2.5

PyTorch
Cuda

PyTorch 是一个开源的 Python 机器学习库,基于 Torch 库,底层由 C++ 实现,应用于人工智能领域,如计算机视觉和自然语言处理

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值