☰
Logistic回归实战:从疝气病马预测到梯度上升优化全解析
2026/10/20 7:11:56 网站建设 项目流程

1. 为什么偏偏是Logistic回归:疝气病马预测的核心问题与算法适配

1.1 问题本质:从"马会不会死"到二分类预测

在《机器学习实战》这本书的第五章里,作者用了一个很有代表性的数据集——疝气病马的存活预测。这个案例我前前后后复现了好几遍,每次都有新体会。先说说这个场景本身:我们的输入是马儿的一系列生理指标,包括消化道问题、肢体情况、排尿状况、体温、脉搏等体征特征,输出只有一个结论——这匹马是否能够存活。

这是一个标准的二分类监督学习问题。但在动手之前,我们需要想清楚一个关键问题:为什么在这个场景里,大哥级别的支持向量机、树模型没有成为首选,偏偏是Logistic回归被单独拿出来讲?

答案在于数据的特性。疝气病马的数据集来自UCI,一共368个样本、22个特征,样本量并不大。更关键的是,这个数据集缺失值非常多,接近三分之一的特征列都有空缺记录。这种数据如果直接喂给SVM或者树模型,处理起来会非常折腾。而Logistic回归对缺失值有一种天然宽容——我们后面会讲到,缺失值统一填0在sigmoid函数框架下是一个极其巧妙的选择。

另外,这个问题本质上是在估概率:这匹马"有多大的可能"活下来。Logistic回归输出的不是一个硬性的0或1,而是一个介于0到1之间的概率值。对于医疗类预测场景,我们往往希望得到的不只是一个结论,还有结论的置信度。比如医生说"这匹马大概率能活",如果后面跟着一个85%和跟着一个52%,处理方式完全不一样。Logistic回归天然支持这种输出形式。

1.2 sigmoid函数:把一切打分映射成概率的那道阀门

Logistic回归的核心引擎是sigmoid函数,它的形式是:

[ \sigma(z) = \frac{1}{1 + e^{-z}} ]

这个函数做的事情非常朴素:接受任意实数输入,输出一个0到1之间的值。当z趋向正无穷,输出趋近1;z趋向负无穷,输出趋近0;z等于0时,输出正好0.5。

在模型里,z是我们对输入特征做的线性加权和:

[ z = w_0x_0 + w_1x_1 + w_2x_2 + \dots + w_nx_n ]

那么整个模型干的事情就是:先对马的22个体征特征做一个加权打分,再套上sigmoid这道阀门,把分数翻译成存活概率。如果概率大于0.5,预测为存活;否则预测为死亡。

我第一次跑这个案例的时候觉得它平平无奇,直到后来做其他项目踩了坑才真正理解:sigmoid这个函数的求导性质简直是专门为梯度优化设计的。它的导数为:

[ \sigma'(z) = \sigma(z)(1 - \sigma(z)) ]

这意味着在做梯度计算的时候,激活值本身就是导数的一部分,计算量大减。对比一下如果换成tanh或者ReLU,推导和实现都要再绕一些弯路。这也是为什么Logistic回归看起来简单,但在工程里生命力如此顽强的原因——它把"简单"和"有效"做到了极致的平衡。

2. 数据准备与缺失值处理:实战中最大的坑

2.1 数据长什么样:368个样本、22个特征的初步审视

先说说数据集的基本情况。疝气病马数据集记录了368个样本,每个样本包含28个原始属性,但在实践中我们通常用的是处理后的22个特征版本。这22个特征涵盖了马的常规生命体征、消化系统状态、四肢敏感度、黏膜颜色、毛细血管充盈时间等指标,最后一列是标签:1代表存活,0代表死亡。

需要提示的是,这个数据集不是一次成型的高质量数据,它本身就带着"野外采集"的痕迹。我在第一次拿到数据时习惯性地用pandas的describe()瞄了一眼分布,立刻发现很多列存在大量的NaN。这非常影响后续建模,因为绝大多数机器学习算法对NaN都是零容忍的。

特征说明表(简版):

特征类型示例缺失情况处理策略
生命体征脉搏、呼吸频率、体温少量缺失用0填充
消化系统腹部听诊、胃部状态中等缺失用0填充
四肢与神经疼痛反应、肢体状态部分缺失用0填充
黏膜与循环毛细血管充盈时间、黏膜颜色较多缺失用0填充

2.2 缺失值为什么必须用0填充:sigmoid的特殊性质

这是整个案例里最值得琢磨的一个细节。

大多数教科书或者博客会告诉你"把缺失值替换为0",但不说为什么。我当时也是一头雾水,0又不是均值也不是中位数,凭什么用0?后来仔细想了sigmoid的数学形式才恍然大悟。

问题出在模型的线性加权和z上。如果某个特征(x_i)缺失了,用0填充,那么无论学到的权重(w_i)是多少,(w_i \times 0 = 0),这个特征对整个加权和的贡献是0。换句话说,用0填充相当于告诉模型"这个样本的这个特征没有信息可用",模型不会因为它做出任何偏置判断。

如果改用均值填充,比如某个特征均值是53,那所有缺失样本在这一个维度上都会获得53的固定贡献。这个贡献被乘上权重后,会系统性地改变每个缺失样本的得分,相当于人为制造了偏差,而且这个偏差方向是一致的,模型很容易学到错误的偏移。

更有意思的是,对于标准化或者归一化后的特征,0往往恰好代表"平均水平"。但在这个案例里特征没有做标准化,所以0填充的核心价值就是"中性化"。这个处理方式配合sigmoid的"0点恰好是概率分水岭"的特性,意外地形成了一个简洁而鲁棒的缺失值方案。

2.3 特征包括哪些:从指标含义看特征工程的取舍

继续看数据集的细节。22个特征中有很多是有序分类变量,比如黏膜颜色从1到5分别代表不同的异常程度,毛细血管充盈时间也是分级记录的。这些有序分类变量在Logistic回归中被当作数值处理,严格来说略粗糙,但在样本量只有368的情况下,这种简化反而是务实的选择。

还有几个特征是二值变量,比如"是否发烧"这类。这些特征直接参与加权求和没有任何问题。但要注意,如果某个特征在所有样本里都几乎取同一个值,它的实际信息量就很低,后面会导致权重学习不稳定。我在实际复现中发现,这个数据集里确实有几个这样的特征,它们让收敛曲线出现小幅震荡,好在不影响最终的分类效果。

需要补充的一点是,我在跑实验之前通常会先检查一下特征是否已经做了标准化。如果特征尺度差异极大(比如有的特征取值范围0到100,有的只有0到1),梯度上升的收敛速度会非常慢。这本书的原版实现里没有显式做标准化,而是依靠调节步长来弥补。但对于新手,我强烈建议先在脑中有这个概念:尺度不一致时,特征的权重数值也会变得很难解释。

3. 从批量梯度上升到随机梯度上升:算法演进的完整脉络

3.1 梯度上升推导:为什么是上升而不是下降

很多初学者第一次看到"梯度上升"会懵一下——别的算法都在做梯度下降,怎么到这里就变成上升了?

原因在于优化目标不同。对于线性回归,我们的目标是最小化均方误差,所以要沿着负梯度方向走,做梯度下降。而Logistic回归的核心是极大似然估计——我们希望找到一组参数,使得当前这批样本在模型下的联合概率最大。

似然函数取对数之后是:

[ L(w) = \sum_{i=1}^{n} [y_i \log(\sigma(z_i)) + (1 - y_i) \log(1 - \sigma(z_i))] ]

我们的目标是让这个值变大,所以要沿着梯度方向前进,这就是梯度上升。

推导的过程我就不完整展开了,直接说结论:对单个样本而言,权重更新公式最终可以化简为:

[ w := w + \alpha (y - \hat{y}) \mathbf{x} ]

其中(\hat{y} = \sigma(z))是模型预测的概率,(y)是真实标签。这个形式非常漂亮:如果预测偏高((\hat{y} > y)),就朝相反方向调整权重;如果预测偏低,就朝正方向调整。整个逻辑和直觉高度吻合——每一个样本都在"纠正"模型的偏差,纠正的幅度由误差大小和步长共同控制。

初次推导这个公式时值得停一下。建议大家自己动手把梯度项的推导走一遍,搞清楚为什么sigmoid的导数恰好能把整个式子化简到如此简洁的形式。这个化简不是偶然,而是因为Logistic回归本身属于广义线性模型族,指数族分布的共轭性质保证了这种清爽的数学形式。

3.2 批量梯度上升的局限:矩阵运算的代价

《机器学习实战》这本书在实现这条算法时,直接采用了批量处理的方式:每次迭代,把所有样本的预测误差都算出来,求和之后统一更新一次权重。形式简洁,利用矩阵运算也能写得很短:

def grad_ascent(data_matrix, class_labels, num_iter=500): data_array = np.array(data_matrix) label_array = np.array(class_labels).reshape(-1, 1) m, n = data_array.shape weights = np.ones((n, 1)) alpha = 0.001 for _ in range(num_iter): h = 1.0 / (1.0 + np.exp(-np.dot(data_array, weights))) error = label_array - h weights = weights + alpha * np.dot(data_array.T, error) return weights

这段代码跑起来虽然能出结果,但有两个明显问题。第一,每轮迭代要遍历全部368个样本,在书里这个量级还能忍,但放到真实业务里动辄几十万样本,每轮更新一次的时间成本就完全无法接受了。第二,批量梯度上升对步长选择非常敏感。如果步长太大,权重会在最优解附近剧烈震荡,甚至发散;如果步长太小,收敛要几百轮迭代才够。

我自己复现时用步长0.001跑了500轮,画出的收敛曲线还算可以。但试过一次把步长调到0.01,结果权重直接爆炸,预测概率要么接近1要么接近0,整个模型就废了。这是批量梯度算法的典型短板——它没有考虑到不同样本对权重的"纠正"需求可能是冲突的,统一用一个步长去更新,很难兼顾。

3.3 随机梯度上升:每次只用一个样本

随机梯度上升(SGA)的思路是:既然一次算全部样本的梯度太重,那我每次只随机挑一个样本,用它来更新权重。公式变为:

[ w := w + \alpha (y_i - \hat{y}_i) \mathbf{x}_i ]

一眼看上去比批量版简单得多,但背后蕴含的思想变化值得琢磨。批量版用一个确定性的梯度方向去找全局最优;SGA每一步走的方向带有随机性,它不保证单调收敛,但整体上会朝着最优区域靠近。这就像闭着眼睛下山,每一步可能歪歪扭扭,但长期看还是在往下走。

书中给出的实现是这样的:

def stoc_grad_ascent0(data_matrix, class_labels, num_iter=150): m, n = data_matrix.shape weights = np.ones(n) data_array = np.array(data_matrix) for _ in range(num_iter): for i in range(m): h = sigmoid(np.sum(data_array[i] * weights)) error = class_labels[i] - h weights = weights + alpha * error * data_array[i] return weights

跑一遍会发现,这个版本的收敛速度快了很多,但由于每次只用一个样本,权重在迭代过程中上下波动明显。尤其是数据里有一些"边缘样本"(特征值比较极端或者说标签可能反直觉的样本),它们会带来比较大的扰动。

这个震荡本身不算坏事,温和的震荡反而可能帮助算法跳过局部极小点。但问题在于:如果步长始终不变,算法在后期靠近最优解时无法精细收敛,导致最终权重的精度不如批量版。

3.4 改进版随机梯度上升:学习率衰减与随机抽样

针对SGA的震荡问题,书中给出了一个升级版,核心改动有两点:

第一,学习率随迭代次数衰减。不再是固定的alpha,而是:

alpha = 4.0 / (1.0 + i + j) + 0.01

其中i是迭代次数,j是当前样本索引。直观理解就是:初期大步走,快速逼近目标区域;后期步长越来越小,在最优解附近精细打磨。这个思路和很多现代优化器(比如Adam)的学习率调度策略一脉相承。微积分直觉比较好的人一眼就能看出这个公式的设计逻辑:分母里i和j越大,alpha越小,但0.01这个常数项保证了后期学习率不会降到0,从而保留一定的调整能力。

第二,样本遍历顺序随机化。每次迭代前用np.random.shuffle(index)打乱样本顺序,避免模型因为数据自身的顺序而产生某种偏差。比如数据在采集时,前半段可能全是病情较轻的样本,后半段全是重症,如果不打乱顺序,模型在早期迭代中会被某个子集主导,收敛过程会非常不稳定。

改进版的实现大概是这样的:

def stoc_grad_ascent1(data_matrix, class_labels, num_iter=150): m, n = data_matrix.shape weights = np.ones(n) for i in range(num_iter): data_index = list(range(m)) np.random.shuffle(data_index) for j in data_index: alpha = 4.0 / (1.0 + i + j) + 0.01 h = sigmoid(np.sum(data_matrix[j] * weights)) error = class_labels[j] - h weights = weights + alpha * error * data_matrix[j] return weights

这段代码的好处就是"稳重"。每次更新时,随机选样本和动态学习率两个机制配合,既能快速收敛,又不会震荡太剧烈。我在实际跑出来的效果里,改进版跑20次迭代的效果,已经明显优于批量版跑500次迭代的效果。

4. 回归系数收敛性与分类决策边界:如何验证模型真的学到了

4.1 三种算法的收敛曲线对比

我在复现时最大的乐趣之一,就是画三种算法训练出来的回归系数随迭代次数的变化曲线。注意,画的是6个特征的权重的变化轨迹,而不是损失曲线。

批量梯度上升的系数曲线,在前100轮会有大幅度的抖动,之后逐渐平缓,到300轮之后基本收敛。这条曲线的特点是"平滑但慢"。随机梯度上升的系数曲线则完全不同,整条线一直是锯齿状,每一项权重的波动范围都很大,即使到最后一轮依然在跳,这给我的判断造成了不小的干扰——我一度以为它没收敛,但仔细看分布的均值,其实已经稳定在最优解附近了。

改进版随机梯度上升的曲线最漂亮。前10轮的时候权重大幅变动,有点像在"定位"目标区域;10轮之后,可以看到曲线逐渐平稳,波动幅度随着学习率衰减而收缩,到后期几乎是稳定在一条直线附近。这个视觉对比非常直观地解释了为什么书里最后要用这个版本——它兼具了前两者的优点:收敛快,而且稳定。

三种算法对比表:

算法收敛速度稳定性代码复杂度适用场景
批量梯度上升慢高低小数据集、教学演示
随机梯度上升快低低大数据集、在线学习
改进版SGA快中高中大多数实际场景推荐

4.2 决策边界与系数含义解读

训练完成后,我们拿到了22个特征各自的权重,其中一部分是正数,一部分是负数。一个直接的解读是:权重为正且绝对值大的特征,会让马的存活概率上升;权重为负且绝对值大的特征,会让马面临更高的死亡风险。

这里值得提一个操作细节:我在阅读回归系数的时候,不会只看原始权重数值。因为这个数据集的特征没有做标准化,不同特征的取值范围差异悬殊,所以权重的绝对值不能直接代表特征重要性。举个例子,假设某个特征的取值范围是0到1,它的权重是5,看起来很小;另一个特征的取值范围是0到100,它的权重是0.3,看起来更小。但实际对预测结果的影响可能后者更大,因为它乘上特征值之后的变化范围是0到30,远大于前者带来的0到5。

如果想从Logistic回归系数中做特征重要性排序,比较稳妥的做法是先对所有特征做标准化,再重新训练模型,这样系数的绝对值大小就具备了一定的可比性。不过对于这个预测任务本身,我们的主要目标是分类准确率,特征重要性排序只是辅助理解。

4.3 测试集随机划分与错误率统计

这个预测案例还有一个非常"实战"的训练细节:为了让评估结果更有说服力,书中采用了多次随机划分训练集和测试集的方式。每次从368个样本中随机抽取一部分作为训练集,剩余部分作为测试集,训练模型后计算测试集上的分类错误率。重复10次,取平均错误率作为最终评估结果。

我按照这个思路跑了一遍,用改进版SGA、150次迭代,最终的平均错误率大概在35%左右。也就是说,模型的平均正确率约为65%。听上去似乎不高,但需要注意,这个数据集本身的标签分布并不是均衡的,很多样本本身就是边缘情况,能稳定达到65%以上的准确率已经算是不错的基线。

另外我还对比了三种算法的错误率。批量梯度上升和随机梯度上升的结果都很不稳定,不同随机种子下错误率波动很大,训练集和测试集划分的差异甚至能让错误率跳跃超过10个百分点。而改进版SGA的错误率分布更集中,重复实验的方差明显更小。这在实际工程里是非常重要的特性——模型的稳定性直接影响我们是否敢把它推向真实应用场景。

5. 踩坑记录与实操建议

5.1 缺失值处理不当会有什么后果

这个案例给我留下印象最深的一个坑,是我自己犯过的:有一轮实验忘了对缺失值统一填充0,而是用每列的均值去填充。结果训练出来的模型在测试集上的准确率反而更低,而且回归系数的符号都变诡异了,一些医学上本应和存活正相关的特征,权重变成了负值。

事后分析原因,还是得回到那个"系统性偏差"的问题。均值填充相当于给所有缺失样本一个相同的"虚拟读数",这个虚拟读数在医学场景里没有任何真实含义。比如"毛细血管充盈时间"这个特征,正常马的读数可能是1到2,但重症马的读数可能会很高甚至缺失。如果用均值填充,缺失样本的该特征值被统一压到均值附近,这等于强行把重症马的读数拉回正常区间,模型自然学不到"高读数意味着危险"这一规律。

所以,对缺失值处理我需要强调一个原则:在Logistic回归这种线性加权的模型里,填充策略优先考虑"不引入额外信息",而不是"尽可能接近真实值"。0填充正是因为它不会对加权和产生偏移,才成了这个案例里的最佳选择。

5.2 迭代次数与步长参数怎么调

参数调优这块有两个地方值得单拎出来说。

第一个是步长alpha的初值。批量梯度上升里我用0.001还算顺利,但换成随机梯度上升时,这个值就太小了,收敛太慢;改成0.01之后速度快了不少,然而在一些标签分布比较偏的样本上,单次更新造成权重抖动幅度太大。后来我参照改进版SGA的设计思路,把学习率改成了首轮较大、之后衰减的形式,一步到位解决了这两个问题。如果你自己尝试实现,建议先固定数据,画一下不同alpha下的损失曲线,找出"不震荡且收敛快"的区间,不要凭感觉拍脑袋。

第二个是迭代轮数。批量版我用了500轮,改进版SGA只需要150轮甚至更少。判断迭代次数够不够,最靠谱的方式就是画收敛曲线,看权重是否已经趋于稳定。如果你发现曲线还在明显变化,就加迭代次数;如果曲线早已平稳但错误率依然不理想,那问题可能出在特征工程或数据处理上,单靠加轮数解决不了。

5.3 分类阈值是否需要调整

Logistic回归默认用0.5作为分类阈值。但在医疗场景里,把存活预测的阈值调到0.5往往不是最优选择。道理很简单:不同类型的误判代价不一样。如果把一匹其实能活的马预测成会死,后果是多做了一些本不必要的治疗,损失相对可控;但如果把一匹其实很危险的马预测成能活,错过了及时干预,那就是严重事故。

在实际应用中,如果模型输出的概率集中在0.4到0.6之间,说明数据本身就很多边缘样本,直接踩0.5线很容易误判。一个实用的做法是引入"不确定区间"——比如概率低于0.3判死、高于0.7判活,中间地带标记为"需要进一步检查"。这样做虽然损失了一部分"明确分类"的能力,但从临床决策的角度看反而更负责任。

我在复现中还专门测过把分类阈值从0.5改成0.4和0.6的影响。阈值下调到0.4时,测试集的召回率明显提升,但误判为存活的死亡样本也变多了;阈值上调到0.6时,精确率变高但漏判率上升。这就是典型的精确率-召回率权衡,具体怎么选,取决于业务方更需要避免哪一类错误。

最后再分享一个小技巧:这个案例代码总量不多,但包含的步骤很完整——数据加载、特征预处理、模型训练、评估验证,几乎是一个标准机器学习流程的缩影。建议你在跑通之后,尝试把三个版本都写出来对比跑一下,把回归系数曲线画在一起看。别小看这个练习,它比单纯调一个成熟库的API更能培养对算法机理的直觉。我自己就是通过反复跑这个实验,才真正建立了对"梯度更新""收敛""稳定"这些词的体感。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询