ARTICLE DETAIL

资讯详情

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

Logistic回归全解:从线性模型到分类概率与非线性变换

Logistic回归全解:从线性模型到分类概率与非线性变换 这门课讲到L4终于从连续值预测切到分类任务了。如果你是第一次接触模式识别和机器学习这一讲会是个分水岭——很多同学前几讲跟着推公式还觉得轻松到Logistic回归突然开始犯迷糊原因很简单这个算法名字里带着“回归”两个字干的却是分类的活而且它牵扯到的损失函数、梯度推导、特征变换每一个点都能单独出成一道大题。这篇笔记把这一讲讲透Logistic回归是怎么从线性模型里长出来的、它的损失函数为什么长那样、非线性变换到底在变换什么、以及期末复习时那些反复出现的考点到底应该怎么答。无论你是在刷吴恩达的课还是在为周志华那本书的课后题头疼这篇都可以直接拿来当复习提纲用。1. 从回归到分类Logistic回归到底解决了什么问题1.1 线性回归在分类任务上翻车的现场先看一个最常见的例子假设肿瘤医院收集了一批病例特征是肿瘤大小标签是“良性/恶性”分别记为0和1。你可能会想这还不简单直接用线性回归去拟合这个0/1标签不就行了实测下来你会发现一个很尴尬的现象拟合出来的直线对于特别大的肿瘤尺寸预测值能飙到1.3甚至2.0对特别小的尺寸预测值可能是-0.5。你说这算恶性还是良性完全没法解释。有人会说那我加一个阈值——预测值大于0.5就判为恶性小于0.5就判为良性这不就行了吗问题是线性回归对异常值极其敏感。假设数据里出现一个巨无霸肿瘤尺寸远超其他样本最小二乘为了拟合这个点会把整条直线往下压导致原来一些恶性样本的预测值低于0.5直接判错。这不是调参能解决的问题而是线性回归的本质就是让预测值逼近实数而你需要的输出是一个“概率”。另一个更隐蔽的问题线性回归用来拟合0/1标签时损失函数是均方误差但均方误差对分类任务来说并不是一个好的度量。分类错误的后果不应该是“预测值偏离标签的平方差”而应该是“这个样本被判错了”这两者的数学性质完全不同。所以分类任务需要的不是数值拟合而是概率建模。1.2 分类问题的本质估计概率而不是拟合数值把问题重新定义一下给定特征x我们想知道的是P(y1|x)也就是这个样本属于正类的概率。注意这个概率天然落在[0,1]区间内。而线性回归给出的 w^T x b 是一个实数范围是负无穷到正无穷。所以核心矛盾变成了怎么把一个实数轴上的值压到(0,1)区间里并且保持单调性——线性组合越大概率越高。这就是Logistic回归的出发点。它做的事可以拆成两步第一步仍然保持线性组合 z w^T x b第二步套一个sigmoid函数 σ(z) 1 / (1 e^{-z})把z映射到(0,1)。这个函数在z0时取值0.5z趋向正无穷时趋近1z趋向负无穷时趋近0刚好满足概率的所有性质。这里需要强调一个关键认知分类问题的误差度量方式天然是“0/1损失”——分对就是0分错就是1。但0/1损失在数学上不可导梯度下降没法用。所以Logistic回归选择用一种“替代损失”这就是下一章要讲的交叉熵。理解这条逻辑链很重要期末如果考“为什么分类不用线性回归”或“为什么损失函数是交叉熵”你只需要把这条链讲清楚基本就满分了。2. Logistic回归的核心数学原理从对数几率到交叉熵2.1 Sigmoid函数与对数几率两个角度推导出同一个模型先直接给出sigmoid的形式σ(z) 1 / (1 e^{-z})其中 z w^T x b。模型输出 p σ(z)我们把它解释为P(y1|x)的估计值。如果p 0.5判为正类p 0.5判为负类。决策边界就是 p 0.5即 w^T x b 0这在特征空间里是一条直线或超平面。但为什么一定是sigmoid而不是别的函数这里面有一个很漂亮的推导假设对数几率log-odds与特征x是线性关系也就是log(p / (1-p)) w^T x b这个式子左边叫“对数几率”几率odds指的是事件发生概率与不发生概率的比值 p/(1-p)。一个事件发生的几率越大说明它越可能发生。对几率取对数后取值就是整个实数轴可以和右边的线性组合完全匹配。把这个式子反解出来p / (1-p) e^{w^T x b}p e^{w^T x b} / (1 e^{w^T x b}) 1 / (1 e^{-(w^T x b)})正好就是sigmoid函数。所以Logistic回归的名字里虽然有“回归”但它其实是“对对数几率做线性回归”本质上是个分类模型。这个考点几乎年年出现 “Logistic回归为什么叫回归” 标准答法就是这个对数几率推导。2.2 损失函数为什么是交叉熵而不是均方误差模型输出的是一个概率p真实标签y是0或1。我们希望p尽量贴近y当y1时p越大越好当y0时p越小越好。把这两种情况统一成一个表达式就是单样本损失L -[ y * log(p) (1-y) * log(1-p) ]这就是二分类交叉熵损失。为什么用log因为log是单调递增函数而且当预测完全错误时例如y1但p接近0-log(p)会趋向正无穷惩罚非常重当预测正确时惩罚趋近0。这种“错得越离谱、罚得越狠”的性质正符合分类任务的需求。那为什么不用均方误差(MSE)呢这是考试最爱考的一个点。把sigmoid代入MSE得到的损失函数关于参数w是非凸的存在多个局部极小值梯度下降很可能陷进去出不来。而交叉熵配上sigmoid在数据满足一定条件下是关于w的凸函数凸函数只有一个全局最小值梯度下降能可靠收敛。另外从最大似然的角度看交叉熵恰好是“伯努利分布假设下的负对数似然”。也就是说Logistic回归的损失函数是有概率理论基础支撑的不是随便拍脑袋定的。2.3 梯度下降与参数更新规则有了损失函数接下来就是求梯度、更新参数。对单个样本令 z w^T x bp σ(z)可以算出∂L/∂w (p - y) * x∂L/∂b p - y这个结果简洁得让人惊讶。每个样本对梯度的贡献就是“预测残差(p-y)”乘以该样本的特征向量x。参数更新规则就是w ← w - η * (p - y) * xb ← b - η * (p - y)其中η是学习率。注意这个梯度形式里没有出现sigmoid的导数项σ(z) σ(z)(1-σ(z))。这正是交叉熵的巧妙之处如果换用MSE梯度里会多乘一个σ(z)而sigmoid两端趋于饱和σ(z)接近0会导致参数几乎不更新也就是梯度消失。所以“交叉熵sigmoid”的组合在数值上远比“MSEsigmoid”稳定训练速度也快得多。这是我实际跑实验时的切身体会一开始没理解这点换过MSE试了一次loss曲线直接躺平。3. 非线性变换线性模型如何学会“拐弯”3.1 线性模型的边界限制从异或问题说起线性模型的决策边界是超平面这既是它的简单之处也是它的天花板。最经典的例子是异或XOR问题四个点(0,0)、(1,1)属于一类(0,1)、(1,0)属于另一类。你画一条直线试试无论如何都分不开这两类。另一个例子就是两个同心圆环的分布内圈和外圈各为一类线性分类器只能找到一条直线无论怎么摆都会把两类切得乱七八糟。这些问题迫使我们去思考如果原始特征空间里线性不可分那能不能换一个特征空间让数据在新的空间里变得线性可分这就是非线性变换的核心思想。这里有必要提一下Cover定理在高维空间中数据被线性可分的概率会随着维度增加而提高。换句话说特征维度越高分起来越容易但代价是什么下面马上讲。3.2 多项式特征与基函数展开具体怎么构造非线性变换最简单的落地方式就是构造多项式特征。假设原始特征只有两个x1、x2那我们把它扩展成φ(x) (1, x1, x2, x1², x1*x2, x2²)也就是说从2个特征变成6个特征。在这个6维空间中我们仍然用线性模型Logistic回归但找到的超平面会对应回原始空间的一条曲线。拿圆环数据举例内圈和外圈在原始二维空间里是线性不可分的但只要你构造一个新特征 r² x1² x2²问题一下就简单了——内圈半径小r²小外圈半径大r²大。在“原始特征新特征”组成的空间里用一条直线就能分开。这就是非线性变换最直观的威力把原始空间里的曲线边界转化成新空间里的直线边界。更一般地我们可以用基函数展开basis function expansion来构造特征比如高斯基函数、傅里叶基、样条基等。每个基函数负责捕捉局部或全局的一种形态。这也是“广义线性模型”的思想先做特征变换φ(x)再把φ(x)喂给线性模型。逻辑上完全自洽实现起来也简单只需要在数据预处理阶段加一步PolynomialFeatures。3.3 维度爆炸与过拟合非线性变换不是免费午餐非线性变换听起来很香但它有一个致命的代价特征数量会爆炸式增长。如果原始特征有m个用d次多项式展开特征数量大约是O(m^d)。假设m10d5特征数量就能到上千。特征多了参数也多了模型就特别容易把训练样本背下来而不是学到真正的规律——这就是过拟合。判断是否过拟合有个很实际的方法看训练集和验证集的准确率差距。训练集98%、验证集70%基本就是过拟合了。解决方法无非三条路一是降低多项式次数别一味加高二是加正则化L1能帮你把无关特征的系数压成0L2能把所有系数整体缩小三是增加数据量但数据往往是最难搞的。这里埋个伏笔后面讲到的核方法Kernel Method本质就是“既想做高维变换又不想显式计算高维特征”的聪明解法它用核函数直接计算高维空间的内积绕开了维度爆炸问题。L4这一讲你先理解线性变换→非线性变换这条逻辑线就够了。4. Python实操从零手写到sklearn调参4.1 数据准备构造一个线性不可分的例子理论讲了这么多不跑代码等于白讲。我经常用sklearn自带的make_circles来演示非线性变换的效果import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_circles X, y make_circles(n_samples300, factor0.5, noise0.05, random_state42) plt.scatter(X[y 0, 0], X[y 0, 1], labelclass 0) plt.scatter(X[y 1, 0], X[y 1, 1], labelclass 1) plt.legend() plt.show()这个数据长什么样呢外面的圆是一类里面的小圆是另一类肉眼可见地需要一条曲线才能分开。现在直接拿原始特征去训练Logistic回归看看效果如何。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, random_state42) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) model LogisticRegression() model.fit(X_train_scaled, y_train) print(训练集准确率:, model.score(X_train_scaled, y_train)) print(测试集准确率:, model.score(X_test_scaled, y_test))实测下来训练集和测试集准确率都在0.5左右跟瞎猜没区别。这印证了上面的结论线性模型在原始特征空间里拿这个数据没办法。注意我在这里先做了StandardScaler这个步骤不是可选项——Logistic回归用梯度下降求解时特征的尺度如果不一致梯度更新会来回震荡收敛速度极慢后面第5章我们会再聊这个问题。4.2 手写一个Logistic回归只有三十行代码为了让你彻底搞懂内部机制我用numpy把整个训练过程手写一遍。别怕核心就四个部分sigmoid、损失计算、梯度计算、参数更新。class LogisticRegressionScratch: def __init__(self, lr0.1, epochs500): self.lr lr self.epochs epochs self.w None self.b 0.0 def sigmoid(self, z): return 1 / (1 np.exp(-z)) def loss(self, X, y): p self.predict_proba(X) # 加一个极小值防止log(0) return -np.mean(y * np.log(p 1e-9) (1 - y) * np.log(1 - p 1e-9)) def predict_proba(self, X): z X self.w self.b return self.sigmoid(z) def fit(self, X, y): n_samples, n_features X.shape self.w np.zeros(n_features) for epoch in range(self.epochs): p self.predict_proba(X) # 梯度见2.3节推导 grad_w (X.T (p - y)) / n_samples grad_b np.mean(p - y) self.w - self.lr * grad_w self.b - self.lr * grad_b if epoch % 100 0: print(fepoch {epoch}, loss {self.loss(X, y):.4f}) def predict(self, X): return (self.predict_proba(X) 0.5).astype(int)训练过程用我们手写的模型跑一遍scratch_model LogisticRegressionScratch(lr0.1, epochs1000) scratch_model.fit(X_train_scaled, y_train) print(手写模型测试集准确率:, np.mean(scratch_model.predict(X_test_scaled) y_test))你会发现loss在慢慢下降但准确率依旧只有0.5左右。这说明什么说明问题不在优化算法而在特征空间本身。这就引出了下一步的重头戏——非线性变换。4.3 加入非线性变换后再训练现在我们对特征做二阶多项式展开再喂给同一个模型from sklearn.preprocessing import PolynomialFeatures poly PolynomialFeatures(degree2) X_train_poly poly.fit_transform(X_train_scaled) X_test_poly poly.transform(X_test_scaled) scratch_model_poly LogisticRegressionScratch(lr0.1, epochs1000) scratch_model_poly.fit(X_train_poly, y_train) print(变换后训练集准确率:, np.mean(scratch_model_poly.predict(X_train_poly) y_train)) print(变换后测试集准确率:, np.mean(scratch_model_poly.predict(X_test_poly) y_test))这次你再看结果训练集和测试集准确率都能到1.0。需要注意的是多项式特征会把原始特征的多项式交叉项全部算出来。以degree2为例原始特征[x1, x2]会变成[1, x1, x2, x1², x1*x2, x2²]其中1是偏置项对应的常数特征。如果你的模型已经单独学了偏置b那这个1就有些冗余但不会影响训练sklearn内部会处理好。做完这个实验你会发现真正让模型“拐弯”的不是Logistic回归本身而是喂给它的特征。Logistic回归始终是一个线性模型是特征变换让决策边界从直线变成了曲线。这个认知很重要因为在很多教材里Logistic回归被称为“线性分类器”但实际工程里大家都会配合特征工程来用。你既不能说它是纯线性也不能说它是纯非线性关键看你喂什么特征。4.4 sklearn版本与关键参数调优实际项目里当然不需要手写梯度下降直接用sklearn就好但参数你得读懂。from sklearn.pipeline import Pipeline pipe Pipeline([ (poly, PolynomialFeatures(degree2)), (scaler, StandardScaler()), (clf, LogisticRegression(C1.0, penaltyl2, solverlbfgs, max_iter1000)) ]) pipe.fit(X_train, y_train) print(pipeline测试集准确率:, pipe.score(X_test, y_test))Pipeline把特征变换、标准化、分类器串在一起防止数据泄漏这是工程里的标准写法。这里重点解释几个参数C正则化强度的倒数。C越小正则化越强模型越不容易过拟合但也可能欠拟合C越大模型越倾向拟合训练数据越容易过拟合。我一般按 0.001、0.01、0.1、1、10、100 这个量级去试。penalty可选l1或l2。l1会把部分特征的系数压成0相当于自动特征选择l2会把所有系数均匀缩小是默认选择。数据特征多且有冗余时可以试试l1。solver优化算法的选择。小数据集推荐liblinear大数据集推荐lbfgs如果penalty是l1liblinear和saga都支持lbfgs不支持。max_iter最大迭代次数。如果你看到“ConvergenceWarning: Maximum iterations reached”说明没收敛两条路加大max_iter或者对特征做标准化。多数情况下是后者没做好。用不同C值去调一遍你会发现C0.01时测试集准确率可能是0.98C100时可能会掉到0.94因为模型过拟合了训练集里包含噪声的那个圆环边界。这就是前面讲的“非线性变换不是免费午餐”——变换能力越强越需要正则化来踩刹车。5. 常见错误排查与期末高频考点5.1 训练Loss不降或震荡问题出在哪手写模型最大的坑我挨个踩过先列一个速查表现象可能原因解决方式loss完全不动学习率太小调大lr比如从0.01调到0.1或0.5loss剧烈震荡学习率太大调小lr或用学习率衰减效果很差但loss正常没做特征标准化加StandardScalerloss直接nan特征含有极端值/梯度爆炸标准化 减小学习率 检查是否有inf二分类但输出三个值标签传成了稀疏矩阵用y.ravel()或保证y形状为(n_samples,)有一个我特别想强调的点学率率的设置。手写模型里我用的是固定学习率但实际训练中随着loss逐渐变小固定学习率可能会导致最后阶段在最优值附近来回震荡。简单做法的调小学习率高级点的可以用Adam这类自适应优化器但那是后面优化专题的内容。另外如果你发现加非线性变换后loss降得更快了别高兴太早。高维特征会让模型表达能力变强但也可能让训练loss降到非常接近0然后在测试集上崩掉——典型的过拟合。一定要看测试集的表现不能只看训练loss。5.2 过拟合怎么判断与处理判断过拟合我有个非常直观的经验把训练出的模型系数w打出来如果某些系数的绝对值大得离谱比如上千那大概率过拟合了。因为模型在用很大的系数去强行扭曲决策边界好把训练集里那些离群点和噪声点也分对。处理手段按优先级排序降低多项式次数。不要一上来就degree5先从2、3试起。调大C其实是降低正则化强度不对——是调小C来增强正则化。注意方向别搞反C是正则化强度的倒数。换L1正则化让模型自动丢弃无关特征。增加样本量或者做数据增强。这里有张我常用的对照表C值正则化强度训练集准确率测试集准确率判断100弱1.000.93过拟合1中0.990.98合适0.01强0.940.94欠拟合从这个表能看出正则化太强会压制模型的表达能力导致训练集都学不好太弱又会死记硬背。交叉验证就是干这个的找个验证集调C别用测试集调参否则测试集就“脏”了。5.3 期末高频考点与易错点速查每到期末总有同学拿着各种版本的复习资料来问我其实考点翻来覆去就这些。这里按出现频率整理一份清单考点标准答法要点Logistic回归是回归还是分类分类模型。“Logistic回归”名字来源于对“对数几率”做回归为什么用sigmoid函数把线性组合映射到(0,1)使其可解释为概率从对数几率推导自然得出为什么损失用交叉熵而不用MSEMSEsigmoid非凸梯度会乘sigmoid导数易梯度消失交叉熵是凸函数且对应伯努利负对数似然写出梯度更新公式∂L/∂w (p-y)xw ← w - η(p-y)x决策边界是什么w^T x b 0是一条直线或超平面非线性变换的作用把原始特征映射到高维空间让线性模型能处理非线性可分数据非线性变换的代价特征维度爆炸、过拟合风险需要正则化控制二分类如何扩展到多分类OvR一对多、OvO一对一或softmax回归多分类的Logistic回归推广Logistic回归与线性回归的联系都是广义线性模型区别在于输出经过sigmoid且损失函数不同另外有一个非常容易丢分的细节sigmoid的输出 p 到底是不是真实的概率准确说p 是模型对条件概率 P(y1|x) 的一个估计但它不一定是“校准”过的概率——就是说如果模型输出0.7不代表100个这样的样本里一定有70个是正类。如果想要校准的概率需要做Platt Scaling之类的事后校准。这个概念很多教材放在高级部分但试卷里如果考到“Logistic回归输出的概率是否可信”你要能说出这一层。我个人在带期末复习时发现大家最容易栽的坑是把Logistic回归当成一个黑盒公式能默写但一被问“为什么这里用log”就卡住。所以这一讲真的不要跳着看把第2章的推导自己闭卷推两遍再把第4章的代码完整跑一遍比你考前刷十道选择题都管用。这也是为什么我把推导和代码放在一起写——它们本来就是同一件事。
返回列表