ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MATLAB模式识别算法实战:从特征工程到分类器与降维评估

MATLAB模式识别算法实战:从特征工程到分类器与降维评估 简介这是《模式识别与智能计算——MATLAB技术实现》第3版的完整PDF融合统计学、机器学习、神经网络、数据挖掘与群体智能等思想聚焦模式识别算法从理论到MATLAB落地的全过程适合高校师生及相关工程技术人员参考。全书共14章覆盖特征选择与优化、相似性测度、贝叶斯分类器、判别函数以及BP、径向基、自组织竞争、概率、对向传播等神经网络分类器和决策树、粗糙集设计聚类部分包括基本聚类、模糊聚类、禁忌搜索、遗传算法、蚁群与粒子群聚类分析体系非常完整。书中以手写数字识别为实例给出实现步骤、核心MATLAB代码及多种算法的效果对比便于读者快速将理论转化为可运行程序实践参考价值突出。资源为单个PDF文件大小约17.87MB已有311人学习下载适合作为研究生教材、教学参考书或算法实践案头资料。1. 模式识别算法MATLAB实现不是调包是想清楚决策面在哪儿当你手里已经有一套带标签的样本目标只是让模型学会区分新样本的类别时模式识别算法MATLAB实现比大多数人想象中要直接得多。它不等于你非得从零推导二次判别函数的解析解而是指一条能复现、能调试、能交付的完整路径从特征矩阵出发选一个合适的分类器通过交叉验证调参最后用测试集给效果下结论。我见过实验室同学一上来就打算用PyTorch做图像分类结果导师给的基线代码全是MATLAB脚本特征也已经是提取好的数值矩阵——这时候MATLAB的统计机器学习工具箱反而是最快的。这条路径适合两类人一类是要快速出基线结果的研究生另一类是在工业现场拿到带标签数据、想把识别逻辑做成可交付模块的工程师。下文所有代码都围绕“特征矩阵 分类器 交叉验证”这条主线展开每段都能直接抄走。2. 数据准备与特征矩阵模式识别算法在MATLAB里跑起来的第一步2.1 加载内置数据集与自定义数据从iris到CSV文件我习惯用fisheriris讲通全流程因为特征矩阵和标签都是内置的能排除读取格式带来的干扰。但如果要落地到实际项目你手上更多是散落的CSV或Excel文件所以这一节把两种入口都覆盖到。% 方式一加载MATLAB内置鸢尾花数据集 % meas 是150x4的数值矩阵species 是150x1的分类变量 load fisheriris; % 把分类变量转成数值标签便于后续算准确率、画混淆矩阵 Y double(categorical(species)); % 类别1/2/3 % 方式二从CSV加载你自己的数据 % data readtable(your_data.csv); % X data{:, 1:end-1}; % 前若干列是特征 % Y data{:, end}; % 最后一列是标签 % 固定随机种子让数据划分可复现 rng(0); % 按3/7比例划分训练集与测试集 cv cvpartition(Y, HoldOut, 0.3); idxTrain training(cv); idxTest test(cv); X_train meas(idxTrain, :); Y_train Y(idxTrain); X_test meas(idxTest, :); Y_test Y(idxTest);最后三行是关键取数操作。cvpartition返回一个cvpartition对象training和test两个方法分别返回逻辑索引向量直接用矩阵下标就能把训练集和测试集分开。这样做的价值在于训练集和测试集完全不重叠且每次运行结果一致不会出现“这次跑90分下次跑70分”的复现困境。参数说明HoldOut后面的0.3表示30%样本留作测试剩下70%参与训练rng(0)不是随便写的它把随机数生成器重置到固定状态保证cvpartition每次切分结果一致。如果你把rng这行删掉每次运行划分都会变调参时根本无法判断效果变化来自参数还是来自数据划分。2.2 归一化与数据划分为什么训练集和测试集要分开处理所有基于距离的分类器KNN、SVM的某些核函数都默认每个特征等权。如果特征A的量纲是0.01到1特征B的量纲是1000到5000距离计算会被B完全主导特征A等同白给。归一化是解决这个问题的常规手段但放在什么位置直接决定你的评估结果是不是虚高。% 错误示范先合并所有数据做zscore再划分 % zscore操作了包含测试集在内的全部数据统计量已经泄露 % 推荐写法只在训练集上估计mu和sigma mu mean(X_train, 1); sigma std(X_train, 0, 1); X_train_norm (X_train - mu) ./ sigma; X_test_norm (X_test - mu) ./ sigma;每一行都不能省略mean(X_train, 1)是按列求均值std(X_train, 0, 1)是按列求标准差第二个参数0表示除以n-1而不是n。先算统计量再用它变换测试集是为了保证测试集在训练阶段不被“看见”。如果你图省事拿全量数据跑zscore(meas)再划分测试集的分布信息已经通过统计量进入缩放过程分类器在测试集上的表现会系统性偏高这个偏差在小样本场景下足以让你把一个不合适的模型误判为可用。还有一类特征是类别型或二值型比如性别、是否故障这类特征不适合做zscore保留0/1本身更合理。混合类型数据可以只对连续特征归一化离散特征保持原值不要把两者混在一个矩阵里统一处理。2.3 用散点矩阵图和分类散点图先探一探数据的可分性在跑任何分类器之前先画图这一步能省掉大量无用功。模式识别算法的第一步从来不是选模型而是确认数据在特征空间里到底长什么样。figure; gplotmatrix(meas, [], species, [krb], [], [], on, [], [], []); title(原始特征两两散点图);gplotmatrix会把4个特征两两组合铺开成4x4的子图每张子图展示某两个特征维度下的类别分布[krb]指定三种颜色on开启对角线上的直方图。我通常先花三分钟看这张图如果某个组合里三个类别的点已经明显分成几团说明线性分类器就有机会如果所有子图都糊成一坨说明原始特征的可分性不好这时候应该先考虑特征降维或换特征而不是盲目上复杂分类器。iris数据集在花瓣长度和花瓣宽度两个维度上天然可分所以后续每个分类器都能跑出不错的结果。真正容易翻车的是那些高维稀疏特征比如文本TF-IDF矩阵或one-hot编码的类别特征这种数据画散点图几乎永远看不出规律需要靠降维手段把结构压缩到可视空间。下一节先落三个最常用的分类器然后再讲降维怎么跟它们串联。3. 实现三种经典分类器贝叶斯、KNN、SVM的MATLAB代码与参数3.1 朴素贝叶斯分类器fitcnb的分布假设与后验概率输出朴素贝叶斯的工作机制是假设特征在类别给定的条件下相互独立然后通过贝叶斯公式计算后验概率。它在特征维数高但样本量不大的场景下往往比复杂模型更稳因为它要估计的参数少不容易过拟合。MATLAB里一句话就能建好模型% 训练朴素贝叶斯分类器 mdlNB fitcnb(X_train_norm, Y_train, ... DistributionNames, kernel, ... % 用核密度估计不假设高斯分布 Prior, empirical); % 先验概率按训练集各类别比例计算 % 预测并输出后验概率 [predNB, posterior] predict(mdlNB, X_test_norm); % 计算准确率 accNB sum(predNB Y_test) / numel(Y_test); fprintf(朴素贝叶斯准确率: %.2f%%\n, accNB * 100);DistributionNames有normal和kernel两个常用选项。默认是normal假设每个特征在各类别内服从高斯分布如果你的数据分布明显不对称或有多峰kernel会用核密度估计去拟合效果通常更好但计算量也更大。Prior默认就是empirical即按训练集中各类别占比作为先验概率。这里有一个值得注意的细节predict返回的第二输出就是后验概率你不仅能拿到类别标签还能看到模型有多“确信”这个判断。实际项目里我常常把后验概率低于0.6的样本单独挑出来归为“不确定类”交给人工复核。比直接输出一个标签更有工程价值。朴素贝叶斯不是黑匣子它的决策边界可以被清晰地表达为各类别分布的交界处。如果某个特征维度在训练集上只有一种取值fitcnb会报“类内方差为零”的警告解决办法是在该维度上加一点点噪声或者直接用DistributionNames, kernel回避高斯假设。3.2 KNN最近邻fitcknn的K值选择与KDTree切换KNN的核心思路直接到近乎朴素新样本的类别由特征空间里距离它最近的K个已知样本投票决定。没有显式训练过程树结构本身就是为了加速查找。MATLAB实现如下% 训练KNN分类器K5欧氏距离 mdlKNN fitcknn(X_train_norm, Y_train, ... NumNeighbors, 5, ... Distance, euclidean, ... Standardize, false); % 注意数据已经手动归一化这里不能重复标准化 % 做5折交叉验证评估K5时的泛化能力 cvmdlKNN crossval(mdlKNN, KFold, 5); knnLoss kfoldLoss(cvmdlKNN); fprintf(KNN 5折交叉验证误差: %.2f%%\n, knnLoss * 100); % 用测试集最终验证 predKNN predict(mdlKNN, X_test_norm); accKNN sum(predKNN Y_test) / numel(Y_test); fprintf(KNN测试集准确率: %.2f%%\n, accKNN * 100);NumNeighbors是K值Distance可选euclidean、cityblock、chebychev等。K值的选择没有理论最优解3到10之间比较常见K越小决策边界越碎、越容易过拟合K越大边界越平滑、但可能把邻近的异类样本也卷进来。Standardize参数一旦设了truefitcknn会在训练前自动做标准化但我们前面已经手动归一化过这里必须设false否则等于做了两次尺度变换。crossval加kfoldLoss是我最常用的快速评估组合。KFold, 5表示把训练集再切成5份轮流拿4份训练、1份验证最后返回平均误差。这一步比直接跳去测试集靠谱得多因为调参过程中反复使用测试集会让你在测试集上“记住答案”。KNN在样本量超过几万条时会遇到性能瓶颈默认的搜索方法会退化成穷举。此时可以显式指定搜索方法NSMethod, kdtree但KDTree在高维数据下效率会急剧下降一般超过20维我宁愿用exhaustive配合Distance设为快速计算的度量或者干脆换SVM。3.3 支持向量机fitcsvm的核函数、BoxConstraint与多分类扩展SVM找的是让两类样本间隔最大的超平面在特征维数远大于样本数的时候尤其抗过拟合。fitcsvm本身只支持二分类多分类要用fitcecoc包装成一对其余one-vs-all的组合。下面是完整的二分类及多分类实现% 二分类示例把iris的类别1设为正类其余为负类 Y_bin double(Y_train 1); % 类别1为1类别2/3为0 mdlSVM fitcsvm(X_train_norm, Y_bin, ... KernelFunction, rbf, ... % 高斯核处理非线性边界 BoxConstraint, 1, ... % 松弛变量惩罚系数 KernelScale, auto); % 核宽度由算法自动估计 predSVM1 predict(mdlSVM, X_test_norm); accSVM1 sum(predSVM1 double(Y_test 1)) / numel(Y_test); fprintf(二分类SVM准确率: %.2f%%\n, accSVM1 * 100); % 多分类扩展fitcecoc自动做一对余组合 mdlSVM fitcecoc(X_train_norm, Y_train, ... Learners, templateSVM(KernelFunction, rbf, ... BoxConstraint, 1, ... KernelScale, auto)); predSVM predict(mdlSVM, X_test_norm); accSVM sum(predSVM Y_test) / numel(Y_test); fprintf(多分类SVM准确率: %.2f%%\n, accSVM * 100);BoxConstraint是误分类样本的惩罚力度值越大边界越硬越容易过拟合值越小边界越软容忍更多误分类但泛化能力可能更好。我一般在小数据集上从0.1开始调然后1、10、100各跑一遍看交叉验证误差变化趋势。KernelScale设auto时算法会根据样本之间的距离自动估计高斯核的宽度省去手动试探不同σ的麻烦。fitcecoc的Learners必须传一个模板而不是直接传fitcsvm对象templateSVM就是干这个事的。iris是三分类fitcecoc会创建3个二分类器每个负责区分“这一类”和“其余两类”最后通过投票决定预测标签。多分类的准确率比二分类略低是正常现象因为每个二分类器都面临类别不平衡的问题。如果你自己构造训练集尽量保证每个类别样本数量接近。核函数的选择上linear适合特征维数高、样本量大的场景训练快且可解释性强rbf适合特征维数中等、边界非线性的场景但它需要调的参数多一个训练时间也长。只从准确率上看不到可解释性差异但如果后续需要把模型部署到生产环境线性核的权重向量可以直接导出作为特征重要性参考RBF核就只能当黑匣子用。4. 特征降维与可视化PCA和LDA怎么选、怎么串联分类器4.1 主成分分析PCA累计方差贡献率与score矩阵的含义特征维数太高会让KNN这类基于距离的分类器效果滑坡也会让SVM训练时间暴涨。PCA是应用最广的无监督降维方法它只考虑数据本身的方差结构完全不看标签找到的方向是“数据方差最大”的主成分方向。% 在训练集上做主成分分析 % 注意只在训练集上估计主成分方向再变换测试集 [coeff, score, latent, ~, explained] pca(X_train_norm); % 查看累计方差贡献率决定保留几个主成分 cumsum(explained) % 按累计方差95%选取维数通常前2-3个主成分就够 X_train_pca score(:, 1:2); % 用训练集的主成分系数变换测试集 X_test_pca (X_test_norm - mean(X_train_norm, 1)) * coeff(:, 1:2);pca返回的五个输出依次是主成分系数矩阵coeff每列是一个主成分方向、投影后的得分score、各主成分方差latent、T²统计量和各主成分解释的方差百分比explained。cumsum(explained)就是你需要的累计贡献率比如结果显示[72.5, 94.3, 99.0, 100.0]那么保留前两个主成分就能覆盖94.3%的信息量。维数的选择没有绝对标准95%是一个常用阈值但不是真理信号与噪声接近时建议多保留一两个维度让分类器自己去降权。这里有一个我踩过的坑X_test_pca的变换必须用mean(X_train_norm, 1)和coeff而不是在测试集上单独跑一次pca。因为降维方向和数据均值都只能用训练集的信息来估计测试集的作用是验证一旦让它参与主成分估计同样会产生信息泄露。4.2 线性判别分析LDA用Fisher判据找到最具判别力的投影方向PCA不考虑类别标签所以在分类任务里PCA降维后的方向不一定是最能区分类别的方向。LDA正好反过来它的优化目标是让投影后类间散度尽量大、类内散度尽量小是有监督降维。对于三分类问题LDA最多能给出类别数减一个判别方向。MATLAB里除了fitcdiscr可以直接做分类也可以手动求解投影方向方便大家理解内部发生了什么% 手动求解LDA投影方向 classes unique(Y_train); % 初始化类内散度矩阵Sw和类间散度矩阵Sb Sw zeros(size(X_train_norm, 2)); Sb zeros(size(X_train_norm, 2)); % 全局均值 mu_all mean(X_train_norm, 1); for i 1:numel(classes) Xi X_train_norm(Y_train classes(i), :); % 当前类的所有样本 mu_i mean(Xi, 1); % 当前类均值 Sw Sw (Xi - mu_i) * (Xi - mu_i); % 累加类内散度 Sb Sb size(Xi, 1) * (mu_i - mu_all) * (mu_i - mu_all); % 累加类间散度 end % 求解广义特征值问题Sw^(-1) * Sb [V, D] eig(Sb, Sw); [d, idx] sort(diag(D), descend); V V(:, idx); % 按判别能力从大到小排列 % 取前两个判别方向做投影 W V(:, 1:2); X_train_lda X_train_norm * W; X_test_lda X_test_norm * W;eig(Sb, Sw)是LDA求解的核心它在找一组方向使投影后的类间散度除以类内散度达到最大。返回值D的对角线就是广义特征值特征值越大代表该方向的判别能力越强idx排序保证我们取到的是前两个最强方向。手动实现这个公式会暴露一个问题当特征维数大于样本数时Sw往往是奇异矩阵eig会报错。解决方法是给Sw加一个很小的正则项比如Sw Sw 0.001 * eye(size(Sw))或者改用fitcdiscr的DiscrimType, pseudoLinear选项自动处理奇异矩阵。4.3 把PCA与分类器串成完整流程训练集、验证集、测试集三段式降维做完以后必须把降维步骤和分类器打包成一个可复现的流程否则线上使用时会因为少一步变换而出错。下面的代码是完整串联示例% 第一阶段训练集内部交叉验证选参数 rng(0); % 固定随机种子 % 对训练集做PCA投影 [coeff, score, ~, ~, explained] pca(X_train_norm); % 以累计贡献率95%为阈值确定保留维数 cumVar cumsum(explained); numDim find(cumVar 95, 1, first); X_train_pca score(:, 1:numDim); % 用PCA降维后的训练集训练SVM并做5折交叉验证 mdl fitcecoc(X_train_pca, Y_train, ... Learners, templateSVM(KernelFunction, rbf, BoxConstraint, 1)); cvmdl crossval(mdl, KFold, 5); fprintf(训练集交叉验证误差: %.2f%%\n, kfoldLoss(cvmdl) * 100); % 第二阶段拿到最优模型后变换测试集做最终评估 X_test_pca (X_test_norm - mean(X_train_norm, 1)) * coeff(:, 1:numDim); pred predict(mdl, X_test_pca); acc sum(pred Y_test) / numel(Y_test); fprintf(测试集准确率: %.2f%%\n, acc * 100);这段代码把整条链路浓缩成了三个关键动作pca只跑在训练集上降维矩阵coeff从训练集产生交叉验证发生在降维之后保证调参对象就是最终部署的对象测试集的变换完全复用训练集的均值向量和主成分系数不引入任何测试集统计量。这个三段式流程训练-验证-测试对任何分类器都成立换成fitcknn或fitcnb只需替换中间那两行模型训练和预测代码降维部分完全不用动。关于PCA和LDA怎么选如果标签质量高LDA通常比PCA效果更好因为它从设计上就偏向“区分”而非“表示”如果标签可能有噪声或类别本身不均衡PCA更稳健毕竟它根本不需要标签信息只压缩结构。5. 模式识别算法MATLAB实现的避坑与常见问题排查5.1 现象分类器把所有样本都判为同一类准确率却异常高这在小数据集上非常容易遇到尤其是二分类且正类样本只占5%的时候。模型学会“偷懒”所有样本一律输出多数类准确率照样95%看起来很美实际毫无用处。原因在于多数分类器默认使用经验先验训练集中正类样本太少贝叶斯分类器计算后验概率时正类的先验乘出来始终小于负类SVM决策面也会偏向多数类一侧。解决方法是先看混淆矩阵而不是只看准确率。确认问题后给少数类加权重fitcsvm的Prior参数可以直接设[0.5, 0.5]强行平衡先验或者设置Cost矩阵让少数类误分类的代价更高。对于fitcnb指定Prior, [0.5, 0.5]同样有效。更早期的做法是手动对少数类做上采样但MATLAB内置参数已经能解决大部分场景不建议平白引入SMOTE等重采样逻辑。5.2 现象测试集准确率比交叉验证高出好几个百分点先别高兴交叉验证误差是用来估计模型在没见过数据上的表现的。如果测试集结果明显高于交叉验证通常是数据划分时出了问题。最常见的原因是归一化泄露先用zscore(meas)处理了全部数据再划分训练测试集或者用全体数据估计了PCA的主成分方向。两种操作都让测试集的信息以统计量形式进入了训练过程模型相当于提前“见过”了测试集的分布。另一种可能是随机划分刚好分到一组容易预测的测试样本小样本下尤其常见。解决方法是回到第2章和第4章的规范写法均值、标准差、PCA系数三个统计量全部只在训练集上估计测试集统一用训练集算好的统计量进行变换。要排查是否泄露可以把交叉验证去掉反复用不同随机种子跑50次看测试集准确率是否稳定在一个窄区间内。如果波动超过5个百分点多半是数据量太少而不是算法问题。5.3 现象KNN分类器在训练集上准确率很高测试集却表现拉胯KNN的训练阶段几乎不做任何泛化处理它记住的是训练样本本身的位置。当训练样本量少、特征维数高或者K值设得太小决策边界会过度贴合训练样本新样本只要稍有偏移就被分到错误类别。原因一是K太小比如K1决策边界把所有训练样本都圈成孤岛泛化能力极差原因二是维数灾难特征维数超过30维后样本间的距离趋于同化欧氏距离不再能反映真实的近邻关系原因三是特征没有归一化量纲大的维度主导了距离计算。解决方法是先归一化再对K值做5到20的网格搜索每次用5折交叉验证打分选交叉验证误差最小的K。特征维数过高时先做第4章的PCA/LDA降维把有效维度压到10维以内再上KNN。若降维后KNN仍然表现差果断换SVM或者随机森林不要在一个算法上死磕。5.4 现象交叉验证误差很低但模型对新数据预测一塌糊涂这是最隐蔽的坑原因是你在调参过程中反复使用交叉验证结果交叉验证本身变成了“训练集”的一部分。当你用网格搜索跑了几百组参数每次都挑交叉验证误差最低的那组实际上已经过拟合到验证集上跟直接用测试集调参没有任何区别。解决方法是把数据集切成三段训练集用于训练、验证集用于选参数、测试集只做最终一次评估。网格搜索和参数调整只在验证集上进行测试集严格保持“一次机会”原则。如果样本量实在不够分三份那就用嵌套交叉验证内层交叉验证选参数外层交叉验证评估泛化误差。虽然计算量翻了好几倍但得到的误差估计才真正可信。6. 进阶验证用混淆矩阵和ROC曲线给分类器做一次全面体检准确率只能告诉你一个笼统的比例真正上线前还是要做一次全面体检。混淆矩阵能告诉你错误都发生在哪两类之间ROC曲线能告诉你模型在不同决策阈值下的权衡。MATLAB里两行代码就能出结果。% 以KNN为例画混淆矩阵 figure; cm confusionchart(Y_test, predKNN); cm.Title KNN在测试集上的混淆矩阵; cm.RowSummary row-normalized; % 显示每行百分比便于看漏判 cm.ColumnSummary column-normalized; % 做ROC曲线以类别1为正类其余类别为负类 % 注意perfcurve需要后验概率作为打分KNN默认支持输出 [~, scoreKNN] predict(mdlKNN, X_test_norm); [rocX, rocY, ~, auc] perfcurve(Y_test, scoreKNN(:, 1), 1); figure; plot(rocX, rocY); xlabel(假正率); ylabel(真正率); title(sprintf(KNN ROC曲线AUC%.3f, auc));confusionchart直接接收真实标签和预测标签自动生成带颜色热力的矩阵。RowSummary设为row-normalized后每个单元格显示的是“该类别中实际有百分之多少被预测成了某类”可以快速定位哪些类别最容易相互混淆。如果发现类别2和类别3频繁互判说明这两个类别在特征空间里本来就很接近要么增加新特征区分它们要么在业务上接受两类的合并。perfcurve是画ROC的另一种路径它需要的是样本属于正类的概率而不是硬标签。KNN的predict返回两个输出第二输出就是后验概率取第一列作为“类别1的概率打分”。曲线越接近左上角AUC越接近1模型判别力越强。如果AUC在0.5附近徘徊说明该分类器在这个特征集上基本没有判别能力换什么参数都救不了回头重新构造特征才是正路。这套体检流程我也不是一开始就想通的。早年间我拿到80%的准确率就急着给导师报结论结果模型部署到新批次数据上直接被打回原形那个翻车场面至今记忆犹新。后来养成了固定习惯任何分类器跑完先看混淆矩阵的行归一化百分比再看AUC最后才写报告。这两步花不了两分钟却能挡住绝大多数过度乐观的误判。希望帮到你。本文还有配套的精品资源点击获取
返回列表