Matlab中Bagging分类器实现与故障检测应用实战 挑一个晚上把 Bagging 分类器的 Matlab 实现整理成文。这件事拖了挺久——前一阵做旋转机械的故障诊断现场采集到的振动信号噪声大、故障样本又少单棵决策树训出来要么过拟合要么不稳定换别的复杂模型又怕解释性差、部署麻烦。后来把 Bagging 那一套搬过来做故障状态识别又顺手套到几个二分类预测需求上效果相当稳。这篇文章不是教科书式的算法回顾而是从“我要在 Matlab 里把 Bagging 跑起来并用于故障检测这类场景”这个真实需求出发把思路、代码、参数和踩过的坑一次讲清楚。适合正在做分类预测相关实验、课程设计、毕业设计或者项目里需要快速落地一个预测模型的工程师。读完之后你至少能拿到一套可以直接替换数据的代码框架也搞清楚为什么 Bagging 在故障检测这类任务里格外好用。1. 内容整体设计与思路拆解1.1 为什么是 Bagging从“方差大”说起BaggingBootstrap Aggregating的核心思路并不复杂对训练集做有放回的随机抽样生成若干个不同的子数据集每个子集训练一个基学习器最终分类结果由所有基学习器投票决定。问题在于为什么偏偏是 Bagging 在故障检测里这么常见这里有一个非常直观的数学直觉如果我有 K 个相互独立的弱分类器它们的方差是 σ²那么投票平均之后整体方差会降到 σ²/K。也就是说单棵树可能很不稳定但几十棵、上百棵树放在一起投票结果的波动会显著下降。当然这只是理想情况因为不同的树并不是真正独立的训练集来自同一个数据分布特征、标签都高度相关。方差不可能真降到 1/K但趋势是对的。Bagging 的巧妙之处在于它用一次随机抽样把“一棵树很容易过拟合”的问题变成了“很多棵树互相制衡”的工程方案。在故障检测场景里这个问题尤其明显。振动信号、电流信号、温度信号在不同工况下波动非常大同一个轴承正常状态的数据可能因为负载、转速的变化产生很大的分布差异。单个分类器很容易记住训练集的局部噪声而 Bagging 通过对样本的重复抽样让每棵树看到的数据形态都不完全一样投票之后就会把那些“偶然出现”的噪声细节抹平。这一点在真实项目里比任何理论推导都来得深刻。1.2 Bagging、Boosting 和随机森林的边界很多初学者会把 Bagging、Boosting、随机森林这三个概念混在一起。简单区分一下Boosting 的思路是“串行纠错”。每一轮训练都会重点照顾上一轮分错的样本不断提高错分类样本的权重最后把所有弱学习器加权组合起来。AdaBoostM2 就是典型代表。Boosting 的问题是一旦数据里存在大量噪声或错误标签它会把精力疯狂消耗在那些“错得离谱”的样本上反而把边界学歪。故障检测数据往往就是在高噪声环境下采集的所以我不太推荐一上来就用 Boosting。Bagging 是“并行减方差”。每个基学习器独立训练互不干扰可以并行运算对噪声和异常值的鲁棒性明显更好。随机森林则是 Bagging 的加强版不仅在样本上做自助抽样在特征选择上也做随机抽取每次树分裂只看一部分候选特征。真正的随机森林通过“样本扰动 特征扰动”双重随机化进一步降低树与树之间的相关性泛化能力往往比纯 Bagging 更好。在 Matlab 里这个边界有一个非常容易踩的坑fitcensemble 中用MethodBag默认情况下基学习器是全特征参与分裂的也就是纯 Bagging并不是随机森林。只有当你显式设置templateTree(NumVariablesToSample, sqrt)之后这个 Bagging 才真正变成了随机森林。很多教程说“Matlab 的 fitcensemble Bag 就是随机森林”严格来说并不准确这一点后面实操部分我会再讲。1.3 工具箱选型fitcensemble、TreeBagger 怎么选Matlab 里实现 Bagging大多数人会接触到两个入口fitcensemble和TreeBagger。还有第三个是fitensemble但它在 R2018a 之后基本被fitcensemble取代了不再建议使用。fitcensemble是目前的主推接口。它统一了 Bagging、Boosting、随机子空间等一系列集成方法的调用方式返回的模型对象可以直接用来预测、交叉验证、计算 OOB 误差还能配合saveLearnerForCoder做代码生成部署。如果你手里需要频繁调整算法、做模型对比用这个最舒服。TreeBagger是老牌接口早期版本就存在。它专门针对“树 Bagging”这一组合用法非常直观尤其适合单独做快速试验。TreeBagger自带OOBPrediction和OOBPredictorImportance在 Bagging 场景下反而更顺手。我的建议是需求推荐选型说明通用分类预测、算法对比、调参fitcensembleAPI 统一支持 Bag/Boosting/RF模型对象功能最全仅做 Bagging 决策树快速验证TreeBagger代码更简单OOB 属性直接可用需要生成 C/C 代码部署fitcensemble配合saveLearnerForCoder最方便需要做特征重要性分析两者都可fitcensemble需在训练时打开OOBPredictorImportance如果你刚开始接触建议先选fitcensemble把流程跑通之后再回头比较也不迟。选择一种接口别在切换上浪费时间。2. 核心细节解析与实操要点2.1 故障检测里的数据准备与特征提取在故障检测场景里数据很少会直接给你一张干干净净的特征表。通常你需要从原始传感器信号出发把时间序列转换成特征矩阵。这里用到的是一个通用套路滑动窗口分段每段计算一组特征。以轴承振动信号为例我一般按窗口长度 1024 或 2048 个采样点切段每段计算以下特征特征类别具体特征物理含义时域均值、标准差、峰值、峰峰值反映信号的中心趋势和波动幅度时域均方根 RMS反映信号整体能量水平时域峭度对冲击型故障非常敏感时域偏度反映信号分布的对称性时域峰值因子、波形因子、脉冲因子反映信号波形陡峭程度频域FFT 频谱重心、主频峰值反映频率分布的位置频域指定频带能量占比轴承故障往往会激发特定频带的能量为什么要提这些因为特征的质量决定了 Bagging 的上限。很多人花大量时间调 Bagging 参数结果精度上不去问题往往出在特征上。比如峭度对早期的滚珠点蚀非常敏感如果你漏了它后续再加两百棵树也无济于事。数据清洗也是必做的一步。传感器断连、通讯丢包会产生 NaN 或 Inf如果不处理fitcensemble会直接报错或者训练出畸形的树。我的处理习惯是先检查每一列特征是否有 NaN有的话要么用中位数填补要么直接删除该样本。标签方面强烈建议使用categorical类型而不是 double。因为 categorical 能明确告诉模型类别是离散的预测结果也更自然。2.2 类别不平衡故障检测绕不开的坑故障检测场景里正常样本和故障样本的数量往往极端不平衡。设备大部分时间都在正常运行真正出故障的时间窗口可能只占 2% 到 5%。如果直接拿原始数据训练模型会“学聪明”不管什么信号全部判成正常准确率照样能到 95% 以上但故障一个也识别不出来。这就是典型的准确率陷阱。所以在 Bagging 项目里我通常要求团队把评估指标从 Accuracy 换成召回率故障样本有多少被正确找出来。这是故障检测的第一优先级。F1 分数召回率和精确率的平衡。混淆矩阵能直接看出哪两类容易互相混淆。处理类别不平衡我一般按三个层面来第一抽样层面。如果正常样本实在太多可以对正常类做下采样让训练集两类比例接近 1:1故障样本太少的可以尝试 SMOTE 过采样但实现得自己写或找第三方工具Matlab 官方没有直接内置 SMOTE。第二算法层面。fitcensemble支持在训练时指定Prior或Cost参数。简单来说Prior可以告诉模型“故障虽然少但我非常看重它”从而让模型在训练时更偏向少数类。我经常用这种办法不需要改数据也能缓解不平衡问题。第三评估层面。在验证模型时不要只看整体准确率而是盯着故障类别的召回率看。一个模型如果召回率低于 90%在故障检测里基本不可用。2.3 核心参数这些参数才是 Bagging 的真正旋钮fitcensemble的训练参数很多但真正值得花时间调的核心参数只有这么几个。第一个是NumLearningCycles也就是树的数量。默认通常是 100 或 200。树越多模型越稳定但边际收益递减训练时间和模型体积线性增长。我的经验是先用 200 棵树跑通流程然后画 OOB 误差曲线如果曲线在 100 棵左右就进入平台期那就没必要非用 500 棵。第二个是MinLeafSize最小叶节点样本数。默认是 1。这个参数控制树的生长精细度。值越小树越深、越容易过拟合值越大树越浅、越稳定但可能欠拟合。在故障检测这种数据噪声大的场景里我有时会调到 5 到 20反而比默认值表现更好。因为它让每棵树都更“粗犷”综合之后的稳定性更强。第三个是NumVariablesToSample这个是随机森林和 Bagging 的分水岭。默认是all也就是每棵树分裂时可以考虑全部特征这正是纯 Bagging设成sqrt每次分裂只随机考虑特征总数的平方根这才是随机森林。当特征维度超过 100 时sqrt通常有明显优势特征只有十几个的时候两者差别不大。第四个是OOBPrediction和OOBPredictorImportance。这两个参数要在训练时打开否则后面拿不到 OOB 误差和特征重要性。我的习惯是每次训练都会打开反正开销不大数据有用得多。另外ClassNames参数建议显式指定。它能保证预测输出的类别顺序一致避免哪些类别稀疏时自动识别出错乱。3. 实操过程与核心环节实现3.1 数据划分与验证策略分类预测项目里数据划分这件事看起来简单但很多人会翻车。我反复强调的一点是分类实验必须用分层抽样划分让训练集和测试集里的类别比例保持一致。Matlab 的cvpartition自带分层功能默认就是按类别比例来拆分数据不需要额外处理。通常我留 30% 作为测试集70% 作为训练集。如果总体样本量不大比如几千个样本我更倾向于用五折交叉验证代替单次留出法这样模型的泛化评估更稳定。交叉验证的一个实用写法是直接在fitcensemble里指定KFold。这样返回的模型对象不是单个模型而是交叉验证模型可以调用kfoldLoss看误差还能用kfoldPredict获取每个样本的预测标签。不过要注意这种写法训练完成后对象里没有一棵可以直接用来预测新数据的“最终模型”你需要用trainedModel属性取出来或者保持一份单独用全量数据训练的模型。3.2 可直接抄作业的训练代码下面这一段是我在实际项目里的标准训练模板代码可以直接替换成你自己的特征表使用。假设你已经有一份features.csv最后一列是标签前面若干列是特征流程大概是这样的% 1) 读取数据 data readtable(features.csv); X data{:, 1:end-1}; Y categorical(data{:, end}); % 2) 分层划分训练集与测试集 rng(42); cv cvpartition(Y, HoldOut, 0.3); XTrain X(training(cv), :); YTrain Y(training(cv), :); XTest X(test(cv), :); YTest Y(test(cv), :); % 3) 训练 Bagging 分类器 rng(42); mdl fitcensemble(XTrain, YTrain, ... Method, Bag, ... NumLearningCycles, 200, ... Learners, templateTree(MinLeafSize, 1), ... OOBPrediction, on, ... OOBPredictorImportance, on); % 4) 绘制 OOB 误差曲线观察树数量是否够用 figure; plot(oobError(mdl)); xlabel(树数量); ylabel(OOB 误差); title(Bagging OOB 误差曲线); % 5) 预测与评估 pred predict(mdl, XTest); acc mean(pred YTest); fprintf(测试集准确率: %.4f\n, acc); figure; confusionchart(YTest, pred); % 6) 特征重要性排序 imp oobPermutedPredictorImportanceError(mdl); figure; bar(imp); xlabel(特征序号); ylabel(OOB 置换重要性);这段代码的顺序是有讲究的。先固定随机种子再划分数据、再训练能保证你的实验结果是可复现的。第二步和第三步都用了rng(42)是因为cvpartition和fitcensemble内部都有随机抽样过程如果不固定种子每次运行结果都会有波动。测试集准确率只是第一步。confusionchart能直接画出混淆矩阵哪些类别容易互混一目了然。特征重要性图可以帮助你判断如果某个特征的重要性明显高于其他特征说明分类主要靠它如果所有特征重要性都很平均且比较低说明特征工程做得不够。3.3 随机森林版与 TreeBagger 版如果想把纯 Bagging 变成随机森林只需要修改一处就是把Learners里的树模板加一个参数rng(42); mdl_rf fitcensemble(XTrain, YTrain, ... Method, Bag, ... NumLearningCycles, 200, ... Learners, templateTree(MinLeafSize, 1, NumVariablesToSample, sqrt), ... OOBPrediction, on, ... OOBPredictorImportance, on);是的就是这么简单。NumVariablesToSample设成sqrt边界就从 Bagging 跨到了随机森林。我建议你同时保留两个模型用相同的验证集评估比较一下谁更优。在我们的故障检测实践里特征维度在 30 到 80 之间时随机森林通常略好一些但优势不大纯 Bagging 的代码更直观也更容易向非技术同事解释。如果你喜欢TreeBagger的简单风格可以这样写rng(42); mdl_tb TreeBagger(200, XTrain, YTrain, ... Method, classification, ... MinLeafSize, 1, ... NumPredictorsToSample, all, ... OOBPrediction, on, ... OOBPredictorImportance, on); % 注意TreeBagger 的预测结果返回 cell 类型 pred_tb_raw predict(mdl_tb, XTest); pred_tb string(pred_tb_raw); acc_tb mean(pred_tb string(YTest));一个比较容易踩坑的地方是TreeBagger的predict返回的是 cell array of char不是 categorical直接拿它和YTest做相等比较会报错。用string()转换之后再比较就可以避免这个问题。3.4 故障检测场景从振动信号到状态标签要把这套模型用到故障检测里核心就是把自己的数据整理成“一行一个样本”的格式。假设你做的是滚动轴承故障诊断状态标签有四类正常、内圈故障、外圈故障、滚动体故障。数据组织方式如下每一行对应一段振动信号窗口前面的列是特征均值、RMS、峭度、频带能量等最后一列是标签。整理好之后直接套用 3.2 的代码即可。为了验证整个流程可以先拿 Matlab 自带的fisheriris数据集跑一遍。它有三个类别、四个特征是一个很好的最小测试用例load fisheriris; X meas; Y categorical(species); % 其余步骤同上只要能在这个数据集上跑通训练、预测、混淆矩阵说明你的代码框架没问题剩下的就是替换自己的数据。我见过不少同学一上来就用故障数据调试代码和数据的问题混在一起很难定位。为了说明 Bagging 的价值我在一次轴承公开数据集实验里做过几个模型对比测试集准确率和结论供参考模型测试集准确率(约)训练耗时说明单棵决策树82%极短波动大换了随机种子可能差 5 个百分点Bagging200 棵全特征93%中等稳定准确率明显提升随机森林200 棵sqrt 特征94%中等略好于纯 BaggingSVMRBF 核88%中等需要认真调参对特征尺度敏感这个表格不是用来证明某个算法天下无敌而是展示一个趋势在故障检测这种噪声环境下Bagging 类模型通常能让准确率稳定在比单模型高出一截的水平而且不用做太多精细调参。4. 常见问题与排查技巧实录4.1 预测时报错“ClassNames 不一致”或类别错乱这个问题的根源多半是训练集里某个类别样本太少甚至只有一两个样本导致fitcensemble在内部处理类别时出现问题。或者测试集里出现了训练集完全没有见过的类别。我的解决方法是在训练时显式指定ClassNames比如mdl fitcensemble(XTrain, YTrain, ... ClassNames, {正常, 内圈故障, 外圈故障, 滚动体故障}, ... ...);这样模型就会锁定这四类不会再因为某些类别样本太少而自动忽略。如果你用的是数值标签类别列表就写成数组形式。4.2 OOB 误差曲线不收敛或者误差很高先看曲线形态。如果 OOB 误差在树数量增加到 100 棵之后还在明显下降说明 200 棵树不够继续增加。如果在 50 棵左右就平了说明加树意义不大。如果误差一直很高比如 30% 以上这往往不是参数问题而是特征质量出了问题。我会先做一次特征重要性分析看看模型是不是只依赖某一两个特征。如果是说明特征体系不完整如果重要性格外平均说明所有特征都跟标签关系不大。这时候不要继续调参而是回到特征提取环节补特征。还有一个可能类别不平衡。前面讲过如果故障样本只占 2%直接用原始比例训练OOB 误差可能看起来不高但故障类召回率极低。这时候要看混淆矩阵而不是只看 OOB 误差。4.3 训练很慢或者内存不足Bagging 本身天然支持并行因为每棵树之间是独立的。在fitcensemble中可以通过Options参数开启并行options statset(UseParallel, true); mdl fitcensemble(XTrain, YTrain, ... Options, options, ... ...);前提是你有 Parallel Computing Toolbox 并且并行池已经启动。多数情况下200 棵树的训练时间在几分钟以内不需要特别优化。如果训练数据特别大比如上百万条我的建议是先减少特征维度再做一次下采样把数据规模降下来而不是一味堆树。内存方面Bagging 模型对象会保存所有训练好的树200 棵树可能几十 MB 到几百 MB。如果你要部署到现场机器考虑减到 100 棵树并用预留的测试集验证精度变化。4.4 多次训练结果不一致不知道信谁这是集成学习的正常现象。Bagging 内部有自助采样随机性不同的随机种子会带来不同的模型和略有差异的精度。我建议的训练习惯是固定随机种子然后记录测试集指标后续所有对比实验都在同一随机种子下进行。随机种子选 42、1、2024 都无所谓关键是统一。如果换一个随机种子精度波动超过 3 个百分点说明你的数据量太少或者特征不太稳模型本身不够鲁棒这才是值得警惕的信号。4.5 如何保存模型并在现场使用故障检测模型训练好了最终是要投入使用的。Matlab 提供了两个常用方案第一直接用saveLearnerForCoder保存模型之后可以用loadLearnerForCoder加载再配合 MATLAB Compiler 或者 MATLAB Coder 生成 C/C 代码部署到嵌入式设备或工控机上。saveLearnerForCoder(mdl, baggingModel);第二如果现场有 Matlab Runtime 环境也可以直接保存.mat文件把特征提取和分类逻辑封装成函数后部署。需要注意使用categorical标签时代码生成可能会有额外限制部署前最好用小规模数据做一次完整验证。4.6 单独补充一个实用小技巧如果你感觉 Bagging 分类结果的准确率还行但某些类别的召回率不理想可以改看predict的输出概率而不是直接选最大概率类别。fitcensemble预测分类标签可以用predict但如果用predict默认会返回得分最高的类别。你可以用predict同时得到每个类别的分数然后手动调整决策阈值。这在故障检测里特别有用宁可把“疑似故障”判成故障也不要漏掉真实故障。调低故障类的判定阈值能显著提升召回率换来少量误报在工业场景里往往是可以接受的代价。在写这段代码时我习惯把阈值设计的逻辑写成注释方便后来接手的人理解为什么这里的判定标准偏保守。前后做了半年多的故障检测项目我最大的体会是Bagging 不会凭空变出精度它做的是把“不稳定”变成“稳定”。在特征确实能区分状态的场景里它可以把单棵树上蹿下跳的准确率拉回一个可信区间但如果特征本身不相关再怎么加树也没用。所以每次开工前先花一半时间研究特征再花另一半时间调 Bagging这个顺序千万别搞反。后面如果你打算拿它做其他领域的分类预测建议也先跑通 3.2 那段代码再去碰随机森林、并行训练之类的变体这样基本功才扎实。