2026/8/28 3:11:54

PyTorch实现逻辑斯蒂回归:从Sigmoid函数到二分类实战

PyTorch实现逻辑斯蒂回归:从Sigmoid函数到二分类实战 1. 项目概述从线性到概率的跨越逻辑斯蒂回归这个名字听起来可能有点唬人尤其是对于刚接触机器学习的同学来说。我第一次听到时也以为它是个复杂的回归算法。但实际上它是个不折不扣的分类模型而且是理解神经网络最基础、最重要的一块敲门砖。今天我们就用PyTorch这个强大的工具亲手把它实现一遍把原理和代码都掰开揉碎了讲清楚。简单来说逻辑斯蒂回归要解决的核心问题是给你一堆数据点每个点都有一些特征比如身高、体重并且有一个明确的类别标签比如“健康”或“不健康”我们的目标是找到一个模型能够根据新的特征数据预测它属于哪个类别的概率。这和我们熟悉的线性回归有本质区别线性回归预测的是一个连续的数值比如房价而逻辑斯蒂回归预测的是一个介于0和1之间的概率值代表属于某个类别的可能性。为什么它如此重要因为在深度学习中尤其是处理分类任务的神经网络输出层逻辑斯蒂回归的思想无处不在。那个将神经网络最后一层线性输出“挤压”成概率的Sigmoid函数就是逻辑斯蒂回归的核心。所以掌握逻辑斯蒂回归就等于拿到了理解深度学习分类模型的钥匙。无论你是想入门机器学习还是为后续学习卷积神经网络CNN、循环神经网络RNN打基础这个项目都是绝佳的起点。它代码量不大但涵盖了数据准备、模型定义、损失函数、优化器、训练循环等深度学习流水线的所有核心环节。2. 核心原理Sigmoid函数与交叉熵损失要搞懂逻辑斯蒂回归我们必须先理解它的两个核心数学部件Sigmoid激活函数和二元交叉熵损失函数。很多教程只讲怎么用PyTorch调用nn.Linear和nn.BCELoss但如果不明白背后的“为什么”一旦出了问题就会束手无策。2.1 Sigmoid函数将任意值映射为概率逻辑斯蒂回归模型的第一步是对线性变换的结果进行非线性处理。假设我们有输入特征向量x通过一个线性层得到z w*x b。这里的z是一个实数范围是负无穷到正无穷显然不能直接作为概率。这时Sigmoid函数登场了。它的公式是σ(z) 1 / (1 e^(-z))。这个函数有什么魔力我们可以从几个角度理解输出范围无论输入z多大或多小σ(z)的输出永远被限制在 (0, 1) 之间。这完美符合概率的定义。函数形态它是一个平滑的、单调递增的S形曲线。当z0时σ(z)0.5可以视为决策边界。当z趋向正无穷时σ(z)无限趋近于1可以理解为100%确信是正类当z趋向负无穷时σ(z)无限趋近于0100%确信是负类。导数特性它的导数有一个非常优美的形式σ(z) σ(z) * (1 - σ(z))。这个特性在反向传播计算梯度时带来了极大的便利我们后面会看到。在PyTorch中我们可以直接用torch.sigmoid()函数或者作为nn.Sigmoid()层来使用。注意Sigmoid函数在深度神经网络中间层中使用时可能会引起“梯度消失”问题因为当输入z的绝对值很大时其导数会趋近于0。但在逻辑斯蒂回归这种单层模型中或者在多层网络的输出层用于二分类它仍然是标准且可靠的选择。2.2 交叉熵损失衡量概率预测的“距离”模型输出了概率我们如何衡量这个预测概率和真实标签之间的差距呢不能用均方误差MSE因为那更适合回归问题。对于分类问题我们使用交叉熵损失。对于二分类问题其损失函数称为二元交叉熵Binary Cross-Entropy, BCE。公式如下Loss - [y * log(ŷ) (1-y) * log(1-ŷ)]其中y是真实标签0或1ŷ是模型预测的正类概率即Sigmoid的输出。这个公式直观上怎么理解我们可以分情况看当真实标签y1时损失变为-log(ŷ)。这意味着如果模型预测概率ŷ越接近1预测正确log(ŷ)越接近0损失就越小如果ŷ越接近0预测错误log(ŷ)会趋向负无穷-log(ŷ)就会变得非常大惩罚很重。当真实标签y0时损失变为-log(1-ŷ)。逻辑同理预测概率ŷ越接近0损失越小。所以交叉熵损失函数本质上是在鼓励模型对正确类别给出高置信度的预测同时对错误预测施以严厉的惩罚。在PyTorch中对应的类是nn.BCELoss()它要求输入ŷ已经是经过Sigmoid处理后的概率值。还有一个更常用的nn.BCEWithLogitsLoss()它内部集成了Sigmoid激活和BCE损失计算数值上更稳定通常推荐使用。3. 实战构建从数据到PyTorch模型理论说再多不如动手写一行代码。我们现在就基于PyTorch构建一个完整的逻辑斯蒂回归模型。为了有直观感受我们使用一个经典的二分类数据集乳腺癌数据集Breast Cancer Dataset目标是根据肿瘤的特征判断其是良性0还是恶性1。3.1 数据准备与预处理任何机器学习项目的基石都是数据。PyTorch提供了torch.utils.data.Dataset和DataLoader来优雅地处理数据加载。import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader, random_split from sklearn.datasets import load_breast_cancer from sklearn.preprocessing import StandardScaler import numpy as np # 1. 加载数据并转换为PyTorch张量 data load_breast_cancer() X, y data.data, data.target # 2. 数据标准化至关重要的一步 # 逻辑回归/神经网络对输入特征的尺度非常敏感。 # 标准化可以加速模型收敛并提高性能。 scaler StandardScaler() X_scaled scaler.fit_transform(X) # 3. 转换为PyTorch张量 X_tensor torch.from_numpy(X_scaled).float() y_tensor torch.from_numpy(y).float().view(-1, 1) # 将标签reshape为 [n_samples, 1]以匹配输出形状 # 4. 创建自定义Dataset class CancerDataset(Dataset): def __init__(self, features, labels): self.features features self.labels labels def __len__(self): return len(self.features) def __getitem__(self, idx): return self.features[idx], self.labels[idx] dataset CancerDataset(X_tensor, y_tensor) # 5. 划分训练集和测试集 train_size int(0.8 * len(dataset)) test_size len(dataset) - train_size train_dataset, test_dataset random_split(dataset, [train_size, test_size]) # 6. 创建DataLoader # batch_size的选择太小训练慢且噪声大太大内存可能不够且可能陷入局部最优。 # 对于这个规模的数据集32或64是个不错的起点。 batch_size 32 train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) test_loader DataLoader(test_dataset, batch_sizebatch_size, shuffleFalse) print(f训练集样本数: {len(train_dataset)}) print(f测试集样本数: {len(test_dataset)}) print(f特征维度: {X_tensor.shape[1]})实操心得shuffleTrue在训练时非常重要它会在每个epoch开始时打乱数据顺序防止模型学习到数据顺序带来的虚假模式有助于提升泛化能力。而测试集shuffleFalse则可以保证每次评估的顺序一致便于结果复现和调试。3.2 模型定义与初始化数据准备好了接下来定义模型。逻辑斯蒂回归模型本质上就是一个线性层加上一个Sigmoid激活函数。class LogisticRegressionModel(nn.Module): def __init__(self, input_dim): super(LogisticRegressionModel, self).__init__() # 定义一个线性层input_dim - 1 self.linear nn.Linear(input_dim, 1) # 注意我们这里不显式定义Sigmoid层因为将使用BCEWithLogitsLoss def forward(self, x): # 前向传播线性变换 # outputs 是“logits”未经过激活的分数 outputs self.linear(x) return outputs # 实例化模型 input_dim X_tensor.shape[1] model LogisticRegressionModel(input_dim) print(model) # 模型参数初始化可选但推荐 # PyTorch的Linear层默认使用Kaiming均匀初始化这对有非线性激活的层很好。 # 但对于输出层直接接Sigmoid/Softmax的情况有时使用更小的初始化权重有助于稳定训练初期。 # 这里我们使用Xavier初始化它对Sigmoid/Tanh这类对称激活函数更友好。 def init_weights(m): if isinstance(m, nn.Linear): nn.init.xavier_uniform_(m.weight) nn.init.constant_(m.bias, 0.0) model.apply(init_weights)这里有一个关键点我们的forward函数只返回了线性层的输出logits而没有经过Sigmoid。这是因为我们将使用nn.BCEWithLogitsLoss它内部包含了Sigmoid操作并且数值稳定性更好。直接计算Sigmoid BCELoss在数学上是等价的但在计算机中当logits很大或很小时先算Sigmoid再算log可能会得到数值0或inf导致梯度计算出错。BCEWithLogitsLoss使用了技巧来避免这个问题。3.3 损失函数与优化器选择模型输出logits我们需要一个损失函数来度量误差并选择一个优化器来更新模型参数。# 1. 损失函数二元交叉熵带Logits # pos_weight参数可用于处理类别不平衡问题。如果正样本很少可以设置pos_weight1来增加其对损失的贡献。 criterion nn.BCEWithLogitsLoss() # 2. 优化器随机梯度下降SGD或Adam # SGD: 最经典需要手动调学习率lr和动量momentum # optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.9) # Adam: 自适应学习率通常收敛更快对初始学习率不那么敏感是现在的默认选择。 optimizer torch.optim.Adam(model.parameters(), lr0.001) # 学习率调度器可选在训练过程中动态降低学习率有助于后期精细调参。 # scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.1)为什么选择Adam在大多数深度学习任务中Adam优化器因其自适应学习率特性通常比普通的SGD表现更好收敛更快且需要调的超参数更少主要是初始学习率lr。对于逻辑回归这种相对简单的凸优化问题SGD也能工作得很好但Adam通常更省心。lr0.001是Adam一个非常通用的起始学习率。4. 训练循环与模型评估核心部分来了训练循环。这是让模型从数据中学习的关键过程。4.1 完整的训练循环实现num_epochs 100 train_losses [] test_accuracies [] for epoch in range(num_epochs): # ---------------------- 训练阶段 ---------------------- model.train() # 设置模型为训练模式影响Dropout、BatchNorm等层本例中没有 running_loss 0.0 for batch_idx, (features, labels) in enumerate(train_loader): # 1. 梯度清零PyTorch会累积梯度每次迭代前必须清零 optimizer.zero_grad() # 2. 前向传播得到预测的logits logits model(features) # 计算损失 loss criterion(logits, labels) # 3. 反向传播计算损失关于模型参数的梯度 loss.backward() # 4. 参数更新优化器根据梯度更新参数 optimizer.step() running_loss loss.item() * features.size(0) # 累加批次损失乘以批次大小 epoch_train_loss running_loss / len(train_loader.dataset) train_losses.append(epoch_train_loss) # ---------------------- 评估阶段 ---------------------- model.eval() # 设置模型为评估模式 correct 0 total 0 with torch.no_grad(): # 关闭梯度计算节省内存和计算资源 for features, labels in test_loader: logits model(features) # 将logits通过Sigmoid得到概率然后四舍五入得到预测类别0或1 predictions torch.round(torch.sigmoid(logits)) total labels.size(0) correct (predictions labels).sum().item() epoch_test_acc 100 * correct / total test_accuracies.append(epoch_test_acc) # 打印进度 if (epoch 1) % 10 0: print(fEpoch [{epoch1:03d}/{num_epochs}], fTrain Loss: {epoch_train_loss:.4f}, fTest Acc: {epoch_test_acc:.2f}%) # 训练结束后可以绘制损失和准确率曲线需要matplotlib # import matplotlib.pyplot as plt # plt.figure(figsize(12,4)) # plt.subplot(1,2,1) # plt.plot(train_losses, labelTrain Loss) # plt.xlabel(Epoch) # plt.ylabel(Loss) # plt.legend() # plt.subplot(1,2,2) # plt.plot(test_accuracies, labelTest Accuracy) # plt.xlabel(Epoch) # plt.ylabel(Accuracy (%)) # plt.legend() # plt.show()这个训练循环是PyTorch深度学习项目的标准模板包含了几个关键环节model.train()和model.eval()这像是一个开关。在训练时某些层如Dropout、BatchNorm的行为是不同的。train()模式会启用Dropout和使用当前批次的统计量进行BatchNormeval()模式则会关闭Dropout并使用训练阶段累积的移动平均统计量进行BatchNorm。虽然逻辑回归模型没有这些层但养成这个习惯至关重要。optimizer.zero_grad()这是最容易忘记的一步。如果不清零梯度会在多个批次间累积导致更新方向错误。with torch.no_grad()在评估和推理时我们不需要计算梯度。这个上下文管理器可以显著减少内存消耗并加速计算。4.2 模型决策边界与预测训练完成后我们不仅关心准确率还想知道模型是如何做决策的。# 在测试集上进行最终评估 model.eval() all_predictions [] all_labels [] all_probabilities [] with torch.no_grad(): for features, labels in test_loader: logits model(features) probabilities torch.sigmoid(logits) # 得到概率值 predictions torch.round(probabilities) # 以0.5为阈值进行分类 all_predictions.extend(predictions.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) all_probabilities.extend(probabilities.cpu().numpy()) # 转换为numpy数组便于分析 all_predictions np.array(all_predictions).flatten() all_labels np.array(all_labels).flatten() all_probabilities np.array(all_probabilities).flatten() # 计算最终测试集准确率 final_accuracy np.mean(all_predictions all_labels) print(f\n最终测试集准确率: {final_accuracy*100:.2f}%) # 查看几个样本的预测概率 print(\n部分样本的预测详情真实标签 vs 预测概率 vs 预测类别:) for i in range(5): print(f样本{i}: 真实{all_labels[i]}, 概率{all_probabilities[i]:.4f}, 预测{all_predictions[i]})对于高维数据我们无法可视化整个决策边界。但我们可以通过查看模型权重来理解每个特征的重要性。权重w的绝对值越大说明该特征对最终决策的影响越大。# 获取模型学到的权重和偏置 weights model.linear.weight.data.numpy().flatten() bias model.linear.bias.data.item() print(f\n模型偏置 (b): {bias:.4f}) print(特征重要性权重绝对值前10的特征:) # 假设我们有特征名称从sklearn数据集获取 feature_names data.feature_names sorted_idx np.argsort(np.abs(weights))[::-1] # 按权重绝对值降序排序 for i in range(min(10, len(weights))): idx sorted_idx[i] print(f {feature_names[idx]:30s}: {weights[idx]:.4f})这个分析非常有用它可以告诉我们在判断肿瘤良恶性时哪些特征是模型认为最重要的这有时能提供超出模型本身的洞见。5. 常见问题、调试技巧与进阶思考即使代码看起来很简单在实际操作中你也很可能会遇到各种问题。下面是我在多次实践中总结的一些常见坑点和解决思路。5.1 损失不下降或准确率卡在50%这是新手最常见的问题。模型好像“学不会”。检查数据标签确认你的标签y是0和1还是1和-1BCEWithLogitsLoss要求是0和1。如果是-1和1需要转换(y1)/2。检查数据标准化这是最容易被忽略但至关重要的一步。如果特征尺度差异巨大比如一个特征范围是0-1另一个是0-10000梯度更新会非常不稳定导致训练失败。务必进行标准化或归一化。学习率问题学习率太大损失可能会震荡甚至爆炸变成NaN学习率太小损失下降极其缓慢。尝试调整学习率比如从0.001调到0.01或0.0001。使用Adam优化器通常能缓解这个问题。模型初始化虽然PyTorch默认初始化通常不错但在极端情况下糟糕的初始化可能导致模型一开始就陷入“饱和区”Sigmoid输出接近0或1梯度几乎为0无法更新。尝试使用nn.init.xavier_uniform_重新初始化权重。损失函数使用错误确认你使用的是BCEWithLogitsLoss而不是BCELoss。如果你用了BCELoss必须在模型forward中手动加上torch.sigmoid。5.2 过拟合与欠拟合欠拟合训练集和测试集准确率都低模型太简单无法捕捉数据中的模式。对于逻辑回归这通常意味着特征本身与标签的线性关系不强。解决方案1) 尝试更复杂的模型如神经网络2) 进行特征工程构造更有意义的特征。过拟合训练集准确率高测试集低模型记住了训练数据的噪声。逻辑回归本身是线性模型过拟合风险相对较低但如果特征维度很高而样本量少也可能发生。解决方案1)正则化在优化器中加入L2正则化权重衰减。optim.Adam(model.parameters(), lr0.001, weight_decay1e-4)。weight_decay参数就是L2正则化的强度。2) 获取更多训练数据。3) 减少特征维度特征选择。5.3 数值稳定性与调试技巧NaN损失如果损失突然变成NaN通常是计算中出现除零或log(0)的情况。使用BCEWithLogitsLoss而非SigmoidBCELoss可以极大避免此问题。另外检查输入数据是否有NaN或inf值。梯度检查在怀疑梯度计算有问题时可以用PyTorch的torch.autograd.gradcheck功能进行数值梯度检查但这更多用于自定义复杂函数。使用TensorBoard或简单的打印监控除了损失和准确率还可以在训练初期打印几个批次的权重和梯度范数看它们是否在合理范围内变化。5.4 从二分类到多分类逻辑斯蒂回归本质上是二分类器。那多分类问题怎么办这时就需要Softmax回归或称多项逻辑斯蒂回归。其核心变化是输出层线性层的输出维度等于类别数C而不是1。激活函数使用Softmax函数代替Sigmoid。Softmax将C个logits转换为一个概率分布所有类别概率之和为1。损失函数使用交叉熵损失nn.CrossEntropyLoss。注意这个函数内部已经包含了Softmax所以模型forward应该直接输出logits。并且它的标签是类别的索引LongTensor类型而不是one-hot编码。# 多分类示例框架 class MultiClassLogisticRegression(nn.Module): def __init__(self, input_dim, num_classes): super().__init__() self.linear nn.Linear(input_dim, num_classes) # 输出维度 类别数 def forward(self, x): return self.linear(x) # 输出logits model MultiClassLogisticRegression(input_dim30, num_classes3) criterion nn.CrossEntropyLoss() # 自动处理Softmax # 训练时标签y应该是形状为 [batch_size] 的LongTensor每个元素是类别索引0, 1, 2...掌握了二分类的逻辑斯蒂回归理解多分类的Softmax回归就是水到渠成的事情。它们共同构成了深度学习分类任务最基础的输出层单元。通过这个项目你不仅学会了用PyTorch实现一个模型更重要的是你理解了数据流、损失计算、梯度更新这一整套深度学习训练流程。这是你通往更复杂模型如CNN、RNN、Transformer的坚实第一步。下次当你看到神经网络最后一层的Sigmoid或Softmax时你会会心一笑因为你知道老朋友逻辑斯蒂回归一直都在那里。