ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PyTorch线性回归实战:从梯度下降到自动求导完成模型训练

PyTorch线性回归实战:从梯度下降到自动求导完成模型训练 深度学习的入门之路最常见的卡点往往不是复杂的数学公式而是第一步就不知道怎么写一个完整的训练流程。这个系列前面几篇我把Python和PyTorch Tensor的基础扒了一遍今天这篇终于要迎来第一个真正意义上的模型训练——用PyTorch实现线性回归Linear Regression。这篇会从原理讲到代码复现把梯度下降、自动求导、模型定义这些核心概念全部串起来适合刚学完PyTorch基础、准备开始跑第一个模型的读者也适合想系统梳理一遍线性回归底层的开发者。1. 先理解Linear Regression为什么它是一切深度学习的入口线性回归在深度学习里就是Hello World级别的存在。它简单到可以用一张图说清楚但深度学习里几乎所有核心组件——数据、模型、损失函数、优化器、训练循环——都能在它身上找到对应的影子。线性回归的任务说白了就是给你一堆数据点这些点大概落在一个直线附近或者平面附近你去找一条直线或者超平面来拟合它们。比如预测房价和面积的关系、预测广告投入和销量的关系都属于这个范畴。一个最简单的单变量线性回归可以写成y wx bw是斜率b是偏置。我们要做的就是根据数据去找到最合适的w和b让这条直线尽可能贴近所有的数据点。尽可能贴近怎么度量这就引出了损失函数Loss Function。深度学习的训练过程可以抽象成四个步骤前向传播把数据喂给模型算出预测值。计算损失比较预测值和真实值的差距。反向传播算出每个参数对损失的贡献也就是梯度。更新参数沿着梯度方向调小参数让损失降低。线性回归把这四步全部走了一遍。只不过它的模型只是一个简单的线性函数而已。当你把这个流程跑通之后后面换成卷积神经网络、循环神经网络、Transformer框架层面几乎是一模一样的套路只是模型结构变得更复杂了。这里有一个很重要的认知深度学习并不是什么高深莫测的黑魔法它本质上就是一个不断调整参数让损失变小的过程。线性回归恰好是这个过程最透明、最容易被理解的一个载体。顺便说一句网上经常有初学者把回归和分类搞混。回归输出的是连续值比如预测价格、温度、得分分类输出的是离散标签比如猫狗识别、垃圾邮件判断。线性回归解决的是回归问题这是它的边界。2. PyTorch环境搭建与安装验证学PyTorch之前先把环境搞定不然后面写代码跑不动会很痛苦而且很多报错其实都是环境引起的和代码本身没关系。我自己在本地实践的时候最推荐的方式是用Anaconda创建一个独立的虚拟环境然后再装PyTorch。这样做的好处是环境隔离——你给别人写demo、或者过几个月再回来跑老项目都不会因为Python库版本冲突而崩溃。创建一个新环境conda create -n dl_basic python3.10 conda activate dl_basicPython版本建议选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封装好的APInn.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, sizex.shape)这里的np.random.normal(0, 0.3, sizex.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)其中lrlearning_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(fEpoch {epoch:3d}, loss{loss:.6f}, w{w:.4f}, b{b:.4f})跑完之后你会发现w会慢慢逼近2b会慢慢逼近3loss在逐步下降。这个过程就是完整的训练。你可能注意到这里w和b是从0开始初始化的。但是梯度下降的初始值设成多少其实会直接影响收敛速度。比如一开始的预测值离真实值差很远loss非常大梯度也非常大前面的几步参数会跳得很厉害。一个更合理的做法是小范围内随机初始化不过在线性回归这个例子里从0开始也没毛病因为MSE的损失曲面是凸的只有一个全局最小值怎么初始化都能收敛到同一个地方。这也是线性回归适合入门的重要原因——你不用太操心初始化这种在深度学习中让人头大的问题。3.4 学习率怎么选在学习率这个问题上我踩过的坑不算少。太小的学习率比如0.0001训练100轮下来w和b几乎没怎么动损失曲线下降得跟蜗牛一样。太大的学习率比如10loss不但不降反而会爆涨甚至变成NaN。我习惯的做法是先试一个中间值0.01把loss曲线打出来看一眼。如果下降太慢就调大一点如果震荡就调小一点。线性回归这种简单模型对学习率不太敏感但到后面训练神经网络学习率会是第一个需要重点调试的超参数。4. PyTorch autograd把梯度计算交给框架numpy版本的线性回归跑通之后你已经理解了训练的全部流程。现在可以引入PyTorch了。PyTorch和numpy最核心的区别在于autograd机制——自动求导。手动求导在简单模型里还能忍受一旦模型复杂起来比如50层的神经网络你要手动推导每一层的梯度公式那基本是不可能完成的任务。PyTorch的做法是在每次前向传播的时候自动构建一张计算图记录下张量之间的运算关系。当你调用backward()梯度就会按照计算图自动反向传播到每个叶子节点上。4.1 requires_grad和backward在PyTorch中如果一个张量设置了requires_gradTrue那么所有基于它的运算都会被追踪用于后续的梯度计算。import torch w torch.tensor(2.0, requires_gradTrue) b torch.tensor(1.0, requires_gradTrue) x torch.tensor(3.0) y w * x b y.backward() print(w.grad) # tensor(3.) print(b.grad) # tensor(1.)输出结果是w.grad3、b.grad1这正好就是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, sizex.shape) w torch.tensor(0.0, requires_gradTrue) b torch.tensor(0.0, requires_gradTrue) 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(fEpoch {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_features1, out_features1)这里的in_features1和out_features1表示输入是一维的输出也是一维的。换句话说模型就是一个单变量的线性函数y wx b。真正的深度学习代码模型定义也遵循同样的模式——只是Linear层会更多、更大。5.2 选择损失函数和优化器损失函数我们用nn.MSELoss()这就是前面手写的均方误差只不过封装好了。优化器用optim.SGD(model.parameters(), lr0.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(), lr0.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(fEpoch {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.0bias大约接近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(), s10, labeltrue data) plt.plot(x.numpy(), model(x).numpy(), colorred, labelprediction) 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 对应的学习路径学完线性回归之后接下来自然的顺序是逻辑回归Logistic Regression——线性模型做分类理解Sigmoid和交叉熵。多层感知机MLP——理解隐藏层、激活函数和非线性表达。优化器进阶——从SGD到Adam理解动量、自适应学习率。卷积神经网络——开始接触图像数据。循环神经网络——接触序列数据。每一步都是站在前一步的基础上。而这个系列的下一个主题大概率会是逻辑回归或者多层感知机。等我写出来之后再回来补齐链接。先到这里。如果你跟着把这篇文章的内容亲手敲完、跑通了你对PyTorch训练流程的掌握就已经超过了相当一部分人。接下来要做的就是把那个五行训练循环刻进脑子里然后往里面换不同的模型、不同的数据你的深度学习之路就正式开始了。
返回列表