☰
Python训练+PHP推理:逻辑回归心脏病预测实战
2026/10/2 2:38:31 网站建设 项目流程

简介:这份资源是面向机器学习与Web开发初学者的综合实战案例,围绕逻辑回归二分类算法构建心脏病预测模型,帮助读者理解从数据处理到模型部署的完整链路。压缩包共8个文件,约7KB,以xml配置文件、csv数据集、py脚本和md说明文档为主,其中csv提供医疗指标样本,py承载模型训练与评估逻辑,xml与iml负责工程配置,md记录项目说明。已有245人学习下载,适合希望将Python模型集成到PHP Web应用中的开发者参考。读者可从中获取逻辑回归的sigmoid概率映射、特征预处理、超参数选择与交叉验证评估等关键实现思路,并了解PHP如何通过接口调用Python脚本完成前后端交互,从而掌握模型训练、评估与Web部署的全流程实践方法。

1. 心脏病预测这套 Python+PHP 组合拳,到底解决什么问题

体检报告上那一串指标——年龄、血压、胆固醇、最大心率——单看每一项都在正常范围边缘,合在一起却可能指向一个高风险信号。逻辑回归做二分类预测,就是把这些指标揉进一个 sigmoid 函数里,输出一个 0 到 1 之间的概率值,告诉你“这个人未来十年患心脏病的概率是 0.73”。这个案例源码用 Python 做模型训练、PHP 做 Web 端推理接口,本质上是一套“离线训练 + 在线预测”的最小闭环。

为什么不是纯 Python 一把梭?因为实际业务里,模型训练往往在数据团队的 Jupyter Notebook 里跑,但预测服务要嵌到已有的 PHP 业务系统中——比如医院挂号系统、体检报告平台。你不可能让 PHP 工程师去调 Python 进程,所以常见做法是 Python 训练完导出权重参数,PHP 端用同样的公式做前向计算。这套源码的价值就在于把这条链路完整跑通了:从 CSV 数据清洗、特征标准化、梯度下降训练,到 PHP 接收表单、加载权重、输出预测结果。

适合谁看?有 Python 基础想入门机器学习落地的后端工程师,或者手上有 PHP 项目想加一个预测模块的全栈开发者。不需要你懂反向传播的数学推导,但得能看懂矩阵乘法和 sigmoid 公式。下面按“数据怎么处理 → 模型怎么训 → PHP 怎么接 → 坑在哪”的顺序拆开讲。

2. 数据管道与特征工程:从原始 CSV 到模型能吃的矩阵

2.1 心脏病数据集的特征分布与清洗策略

常见的心脏病预测数据集(比如 UCI 的 Cleveland 数据集)一般有 13 个特征加 1 个标签列。特征类型混杂:年龄、静息血压、胆固醇、最大心率是连续数值;性别、胸痛类型、空腹血糖、心电图结果、运动诱发心绞痛是类别变量;还有斜率、主血管数、缺陷类型这种有序或半有序的。标签列通常是 0 和 1,0 表示无心脏病,1 表示有。

清洗第一步是处理缺失值。这个数据集里“主血管数”和“缺陷类型”经常有问号占位。我一般直接删掉缺失行,因为样本量本来就不大(约 300 条),填充反而引入噪声。第二步是类别变量编码。胸痛类型有 4 个取值,不能直接当连续值喂给逻辑回归,得做 one-hot 编码。但注意:逻辑回归对多重共线性敏感,one-hot 之后要 drop 掉一列作为基准,否则设计矩阵秩亏,梯度下降会震荡。

import pandas as pd import numpy as np # 读取原始数据,问号当缺失值处理 df = pd.read_csv('heart.csv', na_values='?') df = df.dropna() # 样本量小,直接删缺失行 # 类别变量做 one-hot,drop_first=True 避免虚拟变量陷阱 categorical_cols = ['cp', 'restecg', 'slope', 'thal'] df = pd.get_dummies(df, columns=categorical_cols, drop_first=True) # 分离特征和标签 X = df.drop('target', axis=1).values y = df['target'].values.reshape(-1, 1) # 特征标准化:逻辑回归对尺度敏感,不标准化收敛极慢 mu = X.mean(axis=0) sigma = X.std(axis=0) X_norm = (X - mu) / sigma # 保存标准化参数,PHP 端推理时必须用同一套 mu/sigma np.savez('norm_params.npz', mu=mu, sigma=sigma)

这段代码的关键在最后一步:标准化参数必须持久化。PHP 端拿到原始输入后,要用训练时算出的 mu 和 sigma 做同样的变换,否则预测结果完全错乱。我见过有人 PHP 端直接拿原始值算,准确率从 85% 掉到 50% 出头,排查半天才发现是标准化没对齐。

参数说明:drop_first=True对每个类别变量删掉第一个水平,比如胸痛类型 1/2/3/4 变成三个 0/1 列。sigma里可能有 0 值(某个特征所有样本取值相同),除法会出 inf,稳妥做法是加一个极小值sigma + 1e-8。

2.2 训练集/测试集划分与类别不平衡检查

划分比例常见 80/20 或 70/30。这个数据集正负样本大致均衡,不需要 SMOTE 过采样。但如果你换一个真实医院的数据集,患病样本可能只占 5%,这时候逻辑回归会偏向预测多数类,准确率看着高但召回率惨不忍睹。检查方法很简单:打印np.bincount(y.flatten()),如果两类比例超过 1:4,就得考虑加class_weight='balanced'或者手动调阈值。

from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X_norm, y, test_size=0.2, random_state=42, stratify=y ) # 检查类别分布 print('训练集正负样本数:', np.bincount(y_train.flatten())) print('测试集正负样本数:', np.bincount(y_test.flatten()))

stratify=y保证划分后训练集和测试集的正负比例与原始数据一致。random_state=42固定随机种子,方便复现。如果不用 stratify,小样本下可能出现测试集全是负样本的极端情况,评估指标就失去意义了。

注意:标准化参数只能用训练集计算,然后应用到测试集。如果先对全量数据算 mu/sigma 再划分,测试集信息泄露到训练过程,评估结果会虚高。

3. 逻辑回归训练:梯度下降、损失函数与权重导出

3.1 从 sigmoid 到交叉熵损失的手动实现

逻辑回归的前向传播就两步:线性组合z = X @ w + b,然后过 sigmoid 得到概率a = 1 / (1 + exp(-z))。损失函数用二元交叉熵,公式是L = -1/m * sum(y*log(a) + (1-y)*log(1-a))。梯度推导也不复杂:dw = 1/m * X.T @ (a - y),db = 1/m * sum(a - y)。

手动实现一遍比直接调 sklearn 更有价值,因为 PHP 端推理时你只做前向,但理解反向传播的维度变化能帮你排查权重加载后的 shape 错误。

def sigmoid(z): # 裁剪 z 防止 exp 溢出 z = np.clip(z, -500, 500) return 1 / (1 + np.exp(-z)) def train_logistic(X, y, lr=0.01, epochs=5000): m, n = X.shape w = np.zeros((n, 1)) b = 0.0 losses = [] for i in range(epochs): z = X @ w + b a = sigmoid(z) # 交叉熵损失,加 1e-8 防止 log(0) loss = -1/m * np.sum(y * np.log(a + 1e-8) + (1-y) * np.log(1-a + 1e-8)) losses.append(loss) dw = 1/m * X.T @ (a - y) db = 1/m * np.sum(a - y) w -= lr * dw b -= lr * db if i % 1000 == 0: print(f'Epoch {i}, Loss: {loss:.4f}') return w, b, losses

学习率lr=0.01是保守值,配合 5000 轮迭代一般能收敛。如果损失曲线震荡,降到 0.001;如果下降太慢,升到 0.05 试试。epochs不是越多越好,超过收敛点后损失不再下降,反而可能因浮点误差轻微上升。我一般看最后 500 轮损失变化小于 1e-5 就停。

3.2 权重持久化与 PHP 端加载格式设计

训练完得到w和b,需要导出成 PHP 能读的格式。最省事的是 JSON:把 w 展平成一维数组,b 存成标量,再带上 mu 和 sigma。

import json w, b, losses = train_logistic(X_train, y_train) # 导出为 JSON,PHP 用 json_decode 直接读 model = { 'weights': w.flatten().tolist(), 'bias': float(b), 'mu': mu.tolist(), 'sigma': sigma.tolist() } with open('model.json', 'w') as f: json.dump(model, f, indent=2) print('权重维度:', w.shape) print('最终训练损失:', losses[-1])

导出后检查两件事:权重数组长度是否等于特征数(one-hot 之后特征数会变多),bias 是否是标量。PHP 端json_decode默认返回对象,用$model->weights访问;如果传true参数则返回关联数组,用$model['weights']访问。两种都行,但整个项目里保持一致,别混用。

提示:如果特征维度超过 50,JSON 文件会比较大,PHP 每次请求都读文件解析有性能开销。生产环境建议把权重存进 Redis 或 APC 缓存,启动时加载一次。

4. PHP 推理接口:接收表单、加载权重、输出概率

4.1 用 PHP 实现 sigmoid 前向计算

PHP 端要做的事很明确:接收 POST 过来的原始特征值,用训练时的 mu/sigma 标准化,然后算z = sum(w_i * x_i) + b,最后过 sigmoid 输出概率。注意 PHP 的数组下标从 0 开始,和 Python 导出的顺序必须一一对应。

<?php // predict.php header('Content-Type: application/json'); // 加载模型权重 $modelJson = file_get_contents('model.json'); $model = json_decode($modelJson, true); $weights = $model['weights']; $bias = $model['bias']; $mu = $model['mu']; $sigma = $model['sigma']; // 接收原始输入,顺序必须和训练时特征列一致 $rawInput = [ floatval($_POST['age']), floatval($_POST['sex']), floatval($_POST['trestbps']), // ... 其余特征按训练顺序排列 ]; // 标准化 $normalized = []; for ($i = 0; $i < count($rawInput); $i++) { $normalized[$i] = ($rawInput[$i] - $mu[$i]) / ($sigma[$i] + 1e-8); } // 前向计算 $z = $bias; for ($i = 0; $i < count($weights); $i++) { $z += $weights[$i] * $normalized[$i]; } // sigmoid $prob = 1 / (1 + exp(-$z)); echo json_encode([ 'probability' => round($prob, 4), 'label' => $prob >= 0.5 ? 1 : 0 ]);

floatval强制转换防止字符串注入。$sigma[$i] + 1e-8处理零方差特征。阈值 0.5 是默认值,但医疗场景下漏诊代价高于误诊,可以把阈值降到 0.3,提高召回率。这个阈值应该做成配置项,别硬编码。

4.2 表单字段与特征顺序的强制对齐

PHP 接收的字段顺序必须和 Python 训练时的列顺序完全一致。one-hot 编码后列顺序是 pandas 决定的,通常是按字母序排列类别水平。最稳妥的做法是在 Python 端导出时同时保存一个feature_order列表,PHP 端按这个列表从关联数组里取值。

# Python 端导出特征顺序 feature_order = df.drop('target', axis=1).columns.tolist() model['feature_order'] = feature_order
// PHP 端按 feature_order 重排输入 $orderedInput = []; foreach ($model['feature_order'] as $featName) { $orderedInput[] = floatval($_POST[$featName] ?? 0); }

这样即使前端表单字段顺序变了,只要字段名对得上,推理结果就不会错。我踩过一次坑:前端加了一个新字段插在中间,PHP 端没重排,所有特征错位,预测结果全反了,排查了两小时才定位到。

注意:PHP 的exp()函数在参数过大时返回 INF,1/(1+INF)得 0,逻辑上没错但会丢失精度。稳妥做法和 Python 端一样,先 clip 到 [-500, 500]。

5. 避坑与排查:这套组合拳最容易翻车的五个地方

5.1 现象:PHP 预测结果全是 0 或全是 1

原因:标准化参数没对齐。Python 端用了sigma + 1e-8,PHP 端忘了加,某个零方差特征导致除零,整个向量变成 INF 或 NAN。或者 mu/sigma 在导出时被 JSON 序列化成了字符串,PHP 做减法时隐式转换出问题。

解决:在 PHP 端加一行var_dump($normalized)看标准化后的值是否在合理范围(一般在 -3 到 3 之间)。如果出现 INF/NAN,检查 sigma 是否为零。另外用json_encode导出时确保JSON_PRESERVE_ZERO_FRACTION标志,避免 0.0 变成 0。

5.2 现象:Python 训练损失不下降,始终在 0.69 附近

原因:学习率太大导致震荡,或者特征没标准化。逻辑回归的损失函数是凸的,但梯度下降步长过大时会跳过最小值点。0.69 约等于-log(0.5),说明模型输出始终在 0.5 附近,权重根本没更新。

解决:先把学习率降到 0.001 跑 1000 轮看损失是否下降。如果还不降,检查X_norm的均值和方差是否接近 0 和 1。我一般会在训练前打印X_norm.mean(axis=0)和X_norm.std(axis=0),确认标准化生效。

5.3 现象:PHP 接口返回 500 错误,日志显示 json_decode 失败

原因:model.json 文件路径不对,或者文件内容被截断。Python 导出时如果 w 里有 numpy 的 float32 类型,json.dump会报TypeError: Object of type float32 is not JSON serializable。

解决:导出前用.astype(float)转成 Python 原生 float,或者自定义default=lambda x: float(x)。PHP 端用file_exists先判断文件存在,再用json_last_error()检查解析结果。

5.4 现象:测试集准确率 85%,上线后实际预测准确率不到 60%

原因:训练数据分布和线上数据分布不一致。比如训练集里年龄集中在 40-60 岁,线上来了一个 20 岁的输入,标准化后 z 值偏离训练分布,sigmoid 输出接近 0 或 1,但方向可能反了。

解决:在 PHP 端加输入范围检查,超出训练集 min/max 范围的输入给出警告。更彻底的做法是定期用线上数据重新训练模型,保持分布对齐。

5.5 现象:并发请求下 PHP 预测结果随机波动

原因:多个请求同时读写同一个 model.json 文件,或者用了全局变量缓存权重但没加锁。PHP 的 file_get_contents 在文件被写入时可能读到半截内容。

解决:权重文件写入用原子操作——先写临时文件再rename。读取时如果用了 APC 或 Redis 缓存,设置合理的过期时间,并在缓存失效时加锁重建。

6. 进阶技巧:用 PHP 做批量预测与模型版本管理

单条预测跑通之后,下一步自然是批量处理。体检中心一次导入几百条数据,逐条发 HTTP 请求太慢,我一般会在 PHP 端写一个批量推理函数,一次加载权重,循环计算所有样本。

function batchPredict($model, $samples) { $results = []; foreach ($samples as $rawInput) { $normalized = []; for ($i = 0; $i < count($rawInput); $i++) { $normalized[$i] = ($rawInput[$i] - $model['mu'][$i]) / ($model['sigma'][$i] + 1e-8); } $z = $model['bias']; for ($i = 0; $i < count($model['weights']); $i++) { $z += $model['weights'][$i] * $normalized[$i]; } $results[] = 1 / (1 + exp(-max(-500, min(500, $z)))); } return $results; }

这个函数把权重加载提到循环外,1000 条数据耗时从 3 秒降到 0.2 秒左右。注意max(-500, min(500, $z))是 PHP 里做 clip 的写法,比 Python 的np.clip啰嗦但效果一样。

模型版本管理方面,我习惯在 model.json 里加一个version字段和trained_at时间戳。PHP 端加载时检查版本号,如果和配置文件里的期望版本不一致就告警。这样模型更新后忘了同步 PHP 端的情况能第一时间发现。

{ "version": "20250115-01", "trained_at": "2025-01-15T10:30:00", "weights": [...], "bias": -0.42, "mu": [...], "sigma": [...], "feature_order": ["age", "sex", "trestbps", ...] }

验证方法很简单:拿一条已知标签的测试样本,Python 端和 PHP 端各跑一次,概率差值应该在 1e-6 以内。如果差得多,八成是标准化或特征顺序出了问题。我每次更新模型都会跑这个对齐测试,省得线上翻车再回头查。

最后说个血泪教训:别在 PHP 端用==比较浮点数。$prob == 0.5永远为 false,因为浮点精度问题。用abs($prob - 0.5) < 1e-6判断。这个坑我踩过两次,第一次查了一下午,第二次一眼就看出来了。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询