波士顿房价回归任务为案例,提供两套实现:底层手写实现帮助吃透原理;PyTorch 高级 API 用于工程开发。完整覆盖数据集、预处理、训练循环、模型保存推理、调优拓展。
一、数据集:波士顿房价数据集
- 样本:506 条,13 维输入特征,目标
MEDV房价中位数(回归任务) - 特征包含:犯罪率、住宅用地占比、房间数量、距离市中心距离、师生比例、低收入人口占比等。
二、回归项目完整标准流程
全连接网络以nn.Linear线性层为核心,完整流程:
- 读取原始数据集
- 数据集划分 + 特征标准化预处理
- 构建数据集、批处理加载器
- 搭建网络模型(单层线性 / 多层全连接)
- 训练循环:前向传播→计算损失→反向传播→参数更新→梯度清零
- 独立测试集做模型评估
- 模型持久化保存,编写推理预测函数
拓展调优方向:修改预处理、调整 batch‑size、增加网络层数、更换激活函数、调整学习率 / 优化器、可视化损失、模型保存加载。
三、方案一:底层手动手写实现(原理学习)
不使用
nn.Linear、Dataset、优化器高层封装,手动实现全部逻辑
1. 数据预处理
- pandas 读取 csv,切分训练集 / 测试集;转为 float32 张量。
- Z‑score 标准化 ,只使用训练集的均值 μ、标准差 σ做缩放;加极小值\(10^{-9}\)防止标准差为 0 造成除零报错。
\(X_{scaled}=\frac{X-\mu}{\sigma+1e^{-9}}\)
标准化作用:消除不同特征量纲差异,加速梯度下降收敛。
2. 手写批数据加载器
使用 Python 生成器yield自定义get_dataloader,按batch_size切分数据;
- 计算总批次:向上取整\(\lceil 样本数/batch\_size\rceil\)
- 处理最后一个批次样本不足的边界情况。
3. 手动定义模型、损失
- 手动创建可求导权重w、偏置b,设置
requires_grad=True开启自动微分。 - 线性模型:\(\hat y = X @ w + b\),矩阵乘法实现线性变换。
- 手写 MSE 均方误差损失:
\(M S E=\frac{1}{n} \sum_{i=1}^{n}\left(y_{i}-\hat{y}_{i}\right)^{2}\)
4. 手写完整训练循环
训练的 5 个核心步骤:
- 前向传播 :
y_pred = model(batch_X,w,b),计算预测值 - 计算损失:对比预测与真实标签
- 反向传播 :
loss.backward()自动求导,计算 w、b 梯度 - 手动参数更新:\(w.data -= lr * w.grad\);\(b.data -= lr * b.grad\)
- 梯度清零 :
w.grad.zero_()、b.grad.zero_(),避免梯度累积叠加。
底层实现中没有优化器对象,参数更新、梯度清零全部手写。
5. 底层方式模型保存与推理
- 保存:把
w、b、训练集均值mu、sigma存入字典,torch.save()保存为.pt文件。
注意推理时必须同时保存标准化统计量,新样本推理要复用训练集的 μ、σ。
- 推理函数:输入数据转为张量,执行标准化,做矩阵运算输出预测房价。
python
import pandas as pd
import torch
import math
from sklearn.model_selection import train_test_split
# ==========1. 数据加载与预处理 ==========
data = pd.read_csv("boston_house_prices.csv", skiprows=1)
X = data.loc[:, :"LSTAT"].to_numpy()
y = data.loc[:, "MEDV"].to_numpy().reshape(-1,1)
# 划分训练、测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=0
)
# 转为张量
X_train = torch.tensor(X_train, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.float32)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_test = torch.tensor(y_test, dtype=torch.float32)
# Z‑score标准化,使用训练集统计量,加1e‑9防止除零
mu = X_train.mean(axis=0)
sigma = X_train.std(axis=0)
X_train = (X_train - mu) / (sigma + 1e-9)
X_test = (X_test - mu) / (sigma + 1e-9)
# ==========2.手写生成器dataloader ==========
def get_dataloader(X, y, batch_size=16):
num_samples, _ = X.shape
num_batches = math.ceil(num_samples / batch_size)
for idx in range(num_batches):
batch_X = X[idx*batch_size : (idx+1)*batch_size, :]
batch_y = y[idx*batch_size : (idx+1)*batch_size, :]
if len(batch_X) > 0:
yield batch_X, batch_y
# ==========3.手动定义模型、损失 ==========
# 初始化可求导权重、偏置
w = torch.randn(13, 1, dtype=torch.float32, requires_grad=True)
b = torch.randn(1, dtype=torch.float32, requires_grad=True)
def model(X, w, b):
return X @ w + b
def mse_loss_fn(y_pred, y_true):
return ((y_pred - y_true) ** 2).mean()
# ==========4.手写训练循环 ==========
epochs = 50
lr = 1e-3
batch_size =16
def get_loss(dataloader):
total_loss = 0.0
count =0
for bx,by in dataloader:
pred = model(bx,w,b)
loss = mse_loss_fn(pred,by)
total_loss += loss.item()
count +=1
return total_loss / count
for epoch in range(epochs):
train_loader = get_dataloader(X_train, y_train, batch_size)
for batch_X, batch_y in train_loader:
# 1 前向传播
y_pred = model(batch_X, w, b)
# 2 计算损失
loss = mse_loss_fn(y_pred, batch_y)
# 3 反向传播求梯度
loss.backward()
#4 手动更新参数
w.data -= lr * w.grad
b.data -= lr * b.grad
#5 梯度清零,防止梯度累加
w.grad.zero_()
b.grad.zero_()
train_loss = get_loss(get_dataloader(X_train,y_train,batch_size))
test_loss = get_loss(get_dataloader(X_test,y_test,batch_size))
print(f"Epoch:{epoch+1:2d} | Train Loss:{train_loss:.3f} | Test Loss:{test_loss:.3f}")
# ==========5.底层方式模型保存与推理 ==========
# 保存,把均值、标准差、权重全部存入
w.requires_grad = False
b.requires_grad = False
save_dict = {"mu":mu, "sigma":sigma, "w":w, "b":b}
torch.save(save_dict, "model_raw.pt")
# 推理函数
def infer(x, mu, sigma, w, b, normalized=False):
if not torch.is_tensor(x):
x = torch.tensor(x, dtype=torch.float32)
if x.ndim == 1:
x = x.view(1,-1)
if not normalized:
x = (x - mu)/(sigma +1e-9)
return model(x,w,b)
# 加载测试
ckpt = torch.load("model_raw.pt", weights_only=True)
pred_result = infer(X_test[0], ckpt["mu"], ckpt["sigma"], ckpt["w"], ckpt["b"])
print("单样本预测房价:", pred_result.item())
核心公式
\(X_{scaled}=\frac{X-\mu}{\sigma+1e^{-9}}\)
\(M S E=\frac{1}{n} \sum_{i=1}^{n}\left(y_{i}-\hat{y}_{i}\right)^{2}\)
\(\hat y = X @ w + b\)
四、方案二:PyTorch 高级 API 实现(工程开发)
使用 PyTorch 封装组件,代码简洁,工业项目优先使用。
1. 数据集与 DataLoader
- 自定义类继承
torch.utils.data.Dataset,必须实现两个魔术方法:__getitem__(idx):取出单条样本__len__():返回数据集总样本数量
DataLoader:自动完成批处理、多线程、shuffle 打乱;- 训练集:
shuffle=True打乱样本顺序; - 测试集:
shuffle=False,不需要打乱。
- 训练集:
2. 模型、损失、优化器
-
模型:直接实例化
nn.Linear(in_features=13, out_features=1),内部自动初始化权重与偏置。 -
损失函数:
nn.MSELoss()。 -
优化器:
优化器 特点 适用场景 SGD 简单稳定,适合入门学习 基础练习 Adam 自适应学习率,收敛快 绝大多数任务 RMSprop 适配序列数据 RNN、时序模型
3. 高级版训练循环重点细节
model.train():训练模式,启用 Dropout、BN 等训练专属逻辑。model.eval():评估模式,关闭网络随机性;评估代码块包裹with torch.no_grad()关闭梯度计算,节省显存。- 训练四步:前向传播→算 loss→
loss.backward()→optimizer.step()更新参数→optimizer.zero_grad()清空梯度。
4. 高级 API 模型保存与推理
- 保存:字典存储标准化参数
mu、sigma+model.state_dict()(网络权重状态)。 - 加载:重建网络模型,使用
model.load_state_dict()恢复权重;推理阶段放在with torch.no_grad()上下文管理器。
python
import pandas as pd
import torch
from torch import nn
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split
# ==========1.数据加载、预处理(同底层版本) ==========
data = pd.read_csv("boston_house_prices.csv", skiprows=1)
X = data.loc[:, :"LSTAT"].to_numpy()
y = data.loc[:, "MEDV"].to_numpy().reshape(-1,1)
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.2,random_state=0)
X_train = torch.tensor(X_train, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.float32)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_test = torch.tensor(y_test, dtype=torch.float32)
mu = X_train.mean(axis=0)
sigma = X_train.std(axis=0)
X_train = (X_train - mu)/(sigma + 1e-9)
X_test = (X_test - mu)/(sigma + 1e-9)
# ==========2.自定义Dataset数据集类 ==========
class HousePriceDataset(Dataset):
def __init__(self, X_data, y_data):
self.X = X_data
self.y = y_data
def __getitem__(self, idx):
return self.X[idx], self.y[idx]
def __len__(self):
return len(self.X)
train_set = HousePriceDataset(X_train, y_train)
test_set = HousePriceDataset(X_test, y_test)
train_loader = DataLoader(train_set, batch_size=16, shuffle=True)
test_loader = DataLoader(test_set, batch_size=32, shuffle=False)
# ==========3.模型、损失函数、优化器 ==========
model = nn.Linear(in_features=13, out_features=1)
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
epochs = 50
# ==========4.训练循环 ==========
def eval_loss(loader):
model.eval()
total = 0.0
cnt = 0
with torch.no_grad():
for bx,by in loader:
pred = model(bx)
loss = loss_fn(pred,by)
total += loss.item()
cnt += 1
return total / cnt
train_loss_list = []
test_loss_list = []
for epoch in range(epochs):
model.train()
for batch_X, batch_y in train_loader:
# 前向传播
y_pred = model(batch_X)
loss = loss_fn(y_pred, batch_y)
#反向传播
loss.backward()
#参数更新
optimizer.step()
#梯度清零
optimizer.zero_grad()
tr_loss = eval_loss(train_loader)
te_loss = eval_loss(test_loader)
train_loss_list.append(tr_loss)
test_loss_list.append(te_loss)
print(f"Epoch:{epoch+1:2d} | Train Loss:{tr_loss:.3f} | Test Loss:{te_loss:.3f}")
# ==========5.模型保存、加载推理 ==========
save_dict = {
"mu": mu,
"sigma": sigma,
"model_state": model.state_dict()
}
torch.save(save_dict, "model_api.pt")
# 加载推理
checkpoint = torch.load("model_api.pt", weights_only=True)
new_model = nn.Linear(13, 1)
new_model.load_state_dict(checkpoint["model_state"])
def api_infer(x, mu, sigma, model, normalized=False):
if not torch.is_tensor(x):
x = torch.tensor(x, dtype=torch.float32)
if x.ndim ==1:
x = x.view(1,-1)
if not normalized:
x = (x - mu)/(sigma +1e-9)
model.eval()
with torch.no_grad():
return model(x)
res = api_infer(X_test[0], checkpoint["mu"], checkpoint["sigma"], new_model)
print("API方式预测房价:", res.item())
优化器对比表
表格
| 优化器 | 特点 | 适用场景 |
|---|---|---|
| SGD | 简单稳定,适合入门学习 | 基础练习 |
| Adam | 自适应学习率,收敛快 | 绝大多数任务 |
| RMSprop | 适配序列数据 | RNN、时序模型 |
五、两种实现方式对比
表格
| 特性 | 底层手写实现 | 高级 API 实现 |
|---|---|---|
| 学习价值 | 适合理解底层原理 | 适合快速工程开发 |
| 代码复杂度 | 高,全部逻辑手动写 | 低,大量逻辑框架封装 |
| 灵活性 | 完全可控 | 受框架约束 |
| 可维护性 | 差 | 好 |
| 性能优化 | 需要自己处理 | 框架自动做优化 |
六、核心数学公式
- 线性回归:\(\hat{y}=X \cdot W+b\)
2.MSE 损失:\(L=\frac{1}{n}\sum_{i=1}^n(y_i-\hat y_i)^2\)
- 梯度下降更新
\(W_{t+1}=W_{t}-\alpha \cdot \frac{\partial L}{\partial W}\)
\(b_{t+1}=b_{t}-\alpha \cdot \frac{\partial L}{\partial b}\)
七、PyTorch 核心机制
- 自动微分 :
requires_grad=True开启梯度跟踪,backward()自动求导。 - 动态计算图:运行时构建计算逻辑,灵活适配模型。
- 张量运算:支持 CPU/GPU 异构加速。
- 模块化:
nn.Module提供网络封装规范。
八、最佳实践
1)数据预处理
- 回归任务务必做特征标准化;只能用训练集统计量缩放测试集;
- 检查张量维度、数据类型,避免维度不匹配报错。
2)模型训练
- 同时监控训练损失、测试损失,观察过拟合;
- 合理调学习率、batch‑size,按需选择优化器。
3)模型评估
- 使用独立测试集评估泛化能力;多指标评估;绘制损失变化曲线。
4)工程实践
- 保存模型时,同步保存预处理统计参数(mu、sigma);
- 推理阶段务必使用
model.eval()和with torch.no_grad(); - 封装可复用推理函数;增加异常判断。
九、全连接网络拓展示例(多层网络)
将单层线性回归改成多层全连接,引入非线性激活
class MLPReg(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(13,32),
nn.ReLU(),
nn.Linear(32,16),
nn.ReLU(),
nn.Linear(16,1)
)
def forward(self,x):
return self.net(x)
mlp_model = MLPReg()
拓展实验方向:修改 batch 大小、调整学习率、替换 Adam 优化器、绘制 train/test 损失曲线。
提示:运行代码需要准备
boston_house_prices.csv数据集文件。