做故障诊断的朋友估计都遇到过这种场景:辛辛苦苦从传感器信号里提了一批特征,喂给随机森林,结果准确率卡在某个水平上不去。不是模型不行,是几个超参数怎么调都不对劲。手动试参像开盲盒,网格搜索慢得让人怀疑人生。后来我把贝叶斯优化和随机森林搭在一起,让算法自己去找合适的超参数,准确率上去了,时间还省了一大半。这篇文章就把这套贝叶斯优化随机森林的故障诊断流程完整拆开讲讲,并且附上Matlab实现代码。正在做设备故障诊断、或者想提高分类模型调参效率的朋友,可以从这里直接入手。
我先说明一下自己的使用背景,免得大家对着流程走偏。我一般处理的是旋转机械的振动信号、电流信号这类工业现场数据,特征维度不高,但样本量往往不大,而且故障样本比正常样本少很多。随机森林在这种场景下非常稳,前提是它的几个关键超参数别太离谱。用贝叶斯优化调参之后,不需要手动试几十组参数,大概几十次迭代就能收敛到一组不错的结果。下面我就按思路、原理、实现、避坑、扩展五个部分来写,整个流程可以直接照抄,再替换成自己的数据。
1. 为什么在故障诊断里选随机森林
1.1 故障诊断任务的数据特点
故障诊断本质上是一个多分类或二分类问题,输入是各种传感器信号里提取的特征,输出是设备状态类别,比如正常、不对中、磨损、松动等等。这类数据和普通的图像分类、文本分类有很大差别,最大的特点是“干净样本不够,脏样本更不够”。
工业现场采集到的特征,通常存在几个问题。第一,正常工况下的样本占绝大多数,故障样本稀少,类别分布严重不平衡。第二,现场噪声大,传感器偶尔还会丢包或者漂移,特征里会有离群点。第三,不同工况、不同负载下,同类故障的特征分布也会漂移,导致模型在新工况下性能下降。第四,特征之间往往高度相关,比如时域均值、峰值、有效值这些指标,可能本质上都来源于同一段振动波形。
随机森林对这种数据非常友好。它本身是Bagging加随机特征选择的集成方法,每棵树只看到一部分训练样本和一部分特征,最终结果通过投票产生。单棵树即使过拟合,多棵树投票之后也能把方差压下来,对离群点和噪声有一定容忍度。而且随机森林能直接计算特征重要性,这在做故障诊断时特别关键,因为很多时候我们不光要判断“有没有故障”,还要知道“哪些传感器特征起了主要作用”,方便后续做传感器布点和特征降维。
1.2 随机森林的主要超参数与调参难点
随机森林虽然好用,但不是建好模型就完事,它有一堆超参数需要确定。最核心的几个包括:树的数量、每棵树的深度或最大分裂数、叶子节点最少样本数、以及每次分裂时随机抽样的特征数量。
树的数量越多,模型越稳定,但训练时间和内存占用也会线性增长。树太深,容易把某些故障样本的局部噪声学进去;树太浅,又可能整体欠拟合。叶子节点最少样本数控制着模型的平滑程度,设成1时模型会对每一个训练样本都记下来,非常容易过拟合。最大分裂数则直接限制每棵树的复杂度。
这些参数之间的相互影响不是线性的。比如树的数量可以稍微多一点来补偿单棵树的方差,但叶子节点数过少又会引入过拟合。手动调参时,经常出现调了A参数,B参数又不行了的情况。网格搜索虽然可以系统遍历,但超参数空间大到一定程度后,计算量会爆炸。比如树数量取50个候选值、最大分裂数取50个候选值、最小叶子数取50个候选值,组合数就是125000次,每次都要完整训练一个随机森林,在故障诊断场景下完全不现实。
所以我们需要一种更聪明的搜索策略。它应该能在之前的评估结果基础上,判断下一组参数在哪里更有可能出好效果。这正是贝叶斯优化的核心价值。
2. 用贝叶斯优化替代网格搜索的关键思路
2.1 贝叶斯优化为什么适合超参数搜索
贝叶斯优化不是一种模型,而是一种黑盒全局优化策略。它假设目标函数是未知的,我们每次评估一组超参数,都会得到一个目标值,比如交叉验证错误率。然后它用一个概率代理模型,通常是高斯过程,去拟合“超参数到目标值”的关系。这个拟合结果不仅给出每一点的预测均值,还给出预测的不确定性。
有了均值和不确定性,贝叶斯优化就可以计算“在哪个点继续评估最有价值”。它的采集函数会平衡两方面:一是往已知的低目标值区域靠拢,这叫利用;二是往不确定性高、还没被探索过的区域采样,这叫探索。网格搜索是纯粹的盲试,随机搜索也只是增加了盲试的随机性,而贝叶斯优化像是一个有经验的寻宝者,每走一步都会根据之前的痕迹判断下一步该往哪挖。
Matlab中的bayesopt函数就是基于这套理论实现的。我们只需要定义超参数空间、目标函数和迭代次数,剩下的采样策略、代理模型更新都会自动处理。实际用下来,通常20到50次评估就能找到一组相当不错的参数,对比网格搜索动辄几百上千次,效率提升非常明显。
2.2 优化目标该怎么定义
贝叶斯优化的目标是让一个标量损失函数尽量小。很多初学者直接把测试集准确率当成目标,这很容易翻车。因为如果直接用测试集指导搜索,就等于让测试集参与了训练,最终的准确率是虚高的,换一批新数据马上打回原形。
正确的做法是在训练集内部做交叉验证,用交叉验证损失作为目标函数。我习惯用5折分层交叉验证,每次训练随机森林,然后算平均的分类损失。分层的意思是每一折训练集和验证集中,各类别比例尽量保持一致,这对故障诊断这种不平衡数据很重要。如果数据特别不平衡,直接使用分类错误率会造成模型严重偏向多数类,这时候最好把目标函数改成F1分数的负值,或者AUC的负值。因为bayesopt默认最小化目标,所以F1分数要取负数,才能让“F1越大”对应“损失越小”。
另外还要注意目标函数中是否包含随机性。随机森林训练本身有随机性,同样的超参数在不同随机种子下会得到略有不同的错误率。bayesopt有一个选项叫IsObjectiveDeterministic,默认是false,意思是它会认为目标函数带有噪声。如果强行设成true,可能会导致优化过程过于自信,把某些偶然的低错误率当成真实的最佳点。
3. Matlab下的完整实现流程
3.1 数据准备与特征工程
为了让大家能直接跑通流程,我这里用一个仿真特征数据来演示。假设已经通过振动信号处理,得到了一张特征表X,每一行是一个样本,每一列是一个统计特征,比如均值、峰值、有效值、峭度、频域重心等。标签向量Y是故障类型编码,1表示正常,2表示某类故障,3表示另一类故障。
第一步是划分训练集和测试集。这里有一个必须遵守的顺序:先划分数据,再做归一化。如果先对整个数据集做归一化,再把训练集和测试集分开,就会出现信息泄漏,测试集的信息已经通过均值和标准差混进了训练过程。正确的做法是只从训练集中计算归一化用的均值和标准差,然后用这套参数去变换训练集和测试集。
rng(42); holdout = cvpartition(Y, 'Holdout', 0.3); idTrain = training(holdout); idTest = test(holdout); Xtrain = X(idTrain, :); Ytrain = Y(idTrain, :); Xtest = X(idTest, :); Ytest = Y(idTest, :); % 基于训练集计算归一化参数 mu = mean(Xtrain, 1); sigma = std(Xtrain, 0, 1); sigma(sigma == 0) = 1; % 防止常数列导致除零 Xtrain = (Xtrain - mu) ./ sigma; Xtest = (Xtest - mu) ./ sigma;很多特征可能不在一个量纲上,比如均值和峭度差了几十倍,如果不归一化,树模型虽然在分裂时按阈值找,不太受量纲影响,但后续做特征重要性和模型解释时会别扭。更关键的是,某些特征工程步骤需要算距离,比如后续要加SMOTE或做聚类,归一化就是必须的了。
3.2 随机森林训练与贝叶斯优化参数设置
这一步是整个流程的核心。我先定义一个目标函数,输入一组超参数,通过交叉验证返回一个标量损失。这个函数要做的事情很简单:用当前的树数量和叶子节点限制训练一个随机森林,然后计算交叉验证错误率。
function loss = rfObjFun(params, X, Y) cv = cvpartition(Y, 'KFold', 5, 'Stratify', true); tpl = templateTree(... 'MinLeafSize', params.MinLeafSize, ... 'MaxNumSplits', params.MaxNumSplits); mdl = fitcensemble(X, Y, ... 'Method', 'Bag', ... 'NumLearningCycles', params.NumTrees, ... 'Learners', tpl, ... 'CVPartition', cv); loss = kfoldLoss(mdl); end然后定义超参数搜索空间,并调用bayesopt。这里我选择三个最关键的参数:树的数量、叶子节点最少样本数、每棵树最大分裂数。搜索范围不要设得太夸张,比如树数量从50到500,最小叶子从1到50,最大分裂从5到200。范围太大,高斯过程会很难拟合出有意义的代理模型;范围太小,真正的最优值可能被排除在外。
vars = [ optimizableVariable('NumTrees', [50, 500], 'Type', 'integer') optimizableVariable('MinLeafSize', [1, 50], 'Type', 'integer') optimizableVariable('MaxNumSplits', [5, 200], 'Type', 'integer') ]; objFun = @(params) rfObjFun(params, Xtrain, Ytrain); results = bayesopt(objFun, vars, ... 'MaxObjectiveEvaluations', 40, ... 'AcquisitionFunctionName', 'expected-improvement-plus', ... 'IsObjectiveDeterministic', false, ... 'UseParallel', true, ... 'Verbose', 1);这里有几处细节值得说明。MaxObjectiveEvaluations设为40,对于三维参数空间来说已经足够,如果数据量很大、每次训练很耗时,可以先设成15跑通流程,再增加到40。AcquisitionFunctionName用的是expected-improvement-plus,它对带噪声的目标函数更稳,不容易陷入局部最优。UseParallel设为true时,可以让多个超参数组合在不同worker上同时训练,但前提是Matlab并行池已经开启,可以用parpool命令手动启动。
3.3 优化结果分析
贝叶斯优化跑完后,bayesopt会在命令行输出一张收敛图,显示每一轮评估的最小目标值。正常情况下,目标值会先快速下降,然后逐渐趋于平稳。如果最后几十轮还在明显波动,说明目标函数噪声太大,或者搜索范围不合适。
取出最优超参数,然后重新训练最终的随机森林模型。注意这次是在全部训练数据上训练,不再做交叉验证。
bestParams = results.XAtMinObjective; disp(bestParams); tplFinal = templateTree(... 'MinLeafSize', bestParams.MinLeafSize, ... 'MaxNumSplits', bestParams.MaxNumSplits, ... 'Reproducible', true); finalModel = fitcensemble(Xtrain, Ytrain, ... 'Method', 'Bag', ... 'NumLearningCycles', bestParams.NumTrees, ... 'Learners', tplFinal); pred = predict(finalModel, Xtest); acc = mean(pred == Ytest); disp(['测试集准确率: ', num2str(acc)]); confMat = confusionmat(Ytest, pred); disp(confMat);故障诊断场景下,我一般不会只看准确率,至少要看混淆矩阵。比如一个二分类问题中,正常样本有900个,故障样本只有100个,就算把所有样本都判成正常,准确率也有90%,但这个模型完全没有诊断能力。所以我会额外计算每一类的召回率,重点看少数类是不是被大量错判。
特征重要性也是随机森林的一大卖点,可以在训练完之后直接拿到:
imp = predictorImportance(finalModel); [~, idx] = sort(imp, 'descend'); disp(idx(1:10));这样就能看出是哪几个传感器特征对故障决策贡献最大。如果发现前几位特征和我们做机理分析时判断的特征不一致,就需要回头检查特征提取流程或数据质量。
4. 实操中的避坑与问题排查
4.1 常见错误与解决
第一个坑是数据泄漏。除了归一化要在划分之后做,特征选择也要小心。如果先用全样本做了卡方检验或互信息筛选,再划分训练集和测试集,筛选时已经看过了测试集的分布,后面评估的准确率就会虚高。一种安全的做法是把特征筛选也放到每一折交叉验证里面去做,或者用随机森林自身的特征重要性来辅助判断,但不要在优化流程外面用全样本筛选后再送入模型。
第二个坑是类别不平衡导致优化方向跑偏。我刚开始做某套故障诊断流程时,正常样本占85%,故障样本占15%,直接优化交叉验证错误率,最后贝叶斯优化找到的参数偏向于把大多数样本判成正常。后来我把目标函数改成了宏平均F1的负值,少数类的召回率才明显提升。在目标函数里使用F1需要先获得每一折的预测标签,再按类别计算精确率和召回率。代码可以写成循环交叉验证的每一折然后汇总,也可以使用kfoldPredict先生成所有样本的预测标签,再和真实标签计算F1,后者写起来更简洁。
第三个坑是搜索范围设置不当。有一回我把MaxNumSplits的上限设到了500,但训练样本一共才200个,这个上限完全没有约束力,高斯过程反而觉得这个参数不重要,搜索效率下降。后来我根据样本量把上限改成差不多样本数的三分之一,效果就好多了。经验法则是:最大分裂数不用超过样本数,叶子节点最少样本数不超过总样本量的10%,树的数量在100到300之间往往比较够用。
第四个坑是并行计算时随机种子不一致。UseParallel开启后,每个worker的随机数流是独立的,同一组超参数在不同worker上可能得到不太一样的损失值。如果发现每次重复运行bayesopt得到的最优参数差异很大,可以试着固定随机种子,或者减少并行度,先保证流程稳定再考虑速度。
4.2 参数选择与性能权衡速查表
我把常用的超参数范围整理成一个速查表,方便大家在开始调参前有一个基准。注意这些范围只是起点,具体数据还是要适当调整,尤其是样本量很小的时候。
| 超参数 | 建议范围 | 对模型的影响 | 补充说明 |
|---|---|---|---|
| 树的数量 | 50~500 | 越多越稳定,训练越慢 | 超过1000后收益很小 |
| 叶子节点最少样本数 | 1~50 | 越小越容易过拟合 | 样本少时适当调大 |
| 每棵树最大分裂数 | 10~样本数 | 越大越能拟合细节 | 样本量小时别设太大 |
| 每次训练交叉验证折数 | 5或10 | 折数多估值更稳但更慢 | 用分层抽样 |
| 贝叶斯优化迭代次数 | 20~50 | 越多越接近最优 | 先跑小迭代验证流程 |
这里还有一个容易忽略的点:树模型在训练前不需要做特征归一化,但我在前面还是做了归一化,原因是为了后续扩展方便。比如后面要加入SMOTE过采样,或者用距离类算法对比,就需要标准化特征。如果你确定只用一个随机森林模型,那归一化可以省去。不过我个人建议还是保留,因为一旦流程中加入了其他对比模型,统一的数据预处理会让结果更公平。
4.3 如何理解贝叶斯优化的收敛曲线
很多第一次用bayesopt的朋友,看到控制台里每轮输出的目标值上下跳动,会以为程序跑飞了。其实这是非常正常的。因为目标函数本身带有随机性,交叉验证错误率会有波动,而且贝叶斯优化前期会刻意探索一些不确定的区域,目标值自然会有起伏。
我判断收敛的指标有三个。第一,最小目标值是否在最后十轮内没有明显下降;第二,最优超参数是否集中在某个小范围内;第三,results对象里的ObjectiveMinimumTrace曲线是否已经进入平台期。如果满足这三点,基本可以放心把当前最优参数用到最终模型上。如果曲线还是一路走低,说明搜索还没收敛,可以增加评估次数,或者适当缩小搜索空间。
5. 扩展:从离线诊断到在线监测
5.1 模型部署与预测流程
优化完成的随机森林模型不只是用来跑一次测试集,最后还是要部署到监测流程里。我在实际项目中会把模型和归一化参数保存成一个文件,在线预测时一起加载。
modelData.model = finalModel; modelData.mu = mu; modelData.sigma = sigma; save('modelData.mat', 'modelData');在线预测时,新来的传感器数据要先提取同样的特征,再利用保存的mu和sigma做归一化,然后调用predict。这里常见的错误是部署端忘记了归一化,或者用新数据的均值重新归一化,这会导致训练和预测时特征分布不一致,模型性能大打折扣。
S = load('modelData.mat'); model = S.modelData.model; mu = S.modelData.mu; sigma = S.modelData.sigma; newFeature = extractFeatureFromSignal(currentSignal); newFeature = (newFeature - mu) ./ sigma; label = predict(model, newFeature);实时监测系统里还需要考虑一个延迟问题:每段信号要计算多少个特征、多长时间输出一次诊断结论。随机森林的预测速度很快,几百棵树处理一条样本也只需要毫秒级,瓶颈通常在特征提取环节。如果特征计算太慢,可以考虑把滑动窗口重叠率降低,或者用更简单的统计特征。
5.2 后续可以做的优化方向
这套贝叶斯优化随机森林流程跑通之后,还有很多可以扩展的地方。
第一个方向是增加更多超参数。比如每次分裂时的特征抽样数量,Matlab中对应NumVariablesToSample,它控制每棵树的随机性,调好后往往能进一步降低错误率。但参数维度越高,贝叶斯优化需要的评估次数也越多,不能盲目往上加。
第二个方向是做特征选择。先用一次随机森林算出特征重要性,去掉后30%的不重要特征,再重新跑贝叶斯优化。这样做的好处是降低特征维度和噪声,模型效果可能会更好,训练速度也会更快。
第三个方向是和其他模型对比。贝叶斯优化不只适用于随机森林,也适用于SVM、K近邻、神经网络。关键是把目标函数设计成通用的,输入超参数结构体,内部调用对应模型训练和交叉验证,这样就可以在同一份数据上公平对比不同算法的上限。
第四个方向是处理极度不平衡的故障样本。除了修改目标函数,还可以在训练集内部做SMOTE过采样,但千万不能对测试集做过采样。把过采样放到交叉验证的每一折内部,流程会稍微复杂一点,但结果更可信。
最后再分享一点我个人的体验。贝叶斯优化最大的价值不是玄学调参,而是把“试参数”变成有信息积累的搜索过程。但也不要指望模型能靠调参一步登天,特征工程和数据质量才是故障诊断效果的地基。我踩过不少坑之后学到的习惯是:先用一小部分数据快速跑通整套流程,确认没有数据泄漏再上全量数据;先观察交叉验证和测试集指标是否一致,再去优化参数。这套方法本身没有太多门槛,真正拉开差距的是分析数据的细致程度。