简介:差分进化算法改进随机森林的多分类MATLAB代码(DA-RF),面向希望结合启发式优化与集成学习提升模型精度的研究者和工程师。该代码将差分进化(DE)引入随机森林超参数寻优,通过初始化种群、变异、交叉与选择等步骤,自动调整树数量、特征选择策略等关键参数,并基于真实农业区域的多组数据验证多分类效果。包内共24个文件,含10个m源码、10个mat数据、4个mexw64/mexw32编译接口,rar压缩包仅65KB,结构紧凑,便于阅读、部署和二次开发。目前已有273人学习,适合具备MATLAB和机器学习基础、希望系统掌握DE优化RF流程的读者。价值方面,可一键运行对比优化前后的分类精度与特征重要性,结合常用辅助函数与变异策略模块理解DE底层机制,并为其他模型参数调优提供可复用的优化框架。
1. 差分进化算法改进随机森林的多分类代码:DA-RF 能替你省掉多少次手动调参
随机森林做多分类,大多数人的第一版代码就是RandomForestClassifier()默认参数一把梭,验证集 F1 看着还行,换到自己的数据上立刻垮掉。手动调参则是另一场持久战:树的数量、最大深度、叶子节点最小样本数,每个参数都在影响结果,调着调着就分不清是参数变好了还是随机种子带来的波动。差分进化算法改进随机森林(DA-RF)的思路,是把“超参数组合”编码成一组连续向量,用差分进化算法做全局寻优,用交叉验证的宏平均 F1 当适应度函数,十几代迭代就能逼近一组可靠参数。这篇文章把 DA-RF 多分类代码从数据准备、DE 主循环、随机森林训练到混淆矩阵验证完整走一遍,并写清容易让人翻车的几个参数坑。适合手动调参调腻了、想让模型自己找到靠谱超参数的从业者和研究生。
2. DA-RF 的原理拆解:随机森林多分类的瓶颈与差分进化算法的寻优机制
2.1 随机森林多分类卡在哪三个参数上
随机森林是决策树 Bagging 集成的产物:决策树负责学习分裂规则,Bagging 用有放回抽样扰动数据和特征空间来降低方差。多分类和回归、二分类的差别在于,类别数变多之后每一棵树内部的分裂计算量变大,叶节点的纯度判断更复杂,同时少数类样本很容易在投票环节被多数类淹没。先厘清随机森林和决策树区别:单棵决策树在多分类问题上方差大、边界敏感,随机森林正是用多棵树的投票来平滑边界,但超参数一旦离谱,边界照样会被带偏。
对多分类影响最直接的三个参数是:max_depth控制树能长多深,太浅则欠拟合、抓不到类别之间的细粒度差异;min_samples_leaf控制叶节点最少样本量,太小则每棵树都打过拟合,投票结果被噪声主导;max_features控制每棵树随机抽取的特征子集大小,通常取sqrt,在高维特征的多分类任务里这个参数的作用会更明显,比如遥感随机森林做土地覆盖分类时,几十个光谱波段里真正有区分度的往往只有少数几个,max_features取大了反而让每棵树都看到同样的强特征,树之间的多样性下降。n_estimators也会影响结果,但它更多是“越大越稳”,边际收益递减且耗时线性增长,适合放在 DE 优化的后半段再微调。
2.2 差分进化算法的变异、交叉、选择如何搜索超参数空间
差分进化算法(Differential Evolution)是一种基于种群的全局优化算法,核心思想是用种群内个体之间的差分向量来驱动搜索。它把每个候选解编码为一个 D 维实数向量,在 DA-RF 场景里 D=4,对应n_estimators、max_depth、min_samples_split、min_samples_leaf四个超参数。迭代过程分三步:变异、交叉、选择。
变异是 DE 区别于粒子群和遗传算法的地方。对种群中的每个个体 x_i,从种群中随机抽取三个互不相同的个体 x_r1、x_r2、x_r3,用差分向量构造变异个体:
v_i = x_r1 + F * (x_r2 - x_r3)
F 是缩放因子,控制差分步长。交叉阶段把变异个体和当前个体按维度混合,常用二项交叉:u_ij 在 rand_j < CR 或 j 等于随机维度 j_rand 时取 v_ij,否则取 x_ij。CR 是交叉概率,决定试验个体继承变异向量的程度。选择阶段做贪婪选择:计算试验个体 u_i 的目标函数值,如果比当前个体 x_i 更优就替换,否则保留。整个过程不需要求梯度,所以目标函数可以是黑匣子,随机森林这种不可导的模型天然适合被 DE 优化。
适应度函数的选择直接决定搜索方向。DA-RF 把 5 折交叉验证的宏平均 F1 作为适应度,每评估一个个体相当于完整训练 5 个随机森林,计算量不小,所以种群规模和迭代代数不能盲目加大。种群规模 NP 的经验取值一般是问题维度 D 的 5 到 10 倍,D=4 时 NP 取 12 到 40 都合理;NP 太小,变异可选的差分向量就少,搜索空间覆盖不足;NP 太大,每轮评估次数暴涨,交叉验证成本随种群线性上升。迭代代数建议先跑 20 代记录收敛曲线,如果最优适应度还在明显上升就继续加代数。
2.3 “改进”的两种落地路径:超参数优化与特征选择
“改进”不是把随机森林换成别的算法,而是在不动 RF 结构的前提下,用 DE 帮它找到一组更优的超参数,或者筛出更有效的特征子集,让同一个数据集上的结果比默认参数或手动调参更好。实际落地最常见的有两条路径。
路径一是超参数优化,也是本篇采用的形态。把随机森林的四到六个超参数编码成 DE 个体,在连续空间搜索,评估时取整后传入 RF,用交叉验证指标当适应度。优势是改动最小,对已有代码侵入为零,只要把模型构建函数抽出来替换即可。路径二是特征选择,把每个特征编码成 [0,1] 区间的连续值,DE 迭代结束后用阈值 0.5 截断成 0/1 掩码,或者按前 K 大特征取值筛选特征子集。这种方式适合特征维度高、冗余明显的多分类数据,比如高维文本向量和遥感影像光谱特征。
那为什么不直接用网格搜索或贝叶斯优化?网格搜索的组合数随参数个数指数增长,假设每个参数取 5 个候选值,4 个参数就是 625 次评估;DE 一般用 12 到 20 个种群个体、迭代 15 到 30 代,总评估次数也在 300 次以内,但搜索方向是朝更优区域收敛的,不是均匀撒网。随机搜索虽然逃过了维度灾难,却没有利用已评估样本的先验信息,纯靠运气找最优区间的效率偏低。贝叶斯优化在高维离散超参数空间上建代理模型成本不低,而且对每一轮评估的反馈很敏感;DE 的优势在于实现简单、全局搜索能力强、没有额外依赖,十几行纯 Python 就能写完主循环,对大多数中小规模多分类任务来说性价比已经足够,这也是 DA-RF 这类方法在工程里更容易被直接采纳的原因。
3. 多分类评估口径与数据准备:混淆矩阵、宏平均 F1 与类别不平衡
3.1 多分类混淆矩阵怎么读:对角线、误差分布与 python 实现
多分类的混淆矩阵是一个 N 乘 N 的矩阵,N 是类别数。第 i 行第 j 列表示真实类别 i 被预测为类别 j 的样本数。对角线上的数值代表正确分类的样本数,非对角线元素代表错误混叠的方向:哪一行非对角元素多,说明这一类样本容易被漏掉;哪一列非对角元素多,说明其他类的样本容易被误判进来。
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt cm = confusion_matrix(y_test, y_pred) disp = ConfusionMatrixDisplay(confusion_matrix=cm) disp.plot(cmap="Blues") plt.title("Confusion Matrix of DA-RF") plt.show()这段代码放在第 4 章模型训练完成之后执行,把测试集真实标签y_test和模型预测标签y_pred传入confusion_matrix生成矩阵,再用ConfusionMatrixDisplay做可视化。判断多分类效果时,重点看非对角线上的热点:如果第 j 列出现大值,说明有大量其他类别的样本被错误归入类别 j,对应 precision 低;如果第 i 行出现大值,说明类别 i 的样本大量外流到其他类别,对应 recall 低。颜色越深,问题越集中。
3.2 宏平均、微平均与加权平均:目标函数选哪个
多分类报告的指标和二分类的 F1 不同,常用三种平均口径。宏平均(macro)先对每个类别分别计算 precision 和 recall,再取算术平均,每个类别权重相同,不关心样本量。微平均(micro)把所有类别的 TP、FP、FN 汇总后计算总体的 precision 和 recall,相当于按样本量加权,结果受多数类主导。加权平均(weighted)对每个类别的 F1 按样本占比加权求和,反映实际预测分布下的综合表现。
| 口径 | 计算方式 | 对少数类的影响 | 适用场景 |
|---|---|---|---|
| 宏平均 macro | 每类单独算 F1 后取算术平均 | 少数类与多数类等权,少数类差会明显拉低分数 | 各类别重要性相同,关注少数类 |
| 微平均 micro | 汇总所有类别 TP、FP、FN 后算 F1 | 受多数类主导,少数类问题被稀释 | 各类别样本量均衡 |
| 加权平均 weighted | 每类 F1 按样本占比加权求和 | 接近实际预测分布的表现 | 业务更关心总体准确情况 |
在 DA-RF 里目标函数选哪个,直接决定搜索方向。如果业务更关注少数类的识别率,优先用f1_macro作为交叉验证的 scoring 参数;如果数据集各类别样本量均衡,accuracy和f1_macro基本等价;如果类别严重不平衡,千万不要只用 accuracy,否则 DE 会发现“把训练集里最多的类别全预测对”就能拿高分,少数类的 F1 直接归零,整个优化白跑。
3.3 类别不平衡对多分类的影响与处理策略
实际项目里经常出现“整体准确率 0.97,但某个小类别的召回率是 0”的情况,这在遥感土地覆盖分类、设备故障多分类这类数据里特别典型:多数类占八成以上,少数类只有几十个样本。随机森林默认的投票机制会让多数类占据优势,少数类即便被树学到了,在投票环节也会被压下去。
处理策略一般有三种。第一种是样本层面做重采样,用 SMOTE 或 ADASYN 合成少数类样本,但多分类里要小心合成样本跨越类别边界的问题。第二种是算法层面设置class_weight='balanced',让随机森林在节点分裂时自动对少数类的错误分类施加更高惩罚。第三种是在 DE 的目标函数层面处理,把少数类的召回率或加权 F1 单独纳入适应度,让 DE 朝少数类也拉得住的方向搜索。三种策略可以叠加,但建议每次只动一个变量,方便定位是哪部分起了作用。
数据准备阶段还要注意标签编码。如果原始标签是字符串,比如“正常”“异常A”“异常B”,需要先转成从 0 到 N-1 的整数索引,一般用sklearn.preprocessing.LabelEncoder或 pandas 的factorize,漏掉这一步会在 fit 时报出could not convert string to float的错误。另一个容易被忽略的点是数据划分顺序:务必在 DE 启动前就把train_test_split跑完,DE 只接触训练集,测试集留到最后才暴露给模型。否则测试集一旦参与了任何一次交叉验证,最终报告的指标都会虚高。
4. DA-RF 多分类代码实现:从 DE 种群初始化到最优随机森林训练全流程
4.1 环境与数据集:用 sklearn 内置多分类数据跑通最小闭环
环境要求 Python 3.8 以上,需要 numpy、scikit-learn 和 matplotlib。数据集用 sklearn 自带的 digits 手写数字识别,8 分类任务,1797 个样本,每类样本比较均衡,非常适合演示 DA-RF 的多分类效果。也可以替换成 load_wine,3 类、样本更少、跑得更快;或者换成你自己的 CSV 数据,只要特征矩阵和标签准备好即可。下面这段代码完成导入和数据切分。
import numpy as np from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt data = load_digits() X, y = data.data, data.target X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) print(X_train.shape, X_test.shape) print("类别分布:", np.bincount(y_train))stratify=y保证训练集和测试集的类别比例与原始数据一致,这是多分类的基本要求。后面替换成不平衡数据集时更要留意,不用 stratify 极容易造成某一类在测试集里样本太少,最终报告没法看。这里固定random_state=42,是为了让后续对比实验具备可复现性。
4.2 DE 目标函数封装:K 折交叉验证 + 宏平均 F1
DE 的核心是适应度函数,它必须把一组超参数映射成一个可比较的实数。这里封装一个函数,接收一个 numpy 数组(DE 个体),返回交叉验证的宏平均 F1。注意 DE 搜索的是连续空间,但随机森林的n_estimators、max_depth等参数必须传整数,所以要在函数内部做取整。
def evaluate_fitness(params): n_estimators, max_depth, min_samples_split, min_samples_leaf = params model = RandomForestClassifier( n_estimators=int(round(n_estimators)), max_depth=int(round(max_depth)), min_samples_split=int(round(min_samples_split)), min_samples_leaf=int(round(min_samples_leaf)), max_features="sqrt", n_jobs=-1, random_state=42 ) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(model, X_train, y_train, cv=cv, scoring="f1_macro") return scores.mean()参数边界我一般控制在:n_estimators取 50 到 500,max_depth取 3 到 30,min_samples_split取 2 到 20,min_samples_leaf取 1 到 10。这几个范围覆盖了多数中小型多分类任务的合理区间。为了保证跨代评估结果可比,必须给每个 RF 固定random_state,否则同一组参数两次评估的 F1 会因为森林的随机性不同而摆动,适应度函数就变成了噪声函数,DE 很难收敛。
4.3 差分进化主循环:变异、交叉、选择与参数边界截断
接下来是 DE 主循环,选用最经典的 DE/rand/1/bin 策略,实现简单、泛化性好。
def de_optimize(pop_size=12, max_iter=30, F=0.7, CR=0.8, lb=[50, 3, 2, 1], ub=[500, 30, 20, 10], dim=4): lb = np.array(lb, dtype=float) ub = np.array(ub, dtype=float) pop = np.random.rand(pop_size, dim) * (ub - lb) + lb fitness = np.array([evaluate_fitness(p) for p in pop]) best_idx = np.argmax(fitness) best_x = pop[best_idx].copy() best_f = fitness[best_idx] for gen in range(max_iter): for i in range(pop_size): candidates = [idx for idx in range(pop_size) if idx != i] r1, r2, r3 = np.random.choice(candidates, size=3, replace=False) mutant = pop[r1] + F * (pop[r2] - pop[r3]) mutant = np.clip(mutant, lb, ub) trial = pop[i].copy() j_rand = np.random.randint(dim) for j in range(dim): if np.random.rand() < CR or j == j_rand: trial[j] = mutant[j] trial_f = evaluate_fitness(trial) if trial_f >= fitness[i]: pop[i] = trial fitness[i] = trial_f if trial_f > best_f: best_x = trial.copy() best_f = trial_f print(f"gen {gen+1:02d} best_f1_macro = {best_f:.4f}") return best_x, best_f逻辑说明:第一步初始化种群,pop_size 个个体在边界内均匀随机生成,每个个体是一个 4 维向量,对应 4 个超参数。第二步进入主循环,对每个个体做变异:随机抽 3 个互不相同的个体 r1、r2、r3,用差分向量构造 mutant。第三步做交叉:trial 先复制当前个体,再在每一维上以 CR 概率取 mutant 的值,j_rand强制至少翻转一维,保证 trial 与当前个体不同。第四步做选择:如果 trial 的适应度不低于当前个体就替换,否则保留。选择时用了>=,让种群中不差也不优的个体有机会被更早淘汰,实际效果比严格>略稳。
F=0.7、CR=0.8是搜索初期的常用取值:F 偏大一点避免早熟,CR 偏大让变异信息更充分进入候选解。训练前期可以跑一组 F 在 0.5 到 0.9、CR 在 0.3 到 0.9 的对比,用第 6 章的收敛曲线判断哪组更稳。np.clip对越界变异体的处理方式有一个隐藏坑:它会直接越界值压回边界,迭代几代后边界上容易堆积大量个体,第五章节会专门展开。
4.4 用最优超参数训练 RF 并输出多分类评估报告
DE 收敛之后,用最优参数在整个训练集上训练最终的随机森林,再对测试集做预测。
best_params, best_f = de_optimize() print("最优参数:", best_params) n_estimators, max_depth, min_samples_split, min_samples_leaf = best_params rf_best = RandomForestClassifier( n_estimators=int(round(n_estimators)), max_depth=int(round(max_depth)), min_samples_split=int(round(min_samples_split)), min_samples_leaf=int(round(min_samples_leaf)), max_features="sqrt", n_jobs=-1, random_state=42 ) rf_best.fit(X_train, y_train) y_pred = rf_best.predict(X_test) print(classification_report(y_test, y_pred))注意最终模型用整个X_train训练,而 DE 内部评估时用的是 5 折交叉验证的子集,最终模型看到的数据更充分,所以测试集指标通常略高于交叉验证分数,这不算偏差。输出classification_report后,把y_test和y_pred传给第 3 章的混淆矩阵代码,可以直观看出 DA-RF 最容易在哪些类别之间混叠。我在 digits 上实际跑的时候,类别 8 和 9 之间的混淆通常是最后才消掉的;如果测试集上某个类别 recall 明显低于其他类,先别急着调参,把该类别对应的测试样本可视化看看是不是数据本身标注就有歧义。
如果你用的是 MATLAB,随机森林部分可以用 treeBagger 实现,但 DE 部分我建议还是用 Python,或者用 Optimization Toolbox 里的 ga 做替代。MATLAB 里我实际用下来,自定义差分进化逻辑不如 Python 灵活,调试一轮变异、交叉的中间状态也麻烦得多,所以这套流程的完整落地还是推荐 Python 版本。
5. DA-RF 的排错与避坑:收敛失败、指标失真与种群退化的典型坑
5.1 测试集指标比验证集高一大截:目标函数泄漏了
现象:DE 日志里报告 best_f1_macro 接近 0.95,测试集一跑只有 0.86,两者差距远大于正常波动。
原因:最常见的是缩放或特征选择在切分之前做了。比如先用StandardScaler对全部数据做了 fit,再切训练测试集,scaler 已经看到了测试集的统计量,DE 的交叉验证在每一折里也间接接触了测试集信息,属于数据泄漏。另一种情况是 DE 内部做交叉验证时没带 shuffle,数据按原始顺序排列,某一折的类别分布与整体严重不一致,分数被异常拉低。
解决:先train_test_split,再在X_train上 fit scaler 并 transform,X_test用同一个 scaler 只做 transform。特征选择也必须放进 DE 的目标函数内部,或者用 sklearn 的 Pipeline 包起来。如果确定没有做标准化,就检查StratifiedKFold是否加了shuffle=True, random_state=42。
5.2 DE 收敛到同一点不再变化:种群多样性丢失
现象:打印日志发现 best_f 从第 5 代开始不再变化,best_x 的各维度也完全静止,连续十几代没有任何更新。
原因:F 取值太小或者种群规模太小。F 太小时,种群个体间的差分向量趋近于零,变异步长几乎消失,算法失去探索能力;NP 太小时,初始种群本身多样性就不够,所有个体很快聚到同一个局部最优附近。
解决:先做一轮快速实验,把 F 改成 0.9,后面逐渐衰减到 0.4;如果还是早熟,把 NP 从 12 提到 20 或 24,代数提到 50 代看收敛曲线。计算资源允许的话,改用 jDE 自适应策略,每一代按独立概率调整每个个体的 F 和 CR,这是目前防早熟最可靠的办法。
5.3 参数越界截断导致种群退化:clip 的隐藏代价
现象:迭代到后期,种群中的个体大量整齐排列在边界上,比如 max_depth 全部堆在 30,或 min_samples_leaf 全部堆在 1。
原因:变异向量经常越过边界,np.clip把所有越界个体直接压到边界上,边界上的个体被反复选中参与变异,种群多样性被“吸”到角落。
解决:把 clip 改成越界反射策略,越界后按模长折回边界内:
mutant = pop[r1] + F * (pop[r2] - pop[r3]) for j in range(dim): if mutant[j] < lb[j] or mutant[j] > ub[j]: range_len = ub[j] - lb[j] mutant[j] = lb[j] + (mutant[j] - lb[j]) % range_len这个细节改动只影响越界个体,对合法个体没有副作用,能让种群在边界附近的分布更自然。如果数据集不大、每一轮评估成本可控,也可以让越界个体重新在界内均匀随机采样。
5.4 随机种子不一致:对比实验不可复现
现象:同一份 DA-RF 代码跑两次,最优参数完全不同,收敛曲线的形状也对不上,baseline 和 DA-RF 的差异忽正忽负。
原因:DE 内部的np.random.rand和np.random.choice每次产生不同序列,没有显式设置np.random.seed的话整个实验不可复现。对比实验里更常见的问题是:基线随机森林没固定random_state,DA-RF 固定了,两组数据的差值来自随机性而非算法改进。
解决:在脚本开头写np.random.seed(42),同时给每个 RandomForestClassifier 固定random_state=42。两个随机源必须同时控制:只设 numpy 种子,RF 内部的 bagging 抽样仍然是随机的;只固定 RF,DE 的变异交叉过程又不可复现。
5.5 交叉验证某折缺类报错:类别数与 K 值不匹配
现象:StratifiedKFold报错,提示某个类别样本数不足,或者在某折计算f1_macro时出现 NaN 警告。
原因:少数类样本数少于 K 折数,分层抽样无法保证每一折都包含该类样本。比如某个类别只有 5 个样本,5 折交叉验证里总有一折分不到,该折的宏平均 F1 就定义不了。
解决:先打印np.bincount(y),看每个类别的数量。最少的类别样本量超过 20 时,5 折是安全的;如果只有个位数,对这个类做重采样之后再进入 DE,或者把 K 从 5 降到 3。降 K 的代价是验证分数方差变大,作为临时办法可以接受。
6. 让 DA-RF 更稳:收敛曲线、早停策略与特征重要性验证
6.1 用收敛曲线判断代数够不够
在 DE 主循环里把每一代的 best_f 存进列表,结束后用 matplotlib 画出来。如果曲线末尾还在明显上升,说明代数不够,继续加;如果 10 代之后就平了,说明种群早熟,需要调 F 或 NP。我一般顺手加一个早停逻辑:连续 8 代 best_f 提升小于 0.0001 就跳出循环,能省下不少交叉验证时间。
best_history = [] no_improve = 0 for gen in range(max_iter): # ... 省略 DE 主循环体 ... best_history.append(best_f) if gen > 0 and abs(best_f - best_history[-2]) < 1e-4: no_improve += 1 if no_improve >= 8: break else: no_improve = 0 plt.plot(best_history) plt.xlabel("generation") plt.ylabel("best f1_macro") plt.title("DA-RF Convergence Curve") plt.show()早停阈值不要设得太激进,否则容易在局部平坦区提前退出。0.0001 这个量级是我在 digits 上实测比较稳的取值,换成你自己的数据后先不开启早停跑一次完整日志,看每代提升的典型幅度再定。
6.2 用特征重要性验证 DE 优化是否真的改对了方向
rf_best.feature_importances_可以给出每个特征在森林中的分裂贡献。我习惯把重要性排序后取前 20 到 30 个特征,用同样流程重训一个 DA-RF,对比测试集 F1:如果指标基本不降,说明原数据冗余较多,后续可以转成特征选择版的 DA-RF;如果指标明显下降,说明当前模型的分类能力确实依赖更多特征,特征选择要谨慎。这颗验证的模型也能用来定位线上特征质量问题的源头,重要性最高的特征如果来自业务上已知不太可靠的来源,就需要回头检查上游数据管道。
我第一次完整跑通 DA-RF 时就栽在 5.1 那个坑里:在切分前先对全量数据做了归一化,DE 日志一路高歌,测试集一测直接崩掉,后面花了一整天才定位到是泄漏。现在我的习惯是任何预处理都写成函数,先切分再 fit,DE 里所有随机源全部固定,每跑一组实验先在日志里确认 best_f 的收敛曲线再谈结果。这套流程本身不复杂,但把每一步的边界和随机性控制住,DA-RF 才能真正成为可以复现、可以谈置信度的方案。希望帮到你。
本文还有配套的精品资源,点击获取