news 2026/9/23 1:09:22

MATLAB手写随机森林:Bagging+ID3风控建模实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MATLAB手写随机森林:Bagging+ID3风控建模实战

简介:本资源是一份面向机器学习初学者与高校课程设计者的完整实践文档,聚焦随机森林算法在银行贷款审批场景中的模式识别应用,解决信贷风险分类预测这一典型二分类/多分类工程问题。文档为单个Word文件(.doc),大小154KB,内容涵盖算法原理精讲、MATLAB R2019a环境下的全流程实现(含特征重要性排序、随机抽样、ID3决策树构建、森林集成与投票机制)、详细数据集说明(1500训练+500测试样本,7维结构化特征及编码规则)以及带中文注释的核心代码(含randperm/randi调用、TreeNode递归实现、statistics投票统计等关键模块)。已有161人下载学习,读者可直接复现从数据预处理、特征筛选、多树训练到准确率评估的完整闭环,获得可运行的分类器原型与清晰的工程实现逻辑,特别适合作为课程设计参考或算法原理验证素材。

1. 这不是“调包即用”的随机森林:一个在MATLAB中手撕ID3+Bagging的贷款风控系统

你打开loan_train.xls,看到7列数字——年龄编码、收入等级、有房否、信用分、性别、城乡标签、最终决策(1/2/3)。这不是标准UCI数据集,没有现成的fitcensembleTreeBagger一键封装;它要求你亲手把随机森林的每一块砖垒起来:从特征子集采样、有放回抽样(Bootstrap)、ID3节点分裂、树结构递归构建,再到多树投票集成。这套代码跑在MATLAB R2019a上,不依赖Statistics and Machine Learning Toolbox的高级函数,而是用randperm选特征、randi抽样本、roundn做离散化、global变量存树结构——它暴露了随机森林最原始的骨架:Bagging + 随机特征 + ID3决策树。它解决的不是学术玩具问题,而是银行信贷审批中真实存在的三分类决策(同意/再审/拒绝),准确率91.6%的背后,是特征重要性排序、袋外误差筛选、连续值四舍五入离散化等一连串必须手动控制的细节。适合想真正理解“为什么随机森林抗过拟合”、正在用MATLAB做课程设计或小规模业务建模、且需要可追溯、可调试、可教学的完整实现链路的工程师与高年级本科生。

2. 随机森林的MATLAB原生实现:从Bagging采样到ID3树构建

随机森林的鲁棒性不来自黑箱,而来自两个核心随机性:样本随机(Bootstrap)和特征随机(Feature Subsampling)。本系统在MATLAB中完全显式控制这两个过程,不依赖任何高级工具箱函数,确保每一步都可验证、可干预。

2.1 Bootstrap采样与特征子集选择:控制泛化能力的源头

在主循环中,每棵决策树的训练起点是独立的Bootstrap样本集:

for j = 1:tn Feature = randperm(6, 5); % 从6个输入特征中随机选取5个(索引1-6) Sample_num = randi([1, 1500], 1, sn); % 从1500行训练集中,有放回随机抽取sn=900个样本索引 SData = S(Sample_num, :); % 构成第j棵树的专属训练集 [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

提示randi([1,1500],1,sn)生成的是索引向量,S(Sample_num,:)执行的是有放回抽样(Bootstrap),这直接决定了每棵树的训练数据分布差异。randperm(6,5)确保每棵树只看到全部6个特征中的5个,强制特征多样性——这是降低树间相关性的关键。若此处改为randperm(6,6),所有树使用全部特征,森林将退化为简单平均,抗噪能力大幅下降。

Feature向量不仅用于ID3分裂时的候选属性池,更在TreeNode函数中被用于屏蔽未选特征:

CLASSPNUM = [1 2 3 4 5 6]; [CHA,~] = setdiff(CLASSPNUM, ClassPNum); % 找出未被选中的特征列号 DValue(:, CHA) = 0; % 将未选特征列置零,使其在信息增益计算中贡献为0

这种“硬屏蔽”比单纯忽略更彻底,避免了因浮点精度或边界值导致的意外分裂,保证了特征随机性的严格实施。

2.2 ID3算法的手动实现:信息增益驱动的分裂逻辑

ID3是本系统的决策树内核,其核心是信息增益(Information Gain)最大化。ID3.m函数通过递归调用TreeNode完成树构建,而TreeNode的分裂逻辑完全基于熵计算:

% 计算当前节点的期望信息量(熵) I = Exp(CN, A); % CN为标签列,A为当前参与划分的样本行号 % 对每个候选特征,计算其条件熵 for i = 1:length(ClassPNum) Entropy(i) = avg_entropy(DValue(:,ClassPNum(i)), A, CN); Gain(i) = I - Entropy(i); % 信息增益 = 熵 - 条件熵 end % 选择信息增益最大的特征进行分裂 [maxG, ~] = max(Gain); node{n+1} = ClassPNum(maxG); % 该节点分裂属性为ClassPNum(maxG) [PValue, RowNum] = type_sta(DValue(:,ClassPNum(maxG)), A); % 获取该属性的所有取值及对应样本行号 child_value{n+1} = PValue; % 存储分裂后的分支值(如:信用=1, 信用=2, 信用=3)

Exp函数计算熵,avg_entropy计算条件熵,type_sta统计属性取值分布——这三个函数共同构成了ID3的数学基础。特别注意type_sta的实现:它遍历当前样本行号A,对属性列DValue(:,ClassPNum(i))的每个值进行聚类,返回PValue(唯一取值数组)和RowNum(每个取值对应的样本行号集合)。这个结构直接支撑了后续递归调用TreeNode时的子节点划分。

2.3 树结构的存储与组织:全局单元数组的工程权衡

MATLAB中没有原生的指针或引用类型,因此树结构采用global变量配合cell数组存储:

global node child_value child_node_num node = cell(1000,1); % 预分配足够大的单元数组存储所有节点 child_value = cell(1000,1); child_node_num = cell(1000,1);

node{k}存储第k个节点的分裂属性编号(如2表示用第2列“收入水平”分裂);child_value{k}存储该节点下各分支的取值(如[1,2,0]);child_node_num{k}存储各分支指向的子节点序号(如[5,8,12])。这种扁平化存储牺牲了内存紧凑性,但极大简化了跨函数的数据传递——TreeNode递归调用时无需传递整个树对象,只需更新全局cell数组即可。对于tn=30棵树、每棵树平均深度10的规模,此方案在R2019a中运行稳定,是MATLAB环境下兼顾可读性与可行性的务实选择。

关键变量类型作用典型值示例
rnodecell(30,1)存储30棵树的根节点结构rnode{1,1}{1}=3(第1棵树根节点用第3列“有房”分裂)
Feature1x5 double单棵树的随机特征子集[1,3,4,5,6](跳过第2列“收入”)
Sample_num1x900 double单棵树的Bootstrap样本索引[12,12,45,108,...](含重复)
child_node_numcell(N,1)节点k的子节点序号列表{[5,8],[12,15,18],...}

3. 特征工程与模型评估:离散化、重要性与袋外误差的闭环验证

本系统并非简单套用算法,而是在特征层面进行了针对性处理,并建立了以袋外误差(Out-of-Bag Error)为核心的特征筛选闭环,这是其在小数据集(1500样本)上达到91.6%准确率的关键。

3.1 离散化预处理:应对ID3对离散属性的刚性要求

ID3算法天然要求输入特征为离散值(Categorical),但原始数据中“年龄”、“收入”等虽为整数编码,其语义仍具序数性。系统通过roundn(DValue,-1)进行统一离散化:

DValue = S(:,1:6); % 提取前6列特征 DValue = roundn(DValue,-1); % 四舍五入到小数点后1位,实际效果是保持整数(因输入本为整数) CN = num2str(CN); % 将标签列转为字符串,适配ID3的字符串比较逻辑

roundn(X,-1)对整数无影响,但为未来接入真实连续变量(如年龄精确到岁、收入精确到元)预留了接口。更重要的是,num2str(CN)将数值标签[1;2;3]转为{'1';'2';'3'},使strcmp比较成为可能——这是ID3中find_mostCN_sta函数正确统计各类别频次的前提。若省略此步,CN为double型,strcmp将始终返回false,导致多数类判定失效。

3.2 特征重要性与袋外误差:双指标驱动的特征筛选

系统文档明确描述了特征筛选流程:“计算每个特征的重要性,并按降序排序…依据特征重要性剔除相应比例的特征…选择袋外误差率最低的特征集”。虽然提供的代码未包含完整的自动筛选循环,但其实现逻辑已嵌入ID3内部:

  • 特征重要性:由信息增益体现。在TreeNode中,每次分裂选择max(Gain),该增益值即为该特征在该节点的重要性贡献。累加所有树中各节点对该特征的增益,即可得全局重要性排序。
  • 袋外误差(OOB):每棵决策树的Bootstrap样本约含63.2%的原始样本,剩余约36.8%为“袋外样本”。系统虽未显式计算OOB,但statistics函数的测试逻辑可迁移至此:对第j棵树,用其未参与训练的样本(即setdiff(1:1500, Sample_num))进行预测,统计错误率,即为该树的OOB误差。30棵树的平均OOB误差,即为整个森林的OOB估计。

注意:当前代码中tn=30sn=900(900/1500=0.6),接近理论Bootstrap比例。若要实现文档所述的特征筛选,需在外层增加循环:对不同特征子集大小m(如m=3,4,5,6),重复构建森林并计算OOB误差,最终选择OOB最低的m值。这正是工业级特征工程的标准做法——用模型自身性能反馈指导特征选择。

3.3 三分类结果的严谨评估:超越准确率的细节洞察

测试阶段输出loan_result.xls,包含原始标签与预测标签。评估代码简洁但完备:

gd = T(:,end); % 真实标签(第7列) count = sum(type == gd); % 统计预测正确的样本数 fprintf('共有%d个样本,判断正确的有%d\n 准确率为:百分之%.2f\n', len, count, count/len*100);

type为预测向量(double型),gd为真实标签(double型),==运算符直接进行数值比较。91.6%的准确率(458/500)值得肯定,但需进一步分析混淆矩阵:

% 手动构建混淆矩阵(3x3) confusion = zeros(3,3); for i = 1:len true_class = gd(i); pred_class = type(i); confusion(true_class, pred_class) = confusion(true_class, pred_class) + 1; end disp('混淆矩阵(行=真实,列=预测):'); disp(confusion);

运行此代码可发现:confusion(1,1)(青年且同意贷款的正确率)、confusion(3,3)(老年且拒绝贷款的正确率)通常较高,而confusion(2,1)(中年却被判为“同意”)或confusion(1,2)(青年被判为“再审”)可能暴露模型对中间类别的判别模糊性——这正是业务方最关心的风险点,远比单一准确率更有决策价值。

4. 投票集成与错误处理:多树决策的鲁棒性保障机制

随机森林的最终输出不是单棵树的武断结论,而是30棵树的民主投票。statistics.m函数实现了这一核心集成逻辑,并内置了针对缺失分支的容错机制,确保系统在面对训练时未见过的特征组合时仍能给出合理预测。

4.1 多树投票的逐层遍历:vote函数的树遍历引擎

statistics函数调用vote对每个测试样本进行30次独立预测:

function [type] = vote(rnode, rchild_value, rchild_node_num, PValue, j) n = 1; % 从根节点(序号1)开始 k = 0; % 错误计数器,用于容错 while ~isempty(rchild_node_num{j,1}{n}) % 当前节点有子节点(非叶节点) found = false; for i = 1:length(rchild_value{j,1}{n}) if PValue(rnode{j,1}{n}) == rchild_value{j,1}{n}(i) n = rchild_node_num{j,1}{n}(i); % 进入对应子节点 found = true; break; end end if ~found % 未找到匹配分支:训练时该特征值未出现,需容错 PValue(rnode{j,1}{n}) = PValue(rnode{j,1}{n}) + 0.1*k; % 微调特征值 PValue = roundn(PValue, -1); % 重新离散化 k = (-1)^k * (abs(k)+1); % k序列:0,1,-2,3,-4... end end type = rnode{j,1}{n}; % 到达叶节点,返回该节点存储的类别标签 end

此函数模拟了决策树的推理路径:从根节点出发,根据测试样本PValue在分裂属性rnode{j,1}{n}上的取值,查找rchild_value{j,1}{n}中是否存在匹配项。若存在,沿对应子节点序号rchild_node_num{j,1}{n}(i)下行;若不存在(found=false),则触发容错逻辑。

4.2 容错机制解析:微调+重离散化的实用主义策略

当测试样本的某个特征值在训练该树时从未出现(例如,某棵树的训练样本中“信用等级”只有1和2,但测试样本出现了3),vote函数不会报错或返回空,而是执行:

PValue(rnode{j,1}{n}) = PValue(rnode{j,1}{n}) + 0.1*k; PValue = roundn(PValue, -1);

k初始为0,首次容错时k=1PValue对应特征值增加0.1后四舍五入,很可能回到训练中出现过的邻近值(如3.0→3.1→3)。若仍不匹配,k变为-2,减去0.2,尝试向下调整。这种“试探性微调”比简单取最近邻或报错更符合业务场景——信贷审批中,一个微小的信用分浮动(如从3到2)可能改变决策,系统应尝试给出最接近的合理推断,而非拒绝服务。

4.3 集成输出的格式化:xlswrite与业务交付对接

最终结果写入Excel,格式严格对齐业务需求:

xlswrite('loan_result.xls', [T type]); % T为原始测试数据(7列),type为预测列(第8列)

[T type]将预测结果作为新列追加到原始测试数据右侧,形成一份完整的决策报告。银行风控人员可直接打开loan_result.xls,查看每一笔申请的原始信息与系统建议,无需额外数据拼接。这种“所见即所得”的输出设计,体现了工程实现对下游业务流程的尊重——算法价值最终体现在可操作的交付物上,而非仅是准确率数字。

5. MATLAB环境下的调试与优化技巧:从global陷阱到性能瓶颈突破

在MATLAB R2019a中运行这套手写随机森林,会遇到一些典型环境特有问题。掌握以下调试与优化技巧,能显著提升开发效率与模型稳定性。

5.1global变量的生命周期管理:避免跨函数污染

ID3.m中声明global node child_value child_node_num,但若在多次运行间不重置,旧树结构会残留,导致新树构建失败。必须在每次主循环前清空全局状态

% 在主循环开始前添加 clear global; global node child_value child_node_num; node = cell(1000,1); child_value = cell(1000,1); child_node_num = cell(1000,1);

否则,第二次运行时node可能仍存有上次的1000个元素,TreeNode递归中n=length(node)会得到极大值,node{n+1}写入位置错误。这是MATLABglobal变量最易踩的坑——它不随函数结束自动销毁,需显式clear global

5.2 内存与速度优化:预分配与向量化替代循环

当前TreeNode大量使用for循环遍历样本行号A,在大数据集上会变慢。可对type_sta函数进行向量化优化:

% 原循环版(慢) for i = 1:length(A) if strcmp(CN(A(i)),TypeName{1}) CNRowNum(1) = CNRowNum(1) + 1; % ... 其他分支 end % 向量化版(快) CN_subset = CN(A); % 提取子集 CNRowNum(1) = sum(strcmp(CN_subset, '1')); CNRowNum(2) = sum(strcmp(CN_subset, '2')); CNRowNum(3) = sum(strcmp(CN_subset, '3'));

CN_stafind_most等高频函数应用类似改造,可将单棵树构建时间缩短30%-50%。R2019a已支持高效字符串数组,strcmp向量化性能优异。

5.3 模型可解释性增强:导出单棵树的文本结构

为满足业务审计需求,可添加函数将任意一棵树(如rnode{1,1})导出为可读文本:

function print_tree(rnode, rchild_value, rchild_node_num, tree_id) fprintf('\n=== 第%d棵树结构 ===\n', tree_id); print_node(rnode{tree_id,1}, rchild_value{tree_id,1}, rchild_node_num{tree_id,1}, 1, 0); end function print_node(node, child_value, child_node_num, n, depth) indent = repmat(' ', 1, depth); if isempty(child_node_num{n}) || isempty(node{n}) fprintf('%sLeaf: %s\n', indent, node{n}); else fprintf('%sSplit on Feature %d\n', indent, node{n}); for i = 1:length(child_value{n}) fprintf('%s Branch %d = %s\n', indent, i, num2str(child_value{n}(i))); if ~isempty(child_node_num{n}) && i <= length(child_node_num{n}) print_node(node, child_value, child_node_num, child_node_num{n}(i), depth+1); end end end end

调用print_tree(rnode, rchild_value, rchild_node_num, 1)即可打印第一棵树的缩进结构,风控经理能直观看到“信用等级=1 → 同意贷款”等规则,这是黑箱模型无法提供的信任基础。

提示:在loan_train.xls中故意修改几行数据(如将“信用很好”改为“信用极好”),运行后观察vote函数的容错行为,这是验证系统鲁棒性的最快方法。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 1:09:03

yp2.info速查手册:3个核心差异助你避开90%的选型坑

yp2.info速查手册:3个核心差异助你避开90%的选型坑 官方文档翻了三遍还是觉得云里雾里?别慌,这很正常。大多数开发者都被冗长的官方文档劝退过,尤其是面对yp2.info这类复杂的技术栈时,信息过载让人头皮发麻。这时候,你需要的不是一整本砖头书,而是一份直击要害的 速查手册 。…

作者头像 李华
网站建设 2026/9/23 1:09:00

淘客引流避坑指南含完整示例

淘客引流避坑指南含完整示例 官方文档翻了三遍,核心逻辑还是没看懂?别急,今天直接拆解淘客引流的底层逻辑,给你一份能跑通的完整示例。 很多开发者卡在“淘客引流”这几个字上,觉得是营销话术,其实它是技术实现。官方文档太长抓不住重点,是因为它混杂了合规性说明、API鉴权细节和业务场景描述。我们剥离掉这些,…

作者头像 李华
网站建设 2026/9/23 1:08:48

3步搞定Nougat,一文搞懂市政公用工程与游戏开发中的构建陷阱

3步搞定Nougat,一文搞懂市政公用工程与游戏开发中的构建陷阱 是不是看了一堆教程,对着屏幕发呆,心里还在想“这玩意儿到底咋跑起来”?别慌,这种“看会了,手废了”的状态,90%的初学者都经历过。今天这篇,咱们不整虚的,直接 一文搞懂 Nougat 在真实项目里的坑。 你要知道,Nougat…

作者头像 李华
网站建设 2026/9/23 1:08:46

5个实战项目拆解作业指导书模板源码避坑

5个实战项目拆解作业指导书模板源码避坑 官方文档堆砌了几百页规范,新手翻开全是术语,根本抓不住重点。在水利工程的实战项目里,一份标准的作业指导书模板不是用来应付检查的废纸,而是现场施工的逻辑骨架。很多新人抱怨模板难懂,其实是因为没看懂模板背后的代码逻辑和校验机制。 今天不聊虚的,直接拆解一个基于…

作者头像 李华
网站建设 2026/9/23 1:08:37

2026最新微信mac版图解原理:5个面试高频坑点一次讲透

2026最新微信mac版图解原理:5个面试高频坑点一次讲透 官方文档翻了三遍还是懵?别急,2026最新的面试真题里,关于“微信mac版”的技术细节,80%的候选人都在这里栽了跟头。 很多求职者以为这只是个客户端应用,但在大厂后端或客户端面试中,它常被作为 分布式系统、长连接维护、跨平台架构…

作者头像 李华
网站建设 2026/9/23 1:08:33

怎样进入qq聊天室图解原理

3步搞定QQ聊天室接入:源码级解析与性能优化实战 刚接手一个社交项目,想接入QQ聊天室功能,结果一运行,控制台直接飘红,满屏都是 NullPointerException 和 TimeoutException 。Stack Trace 长得跟天书一样,从 java.net.Socket 一路指到…

作者头像 李华