📋 前言
大家好!今天是意义非凡的 Day 36,一个承上启下的复习日。经过前几天的学习,我们已经掌握了 PyTorch 的基本操作。现在,是时候将这些"零件"组装起来,完成一次从"手工作坊"到"现代化工厂"的升级了!
今天的核心任务是:重拾之前的信贷预测项目,但这一次,我们将用全套的 PyTorch 流程来重构它 。我们的目标不仅是让代码跑通,更是要让它变得规范、美观、易于维护 。同时,我们还会深入探索 PyTorch 的灵魂------nn.Module,看看它内部到底藏着什么秘密。
准备好了吗?让我们开始这场代码的"精装修"之旅!
一、知识点回顾:我们的"工具箱"里有什么?
在开始重构之前,我们先盘点一下最近学到的、即将用于本次项目的"神兵利器":
-
数据层:
torch.Tensor: PyTorch 的基本数据结构。torch.FloatTensor,torch.LongTensor: 分别用于特征和标签。- 数据预处理:
MinMaxScaler依然适用,但结果需转换为 Tensor。 - 设备管理:
.to(device),轻松实现 CPU/GPU 切换。
-
模型层:
nn.Module: 所有模型的基类,我们的"蓝图"。nn.Linear,nn.ReLU: 构建全连接神经网络的核心组件。super().__init__(): 在子类中调用父类的构造函数,标准范式。
-
训练层:
- 损失函数 :
nn.CrossEntropyLoss(用于多分类)。 - 优化器 :
torch.optim.SGD或torch.optim.Adam。 - 黄金三步 :
optimizer.zero_grad()->loss.backward()->optimizer.step()。
- 损失函数 :
-
工程实践层:
- 模型诊断 :
torchinfo,一键查看模型结构和参数。 - 进度可视化 :
tqdm,让漫长的训练过程不再枯燥。 - 推理模式 :
model.eval()和with torch.no_grad(),确保评估的严谨性和高效性。
- 模型诊断 :
今天,我们将把以上所有知识点,像拼乐高一样,严丝合缝地应用到我们的项目中。
二、实战作业:用神经网络重构信貸预测项目
1. 项目目标
使用 PyTorch 构建一个多层感知机 (MLP) 模型,对信贷数据集进行训练和评估,并确保代码结构清晰、过程可追溯。
2. 重构步骤与代码实现
我们将遵循"数据-模型-训练-评估"的经典流程来组织代码。
python
# 【我的代码 - Day 36 作业】
import pandas as pd
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import classification_report, accuracy_score
from torchinfo import summary
from tqdm import tqdm
import time
# --- 1. 环境准备 ---
# 检查是否有可用的 GPU,否则使用 CPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"======== 当前使用的设备: {device} ========\n")
# --- 2. 数据加载与预处理 ---
# 注意:请确保 'data.csv' 文件与此脚本在同一目录下,或提供完整路径
try:
data = pd.read_csv('data.csv')
except FileNotFoundError:
print("错误:未找到 'data.csv' 文件。请将数据文件放在正确的位置。")
exit()
# 简单的预处理,与之前保持一致
data['Term'] = data['Term'].replace({'Short Term': 0, 'Long Term': 1})
data.drop_duplicates(inplace=True)
data.dropna(inplace=True)
X = data.drop('Term', axis=1)
y = data['Term']
# 数据划分
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# 特征缩放
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 转换为 PyTorch Tensors 并移动到指定设备
X_train_tensor = torch.FloatTensor(X_train_scaled).to(device)
y_train_tensor = torch.LongTensor(y_train.values).to(device)
X_test_tensor = torch.FloatTensor(X_test_scaled).to(device)
y_test_tensor = torch.LongTensor(y_test.values).to(device)
print("======== 数据准备完成 ========")
print(f"训练集大小: {X_train_tensor.shape}")
print(f"测试集大小: {X_test_tensor.shape}\n")
# --- 3. 模型定义 ---
class CreditMLP(nn.Module):
def __init__(self, input_size, hidden_size1, hidden_size2, num_classes):
super(CreditMLP, self).__init__()
self.network = nn.Sequential(
nn.Linear(input_size, hidden_size1),
nn.ReLU(),
nn.Linear(hidden_size1, hidden_size2),
nn.ReLU(),
nn.Linear(hidden_size2, num_classes)
)
def forward(self, x):
return self.network(x)
# 模型实例化
input_dim = X_train.shape[1]
model = CreditMLP(input_size=input_dim, hidden_size1=64, hidden_size2=32, num_classes=2).to(device)
# 使用 torchinfo 打印模型摘要
print("======== 模型结构摘要 ========")
summary(model, input_size=(1, input_dim)) # (batch_size, features)
print("\n")
# --- 4. 训练过程 ---
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
num_epochs = 100
print("======== 开始模型训练 ========")
start_time = time.time()
for epoch in range(num_epochs):
model.train() # 设置为训练模式
# 前向传播
outputs = model(X_train_tensor)
loss = criterion(outputs, y_train_tensor)
# 反向传播与优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (epoch + 1) % 10 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')
end_time = time.time()
print(f"======== 训练完成,耗时: {end_time - start_time:.2f} 秒 ========\n")
# --- 5. 模型评估 ---
print("======== 开始模型评估 ========")
model.eval() # 切换到评估模式
with torch.no_grad(): # 在此代码块中不计算梯度
# 在测试集上进行预测
test_outputs = model(X_test_tensor)
# 获取预测结果
# torch.max 返回 (values, indices),我们只需要索引
_, predicted = torch.max(test_outputs.data, 1)
# 将 GPU 上的 tensor 移动到 CPU 以便使用 sklearn
predicted_cpu = predicted.cpu().numpy()
y_test_cpu = y_test_tensor.cpu().numpy()
# 计算并打印评估指标
accuracy = accuracy_score(y_test_cpu, predicted_cpu)
report = classification_report(y_test_cpu, predicted_cpu, target_names=['Short Term', 'Long Term'])
print(f"测试集准确率: {accuracy * 100:.2f}%\n")
print("分类报告:")
print(report)
3. 结果分析与心得
- 规范性:代码被清晰地划分为环境准备、数据处理、模型定义、训练、评估五个部分。每个部分职责单一,逻辑清晰。
- 美观性 :使用了
print语句作为分隔符,输出了关键信息(设备、数据大小、模型结构等),让整个执行过程一目了然。 - 健壮性 :模型定义被封装在
CreditMLP类中,实现了代码的复用和模块化。 - 用户体验 :虽然本次训练很快,但如果 epochs 很多,
tqdm进度条将极大提升体验。在未来的项目中,这是一个好习惯。 - 严谨性 :严格区分了
model.train()和model.eval()模式,并使用torch.no_grad()进行高效推理。
通过这次重构,我们的代码不再是杂乱无章的脚本,而是一个有模有样的迷你项目,这正是专业开发的起点。
三、探索性作业:深入 nn.Module 的"心脏"
问题 :我们总是写 model = MyModel(),然后用 model(data) 来进行预测,但我们自己只定义了 forward 方法,model() 这个调用是怎么工作的?model.to(device) 又是从哪来的?
答案 :所有秘密都藏在 nn.Module 的源码里。
在 VSCode 或 PyCharm 中,按住 Ctrl (或 Cmd) 键,然后用鼠标点击你代码中的 nn.Module,就可以跳转到它的定义。你会发现许多以双下划线开头和结尾的"魔法方法"。
这里揭示几个最重要的秘密:
-
__call__的魔法:- 你以为
model(data)直接调用了forward?不完全是!它实际上调用的是__call__方法。 __call__是一个"总管",它在调用我们写的forward之前和之后,会做很多额外的工作,比如执行我们后面会学到的"钩子 (Hooks)"。- 结论 :
nn.Module的设计鼓励我们只关心核心逻辑 (forward),而把复杂的管理工作交给框架。所以,永远使用model(data),而不是model.forward(data)。
- 你以为
-
train()和eval()的开关:nn.Module内部有一个名为self.training的布尔值标志。- 调用
model.train()会将self.training设为True。 - 调用
model.eval()会将其设为False。 - 像
nn.Dropout和nn.BatchNorm2d这样的层会检查这个标志,来决定自己应该如何工作。
-
parameters()的"户口本":nn.Module会自动追踪所有被赋值为nn.Parameter或其子模块的属性。- 当你调用
model.parameters()时,它会递归地遍历所有子模块,把所有需要训练的参数(权重、偏置)收集起来,像一个"户口本",然后交给优化器去更新。
比喻时间 :
nn.Module 就像一个智能汽车底盘。
__init__: 你在底盘上安装引擎、轮胎(定义nn.Linear等层)。forward: 你定义了踩油门时,动力如何从引擎传递到轮胎(数据如何流过网络)。__call__: 是整个汽车的驾驶系统。你踩下油门踏板 (model(data)),系统不仅会执行你的forward逻辑,还会检查车况、调整悬挂(执行 Hooks)。parameters(): 是汽车的零件清单,方便你去保养和升级(交给优化器)。to(device): 是把汽车开到不同的赛道(CPU/GPU)上。
通过这次探索,我们不再是 nn.Module 的简单使用者,而是开始理解其设计哲学的思考者。
四、总结
Day 36 是一个里程碑。我们不仅成功地将神经网络应用于一个真实的项目,更重要的是,我们学会了如何有组织、有纪律地编写深度学习代码。代码的规范性和美观性,与它的功能性同等重要。
通过重构和探索,我们内化了 PyTorch 的设计思想,为未来构建更复杂、更强大的模型打下了坚实的地基。
再次感谢 @浙大疏锦行 老师的精心安排,这次复习让我对之前的知识有了脱胎换骨的理解!