1. 这段核心代码解决的是哪一类问题
最近找我聊 MATLAB 的人里,好几位都是同一个状态:手里有个data.xlsx,打开一看几十列,第一列到倒数第二列是输入参数,最后一列是输出结果,任务就是用前面这些列把最后那一列预测出来。标题里那句"直接打开 MATLAB,新建脚本把这段代码贴进去",我太熟悉了,因为我带新人也是这么带的——先给一段能跑的完整脚本,跑通了再讲为什么,最后才谈怎么改。
这种"特征表 + 标签列"的结构,是表格型监督学习最通用的数据组织方式。它不只是出现在课程作业里,很多工程场景交付给你的数据也是这么排的:设备老化测试记录(温度、电流、电压、运行时长做输入,剩余寿命做输出)、量化策略研究(因子列做输入,下一期收益做输出)、图像特征表格(提取好的特征描述子做输入,类别标签做输出)。数据来源五花八门,但落到 Excel 里之后,结构完全一样。所以学会处理这种格式,等于学会了 MATLAB 里 80% 的表格建模套路。
我这里先给你一版可以直接复制运行的"核心代码"。它做的事情很明确:读取 Excel → 分离特征和标签 → 处理缺失值 → 划分训练集测试集 → 归一化 → 训练线性回归基线模型 → 输出 R² 和 RMSE → 画预测对比图。
%% 核心代码:data.xlsx 数据读取与回归建模通用流程 clear; clc; close all; %% 第1步:读取桌面上的 data.xlsx % 方法A:如果脚本所在文件夹就是当前目录,直接写文件名 % raw = readtable('data.xlsx'); % 方法B:最省事,弹窗选文件,路径写死不容易错 [file, path] = uigetfile('*.xlsx', '请选择 data.xlsx'); if isequal(file, 0) error('没有选择文件,脚本终止'); end raw = readtable(fullfile(path, file)); % 快速预览前5行和变量名 disp('数据预览:'); disp(raw(1:5, :)); disp('变量列表:'); disp(raw.Properties.VariableNames); %% 第2步:分离输入参数与输出结果 % 第一列到倒数第二列是输入参数,最后一列是输出结果 X = raw{:, 1:end-1}; % 特征矩阵 y = raw{:, end}; % 目标向量 %% 第3步:缺失值处理,直接删除含NaN的行 if any(isnan(X(:))) || any(isnan(y)) fprintf('发现缺失值,正在删除含有NaN的行...\n'); valid = ~any(isnan(X), 2) & ~isnan(y); X = X(valid, :); y = y(valid, :); end %% 第4步:训练集/测试集划分,先划分再归一化 rng(42); cv = cvpartition(size(X, 1), 'HoldOut', 0.2); idxTrain = training(cv); idxTest = test(cv); XTrain = X(idxTrain, :); yTrain = y(idxTrain, :); XTest = X(idxTest, :); yTest = y(idxTest, :); % 用训练集的均值/标准差做归一化,防止数据泄露 mu = mean(XTrain, 1); sigma = std(XTrain, 0, 1); sigma(sigma == 0) = 1; % 防止常数列除零 XTrain = (XTrain - mu) ./ sigma; XTest = (XTest - mu) ./ sigma; %% 第5步:训练模型,先用线性回归做基线 mdl = fitlm(XTrain, yTrain); pred = predict(mdl, XTest); %% 第6步:评估 SS_res = sum((yTest - pred).^2); SS_tot = sum((yTest - mean(yTest)).^2); R2 = 1 - SS_res / SS_tot; RMSE = sqrt(mean((yTest - pred).^2)); fprintf('线性回归基线结果:R² = %.4f,RMSE = %.4f\n', R2, RMSE); %% 第7步:画图 figure; plot(yTest, pred, 'o', 'LineWidth', 1); hold on; plot([min(yTest), max(yTest)], [min(yTest), max(yTest)], 'r--', 'LineWidth', 1.5); xlabel('真实值'); ylabel('预测值'); title(sprintf('预测值与真实值对比 (R²=%.4f)', R2)); grid on;跑完之后,你手里会多三样东西:控制台里两个数字(R² 和 RMSE),一张预测值对真实值的散点图,还有一个训练好的线性模型对象mdl。注意cvpartition、fitlm都属于 Statistics and Machine Learning Toolbox,没装这个工具箱的话会直接报错。另外fitrnet这类神经网络回归器要 R2021a 之后才有,下面第 4 章我会再提。
2. 逐行拆解:为什么是这个写法,而不是别的写法
新手最容易犯的错不是模型不会选,而是代码"差一点就跑不通"。这一章我把核心代码里几个关键写法逐个拆开,讲清楚每个选择的理由。
2.1 读取文件:三种方式,我建议你用弹窗
readtable('data.xlsx')看着最省事,但它有个前提:MATLAB 当前工作目录必须正好就是data.xlsx所在目录。很多人把脚本和 Excel 都放在桌面,然后直接运行,报错Unable to find file or directory。原因很常见:MATLAB 打开后的当前路径往往是安装目录或上一次保存的位置,脚本在桌面不代表当前目录在桌面。
我处理这类问题有三个办法,从麻烦到省事排列:
% 方式1:先 cd 到桌面(要改用户名) cd('C:\Users\你的用户名\Desktop'); % 方式2:写绝对路径 raw = readtable('C:\Users\你的用户名\Desktop\data.xlsx'); % 方式3:弹窗选文件(我最推荐) [file, path] = uigetfile('*.xlsx', '请选择 data.xlsx'); raw = readtable(fullfile(path, file));弹窗方案的优点是:文件放哪都行,你只需要在弹出窗口里点一下,路径交给系统拼接,永远不会有手滑打错字的问题。对新手来说,这是最不容易翻车的一条路。
2.2 raw{:, 1:end-1}:表格索引的精确语义
readtable读进来的是一个 table 对象,不是普通矩阵。table 有两种索引方式:
raw(:, 1:end-1):出来的还是 table,适合继续按列名操作;raw{:, 1:end-1}:把对应位置的数据"掏"出来,拼成普通数值矩阵,适合直接喂给fitlm、fitrtree这类函数。
end在 MATLAB 里会自动展开成当前维度的最大索引。1:end-1的意思就是"第一列到倒数第二列",正好对应题目里"第一列到倒数第二列是输入参数"这句描述。好处是你完全不需要关心表格一共有多少列,每次拿到不同宽度的data.xlsx都是同一段代码。
这个写法有个前提:被{:, ...}掏出来的所有列必须是同一种类型(数值或逻辑)。如果中间混了一列文本(比如设备编号A001、A002),MATLAB 会直接报"Cannot concatenate table variable"之类的错。怎么快速判断哪些列不能参与建模,我在第 3 章会给你一套完整排查链路。
2.3 先划分数据集再归一化,这是个不能妥协的顺序
很多教程会先写X = zscore(X),然后再切训练测试集。这在严格意义上是有问题的:zscore用的是整个数据集的均值和标准差,也就是说测试集的统计信息已经提前混进了训练过程。这叫数据泄露,后患是你在测试集上看到的指标会虚高,真实部署时模型性能明显下降。
理解这件事最好的类比是考试:你提前偷看了测试卷的答案,再用这套答案反过来衡量自己考得多好,分数当然漂亮,但到了真正上考场就露馅。正确做法是只从训练集算均值mu和标准差sigma,再拿这两个数去归一化训练集和测试集。第 1 章的代码里就是这么写的:
mu = mean(XTrain, 1); sigma = std(XTrain, 0, 1); sigma(sigma == 0) = 1; % 有些列是常数,标准差为0,除零会得到NaN XTrain = (XTrain - mu) ./ sigma; XTest = (XTest - mu) ./ sigma;那行sigma(sigma == 0) = 1是很多人没想到的:如果某一列全是同一个值(比如所有样本的温度都设成了 25),标准差是 0,归一化会产生无穷大和 NaN。把它临时改成 1,这列数据就会被归一化成全 0,模型等于自动忽略了这个无信息列。
2.4 为什么先用 fitlm 做基线
我见过不少新手一上来就上神经网络,结果训练半天、过拟合满天飞、最后报告里连一个能解释的结论都写不出来。线性回归fitlm有四个不可替代的好处:快(几千行数据瞬间完成)、稳(几乎没有调参需求)、可解释(mdl.Coefficients直接给出每个特征的系数和 p 值)、自带诊断图(直接plot(mdl)就可以看残差分布)。
更关键的是,线性模型的结果本身就是一把"尺子"。如果线性模型 R² 已经 0.9,说明问题很可能是线性可解的,你只需要在这个基础上微调;如果线性模型 R² 只有 0.3,说明数据里存在明显的非线性关系,这时候再上树模型或神经网络才有意义。反过来,数据明明接近线性,你却直接上了复杂模型,最后精度没提升多少,解释性全丢了,报告很难写。
另外如果你拿到的是分类问题(最后一列是 0/1 标签),这整套流程不需要推倒重来,只要把第 5 步的fitlm换成fitctree、fitcensemble,评估指标从 R² 换成准确率即可。
3. data.xlsx 的四个高频翻车现场与排查链路
代码给了之后,我收到最多的反馈不是"模型效果不好",而是"一运行就报错,怎么办"。这里我把最常见的问题和排查思路整理成一张表,按出现频率排序。
| 报错症状 | 根本原因 | 最快修复 |
|---|---|---|
Unable to find file or directory | 当前路径不在 Excel 所在目录 | 用uigetfile弹窗选文件,或用绝对路径 |
Cannot concatenate table variable | 特征列里有文本/逻辑列,raw{:, 1:end-1}拼不出数值矩阵 | 只保留数值列,或把文本列编码成数值 |
fitlm报错:要求 Y 为数值向量 | 最后一列是文本标签(如"好/坏"),被读成了 categorical | 用double()或grp2idx()转换标签 |
| 模型能跑但指标是 NaN | 数据里有缺失值,某些模型(如神经网络)不接受 NaN | 先rmmissing或fillmissing,再建模 |
具体排查时,我建议按这个链路一步步走,不要凭感觉猜:第一步,disp(raw(1:5, :))看表格长什么样;第二步,class(raw{:, 1})确认第一列的数据类型;第三步,varfun(@isnumeric, raw)把每一列是不是数值一次性列出来;第四步,根据结果决定删除文本列,还是用double、str2double、grp2idx做类型转换。
我举个实际遇到的例子。有个人跑我的核心代码,报错Cannot concatenate table variable。他data.xlsx里第二列是"设备编号",内容是A001, A002, ...。我的处理办法是先把编号从特征里剔除:
% 删除第2列文本列,剩下的再进入建模流程 raw{:, '设备编号'} = []; % 按列名删 % 或者按位置删 % raw(:, 2) = [];如果文本列本身是有序等级(比如老化等级 A/B/C),可以用grp2idx转成 1、2、3 参与建模;如果是无意义的 ID,直接删掉最干净。
还有一个很隐蔽的坑:表头。如果data.xlsx第一行不是变量名,而是直接从数据开始的,readtable会自动把第一行当成列名,导致你的特征矩阵少了一行数据。这种情况要显式告诉它没有表头:
raw = readtable('data.xlsx', 'ReadVariableNames', false);读进来之后,第一列到倒数第二列的处理逻辑不变,只是列名会变成Var1, Var2, ...,后期操作稍微注意别写错列名就行。
4. 从"能跑"到"能比":多算法评估与结果交付
第 1 章的代码只训练了一个线性回归,作为作业和基线足够。但如果你做的是课程设计、毕业设计或者稍微正式一点的项目,评审基本都会问一句:"你有没有对比过其他算法?为什么选这个?"所以这一章我讲怎么在同一份数据上快速比较多个模型,以及怎么把结果交付得干干净净。
4.1 把评估流程扩展成多算法版本
最简单的做法是把第 1 章的代码稍作修改,把第 5 到 7 步封装成一个多模型对比脚本。这里我给一个可直接套用的版本,它接管了归一化之后的XTrain, yTrain, XTest, yTest:
%% 多算法对比:线性回归 / 回归树 / 集成提升树 / 神经网络 rng(42); modelNames = {'fitlm', 'fitrtree', 'fitrensemble', 'fitrnet'}; predCell = cell(1, 4); % 1) 线性回归 mdlLinear = fitlm(XTrain, yTrain); predCell{1} = predict(mdlLinear, XTest); % 2) 回归树 mdlTree = fitrtree(XTrain, yTrain); predCell{2} = predict(mdlTree, XTest); % 3) 集成提升树(LSBoost,100轮) mdlEns = fitrensemble(XTrain, yTrain, ... 'Method', 'LSBoost', 'NumLearningCycles', 100); predCell{3} = predict(mdlEns, XTest); % 4) 神经网络(需要 R2021a+) if exist('fitrnet', 'file') mdlNet = fitrnet(XTrain, yTrain, 'LayerSizes', [16 8], 'Activations', 'relu'); predCell{4} = predict(mdlNet, XTest); else predCell{4} = nan(size(yTest)); end % 统一输出评估指标 fprintf('%-16s %8s %8s\n', '模型', 'R²', 'RMSE'); for i = 1:4 p = predCell{i}; if all(isnan(p)) fprintf('%-16s 当前MATLAB版本不支持\n', modelNames{i}); continue; end R2 = 1 - sum((yTest - p).^2) / sum((yTest - mean(yTest)).^2); RMSE = sqrt(mean((yTest - p).^2)); fprintf('%-16s %8.4f %8.4f\n', modelNames{i}, R2, RMSE); endexist('fitrnet', 'file')这个判断很重要。fitrnet是 R2021a 才引入的,老版本根本没有这个函数,直接写会被 MATLAB 当成普通变量名解析然后报错。用exist先探一下,既保证脚本在任何版本都能跑,又能在老版本上优雅地跳过神经网络。
4.2 四个模型的选型对照
拿到对比结果后,怎么判断用哪个?我的习惯是看下面这张表:
| 模型 | 优势 | 短板 | 典型适用场景 |
|---|---|---|---|
fitlm | 快、可解释、有系数显著性 | 只能表达线性关系 | 基线对比、特征筛选、报告首选 |
fitrtree | 自动处理非线性、不要求归一化 | 单棵树精度有限、易过拟合 | 数据量中等、想快速看非线性趋势 |
fitrensemble | 精度高、抗过拟合能力强 | 计算慢、黑箱、较难解释 | 精度优先、数据量几千到几万行 |
fitrnet | 能拟合复杂非线性 | 需要归一化、小数据易过拟合 | 数据量足够、特征和输出关系复杂 |
我的建议是看结果说话:先跑对比脚本,如果fitlm和fitrensemble的 R² 差距不到 0.05,选线性回归,理由是可解释性完胜;如果集成树明显高出线性回归一大截,说明数据非线性强,报告里可以围绕这个差距展开分析,这是价值点。
4.3 结果导出:让交付对象直接看得懂
模型评估完之后,通常还要把测试集上的预测结果交出去。我一般会导出两个文件:
%% 导出预测结果和训练好的模型 outTable = table(yTest, predCell{3}, ... 'VariableNames', {'Actual', 'Predicted'}); writetable(outTable, '预测结果.xlsx'); save('finalModel.mat', 'mdlEns', 'mu', 'sigma');这里有两个经验。第一,导出的表格列名建议用英文,writetable写中文列名偶尔会在不同版本的 Excel 兼容性上出问题,英文名最稳,需要中文的话让交付方自己改表头。第二,一定要把归一化用的mu和sigma连同模型一起保存。因为模型是在归一化后的数据上训练的,以后来了一条新数据,必须先用同一套mu和sigma归一化再喂给predict,否则预测结果完全失真。这个细节我在很多项目里看到被漏掉,部署时才开始返工。
4.4 多算法融合和 OOP 架构的思路延伸
现在热门的课程设计方向里,很多是"基于 MATLAB OOP 架构的多算法融合系统"。听着唬人,其实拆开就三层:算法层、融合层、调用层。上面这段对比脚本就是算法层的雏形。融合层最简单可靠的做法是对多个模型的预测值做加权平均,权重用验证集上的 RMSE 倒数来定;更正规的做法是 Stacking——把各模型的预测值当成新特征,再训练一个上层回归器。
项目一旦变大,脚本会越堆越长,这时候建议引入classdef,把每个算法封装成一个类:
classdef BaseModel properties Model Name end methods (Abstract) train(obj, X, y) pred = predict(obj, Xnew) end end每个具体模型继承这个基类并实现train和predict,融合器则负责组合多个子类对象。这样的设计让新增算法、替换核心、单元验证都变得清晰,也是这类课程设计最容易拿高分的骨架。而不管架构多复杂,数据入口基本都是我们第 1 章那段readtable + raw{:, 1:end-1}的核心代码。
5. 一些只有自己跑过才知道的细节
代码能跑通之后,真正拉开差距的是一些细节习惯。我把自己踩过的坑和经验集中写在这一章,你照着做可以少走很多弯路。
第一,不要用input做变量名。MATLAB 自带的input()是从命令行读取用户输入的函数,你把变量命名为input之后,这个函数就被你屏蔽了,后面想用的时候会报错或者行为诡异。同理,data这个名字虽然不冲突,但太宽泛,不好辨认,我更推荐raw、X、y这样的风格。
第二,clear all这个命令要慎用。很多教程开头写clear all; close all; clc,但clear all会把断点、全局变量、Java 对象引用一起清掉,调试多轮之后非常不方便。我一般只写clear; close all; clc,必要时用clearvars -except mu sigma保留关键变量。
第三,调试时先埋一句dbstop if error。脚本很长的时候,报错信息往往只告诉你第几行出错,但原因链可能很复杂。在代码开头写上dbstop if error,出错时 MATLAB 会停在出错那一行,你可以在命令行直接查看每个变量的当前值,整个过程比反复加disp快得多。
第四,R² 不是万能的。线性模型跑完,建议顺手执行一次plot(mdl),它会生成四张诊断图,重点是残差图。如果残差明显呈弯曲分布,说明线性模型的假设不成立,这时候 R² 再高也是假象。非线性模型没有这套自动诊断,至少也要画一张"预测值对真实值"的散点图,看它是不是沿着 45 度线分布。
第五,小数据集不要盲目上神经网络。几百行数据训练fitrnet,几乎一定会过拟合,训练集美如画,测试集稀烂。遇到小数据集,优先选fitrensemble里的 Bag 方法,它抗过拟合的能力比单棵树强很多。
我自己的习惯是:每个新项目都从第 1 章那段核心代码起步,先拿线性回归摸清数据的底,再逐步升级成第 4 章的对比框架,最后根据场景决定解释优先还是精度优先。这套流程本身不复杂,但每一步都有它存在的理由,跑顺了之后,不管是课程作业、毕业设计,还是工程上的表格建模需求,你都能在一间会议室里给出一个漂亮的起点。