二叉决策树分类MATLAB代码实战:从CART原理到fitctree调优

发布时间:2026/9/12 22:26:37
二叉决策树分类MATLAB代码实战:从CART原理到fitctree调优 简介这是一份以MATLAB编写的二叉决策树分类实现代码包适用于本科、硕士阶段教研学习以及需要在智能优化、神经网络、图像处理等方向快速搭建分类基线的开发者。代码基于决策树算法实现分类流程覆盖数据加载、树构建、剪枝与预测等关键环节可作为模式识别、数据挖掘课程的实验参考。压缩包共1个文件为m脚本整体仅947B轻量易读适合直接运行调试或嵌入更大工程。资源目前已吸引156人浏览学习小巧但不失实用性。通过该代码可快速理解二叉决策树分裂规则与递归实现思路并结合博主提供的MATLAB多领域仿真内容延伸应用于信号处理、路径规划等场景若运行不通过还可私信交流获取协助。总体而言这份代码适合入门级分类任务演示与教学示范帮助学习者节省从零搭建的时间。1. 二叉决策树分类MATLAB代码.zip的使用价值与场景拿到一个名为二叉决策树分类MATLAB代码.zip的压缩包很多人第一反应是直接解压运行主脚本看到准确率就完事。但真正有经验的工程师会先做两件事一是用whos -file看里面有没有训练好的模型文件二是扫一遍代码里是否用了fitctree还是自定义递归划分。二叉决策树分类在MATLAB里并不缺官方实现缺的是能跑通业务数据的预处理、参数调优和结果解释。这个压缩包的价值通常在于把树模型从“一行API”变成“看得见的规则”。下面按这个思路把它拆开从最小可运行代码到特征重要性分析给你一套可以直接移植进自己项目的流程。2. 从CART原理到MATLAB的fitctree这包代码里最核心的那几个函数2.1 为什么二叉决策树在MATLAB里默认叫CARTMATLAB的ClassificationTree模型实现的是CARTClassification and Regression Tree算法。CART有一个核心约束每个非叶节点只生成两个分支即使特征是分类变量也会被处理成“属于某个集合/不属于某个集合”的二元切分。这与ID3、C4.5支持多叉树的思路不同。二叉分裂的优点在于分割面是超平面的一部分适合连续特征也容易用递归方式实现缺点是如果特征本身层次很多需要多次分裂才能表达树会变深。在统计学里这种递归划分方法被称为“二分递归分割”。fitctree在每次分裂时扫描所有特征和所有可能的切分点选择让不纯度下降最多的那一个。不纯度的度量默认是基尼指数也可以选“deviance”。所以当你看到fitctree的文档里写“Decision tree for classification”时它默认就是二叉CART树而不是一般意义上的“多叉决策树”。理解这一点对后面调整MaxNumSplits和MinParentSize很有帮助。2.2 fitctree的调用签名与最小可运行代码在MATLAB里训练一棵二叉决策树分类器最常见的一行是% 加载内置鸢尾花数据 load fisheriris X meas; % 150x4 特征矩阵 Y species; % 150x1 分类标签 % 训练一棵二叉决策树 tree fitctree(X, Y, ... PredictorNames, {SepalLength,SepalWidth,PetalLength,PetalWidth}, ... SplitCriterion, gdi, ... MaxNumSplits, 20, ... MinParentSize, 10);代码逻辑说明fitctree返回一个ClassificationTree对象里面保存了树结构、训练参数和预测函数。PredictorNames会在后续可视化和特征重要性输出中显示为可读的变量名SplitCriterion设为gdi表示用基尼多样性指数作为不纯度度量数据类别很均衡时也可以用devianceMaxNumSplits限制整棵树的分支总数为20防止树无限生长MinParentSize表示一个节点样本数少于10时就停止分裂。参数说明这四个参数是调参时最先改的。MaxNumSplits设得越小模型越保守设得越大越可能过拟合。MinParentSize则控制树的局部生长通常设置成总样本量的1%到5%之间。如果训练数据有10000行MinParentSize可以从50开始试。2.3 参数表SplitCriterion、MaxNumSplits、MinParentSize到底怎么设把这几个参数放到一起看它们的默认值并不是为你的数据量设计的。下表是我经常使用的初始值组合适合中小规模数据集1000到50000行参数默认值作用建议初始值SplitCriteriongdi节点不纯度度量方式分类用gdi多分类用devianceMaxNumSplitssize(X,1)-1整棵树的分支总数上限先设10~50再交叉验证MinParentSize10父节点允许分裂的最小样本数总样本量的1%左右MinLeafSize1叶节点最小样本数5~10防止叶子过细Pruneon是否启用成本复杂度剪枝保持默认onMaxNumSplits和MinParentSize都能限制树的复杂度但作用点不同。MaxNumSplits是全局刹车树一旦达到指定分支数就停止分裂MinParentSize是局部闸门某个节点样本过少就停止。两者配合时优先保证MinParentSize符合业务经验。比如信贷场景中某个节点只剩30个违约样本再分下去统计意义不大就可以把MinParentSize设为30。另一个容易忽略的参数字段是ClassNames。当标签是数字时fitctree默认按数值排序但你可能希望类别顺序与业务定义一致。可以在调用时显式指定ClassNames, {good,bad}这样confusionchart和ROC的曲线顺序才不会被搞乱。zip包里的代码如果用到多分类建议检查有没有设置这个参数。3. 手写一份最小二叉决策树分类代码当fitctree不够用的时候3.1 递归划分的停止条件与信息增益计算很多时候你拿到zip包里的mytree.m会发现它不是简单调用fitctree而是自己写了一个递归函数。为什么要手写因为教学演示要展示每个分裂点的选择过程或者是想在不支持ClassificationTree的旧版MATLAB上运行。不管是哪种手写树的核心只有三件事停止条件、不纯度计算、分裂点枚举。停止条件一般有三个一是当前节点样本类别完全一致直接返回这个类别二是样本数低于minLeaf阈值返回当前多数类别三是遍历所有特征和切分点后不纯度没有下降返回多数类别。这三个条件缺一不可否则递归会无限循环或者产生大量叶子导致过拟合。在实际代码里第二个条件通常写在最前面因为它是成本最低的判断。不纯度度量最常用的是基尼指数。对样本集合D基尼指数为Gini(D) 1 - sum(p_k^2)其中p_k是第k类样本占比。基尼指数越低集合越纯。分裂后以样本量加权得到总基尼指数然后选择使加权基尼指数最小的特征和切分点。比信息熵的好处是它不涉及对数运算在MATLAB中枚举大量切分点时更快。3.2 用MATLAB脚本实现二叉分裂下面给一个可在R2016b及以上版本运行的最小实现。buildTree递归返回结构体左子树和右子树分别保存在left和right字段里。function tree buildTree(X, y, minLeaf) % X: nxp 特征矩阵y: nx1 数值标签minLeaf: 叶子最小样本数 % 返回结构体 tree叶子节点包含 label内部节点包含 feature、value、left、right n size(X, 1); % 停止条件样本太少或类别单一 if n minLeaf || numel(unique(y)) 1 tree struct(label, mode(y)); return; end bestGini Inf; bestFeature 0; bestValue 0; % 枚举所有特征 for f 1:size(X, 2) vals unique(X(:, f)); if numel(vals) 1 continue; % 该特征只有一个取值无法分裂 end % 取相邻值的中点作为候选切分点 for i 1:numel(vals)-1 splitVal (vals(i) vals(i1)) / 2; idx X(:, f) splitVal; g weightedGini(y, idx); if g bestGini bestGini g; bestFeature f; bestValue splitVal; end end end % 如果没有找到有效分裂返回多数类别 if bestGini Inf tree struct(label, mode(y)); return; end idx X(:, bestFeature) bestValue; tree struct(feature, bestFeature, ... value, bestValue, ... left, buildTree(X(idx, :), y(idx), minLeaf), ... right, buildTree(X(~idx, :), y(~idx), minLeaf)); end function g weightedGini(y, idx) % 计算按 idx 划分后的加权基尼指数 y y(:); classList unique(y); n1 sum(idx); n2 sum(~idx); if n1 0 || n2 0 g Inf; return; end g1 1; g2 1; for c 1:numel(classList) p1 sum(y(idx) classList(c)) / n1; p2 sum(y(~idx) classList(c)) / n2; g1 g1 - p1^2; g2 g2 - p2^2; end g (n1 * g1 n2 * g2) / (n1 n2); end代码逻辑说明weightedGini先计算左右子集的基尼指数再按样本量加权平均得到整次划分的总不纯度。buildTree里每次分裂后把左右子集分别传给递归调用所以这棵树是标准二叉树。注意mode(y)在连续数值标签时取最小类别这里假设y已经是正整数编码。调用示例与参数说明rng(42); X randn(200, 2); y double(X(:,1) X(:,2) 0); % 二分类线性可分 tree buildTree(X, y, 5); % 预测函数 function label predictTree(tree, x) if isfield(tree, label) label tree.label; elseif x(tree.feature) tree.value label predictTree(tree.left, x); else label predictTree(tree.right, x); end end这里的minLeaf5表示每个叶子至少有5个样本。取值越小树越深取值越大树越浅一般从5到20之间试。predictTree按节点上的特征和阈值向下走走到叶子就返回类别。这个结构与CART是完全一致的。3.3 手写代码与fitctree结果对比的验证方式写完手写树最好和官方fitctree在同一个数据上做一次对比确认分裂逻辑没有系统性错误。对比分两步先比较预测准确率再比较关键分裂点。myPred arrayfun((i) predictTree(tree, X(i,:)), 1:size(X,1)); mdl fitctree(X, y, MinParentSize, 5, SplitCriterion, gdi); fitPred predict(mdl, X); fprintf(手写树训练准确率: %.2f%%\n, mean(myPred y) * 100); fprintf(fitctree训练准确率: %.2f%%\n, mean(fitPred y) * 100); % 查看 fitctree 的根节点分裂特征 view(mdl, Mode, text);逻辑说明由于手写版只用了基尼指数、没有剪枝准确率会略低于或接近fitctree。如果差距超过5个百分点通常是因为手写版没有处理缺失值或者切分点只取了相邻值的中点而没取更细的网格。对比时还要注意fitctree默认会做成本复杂度剪枝所以训练集准确率不一定会最高。参数说明上面的arrayfun逐行预测慢但直观如果数据量大可以改为向量化批处理但二叉树本身不适合向量化通常还是循环。这里MinParentSize5是为了和手写树的minLeaf5保持一致。view(mdl,Mode,text)会输出文本规则你可以手动检查第一层分裂是否和手写树计算的bestFeature一致。对比点手写树fitctree分裂准则基尼指数默认基尼指数剪枝无默认成本复杂度剪枝缺失值处理不支持支持效率慢适合教学快适合生产4. 处理zip包里的数据文件从CSV到table再到预测新样本4.1 用readtable导入CSV并处理缺失值zip包里最常见的三个文件是train.csv、test.csv和run_me.m。run_me.m里通常第一段就是用readtable读数据但如果你直接运行很容易踩两个坑第一缺失值没有处理fitctree在训练阶段会自动跳过缺失值但预测阶段对缺失值不友好第二类别变量被读成cell字符串数组fitctree能接受字符串但速度会变慢。所以建议先做一次缺失值体检。data readtable(train.csv, PreserveVariableNames, true); % 查看缺失值数量 disp(sum(ismissing(data))); % 删除缺失值过多的行 data rmmissing(data); % 如果不想删行用滑窗中位数填充连续特征 data fillmissing(data, movmedian, 5, DataVariables, isnumeric);代码逻辑说明PreserveVariableNames会保留CSV中的原始列名避免MATLAB把空格替换成下划线。ismissing对double和categorical都有效但对于cell字符串类型需要先用categorical转换。rmmissing是删除整行适合缺失比例小于1%的场景fillmissing的movmedian是用滑动中位数填充DataVariables限定只对数值列操作避免不小心把标签列也填了。参数说明滑动窗口5表示用当前值前后共5个点的中位数填充这个值要和数据的采样频率匹配。如果是面板数据建议先按日期排序再填充。另外如果CSV里有中文列名readtable默认会把变量名转为合法标识符中文不会变但空格和特殊字符会被替换这对后续PredictorNames的显示有影响。4.2 用cvpartition划分训练集和测试集数据读进来之后不要直接用datasample随机抽样因为那样会打乱类别分布。正确的做法是用cvpartition做留出法或K折交叉验证。下面代码假设第一列是标签列其余列是特征。labelCol 1; X data{:, 2:end}; y data{:, labelCol}; % 如果y是cell字符串转为categorical if iscell(y) y categorical(y); end cv cvpartition(y, HoldOut, 0.3); trainIdx training(cv); testIdx test(cv); X_train X(trainIdx, :); X_test X(testIdx, :); y_train y(trainIdx); y_test y(testIdx);代码逻辑说明cvpartition(y,HoldOut,0.3)按类别比例分层抽样测试集包含30%的样本。training和test返回逻辑索引用它们切分特征和标签可以保证矩阵行对应关系不被打乱。categorical转换很重要因为fitctree要求标签是categorical或数值不推荐直接使用字符串。参数说明HoldOut比例的选择要考虑总样本量和类别数量。1000行以内可以用0.310000行以上用0.2就足够。如果数据类别极不均衡比如正样本只有2%留出法可能把少量类别样本切没了。这种情况优先用cvpartition(y,KFold,10)做交叉验证而不是一次留出。4.3 用PredictorNames和predict返回结果解读训练和测试都准备好后调用fitctree时最好把变量名传给模型。这样后续画特征重要性图时坐标轴显示的是业务字段名而不是x1、x2。mdl fitctree(X_train, y_train, ... PredictorNames, data.Properties.VariableNames(2:end), ... MaxNumSplits, 20); % 预测测试集 [pred, score] predict(mdl, X_test); % 混淆矩阵 confusionchart(y_test, pred); % 查看第一行预测概率 disp(score(1, :));代码逻辑说明predict返回两个值pred是与y_test同类型的数组score是一个n*k的矩阵每行是该样本属于各个类别的后验概率各列之和为1。confusionchart在R2018b之后可用可以直接展示错分情况。这里把MaxNumSplits设为20目的是让模型更容易画图实际项目中这个值应该通过交叉验证选择。参数说明PredictorNames要和训练矩阵的列一一对应。如果数据里有日期、ID等不参与建模的列必须在构造X时去除。还有一个容易踩的坑是测试集的标签y_test中可能出现训练集没有的类别这通常意味着数据泄露或者划分前没有统一类别水平。解决办法是用categories函数检查两边的类别集合是否一致不一致时用reordercats强制对齐。为了更清晰下表列出了三种常见数据问题的处理函数问题处理函数注意点缺失值rmmissing / fillmissing先看缺失比例再决定标签是字符串categorical训练和测试用同一类别集合特征量纲不同normalize / zscore树模型本身不敏感但可视化方便5. 用可视化与特征重要性调优二叉决策树分类MATLAB代码5.1 用view和treeplot把树画出来看决策树最大的优点是能“看见”规则。fitctree返回的对象可以直接用view查看两种形式的树图形和文本。图形模式适合向业务人员演示文本模式适合排查错误分裂。% 图形化显示 view(mdl, Mode, graph); % 文本规则输出 view(mdl, Mode, text);代码逻辑说明view(...,Mode,graph)会打开一个Figure窗口绿色节点表示分类终结点红色节点表示继续分裂。节点内部会显示分裂规则和样本数。view(...,Mode,text)在命令行输出类似“1 if x2 0.5 then node 2 else node 3”的规则。如果树特别深文本模式输出会很长建议先抑制屏幕显示用files view(mdl);把规则写入临时文件再查看。参数说明当MaxNumSplits超过30时图形模式会变得拥挤节点之间连线重叠。这种情况下要么调小MaxNumSplits要么改用treeplot绘制自定义树形结构。treeplot需要自己把树的父子关系转换成一维数组pp(i)代表第i个节点的父节点编号。这个转换可以用mdl.NodeSize和递归遍历实现但更省事的方法是直接使用view。5.2 用predictorImportance做特征重要性排序调参之前先看哪些特征在真正起作用。predictorImportance返回一个和特征数量相同的一维数组值越大说明该特征在减少节点不纯度时贡献越大。特征重要性不依赖特征量纲所以不需要预先标准化。imp predictorImportance(mdl); % 排序并画横向条形图 [sortedImp, idx] sort(imp, descend); barh(sortedImp); set(gca, YTick, 1:numel(idx), YTickLabel, mdl.PredictorNames(idx)); xlabel(预测器重要性);代码逻辑说明predictorImportance实际上累加每个节点分裂时风险减少量的总和。由于树是二叉树每个特征可能在多个节点被使用重要性会被累加因此特征数的多少会影响数值规模。排序后绘制条形图能直观看到哪些特征需要保留哪些可以删除。参数说明如果两个特征完全相关树可能随机选其中一个导致重要性在两个特征之间分摊这是树模型的常见现象。不要因此认为另一个特征不重要。更好的做法是先用corrcoef检查相关性再决定是否去重。另外这个重要性和MaxNumSplits密切相关树越深特征被重复使用的机会越多重要性分布可能更分散所以在对比重要性前先固定复杂度参数。5.3 用kfoldloss找最优树复杂度参数特征重要性只能排序不能直接告诉我们树该长多深。选定复杂度参数最稳妥的方式是K折交叉验证。fitctree的CrossVal,on参数会直接返回一个ClassificationPartitionedModel对象再配合kfoldLoss可以画出累积损失曲线。rng(42); cvmdl fitctree(X_train, y_train, ... CrossVal, on, ... MaxNumSplits, 100, ... MinParentSize, 5); % cumulative loss 随分支数增加的变化 kfl kfoldLoss(cvmdl, Mode, cumulative); [minLoss, bestIdx] min(kfl); fprintf(最优分支数: %d, 损失: %.4f\n, bestIdx, minLoss); % 用整个训练集重新训练 finalMdl fitctree(X_train, y_train, ... MaxNumSplits, bestIdx, ... MinParentSize, 5);代码逻辑说明kfoldLoss(cvmdl,Mode,cumulative)返回一个向量第i个元素代表树在第i次分支时的平均交叉验证损失。通常曲线先下降后上升最低点对应的bestIdx就是推荐的复杂度。注意这里bestIdx是损失最小处的分支序号而不是损失本身因此输出时也用bestIdx。参数说明CrossVal,on默认使用10折但样本量不足时可能导致某些折没有某一类样本这会报错或产生NaN损失。这时可以显式指定KFold, 5或者改用cvpartition手动生成折再循环训练。另外cumulative模式依赖MaxNumSplits上限上限太小时找不到拐点建议设成样本量的10%。下表是这几种可视化与验证工具的适用场景调试时可以直接对照函数作用适用场景view(mdl,Mode,graph)交互式树图展示给非技术同事view(mdl,Mode,text)文本规则输出调试分裂逻辑predictorImportance特征重要性特征筛选kfoldLoss cumulative交叉验证损失曲线确定树深6. 把二叉决策树分类代码封装成可复用函数的三个小技巧在多个项目中重复使用决策树时最麻烦的不是训练而是每次都要重写数据读取、缺失值处理和参数调优。把这套东西封装成一个函数返回一个带预测函数句柄的model结构是一个很实用的技巧。这样调用方不需要关心ClassificationTree的属性名只需要调用model.predict(Xnew)。function model trainTreeModel(dataPath, nfolds) % 读取数据、训练二叉决策树并返回可复用的预测函数 % dataPath: CSV文件路径 % nfolds: 交叉验证折数默认5 if nargin 2 nfolds 5; end data readtable(dataPath, PreserveVariableNames, true); data rmmissing(data); X data{:, 2:end}; y categorical(data{:, 1}); % 用交叉验证确定最大分支数 cvmdl fitctree(X, y, CrossVal, on, ... KFold, nfolds, ... MaxNumSplits, round(size(X, 1) / 20), ... MinParentSize, max(5, round(size(X, 1) * 0.01))); kfl kfoldLoss(cvmdl, Mode, cumulative); [~, bestIdx] min(kfl); % 用全部数据训练最终模型 mdl fitctree(X, y, ... MaxNumSplits, bestIdx, ... MinParentSize, max(5, round(size(X, 1) * 0.01)), ... PredictorNames, data.Properties.VariableNames(2:end)); % 将预测器封装进结构体 model.predict (newX) predict(mdl, newX); model.model mdl; model.bestSplits bestIdx; end调用时只需要一行treeModel trainTreeModel(creditcard.csv); newPred treeModel.predict(X_test); save(treeModel.mat, treeModel);再次使用时load(treeModel.mat);直接调用treeModel.predict即可。这个封装的好处是训练时的数据预处理、参数搜索全部隐藏在函数内部调用方不容易用错。如果要换特征只需改CSV的列不需要改动预测代码。注意model.predict只接受数值矩阵不接受table。如果业务上拿到的数据也是table建议在封装函数里再包一层(T) predict(mdl, table2array(T(:, 2:end)))。另外model结构体里的bestSplits记录了交叉验证选出来的最优分支数这样出报告时能追溯模型复杂度。调试时如果在新数据上预测报错“Predictor names must match”检查训练时PredictorNames是否和newX的列顺序完全一致。树模型对列顺序是有要求的因为predict会把新数据当作和训练数据相同的特征空间处理但不会自动按名字对齐。所以封装函数最好保留model.predictorNames字段并在调用前用assert检查列数。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询