随机森林模式识别:双随机性与投票机制如何提升贷款审批准确率
发布时间:2026/10/17 15:45:10 锦皓数字建站

简介一份面向机器学习学习者与MATLAB开发者的随机森林模式识别系统完整设计文档核心场景为银行贷款审批中的分类预测。文档从算法原理讲起逐步展开系统流程图、软硬件环境、数据集描述、特征提取、分类流程和结果评估同时给出基于MATLAB R2019a的完整实现代码包含随机森林构建、ID3决策树生成、特征随机抽样与结果投票统计等核心模块并附有训练集1500样本、测试集500样本的详细特征说明便于读者对照理解。压缩包共包含1个doc文件大小154KB内容组织层次清晰既可作为课程设计、毕业设计的参考报告也可作为随机森林与决策树算法的入门实战资料。目前已有162人浏览学习。通过学习这份文档读者可以掌握随机森林如何通过集成多棵决策树提升分类准确率理解特征重要性排序与袋外误差率在特征筛选中的作用并能借助附带的MATLAB代码快速复现实验、进一步优化模型。1. 随机森林模式识别为什么 30 棵决策树的投票能把贷款审批准确率打到 91.6%随机森林常被当成一个“黑匣子”你把数据丢进去它吐出一个预测中间发生了什么全靠猜。可真正拆开这个黑匣子你会发现它既没有玄学也没有高深的数学核心就是两个随机性加一个投票规则。这份贷款审批系统的源码把这条链路完整跑了一遍1500 个训练样本、7 个特征、30 棵 ID3 决策树在 500 个测试样本上得到 458 个正确准确率 91.6%。值得搞懂的不是这个数字本身而是为什么每棵树只随机看 5 个特征还能work、为什么有放回抽样反而比不放回更稳、以及投票为什么能纠错。这篇文章就从这套模式识别系统源码出发把随机森林从“用起来”拆到“设计出来”。2. 双随机性Bootstrap 抽样与特征子集是怎么撑起泛化能力的随机森林最容易被忽略的一点是它不是一棵树的预测而是一群“各自带偏”的树的集体决策。要让集体决策有效前提是每个成员不能全站一边。随机森林用两种手段制造这种“不同”——对样本做有放回抽样对特征做随机子集选择。这一章逐个拆开看。2.1 有放回抽样为什么每棵树必须“看得不一样”先看主程序里构建森林的这段代码注意随机性体现在哪两个地方sn 900; % 每棵树有放回抽取 900 个样本 tn 30; % 森林中决策树的棵数 S xlsread(loan_train.xls); for j 1:tn % 随机选取 5 个特征原始特征 6 个标签列除外 Feature randperm(6, 5); % 从 1 至 1500 内随机抽取 sn 个样本有放回 Sample_num randi([1, 1500], 1, sn); SData S(Sample_num, :); % 一棵树的训练集 [node, child_value, child_node_num] ID3(SData, Feature); rnode{j,1} node; rchild_value{j,1} child_value; rchild_node_num{j,1} child_node_num; endrandi([1, 1500], 1, sn)是关键。它每次在 1 到 1500 之间独立抽取 900 个索引抽取过的索引还可能再被抽到这就是“有放回抽样”也叫 Bootstrap 抽样。直观上你可能会问有放回会让同一份数据反复出现难道不是浪费恰恰相反这正是 BaggingBootstrap Aggregating的核心让每棵树看到的数据分布略有不同从而天然产生“意见分歧”。有个经典结论值得记住在 N 个样本中有放回地抽 N 次期望上大约有 63.2% 的样本至少被抽中一次剩下约 36.8% 的样本从未被抽中。这个比例来自1 - (1 - 1/N)^N ≈ 1 - e^{-1}。也就是说每棵树其实只“真正见过”原始数据集的六成多其余样本对它来说永远是陌生的。等 30 棵树都建成每个样本都有机会在某些树里被重复强化、在某些树里完全缺席这种不平衡反而让集成结果更稳健。2.2 随机特征子集m M 的去相关设计再看Feature randperm(6, 5)。原始的 7 列数据中最后一列是标签——贷款决策1 同意、2 还需考虑、3 不同意所以真正的输入特征只有 6 个。randperm(6, 5)表示从 1 到 6 中随机挑 5 个不重复的数作为这棵树的可用特征集合。这里要特别澄清一个源码与理论描述的差异。论文式的随机森林描述是在每个节点分裂时从全部 M 个特征里重新随机抽 m 个再从中选最优分裂特征。但这套代码的实际做法是每棵树在训练前随机定一个 5 特征集合之后整棵树的分裂都在这 5 个特征内进行节点分裂时只是从剩余属性里选信息增益最大的那个。换句话说代码实现的是“每棵树一个特征子集”而不是“每个节点一次重抽样”。从效果上看它依然达成了特征去相关的主要目的——树与树之间拿到的“可用特征池”不同最终形成的分割边界就不会高度雷同。如果你在复刻经典随机森林建议每个节点都用randperm(6, 5)重新抽样那才是 Breiman 原始描述的做法但这里的形式足够支撑这个数据规模下的模式识别任务。为什么必须“随机选一部分特征”而不是全部都用想象一下如果 6 个特征全部参与分裂那么每棵树都会把信息增益最大的那一个特征放在根节点结果所有树的开局都一样后续再怎么抽样本森林里也是一群“体型相似”的树——投票的纠错能力就被削弱了。随机特征子集等于强行给每棵树戴上不同的“眼镜”让它们从不同角度观察数据保证集成后依然有足够多样性。2.3 投票机制为什么少数服从多数能纠错到这里森林已经有了30 棵结构各异的决策树。最后一步是让它们对测试样本各自表态再按少数服从多数出最终结果。源码里的统计函数如下function [type] statistics(tn, rnode, rchild_value, rchild_node_num, PValue) TypeName {1, 2, 3}; TypeNum [0 0 0]; for i 1:tn % 对测试向量进行投票共有 tn 棵树 [type] vote(rnode, rchild_value, rchild_node_num, PValue, i); if strcmp(type, TypeName{1}) TypeNum(1) TypeNum(1) 1; elseif strcmp(type, TypeName{2}) TypeNum(2) TypeNum(2) 1; else TypeNum(3) TypeNum(3) 1; end end maxn find(TypeNum max(TypeNum)); type str2num(TypeName{maxn(1)}); % 取票数最多的类 end投票为什么能纠错可以用一个简单的概率直觉理解。假设每棵树的分类错误率都是 pp 0.5且树之间独立那么 30 棵树多数投票出错等价于超过 15 棵树同时出错。根据二项分布这个概率会随树的数量指数级下降。举例单棵树准确率 85%30 棵独立树多数投票的理论准确率能逼近 99% 以上。当然现实里树之间不可能完全独立特征随机性和样本抽样已经努力把相关性压低了这是随机森林在真实数据集上“准确率高于单棵决策树”的根本原因它不是某种玄学加成而是概率论在起作用。3. 特征提取与特征重要性袋外误差率是怎么决定留哪些列的模式识别系统里特征选择往往比分类器本身更影响上限。贷款数据集只有 6 个输入特征看似不需要筛选但这套系统仍然设计了一套完整的特征重要性评估流程不直接拿全部特征训练而是先算重要性、剔除低价值特征、再看袋外误差率决定保留哪些。这个思路值得单独拆开因为它直接关系到你对“特征提取”这个环节的理解——很多初学者把特征提取等同于 PCA 或降维其实在树模型中最常见的做法是按特征对预测的贡献大小倒序剔除。3.1 特征重要性的两种主流度量这套系统使用的数据有 7 列年龄、收入、房、信用、性别、居住地以及最后的贷款决策。注意第一列年龄是 1/2/3 离散编码收入是 1/2/0 三类信用是 1/2/3——所有输入都是标签式离散值这决定了特征重要性可以用“基于不纯度减少”的方式计算也就是统计某个特征在所有树中被选中为分裂属性的次数以及它带来的信息增益总和。一个特征越常出现在靠近根节点的位置、每次分裂降低的熵越多它对分类的贡献越大。另一种常见度量是 OOB 置换重要性把某个特征的值随机打乱然后看袋外样本预测准确率下降多少下降越多说明该特征越重要。这套源码没有显式实现 OOB 置换逻辑但它描述的“按重要性降序排序逐轮剔除比较各特征集对应的袋外误差率”本质上就是拿 OOB 误差作为特征子集的评估标准。典型的实现思路会走这样一个循环先训练完整森林算出 OOB 误差再剔除最不重要的 10% 特征用剩余特征重新训练重复直到特征数降到预设的 m最后挑 OOB 误差最低的那组特征集。3.2 特征剔除的迭代流程为什么能选出更稳的特征集特征重要性排序本身是静态的但特征之间往往存在交互单独看某个特征不重要它和另一个特征组合起来却能显著提升区分度。一次性把“最不重要的几个”全部删掉可能误伤这种组合。所以更稳妥的做法是逐步剔除每轮只删固定比例的特征重新训练后再算重要性让剩余特征的贡献被重新评估。这套系统的流程描述正是如此——“确定要剔除的比例依据特征重要性剔除相应比例的特征得到一个新的特征集”然后用新特征集重复直到剩下 m 个特征再选袋外误差率最低的特征集。这里有一个容易被忽略的设计动机为什么最终选的是“袋外误差率最低的特征集”而不是“分类准确率最高的特征集”因为袋外数据来自训练过程中从未被抽中的样本对每棵树来说是完全陌生的用它评估等价于做了一次免费的交叉验证。分类准确率只反映在已有测试集上的表现而袋外误差率更接近模型在新数据上的真实水平。对贷款审批这种决策场景宁可牺牲一点训练集上的分数也要换泛化能力的确定性。3.3 代码里的特征操作从 6 个特征到 5 个特征的选择回到源码特征选择的实际操作体现在两处。第一处是主程序里为每棵树随机指定特征池[Feature, ~] sort(Feature); % 排序保证递归过程中特征顺序一致 ClassPNum Feature; % 当前可用的特征集合 CLASSPNUM [1 2 3 4 5 6]; [CHA, ~] setdiff(CLASSPNUM, ClassPNum); DValue(:, CHA) 0; % 把没用到的特征列置 0这行DValue(:, CHA) 0很有意思。它没有真正把特征列从矩阵中删掉而是把本轮不参与分裂的特征全部置零。置零的目的是避免递归计算信息增益时误用到这些列——因为置零后的列不会产生有效分裂信息。严格来说置零后这些列的熵特性已经改变计算增益时对它们没有帮助但它保证了下游节点不会再分裂到已被排除的特征。这是一种实现上的取巧不动态裁剪矩阵只把“禁用的列”数值清空省去了大量索引重排。第二处是 ID3 递归内部的ClassPNum(max) []。当某个特征被选作当前节点的分裂属性后它会从可用的特征集合里被删除防止同一棵树上一条从根到叶的路径重复使用同一特征。这符合 ID3 的基本约束一棵树的分类路径上同一个属性最多出现一次。这种“路径内不重复”的设计加上“森林内特征池随机化”构成了整套系统特征层面的两层去重。4. ID3 决策树构建信息增益从公式到 cell 数组存储的实现随机森林的每棵树都由 ID3 算法生成。ID3 的核心逻辑是在每个节点计算各特征的信息增益选增益最大的那个特征做分裂递归下去直到样本类别纯净、特征用尽或样本为空。这一整段从熵的计算开始到树的存储结构收尾是整套源码最吃透的部分。4.1 信息熵与信息增益从 Exp 函数到 avg_entropy先看信息熵的计算。贷款决策有三种结果1 同意、2 还需考虑、3 不同意。在某个节点上假设样本集中这三类分别有 n1、n2、n3 个总数为 n那么该节点的经验熵就是function I Exp(CN, A) CNRowNum CN_sta(CN, A); % 统计 A 对应行中三类标签的数量 n length(A); I 0; for i 1:3 if CNRowNum(i) 0 P(i) CNRowNum(i) / n; I I - P(i) * log2(P(i)); % 信息熵公式 end end endP(i)是第 i 类样本占比-P(i) * log2(P(i))累加就是香农熵。熵值越大说明当前集合里类别越混乱如果某一类占比是 100%熵为 0意味着这个节点已经“纯”了不需要再分裂。这个计算在 ID3 里被调用了多次先是算分裂前的总熵IExp(CN,A)再算每个特征划分后的加权条件熵avg_entropy两者相减就是信息增益for i 1:length(ClassPNum) Entropy(i) avg_entropy(DValue(:, ClassPNum(i)), A, CN); Gain(i) I - Entropy(i); % 信息增益 分裂前熵 - 分裂后条件熵 end maxG find(Gain max(Gain)); % 取增益最大的特征作为分裂属性avg_entropy内部会调用type_sta把当前样本按该特征的取值分组然后对每一组分别计算熵再按组占比加权求和。这里有个易错点信息增益对“取值多的特征”天然偏好。如果一个特征每个样本都是唯一取值条件熵会极低增益虚高。后文会再谈这个设计取舍。识别这套代码里最核心的数学链路就是总熵 → 条件熵 → 增益 → 取最大四步循环。4.2 三个 cell 数组怎么装下一棵完整的树决策树的存储是这套源码里最值得读的部分。树没有用标准的结构体而是用三个等长的 cell 数组并列存放node、child_value、child_node_num。% 全局变量node 存节点内容child_value 存分支取值child_node_num 存子节点编号 global node child_value child_node_num; n length(node); % 当前已存在的节点总数 if m 0 % 如果当前节点有父亲把自己的编号登记到父亲的子节点列表中 k length(child_node_num{m}); child_node_num{m}(k 1) n 1; end理解这三个数组的关系是读懂整套代码的钥匙。node{i}存的是第 i 个节点的内容如果它是内部节点存的是一个特征编号1 到 6 之间的整数表示按这个特征分裂如果它是叶子节点存的是字符串1、2或3表示直接输出的类别。child_value{i}是内部节点各分支对应的特征取值例如某节点按特征 1年龄分裂年龄取值有 1、2、3那么child_value{i}就是[1 2 3]。child_node_num{i}存对应每个分支的子节点编号——注意它可能比child_value短因为某个取值在训练集中没有样本就不会生成对应分支。递归生成节点时每个新节点会先把自身编号追加到父节点的child_node_num中这就是上面代码里child_node_num{m}(k1) n1的作用。建树完成后从node{1}出发按child_node_num的索引层层下探就能完整还原决策结构。测试阶段的vote函数正是沿着这条路走。4.3 递归终止条件与多数投票兜底ID3 的递归必须有明确的停机条件否则会无限生长。这套代码在TreeNode函数里依次处理了四种情况% 1、样本为空则树为空 if isempty(DValue) node{n1} []; return; end % 2、剩余属性为空选择多数元组所在的类作为结点 if isempty(ClassPNum) node{n1} find_most(CN, A); return; end % 3、样本中所有数据都属于同一类直接作为叶子 CNRowNum CN_sta(CN, A); if length(find(CNRowNum 0)) 2 node{n1} CN(A(1)); return; end % 4、否则算增益选最大增益特征分裂前三条对应“空样本”“无可用特征”“类别纯净”三种情况第四条才是正常的分裂流程。注意第二条里有个细节如果此时已经没有剩余特征但样本类别依然混杂就用find_most取当前子集中数量最多的类别作为叶子——这就是“多数投票兜底”保证任何一条路径都能走到类别输出。另外代码里还有一个容错分支算完所有特征的信息增益后如果max(Gain) 0说明分裂并不能降低熵此时也直接返回多数类作为叶子。这个条件在真实数据上经常发生某些节点上任何特征都无法把样本分得更纯再强行分裂只会过拟合。很多初学者实现 ID3 时漏掉这个判断导致树在训练集上深不见底、测试集上崩得厉害。这里的max(Gain) 0是一个朴素但有效的剪枝信号。5. 随机森林落地避坑从测试集精度到连续值属性把源码跑通只是第一步真正让这套系统从“能跑”变成“可信”需要避开几个它自己都踩过的坑。这一章整理 4 条具体的踩坑记录每一条都来自代码实现里的真实细节或这类系统的常见反模式。5.1 测试样本被四舍五入后特征值漂了现象测试阶段有一段TData roundn(T(:, 1:end-1), -1)把测试集特征统一四舍五入保留一位小数。贷款数据本身是整数编码0/1/2/3这段操作看起来无害但如果你把自己的数据代入——比如把信用评分 0.85 这种浮点值放进来四舍五入会制造大量边界值重复。原因roundn的作用是“取整到指定小数位”。对整数编码做保留一位小数根本不会改变数据但对连续型特征它会把 0.85 和 0.86 这类原本可区分的值合并成同一个 0.9直接抹掉大量细粒度信息。这是用离散数据的预处理习惯硬套在连续数据上。解决在调用任何取整函数前先确认特征的真实类型。对本来就是标签编码的列直接用原始整数对连续型特征宁可做 z-score 标准化或分桶也不要统一roundn。我从这里学到的习惯是任何数据预处理代码都要先打印unique(T(:, col))的取值分布确认操作前后数据指纹是否变化。5.2 vote 找不到分支时原地修改样本值污染后续判断现象vote函数在测试样本的某个特征取值在树中不存在时执行了PValue(rnode{j,1}{n}) PValue(...) 0.1*k用一个交替递增的步长去“试探最近的值”。看起来是容错逻辑但它直接修改了当前节点上的样本特征值后续节点继续用这个被改过的值做判断。试过之后发现本来只有第一层缺分支修改后可能一路偏离到错误叶子。原因这是一种“按距离最近原则归入分支”的暴力实现但它没有保存原始值也没有在找不到分支时回溯。k序列 0→1→-2→3→-4 交替放大步长本质是在原值附近做一维搜索可一旦搜索方向选错改动的值会让下一层节点匹配到完全无关的分支。解决更稳妥的做法是“找最近分支值”而不是“改样本值”。比如这一个特征的所有分支值都存在于child_value中计算min(abs(PValue - child_value))把样本归入距离最近的分支全程不动原始样本。我在复刻这套逻辑时把vote改成只读模式测试准确率立刻稳定了。5.3 平票时的首个最大值偏差现象find(TypeNum max(TypeNum))在出现并列最高票时返回多个索引代码只取第一个。比如 3 类投票结果是[2 2 1]系统会固定输出类别 1即使类别 2 同样有两票。原因find返回的索引按从小到大排列maxn(1)永远偏向编号小的类别。如果训练集的类别分布本身不平衡这个“偏向”会被投票机制放大让少数类更难被识别。解决平票时随机选取一个并列类或者按训练集先验概率加权投票。更工程化的做法是收集所有并列类做置信度比较——哪一类的平均叶子置信度更高就选它。这个改动对贷款审批这种三分类问题尤其重要因为“还需考虑”和“同意”的边界本来就很模糊。5.4 连续属性硬塞给 ID3过拟合到怀疑人生现象把收入金额、信用评分这类连续特征直接丢进 ID3 训练训练集准确率接近 100%测试集立刻掉到 70% 以下树深度动辄几十层。原因ID3 天生按离散取值分裂连续特征每出现一个新数值就开一个新分支。如果 1500 个样本的信用评分各不相通根节点一分裂就出现几百个分支每个分支只剩几个样本纯属记住训练数据。解决这套源码的定位很清楚——“只适用于属性值是标签值的离散类型”这是写在原作者注释里的结论。真实场景要处理连续特征要么预先分桶成等级标签要么换成 C4.5 的连续属性二分法按阈值把区间切成两半要么直接用支持连续特征的随机森林库实现。不要试图给 ID3 打补丁去硬扛连续值那是死路。6. 设计取舍为什么用 ID3 而非 C4.5以及这套源码的边界在哪看完整套实现最值得琢磨的不是“怎么把随机森林跑起来”而是作者为什么在明明可以做更多的情况下选择了 ID3 离散特征这条路。这里的设计取舍对理解模式识别系统的工程化边界非常关键。第一个取舍是“信息增益 vs 信息增益率”。ID3 用的信息增益有天然的多值偏向一个特征取值越多条件熵越低增益越虚高。C4.5 为此引入了增益率Gain / 固有值等于给多值特征加了惩罚项。这套系统仍然用 ID3合理的解释是数据集的 6 个特征都是低基数的离散编码取值最多 3 到 4 个多值偏向问题不严重信息增益已经够用。而且随机森林本身通过特征子集随机化缓解了偏向——特征池里值域差异不大时ID3 的偏差不会被放大。如果你换到高基数特征多的数据集C4.5 的增益率几乎是必须的。第二个取舍是“离散特征限定”。贷款审批这个场景的特征——年龄分段、收入等级、是否有房、信用等级——本来就是业务上习惯离散化的变量。银行信贷审批里常用的评分卡模型就是把连续变量切成 WOE 分箱再入模。这套系统的离散限定不是缺陷而是与应用场景严格匹配的结果。反过来说如果你拿它去做图像像素分类或语音信号识别结果必然翻车因为底层数据形态不支持。第三个取舍是“手写 ID3 替代成熟库”。用成熟的随机森林实现无论 MATLAB TreeBagger 还是 Python 的 sklearn都会自带 OOB 误差曲线、特征重要性排序、剪枝参数等工具。这套源码选择手写换来的是对每个环节的完全掌控——你能看到熵怎么算、节点怎么存、投票怎么走这正是它作为学习资源的价值。代价是缺失了常规工程需要的功能没有剪枝策略、没有 OOB 自动评估、没有处理特征缺失的机制。如果你要把它改造成生产可用系统第一件事不是加数据而是补 OOB 误差曲线观察树的数量和特征子集大小。从那以后凡是我手写集成学习 Demo都会强行走一遍“先做特征重要性排序、再画 OOB 误差曲线、最后才看分类准确率”的流程而不是直接盯着一个准确率数字。这套贷款审批源码让我最受益的一点就是把“随机性产生多样性、多样性换来稳健性”这条链路完整地摊开了。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。