简介这份文档基于随机森林算法完成模式识别系统的设计与实现以银行贷款审批为应用场景适合机器学习初学者或正在完成相关课程设计、毕业设计的读者参考。内容从随机森林算法思想入手依次介绍决策树集成原理、特征重要性排序与筛选、袋外误差率评估、训练集与测试集划分并给出1500个训练样本和500个测试样本的完整实验过程。文档还包含系统流程图以及MATLAB核心代码覆盖随机抽样、特征随机选择、ID3决策树构建、多树投票统计等环节可按步骤复现并验证最终分类准确率。此外文档对数据集的七个特征字段进行了逐项说明并给出了错误分类分析的思路有助于理解特征工程与集成分类器的实际应用。资源整体为一个doc文档压缩包约154KB已有162人学习浏览便于快速查阅和对照实现。1. 基于随机森林算法的模式识别系统一个能跑的银行贷款审批分类器某信贷审批部门手上经常积着一批离散特征填好的 Excel 数据年龄段、收入档位、是否有房、信用等级、性别、城乡最后一列是银行最终到底给不给贷款。基于随机森林算法的模式识别系统解决的就是这个场景——不调第三方包用 MATLAB R2019a 从零实现随机森林和 ID3 决策树从样本有放回抽样、特征随机抽取、递归建树到多棵树投票全部手写。最终 500 条测试样本里 458 条分类正确准确率 91.6%分类结果还能写回 Excel 保存。这个项目适合两类人一类想快速把离散特征表变成可解释、可部署的分类器另一类正在做模式识别课程设计或技术改造需要看懂每一行 MATLAB 代码而不是只会调 fitcensemble 的开发者。后端逻辑清晰树的结构全部存在数组里可以随时拆开检查。2. 数据与架构设计7 列特征、30 棵树和 MFE 特征筛选取舍2.1 特征矩阵与三分类标签编码系统处理的数据本身不复杂但每一列的含义必须提前约定死。训练集和测试集都是 7 列前 6 列是特征第 7 列是要预测的标签具体编码如下列号含义取值说明1年龄段1 青年2 中年3 老年2收入水平0 低1 一般2 最高3是否有房0 没房1 有房4信用等级1 很好2 好3 一般5性别0 女1 男6居住地0 农村1 城市7贷款决策标签1 同意2 还需考虑3 不同意这个设计有个关键约束ID3 决策树的分裂规则是「属性值等于某个离散标签」所以所有输入都必须是离散值。素材里训练代码对特征做了 roundn 四舍五入保留一位小数就是为了把可能的连续读数压成稳定可匹配的离散取值。如果把工资、房价这种连续金额直接塞进第 2 列TreeNode 里的 type_sta 会按严格等于分组每个样本值都可能自成一组树深度暴涨过拟合随之而来。这点在第 5 章避坑里会专门展开。贷款标签是三分类而不是二分类也意味着 ID3 的叶子节点要区分出三类字符串不能简单用 0/1 映射。2.2 随机森林的三个随机源与参数选择随机森林在这套系统里的核心思想是让每一棵树都「与众不同」三个随机源分别对应三处代码样本随机Sample_numrandi([1,1500],1,sn)从 1500 条训练数据里有放回地抽 900 条作为一棵树的训练集同一条样本可被重复抽中。特征随机Featurerandperm(6,5)每棵树只随机看到 6 个特征里的 5 个有一个特征被刻意排除。结构随机每棵树由 ID3 独立递归生成互不影响最终用投票合并。参数上默认 sn900、tn30M6、m5满足 m M 的条件。sn 为什么不取 1500有放回抽样会让一部分样本重复出现在同一棵树里这是随机森林降低树与树之间相关性的关键机制按概率大约 36.8% 的样本不会被某棵树抽中这些样本在改造时可以当作袋外数据 OOB 用来做误差验证。tn30 对三分类加 500 条测试样本来说够用树太少投票结果容易抖树太多建树时间线性上涨收益变缓。2.3 MFE 特征筛选流程系统设计里有一段特征提取过程先算每个特征的重要性并按降序排序然后按一定比例剔除最不重要的特征得到一个新特征集用新特征集重复上述过程直到剩下 m 个特征最后比较各特征集对应的袋外误差率选误差率最低的特征集。放到这套实现里MFE 的完整路径是用全量 6 个特征训练一遍森林保存每棵树的分类效果。估算特征重要性常见做法是累加每个特征在所有分裂节点上带来的信息增益增益总和越大特征越重要也可以对某一列做随机打乱观察袋外误差上升量。按重要性降序排列剔除比例一般从 10% 起步。用剩余特征重新构建随机森林记录袋外误差率。重复直到特征数降到目标 m选择所有迭代中袋外误差率最低的那组特征集。素材里的训练主循环固定用 randperm(6,5)是把这个流程简化到单轮每棵树排除一个特征实际上就是一种轻量特征扰动。如果数据特征增多到几十维MFE 的价值会更明显在 6 维小数据集上它的效果大概率退化成「全选」或「只减掉最差的一两个特征」。3. 核心代码拆解从 randperm 抽样到 ID3 递归建树的完整链路3.1 训练主循环一棵树怎么生成先看训练森林的主循环这是整个系统的入口代码不长但信息密度高clear all; clc; rnodecell(3,1); % 3*1 单元数组存每棵树的节点分裂属性/叶子类别 rchild_valuecell(3,1); % 存每个节点的分支取值列表 rchild_node_numcell(3,1);% 存每个节点的子节点编号列表 sn900; % 每棵树有放回抽取的样本数 tn30; % 森林中决策树棵数 Sxlsread(loan_train.xls); for j1:tn Featurerandperm(6,5); % 随机选取5个特征参与当前树分裂 Sample_numrandi([1,1500],1,sn); % 从1到1500内随机抽取sn个样本允许重复 SDataS(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; end三个 r 开头的 cell 数组就是整个森林的存储结构每一行对应一棵树。rnode{j,1} 里存的是第 j 棵树的分裂属性编号比如 node{1}3 表示根节点按第 3 列“是否有房”分裂如果节点是叶子node{n} 里直接存类别字符串 1、2 或 3。rchild_value{j,1}{n} 存的是节点 n 的分支取值比如根节点有房/没房两个取值rchild_node_num{j,1}{n} 存这些取值分别指向的子节点编号。三个数组对照着读就能把一棵树完整还原出来。参数里 sn900 意味着同一棵树训练时可能重复看到同一条样本这是有放回抽样的正常表现不用去重。3.2 ID3 递归建树与四个终止条件每棵树的建树过程都在 ID3 函数里完成核心递归函数是 TreeNode。它有几个终止条件直接决定了树的深度和叶子内容function [node,child_value,child_node_num]TreeNode(DValue, CN, A, ClassPNum, m) global node child_value child_node_num nlength(node); if m0 klength(child_node_num{m}); child_node_num{m}(k1)n1; % 把当前节点注册到父节点的子节点列表 end % 条件1样本为空则树为空 if isempty(DValue) node{n1}[]; child_value{n1}[]; child_node_num{n1}[]; return; end % 条件2用于划分的属性为空返回多数类 if isempty(ClassPNum) node{n1}find_most(CN,A); child_value{n1}[]; child_node_num{n1}[]; return; end % 条件3所有样本都属于同一类直接返回该类别 CNRowNumCN_sta(CN,A); if length(find(CNRowNum0))2 node{n1}CN(A(1)); child_value{n1}[]; child_node_num{n1}[]; return; end % 条件4信息增益最大值小于等于0返回多数类 IExp(CN,A); for i1:length(ClassPNum) Entropy(i)avg_entropy(DValue(:,ClassPNum(i)), A, CN); Gain(i)I-Entropy(i); end if max(Gain)0 node{n1}find_most(CN,A); child_value{n1}[]; child_node_num{n1}[]; return; end % 否则选信息增益最大的属性分裂并递归生成子节点 maxGfind(Gainmax(Gain)); [PValue,RowNum]type_sta(DValue(:,ClassPNum(maxG(1))), A); node{n1}ClassPNum(maxG(1)); child_value{n1}PValue; child_node_num{n1}[]; ClassPNum(maxG)[]; for i1:length(PValue) [node,child_value,child_node_num]TreeNode(DValue, CN, RowNum{i}, ClassPNum, n1); end end这里用了全局变量 node、child_value、child_node_numMATLAB 里递归函数共享同一个结构数组比每次传值高效但也要注意递归前必须 clear 全局变量否则上一次运行残留的树会叠加到新树上。四个终止条件的意义条件 1 处理极端空集条件 2 和条件 4 都是回退到多数类防止树无限生长条件 3 是正常收敛路径所有样本标签一致就直接封叶子。分裂属性选择上代码遍历 ClassPNum 里剩余的所有可用特征每次选择一个信息增益最大的特征进行分裂。注意 ID3 函数开头有一次[Feature,~]sort(Feature)随后用 setdiff 把没被选中的特征列全部置 0保证每棵树实际只用 randperm 选出的 5 个特征建树。这就是随机森林“特征随机”的落地方式。3.3 信息增益熵与条件熵的计算ID3 的灵魂是信息增益TreeNode 靠它决定分裂属性。两个核心子函数是 Exp 和 avg_entropyfunction IExp(CN,A) CNRowNumCN_sta(CN,A); nlength(A); I0; for i1:3 if CNRowNum(i)0 P(i)CNRowNum(i)/n; II-P(i)*log2(P(i)); end end end function entropyavg_entropy(Attri, A, CN) nlength(A); IExp(CN,A); [PValue,RowNum]type_sta(Attri,A); entropy0; for i1:length(PValue) CIExp(CN,RowNum{i}); entropyentropy-length(RowNum{i})/n*CI; end endExp 算的是样本集合的熵先统计类别 1、2、3 各有多少条除以总样本数得到概率 P(i)再套信息熵公式 I-ΣP·log2(P)。avg_entropy 算的是按某个特征划分后的条件熵type_sta 把样本按该特征的不同取值分成若干组 RowNum对每个组分别算熵按组内样本数占总数的比例加权求和。TreeNode 里 GainI-Entropy信息增益越大说明该特征让分类不确定性下降得越多就选它做当前节点的分裂属性。代码里有个细节条件 3 判断length(find(CNRowNum0))2实现“三类里两类数量为 0”说明当前节点样本已经纯到只剩一个类别直接返回 CN(A(1)) 作为叶子标签。这类字符串判断方式在后面做二次开发时最容易改错因为 MATLAB 里字符串 1 和数值 1 用比较会得到意外结果。3.4 多树投票与 0.1*k 回退策略森林建好后测试阶段对每一条样本调用 statistics让所有树各自给出预测再按少数服从多数输出最终类别function [type]statistics(tn,rnode,rchild_value,rchild_node_num,PValue) TypeName{1,2,3}; TypeNum[0 0 0]; for i1: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 maxnfind(TypeNummax(TypeNum)); typestr2num(TypeName{maxn(1)}); endvote 函数里有一段容易看晕但很有工程味道的处理当测试样本在当前节点的特征取值在训练时没出现过代码用 0.1*k 的方式做“近邻回退”。k0; while ~isempty(rchild_node_num{j,1}{n}) for i1:length(rchild_value{j,1}{n}) if PValue(rnode{j,1}{n})rchild_value{j,1}{n}(i) nrchild_node_num{j,1}{n}(i); k0; break; end end if ilength(rchild_value{j,1}{n}) % 没找到匹配取值向两侧微调后继续找 PValue(rnode{j,1}{n})PValue(rnode{j,1}{n})0.1*k; PValueroundn(PValue,-1); end k(-1)^k*(abs(k)1); end这段代码的实际行为是当前分裂属性的取值在 child_value 里找不到时先把该值加 0.1、再减 0.2、再加 0.3……符号交替、步长递增直到四舍五入后能命中一个训练集中存在的最接近取值。离散值匹配因此变成了带搜索的回退匹配这也是 500 条测试样本能稳定跑到 91.6% 准确率的一个隐性保障。代价是它掩盖了训练集与测试集取值不一致的数据问题真正的修复应该在数据预处理阶段完成这个在第 5 章展开。4. 从零跑通环境准备、数据生成与结果文件解读4.1 环境与文件清单运行环境要求不复杂Windows 10 系统安装 MATLAB R2019a 或更高版本即可不依赖任何第三方工具箱。工程目录里需要准备好以下文件ID3.mID3 主函数内部包含 TreeNode、find_most、Exp、avg_entropy、type_sta、CN_sta 等子函数statistics.m投票统计函数内部包含 voteforest_main.m主脚本按顺序完成训练、测试、结果输出loan_train.xls1500 行 7 列的训练数据loan_test.xls500 行 7 列的测试数据最后一列是真实标签。所有 .m 文件和 .xls 文件放在同一个工作目录里MATLAB 切换当前路径到该目录即可。注意路径和文件名尽量避免中文xlsread 对中文路径的兼容性在不同版本上表现不一致这是最容易提前埋坑的地方。4.2 主脚本运行过程与命令含义训练循环跑完后进入测试阶段主脚本后半段负责读取测试集、逐条分类、写回结果并打印准确率Txlsread(loan_test.xls); TDataroundn(T(:,1:end-1),-1); % 测试特征保留一位小数保证与训练特征编码一致 lenlength(TData(:,1)); typezeros(len,1); for j1:len [type(j)]statistics(tn,rnode,rchild_value,rchild_node_num,TData(j,:)); end xlswrite(loan_result.xls,[T type]); gdT(:,end); countsum(typegd); fprintf(共有%d 个样本判断正确的有%d\n准确率为百分之%s\n,len,count,count/len*100);这个脚本的运行顺序是先 xlsread 读测试集TData 去掉最后一列标签只保留特征并做和训练侧一致的 roundn 四舍五入然后循环调用 statistics每次传入一行测试特征返回一个预测类别xlswrite 把原始测试数据 T 和预测结果 type 拼接成 8 列写入 loan_result.xls最后用 gd 取真实标签计算准确率。需要说明两个容易踩的点。第一roundn 保留一位小数这一步不能省训练侧在 ID3 函数里同样对 DValue 做了 roundn两边精度不一致会导致 type_sta 匹配不到取值触发大量近邻回退准确率会掉到 80% 以下。第二素材里 printf 的百分之%s配合数值参数并不严谨R2019a 下这里应该改成fprintf(准确率为%.2f%%\n,count/len*100)否则会出现格式符和参数类型不匹配的警告甚至报错读者在自己复跑时建议直接替换。4.3 没有原始 Excel 时用模拟数据验证流程如果手上没有 loan_train.xls 和 loan_test.xls可以临时生成一份模拟数据来验证代码能不能跑通rng(2024); X1randi([1,3],1500,1); % 年龄段 X2randi([0,2],1500,1); % 收入 X3randi([0,1],1500,1); % 是否有房 X4randi([1,3],1500,1); % 信用等级 X5randi([0,1],1500,1); % 性别 X6randi([0,1],1500,1); % 居住地 yrandi([1,3],1500,1); % 贷款决策 xlswrite(loan_train.xls,[X1 X2 X3 X4 X5 X6 y]); % 测试集同理生成500行模拟数据只能验证代码链路完整不能复现 91.6% 的准确率因为标签 y 是纯随机生成的特征之间没有真实相关性。跑通之后要验证分类效果还是得拿到和业务场景一致的原始数据集。rng(2024) 固定随机种子能让每次生成的模拟数据一致方便排查问题。4.4 输出结果与树结构还原运行结束后生成 loan_result.xls共 8 列前 7 列是原始测试数据第 8 列是预测标签。真实标签在第 7 列预测标签在第 8 列这两个别搞反。用 MATLAB 读回来可以直接算混淆矩阵resxlsread(loan_result.xls); true_labelres(:,7); pred_labelres(:,8); confusionmat(true_label,pred_label)对角线上的数字就是分对的样本数非对角线能看到具体哪两类容易混淆。素材里的 91.6% 对应 500 条中 458 条正确如果读者复现的结果明显低于这个数优先怀疑特征取值处理和随机种子设置。树的还原方式是从三个数组中递归读取function show_tree(node, child_value, child_node_num, n, depth) if ischar(node{n}) fprintf(%*sclass %s\n, depth*2, , node{n}); return; end fprintf(%*sfeature %d\n, depth*2, , node{n}); for i1:length(child_value{n}) fprintf(%*s %g - child %d\n, (depth1)*2, , child_value{n}(i), child_node_num{n}(i)); show_tree(node, child_value, child_node_num, child_node_num{n}(i), depth1); end end show_tree(rnode{1,1}, rchild_value{1,1}, rchild_node_num{1,1}, 1, 0);这段递归不是素材自带是通用的决策树还原方式。node 里叶子节点存的是字符类型分裂属性存的是数值类型靠 ischar 区分两者。能完整打印出一棵树说明对随机森林三个数组的理解到位了后面改造成本会低很多。5. 避坑实录随机森林ID3 在 MATLAB 里的几处翻车点5.1 NaN 混进 Excel 数据信息增益计算变成 NaN现象训练集读入后准确率时高时低TreeNode 里 Gain 出现 NaNmax(Gain) 直接失效。原因xlsread 遇到空单元格返回 NaNtype_sta 会把 NaN 当成一个独立取值样本被分到「NaN 组」CN_sta 统计时该组类别概率混乱信息熵计算中 log2 的参数变成 0 或负数Gain 随之变成 NaN。解决读入数据后先筛掉含 NaN 的行再做后续计算dataxlsread(loan_train.xls); data(any(isnan(data),2),:)[];同样的检查也应用在测试集读入后尤其是从数据库导出的 Excel空行经常隐藏在第 1501 行以后肉眼很难发现。5.2 测试集出现训练集没有的属性值vote 陷入死循环现象某条测试样本被判成明显不合理的类别或者 while 循环长时间卡住不退出。原因ID3 建树时只见过训练集里的离散取值测试样本如果出现训练集没出现过的值child_value 里找不到匹配项。代码虽然有 0.1*k 近邻回退但 k 的交替方向是 0.1、-0.2、0.3……如果当前特征值在取值边界比如 0向左偏移会变成负数反而离真实值越来越远。解决在训练前统计每一列特征的取值并集测试时对特征值做合法性校验train_valsunique(S(:,1)); test_bad~ismember(T(:,1),train_vals);发现非法取值时把它映射到训练集里最接近的值而不是依赖 vote 内置的回退机制。vote 的回退适合兜底不适合当主流程。5.3 连续属性直接训练导致过拟合现象训练集准确率接近 100%测试集准确率掉到 70% 以下树结构非常深。原因ID3 信息增益天然偏好取值个数多的属性连续值每个取值只覆盖少量样本分裂后每个子节点样本太少叶子几乎是为训练样本量身定做。这就是素材最后提到的「连续属性样本下训练出的决策树存在较严重过拟合」。解决先把连续属性分箱成离散标签比如把收入映射成 0/1/2 三档或者把分裂逻辑从「等于某个值」改成「小于等于阈值走左、大于阈值走右」后面第 6 章会说明具体改造点。5.4 结果每次运行不一样报告数据对不上现象同一份训练集和测试集连续跑两次准确率在 90% 附近波动甚至某次跑出 85%。原因有放回抽样 randi 和特征选择 randperm 每次执行都会重新生成随机序列森林中 30 棵树结构不同投票结果自然不同。解决主脚本最前面加固定随机种子rng(1);之后每次运行都能复现相同结果。写实验报告或对比试验时固定随机种子后还要跑 5 到 10 次取平均避免单次随机性误导判断。5.5 xlswrite 写结果时报错或文件内容更新不及时现象MATLAB 抛出 COM 相关报错或者生成的 loan_result.xls 里还是上一次的旧数据。原因xlswrite 在 Windows 平台底层走 Excel ActiveX 接口如果 Excel 进程里已经打开了同名文件文件被占用导致写入失败即使没报错旧文件缓存也可能让内容看起来没更新。解决写之前先删除旧文件生成新的if exist(loan_result.xls,file) delete(loan_result.xls); end更稳妥的做法是改用 writetable 输出 CSV 文件比如writetable(table(T,type),loan_result.csv)绕开 COM 组件后续用 Python 或 Excel 读 CSV 也更方便。6. 二次开发把这套随机森林改成你自己的分类器想要把这套系统迁移到新业务上核心改动点有三个数据集维度、分裂规则、误差评估方式。如果新业务只需要「同意」和「拒绝」二分类比如把「还需考虑」并入拒绝最简单的方式是把标签预处理改成两档yS(:,7); y(y2)3; % 还需考虑归为不同意 y(y3)0; % 重映射为 0/1投票函数再相应缩减这一步不需要动 ID3只需要改 statistics 里的 TypeName 和投票计数逻辑改完对三分类混淆矩阵的解读也会变成二分类敏感度/特异度。第二个值得做的改造是把 ID3 的信息增益换成 C4.5 的信息增益率。ID3 偏好取值多的特征在真实连续特征丰富的场景下翻车是必然的。改造位置在 TreeNode 函数 Gain 计算处将 Gain 除以分裂信息量 SplitInfoSplitInfo-sum((count_k/n).*log2(count_k/n)); GainRatioGain/SplitInfo;这样多取值特征不再天然占优同时配合阈值二分就能直接消费连续属性树不会因为每个样本值自成一组而疯狂加深。第三个改造点是用袋外样本自动确定特征数 m。训练主循环里Sample_numrandi([1,1500],1,sn)抽取的是有放回样本没被抽中的样本可以取出作为 OOB 验证集all_idx1:1500; oob_idxsetdiff(all_idx,unique(Sample_num));对每种特征候选数 m 分别训练森林用 OOB 样本投票预测并记录错误率选错误率最低的 m 作为最终配置。这是替代手动 MFE 特征筛选的工程化做法数据特征越多越能看出效果差距。说起这套系统我印象最深的教训来自一次自以为聪明的改造把 roundn 删掉让决策树直接消费原始一位小数数据结果测试准确率从 91% 掉到 79%当时第一反应是怀疑随机森林的随机性出了玄学问题查到最后才发现是数据精度不一致导致大量特征匹配失败。从那以后我每次跑这种离散决策树模型前都会强制执行一遍「特征取值并集检查 两边精度对齐 固定随机种子」三步操作数据链路干净了算法才能真正发挥实力。希望这篇拆解能帮你把项目跑起来也少走我踩过的弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?