资讯动态

用Portorch实现线性回归:从零掌握深度学习框架训练全流程

发布时间:2026/10/22 3:57:39 来源:尧图企业网站定制
要说线性回归这类基础模型很多人第一反应是“这么简单的东西还需要专门用深度学习框架写一遍吗”但如果把维度换一下线性回归恰恰是理解深度学习框架工作流的最佳载体没有之一。你把一个线性回归跑通了后面所有复杂的网络结构本质上都是在同一个骨架上换不同的积木。我最近用Portorch重写了线性回归建模的全流程这篇文章就把整个过程掰开揉碎讲清楚包括那些文档里不会写、只有真跑过代码才懂的东西。1. 为什么用Portorch写线性回归而不是直接用传统统计库1.1 从一行公式到完整训练闭环的思维转变传统统计库做线性回归你干的事情就是一行代码拟合系数。比如用某个统计库把数据喂进去它背后可能用最小二乘法直接解出解析解整个过程非常快但对你理解“模型怎么学习”这件事几乎没有帮助。而Portorch写线性回归本质是用深度学习的最小化思维去逼近最优解。线性回归的解析解是闭式的但训练过程的每一步都是开放的你可以手动控制梯度怎么回传、损失怎么下降、参数怎么更新、什么时候过拟合。这种“可控感”是传统统计库给不了的。我当时第一次在Portorch里把线性回归跑通之后回头再看神经网络那些概念什么前向传播、反向传播、优化器更新全都串起来了。所以我的建议是哪怕你以后根本不会用线性回归去做实际业务也值得用Portorch从头到尾实现一遍。它不是一个“任务”它是一个“透镜”。1.2 Portorch框架在这里扮演什么角色Portorch的核心操作逻辑是定义计算图、自动计算梯度、按优化器策略更新参数。线性回归模型只需要一个全连接层甚至可以直接用参数矩阵手动定义。但即使是这么简单的模型你依然能感受到框架设计的一致性——你在手写线性回归时写的每一行代码几乎都能被原封不动地迁移到多层感知机、卷积网络甚至Transformer里。这也是为什么现在很多教学路线都改成“用框架从线性回归开始学起”而不是“先学一堆数学推导再碰框架”。因为计算图的概念太难靠抽象理解但放到线性回归这个最简单的例子里一下子就具象了。2. 数据准备别小看这一步训练效果一半由数据决定2.1 构造一个带噪声的线性数据集线性回归的人工数据集构造非常简单核心就是要“带噪声”。如果数据是完全无噪声的完美直线模型学出来的效果会很好看但失去了验证意义——你没法判断模型是真正学到了规律还是只是在背答案。我用的生成逻辑是设定真实权重 weight 2.8设定真实偏置 bias -1.5生成 2000 个样本点特征 x 均匀分布在 -3 到 3 之间加上标准差为 0.5 的高斯噪声代码如下import torch from torch import nn from torch.utils.data import TensorDataset, DataLoader # 设定随机种子保证可复现 torch.manual_seed(42) # 生成特征和真实标签 x torch.linspace(-3, 3, 2000).reshape(-1, 1) true_w 2.8 true_b -1.5 y true_w * x true_b torch.normal(0, 0.5, sizex.shape)这里有一个细节我得单独说一下为什么要用 torch.normal 而不是直接 y x * true_w true_b如果完全不添加噪声模型会轻松学到完美贴合的结果你能观测到的损失曲线几乎是一条瞬间掉到零的竖线整个训练过程没有“渐进感”也就失去了观察学习过程的意义。加入噪声之后你才能真实感受到模型的收敛过程、损失曲线的下降趋势以及最后误差到底能压到多低——那些在实际业务中常见的“损失降不下去”的情况在人工数据集上也能提前体验到。2.2 数据集切分与批次处理数据准备好后下一步是切分训练集和测试集。我习惯按照 8:2 的比例切分并且使用 DataLoader 封装数据方便后续按批次迭代训练。# 切分数据 train_ratio 0.8 split_idx int(train_ratio * len(x)) train_x, test_x x[:split_idx], x[split_idx:] train_y, test_y y[:split_idx], y[split_idx:] # 封装为 Dataset train_dataset TensorDataset(train_x, train_y) test_dataset TensorDataset(test_x, test_y) # 创建 DataLoaderbatch_size 设为 64 train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader DataLoader(test_dataset, batch_size64, shuffleFalse)很多人第一次写的时候会忽略 shuffleTrue 这个参数。我当时也干过这事儿结果训练过程出现了一种特别诡异的“阶梯式下降”损失每走一个台阶才降一次整体收敛速度慢得离谱。原因是数据本身是从小到大排列的每个 batch 里的样本分布非常不均匀梯度方向一直在被顺序性的偏差带着走。设置 shuffleTrue 之后每个 batch 都近似于从全集里随机抽样梯度方向稳定了很多收敛速度肉眼可见地变快了。Batch size 的选择也有讲究。64 是一个很稳妥的起点太小的话梯度噪声太大收敛不稳定太大的话模型学得慢而且在小数据集上没必要。线性回归这种简单模型实际跑起来你会发现 batch size 的影响相对有限但养成“先设 64 再根据情况调”的习惯对后续复杂模型是很有用的。2.3 数据归一化到底是必需还是可选线性回归对数据尺度其实是比较敏感的尤其是在特征量纲差异很大的时候。比如一个特征范围是 0 到 1另一个特征范围是 0 到 10000模型会天然认为第二个特征更重要因为它的数值波动对输出影响更大——但这是错误的学习方向本质上是数值尺度带来的偏见不是真实的关系强度。不过如果特征就一维或者所有特征都在同一个数量级归一化这一步确实可以省掉。在这个例子里x 的范围是 -3 到 3y 的范围大概是 -10 到 8量级差异不大不归一化也完全能收敛。但如果后续你做多特征回归我强烈建议在数据进模型之前做一次标准化from sklearn.preprocessing import StandardScaler scaler_x StandardScaler() scaler_y StandardScaler() x_scaled scaler_x.fit_transform(x.numpy()) y_scaled scaler_y.fit_transform(y.numpy()) # 转换回 torch.Tensor 继续使用注意一个关键细节scaler 必须在训练集上 fit然后用同一个 scaler 去 transform 测试集。不能在整个数据集上 fit 之后再切分那样测试集的信息已经泄露出去了评估结果会虚高。这是机器学习里一个非常经典的数据泄漏陷阱在框架代码里反而容易被忽略。3. 模型搭建与训练流程从零到一的核心步骤3.1 用 nn.Module 组织模型结构Portorch 的模型基类是 nn.Module所有自定义模型都要继承它并实现 forward 方法。线性回归模型在 Portorch 里有两种写法我分别说说它们的区别。第一种是用 nn.Linear 封装好的层最省事class LinearRegression(nn.Module): def __init__(self, in_features, out_features): super().__init__() self.linear nn.Linear(in_features, out_features) def forward(self, x): return self.linear(x)第二种是手动定义权重和偏置你能更清楚地看到每个参数的结构class LinearRegressionManual(nn.Module): def __init__(self, in_features, out_features): super().__init__() self.weight nn.Parameter(torch.randn(in_features, out_features) * 0.01) self.bias nn.Parameter(torch.zeros(out_features)) def forward(self, x): return x self.weight self.bias我建议你两种写法都试一遍。第一种是日常开发最常用的写法特别是在模型规模变大之后手写参数初始化很容易出错第二种则能帮你建立“什么是参数、什么是计算图”的直觉。两个模型结构完全相同训练代码一行都不用改。关于初始化nn.Linear 内部默认的初始化方式其实已经考虑了神经网络的激活函数特性效果很稳定。而手动初始化时如果直接使用 torch.randn初始权重的绝对值可能偏大在你的优化器学习率设置不合理时容易出现训练初期损失波动的现象。我后来习惯了初始化时乘一个 0.01 的缩放因子让参数从较小的值开始。对于线性回归这种只有一个线性层的模型初始化影响不大但这个习惯到了深层网络里能帮你省很多事。3.2 损失函数与优化器的选择逻辑线性回归的默认损失函数是均方误差 MSELoss这个选择没有任何悬念。它的公式是预测值与真实值差的平方再取平均平方操作带来的效果是误差越大惩罚越重误差小的时候梯度也小模型能精准地微调参数。优化器我选了 SGD随机梯度下降学习率设为 0.01。有人可能会问为什么不直接用 Adam在这个例子里如果用 Adam损失下降确实会更快更平滑但 Adam 的自适应学习率机制会把梯度的细节“抹平”你观察不到手动调学习率时那种因为学习率设置不当而损失震荡甚至爆炸的典型现象。用 SGD 去观察学习率变化对训练的影响是理解优化器工作原理最直观的路径之一。model LinearRegression(in_features1, out_features1) loss_fn nn.MSELoss() optimizer torch.optim.SGD(model.parameters(), lr0.01)3.3 训练循环有条不紊地推进训练循环是整个流程里最关键的部分我用代码拆开来说epochs 200 train_losses [] test_losses [] for epoch in range(epochs): # 训练状态 model.train() epoch_train_loss 0.0 for batch_x, batch_y in train_loader: # 1. 梯度清零 optimizer.zero_grad() # 2. 前向传播 y_pred model(batch_x) # 3. 计算损失 loss loss_fn(y_pred, batch_y) # 4. 反向传播 loss.backward() # 5. 参数更新 optimizer.step() # 累计损失 epoch_train_loss loss.item() * batch_x.size(0) epoch_train_loss / len(train_dataset) # 评估状态 model.eval() with torch.no_grad(): test_pred model(test_x) test_loss loss_fn(test_pred, test_y).item() train_losses.append(epoch_train_loss) test_losses.append(test_loss) if (epoch 1) % 20 0: print(fEpoch [{epoch1}/{epochs}] - Train Loss: {epoch_train_loss:.4f}, Test Loss: {test_loss:.4f})这里有几个操作第一次写代码的人经常忽略但我逐个踩过坑之后才知道它们的意义。optimizer.zero_grad() 必须放在每次前向传播之前。如果漏掉这一步梯度会在不同 batch 之间不断累积梯度数值会越来越大参数更新幅度也随之越来越离谱最后模型可能直接发散。新手最常见的症状就是训练没几步loss 突然变成 nan 或者一个巨大的数字。model.eval() 加 torch.no_grad() 的组合在评估阶段是标配。没有 model.eval()某些层的行为会不一致虽然线性回归没有 Dropout 和 BatchNorm但养成这个习惯很重要没有 torch.no_grad()测试阶段也会构建计算图并计算梯度白白增加内存消耗和计算时间。这个组合本质上是在告诉框架“这里只需要前向传播算结果不需要反向传播的梯度链路。”3.4 训练结果的可视化与模型评估训练完之后验证模型好坏的方式不只是看数字。我习惯分三步验证看训练损失曲线。正常情况下loss 应该在前期快速下降然后逐渐趋于平缓。如果曲线持续大幅震荡大概率是学习率太大如果曲线下降得像蜗牛爬那就是学习率太小。对比学习到的参数与真实参数。线性回归最直接的优势在这里体现出来了你能直接看模型学出来的 weight 和 bias 怎么逼近真实值print(fLearn weight: {model.linear.weight.item():.4f}, True weight: {true_w}) print(fLearn bias: {model.linear.bias.item():.4f}, True bias: {true_b})我当时跑出来的结果大概是weight 学到的值是 2.797真实值是 2.8bias 学到的值是 -1.52真实值是 -1.5。这种“肉眼可见的正确”给了我很大的信心——如果连线性回归的参数都学不准那一定是前面哪个环节出了问题而不是模型本身的问题。可视化预测效果。把拟合直线画出来跟真实数据散点图叠在一起看。这一步是最终验证也是最有成就感的一步。拟合线穿过数据云团中间斜率、截距都与真实直线高度吻合噪声点分布两侧整个图看起来就像教科书上的示例图。4. 训练过程中的隐蔽问题与排查心得4.1 损失不降反升的排查清单这个问题我在实验过程中遇到过一次。所有代码都看起来正常但 train loss 不仅没降还在缓慢上升。我当时排查的顺序是这样的按照可能性从高到低排列学习率太大。SGD 的学习率一旦超过模型适应范围损失就会震荡甚至发散发散。把 0.01 改成 0.001 重新跑一遍立刻恢复正常。梯度累积导致步长失控。这个前面说过是 zero_grad 的问题。模型训练和评估模式搞混。eval 状态下有 no_grad 保护但如果在训练过程中调用了 eval梯度就断了参数也停更了。这三条排查完之后90% 的“损失异常”问题都能解决。4.2 损失剧烈震荡但没有发散有次我为了测试学习率的影响把 lr 调到了 0.1在训练集上跑出了锯齿形的损失曲线——整体在降但每一步之间都在上下跳动。这是因为学习率太大SGD 的参数更新幅度已经超过了损失函数的“安全区间”每一步都可能跳过最优点。这个现象本身其实很有教学价值你亲手把学习率往大了调亲眼看到损失从平滑下降变成锯齿跳动再把它调回来恢复平滑这比看任何文档都来得直观。后来我跟别人讲学习率的时候总是喜欢推荐这个实验套路——用同一个模型分别用 0.1、0.01、0.001 三档学习率去训练对比三条损失曲线一次就能理解透彻。4.3 把梯度裁剪当成备选项在小规模线性回归上梯度裁剪通常不需要。但如果在你的实际项目中遇到了梯度异常大的情况Portorch 提供了非常方便的裁剪接口# 在 loss.backward() 之后、optimizer.step() 之前调用 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)这相当于给梯度设了一个“上限绝对值”超过就按比例缩放回去。这个操作在深层网络和 RNN 中极其常用提前在简单模型上掌握它的使用位置和调用时机后面会少走很多弯路。5. 扩展从一维线性回归走向真实场景线性回归的建模流程跑通之后你完全可以自己继续往下推演把输入特征从 1 维变成多维只需要修改模型的 in_features数据侧生成多维特征就行训练代码一行都不用变。可以自己试试把特征变成 3 维真实权重设置为一定的向量生成数据做多变量回归你会发现模型照样能学到每个维度的系数——这时候你就真正理解了线性回归的本质它不过是一个加权求和的线性组合外加一个偏置项。更进一步如果想在这个骨架上加入非线性能力把线性层后面接一个激活函数就成了单层神经网络多叠加几个带激活函数的层就是多层感知机。而这一切的开始就是从线性回归模型里理解了前向传播、反向传播、参数更新这几个核心步骤的排布方式。顺带提一句Portorch 里还有一个值得关注的特性就是可以在 Dataset 的getitem里做实时数据增强。对于线性回归任务来说不需要但以后做图像分类、序列建模时你会发现数据管线这个环节和模型结构本身一样重要。6. 谈一点实操体会整套流程跑完一遍之后我最大的感受是与其说是“用 Portorch 实现线性回归”不如说是“用线性回归来理解 Portorch”。模型只是载体真正宝贵的是你对框架运行机制形成的肌肉记忆——什么时候调用 zero_grad、什么时候切 eval、为什么评估要包 no_grad、batch size 怎么影响训练动态。这些知识从任何一本书里读到的效果都远不如亲自跑完一遍代码、并看到损失曲线真实变化来得深刻。如果这篇文章你只记住一件事那我希望是调试模型别死盯着损失函数那一行把训练循环里每个操作的目的都搞清楚问题往往都出在你看似“理所当然”的代码里。把线性回归这个例子完完整整地跑通你会发现自己对深度学习框架的掌控力在短时间里有了一次明显的提升。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑