ARTICLE DETAIL

资讯详情

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

PyTorch逻辑斯蒂回归实战:从原理到代码的完整指南

PyTorch逻辑斯蒂回归实战:从原理到代码的完整指南 在深度学习里逻辑斯蒂回归往往是最容易被低估的一个模型。很多人觉得它就是线性回归加了个sigmoid没什么值得深究的实际上这个看似简单的模型几乎是所有现代分类网络的起点。我在实际带项目、带新人时发现凡是能把逻辑斯蒂回归的细节想透的人后面理解卷积网络、Transformer里的各种结构都会顺畅得多。这篇文章就围绕PyTorch里逻辑斯蒂回归的完整落地过程把原理、代码、训练细节和踩坑记录一次讲透希望对正在学深度学习的你有所帮助。1. 逻辑斯蒂回归先搞懂一个问题分类和回归差在哪1.1 回归输出连续值分类输出的是概率很多初学者第一次接触逻辑斯蒂回归时心里都会冒出一个疑问名字里带着“回归”两个字为什么干的是分类的事原因很简单历史命名习惯而已——它本质上解决的是分类问题但因为推导过程沿用了线性回归的框架所以就沿用了这个名字。回归任务的输出是连续数值比如预测房价、预测气温模型最后一层是一个线性输出得到的值可以是任意实数。分类任务则不一样模型要回答的是“这张图是不是猫”“这个用户会不会流失”这类离散问题。你当然可以让模型输出一个实数值然后自己定一个阈值来判断但这样做有一个很大的隐患不同样本的得分尺度不一致阈值很难选得合理。更科学的做法是把模型输出限制在0到1之间让这个值可以当作概率来解释。逻辑斯蒂回归做的事情就是在线性变换 wxb 的基础上再接一个sigmoid函数把任意实数压缩到(0,1)区间。这个压缩后的值就可以视为正类的概率。如果概率大于0.5判为正类反之判为负类。这个决策方式非常直观而且后续还能通过调整阈值来平衡精确率和召回率灵活性很高。1.2 sigmoid函数为什么长这样sigmoid函数的公式是 σ(z) 1 / (1 e^(-z))。有人会问能压缩到0到1之间的函数有很多比如分段函数、tanh为什么偏偏选sigmoid这里有两个核心原因。第一sigmoid是光滑可导的。深度学习靠梯度反向传播更新参数如果激活函数处处可导、导数又不恒为0梯度就能稳定传递。分段函数在拐点处不可导实际使用会有麻烦。第二sigmoid的导数形式非常优雅。可以自己推一下σ(z) σ(z) * (1 - σ(z))。也就是说前向计算得到概率 p反向计算梯度时只需要用到 p连额外的浮点运算都不需要。这在算力紧张的时期是非常大的优势也正因为这个性质sigmoid成为逻辑斯蒂回归最自然的配套激活函数。还有一个容易被忽略的点sigmoid让模型的输出天然带了概率解释但这种解释必须搭配相应的损失函数才成立也就是接下来要说的交叉熵。如果还继续用均方误差MSE那sigmoid的输出就只是一个普通的0到1数值并不具备严格的概率意义。这一点很多教程没有讲透导致不少人掉进坑里。2. PyTorch环境准备别让安装问题毁了一整天2.1 conda环境、CUDA版本与PyTorch的匹配逻辑斯蒂回归本身对算力要求不高CPU就能跑但既然要学PyTorch环境迟早要对接GPU。环境的坑我在实际教人过程中见得太多了这里把最常见的几个问题一次性说清楚。首先说conda环境。很多人在Windows终端里输入 conda activate pytorch 时系统报错说“无法将conda项识别为cmdlet、函数、脚本文件或可运行程序的名称”。这个错误几乎都是因为conda没有加到系统环境变量的PATH中。解决方法是打开Anaconda Prompt而不是普通PowerShell窗口或者手动把Anaconda的Scripts目录加到环境变量。我自己的习惯是直接在Anaconda Prompt里管理环境省心不会跟系统自带的Python打架。再说CUDA版本。PyTorch每个版本都有对应的CUDA预编译包比如 cu118 对应CUDA 11.8cu121对应12.1cu128对应12.8。安装PyTorch时不是CUDA版本越新越好而是要看你的显卡驱动支持哪个版本。你可以在终端敲 nvidia-smi 查看驱动版本再对照驱动支持的CUDA版本然后去PyTorch官网挑选对应的安装命令。如果只做逻辑斯蒂回归这类小模型其实可以选择CPU版本等真正训练大模型再上GPU这样初学阶段的复杂度会低很多。2.2 可复现的固定路径从创建环境到验证安装我比较推荐的一套固定操作是这样的conda create -n pytorch python3.11 conda activate pytorch # CPU版本适合学习、调试 pip install torch torchvision torchaudio # GPU版本以CUDA 12.1为例 conda install pytorch torchvision torchaudio pytorch-cuda12.1 -c pytorch -c nvidia装完以后一定要做一次快速验证不要急着写模型。新建一个Python文件输入以下代码import torch x torch.randn(3, 4) print(PyTorch version:, torch.__version__) print(CUDA available:, torch.cuda.is_available()) if torch.cuda.is_available(): print(CUDA device:, torch.cuda.get_device_name(0)) y torch.randn(3, 4).cuda() print(GPU tensor compute OK:, y)如果CUDA可用说明GPU链路通了如果CUDA is available 显示False且你确实需要GPU十有八九是安装包选错了CUDA版本或者驱动版本太老。这个验证步骤几十秒的事能帮你省下一整天的排查时间。顺带说一句初次接触WSL环境的人同样先跑这段验证代码比对着各种博客翻半天更快。3. 代码实现从零训练一个逻辑斯蒂回归模型3.1 准备一份二分类数据集动手写模型前先得有一份能用的数据。这里我用PyTorch自带的make_blobs思路来做演示但为了不引入额外依赖直接用sklearn生成一份二分类的模拟数据也可以完全手工生成高斯分布样本。我就用sklearn写代码简短二维数据还能可视化。import torch import torch.nn as nn import numpy as np from sklearn.datasets import make_blobs from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 生成两类样本每类500个特征为2维 X, y make_blobs(n_samples1000, centers2, n_features2, random_state42) X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) # 转成Tensor注意y要转成float因为BCE损失要求输出和标签都是浮点类型 X_train torch.tensor(X_train, dtypetorch.float32) y_train torch.tensor(y_train, dtypetorch.float32) X_test torch.tensor(X_test, dtypetorch.float32) y_test torch.tensor(y_test, dtypetorch.float32)这里有几个细节值得注意。第一特征维度是2目的是方便画决策边界也方便初学者直观理解模型学到了什么。第二数据标准化很重要。如果不做标准化特征的量纲差异会直接影响线性层的权重初始化导致loss曲线要么收敛极慢要么直接发散。实际项目中尤其是带数值型特征的表征学习场景标准化几乎是标配。3.2 模型定义继承nn.Module是标准姿势在PyTorch里定义模型最基本的方式是继承nn.Module类在__init__中定义网络层在forward中定义前向计算逻辑。逻辑斯蒂回归和线性回归的模型结构几乎一样区别只在于forward里多了一个sigmoid。class LogisticRegression(nn.Module): def __init__(self, n_features): super().__init__() self.linear nn.Linear(n_features, 1) def forward(self, x): out self.linear(x) out torch.sigmoid(out) return out这个模型非常简单输入两个特征经过一个线性层输出一个1维的实数再接sigmoid变成0到1之间的概率。你也可以不写sigmoid直接在训练时用nn.BCEWithLogitsLoss它把sigmoid和交叉熵合并在一起数值上更稳定。两种方式各有利弊我建议初学阶段显式写sigmoid能加深理解等熟练以后再用BCEWithLogitsLoss。其实这里还藏着一个深层问题为什么要定义成一个类而不是直接用一个函数原因在于PyTorch的nn.Module帮我们做了很多底层事情——参数注册、自动反向传播、设备迁移.to(device)、训练/评估模式切换等。这些能力在逻辑斯蒂回归这种小模型上体现不明显但换到ResNet、Transformer这类大网络时你会发现没有这套类的封装代码根本没法维护。所以从入门第一天就保持这个写法是很划算的。3.3 训练循环损失函数、优化器、迭代更新模型有了数据有了接下来就是训练。逻辑斯蒂回归使用的标准损失函数是二元交叉熵BCELoss。在二分类场景下公式可以写成loss - [ y * log(p) (1 - y) * log(1 - p) ]这个公式的直觉是当真实标签 y1 时模型预测的 p 越接近1损失越小当 y0 时模型预测的 p 越接近0损失越小。从信息论角度说它衡量的是模型对真实标签分布的编码代价代价越小说明模型拟合得越好。优化器我首选SGD或Adam。逻辑斯蒂回归是个凸优化问题SGD就能稳定收敛而且收敛路径更容易理解Adam在工程上更省心自适应学习率能省去不少调参麻烦。我个人的教学建议是跑逻辑斯蒂回归用SGD把学习率理解透等后面跑了更复杂的模型再换成Adam。训练循环代码如下model LogisticRegression(n_features2) criterion nn.BCELoss() optimizer torch.optim.SGD(model.parameters(), lr0.1) epochs 500 for epoch in range(epochs): model.train() optimizer.zero_grad() outputs model(X_train).squeeze() loss criterion(outputs, y_train) loss.backward() optimizer.step() if (epoch 1) % 50 0: with torch.no_grad(): test_outputs model(X_test).squeeze() test_loss criterion(test_outputs, y_test) pred_labels (test_outputs 0.5).float() acc (pred_labels y_test).float().mean() print(fEpoch [{epoch1}/{epochs}], Loss: {loss.item():.4f}, Test Acc: {acc.item():.4f})每次迭代都要执行optimizer.zero_grad()这一步经常有人忘记写结果就是梯度不断累加loss和参数都会乱套。用PyTorch写代码时只要遇到loss不降反升、或者loss曲线剧烈震荡第一反应就应该检查是不是忘了清零梯度。在测试阶段用torch.no_grad()包裹是为了告诉autograd引擎不需要记录梯度这样既能省内存也能让推理速度更快。注意模型评估时输出的概率要跟阈值0.5比较才能得到预测标签。这个阈值可以根据业务需求调整比如医疗筛查场景宁可误报也不能漏报就会把阈值调低一些。4. 训练效果评估loss曲线与决策边界4.1 什么时候算训好了很多初学者盯着loss数字看到0.4就以为模型不行看到0.01就觉得完美。经验上逻辑斯蒂回归在简单二分类任务上训练到loss稳定、验证集准确率不再明显变化时就可以停下来了。通常几百个epoch就足够再继续训练也只是在拟合噪声。判断模型是否收敛最直接的方法是打印训练loss和验证集指标。比我前面那段代码里每隔50个epoch打印一次就是为了观察这个趋势。如果训练loss持续下降但验证集准确率停滞甚至下降说明模型已经过拟合了这时候可以试试减少训练轮数、加大数据量或者加L2正则化也就是在损失函数里加权重衰减项。在PyTorch的SGD优化器中直接设置weight_decay参数即可。4.2 画一条决策边界帮助理解逻辑斯蒂回归学出来的模型在特征空间里实际上是一条决策边界。因为模型是线性的所以边界是一条直线如果特征维度更高边界就是一个超平面。画出这条线能非常直观地看到模型是怎么对样本分类的。import matplotlib.pyplot as plt import numpy as np with torch.no_grad(): weight model.linear.weight.numpy().flatten() bias model.linear.bias.numpy().item() # 决策边界w1 * x1 w2 * x2 b 0 x1_vals np.linspace(X_train[:, 0].min(), X_train[:, 0].max(), 200) x2_vals -(weight[0] * x1_vals bias) / weight[1] plt.figure(figsize(6, 6)) plt.scatter(X_train[:, 0], X_train[:, 1], cy_train.numpy(), cmapcoolwarm, alpha0.6) plt.plot(x1_vals, x2_vals, k--, linewidth2) plt.xlabel(Feature 1) plt.ylabel(Feature 2) plt.title(Logistic Regression Decision Boundary) plt.show()虽然逻辑斯蒂回归只能学到线性决策边界但这不代表它没用。线性模型是很多非线性模型的基石树模型能间接做到非线性而神经网络则是通过堆叠多层线性变换加非线性激活来实现的。你能把这条边界画明白说明你对线性变换、sigmoid、阈值判定的整个流程都有了实感这种实感是后面学复杂网络的底气。5. 从二分类到多分类Softmax与交叉熵5.1 多分类逻辑斯蒂回归的推广形式逻辑斯蒂回归从二分类扩展到多分类核心是sigmoid换成softmax。假设有K个类别模型最后一层输出K个实数值softmax做的事情是把这K个数变成和为1的K个概率p_i exp(z_i) / Σ exp(z_j)这里exp的存在让差距大的得分在概率上更悬殊比如某个类别得分明显高时它的概率会非常大。这个性质和sigmoid其实是一脉相承的可以理解为sigmoid就是K2情形下的softmax。在PyTorch中多分类常用的损失函数是nn.CrossEntropyLoss。有一个很容易踩的坑这个损失函数已经内置了softmax运算所以模型最后一层不需要再手动加softmax。如果你在forward里先加了softmax再传给CrossEntropyLoss等于算了两次模型表现多少会受影响。我刚接触的时候犯过这个错训练loss一直下不去排查了好久才发现模型输出层多了一层softmax。5.2 用PyTorch API实现一个mnist分类拿MNIST数据集来做多分类实践非常合适代码量也不大。MNIST是28x28的手写数字图片一共10个类别。先把图片摊平成784维向量然后接一个线性层输出10维得分。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader DataLoader(test_dataset, batch_size64, shuffleFalse) class MultiClassLogisticRegression(nn.Module): def __init__(self, in_features, num_classes): super().__init__() self.fc nn.Linear(in_features, num_classes) def forward(self, x): x x.view(x.size(0), -1) return self.fc(x) model MultiClassLogisticRegression(28 * 28, 10) criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.01) epochs 5 for epoch in range(epochs): model.train() total_loss 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(fEpoch [{epoch1}/{epochs}], Loss: {total_loss/len(train_loader):.4f}, fTest Acc: {100 * correct/total:.2f}%)这段代码跑下来MNIST测试集准确率大概在92%左右。坦率地说这个准确率不高因为线性模型看到的是像素的线性组合没法捕捉笔画结构。但即便如此92%的准确率已经说明即使一个最简单的线性多分类器在有效特征上也能做出相当好的判断。这背后的启示是很多任务里先跑通一个简单模型建立baseline胜过一开始就搬出重型网络。很多人上来就用LeNet、ResNet跑MNIST虽然精度高但学到的东西反而不如先用逻辑斯蒂回归把整个Pipeline吃透。6. 常见问题排查与实战避坑速查6.1 训练loss为nan、不下降、震荡怎么办我把实际教学和项目里碰到过的最典型问题整理成了下面这个速查表每一行都是真实踩过坑的经验。现象可能原因排查方法loss为nan学习率太大降低学习率比如从0.1降到0.01或0.001loss为nan数据未标准化梯度爆炸对特征做StandardScaler避免极端量纲loss为nan数据里有NaN值检查输入数据用np.isnan查看统计loss不下降梯度未清零检查optimizer.zero_grad()是否在每轮开头执行loss不下降模型输出和标签维度不匹配检查squeeze和view操作是否搞错了维度loss震荡厉害批次太小、学习率偏高增大batch_size或降低学习率训练准确率高但测试很低过拟合增加数据量、加weight_decay、减少epoch测试时预测全为同一类类别不平衡使用加权损失或对少数类过采样多分类时loss异常高输出层多加了softmax检查是否误在forward加了softmax后交给CrossEntropyLoss6.2 数据泄露与train/val/test划分还有一个经常被人忽略的问题数据划分。逻辑斯蒂回归的代码很简单所以很多人容易松懈把全部数据丢进模型训练然后用同一批数据评估准确率。这样做出来的准确率虚高一点参考价值都没有。正确的姿势是先划分训练集、验证集、测试集三个部分。训练集用来更新模型参数验证集用来调整超参数和观察是否过拟合测试集只在最终评估时用一次。很多项目实战里还有一种隐蔽的数据泄露做标准化时用了全量数据的均值方差。严格来说标准化器只能在训练集上fit再分别transform训练集和测试集。我在代码里就是先fit_transform训练集再transform测试集这才是标准操作。我在实际带人做项目时发现80%的模型效果差都不是模型结构问题而是数据处理和评估流程出了问题。逻辑斯蒂回归代码量不大正好能让你把注意力聚焦在这些工程细节上养成好习惯后去做复杂的视觉、文本模型会少踩很多坑。6.3 一些小技巧随机种子、GPU启用与模型保存最后分享几个日常训练小技巧。第一复现性很重要。实验跑得好好的重启一下结果翻了个样多半是随机种子没固定。代码开头固定三行import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42)如果用了CUDA最好再加一句torch.cuda.manual_seed_all(42)。在写作业、复现论文或者做项目汇报时固定随机种子能减少很多无谓的纠缠。第二模型保存首选只保存参数而不是整个模型。因为整个模型保存方式跟代码类结构耦合太紧代码改动后加载容易报错。保存参数是标准做法torch.save(model.state_dict(), logistic_regression.pt) # 加载时先创建相同结构模型 model LogisticRegression(n_features2) model.load_state_dict(torch.load(logistic_regression.pt)) model.eval()第三训练时如何选择设备。用device torch.device(cuda if torch.cuda.is_available() else cpu)然后model.to(device)、tensor.to(device)一下就行。逻辑斯蒂回归模型的参数量极小CPU和GPU差别不大但这个写法能保证代码在任何人电脑上都能跑起来不会因为某台机器没有GPU就直接报错。7. 写在最后的一点经验从线性回归走到逻辑斯蒂回归看似只是加了一个sigmoid和换了损失函数实际上思维方式已经变了模型的输出从“预测数值”变成了“预测概率”决策方式从“直接输出结果”变成了“根据概率做判断”。这一个小小的变化是整个深度学习中分类问题的基石后面不管是卷积神经网络做图像分类、Transformer做文本分类本质上都是在解决“如何把特征映射到类别概率”这个问题。我个人的体会是逻辑斯蒂回归值得你花时间把它完全吃透甚至亲手从零推导一遍梯度。你会发现后面遇到的很多模型比如各种attention机制里的温度系数、对比学习里的温度参数本质上都是在控制sigmoid或softmax的锐利程度。理解了这一步你再去看那些看起来高深的算法会突然觉得它们都是老朋友了。把最简单的东西弄明白后面的路会好走很多。
返回列表