PyTorch从零实现逻辑回归:深入理解深度学习训练核心

发布时间:2026/10/15 18:03:36
PyTorch从零实现逻辑回归:深入理解深度学习训练核心 1. 为什么逻辑回归值得用PyTorch重写一遍很多人第一次接触逻辑回归都是在机器学习入门课上用scikit-learn两行代码搞定LogisticRegression().fit(X, y)。跑通了准确率看着还行然后就翻篇了。但如果你真的想搞明白深度学习框架到底在干什么用PyTorch从零实现一遍逻辑回归是最划算的练手项目——它足够小小到你能把每一行代码的意图都看清楚又足够完整完整到包含了现代神经网络训练的全部核心要素前向传播、损失计算、反向传播、参数更新。这篇文章面向的读者很明确已经看过逻辑回归公式但没亲手用PyTorch搭过训练循环的人或者用过PyTorch搭深层网络但对底层梯度计算一知半解的开发者。我会把整个实现过程拆开不光告诉你代码怎么写更要讲清楚每个选择背后的理由——为什么用BCEWithLogitsLoss而不是手动sigmoid加BCELoss为什么参数初始化不能全零学习率设大了会发生什么。这些细节才是从“能跑”到“跑得稳”之间的差距。逻辑回归虽然名字里有“回归”但它解决的是二分类问题。核心思路是把线性组合的结果通过Sigmoid函数映射到(0,1)区间当作样本属于正类的概率。训练目标是最小化预测概率分布和真实标签之间的差异通常用二元交叉熵来衡量。用PyTorch实现它本质上就是把这个数学过程翻译成张量运算和自动求导能识别的计算图。2. 整体设计与方案选型为什么不直接调库2.1 手写实现和调库的分界线在哪用scikit-learn的LogisticRegression你得到的是一个封装好的优化器通常是LBFGS或liblinear它帮你处理了正则化、收敛判断、多分类策略。方便是方便但你失去了对训练过程的控制。比如你想换一个自定义的损失函数想在中途观察梯度的变化想用mini-batch而不是全量梯度下降调库就很难做到。PyTorch的定位不一样。它提供的是张量运算和自动求导这两块积木训练循环得你自己搭。这看起来麻烦但换来的是完全透明的控制权。你可以精确地知道每一步发生了什么梯度是怎么算出来的参数是怎么更新的。对于逻辑回归这种简单模型这种透明度带来的学习价值远大于那几行省下来的代码。我的方案是用nn.Linear定义一个单层线性模块输出维度为1不加激活函数。损失函数用nn.BCEWithLogitsLoss它内部把Sigmoid和二元交叉熵合并在一起数值上更稳定。优化器用torch.optim.SGD或Adam手动写训练循环每个epoch做前向传播、计算损失、反向传播、更新参数这四步。2.2 关键设计决策背后的考量第一个决策输出层不加Sigmoid。很多人直觉上觉得逻辑回归的输出应该是概率所以在线性层后面接一个Sigmoid。但PyTorch的BCEWithLogitsLoss要求输入是未经过Sigmoid的logits。原因在于数值稳定性当logit很大或很小时Sigmoid的输出会接近1或0取对数后容易出现数值溢出。BCEWithLogitsLoss内部用log-sum-exp技巧把这两步合并计算避免了这个问题。所以正确的做法是模型输出logits损失函数负责剩下的。第二个决策参数初始化。线性层的权重和偏置不能全部初始化为零。如果全零所有神经元的输出相同反向传播时梯度也相同参数更新后仍然相同模型永远学不到东西。PyTorch的nn.Linear默认用均匀分布初始化权重偏置也是均匀分布这已经够用了。但如果你想手动控制可以用nn.init.xavier_uniform_或nn.init.kaiming_uniform_。第三个决策优化器选择。SGD是最基础的适合理解原理但对学习率敏感。Adam自适应调整每个参数的学习率通常收敛更快适合快速验证。我的建议是先用Adam跑通确认模型能学到东西再换SGD调参观察损失曲线的变化。这样能区分问题是出在模型结构还是优化过程。3. 核心细节解析与实操要点3.1 数据准备从张量形状说起逻辑回归的输入数据通常是一个二维张量形状为(N, D)其中N是样本数D是特征维度。标签形状为(N, 1)每个样本对应一个0或1的标签。这个形状很关键因为PyTorch的损失函数对输入形状有要求。我见过不少人在这里踩坑标签用(N,)的一维张量然后发现损失计算报错或者结果不对。BCEWithLogitsLoss要求输入和目标的形状一致或者目标能被广播到输入的形状。最稳妥的做法是把标签也整理成(N, 1)和模型输出保持同样的形状。数据标准化的必要性也值得说一句。逻辑回归对特征的尺度敏感如果一个特征范围是0到1另一个是0到10000梯度下降会走得很别扭损失曲面像一个被拉长的椭圆收敛慢甚至震荡。用(x - mean) / std做标准化能让损失曲面更接近圆形梯度下降走得更直。这个操作在PyTorch里可以用torch.mean和torch.std手动算也可以用sklearn.preprocessing.StandardScaler先处理好再转成张量。3.2 模型定义一行代码背后的三件事nn.Linear(in_features, out_features)这一行代码实际上做了三件事创建权重矩阵W形状为(out_features, in_features)创建偏置向量b形状为(out_features,)注册这两个参数为可学习参数让优化器能找到它们。对于逻辑回归in_features等于特征维度Dout_features等于1。前向传播时输入x形状(N, D)经过线性层后输出形状(N, 1)计算过程是x W.T b。这里W.T是W的转置形状(D, 1)矩阵乘法后得到(N, 1)。如果你想把逻辑回归扩展成多分类只需要把out_features改成类别数C损失函数换成nn.CrossEntropyLoss。这就是softmax回归本质上和逻辑回归是同一个东西在不同类别数下的形式。理解了这个对应关系你就能明白为什么PyTorch里没有单独的“逻辑回归层”——它就是一个线性层加一个损失函数。3.3 损失函数BCEWithLogitsLoss的内部机制二元交叉熵的数学形式是L -[y * log(p) (1 - y) * log(1 - p)]其中p sigmoid(z)z是logit。直接按这个公式实现当z很大时sigmoid(z)接近1log(p)接近0没问题但当z很负时sigmoid(z)接近0log(p)会趋向负无穷数值上不稳定。BCEWithLogitsLoss的做法是把sigmoid和log合并log(sigmoid(z)) z - log(1 exp(z))这个表达式在z很大或很小时都能稳定计算。PyTorch内部用log-sum-exp技巧实现所以你传logits进去它自动处理数值稳定性。实操中有一个细节BCEWithLogitsLoss默认对batch内所有样本的损失取平均reductionmean。如果你想对每个样本的损失单独加权可以设置reductionnone然后手动乘权重再求平均。这在类别不平衡的场景下很有用——比如正样本只占5%你可以给正样本更高的权重让模型更关注少数类。4. 完整实操流程从零搭建训练循环4.1 环境准备与数据生成先确认PyTorch装好了。如果没装去官网复制对应系统的安装命令这里不赘述。我习惯用虚拟环境避免版本冲突。import torch import torch.nn as nn import torch.optim as optim from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler import numpy as np # 生成模拟数据1000个样本20个特征二分类 X, y make_classification( n_samples1000, n_features20, n_informative10, n_redundant5, n_classes2, 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) # 转成PyTorch张量注意标签形状为(N, 1) X_train torch.tensor(X_train, dtypetorch.float32) y_train torch.tensor(y_train, dtypetorch.float32).unsqueeze(1) X_test torch.tensor(X_test, dtypetorch.float32) y_test torch.tensor(y_test, dtypetorch.float32).unsqueeze(1) print(f训练集形状: {X_train.shape}, 标签形状: {y_train.shape}) print(f测试集形状: {X_test.shape}, 标签形状: {y_test.shape})这里用make_classification生成模拟数据方便控制特征数量和类别分布。n_informative10表示10个特征真正携带分类信息n_redundant5表示5个特征是其他特征的线性组合剩下的5个是噪声。这种设置能模拟真实数据中特征冗余和噪声并存的情况。unsqueeze(1)把标签从(N,)变成(N, 1)这是为了和模型输出形状对齐。标准化用StandardScaler注意测试集要用训练集的均值和标准差来变换不能单独算否则数据泄露。4.2 模型定义与参数初始化class LogisticRegressionModel(nn.Module): def __init__(self, input_dim): super().__init__() self.linear nn.Linear(input_dim, 1) # 手动初始化权重用xavier均匀分布偏置置零 nn.init.xavier_uniform_(self.linear.weight) nn.init.zeros_(self.linear.bias) def forward(self, x): return self.linear(x) # 输出logits不加sigmoid model LogisticRegressionModel(input_dim20) print(model) print(f权重形状: {model.linear.weight.shape}) print(f偏置形状: {model.linear.bias.shape})继承nn.Module是PyTorch定义模型的标准方式。__init__里声明层forward里定义计算流程。这里手动初始化了权重和偏置虽然nn.Linear默认初始化也能用但手动写一遍能让你清楚参数长什么样。xavier_uniform_的初始化范围是[-sqrt(6/(fan_infan_out)), sqrt(6/(fan_infan_out))]对于输入20输出1的层范围大约是[-0.53, 0.53]。这个范围能保证前向传播时输出的方差不会爆炸或消失。偏置置零是安全的因为权重已经随机了不会出现所有神经元输出相同的问题。4.3 损失函数与优化器配置criterion nn.BCEWithLogitsLoss() optimizer optim.Adam(model.parameters(), lr0.01) # 或者用SGD # optimizer optim.SGD(model.parameters(), lr0.1, momentum0.9)BCEWithLogitsLoss不需要额外参数默认对batch取平均。Adam的学习率设0.01这是比较保守的值通常能稳定收敛。如果用SGD学习率可以设0.1加动量0.9能加速收敛并减少震荡。优化器接收model.parameters()这是一个生成器包含模型中所有nn.Parameter类型的属性。nn.Linear的权重和偏置自动注册为参数所以能被优化器找到。如果你在模型里用了普通的张量而不是nn.Parameter优化器就找不到它参数不会更新——这是新手常犯的错误。4.4 训练循环四步走num_epochs 200 train_losses [] test_losses [] train_accs [] test_accs [] for epoch in range(num_epochs): # 训练模式 model.train() # 前向传播 logits model(X_train) loss criterion(logits, y_train) # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() # 记录训练损失 train_losses.append(loss.item()) # 计算训练准确率 with torch.no_grad(): preds (torch.sigmoid(logits) 0.5).float() train_acc (preds y_train).float().mean().item() train_accs.append(train_acc) # 评估测试集 model.eval() with torch.no_grad(): test_logits model(X_test) test_loss criterion(test_logits, y_test) test_losses.append(test_loss.item()) test_preds (torch.sigmoid(test_logits) 0.5).float() test_acc (test_preds y_test).float().mean().item() test_accs.append(test_acc) if (epoch 1) % 20 0: print(fEpoch [{epoch1}/{num_epochs}], fTrain Loss: {loss.item():.4f}, Train Acc: {train_acc:.4f}, fTest Loss: {test_loss.item():.4f}, Test Acc: {test_acc:.4f})训练循环的四步是固定套路前向传播算输出计算损失清空梯度反向传播算梯度更新参数。顺序不能乱尤其是optimizer.zero_grad()必须在loss.backward()之前否则梯度会累加。model.train()和model.eval()在逻辑回归里影响不大因为没有Dropout和BatchNorm层。但养成习惯是好的以后搭复杂网络时不会忘。torch.no_grad()在评估时用能减少内存占用并加速计算。准确率的计算先对logits做Sigmoid得到概率然后以0.5为阈值判断类别。(preds y_train)得到布尔张量.float()转成0和1.mean()算平均值就是准确率。4.5 训练结果分析与可视化import matplotlib.pyplot as plt fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].plot(train_losses, labelTrain Loss) axes[0].plot(test_losses, labelTest Loss) axes[0].set_xlabel(Epoch) axes[0].set_ylabel(Loss) axes[0].set_title(Loss Curve) axes[0].legend() axes[0].grid(True) axes[1].plot(train_accs, labelTrain Acc) axes[1].plot(test_accs, labelTest Acc) axes[1].set_xlabel(Epoch) axes[1].set_ylabel(Accuracy) axes[1].set_title(Accuracy Curve) axes[1].legend() axes[1].grid(True) plt.tight_layout() plt.show() print(f最终训练准确率: {train_accs[-1]:.4f}) print(f最终测试准确率: {test_accs[-1]:.4f})正常的训练曲线应该是训练损失和测试损失都下降然后趋于平稳。如果测试损失先降后升说明过拟合了需要加正则化或减少特征。如果训练损失震荡厉害说明学习率太大。如果损失几乎不降可能是学习率太小、初始化有问题、或者数据没标准化。逻辑回归的过拟合通常不严重因为模型复杂度低。但如果特征数远大于样本数或者特征之间高度相关过拟合就可能出现。这时候可以加L2正则化在优化器里设置weight_decay参数比如optim.Adam(model.parameters(), lr0.01, weight_decay1e-4)。这等价于在损失函数里加权重平方和的惩罚项。5. 常见问题与排查技巧实录5.1 损失不下降或变成NaN这是最常见的问题。按可能性从高到低排查现象可能原因排查方法解决方案损失几乎不变学习率太小打印梯度范数增大学习率10倍试试损失震荡学习率太大观察损失曲线减小学习率或换Adam损失变NaN学习率太大或数据有异常值检查输入数据范围标准化数据降低学习率损失不降参数未更新检查优化器是否包含参数确认用nn.Parameter定义参数损失不降标签形状不对打印标签形状确保标签形状为(N,1)我踩过的一个坑标签用了(N,)形状模型输出是(N,1)BCEWithLogitsLoss广播后计算出的损失形状是(N,N)取平均后数值完全不对。这种错误不会报错但结果莫名其妙。所以一定要检查形状。另一个坑数据没有标准化某个特征范围是0到100000导致logit非常大Sigmoid输出饱和梯度接近零参数几乎不更新。标准化后问题消失。5.2 准确率卡在某个值上不去如果准确率停在50%左右说明模型没学到任何东西等价于随机猜。可能的原因学习率太大导致参数在最优解附近震荡特征和标签没有对应关系数据生成时n_informative设太小或者标签被错误地打乱。如果准确率停在某个中间值比如70%可能是特征信息量不够或者模型欠拟合。可以尝试增加特征、减少正则化强度、或者换更复杂的模型。逻辑回归是线性分类器如果真实决策边界是非线性的它天然学不好。这时候需要做特征工程比如加多项式特征或者换用神经网络。5.3 训练集准确率高但测试集低这是过拟合的典型表现。逻辑回归的过拟合通常来自特征太多或样本太少。解决方案加L2正则化weight_decay减少特征数量用特征选择或者增加训练数据。有一个容易被忽略的点如果标准化时用了全部数据训练集加测试集来算均值和标准差测试集的信息会泄露到训练过程中导致测试准确率虚高。正确的做法是只用训练集算均值和标准差然后应用到测试集。5.4 梯度消失或梯度爆炸逻辑回归只有一层梯度消失和爆炸不常见但如果学习率设得极大梯度更新后参数变得极大下一轮前向传播的logit极大Sigmoid饱和梯度接近零损失变成NaN。解决办法是降低学习率或者用梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。梯度裁剪在RNN和深层网络里是标配逻辑回归里一般用不上但知道这个工具没坏处。它的作用是在反向传播后、参数更新前把梯度的范数限制在一个阈值内防止单次更新步长过大。5.5 实操心得几个让训练更稳的小技巧第一个技巧先用小学习率跑几个epoch观察损失是否下降。如果下降太慢再逐步增大学习率。这比一上来就设个大学习率然后调半天要高效。第二个技巧记录每个epoch的梯度范数。如果梯度范数在训练过程中逐渐减小到接近零说明模型收敛了如果突然增大说明可能遇到了异常样本或学习率过大。第三个技巧用torch.manual_seed(42)固定随机种子保证每次运行结果可复现。这在调试时特别重要否则你改了一个参数结果变了但你不知道是参数的作用还是随机性的作用。第四个技巧如果数据类别不平衡除了给损失加权还可以用WeightedRandomSampler在采样时让少数类被更频繁地抽到。这两种方法的效果类似但采样方法会改变每个epoch看到的样本分布可能影响收敛速度。6. 从逻辑回归延伸到更复杂的模型逻辑回归的训练循环和神经网络完全一样区别只在于模型定义。把nn.Linear(20, 1)换成多层堆叠加上激活函数就是多层感知机。损失函数和优化器的用法不变。所以把这个项目跑通你就掌握了PyTorch训练任何模型的基本框架。我建议你在跑通基础版本后尝试几个变体把优化器从Adam换成SGD观察收敛速度的差异加L2正则化看测试准确率是否提升把特征数从20增加到100看训练时间的变化用DataLoader做mini-batch训练对比全量梯度下降的效果。这些对比实验能让你对超参数的影响有直观感受比看理论推导印象深得多。还有一个值得尝试的方向把逻辑回归的输出从二分类扩展到多分类。把nn.Linear(20, 1)改成nn.Linear(20, 3)损失函数换成nn.CrossEntropyLoss标签从0/1改成0/1/2。你会发现代码几乎不用改只是形状和损失函数变了。这就是PyTorch设计得好的地方——模型、损失、优化器解耦换组件像换积木一样简单。最后分享一个我经常用的调试方法在训练循环里加一个断言检查损失是否为NaN。assert not torch.isnan(loss), fLoss is NaN at epoch {epoch}。这样一旦出现数值问题程序立刻停下来你能马上定位到是哪个epoch、哪批数据出的问题。比等到训练结束才发现损失曲线是条直线要高效得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询