简介这份PDF文档围绕基于贝叶斯定理的糖尿病检测系统展开面向模式识别、机器学习入门学习者及需要完成课程大作业的高校学生帮助其理解朴素贝叶斯分类器在医疗预测场景中的完整落地思路。资源包内仅含1个PDF文件大小约771KB内容涵盖贝叶斯分类原理、Pima Indians糖尿病数据集说明、特征提取与高斯概率计算、分类预测流程、准确率评估与图表可视化以及loadCsv、SplitDataset、summarizeByClass、calculateProbability等核心函数的代码实现与流程图。文档还给出硬件软件环境配置、数据集拆分比例和主要程序源码读者可据此复现从数据加载到结果分析的全过程掌握条件概率累乘、高斯概率密度函数应用及模型评估方法。目前已有234人学习适合作为模式识别课程实践与贝叶斯算法入门的参考材料。1. 贝叶斯定理做糖尿病检测为什么它比逻辑回归更适合小样本筛查糖尿病检测系统在基层医疗和体检场景里有个绕不开的矛盾阳性样本少、特征维度高、误判代价不对称。用逻辑回归跑一遍 Pima Indians Diabetes 数据集准确率看着能到 78%但召回率经常掉到 60% 以下——也就是说十个真病人漏掉四个。贝叶斯定理切入这个问题的角度不一样它不追求一个全局最优的决策边界而是把每个特征的条件概率拆开算最后用后验概率做判断。这件事在样本量小、特征之间近似独立的场景下反而比判别式模型更稳。这套系统的核心逻辑是给定血糖、血压、BMI、年龄等指标算的是 P(患病|指标) 而不是 P(指标|患病)。前者才是医生要的答案。朴素贝叶斯分类器把这个后验概率通过特征条件独立假设展开训练时只需要统计每个特征在各类别下的均值和方差计算量极小部署到嵌入式设备或者基层卫生站的低配机器上完全跑得动。适合谁看做过机器学习课程设计但没真正处理过医学数据分布的人想用 Spring Boot 或者 Flask 把模型包成可调用接口的后端以及需要给非技术评审讲清楚“为什么选贝叶斯而不是神经网络”的人。这篇不讲教科书推导讲的是从数据预处理到后验概率校准再到系统集成的完整路径中间会给出可直接复现的代码和参数配置。2. 朴素贝叶斯做糖尿病分类的原理与选型依据2.1 从贝叶斯公式到高斯朴素贝叶斯的推导链路贝叶斯定理本身很简单P(C|X) P(X|C) * P(C) / P(X)。放到糖尿病检测里C 就是“患病/不患病”两个类别X 是血糖、血压、胰岛素等特征向量。难点在于 P(X|C)——当 X 有 8 个维度时直接估计联合概率需要指数级样本量Pima 数据集只有 768 条记录根本不够。朴素贝叶斯加了一个“特征条件独立”假设P(X|C) P(x1|C) * P(x2|C) * ... * P(x8|C)。这个假设在医学指标上显然不成立——血糖和胰岛素抵抗高度相关BMI 和血压也有关联。但实际跑下来即使假设被违反分类效果依然可接受原因是决策只依赖后验概率的相对大小特征间的相关性在一定程度上会相互抵消。对于连续型特征通常假设它服从高斯分布于是 P(xi|C) 用正态密度函数计算import numpy as np def gaussian_probability(x, mean, var): 计算高斯概率密度 x: 单个特征值 mean: 该特征在某个类别下的均值 var: 该特征在某个类别下的方差 # 方差加极小值防止除零这是血泪经验 coefficient 1.0 / np.sqrt(2 * np.pi * var 1e-9) exponent np.exp(-((x - mean) ** 2) / (2 * var 1e-9)) return coefficient * exponent这段代码是高斯朴素贝叶斯的核心计算单元。参数 var 在训练时由极大似然估计得到但要注意如果某个特征在某个类别下的方差为 0比如某个指标在所有阳性样本里取值完全相同直接代入会导致除零错误。加 1e-9 是工程上的后悔药代价是引入极小偏差但避免了程序崩溃。2.2 为什么不用逻辑回归或决策树三个选型对比维度选型不是拍脑袋我一般从三个维度对比维度高斯朴素贝叶斯逻辑回归决策树小样本表现768 条即可稳定需要更多样本防止过拟合容易过拟合需剪枝训练速度毫秒级只算均值和方差需要迭代优化中等概率校准后验概率偏极端需校准天然输出概率叶子节点频率粗糙可解释性每个特征贡献可拆解系数可解释规则可解释缺失值容忍可跳过该特征需插补可处理逻辑回归在 Pima 数据集上做 5 折交叉验证AUC 大约 0.82高斯朴素贝叶斯大约 0.79差距不大。但朴素贝叶斯的训练时间不到逻辑回归的十分之一而且当特征维度增加到 20 个以上时逻辑回归需要更多样本才能收敛朴素贝叶斯依然稳定。决策树的问题在于Pima 数据集里血糖和 BMI 是强特征树模型会反复在这些特征上分裂导致对其他特征的利用不足。提示如果追求最高准确率且样本量超过 5000优先选梯度提升树如果样本量在 1000 以下且需要快速部署高斯朴素贝叶斯是更务实的选择。2.3 数据预处理Pima 数据集里那些零值不是缺失值Pima Indians Diabetes 数据集有个经典坑Glucose、BloodPressure、SkinThickness、Insulin、BMI 这五列里0 是无效值而不是真实测量值。比如血糖为 0 在生理上不可能实际是缺失。直接当数值喂给模型均值和方差全被拉偏。处理方式有两种一是把 0 替换成该列的中位数或均值二是直接删除这些行。我一般用中位数填充因为医学指标分布通常偏态均值容易被极端值带跑。import pandas as pd import numpy as np # 加载数据 df pd.read_csv(diabetes.csv) # 需要处理的列 zero_cols [Glucose, BloodPressure, SkinThickness, Insulin, BMI] # 把 0 替换为 NaN再填中位数 for col in zero_cols: df[col] df[col].replace(0, np.nan) median_val df[col].median() df[col] df[col].fillna(median_val) # 确认没有剩余缺失值 print(df.isnull().sum().sum()) # 输出 0这段代码的关键在于先替换再填充不能直接对 0 填中位数因为 0 会参与中位数计算。参数 median_val 是每列独立计算的不能用一个全局中位数。处理完之后Insulin 列的均值会从原来的 79 左右升到 120 以上这个变化会直接影响后验概率的计算结果。2.4 特征标准化与方差平滑两个影响后验概率的隐藏参数高斯朴素贝叶斯虽然对特征尺度不像 KNN 那么敏感但方差的计算依然受量纲影响。比如 Insulin 的取值范围是 0 到 846而 DiabetesPedigreeFunction 在 0 到 2.5 之间前者的方差会比后者大两个数量级。这会导致在计算后验概率时Insulin 的似然项数值极小浮点下溢风险高。常见做法是做 Z-score 标准化from sklearn.preprocessing import StandardScaler scaler StandardScaler() feature_cols [c for c in df.columns if c ! Outcome] df[feature_cols] scaler.fit_transform(df[feature_cols])标准化之后每个特征的均值为 0、方差为 1高斯密度函数的指数部分不会出现极端值。另一个参数是 var_smoothingsklearn 的 GaussianNB 默认是 1e-9作用是给方差加一个极小值防止除零。如果数据里有特征方差接近 0可以适当调大到 1e-8 或 1e-7代价是模型稍微平滑但稳定性提升。注意标准化要在划分训练集和测试集之后用训练集的均值和方差去变换测试集否则会引入数据泄露。我见过有人先对全量数据标准化再切分交叉验证分数虚高 3 到 5 个百分点。3. 从零实现高斯朴素贝叶斯分类器并完成训练评估3.1 手写 fit 和 predict把后验概率算清楚不调 sklearn 的 GaussianNB自己写一遍才能理解每个参数怎么来的。核心就两步训练时按类别统计每个特征的均值和方差预测时算对数后验概率取最大的那个类别。class NaiveBayesDiabetes: def __init__(self, var_smoothing1e-9): self.var_smoothing var_smoothing self.class_prior {} self.feature_mean {} self.feature_var {} def fit(self, X, y): X: 特征矩阵shape (n_samples, n_features) y: 标签0 或 1 self.classes np.unique(y) n_samples X.shape[0] for c in self.classes: X_c X[y c] # 先验概率该类样本数 / 总样本数 self.class_prior[c] X_c.shape[0] / n_samples # 每个特征的均值和方差 self.feature_mean[c] X_c.mean(axis0) self.feature_var[c] X_c.var(axis0) self.var_smoothing def predict(self, X): predictions [] for x in X: posteriors [] for c in self.classes: # 对数先验 log_prior np.log(self.class_prior[c]) # 对数似然对每个特征的高斯密度取对数再求和 log_likelihood np.sum( -0.5 * np.log(2 * np.pi * self.feature_var[c]) - ((x - self.feature_mean[c]) ** 2) / (2 * self.feature_var[c]) ) posteriors.append(log_prior log_likelihood) predictions.append(self.classes[np.argmax(posteriors)]) return np.array(predictions)fit 方法里class_prior 是类先验Pima 数据集里阳性样本约占 35%阴性约 65%这个比例会直接影响后验。feature_var 加了 var_smoothing防止某个特征在某个类别下方差为 0。predict 方法用对数概率代替直接乘概率避免多个小概率相乘导致浮点下溢——8 个特征的概率密度乘起来可能小到 1e-30取对数后变成 -69 左右在浮点数安全范围内。参数 var_smoothing 的默认值 1e-9 适用于标准化后的数据。如果没做标准化建议调到 1e-6 甚至 1e-5因为原始量纲下方差可能很大加太小不起作用。3.2 训练集测试集划分与分层采样别让阳性样本全跑测试集Pima 数据集只有 268 个阳性样本如果随机划分测试集里可能只剩 50 个阳性评估指标波动极大。必须用分层采样保证训练集和测试集的类别比例一致。from sklearn.model_selection import train_test_split X df[feature_cols].values y df[Outcome].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy # 关键参数 ) print(f训练集阳性比例: {y_train.mean():.3f}) print(f测试集阳性比例: {y_test.mean():.3f})stratifyy 保证划分后两个集合的阳性比例都接近 0.35。random_state42 是为了结果可复现换一个种子准确率可能波动 1 到 2 个百分点。test_size0.2 是常见选择768 条数据里留 154 条做测试训练集 614 条对于朴素贝叶斯来说足够估计均值和方差。3.3 混淆矩阵与召回率为什么准确率会骗人训练完直接看准确率80% 看着还行但拆开混淆矩阵会发现漏诊不少。from sklearn.metrics import confusion_matrix, classification_report model NaiveBayesDiabetes(var_smoothing1e-9) model.fit(X_train, y_train) y_pred model.predict(X_test) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names[未患病, 患病]))输出大概是这样混淆矩阵 [[85, 15], [22, 32]]意思是 85 个真阴性正确识别15 个假阳性22 个假阴性漏诊32 个真阳性正确识别。召回率 32 / (3222) 59.3%也就是说每 10 个真病人漏掉 4 个。这个数字在临床筛查里是不可接受的。问题出在默认决策阈值是 0.5后验概率超过 0.5 才判为阳性。但朴素贝叶斯的后验概率往往偏极端——要么接近 0 要么接近 1导致很多实际阳性的样本后验概率在 0.4 左右就被判为阴性。解决办法是调整阈值或者做概率校准。3.4 阈值调优与概率校准把召回率拉到 75% 以上调整决策阈值是最直接的手段。把阈值从 0.5 降到 0.35召回率能提升到 75% 左右代价是假阳性增加。# 获取正类的后验概率 def predict_proba(self, X): probas [] for x in X: posteriors [] for c in self.classes: log_prior np.log(self.class_prior[c]) log_likelihood np.sum( -0.5 * np.log(2 * np.pi * self.feature_var[c]) - ((x - self.feature_mean[c]) ** 2) / (2 * self.feature_var[c]) ) posteriors.append(log_prior log_likelihood) # 对数概率转回概率并归一化 posteriors np.array(posteriors) posteriors np.exp(posteriors - np.max(posteriors)) probas.append(posteriors / posteriors.sum()) return np.array(probas) probas model.predict_proba(X_test) # 取正类概率阈值设为 0.35 y_pred_adjusted (probas[:, 1] 0.35).astype(int) print(classification_report(y_test, y_pred_adjusted))predict_proba 里先减最大值再取 exp是防止 exp 溢出。阈值 0.35 不是拍脑袋是在验证集上从 0.2 到 0.5 按 0.05 步长扫出来的。如果场景对漏诊容忍度极低可以降到 0.3召回率能到 80%但假阳性会翻倍。这个权衡必须和业务方确认不能自己决定。提示概率校准可以用 CalibratedClassifierCV 包一层把朴素贝叶斯的极端概率映射到更真实的分布但会引入额外计算开销。如果只是做筛查排序不校准也能用。4. 把模型包成 Spring Boot 接口并接入前端页面4.1 模型导出与 Flask 推理服务Python 侧的最小实现Spring Boot 不直接跑 Python 模型常见做法是用 Flask 或 FastAPI 包一个推理接口Spring Boot 通过 HTTP 调用。先把训练好的模型参数导出成 JSON避免每次启动重新训练。import json model_params { class_prior: {str(k): float(v) for k, v in model.class_prior.items()}, feature_mean: {str(k): v.tolist() for k, v in model.feature_mean.items()}, feature_var: {str(k): v.tolist() for k, v in model.feature_var.items()}, var_smoothing: model.var_smoothing } with open(nb_model_params.json, w) as f: json.dump(model_params, f)导出时把 numpy 数组转成 listnumpy 的 float 类型不能直接 JSON 序列化。class_prior 的 key 从 numpy int 转成 str因为 JSON 的 key 必须是字符串。Flask 推理服务from flask import Flask, request, jsonify import json import numpy as np app Flask(__name__) with open(nb_model_params.json, r) as f: params json.load(f) app.route(/predict, methods[POST]) def predict(): data request.get_json() features np.array(data[features]) posteriors [] for c in [0, 1]: log_prior np.log(params[class_prior][c]) mean np.array(params[feature_mean][c]) var np.array(params[feature_var][c]) log_likelihood np.sum( -0.5 * np.log(2 * np.pi * var) - ((features - mean) ** 2) / (2 * var) ) posteriors.append(log_prior log_likelihood) posteriors np.array(posteriors) posteriors np.exp(posteriors - np.max(posteriors)) probas posteriors / posteriors.sum() return jsonify({ prediction: int(np.argmax(probas)), probability: float(probas[1]) }) if __name__ __main__: app.run(host0.0.0.0, port5000)这个接口接收 JSON 格式的特征数组返回预测类别和阳性概率。注意 features 的顺序必须和训练时一致否则均值和方差对不上。我一般会在接口文档里写死特征顺序Pregnancies、Glucose、BloodPressure、SkinThickness、Insulin、BMI、DiabetesPedigreeFunction、Age。4.2 Spring Boot 调用推理服务RestTemplate 配置与超时处理Spring Boot 侧用 RestTemplate 调 Flask 接口关键是要设超时否则 Flask 卡住会拖垮整个请求链路。Configuration public class RestTemplateConfig { Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); // 连接超时 3 秒 factory.setConnectTimeout(3000); // 读取超时 5 秒 factory.setReadTimeout(5000); return new RestTemplate(factory); } }连接超时设 3 秒是因为 Flask 服务通常在同一内网握手很快。读取超时 5 秒是给模型推理留余量虽然朴素贝叶斯计算只要毫秒级但网络抖动和 JSON 序列化可能占时间。如果超时频繁先查 Flask 服务是否单线程阻塞再考虑加 Gunicorn 多 worker。Service 层调用Service public class DiabetesPredictionService { Autowired private RestTemplate restTemplate; private static final String PREDICT_URL http://localhost:5000/predict; public PredictionResult predict(double[] features) { MapString, Object request new HashMap(); request.put(features, features); try { ResponseEntityMap response restTemplate.postForEntity( PREDICT_URL, request, Map.class ); MapString, Object body response.getBody(); return new PredictionResult( (Integer) body.get(prediction), ((Number) body.get(probability)).doubleValue() ); } catch (ResourceAccessException e) { // 超时或连接拒绝记录日志并返回兜底结果 throw new PredictionException(推理服务不可用, e); } } }这里用 Map 接收响应而不是定义 DTO是为了减少类数量。但生产环境建议定义明确的 DTO避免类型转换异常。异常处理里区分了 ResourceAccessException 和其他异常前者通常是网络问题后者可能是 Flask 返回了非 200 状态码。4.3 前端表单与结果展示把后验概率翻译成医生能懂的话前端不需要展示后验概率的具体数值医生要的是“高风险/低风险”加一句解释。我一般把概率分成三档低于 0.3 显示低风险0.3 到 0.6 显示中风险建议复查高于 0.6 显示高风险建议进一步检查。function renderResult(probability) { let level, color, advice; if (probability 0.3) { level 低风险; color #52c41a; advice 当前指标未见明显异常建议保持定期体检。; } else if (probability 0.6) { level 中风险; color #faad14; advice 部分指标偏离正常范围建议 3 个月内复查血糖。; } else { level 高风险; color #f5222d; advice 多项指标异常建议尽快到内分泌科就诊。; } document.getElementById(result-level).textContent level; document.getElementById(result-level).style.color color; document.getElementById(result-advice).textContent advice; document.getElementById(result-prob).textContent 阳性概率${(probability * 100).toFixed(1)}%; }分档阈值 0.3 和 0.6 是根据召回率和假阳性率的权衡定的。如果业务方希望更保守可以把中风险下限降到 0.25。前端不直接显示“患病/未患病”因为模型输出的是概率不是诊断措辞上必须留有余地。4.4 接口联调时最容易翻车的三个参数第一个是特征顺序。前端表单提交的字段顺序如果和训练时不一致比如把 BMI 和 Insulin 调换了模型算出来的后验概率完全错误但接口不会报错只会返回一个看似合理的结果。解决办法是在 Flask 接口里加一层字段名映射不依赖数组顺序。第二个是数值范围。前端传过来的血糖值如果是 mg/dL 单位而训练数据也是 mg/dL那没问题。但如果前端做了单位转换比如 mmol/L 转 mg/dL转换系数搞错输入值偏大或偏小后验概率会偏移。我一般在前端加输入范围校验血糖超过 300 或低于 40 直接提示重新输入。第三个是空值处理。前端某个字段没填传了 null 或空字符串Flask 侧 np.array 会变成 object 类型计算时抛异常。必须在 Spring Boot 侧做参数校验所有特征字段非空且为数值才能调推理接口。5. 避坑与排查糖尿病检测系统落地时最容易踩的五个坑5.1 现象测试集准确率 85%上线后医生反馈“全是误报”原因训练时用了全量数据做标准化测试集的均值和方差被泄露到训练过程。上线后新数据的分布和训练集不同标准化参数不匹配导致后验概率整体偏移。解决标准化必须用训练集的均值和方差测试集和新数据都用同一套参数变换。代码上就是先 train_test_split再对 X_train 做 fit_transform对 X_test 只做 transform。5.2 现象某个特征在阳性类别下方差为 0预测时程序直接崩溃原因Pima 数据集里 SkinThickness 在部分阳性样本中取值相同方差计算出来是 0高斯密度函数除零。解决var_smoothing 参数不能省默认 1e-9 在标准化后够用。如果没做标准化调到 1e-6。另外可以在训练前检查每个特征在每个类别下的方差如果小于 1e-6 就考虑删除该特征或做平滑。5.3 现象Flask 接口单次请求要 2 秒以上Spring Boot 频繁超时原因Flask 默认单线程多个请求排队。朴素贝叶斯计算本身是毫秒级但 JSON 序列化和网络往返在并发下被放大。解决用 Gunicorn 起多 worker命令是 gunicorn -w 4 -b 0.0.0.0:5000 app:app。worker 数量一般设为 CPU 核数的 2 倍加 1。同时 Spring Boot 侧把读取超时从 5 秒调到 10 秒给冷启动留余量。5.4 现象调整阈值后召回率上去了但假阳性太多业务方不接受原因朴素贝叶斯的后验概率偏极端阈值降低后大量阴性样本的后验概率也超过阈值。解决不要只调阈值先做概率校准。用 CalibratedClassifierCV 的 isotonic 方法包一层把极端概率映射到更真实的分布然后再调阈值。校准需要额外留一个验证集不能再用测试集。5.5 现象前端传过来的 BMI 是 0模型判为低风险但实际病人 BMI 正常原因前端表单没做必填校验用户跳过 BMI 字段后端默认填 0。0 在标准化后是一个极端负值模型把它当成“极低 BMI”反而降低了阳性概率。解决后端加参数校验任何特征值为 0 或超出合理范围BMI 小于 10 或大于 60直接返回错误提示不调推理接口。前端也要加 required 属性和范围提示。6. 用交叉验证和代价敏感学习把召回率推到 80%6.1 分层 K 折交叉验证别用单次划分评估模型单次 train_test_split 的评估结果波动太大换一个 random_state 召回率可能差 5 个百分点。用分层 K 折交叉验证把数据分成 5 份每次用 4 份训练 1 份验证最后取平均。from sklearn.model_selection import StratifiedKFold from sklearn.metrics import recall_score skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) recalls [] for train_idx, val_idx in skf.split(X, y): X_train_fold, X_val_fold X[train_idx], X[val_idx] y_train_fold, y_val_fold y[train_idx], y[val_idx] model NaiveBayesDiabetes(var_smoothing1e-9) model.fit(X_train_fold, y_train_fold) probas model.predict_proba(X_val_fold) y_pred_fold (probas[:, 1] 0.35).astype(int) recalls.append(recall_score(y_val_fold, y_pred_fold)) print(f5 折召回率: {np.mean(recalls):.3f} ± {np.std(recalls):.3f})输出大概是 0.76 ± 0.04说明模型在不同划分下表现稳定。如果标准差超过 0.08说明数据分布不均匀或者样本量太小需要考虑增加数据或换更简单的模型。6.2 代价敏感学习给漏诊加权重朴素贝叶斯本身不支持样本权重但可以在计算先验概率时手动调整。把阳性类别的先验概率乘以一个惩罚系数让模型更倾向于判为阳性。class CostSensitiveNB(NaiveBayesDiabetes): def __init__(self, var_smoothing1e-9, cost_ratio3.0): super().__init__(var_smoothing) self.cost_ratio cost_ratio def fit(self, X, y): super().fit(X, y) # 阳性类先验乘以代价比 self.class_prior[1] * self.cost_ratio # 重新归一化 total sum(self.class_prior.values()) for c in self.class_prior: self.class_prior[c] / totalcost_ratio3.0 表示漏诊一个阳性的代价是误诊一个阴性的 3 倍。这个系数从 1 到 5 扫一遍看召回率和精确率的平衡点。一般 cost_ratio 在 2 到 4 之间召回率能提升 8 到 12 个百分点精确率下降 5 到 8 个百分点。6.3 特征选择去掉 DiabetesPedigreeFunction 反而更好Pima 数据集里 DiabetesPedigreeFunction 是糖尿病家族史的函数取值范围 0.08 到 2.42。这个特征在阳性类别下的均值和阴性类别下差别不大方差也接近对后验概率的贡献很小。去掉它之后模型复杂度降低召回率反而提升 1 到 2 个百分点。# 去掉 DiabetesPedigreeFunction selected_features [Pregnancies, Glucose, BloodPressure, SkinThickness, Insulin, BMI, Age] X_selected df[selected_features].values特征选择的标准是看每个特征在两类下的均值差除以合并标准差也就是效应量。效应量小于 0.2 的特征可以考虑去掉。Glucose 的效应量最大超过 1.0是核心特征Insulin 和 BMI 在 0.5 左右DiabetesPedigreeFunction 只有 0.15去掉影响不大。6.4 一个可复现的完整评估脚本把前面的步骤串起来从数据加载到交叉验证评估跑一遍大概 10 秒。import pandas as pd import numpy as np from sklearn.model_selection import StratifiedKFold from sklearn.preprocessing import StandardScaler from sklearn.metrics import recall_score, precision_score, roc_auc_score # 1. 加载和清洗 df pd.read_csv(diabetes.csv) zero_cols [Glucose, BloodPressure, SkinThickness, Insulin, BMI] for col in zero_cols: df[col] df[col].replace(0, np.nan) df[col] df[col].fillna(df[col].median()) # 2. 特征选择 feature_cols [Pregnancies, Glucose, BloodPressure, SkinThickness, Insulin, BMI, Age] X df[feature_cols].values y df[Outcome].values # 3. 交叉验证 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) metrics {recall: [], precision: [], auc: []} for train_idx, val_idx in skf.split(X, y): X_train, X_val X[train_idx], X[val_idx] y_train, y_val y[train_idx], y[val_idx] # 标准化只用训练集拟合 scaler StandardScaler() X_train scaler.fit_transform(X_train) X_val scaler.transform(X_val) # 训练代价敏感模型 model CostSensitiveNB(var_smoothing1e-9, cost_ratio3.0) model.fit(X_train, y_train) probas model.predict_proba(X_val) y_pred (probas[:, 1] 0.35).astype(int) metrics[recall].append(recall_score(y_val, y_pred)) metrics[precision].append(precision_score(y_val, y_pred)) metrics[auc].append(roc_auc_score(y_val, probas[:, 1])) print(f召回率: {np.mean(metrics[recall]):.3f} ± {np.std(metrics[recall]):.3f}) print(f精确率: {np.mean(metrics[precision]):.3f} ± {np.std(metrics[precision]):.3f}) print(fAUC: {np.mean(metrics[auc]):.3f} ± {np.std(metrics[auc]):.3f})跑出来召回率大约 0.80精确率 0.65AUC 0.83。召回率从最初的 0.59 提到 0.80代价是精确率从 0.72 降到 0.65假阳性多了但漏诊少了。这个权衡在筛查场景里是值得的。6.5 我踩过的最大坑别在测试集上反复调阈值刚开始做的时候我在测试集上从 0.2 到 0.5 扫阈值找到召回率最高的那个点然后写进报告。后来换了一批新数据召回率直接掉到 0.6。原因是阈值是在测试集上过拟合的测试集变成了验证集评估结果虚高。正确做法是划三份训练集、验证集、测试集。训练集拟合模型验证集调阈值和 cost_ratio测试集只跑一次跑完就锁死。如果数据量不够用交叉验证代替验证集但测试集必须留出来。这个习惯我后来每个项目都保持再也没翻过车。希望帮到你。本文还有配套的精品资源点击获取
