简介:一套基于机器学习的软件缺陷预测系统完整源码与配套数据资料,面向软件测试、数据挖掘与机器学习方向的初学者及科研人员,可帮助快速搭建从数据预处理、特征选择到模型训练与评估的完整预测流程。压缩包共90个文件,以40个arff格式数据集、29个Python源码文件、10个已训练好的pkl模型为核心,另含界面UI、项目配置及依赖库等,整体大小仅8.74MB,目录结构清晰便于按模块查阅。数据覆盖PC1、PC2、MC1、KC1等公开缺陷数据集,源码实现SVM、逻辑回归、随机森林、朴素贝叶斯、KNN、ANN等多种经典算法,并提供图形界面与测试工具,便于直观操作与结果展示。附带训练完成的模型文件可直接加载调用,省去重复训练时间,适合用于毕业设计、课程实验、缺陷预测技术研究或算法对比分析。目前已有439人学习下载。
1. 机器学习软件缺陷预测:先用静态指标锁定高风险模块
刚接手一个遗留 Java 系统时,测试排期的第一反应往往是按代码行数或上线时间排,结果常常是在没问题的模块上浪费两轮迭代,真正的重灾区拖到最后。基于机器学习的软件缺陷预测做的,是用历史缺陷记录加代码静态度量训练一个分类器,给每个模块输出有缺陷概率,测试资源按概率分配。标题里这个 zip 装的就是这类系统常见的组成:训练源码、模型或完整训练流程、一批公开缺陷数据集(NASA MDP、PROMISE 系列)。适合三类人:做质量看板的测试负责人、给项目设缺陷阈值的研发主管、拿公开数据集做对照实验的学生和研究者。下面直接拆特征、数据、训练、评价到落地这条链路。
2. 软件缺陷预测的建模基础:特征、数据集与评价口径
2.1 特征从哪来:代码度量与提交历史
缺陷预测的特征分两大来源。第一类是静态代码度量,直接从源码或中间表示算出来,不需要运行程序,最常见的是规模类(总行数、语句数、注释比例)、复杂度类(McCabe 圈复杂度、Halstead 难度与工作量)、耦合内聚类(扇入扇出、模块间耦合 CBO、内聚缺乏度 LCOM)。第二类是过程度量,来自版本库历史,比如代码变更行数 churn、参与修改的开发人数、最近 90 天的提交次数。实践里静态度量更容易获取,大部分公开数据集也只含静态度量,所以第一版落地通常先用它。
| 类别 | 典型度量 | 实际含义 |
|---|---|---|
| 规模 | LOC_total, LOC_code, LOC_comment | 模块体量与注释比例,体量大往往缺陷绝对数高 |
| 复杂度 | cyclomatic_complexity, Halstead_volume | 控制流分支多则测试路径多,回归风险高 |
| 耦合 | fan_in, fan_out, CBO | 依赖越多,变更影响面越大 |
| 内聚 | LCOM, LCOM3 | 方法各做各的事,模块职责不清晰 |
| 过程 | churn, developers_count, commits_90d | 变更频繁的区域,缺陷注入概率高 |
圈复杂度是最常用也最容易解释的单特征:v(G) = E - N + 2,E 是控制流图的边数,N 是节点数。一个模块 v(G) 超过 10,测试路径就开始指数增长,这类模块在统计数据里与缺陷密度的相关性也非常一致。注意,表格里的度量列天然存在高相关,比如 LOC 和 Halstead volume 经常相关系数超过 0.95,预处理阶段必须处理,否则线性模型和树模型的特征重要性都会被重复投票带偏。
2.2 数据集的常见格式与类不平衡现状
标题里"全部数据资料"在绝大多数这类项目里,指的就是 NASA MDP 与 PROMISE 仓库的公开数据集。常见文件是 CSV,一行一个模块或文件,列是上一节的度量,最后一列是缺陷标签,取值多为 True/False 或 Y/N。NASA 系列的 CM1、KC1、PC1 等数据集年代久远、字段命名不统一,解压后第一件事是用 pandas 查看列名和类型,而不是直接套模板。
这类数据有个绕不开的现状:缺陷模块占比通常只有 5% 到 20%,KC1 大约 15%,类不平衡是数据集的默认属性。直接用原始比例训练,分类器会倾向把所有样本判为无缺陷,因为这样就能拿到 80% 以上的准确率。所以后续流程里,过采样、class_weight 和评价口径三者至少要选一个,这是判断一个缺陷预测实现是否专业的分水岭。
2.3 为什么先定基线分类器和评价口径
拿到数据后最常见的错误是先挑"看起来高级"的机器学习模型堆上去。我一般会先固定两个基线:逻辑回归和随机森林。逻辑回归作为线性基线,参数少、结果可解释,能反映原始特征空间里缺陷模式是否线性可分;随机森林作为非线性基线,能处理特征交互,且自带 feature_importance 可以做后续剪枝参考。评价口径固定为分层 10 折交叉验证下的 F1 和 AUC,不在任何实验里单独报 accuracy。评价计分器可以这样搭:
from sklearn.model_selection import cross_val_score, StratifiedKFold from sklearn.metrics import f1_score, roc_auc_score, make_scorer # 分层 10 折,保证每一折里缺陷样本比例与整体一致 cv = StratifiedKFold(n_splits=10, shuffle=True, random_state=42) f1_scores = cross_val_score(model, X_s, y, cv=cv, scoring='f1') auc_scores = cross_val_score(model, X_s, y, cv=cv, scoring=make_scorer(roc_auc_score, needs_proba=True))这里的关键点是needs_proba=True,AUC 需要预测概率而不是类别标签,make_scorer不传这个参数会直接报错。F1 取宏平均还是二分类的 F1 也要在实验记录里写清楚,缺陷预测领域默认报的是缺陷类(少数类)的 F1,即 precision 和 recall 都只针对标签为缺陷的样本计算,这个口径直接决定后面调参的方向。
3. 搭建缺陷预测系统:从解压 zip 到跑通最小训练流程
3.1 解压后先看工程结构,再定入口
这类 zip 解压出来通常有 data、src、model、doc 四个目录。data 下是原始 CSV 和一份说明缺陷标签与列名含义的文档;src 里是预处理、训练、评估三个脚本;model 目录放训练产物 .pkl 或 .joblib;doc 里是复现步骤和版本记录。我的习惯是先读 README 或数据说明,确认标签列名,再执行预处理脚本,因为不同来源的列名差异很大,直接跑会栽在字段上。一个常见的最小结构长这样:
defect-predictor/ ├── data/kc1.csv ├── src/ │ ├── preprocess.py │ ├── train.py │ └── evaluate.py ├── model/rf_20240601.pkl └── README.md不追求目录华丽,但 preprocess 和 train 必须分开,因为数据清洗和特征构建是可复用的独立环节,后面换数据集或换算法时不需要重跑清洗。
3.2 数据加载与预处理:缺失值、高相关列和缺陷标签
预处理脚本的第一步是把原始 CSV 洗成可以直接喂模型的矩阵:
import pandas as pd import numpy as np df = pd.read_csv('data/kc1.csv') # 标签列常见命名:defective/bug/flag,值可能是 Y/N 或 True/False label_col = 'defective' df[label_col] = df[label_col].map({'Y': 1, 'N': 0}) # 模块名、路径这类非度量列直接去掉 id_cols = [c for c in df.columns if c.lower() in ('module_name', 'name', 'path', 'file')] X = df.drop(columns=id_cols + [label_col]) # 数值列缺失值用中位数填充,比均值对异常值更鲁棒 X = X.fillna(X.median()) # 删除相关系数超过 0.95 的冗余列,保留出现顺序靠前的那一个 corr = X.corr().abs() upper = corr.where(np.triu(np.ones(corr.shape), k=1).astype(bool)) drop_cols = [col for col in upper.columns if any(upper[col] > 0.95)] X = X.drop(columns=drop_cols) print(f'drop {len(drop_cols)} high-corr columns: {drop_cols}')np.triu取得相关矩阵的上三角,避免每对特征被重复判断两次;阈值 0.95 是经验值,对可解释性要求高的场景可以降到 0.9。缺失值用median()而不是mean(),因为复杂度类指标的分布是长尾的,均值会被少数极高复杂度模块拉偏。完成这一步后,特征列从二十几个降到十几个是正常现象。
3.3 类不平衡处理:SMOTE 必须放在划分之后
过采样是缺陷预测里最容易被用错的一步。正确做法是先划分训练集和测试集,再用 SMOTE 只对训练集合成少数类样本,测试集保持真实分布,否则过采样样本泄露进测试集,评价指标虚高,上了真实项目立刻现原形。
from sklearn.model_selection import train_test_split from imblearn.over_sampling import SMOTE # stratify=y 保证划分前后缺陷比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) smote = SMOTE(random_state=42, sampling_strategy='auto') X_train_r, y_train_r = smote.fit_resample(X_train, y_train)sampling_strategy='auto'会把少数类合成到与多数类等量;想更贴近真实缺陷率,就写sampling_strategy=0.5,把缺陷类合成到多数类的一半,避免引入太多合成噪声。SMOTE 生成的样本是两个近邻在特征空间连线上插值,如果特征还处于原始量纲(比如 LOC 和 LCOM 差几个数量级),先标准化再过采样,效果更可靠。
3.4 训练逻辑回归与随机森林两个基线
预处理和过采样完成之后,训练脚本就很薄了:
from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import f1_score, roc_auc_score scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train_r) # 逻辑回归对量纲敏感 X_test_s = scaler.transform(X_test) # 测试集只用 transform # 基线一:逻辑回归,class_weight 抵消不平衡 lr = LogisticRegression(max_iter=1000, class_weight='balanced') lr.fit(X_train_s, y_train_r) # 基线二:随机森林,树模型对量纲不敏感 rf = RandomForestClassifier(n_estimators=300, max_depth=12, class_weight='balanced', random_state=42) rf.fit(X_train_r, y_train_r) for name, model, Xt in [('LR', lr, X_test_s), ('RF', rf, X_train_r)]: pred = model.predict(Xt) proba = model.predict_proba(Xt)[:, 1] print(f'{name}: F1={f1_score(y_test, pred):.3f}, AUC={roc_auc_score(y_test, proba):.3f}')class_weight='balanced'是比 SMOTE 更轻量的不平衡处理,按类别频率自动加权,二者可叠加但建议先单独验证效果。随机森林喂 X_train_r 而不喂标准化版本,因为树的分裂只看特征相对排序,标准化不会带来增益;如果后续换线性模型或 SVM 再统一走标准化分支。300 棵树通常够收敛,max_depth=12 防止树过深导致训练集 F1 虚高,预测集掉点。
3.5 用特征重要性做第一轮特征剪枝
随机森林的 feature_importances_ 可以直接拿来指导特征取舍:
import pandas as pd importance = pd.Series(rf.feature_importances_, index=X.columns) top = importance.sort_values(ascending=False).head(10) print(top) # 剪掉重要性明显接近 0 的特征,重训看 F1 是否下降 low = importance[importance < 0.01].index.tolist() print('candidate to drop:', low)特征重要性是树模型基于不纯度减少的统计值,只能做相对排序,不能当绝对贡献率解读。常见做法是先把重要性低于 0.01 的特征删掉重训,如果 F1 不降甚至略升,说明删掉的是噪声;如果明显下降,说明该特征与缺陷存在弱关联,哪怕数值低也要保留。这一步要配合 2.3 的交叉验证口径,单次划分的 F1 波动太大,无法判断剪枝是否有效。
4. 评价指标与参数调优:别被 Accuracy 骗了
4.1 精确率、召回率与 F1:缺陷预测的取舍逻辑
在缺陷率 15% 的数据集上,一个把所有模块都判为无缺陷的模型 accuracy 有 85%,但对质量保障毫无用处。缺陷预测真正关心的是少数类指标:精确率是"模型报出的缺陷模块里真是缺陷的比例",召回率是"真实缺陷模块被模型找出来的比例"。测试资源是一个固定预算,精确率低意味着测试人力浪费在误报模块上,召回率低意味着漏测真实缺陷,F1 是二者的调和平均,用来做单值比较。
| 业务场景 | 优先指标 | 原因 |
|---|---|---|
| 测试预算极其紧张 | Precision | 报 10 个中 8 个,不浪费人力 |
| 发布前全量回归 | Recall | 少漏一个严重缺陷比多测一轮更值 |
| 常规迭代 | F1 / AUC | 在误报与漏报之间取均衡 |
AUC 是另一个必报指标,它衡量模型在所有阈值下的排序质量,不依赖最后选哪个概率阈值。同一个模型,把预测阈值从 0.5 降到 0.3,召回率提升但精确率下降,F1 可能变化不大,AUC 却完全不变。所以 AUC 适合对比不同机器学习算法,F1 适合对比同一算法在不同阈值下的实际效果。
4.2 同项目内、跨项目与跨版本:三种实验设定
缺陷预测里最常见的实验设定是 WPDP(同项目内预测):用项目 A 的历史版本数据训练,预测项目 A 的下一版本,这是最乐观的设定,因为数据分布最接近。再往外是 CPDP(跨项目预测):用项目 A 训练预测项目 B,由于语言、团队、测试力度不同,F1 通常会掉 10 到 20 个百分点。中间还有跨版本设定,用同一项目的前三个版本预测第四个版本,比纯跨项目更接近真实上线场景。
标题数据包里如果包含多个数据集的 CSV,就能把三种设定都跑一遍:同项目内做 10 折交叉验证,跨版本按时间顺序划分,跨项目用 A 训练预测 B。实践上不要再加随机打乱,跨版本必须按时间切分,并在文档里写清楚划分逻辑,否则结果站不住。
4.3 GridSearchCV 调随机森林的三个关键参数
调参不必扫全参数空间,先固定最影响过拟合的三个参数:
from sklearn.model_selection import GridSearchCV param_grid = { 'n_estimators': [200, 400], 'max_depth': [8, 12, None], 'min_samples_leaf': [1, 3, 5] } gs = GridSearchCV( RandomForestClassifier(class_weight='balanced', random_state=42), param_grid=param_grid, cv=StratifiedKFold(n_splits=5, shuffle=True, random_state=42), scoring='f1', n_jobs=-1, refit=True ) gs.fit(X_train_r, y_train_r) print('best:', gs.best_params_, 'cv F1:', round(gs.best_score_, 3))min_samples_leaf是抑制过拟合最有效的参数,缺陷数据里少数类样本少,叶子节点样本数太少会记住训练集的噪声。refit=True表示搜索结束后用全部训练集重训最佳组合,直接拿gs.best_estimator_预测测试集。搜索空间不要开太大,三个参数各给两三个候选值,5 折交叉验证就是几十次拟合,缺陷数据通常只有几百到几千行,几分钟内跑完。换成梯度提升树(XGBoost/LightGBM)时同理,只不过把网格搜索换成 early_stopping_rounds 按轮早停,观察的是验证集 F1 不再上升就停。
4.4 数据泄漏与过采样顺序:两个高频翻车点
数据泄漏是这类系统最容易出的隐蔽问题。常见有三种:一是用全量数据标准化后再划分,scaler 已看到测试集的均值和方差,属于轻微泄漏;二是 SMOTE 在整个数据集上过采样后再划分,合成样本和它的近邻很可能同时落在训练集和测试集里,测试指标虚高;三是用全量数据计算特征选择统计量再筛选,让选择结果间接包含测试集信息。解法一致:先划分,再在任何需要拟合统计量的操作上只用训练集。
提示:判断一个缺陷预测实现是否干净,看 fit/transform 的调用次数即可。X_test 只允许出现在
.transform里,任何.fit出现在测试集上都是泄漏。
5. 把缺陷预测接到真实代码库的三个落地技巧
5.1 用 radon 把自己的 Python 代码变成度量表
公开数据集练完手,最自然的落地是把同一套流程搬到自己的仓库。对 Python 项目,radon 能直接生成大部分静态度量:
pip install radon radon cc src/ -s -j > complexity.json # 圈复杂度,-s 附带复杂度数值 radon mi src/ -s # 可维护性指数,10-20 分区间风险高 radon raw src/ -s # 原始行数与注释占比radon 按函数输出 CC 值,需要按文件聚合成模块级特征,聚合口径建议同时保留 mean 和 max,因为一个文件里单个极高圈复杂度的函数,往往比平均复杂度更能预示缺陷。再把git log --numstat拉出来统计每个文件的 churn 和提交次数,合并进同一张表,就得到一份带过程度量的特征矩阵。
5.2 用混淆矩阵和概率排序验证模型可用性
模型训练完别只看 F1 数字,出一张混淆矩阵,重点看真实缺陷类的召回率:
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay best = gs.best_estimator_ cm = confusion_matrix(y_test, best.predict(X_test_s)) disp = ConfusionMatrixDisplay(cm, display_labels=['clean', 'buggy']) disp.plot()混淆矩阵右下角是真实缺陷且被预测为缺陷的数量,左下角是漏报的缺陷。如果左下角占比过高,说明当前阈值偏保守,改用best.predict_proba(X_test_s)[:, 1]拿到连续概率后自己定阈值,不要直接接受默认 0.5。我常用的验证动作是:按预测概率降序取前 20% 的模块,看真实缺陷覆盖率,画出缺陷率随预测概率递减的排序曲线,曲线下降越快,模型越可用。
5.3 按预测概率从高到低排测试优先级
最后一个技巧是把二分类输出改成概率排序,直接接进排期流程。系统上线时不输出"有缺陷/无缺陷",而是输出module_id, defective_probability两张字段的表,测试计划按概率降序安排:概率前 20% 的模块做全量回归并补新增用例,中间 40% 做冒烟,后 40% 仅在时间剩余时补充。阈值按团队能承受的误报成本调整,比如试点时误报率 30% 但漏报率只有 10%,就把阈值下调,重新跑一轮混淆矩阵确认漏报变化,直到测试资源的边际收益不再增加。
本文还有配套的精品资源,点击获取