
1. 混淆矩阵到底在解决什么问题混淆矩阵这个词刚接触分类任务的朋友可能会觉得有点抽象。我第一次看到它的时候脑子里浮现的是一团乱麻的表格完全不明白为什么评价一个模型好坏要用这么个东西。后来做多了分类项目才慢慢体会到混淆矩阵本质上就是一张“分类结果的账本”它把模型在每一类上的判断情况一笔一笔记得清清楚楚让你一眼就能看出模型到底在哪儿犯了错、错得有多离谱。举个生活化的例子。假设你是一个门诊医生每天要判断来就诊的人是不是感冒。你的判断只有两种结果感冒或者不感冒。那么一天下来你的判断会出现四种情况感冒的人你说感冒判断正确、感冒的人你说不感冒漏诊、不感冒的人你说感冒误诊、不感冒的人你说不感冒判断正确。把这四种情况分别计数排成一个2乘2的表格这就是最基础的二分类混淆矩阵。在机器学习里这个逻辑完全一样。模型对每个样本给出一个预测类别和真实标签一对比就能统计出四种计数真正例TP、假正例FP、真负例TN、假负例FN。这四个数字就是混淆矩阵的全部内容但由它们衍生出来的指标——准确率、精确率、召回率、F1分数——才是真正用来衡量模型性能的核心工具。那为什么不能只看准确率呢因为准确率在类别不平衡的时候会骗人。比如一个数据集里99%的样本都是负类模型只要全部预测为负类准确率就有99%看起来很高但实际上它一个正类都没识别出来。混淆矩阵能把这个骗局拆穿让你看到正类的召回率是0。这就是为什么在做分类任务时混淆矩阵几乎是绕不开的一步。这篇文章我会从零开始把MATLAB环境下混淆矩阵的代码实现讲透。不管你是刚入门机器学习的学生还是做工程项目的开发者只要你在用MATLAB做分类相关的任务这篇内容都能直接拿来参考。我会把每一步的代码写清楚把每个参数的含义解释明白还会分享一些我在实际项目中踩过的坑和总结的技巧。2. 混淆矩阵的核心概念与指标拆解2.1 四个基础计数TP、FP、TN、FN在深入代码之前有必要把这四个基础概念彻底搞清楚。很多人写代码的时候照着函数文档抄一遍就完事了结果出了图也不知道每个数字代表什么更别提根据结果去优化模型了。真正例True PositiveTP模型预测为正类真实标签也是正类。这是模型判断正确的情况是我们希望越多越好的数字。假正例False PositiveFP模型预测为正类但真实标签是负类。这就是常说的“误报”模型把不该归为正类的东西归进去了。真负例True NegativeTN模型预测为负类真实标签也是负类。这也是判断正确的情况。假负例False NegativeFN模型预测为负类但真实标签是正类。这就是“漏报”模型把本该找出来的正类漏掉了。这四个数字放在一个2乘2的表格里行代表真实类别列代表预测类别或者反过来不同工具的习惯不一样这点后面会专门讲。MATLAB的混淆矩阵图默认是行代表真实类别列代表预测类别对角线上的数字就是判断正确的样本数。注意行和列哪个代表真实、哪个代表预测不同版本的MATLAB和不同的绘图函数可能不一样。看混淆矩阵图的时候第一件事就是确认坐标轴的标签不然很容易把FP和FN搞反。2.2 由混淆矩阵衍生的四大评价指标光看四个计数还不够直观我们需要把它们转化成比率才能在不同数据集之间做比较。下面这四个指标是分类任务中最常用的。准确率Accuracy所有判断正确的样本占总样本的比例公式是 (TPTN)/(TPFPTNFN)。这个指标最直观但在类别不平衡时有很大的欺骗性。精确率Precision在所有被模型预测为正类的样本中真正是正类的比例公式是 TP/(TPFP)。精确率高说明模型说“这是正类”的时候比较靠谱误报少。召回率Recall在所有真实为正类的样本中被模型正确找出来的比例公式是 TP/(TPFN)。召回率高说明模型不容易漏掉正类样本。F1分数F1-Score精确率和召回率的调和平均数公式是 2×Precision×Recall/(PrecisionRecall)。它综合了两个指标在两者之间取一个平衡。这四个指标之间往往存在矛盾。你提高模型的召回率可能会把精确率拉低反过来也一样。具体该侧重哪个指标取决于你的应用场景。比如做疾病筛查漏诊的代价很大那就应该优先保证召回率做垃圾邮件过滤把正常邮件误判为垃圾邮件的代价很大那就应该优先保证精确率。2.3 多分类场景下混淆矩阵的变化二分类的混淆矩阵是2乘2的多分类就是N乘N的。比如一个五分类任务混淆矩阵就是5乘5的表格对角线上的数字是每一类判断正确的样本数非对角线上的数字是各类之间的混淆情况。多分类的指标计算有两种常见策略宏平均Macro-average和微平均Micro-average。宏平均是先算出每一类的精确率和召回率然后取算术平均每一类的权重相同微平均是把所有类的TP、FP、FN分别加起来再统一计算精确率和召回率样本多的类权重更大。这两种策略在类别不平衡时结果差异可能很大。如果每个类都很重要用宏平均如果更关注整体表现用微平均。MATLAB的混淆矩阵函数可以同时输出这两种结果后面代码部分会具体演示。3. MATLAB混淆矩阵代码实操全流程3.1 环境准备与数据构造我用的MATLAB版本是R2020a及以上混淆矩阵相关的函数在这些版本里已经比较完善了。如果你用的是更早的版本部分函数可能不存在建议至少升级到R2018b以上。先来构造一组模拟数据。假设我们有一个三分类任务类别标签分别是“类别A”“类别B”“类别C”一共300个样本每个类100个。为了模拟真实场景中模型不可能百分百准确的情况我手动构造一组预测标签让其中有一部分是错的。% 构造真实标签和预测标签 rng(42); % 固定随机种子保证结果可复现 % 真实标签三个类别各100个样本 trueLabels [repmat(类别A, 100, 1); ... repmat(类别B, 100, 1); ... repmat(类别C, 100, 1)]; % 预测标签在真实标签基础上加入一些随机错误 predictedLabels trueLabels; % 类别A10个错分为B5个错分为C idxA 1:100; predictedLabels(idxA(1:10)) 类别B; predictedLabels(idxA(11:15)) 类别C; % 类别B8个错分为A12个错分为C idxB 101:200; predictedLabels(idxB(1:8)) 类别A; predictedLabels(idxB(9:20)) 类别C; % 类别C6个错分为A9个错分为B idxC 201:300; predictedLabels(idxC(1:6)) 类别A; predictedLabels(idxC(7:15)) 类别B;这段代码构造的数据里类别A有85个正确分类类别B有80个正确分类类别C有85个正确分类。整体准确率是(858085)/300 83.3%。这个数据规模不大但足够演示混淆矩阵的完整流程了。实操心得构造模拟数据时一定要固定随机种子rng函数否则每次运行结果都不一样调试代码时会很痛苦。我在早期做实验的时候没注意这一点调了半天以为代码有问题其实是随机数在变。3.2 使用confusionmat函数计算混淆矩阵MATLAB里计算混淆矩阵最直接的函数是confusionmat。它接收真实标签和预测标签两个参数返回一个矩阵。% 计算混淆矩阵 cm confusionmat(trueLabels, predictedLabels); % 显示混淆矩阵 disp(混淆矩阵); disp(cm);运行结果应该是一个3乘3的矩阵对角线上的数字分别是85、80、85非对角线上的数字对应我们前面设置的错误分类数量。这个矩阵就是后续所有分析和绘图的基础。confusionmat函数还有一个可选参数Order用来指定类别的排列顺序。如果不指定MATLAB会按照标签的字母顺序或者数值顺序自动排列。在多分类任务中类别的排列顺序会影响混淆矩阵的可读性建议手动指定一个符合业务逻辑的顺序。% 指定类别顺序 order [类别A, 类别B, 类别C]; cm confusionmat(trueLabels, predictedLabels, Order, order);3.3 用confusionchart绘制可视化混淆矩阵光看数字矩阵不够直观MATLAB提供了confusionchart函数来绘制可视化的混淆矩阵图。这个函数在R2018b之后引入用起来非常方便。% 绘制混淆矩阵图 figure; confusionchart(trueLabels, predictedLabels); title(三分类混淆矩阵);运行后会弹出一个彩色热力图行代表真实类别列代表预测类别每个格子里的数字是样本计数颜色深浅代表数量多少。对角线上的格子颜色最深因为正确分类的样本最多。confusionchart还支持很多自定义选项。比如可以同时显示行归一化和列归一化的结果% 行归一化按真实类别归一化显示召回率 figure; confusionchart(trueLabels, predictedLabels, ... Normalization, row-normalized, ... Title, 行归一化混淆矩阵召回率视角); % 列归一化按预测类别归一化显示精确率 figure; confusionchart(trueLabels, predictedLabels, ... Normalization, column-normalized, ... Title, 列归一化混淆矩阵精确率视角);行归一化之后每一行的数字加起来是1对角线上的数字就是该类别的召回率。列归一化之后每一列的数字加起来是1对角线上的数字就是该类别的精确率。这两种视角在分析模型问题时非常有用。注意confusionchart函数在R2018b到R2020a之间的版本中部分参数名称可能有差异。如果你用的是这些版本建议先查一下对应版本的文档。我在R2019a上试过Normalization参数是支持的但Title参数需要用title函数单独设置。3.4 手动计算精确率、召回率和F1分数虽然MATLAB有一些内置函数可以计算这些指标但手动算一遍能帮你更深入地理解混淆矩阵的结构。下面这段代码演示如何从混淆矩阵中提取每一类的TP、FP、FN并计算对应的指标。% 从混淆矩阵中提取每一类的TP、FP、FN numClasses size(cm, 1); precision zeros(numClasses, 1); recall zeros(numClasses, 1); f1 zeros(numClasses, 1); for i 1:numClasses TP cm(i, i); FP sum(cm(:, i)) - TP; % 列求和减去对角线 FN sum(cm(i, :)) - TP; % 行求和减去对角线 precision(i) TP / (TP FP); recall(i) TP / (TP FN); f1(i) 2 * precision(i) * recall(i) / (precision(i) recall(i)); end % 显示结果 classNames order; fprintf(%-10s %-12s %-12s %-12s\n, 类别, 精确率, 召回率, F1分数); for i 1:numClasses fprintf(%-10s %-12.4f %-12.4f %-12.4f\n, ... classNames(i), precision(i), recall(i), f1(i)); end % 宏平均 macroPrecision mean(precision); macroRecall mean(recall); macroF1 mean(f1); % 微平均 totalTP sum(diag(cm)); totalFP sum(cm(:)) - totalTP; totalFN totalFP; % 在多分类中总FP等于总FN microPrecision totalTP / (totalTP totalFP); microRecall microPrecision; % 在多分类中微平均的精确率和召回率相等 microF1 microPrecision; fprintf(\n宏平均精确率%.4f召回率%.4fF1%.4f\n, ... macroPrecision, macroRecall, macroF1); fprintf(微平均精确率%.4f召回率%.4fF1%.4f\n, ... microPrecision, microRecall, microF1);这段代码里有一个容易搞混的地方FP是列求和减去对角线FN是行求和减去对角线。为什么因为混淆矩阵的行代表真实类别列代表预测类别。对于第i类来说FP是“真实不是第i类但被预测为第i类”的样本数也就是第i列上除了对角线之外的所有元素之和FN是“真实是第i类但被预测为其他类”的样本数也就是第i行上除了对角线之外的所有元素之和。实操心得行和列的对应关系是混淆矩阵最容易搞错的地方。我的习惯是在代码里写注释明确标注“行真实列预测”并且在画图之后一定检查坐标轴标签。有一次我在一个项目里把FP和FN搞反了导致对模型的优化方向完全判断错误白白浪费了两天时间。3.5 使用classificationReport生成完整报告如果你不想手动写循环计算指标MATLAB的classificationReport函数需要Statistics and Machine Learning Toolbox可以直接从混淆矩阵生成一份完整的分类报告。% 生成分类报告 report classificationReport(trueLabels, predictedLabels); disp(report);这个报告会输出每一类的精确率、召回率、F1分数和支持度该类真实样本数以及整体的宏平均和微平均结果。输出格式和Python的scikit-learn库类似用起来很顺手。如果你的MATLAB版本没有这个函数也可以用perfcurve函数来计算AUC等其他指标或者自己封装一个计算函数。我在没有classificationReport的版本上自己写过一个工具函数核心逻辑就是上面那段手动计算的代码封装成函数之后调用起来也很方便。4. 混淆矩阵分析中的常见问题与排查技巧4.1 类别不平衡导致准确率虚高这是混淆矩阵分析中最常见的问题。假设你的数据集里正类样本只占5%负类占95%。模型把所有样本都预测为负类准确率是95%看起来很高但混淆矩阵会显示正类的召回率是0精确率也是0因为TP0。排查方法很简单看混淆矩阵的每一行。如果某一行的非对角线元素远大于对角线元素说明这个类别的召回率很低。这时候不要只看整体准确率要重点关注少数类的召回率和F1分数。解决思路有几种一是对少数类进行过采样或对多数类进行欠采样二是在训练时给少数类更高的权重三是调整分类阈值让模型更倾向于预测少数类。具体选哪种方法要看你的数据特点和业务需求。4.2 混淆矩阵行列顺序与预期不符MATLAB的confusionmat函数默认按照标签的字母顺序或数值顺序排列类别。如果你的类别标签是字符串可能会得到和预期不一样的顺序。比如“类别10”会排在“类别2”前面因为字符串比较是按字符逐个比较的。解决办法是在调用confusionmat和confusionchart时显式指定Order参数。我通常会把类别顺序定义成一个单独的变量在计算混淆矩阵和绘图时都引用这个变量保证一致性。% 定义统一的类别顺序 classOrder [类别A, 类别B, 类别C]; % 计算混淆矩阵时指定顺序 cm confusionmat(trueLabels, predictedLabels, Order, classOrder); % 绘图时也指定顺序 figure; confusionchart(trueLabels, predictedLabels, Order, classOrder);4.3 混淆矩阵图显示不全或标签重叠当类别数量较多时比如10类以上混淆矩阵图的标签可能会重叠导致看不清。这时候可以调整图的大小和字体。figure(Position, [100, 100, 900, 800]); % 设置图窗大小 h confusionchart(trueLabels, predictedLabels); h.FontSize 10; % 调整字体大小 h.XLabel 预测类别; h.YLabel 真实类别;如果类别特别多可以考虑只显示归一化后的结果或者把混淆矩阵导出为图片后再放大查看。我在处理一个20分类的任务时直接把混淆矩阵导出成PDF然后在PDF阅读器里放大看比在MATLAB图窗里操作方便得多。4.4 常见问题速查表问题现象可能原因排查方法解决思路准确率高但模型实际效果差类别不平衡查看少数类的召回率过采样、欠采样或调整权重混淆矩阵行列顺序不对默认排序规则与预期不符检查类别标签的排序显式指定Order参数FP和FN搞反行列含义理解错误确认行真实、列预测在代码中加注释明确标注混淆矩阵图标签重叠类别数量过多查看图窗大小和字体调整图窗尺寸和字体大小某两类之间混淆严重特征区分度不足查看非对角线元素增加特征或调整模型结构避坑技巧在分析混淆矩阵时我习惯先把矩阵转换成百分比形式每一行除以该行的总和这样能更直观地看出每一类的分类准确率。MATLAB的confusionchart函数支持Normalization参数直接设置成row-normalized就行不用手动计算。5. 混淆矩阵在实际项目中的应用经验5.1 用混淆矩阵指导特征工程混淆矩阵不仅能评价模型还能指导特征工程的方向。如果混淆矩阵显示类别A和类别B之间混淆严重说明当前的特征不足以区分这两类。这时候可以考虑增加新的特征或者对现有特征进行变换。我在一个文本分类项目里遇到过类似的情况。混淆矩阵显示两个类别的F1分数只有0.6左右其他类别都在0.9以上。后来分析发现这两个类别的文本在词频分布上非常接近现有的TF-IDF特征区分度不够。加入词性标注特征和句法结构特征之后这两个类别的F1分数提升到了0.78。5.2 多分类任务中的混淆矩阵分析策略多分类任务的混淆矩阵比二分类复杂得多N个类别就有N乘N个格子。如果逐个格子分析很容易迷失在细节里。我的策略是分三步走第一步看对角线。对角线上的数字越大越好如果某个类别的对角线数字明显偏小说明这个类别的召回率有问题。第二步看非对角线上的最大值。找到混淆矩阵中非对角线上的最大元素看看是哪两类之间混淆最严重。这通常是模型最需要改进的地方。第三步看行和列的分布。如果某一列的非对角线元素普遍偏大说明这个类别的精确率有问题很多其他类别的样本被误判成了这一类。如果某一行的非对角线元素普遍偏大说明这个类别的召回率有问题这个类别的样本被大量误判成了其他类别。5.3 混淆矩阵与ROC曲线的配合使用混淆矩阵给出的是某个特定阈值下的分类结果而ROC曲线展示的是模型在所有可能阈值下的表现。两者配合使用能更全面地评估模型性能。在实际项目中我通常会先画ROC曲线确定一个合适的阈值然后在这个阈值下计算混淆矩阵分析具体的分类错误情况。如果ROC曲线的AUC很高但混淆矩阵显示某些类别的F1分数很低说明模型整体排序能力不错但在某些类别上的区分能力不足需要针对性地优化。MATLAB的perfcurve函数可以计算ROC曲线和AUC值配合混淆矩阵使用效果很好。不过ROC曲线主要用于二分类多分类任务需要用到宏平均或微平均的ROC曲线实现起来稍微复杂一些。5.4 混淆矩阵在模型迭代中的跟踪作用在模型迭代过程中混淆矩阵是一个很好的跟踪工具。每次调整模型参数或特征之后重新计算混淆矩阵对比前后变化能清楚地看到改进效果。我习惯把每次实验的混淆矩阵保存下来用统一的格式命名比如cm_v1.mat、cm_v2.mat。然后在分析阶段批量加载对比不同版本之间的差异。这样能快速定位哪些改动是有效的哪些改动没有带来提升。% 保存混淆矩阵 save(cm_v1.mat, cm); % 加载并对比 load(cm_v1.mat); cm_v1 cm; load(cm_v2.mat); cm_v2 cm; % 计算差异 diffMatrix cm_v2 - cm_v1; figure; confusionchart(diffMatrix); title(混淆矩阵变化v2 - v1);这个差异矩阵能直观地显示哪些格子的样本数增加了哪些减少了。对角线上的正数表示正确分类的样本增加了负数表示减少了。非对角线上的变化则反映了错误分类模式的变化。6. 几个容易被忽略的细节6.1 标签类型的一致性MATLAB的confusionmat函数对标签类型有要求真实标签和预测标签必须是相同的数据类型。如果一个是字符串数组另一个是字符向量元胞数组函数会报错。我在实际使用中遇到过好几次这个问题后来养成了一个习惯在调用confusionmat之前先用string函数把两个标签数组都转成字符串数组。% 统一转换为字符串数组 trueLabels string(trueLabels); predictedLabels string(predictedLabels); % 然后再计算混淆矩阵 cm confusionmat(trueLabels, predictedLabels);6.2 缺失值的处理如果标签数据中存在缺失值NaN或缺失字符串confusionmat函数会直接报错。在计算混淆矩阵之前需要先检查并处理缺失值。% 检查缺失值 if any(ismissing(trueLabels)) || any(ismissing(predictedLabels)) warning(标签中存在缺失值将删除对应样本); validIdx ~ismissing(trueLabels) ~ismissing(predictedLabels); trueLabels trueLabels(validIdx); predictedLabels predictedLabels(validIdx); end注意删除缺失值样本会改变数据集的大小可能影响后续的指标计算。如果缺失值比例较高建议先分析缺失原因再决定是删除还是填充。6.3 混淆矩阵的归一化方式选择confusionchart函数支持三种归一化方式absolute默认显示原始计数、row-normalized按行归一化、column-normalized按列归一化。选哪种方式取决于你想看什么。想看每一类的召回率用行归一化想看每一类的精确率用列归一化想看原始样本分布用绝对计数。我通常会把三种方式都画一遍从不同角度分析模型的表现。6.4 混淆矩阵的导出与报告生成在项目汇报中经常需要把混淆矩阵导出成图片或PDF。MATLAB的confusionchart函数返回的是一个对象可以用saveas或exportgraphics函数导出。% 创建混淆矩阵图 h confusionchart(trueLabels, predictedLabels); % 导出为PNG图片 saveas(h, confusion_matrix.png); % 导出为PDF矢量图放大不失真 exportgraphics(h, confusion_matrix.pdf, ContentType, vector);导出为PDF的好处是矢量图放大后不会模糊适合放在报告或论文里。如果只是快速查看PNG格式就够了。6.5 混淆矩阵与分类报告的结合使用最后再分享一个小技巧把混淆矩阵和分类报告放在一起看能更快地定位问题。混淆矩阵告诉你“错在哪里”分类报告告诉你“错了多少”。两者结合就能形成完整的分析闭环。我通常会在代码里同时输出这两个结果然后在分析时对照着看。如果分类报告显示某个类别的F1分数很低就去混淆矩阵里找这个类别的行和列看看它主要和哪些类别混淆。这样能快速定位问题的根源而不是盲目地调参。% 同时输出混淆矩阵和分类报告 figure; confusionchart(trueLabels, predictedLabels, ... Normalization, row-normalized, ... Title, 混淆矩阵行归一化); report classificationReport(trueLabels, predictedLabels); disp(report);这套组合拳我在多个项目中都用过效果很稳定。尤其是当模型效果不理想时先看混淆矩阵和分类报告往往能比盲目调参更快地找到改进方向。