☰
PyTorch线性回归实战:从梯度下降到自动求导完成模型训练
2026/9/29 2:37:16 网站建设 项目流程

深度学习的入门之路,最常见的卡点往往不是复杂的数学公式,而是第一步就不知道怎么写一个完整的训练流程。这个系列前面几篇我把Python和PyTorch Tensor的基础扒了一遍,今天这篇终于要迎来第一个真正意义上的模型训练——用PyTorch实现线性回归(Linear Regression)。这篇会从原理讲到代码复现,把梯度下降、自动求导、模型定义这些核心概念全部串起来,适合刚学完PyTorch基础、准备开始跑第一个模型的读者,也适合想系统梳理一遍线性回归底层的开发者。

1. 先理解Linear Regression:为什么它是一切深度学习的入口

线性回归在深度学习里就是"Hello World"级别的存在。它简单到可以用一张图说清楚,但深度学习里几乎所有核心组件——数据、模型、损失函数、优化器、训练循环——都能在它身上找到对应的影子。

线性回归的任务说白了就是:给你一堆数据点,这些点大概落在一个直线附近(或者平面附近),你去找一条直线(或者超平面)来拟合它们。比如预测房价和面积的关系、预测广告投入和销量的关系,都属于这个范畴。

一个最简单的单变量线性回归可以写成:

y = wx + b

w是斜率,b是偏置。我们要做的,就是根据数据去找到最合适的w和b,让这条直线尽可能贴近所有的数据点。"尽可能贴近"怎么度量?这就引出了损失函数(Loss Function)。

深度学习的训练过程可以抽象成四个步骤:

  1. 前向传播:把数据喂给模型,算出预测值。
  2. 计算损失:比较预测值和真实值的差距。
  3. 反向传播:算出每个参数对损失的贡献,也就是梯度。
  4. 更新参数:沿着梯度方向调小参数,让损失降低。

线性回归把这四步全部走了一遍。只不过它的模型只是一个简单的线性函数而已。当你把这个流程跑通之后,后面换成卷积神经网络、循环神经网络、Transformer,框架层面几乎是一模一样的套路,只是模型结构变得更复杂了。

这里有一个很重要的认知:深度学习并不是什么高深莫测的黑魔法,它本质上就是一个不断调整参数让损失变小的过程。线性回归恰好是这个过程最透明、最容易被理解的一个载体。

顺便说一句,网上经常有初学者把"回归"和"分类"搞混。回归输出的是连续值,比如预测价格、温度、得分;分类输出的是离散标签,比如猫狗识别、垃圾邮件判断。线性回归解决的是回归问题,这是它的边界。

2. PyTorch环境搭建与安装验证

学PyTorch之前,先把环境搞定,不然后面写代码跑不动会很痛苦,而且很多报错其实都是环境引起的,和代码本身没关系。

我自己在本地实践的时候,最推荐的方式是用Anaconda创建一个独立的虚拟环境,然后再装PyTorch。这样做的好处是环境隔离——你给别人写demo、或者过几个月再回来跑老项目,都不会因为Python库版本冲突而崩溃。

创建一个新环境:

conda create -n dl_basic python=3.10 conda activate dl_basic

Python版本建议选3.9或者3.10,这两个版本是目前PyTorch适配最稳的区间。有些老教程让你装3.7或者3.8,没什么必要,新库对老Python的支持反而在慢慢变弱。

接下来装PyTorch。很多人第一次装这块就被GPU、CUDA这些概念劝退了。我给个最简单的原则:如果你电脑里有NVIDIA独立显卡,就装GPU版;如果没有,就装CPU版。CPU版足够学完整个基础系列,线性回归、全连接网络这种规模的计算,CPU完全扛得住。

CPU版安装命令最简单:

pip install torch torchvision torchaudio

这样默认装的是CPU版本,安装完成后可以用下面这行代码验证:

import torch print(torch.__version__) x = torch.tensor([1.0, 2.0]) print(x)

如果能正常打印出版本号和tensor,说明环境已经通了。

如果你的显卡是NVIDIA的,想用GPU加速,装完之后需要验证CUDA是否可用:

print(torch.cuda.is_available())

这个输出如果是False,常见原因是显卡驱动太老,或者PyTorch版本和CUDA版本不匹配。这时候去PyTorch官网的Get Started页面,根据自己的系统选对应的安装命令就行。官网上会给出不同CUDA版本对应的安装指令,照着复制粘贴,比任何第三方教程都靠谱。

我遇到过很多次这种问题:用户用pip装了个CPU版本,然后又跑GPU代码,结果一直报错说CUDA不可用。这种问题的根源就是安装的时候没分清CPU和GPU版本。记住:你想用GPU,安装包必须是GPU版;CPU版是没法靠设置开启CUDA的。

装完环境,接下来就进入正题写代码了。

3. 用numpy先跑通线性回归:理解梯度下降的本质

很多教程直接让你用PyTorch封装好的API,nn.Linear一拉,optimizer.step()一调,训练就完成了。代码是跑通了,但里面到底发生了什么,很多人一脸懵。

我的建议是:先不要用任何深度学习框架,用纯numpy把线性回归手写一遍。这个过程能让你真正理解梯度下降到底是怎么让损失降下来的。

3.1 造一份模拟数据

先用numpy生成一批带噪声的数据。假设真实的函数是 y = 2x + 3,我们给它加上一些随机扰动,让它看起来像"现实中的数据":

import numpy as np np.random.seed(42) x = np.linspace(0, 2, 100) true_w = 2.0 true_b = 3.0 y = true_w * x + true_b + np.random.normal(0, 0.3, size=x.shape)

这里的np.random.normal(0, 0.3, size=x.shape)是给数据加高斯噪声。为什么要加噪声?因为现实世界采集到的数据从来不可能是完美的直线,总会因为各种因素产生误差。深度学习模型要学的,就是从带噪声的数据里把背后的规律揪出来。

3.2 定义损失函数

损失函数我们选均方误差(Mean Squared Error,简称MSE),它的公式是:

L = (1/N) * Σ(y_pred - y_true)²

为什么用均方误差而不是绝对误差?核心原因是MSE是可导的,且梯度大小与误差成正比。误差大的样本会贡献更大的梯度,模型会优先修正那些错得离谱的预测。这种特性让MSE在优化过程中表现得比较稳定。

用numpy实现非常简单:

def mse_loss(y_true, y_pred): return np.mean((y_true - y_pred) ** 2)

3.3 手写梯度下降

梯度下降的思路可以用爬山来类比。想象你在山谷里,一片迷雾看不清路,你要往下走(让损失变小),你能做的就是感受脚下的坡度,然后往最陡的下坡方向迈一步。在数学里,这个"最陡的下坡方向"就是梯度的反方向。

我们的损失函数是L(w, b),对w求偏导、对b求偏导,就能得到这两个方向上的坡度。然后沿着反方向更新参数:

w -= lr * (∂L/∂w) b -= lr * (∂L/∂b)

其中lr(learning_rate)是学习率,决定每一步迈多大。

对线性回归的MSE损失求导,结果是:

∂L/∂w = (2/N) * Σ((y_pred - y_true) * x) ∂L/∂b = (2/N) * Σ(y_pred - y_true)

完整的训练代码如下:

w = 0.0 b = 0.0 lr = 0.1 epochs = 100 for epoch in range(epochs): y_pred = w * x + b dw = 2 * np.mean((y_pred - y) * x) db = 2 * np.mean(y_pred - y) w -= lr * dw b -= lr * db if epoch % 10 == 0: loss = mse_loss(y, y_pred) print(f"Epoch {epoch:3d}, loss={loss:.6f}, w={w:.4f}, b={b:.4f}")

跑完之后你会发现,w会慢慢逼近2,b会慢慢逼近3,loss在逐步下降。这个过程就是完整的"训练"。

你可能注意到这里w和b是从0开始初始化的。但是梯度下降的初始值设成多少,其实会直接影响收敛速度。比如一开始的预测值离真实值差很远,loss非常大,梯度也非常大,前面的几步参数会跳得很厉害。一个更合理的做法是小范围内随机初始化,不过在线性回归这个例子里从0开始也没毛病,因为MSE的损失曲面是凸的,只有一个全局最小值,怎么初始化都能收敛到同一个地方。这也是线性回归适合入门的重要原因——你不用太操心初始化这种在深度学习中让人头大的问题。

3.4 学习率怎么选

在学习率这个问题上,我踩过的坑不算少。太小的学习率,比如0.0001,训练100轮下来w和b几乎没怎么动,损失曲线下降得跟蜗牛一样。太大的学习率,比如10,loss不但不降反而会爆涨,甚至变成NaN。

我习惯的做法是先试一个中间值0.01,把loss曲线打出来看一眼。如果下降太慢就调大一点,如果震荡就调小一点。线性回归这种简单模型对学习率不太敏感,但到后面训练神经网络,学习率会是第一个需要重点调试的超参数。

4. PyTorch autograd:把梯度计算交给框架

numpy版本的线性回归跑通之后,你已经理解了训练的全部流程。现在可以引入PyTorch了。PyTorch和numpy最核心的区别在于autograd机制——自动求导。

手动求导在简单模型里还能忍受,一旦模型复杂起来,比如50层的神经网络,你要手动推导每一层的梯度公式,那基本是不可能完成的任务。PyTorch的做法是,在每次前向传播的时候,自动构建一张计算图,记录下张量之间的运算关系。当你调用backward(),梯度就会按照计算图自动反向传播到每个叶子节点上。

4.1 requires_grad和backward

在PyTorch中,如果一个张量设置了requires_grad=True,那么所有基于它的运算都会被追踪,用于后续的梯度计算。

import torch w = torch.tensor(2.0, requires_grad=True) b = torch.tensor(1.0, requires_grad=True) x = torch.tensor(3.0) y = w * x + b y.backward() print(w.grad) # tensor(3.) print(b.grad) # tensor(1.)

输出结果是w.grad=3、b.grad=1,这正好就是y对w和b的偏导。你看,PyTorch自己就把梯度算出来了,完全不需要你手动推导。

这个机制的底层是通过动态计算图实现的,也就是每次前向传播都会重新构建图。对比静态图框架,动态图的优势是写代码更灵活——你可以在循环里随时改变网络结构,用Python的if、for来控制计算流程,排错也更容易直观。这也是PyTorch在科研社区越来越流行的重要原因。

4.2 完整的PyTorch版训练循环

PyTorch版的线性回归训练代码如下:

import torch torch.manual_seed(42) x = torch.linspace(0, 2, 100) true_w = torch.tensor(2.0) true_b = torch.tensor(3.0) y = true_w * x + true_b + torch.normal(0, 0.3, size=x.shape) w = torch.tensor(0.0, requires_grad=True) b = torch.tensor(0.0, requires_grad=True) lr = 0.1 epochs = 100 for epoch in range(epochs): y_pred = w * x + b loss = torch.mean((y_pred - y) ** 2) loss.backward() with torch.no_grad(): w -= lr * w.grad b -= lr * b.grad w.grad.zero_() b.grad.zero_() if epoch % 20 == 0: print(f"Epoch {epoch:3d}, loss={loss.item():.6f}")

这里有两个关键细节必须理解。

第一个是with torch.no_grad():。w -= lr * w.grad这个操作本身也是一个运算,如果我们不做包裹,PyTorch会把这一操作也加入计算图,导致计算图越积越大,内存消耗无谓暴涨。no_grad()告诉PyTorch,这里的操作不需要追踪梯度。在深度学习里,这是被反复使用的基础写法,不只是线性回归里用,几乎所有参数更新都这么写。

第二个是w.grad.zero_()。这一步是为了手动清空上一次反向传播留下的梯度。如果不调用zero_,梯度会不断累加。这在显存和数值上都会造成严重问题。记住这个经验:每个batch做一次反向传播后,一定记得把梯度清零。后来你用到optimizer.zero_grad(),本质还是同一个动作,只不过放到了优化器里统一封装。

5. 用nn.Module写出标准线性回归训练代码

numpy手写和手动参数更新,是让你建立直觉的必经之路。但真正在项目里写代码,不会这么原始。PyTorch提供了高度封装好的API,让我们能用更少的代码、更不容易出错的方式完成同样的任务。这一节的内容,就是深度学习最标准的代码模板。

5.1 使用nn.Linear定义模型

torch.nn.Linear是PyTorch里最常用的层之一。它内部已经帮我们定义了权重矩阵W和偏置b,并且默认随机初始化。

import torch import torch.nn as nn import torch.optim as optim model = nn.Linear(in_features=1, out_features=1)

这里的in_features=1和out_features=1表示输入是一维的,输出也是一维的。换句话说,模型就是一个单变量的线性函数y = wx + b。真正的深度学习代码,模型定义也遵循同样的模式——只是Linear层会更多、更大。

5.2 选择损失函数和优化器

损失函数我们用nn.MSELoss(),这就是前面手写的均方误差,只不过封装好了。

优化器用optim.SGD(model.parameters(), lr=0.1)。SGD就是随机梯度下降。你可能好奇为什么叫"随机",因为传统梯度下降要用全部数据算一次梯度(也叫批量梯度下降),数据量一大效率极低;随机梯度下降每次只用一个小批量的数据来估算梯度,效率高很多。虽然引入了随机噪声,但绝大多数情况下都能高效收敛。

5.3 数据批处理

这一节我们先不引入DataLoader,但需要理解数据是怎么组织的。标准的训练数据集要构造成一个Tensor,形状是(样本数, 特征数)。我们把之前的x从形状(100,)变成(100, 1):

x = x.unsqueeze(1) # 形状从(100,)变为(100, 1) y = y.unsqueeze(1) # 同样处理

为什么要做这一步?因为nn.Linear期望输入是一个二维批量数据:第一维是batch size(一批有多少个样本),第二维是feature(每个样本有几个特征)。这是PyTorch几乎所有模型层都遵循的约定,后面学CNN、RNN也逃不开这个形状约定。

5.4 训练循环的标准模板

下面就是深度学习里最经典的训练循环代码,几乎所有项目(无论多复杂)都是这个骨架的变体:

model = nn.Linear(1, 1) criterion = nn.MSELoss() optimizer = optim.SGD(model.parameters(), lr=0.1) epochs = 200 for epoch in range(epochs): y_pred = model(x) loss = criterion(y_pred, y) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 20 == 0: print(f"Epoch {epoch:3d}, loss={loss.item():.6f}")

让我把每一行拆开解释一下为什么是它:

  • y_pred = model(x):前向传播。模型内部计算的是x @ W + b,注意这里的W形状是(1, 1),b是一个标量。
  • loss = criterion(y_pred, y):计算当前批量数据的损失。
  • optimizer.zero_grad():清空上一轮残留的梯度。漏掉这行几乎是新手第一高频报错——loss曲线会很奇怪地不规则震荡。
  • loss.backward():反向传播,计算当前参数的梯度。
  • optimizer.step():根据优化器策略更新参数。这一步做的事情,跟我前面手写的w -= lr * w.grad是完全一样的,SGD就是这么简单直接。

这五行代码,就是整个深度学习的发动机。等到后面换成任何复杂模型,比如一个ResNet或者一个Transformer,你会发现在最外层看,训练循环仍然是这五行。

5.5 从结果里读信息

跑完训练之后,可以打印一下学到的参数:

for name, param in model.named_parameters(): print(f"{name}: {param.data.item():.4f}")

你会看到weight大约接近2.0,bias大约接近3.0——跟真实值匹配。这说明模型真的从噪声数据中学到了规律。

顺带提一句:因为初始化和随机噪声,每次运行的结果会有细微差异,这是正常现象。如果你想固定随机种子让结果可重复,就在训练前加torch.manual_seed(42)。

6. 训练过程中的常见问题与调试思路

这部分内容可能比前面的代码更需要你收藏。我把自己在训练线性回归时踩过的坑、以及后来在更复杂模型里也反复出现的同类问题,集中列成了一份调试清单。

6.1 loss变成NaN

最常见的原因是学习率过大,导致参数更新越过最优值,loss不断膨胀到溢出。解决思路就是调小学习率,比如从0.1改成0.01、0.001。

第二个常见原因是数据本身有问题,比如输入数据有NaN或者无穷大。有时候数据预处理遗漏了这一步,数据里混进了脏数据,模型一算梯度就爆了。

第三种情况跟显卡有关——GPU训练时并不常见,但如果是梯度下降过程中某些中间数值超过了float的表示范围,也会出现NaN。线性回归还不太可能遇到这种情况,但一旦到了深层神经网络,梯度爆炸导致NaN就会变成家常便饭。

6.2 loss下降但很慢

如果你的loss曲线是在往下走,但速度慢得像蜗牛,大概率是学习率太小了。另一种情况是数据没有做归一化。

归一化(Normalization)是一个很容易被新手忽略的操作。假设你的特征x取值范围是0到10000,那么MSE算出来的梯度可能会非常大(或者非常小),整个训练过程会很敏感、很不稳定。把x压到0附近(比如[x - mean] / std),梯度就正常了。这个技巧在深度学习里极其重要。

6.3 预测结果整体偏差

如果权重学得很好,但bias明显偏离真实值,可以检查一下是不是训练数据不够均匀。比如你的x只在0到0.1之间取了100个点,那么模型对于更远处的数据点完全没有感知,bias的推断就很容易跑偏。

在实际业务中还有一种情况更隐蔽:如果数据的采集方式本身有系统性偏差,比如券商的数据只记录了盈利客户的交易记录,那你训练出来的模型天然就带偏差,这不是超参能解决的。

6.4 关于评估指标

训练结束后,光看loss还不够。我习惯把原始数据、预测结果的直线画在一张图里,直观判断拟合效果。如下图(这里不贴图了,你们自己用matplotlib画一下就行):

import matplotlib.pyplot as plt with torch.no_grad(): plt.scatter(x.numpy(), y.numpy(), s=10, label="true data") plt.plot(x.numpy(), model(x).numpy(), color="red", label="prediction") plt.legend() plt.show()

拟合直线如果大致穿过数据中心区域,两端的偏差均匀分布,效果就是可以的。

7. 从线性回归到深度学习的自然过渡

到这里,线性回归的核心内容已经全讲完了。但按照这个系列的定位,不能停在这里,还要带大家看一眼下一步的路。

7.1 从linear到一层神经网络

线性回归其实就是一个没有激活函数的单层神经网络。如果给线性变换加上一个非线性激活函数,比如ReLU或Sigmoid,它就不再是"回归直线"了,而是一个能拟合曲线的基本神经元。

这一小步就是质变。多个非线性神经元组合起来,就可以拟合任意复杂的函数。这就是神经网络"万能逼近"的直觉解释。

PyTorch实现一个带激活函数的"单层网络"很简单:

model = nn.Sequential( nn.Linear(1, 16), nn.ReLU(), nn.Linear(16, 1) )

你立刻就能发现,这个模型拟合曲线的能力比直线强太多了。训练循环几乎不用改——损失函数、优化器、backward()、step(),全都是前面那套模板。

7.2 把线性回归作为"基线模型"

在实际做深度学习项目的时候,我特别建议先跑一个线性模型作为基线(baseline)。什么意思?就是不管你要做什么样复杂的预测,先上最简单的线性模型,看它能达到什么水平。假如简单线性模型已经能到90%的精度,那你费老大劲搭一个深度网络提升到91%,就要反思一下投入产出比了。

反向也很有用:如果线性模型的训练过程都不收敛,那几乎可以肯定问题出在数据上(脏数据、缺失值、尺度不一),而不是模型结构不够高级。先用简单模型排查数据问题,再用复杂模型提升效果,这个顺序能帮你省掉大量无效时间。

7.3 对应的学习路径

学完线性回归之后,接下来自然的顺序是:

  1. 逻辑回归(Logistic Regression)——线性模型做分类,理解Sigmoid和交叉熵。
  2. 多层感知机(MLP)——理解隐藏层、激活函数和非线性表达。
  3. 优化器进阶——从SGD到Adam,理解动量、自适应学习率。
  4. 卷积神经网络——开始接触图像数据。
  5. 循环神经网络——接触序列数据。

每一步都是站在前一步的基础上。而这个系列的下一个主题,大概率会是逻辑回归或者多层感知机。等我写出来之后,再回来补齐链接。

先到这里。如果你跟着把这篇文章的内容亲手敲完、跑通了,你对PyTorch训练流程的掌握就已经超过了相当一部分人。接下来要做的,就是把那个五行训练循环刻进脑子里,然后往里面换不同的模型、不同的数据,你的深度学习之路就正式开始了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询