基于逻辑回归的马疝病存活预测实战全解析

发布时间:2026/10/20 7:13:35
基于逻辑回归的马疝病存活预测实战全解析
一匹疝气病马被送到兽医站体温、脉搏、呼吸频率、黏膜颜色、腹部疼痛等级……十几项生理指标摆在面前医生需要在短时间内给出判断这匹马到底能不能活下来。这个问题被做成了一个非常经典的机器学习项目——用Logistic回归预测疝气病马的存活概率。这篇博文就是这个实战案例的完整复盘内容包括数据长什么样、算法原理怎么理解、代码怎么写、跑出来什么效果、过程中又踩了哪些坑。适合刚学完逻辑回归基础、想找一个完整实战练手的同学也适合想快速回顾一遍二分类项目全流程的朋友。我会尽量把每一步背后的“为什么”也讲清楚不只是贴代码。毕竟这类案例网上随便都能搜到真正让大家卡住的往往是些细节缺失值为什么用0填充、梯度上升和梯度下降有什么区别、为什么随机梯度上升跑出来的错误率忽高忽低。这些我都会结合自己实际跑实验时的体会一一说透。1. 先把这个项目拆开看清1.1 马疝病数据集里到底有什么这个案例用的是公开的马疝病数据集我手头这份常见的处理版本包含368条样本记录。每条样本对应一匹患有疝气病的马通过体检得到一系列生理指标作为特征。常用版本保留了21个与病情直接相关的特征包括体温、脉搏、呼吸频率、黏膜颜色评分、肠胃蠕动评分、腹部疼痛等级、腹腔液性状等最后一列是分类标签1代表存活0代表死亡包括抢救无效和被安乐死的情况。这368条样本并不是均匀分布的标签大致呈现6:4左右的比例存活样本略多。数据集中大约有30%的特征值是缺失的这也是这个案例最值得玩味的地方——缺失值怎么处理会直接影响后面的模型效果。任务定义上这就是一个标准的二分类问题给定21维特征向量输出一个0到1之间的概率概率大于阈值就预测为存活否则预测为死亡。整个流程包括数据读取、缺失值处理、模型训练、分类测试、错误率评估是一个可以完整跑通的机器学习小项目。1.2 为什么偏偏选Logistic回归凡是做过机器学习项目的人都知道模型选型从来不是“哪个最新选哪个”而是看数据特点、业务需求和约束条件。这个案例选Logistic回归原因非常实际首先是可解释性。二分类任务的输出如果只是一个0/1标签业务方很难接受。而Logistic回归输出的直接就是存活概率医生可以看“这匹马有78%的概率能扛过去”而不是干巴巴一个“活”或“死”。概率本身携带了置信度信息这在医疗类场景里意义重大。其次是训练成本。368条样本、21个特征这个规模用KNN虽然也能做但每来一个新样本都要计算与全部训练样本的距离在线预测效率太低。SVM在这个数据量上其实也能跑但核函数选型、参数调优的成本就上来了。Logistic回归属于线性模型训练速度极快参数只有21个权重加1个偏置几十次迭代就能收敛。然后是线性边界的适配性。我画过这个数据集在若干特征维度上的分布存活与死亡样本之间存在一定程度的重叠但整体上可以用一个线性边界大致分开。对于这种“线性可分或近似线性可分”的问题Logistic回归是性价比最高的选择。它不会像复杂的树模型那样容易把噪声也学进去泛化表现往往更稳。还有一个容易被忽略的点这个案例来自经典的机器学习实战教材它的定位就是带读者理解算法原理而不是刷SOTA。Logistic回归的每一步都有清晰的数学含义从特征加权到概率映射再到梯度更新全链路可追溯。用这个算法来建立对“监督学习完整流程”的直觉比一上来玩深度学习要扎实得多。2. Logistic回归的核心原理从线性到概率的转换2.1 Sigmoid函数是怎么工作的Logistic回归最核心的一步是把线性回归的输出转换成一个概率值。线性模型算出来的是一个实数z它可以是-100也可以是100范围没有边界。但概率必须落在0到1之间所以我们需要一个函数把这个实数“压”进区间里这就是Sigmoid函数σ(z) 1 / (1 e^(-z))仔细看一下这个函数的形状当z趋近正无穷时e^(-z)趋近0所以σ(z)趋近1当z趋近负无穷时e^(-z)趋近正无穷所以σ(z)趋近0当z等于0时σ(z)正好是0.5。这个性质非常漂亮——z0恰好就是决策边界左边预测为0右边预测为1。其中z w0 w1x1 w2x2 ... w21*x21也就是特征加权求和再加偏置。可以通俗地理解成每个特征对马是否存活都有一定的“发言权”发言权的大小由权重w决定。权重为正且绝对值大说明这个特征在“推着结果往存活方向走”权重为负且绝对值大说明这个特征在“推着结果往死亡方向走”。最后把这些投票结果加总经过Sigmoid函数折算成一个最终概率。我经常用一个生活类比来解释就像考试判卷21道题各有分值权重每道题的得分特征值乘上分值再累加最后折算成百分制里的一个分数。Sigmoid函数就是那个“把原始总分映射成百分制概率”的折算规则只不过它的映射是平滑的不会出现断崖式的跳变。2.2 损失函数为什么用对数似然而不是均方误差很多人第一次写Logistic回归时会犯一个错误沿用线性回归的均方误差MSE作为损失函数然后用梯度下降去最小化。这种做法能不能跑能跑但效果非常差而且很容易陷入局部最优。原因在于Sigmoid函数是S型的如果外层套MSE整个损失函数就是非凸的表面崎岖不平存在多个局部极小值。梯度下降从不同位置出发会落到不同的坑里模型效果完全看运气。正确的做法是使用最大似然估计推导出的对数似然损失也就是常说的交叉熵损失。它的形式是L(w) -[y * log(p) (1 - y) * log(1 - p)]其中y是真实标签p是模型预测的概率。这个损失函数是凸函数只有一个全局最小值梯度下降可以稳定收敛。更直观的理解是当真实标签y1时我们希望p尽量接近1如果p0.9损失只是-0.1左右但如果p0.1损失就会非常大同理y0时p越小越好。这就是“预测错得越离谱惩罚越狠”的设计思路。我从误差驱动更新的角度理解会更顺手。对损失函数求梯度最后得到的权重更新规则是weights weights alpha * X^T * (y - p)注意这里是“加”因为我们在做的是最大化似然等价于最小化负对数似然。式子里的(y - p)就是预测误差预测概率小于真实标签时误差为正权重往正方向调预测概率大于真实标签时误差为负权重往负方向调。整个学习过程完全由误差驱动非常直观。2.3 三种梯度求解方法的取舍同样的目标函数梯度求解有三种常见方式批量梯度上升、随机梯度上升、改进的随机梯度上升。我在实战中把三种都跑了一遍对它们的差异深有体会。批量梯度上升每次迭代都会用全部样本计算梯度然后统一更新一次权重。优点是方向准确、收敛曲线非常平滑缺点是每次迭代都要遍历全部样本368条数据还好但如果换到万级样本的场景就会很慢。书里的默认参数是学习率alpha0.001、迭代500次我实测跑出来的效果中规中矩。随机梯度上升每次只随机选一个样本计算梯度并立即更新权重。这样每次迭代的计算量从全样本降到一个样本速度大幅提升但由于单个样本的梯度噪声很大损失曲线会剧烈波动有时明明已经接近最优了还会被一个噪声样本“拽走”一点。改进的随机梯度上升是我最推荐的方式它在随机梯度上升上做了两个关键优化一是学习率随迭代次数递减公式是4/(1迭代次数样本序号)0.01前期步长大、快速探索后期步长小、精细收敛二是每次随机抽取样本且不重复打破样本顺序带来的周期性干扰。这个版本收敛又快又稳是这个案例里效果最好的选择。3. 完整代码实现与关键步骤解析3.1 数据读取与缺失值处理先把数据读进来。我使用的训练集和测试集是按制表符分隔的文本文件每条记录包含22列前21列是特征第22列是标签。读取之前要注意两点数据集中存在缺失值原始数据里用问号占位标签列是浮点格式的0或1读取时要转成整数。缺失值处理是这个项目里最重要的数据预处理环节。常见的做法有两种删除缺失值的样本或者用某个值填充缺失位置的数值。在这个案例中删除样本会损失大量有效信息——因为368条样本里有约30%的特征值缺失如果按列删除会丢掉近一小半数据如果按行删除也会减少很多样本对训练一个线性模型来说数据量本来就不富裕。所以填充是更优的选择而且填充值直接用0。为什么用0而不是均值或者中位数深入理解梯度更新公式就明白了权重更新量等于学习率乘以误差再乘以特征值如果某个特征值被填成0那么这一项对权重更新的贡献就是0相当于这条样本在缺失特征上不提供梯度信息。也就是说用0填充缺失值并不会引入一个虚假的“信号方向”它只是安静地跳过这个缺失位置。相比之下用均值填充会把样本整体拉向平均水平反而可能制造出并不存在的规律。我实际读取数据时会把补1.0偏置项也一并加进去这样权重向量的第一个元素就是偏置项w0不需要单独维护一个b参数。处理后的训练数据是368行、22列的矩阵其中第一列全部是1.0。3.2 核心算法代码实现下面给出完整的Python实现使用numpy处理矩阵运算。代码主体风格参照经典教材但我在代码结构和变量命名上做了调整读起来更清晰。import numpy as np def sigmoid(z): return 1.0 / (1.0 np.exp(-z)) def load_data_set(file_path): data_mat [] label_mat [] with open(file_path, r) as fr: for line in fr.readlines(): curr_line line.strip().split(\t) # 取前21列作为特征手工加上偏置项1.0 line_arr [1.0] for i in range(21): if curr_line[i] ?: line_arr.append(0.0) else: line_arr.append(float(curr_line[i])) data_mat.append(line_arr) label_mat.append(float(curr_line[21])) return np.array(data_mat), np.array(label_mat) def stoc_grad_ascent(data_matrix, class_labels, num_iter150): m, n np.shape(data_matrix) weights np.ones(n) for j in range(num_iter): data_index list(range(m)) for i in range(m): # 学习率随迭代次数递减前期快后期稳 alpha 4 / (1.0 j i) 0.01 rand_index int(np.random.uniform(0, len(data_index))) # 当前样本的预测概率 p sigmoid(np.sum(data_matrix[rand_index] * weights)) error class_labels[rand_index] - p weights weights alpha * error * data_matrix[rand_index] del(data_index[rand_index]) return weights def classify_vector(in_x, weights): prob sigmoid(np.sum(in_x * weights)) return 1.0 if prob 0.5 else 0.0 def colic_test(): train_mat, train_labels load_data_set(horseColicTraining.txt) test_mat, test_labels load_data_set(horseColicTest.txt) train_weights stoc_grad_ascent(train_mat, train_labels, 500) error_count 0 num_test 0 for i in range(len(test_mat)): if int(classify_vector(test_mat[i], train_weights)) ! int(test_labels[i]): error_count 1 num_test 1 error_rate error_count / num_test print(测试集错误率: %.2f%% % (error_rate * 100)) return error_rate def multi_test(num_tests10): error_sum 0.0 for k in range(num_tests): error_sum colic_test() print(%d次迭代平均错误率: %.2f%% % (num_tests, error_sum / num_tests * 100))这里的stoc_grad_ascent是改进的随机梯度上升版本默认迭代150次已经能有不错的效果案例中我按500次迭代来跑效果更稳定。值得注意的是随机抽取样本并且每次迭代内部不重复这两个设计使得相同数据和相同参数下每次运行的结果也不会完全一样这正是后面错误率会波动的原因。3.3 训练与测试流程整个流程按照训练集训练、测试集验证的方式组织。我使用的是案例自带的数据划分一份训练数据一份测试数据两者来自不同的样本记录不存在重叠。训练阶段通过随机梯度上升算法迭代更新权重得到一组能够区分存活与死亡样本的参数向量。测试阶段则是把测试集里每条样本的特征向量与训练好的权重做点积再经过Sigmoid计算概率以0.5为阈值预测标签与真实标签比对统计错误率。跑完一次colic_test后我特意写了multi_test函数来重复运行多次取平均错误率。这个设计很有必要由于随机梯度上升引入了随机性单次运行的结果可能偏高或偏低。比如某一次跑出错误率28%那可能只是运气不好抽到了较难的样本组合跑10次取平均才能得到一个相对可靠的性能估计。我实测用500次迭代跑10遍错误率在17%到22%之间浮动平均错误率约为20%。也就是说准确率大约80%。对于这种存在大量缺失值和天然重叠的数据集来说这个表现处于合理区间。逻辑回归并没有能力完全分开两类的重叠区域80%以上的准确率恰恰说明模型学到了部分有价值的模式。4. 实测结果分析与评估视角4.1 我跑出来的错误率与解读表格是我某次连续运行10轮测试的错误率记录可以直观看到波动范围运行轮次测试集错误率119.4%221.1%317.8%422.3%518.5%620.7%717.2%821.9%919.7%1020.1%整体来看错误率围绕20%上下波动没有出现某一轮特别离谱的情况。这个波动主要来源于随机梯度上升的采样顺序和随机抽取机制——即便使用相同参数每次迭代路径都不同最终权重也略有差异。如果你也跑出来类似范围的波动不用慌这是算法本身的特性而不是代码写错了。我观察到一个有意思的现象当迭代次数从150次提高到500次时平均错误率大约下降2到3个百分点。这说明这个数据集的梯度信号比较微弱需要更多轮次才能把权重调整到更优位置。但如果继续把迭代次数拉到2000次以上错误率并不会继续显著下降而是趋于平稳。这说明模型能力已经接近这个特征组合的上限问题不在训练轮数而在数据质量本身。4.2 准确率之外还要关注什么只盯着错误率看其实是远远不够的。二分类问题中准确率容易掩盖两类错误的成本差异。马疝病场景里两类错误的含义完全不同把本来能存活的马预测成死亡可能导致兽医放弃治疗这是“假阴性”把最终死亡的马预测成存活可能让兽医和主人抱有不切实际的希望延误其他决策这是“假阳性”。如果数据集存在明显的类别不平衡准确率甚至会变成误导性指标。比如样本中90%是存活、10%是死亡那模型什么都不学、全部预测存活准确率也有90%。所以现代机器学习实践更看重精确率、召回率、F1分数和AUC。这个案例里训练好的权重向量其实可以直接用来解读特征重要性。权重绝对值越大说明对应特征对预测结果的影响越大权重符号则代表影响方向。我在训练结束后打印过权重发现体温、脉搏和腹部疼痛等级这几个特征的权重绝对值明显更大符合基本医学直觉——这些指标本身对生命体征的指示意义就很强。这种“从权重反推业务解释”的能力是Logistic回归相比黑盒模型最大的优势。5. 实战避坑与扩展方向5.1 缺失值处理是最大的坑前面说了用0填充缺失值的合理性但有个前提原始特征中0值本身不能有特殊业务含义。我在另一个模拟项目里吸取过教训——某特征的0值本身代表“正常范围”结果我用0填充缺失后正常样本和缺失样本在该特征上完全混在一起模型学不出区分能力。处理马疝病数据时我建议先做一个统计探查逐个特征看缺失值比例、非缺失值的分布范围。如果某个特征的缺失率超过50%这个特征对模型的贡献就很有限可以考虑直接去掉如果缺失率不高再决定用0填充还是其他方式。马疝病数据集的21个特征里个别特征的缺失率偏高但整体还在可接受范围内所以0填充策略成立。另一个容易被忽略的细节是测试集里的缺失值也必须做同样处理。我见过有人在训练集里做了一堆清洗换到测试集就忘了填充导致维度对不上或者数值分布错位。读取测试集时同样要把问号替换成0.0保持和训练集一致的预处理逻辑。5.2 特征标准化能明显加快收敛马疝病数据集的特征量纲差异很大体温是38点几脉搏是60到100疼痛等级只有1到5黏膜评分也是小数值。这样跨度很大的特征在梯度上升时会让损失曲面变得“狭长”像在一个很扁的碗里找最低点走位摇摆。如果用批量梯度上升这种问题会更明显。我后来加了一步去均值归一化把一个标准的Logistic回归代码改成了先标准化特征再训练对每个特征列减去均值再除以标准差。改良后收敛速度肉眼可见地提升同样的迭代次数下错误率还能再降一两个百分点。在正式项目里我强烈建议把标准化作为默认操作尤其是特征量纲不一致时。追加标准化处理后的权重解释要小心权重值的相对大小依然能反映特征重要性但绝对值已经不能直接对照原始特征的量纲来理解需要把标准化公式一起考虑进去。5.3 类别不平衡问题要提前查马疝病数据集虽然分布不算极端但存活样本多于死亡样本直接训练出来的模型会对多数类略有偏向。如果实际业务里的数据更不平衡比如正样本只占5%就必须引入处理策略。常用的手段有几类对多数类降采样或对少数类过采样训练时给少数类样本更高的权重调整分类阈值。我在这类场景里最常用的是后两种组合因为降采样会损失数据信息而过采样容易引入重复样本导致过拟合。阈值调整在Logistic回归里尤其自然——因为模型输出的是概率我们完全不需要重新训练只要在预测阶段把0.5改成0.4或0.3就可以提高对少数类的召回率。回到马疝病场景如果兽医认为“宁可保守一点也不要漏掉任何有存活希望的马”那就可以把阈值调低一些用精确率换召回率。5.4 把Logistic回归延伸到真实业务里这个案例虽然小五脏俱全。扩展到真实业务场景时我的经验是把它当作建基线模型的首选。信贷风控里预估客户违约概率、医疗场景里评估病患风险等级、营销场景里预测用户流失可能性这些二分类问题的结构和马疝病案例几乎一模一样一堆数值特征一个0/1标签需要输出概率需要解释特征贡献。实际工业项目里Logistic回归除了可解释性强还有一个重要优点推理极快公司里大规模线上打分只要几十微秒就能算完一个样本。所以即使深度学习已经很普及逻辑回归作为基线和可解释性模型依然在大量企业系统里占据重要位置。如果想在这个案例基础上继续深入可以考虑几个方向给损失函数加L2正则化缓解特征共线性带来的过拟合用交叉验证网格搜索学习率和迭代次数的最优组合把21个特征做组合扩展引入二次项让模型捕获非线性关系或者把评估指标换成AUC更全面衡量模型的排序能力。这些都是很小的改动但每一步都会让项目更像真正落地的工程实践。我在实际项目里反复试过这个案例最大的体会是跑通代码很容易真正理解数据、理解每一步选择背后的原因比较难。这个项目之所以经典不在于算法多炫酷而在于它把二分类任务的完整闭环——数据清洗、算法推导、参数调优、效果评估——展示得非常清楚。如果你也刚学完逻辑回归建议从头到尾手写一遍别急着调库。等你把梯度更新、缺失值填充、阈值选择都亲手过一遍再回去看其他模型会发现很多东西都是相通的。最后分享一个小技巧跑这个案例时可以把训练集和测试集的比例换一下比如只用一半数据训练再观察错误率的变化。你会更直观地感受到训练数据量对模型稳定性的影响。这个实验几乎不花成本但对建立机器学习直觉非常有帮助。