当时跟着某门经典机器学习课一路啃下来最大的感受是逻辑回归这一章名字实在太有迷惑性。它明明叫“回归”但几乎所有拿它当入门工具的人遇到的第一个场景都是分类——邮件是垃圾还是正常、肿瘤是良性还是恶性、用户会买还是不会买。而这个标题里的另一半“二分类与多分类”才是逻辑回归真正的主场。这篇文章就围绕二分类和多分类展开把逻辑回归从“为什么它是个分类算法”到“多分类到底有哪些打开方式”讲清楚。看完之后你可以自己动手实现一个能跑的分类器也能理解在真实项目里遇到的那些坑——损失不下降、预测全是一类、类别不平衡导致阈值失灵——分别是什么原因引起的。适合谁看刚学完线性回归、准备进入分类任务的同学或者已经用过机器学习库但不太清楚底层在做什么的同行。这篇是逻辑回归专题的上篇重点讲模型思路和分类原理下篇再补正则化、边界扩展和工程化细节。1. 逻辑回归为什么“叫回归却在干分类的活”很多初学者在这里会卡很久。我当年也卡过明明叫回归为什么输入一张图出来的是“猫还是狗”这事得从头理。1.1 线性回归在分类问题上翻了车线性回归做的事是拟合一个连续值比如根据房子面积预测房价输出的单位是“万元”。但分类问题要的不是连续数值而是“这个样本属于哪一类”。有人会说那我拿线性回归的输出自己定一个阈值不就行了比如大于0.5判为1小于0.5判为0。理论上好像可行但实操里会翻车。举个例子。假设一组训练数据正样本的标签是0和1特征值分布在0到10之间。你用线性回归拟合得到一条直线然后拿0.5当阈值去切分可能效果还行。但这时候如果新来一个样本特征值非常大比如是100线性回归为了保证整体误差最小会把整条直线硬拉过去导致原来在中间区域的分类边界整体偏移。这个现象从最小二乘的损失函数就能看出来它对“远超过边界”的样本惩罚很重但对分类任务来说那些“已经正确分类且离边界足够远”的样本本来不应该再对模型有任何影响了。换句话说线性回归在分类问题里的病根是它太在乎数值大小而不是在乎分类对不对。分类问题真正需要的是一个能输出“属于某类的概率”的工具。1.2 sigmoid函数把任意实数压缩到0和1之间逻辑回归的解法是把线性回归的输出 z θ^T x 先扔进一个叫做 sigmoid 的函数里。公式很简单g(z) 1 / (1 e^(-z))z 可以是任何实数。z 越大g(z) 越接近1z 越小g(z) 越接近0z 0 时g(z) 0.5。这个函数把线性模型的输出“压”到了 (0,1) 区间于是我们得到了一个可以解释为概率的数。为什么不选其他也能把任意实数映射到0~1之间的函数比如分段函数。sigmoid 之所以被选中有几个非常实在的理由。第一它处处连续可导梯度下降能顺畅跑下去。第二它的导数形式极其优雅g(z) g(z) * (1 - g(z))。这意味着在反向传播时导数的计算只需要用当前输出值不需要额外求导实现起来非常省事。第三它在统计上对应着对数几率log-odds的逆变换和最大似然估计天然契合。1.3 假设函数读作“概率”逻辑回归的完整假设函数写成h(x) g(θ^T x) P(y 1 | x; θ)读作在给定参数 θ 和特征 x 的条件下样本属于类别1的概率。这个概率是连续值但我们通常把它和阈值0.5比较大于等于0.5判为正类小于0.5判为负类。有个很容易忽略但又重要的细节这里的“0.5”只是一个默认值不是必须的。我们完全可以根据业务需求调整阈值。比如垃圾邮件识别宁可误判一些正常邮件为垃圾也不想漏掉任何一封垃圾邮件那就可以把阈值调低到0.3。这个调整在逻辑回归的架构里成本极低但很多新手完全不知道。后面我会专门展开讨论。2. 二分类把逻辑回归从公式变成能跑的代码二分类是逻辑回归最基本、也是最核心的场景。多分类到最后往往也会拆成多个二分类来做所以先把二分类彻底吃透非常关键。2.1 决策边界模型到底学了一条什么线逻辑回归最终要学的是参数 θ。有了 θ 之后模型对样本的判断规则是如果 θ^T x 0就判为正类否则判为负类。所以 θ^T x 0 这条线就是所谓的决策边界。如果特征 x 只有两个维度决策边界在二维平面上就是一条直线。但这里有一个特别值得玩味的点决策边界的形状完全取决于“特征怎么构造”而不取决于逻辑回归本身。什么意思呢如果我们只用原始特征 x1、x2那边界就是一条直线只能解决线性可分的问题。但如果我把特征扩展成 x1、x2、x1²、x2²那么 θ^T x 0 在原始特征空间中就变成了一个圆或者椭圆。例如一个经典例子假设数据在二维平面上的分布是一个圆环圆心附近是负类圆环外部是正类。只用 x1、x2 两个维度任何直线都无法正确分开。但加入 x1² 和 x2² 之后就可以学习出一个类似圆形的边界。这个思路非常实用在很多实际项目里我们并不急着换更复杂的模型而是先对原始特征做多项式扩展、交叉项、平方项再用逻辑回归试一试往往就能解决很多“非线性”问题。2.2 代价函数为什么偏偏要取log逻辑回归的损失函数长这样J(θ) -1/m * Σ [ y*log(h(x)) (1-y)*log(1-h(x)) ]我最早看到这个式子的时候第一反应是为什么不能继续用线性回归的平方误差实验结果会告诉你答案如果用平方误差损失函数关于 θ 是非凸的会有大量局部极小值梯度下降大概率跑进某个不理想的山谷里出不来。而上面这个取log的形式从极大似然估计出发推出来整个函数是凸的——只有一个全局最小值梯度下降可以放心跑。再从直觉层面理解一下这个 log 损失当 y 1 时损失函数是 -log(h(x))。如果模型预测 h(x) 接近1说明预测对了损失接近0如果 h(x) 接近0说明预测错了损失趋向无穷大。y 0 时同理对称。这个设计保证了对“错误非常自信”的预测施以重罚对正确预测不闻不问。2.3 梯度下降更新式长得像线性回归但内核变了对代价函数求偏导之后得到梯度下降更新式θ : θ - α * (1/m) * X^T * (h(x) - y)如果只看这个式子它和线性回归的梯度下降几乎一模一样。这也是很多教程会说“逻辑回归和线性回归的更新公式表面上是一样的”的原因。但要注意这里的 h(x) 是经过了 sigmoid 压缩的概率不是线性输出。所以两者虽然形式神似代表的模型性质完全不同。我在手动实现的时候会特别注意一个实现层面的细节sigmoid 函数里如果 z 是个绝对值很大的负数比如 -1000计算 e^(-z) 会变成 e^1000直接溢出变成 inf导致后面的计算全部崩掉。所以在代码里一般会对 sigmoid 做一下数值上的保护或者统一可以先对输入做标准化让 z 不会出现过分极端的值。2.4 一个最小可用的二分类实现理论讲再多不如动手跑一遍。下面这个实现只用 numpy没有调用任何高级机器学习库方便看清楚每一步在做什么import numpy as np def sigmoid(z): # 防止 exp 溢出z 过大时exp(-z) 下溢为0结果接近1z 过小时提前归0 z np.clip(z, -500, 500) return 1 / (1 np.exp(-z)) def compute_loss(X, y, theta): m len(y) p sigmoid(X theta) # 加一个极小值防止 log(0) eps 1e-15 loss -1/m * np.sum(y * np.log(p eps) (1 - y) * np.log(1 - p eps)) return loss def train_logistic(X, y, learning_rate0.1, num_iters1000): m, n X.shape theta np.zeros(n) loss_history [] for i in range(num_iters): p sigmoid(X theta) grad (1/m) * X.T (p - y) theta - learning_rate * grad if i % 100 0: loss_history.append(compute_loss(X, y, theta)) return theta, loss_history训练之前记得把原始特征拼一列全1作为偏置项。如果特征尺度差很大建议先做标准化。这些我在后面第4节会详细讲。真的把这段代码放到一份简单数据上跑你会看到 loss 逐步下降决策边界慢慢逼近两类样本的分界位置。这份直觉比直接调用某个库要珍贵得多因为当你之后用封装好的库遇到问题时你知道猜应该往哪个方向猜。3. 多分类从二分类到多分类的两条常用路线标题里“二分类与多分类”并不是两个并列的算法而是逻辑回归在同一思路下的两种应用层次。多分类从来不是一个“新模型”而是把二分类的逻辑复用起来。3.1 一对多One-vs-Rest / One-vs-All训练 K 个二分类器假设我们有 K 个类别记为 0,1,...,K-1。一对多的做法是为每个类别单独训练一个逻辑回归分类器。训练类别 i 的分类器时把类别 i 的样本当作正类y1其余所有类别的样本都当作负类y0。预测的时候把样本分别扔进 K 个分类器里得到 K 个概率值最后取输出概率最大的那个类别作为预测结果prediction argmax_i( p_i(x) )这个思路极其简单而且工程实现成本很低。你手头只要有一个还不错的二分类逻辑回归实现套一层循环就能支持任意的多分类任务。很多成熟的机器学习库在好久以前也是这么做的直到后来才把多类损失函数直接集成进优化器。有一个细节需要注意在一对多方案里K 个分类器各自的概率输出之间并不是严格可比的。因为每个分类器的训练数据正负比例不一样模型对“概率”的估计会带有各自的偏向。所以实际应用中有人会在每个分类器内部做一下校准或者干脆只比较“决策函数值”而不是概率。但在大多数入门场景里直接比较概率已经够用。3.2 Softmax一次前向直接输出全类别概率Softmax 是多分类问题的另一个更“现代”的解法。它不需要训练多个分类器而是把线性得分 z_i θ_i^T x 并行算出 K 个值然后用 softmax 函数将它们归一化成一组和为1的概率p_i e^(z_i) / Σ_j e^(z_j)分子分母都是指数函数结果天然满足非负性和归一性而且整个计算过程是端到端可微的训练时直接用一个多分类交叉熵损失梯度回传一次就把所有类别的参数都更新了。这组公式和人脸识别、自然语言处理里常见的分类层是同一个东西。可以说Softmax 现在已经是多分类模型的事实标准。有意思的是当 K2 时Softmax 和 sigmoid 在数学上是等价的。你可以自己推一下两个类别的 Softmax 输出会被化简成 sigmoid 形式。这就是为什么有些课程讲逻辑回归时会把 Softmax 当作“逻辑回归的多分类延伸”而不是另一个独立算法。3.3 一对多 vs Softmax到底怎么选这是我在带新人时经常被问到的问题。选哪个可以从这几个维度看对比维度一对多OvRSoftmax模型数量K 个独立二分类器1 个多分类模型训练开销K 倍二分类训练时间一次训练输出形式K 个独立概率不一定和为1一组概率严格和为1解释性每个类别有独立的系数便于单独分析参数共享一套优化目标整体性强适用范围类别较少时简单直接类别较多、且类别间有互斥关系时更稳以前很多传统场景偏向 OvR因为二分类器可解释性强也方便分布式训练。现在深度学习时代几乎清一色使用 Softmax。但如果你是手工实现逻辑回归或者处理小规模数据OvR 依然是非常务实的选择。另外要提一句多分类任务有一个隐含前提——类别互斥。如果一个样本可能同时属于多个类别比如一张图片里既有人又有猫那就不是“多分类”问题了而是“多标签”问题。多标签标准做法是训练多个独立的二分类器每个类别一个这和 OvR 在实现上很像但损失函数和评估方式不同。这个区别容易被忽略但实际价值很高。3.4 一个多分类的落地示范下面给一个小例子演示 OvR 思路怎么直接套用之前写的二分类函数。假设我们的数据 X 有3个类别先给每个类别生成一份二分类标签def one_vs_rest_train(X, y, num_classes, learning_rate0.1, num_iters1000): models [] for c in range(num_classes): y_binary (y c).astype(int) theta, _ train_logistic(X, y_binary, learning_rate, num_iters) models.append(theta) return models def predict_ovr(X, models): # X 是 (m, n) 特征矩阵 prob_matrix np.zeros((X.shape[0], len(models))) for i, theta in enumerate(models): prob_matrix[:, i] sigmoid(X theta) return np.argmax(prob_matrix, axis1)这里predict_ovr返回的是概率最大的那个类别索引。如果你改用 Softmax只需要把sigmoid(X theta)换成对全类别线性得分做一次 softmax 归一化就行。很多做课后实验的同学会在这里想当然地跳过“数据标准化”直接学。我试过特征尺度不统一时梯度下降会非常辛苦甚至出现 loss 震荡。原因很简单不同特征的数值范围差异大导致参数更新在某个方向上前进特别快在另一个方向上前进特别慢整个优化路径像走之字形。4. 训练过程的手感学习率、特征缩放与正则化这一节说的东西看似不是逻辑回归本身但真正决定一个分类器能不能用起来的往往是这些“手感的细节”。4.1 特征缩放让梯度下降别走得那么痛苦逻辑回归的参数更新依赖梯度。如果某个特征数值范围是 0~1另一个特征数值范围是 0~10000那么同一套学习率对两个方向的参数更新步长影响差异巨大。前者可能更新太慢后者可能更新太大导致震荡甚至发散。常用的处理是标准化standardizationx (x - mean) / std把每个维度的均值归零、方差归1。做完这步之后梯度下降的收敛速度会肉眼可见地变快。数据标准化后学到的参数是针对标准化后特征的预测时也必须用训练时统计的均值和标准差对新的样本做同样处理。这一点新手经常忘记导致模型上线后效果崩掉。我习惯在训练前保存好 mean 和 std 两个数组和模型参数一起存起来。4.2 学习率怎么调先看损失曲线再微调数值学习率没有银弹只能靠观察和迭代。我调学习率的顺序一般是这样的先设一个很小的值比如 0.001训练几百轮看 loss 曲线是否稳定下降。如果 loss 下降极慢说明学习率太小增大到 0.01、0.1。每次改大一个数量级跑50轮左右观察。如果 loss 开始震荡甚至变成 NaN说明学习率过大退回上一个数量级再减半试试。找到大致合理的范围后再在这个范围内做小步微调。这里有三个典型的 loss 曲线形态需要知道曲线缓慢下降但始终降不下去 —— 学习率太小或者特征没有标准化曲线一开始就剧烈上下跳动 —— 学习率太大优化在跨越山谷两侧曲线前期正常后期突然发散 —— 可能是因为数据里有异常值或者学习率仍然偏大需要降低我还见过一种情况loss 曲线平稳下降但最终停在了一个较高的值不继续降。这往往不是学习率问题而是模型容量不足或者特征表达不够比如该加二次项没加。这时候加特征比调学习率更有效。4.3 正则化参数怎么选过拟合和欠拟合之间找平衡逻辑回归的正则化是在损失函数后面加一项惩罚让参数不要过大。常用的有 L1 和 L2 两种。L1 会让一部分参数变成0有特征选择的效果L2 只会让参数整体变小不会变成0。小规模特征场景先试 L2 比较稳。带正则的损失函数是这个样子J(θ) 原损失 λ * (1/2m) * Σ θ_j² (L2正则)λ 是正则化强度。λ 太小模型会过度拟合训练数据训练集和验证集差距大λ 太大模型参数被压得太狠所有特征的影响都被弱化甚至出现欠拟合训练集误差都降不下来。我一般不会一上来就调 λ而是先不设正则化让模型在训练集上勉强过拟合。这有两个目的一是确认模型和代码逻辑没问题方向是对的二是拿到一个“训练集误差下界”。然后我再加上正则化从 λ 0.01 开始试慢慢拉到 0.1、1、10每档观察验证集准确率和损失曲线。画一张训练误差和验证误差随 λ 变化的曲线是判断过拟合最有效的方式。4.4 样本不均衡0.5 这个阈值只配当起点实际业务很少像课堂数据那样两类各占一半。比如点击率预测任务里正样本可能只有1%负样本99%。这时候如果死守0.5这个阈值模型会为了降低整体损失而把几乎所有样本都预测成负类——毕竟这么做准确率都已经99%了。所以二分类场景下要养成看“混淆矩阵”和“精确率/召回率”的习惯不要在准确率上自欺欺人。阈值怎么定要看业务更在乎什么短信拦截更在乎召回率宁肯多拦一些正常短信也不能漏掉垃圾短信阈值调低风险交易识别更在乎精确率误报一次很伤用户体验阈值调高两种指标都要兼顾就画 PR 曲线选曲线拐点附近的位置这里还有一个稍微进阶的处理方案在训练时给少数类样本更高的权重相当于人为提高少数类样本在损失函数中的贡献。有些库直接提供了class_weight参数是同一个思路。这项操作会让概率输出整体偏正所以阈值选择还是要回到业务目标上来。5. 常见问题与排查技巧实录最后这一部分整理了我在实际运行逻辑回归时真正碰到过的几类问题以及对应的排查思路。很多问题不是代码写错了而是对模型运行的某些细节理解不到位。5.1 loss 出现 NaN 或者完全不下降NaN 出现的原因基本就三种学习率太大、数据中有 NaN、exp 计算溢出。排查顺序也按这三步来。第一步看学习率。如果设了 0.5 或 1.0 这样偏大的值梯度更新会跳出合理范围参数直接冲上天后续计算全部溢出。先把学习率降到 0.01 试试。第二步检查数据。用np.isnan(X).any()扫一遍缺失值没处理好就喂进来了梯度计算时也会产生 NaN。第三步优化 sigmoid 计算。把长尾的溢出问题用np.clip(z, -500, 500)处理掉基本能根治。loss 完全不下降而且不是 NaN问题通常出在特征上。比如所有特征都是0模型梯度恒等于0或者特征之间有强共线性参数更新方向被抵消。我遇过一次很隐蔽的问题数据里某列全是一个常数标准化之前没问题标准化之后这一列全变成了0模型等于丢掉了一个维度。5.2 预测结果全是一种类别如果模型把几乎所有样本都判成多数类先别急着怪模型。做一个简单的基线测试直接看训练集里各类别的占比。如果正类只占5%一个输出全是负类的模型准确率就是95%这不一定说明模型坏了而是说明它找到了一个“懒惰”的优化路径。这时候从两个方向处理。一是在训练层面做类别平衡调整损失权重或者对多数类做降采样。二是在预测层面调阈值计算验证集上的 PR 曲线找一个兼顾精确率和召回率的切分点。千万不要一上来就尝试复杂的集成模型逻辑回归还扛得住这些问题。5.3 多分类概率和不为1或者预测结果不稳定如果是自己用 OvR 方式实现的多个二分类器的输出概率加起来不等于1是正常的。它们的训练目标互相独立概率尺度没有对齐。如果强行拿这些概率做阈值判断结果很容易打架。解决办法有两个。简单粗暴一点的预测时直接取最大概率的类别不管它们的绝对数值。稍微精细一点的先对每个分类器的输出做个标准化把概率缩放到相似的尺度上再做 argmax。再要不就干脆换成 Softmax从设计上规避这个头大问题。预测结果不稳定通常是数据噪声大、特征太少、或者模型过拟合。逻辑回归本身是稳定的线性模型不该出现大幅波动。如果同样的输入今天预测这个、明天预测那个大概率是训练数据里存在严重的不一致性或者是标准化的时候用了不同的均值和标准差。5.4 我自己的推荐调试顺序拿一份新数据跑逻辑回归时我会按下面这个顺序来能省下很多没必要的折腾先拿原始数据跑一次设学习率 0.01、迭代 500 轮确认代码链路通畅。观察 loss 曲线是否正常下降。不正常的话先检查特征缩放和 NaN再调学习率。训练集能过拟合之后划分训练集/验证集看验证集指标重点看混淆矩阵而非准确率。加正则化画验证误差随 λ 变化的曲线选一个合理的 λ。如果最终准确率仍然不满意再加特征、加特征组合或者换模型。这套流程在大多数入门分类问题上都走得通。逻辑回归不是一个花哨的模型它最大的价值在于简单、可控、可解释。当你把它的二分类和多分类部分吃透后面学任何更复杂的分类模型都会快得多。