
用CNN做分类预测听起来是个颇为专业的活儿但实际动手时大家多半不会先去调网络结构而是先在数据组织上卡住12个特征对应4个类别数据明明都在表格里摆着卷积层却报错说“维度对不上”。这个问题我见过太多次尤其是用Matlab做深度学习的新手。Matlab的深度学习工具箱Deep Learning Toolbox和Python路线不太一样它对数据格式的要求更严格你得先把“样本、特征、标签”这三样东西按它的规则重新包装一遍模型才肯开工。这篇东西我打算从“怎么造模拟数据”讲起完整走一遍“造数据—组织格式—搭CNN—训练评估”的流程。适合那些刚接触CNN、手里还没有真实数据、想先跑通一套可用流程的朋友。模拟数据虽然简单但用来验证数据管线、排查格式问题比任何文档都靠谱。1. 动手前先理清楚CNN到底要接收什么格式的数据1.1 一张表格和一“张”图片的差别先明确概念CNN传统上吃的是图像图像在Matlab里就是一个H×W×C的三维数组。我们拿到的表格数据是N个样本×12个特征看起来是二维的。要让CNN接手就得把每个样本“伪装”成图像那样的小立方体。具体到12个特征就是把每个样本组织成12×1×1的数组高度12、宽度1、通道1。如果你把数据理解成一个长度为12的一维信号这个形式就自然了。这里就有个初学者容易绕晕的点既然每个样本变成了“图像”那整个训练集的输入就不能再是一个矩阵了。Matlab的trainNetwork要求输入数据是cell数组每个cell放一个观测。所以你得把一个N×12的矩阵拆成N个12×1的小数组再用cell把它们装起来。这个转换过程不复杂但方向一旦搞反后面所有层都会跟着报错。等会到实操部分我会专门演示这个操作。1.2 标签为什么要变成categorical分类任务的标签在Matlab里必须是categorical类型不是double也不是字符串数组。如果用1、2、3、4这样的数字当标签trainNetwork会直接报错或者莫名其妙地把它当作回归问题处理。把数字标签转成categorical很简单Y categorical(Y, 1:4, {类别A,类别B,类别C,类别D});这样写还有个好处后面画混淆矩阵时图里显示的是有意义的类别名而不是干巴巴的数字1、2、3、4。如果你用的是字符串数组比如{A,B,C,D}trainNetwork有时能自动转但我从不让它自动转因为一旦类别顺序不是你想要的后面所有评估都会混乱。手动指定数字到名称的映射是最稳妥的。1.3 别被“图像”思维带偏这是1D卷积12个特征对应4类这个规模其实用不着上二维卷积。二维卷积最适合的是空间上二维分布的数据比如图像我们这只是“一排12个数字”一维卷积就够。在Matlab里对应的层是convolution1dLayer。它的卷积核是在长度方向上滑动的能够提取相邻特征之间的局部组合模式——比如第3个特征和第4个特征一起变大时才属于某一类这种组合模式一维卷积是能学到的。为什么要强调这一点因为很多人一听到CNN下意识就去找convolution2dLayer然后发现输入尺寸怎么都对不上。12个特征转成12×1×1之后你拿一个二维卷积核去扫一个宽度为1的矩阵能扫的地方极其有限这本质上就是把问题复杂化了。选卷积层之前得先想明白这个前提否则后面全是坑。2. 用Matlab造一套有内在规律的模拟数据2.1 为什么先造数据而不是直接上真实数据当有人问我“CNN怎么开始练”我通常建议先用模拟数据把整个流程跑通。原因很直白模拟数据的规律是你自己设定的你知道哪些特征和类别应该强相关所以模型学得好不好你能立即判断。如果拿真实数据一旦准确率只有60%你根本分不清是数据太脏、特征没选对、还是网络结构有毛病。模拟数据就是一条干净的跑道先确认你这辆车能跑再上复杂路况。还有一个很实际的原因练习阶段你手上未必有现成的、标注好的真实数据。与其花时间去爬数据、清洗数据不如直接造一套尺寸正好是12特征×4分类的数据集先把代码流程跑顺。等后面拿到真实数据替换掉数据读取那一段就行网络结构基本不用动。2.2 生成带分类结构的模拟数据模拟数据的生成要保证“有规律”不能纯随机。纯随机的12个特征和4个类别没有任何关联CNN再强也学不出东西准确率会恒定在25%附近。最简单的可行办法是给每个类别定一个不同的中心向量再加正态噪声。我习惯用下面这段代码rng(42); numSamplesPerClass 500; numFeatures 12; numClasses 4; centers [zeros(1,12); % 类别A特征均值全为0 2*ones(1,12); % 类别B特征均值全为2 -2*ones(1,12); % 类别C特征均值全为-2 repmat([2,-2],1,6)]; % 类别D特征正负交替 X zeros(numSamplesPerClass*numClasses, numFeatures); Y zeros(numSamplesPerClass*numClasses, 1); for i 1:numClasses idx (i-1)*numSamplesPerClass (1:numSamplesPerClass); X(idx, :) repmat(centers(i,:), numSamplesPerClass, 1) randn(numSamplesPerClass, numFeatures); Y(idx) i; end这里randn是标准正态噪声均值0、方差1。四个类别的中心在12维空间里互相分得很开加上噪声之后依然可分类。你可能想问类别D的均值向量正负交替平均下来每个特征都接近0在二维投影图上会和类别A重叠这还能分吗答案是能分。CNN是在12维空间里找模式不是在看二维散点图。类别D的特征模式是“一正一负、一正一负”这个模式在12维空间里是一类独特的轨迹卷积核是可以把它捕捉到的。每类500个样本总共2000个样本。这个量级对12特征的小型CNN来说足够又不至于跑得太慢。如果你觉得随机种子影响结果可以多换几个rng种子跑几遍通常结果差异非常小。2.3 数据划分和归一化拿到X和Y之后下一步是划分训练集和测试集。用randperm把样本顺序打乱前80%作训练后20%作测试idx randperm(size(X,1)); idxTrain idx(1:round(0.8*size(X,1))); idxTest idx(round(0.8*size(X,1))1:end); XTrain X(idxTrain,:); YTrain Y(idxTrain); XTest X(idxTest,:); YTest Y(idxTest);再强调一句归一化虽然这组模拟数据的特征本身是标准正态分布均值0方差1归一化看起来可有可无但真实数据往往不是这个尺度。养成习惯第一次跑流程就把标准化加上[XTrainNorm, mu, sigma] zscore(XTrain); XTestNorm (XTest - mu) ./ sigma;注意这里用训练集的mu和sigma去归一化测试集而不是对测试集单独算。为什么因为测试集扮演的是“未来新数据”的角色新数据来的时候我们手里只有训练集统计出的均值方差。如果单独算测试集的均值和方差相当于提前偷看了测试集的分布最后评估出来的准确率会偏乐观。这个细节在真实项目里特别重要。2.4 把矩阵转成CNN能吃的cell数组这是全流程里最机械但最不容出错的一步。注意看转置XTrainCell num2cell(XTrainNorm, 1); XTestCell num2cell(XTestNorm, 1);X把矩阵从“样本×特征”转成“特征×样本”方向然后num2cell按第一维即每一列切分得到1×N的cell数组每个cell是一个12×1的数组正好对应网络输入层期望的12×1×1。如果你忘记转置直接写num2cell(XTrainNorm, 1)你会得到12个cell每个cell是N×1那模型会把“样本”当成“特征”来理解必错。这个方向问题我见过太多次了。你可以用一个简单的方法自查size(XTrainCell{1})应该输出12 1。如果你看到的是1 12说明你少转置了一次。不要嫌我唠叨这一步错了后面所有环节都会在同一个报错上反复翻车。标签也别忘了转categoricalclassNames {类别A; 类别B; 类别C; 类别D}; YTrainCat categorical(YTrain, 1:4, classNames); YTestCat categorical(YTest, 1:4, classNames);到这里数据部分就算准备好了。你看真正耗时的是理解和包装数据格式这个过程跑通之后后面搭网络其实很快。3. 搭1D-CNN分类模型网络结构逐层拆解3.1 输入层先确定“盘子”的尺寸输入层用imageInputLayer。既然特征维度是12宽度和通道都是1那么输入尺寸就是[12 1 1]inputLayer imageInputLayer([12 1 1], Name, input);为什么要写三个数字而不是一个12因为这是给通用卷积网络设计的接口它默认输入是“高×宽×通道”。哪怕你的数据本质是一维也要补上后面两个维度好让它统一处理。这个细节在Matlab里特别容易漏漏了就可能报错或者输入解释和你预想的不一样。顺带一提如果你只是临时起意想用featureInputLayer它确实可以直接吃12维特征向量不需要cell包装。但那就不是CNN了而是全连接网络。既然目标是搞CNN就按CNN的规矩来。3.2 卷积层、BN层、池化层怎么搭配针对12个特征这么小的输入网络用不着很深。我常用的轻量结构是两层卷积加两个池化layers [ imageInputLayer([12 1 1], Name, input) convolution1dLayer(3, 32, Padding, same, Name, conv1) batchNormalizationLayer(Name, bn1) reluLayer(Name, relu1) maxPooling1dLayer(2, Stride, 2, Name, pool1) convolution1dLayer(3, 64, Padding, same, Name, conv2) batchNormalizationLayer(Name, bn2) reluLayer(Name, relu2) maxPooling1dLayer(2, Stride, 2, Name, pool2) fullyConnectedLayer(64, Name, fc1) reluLayer(Name, relu3) fullyConnectedLayer(4, Name, fc_out) softmaxLayer(Name, softmax) classificationLayer(Name, classout) ];逐个解释我为什么这么配convolution1dLayer(3, 32)卷积核长度3生成32个特征图。长度3的意思是每次看相邻3个特征然后往后滑动。Padding设为same保证卷积之后长度仍然保持12。batchNormalizationLayer对每个特征位置在batch维度上做标准化。简单说就是让网络中间层的输出不要忽大忽小训练会稳定很多。maxPooling1dLayer(2, Stride, 2)把长度从12压到6再从6压到3。池化不仅能减少计算量还能让模型对局部位置变化更鲁棒。对这个任务来说它更像是降采样把特征抽象层次拉高。fullyConnectedLayer(64)先把前面卷积层输出的特征拉平再映射到64维最后输出4维对应4个类别。softmaxLayer再把它变成概率分布。为什么第一层就选32个卷积核不选128因为输入长度只有12。你想想一个长度为3的卷积核在这个12维序列上能滑出多少种位置组合大不了就是10个位置你放128个卷积核大部分卷积核学到的东西高度重复参数浪费不说还容易过拟合。32个起步是够用的第二层64个已经算宽裕了。这里可以算一下总参数量第一层卷积是3×3296个参数第二层卷积是3×64×32646208个参数32是输入通道数64是输出通道数64是偏置全连接第一层是192×646412352个参数输出层是64×44260个。加起来不到2万。这个量级在CPU上都能很快跑完完全不需要纠结GPU。3.3 为什么不用纯全连接网络硬怼你可能想问12个特征直接接一个全连接层到4个输出不也能做分类吗能效果可能也不差。但CNN在这里有一个明显优势卷积层通过“局部感受野权值共享”来学习特征组合规则它对“特征之间局部组合模式”的归纳偏置非常强。打个比方全连接网络像是一个桌子边坐着12个人每个人都要和其余11个人两两握手关系复杂记是记住了但也很容易把噪音当信号。卷积网络则是让12个人排成一排每3个人一组轮流接头最后把接头结果汇总。后者参数更少、更不容易记住无关的细节在中小规模数据集上往往更稳。而且你后面迁移到真实数据时如果特征有顺序关系比如时序信号、频谱值、按物理意义排列的传感器读数一维卷积比全连接更贴近数据本身的连续性。现在先用模拟数据把这条思路跑通后面受益。4. 训练、评估和常见参数怎么调4.1 训练选项设置的几个关键参数trainNetwork之前要配一个trainingOptions。我一般这么写options trainingOptions(sgdm, ... MiniBatchSize, 64, ... MaxEpochs, 30, ... InitialLearnRate, 0.01, ... Shuffle, every-epoch, ... Verbose, true, ... Plots, training-progress, ... ValidationData, {XTestCell, YTestCat}, ... ValidationFrequency, 5);每个参数都不白设我挑几个重点讲。优化器用sgdm。虽然adam在不少任务上更省心但对这种线性可分度很高的模拟数据sgdm收敛很直接不容易出幺蛾子。MiniBatchSize选64因为2000个样本量64这个batch大小能让每个iteration看到足够多样的样本梯度方向比较稳。选太大比如512的话每个epoch的更新次数太少收敛会拖慢选太小又容易loss震荡。InitialLearnRate0.01是关键。这个值对sgdm比较常规。学习率初值设大了loss很容易跳到NaN设小了前期收敛慢得让你怀疑人生。模拟数据问题简单0.01基本不会翻车。Shuffleevery-epoch指每个epoch都重新打乱batch顺序避免模型“记住”固定的样本顺序这在训练里是个默认操作。ValidationData直接用了测试集配合ValidationFrequency5表示每5次iteration评估一次验证集。严格来说为了防过拟合应该另留一份验证集但模拟数据阶段这样做完全可以主要是为了实时看验证曲线。4.2 训练过程看什么loss曲线和准确率曲线调用训练net trainNetwork(XTrainCell, YTrainCat, layers, options);训练窗口里会显示两条loss曲线训练loss和验证loss。观察点有这么几个。理想趋势是训练loss从高往低走验证loss也随之下降。如果验证loss下降但训练loss更慢说明学习率偏小如果验证loss到了某个点开始反弹上升而训练loss还在降那就是过拟合先兆需要减少epochs、加正则化、或者减小网络规模。准确率曲线同样重要。模拟数据分得这么开理论上训练完成时训练准确率应当在95%以上。如果你的准确率卡在25%附近不要怀疑网络能力先回去检查数据组织是不是cell数组切分方向错了或者标签categorical没转好或者特征忘了归一化。这三个问题我全都遇到过每一次都在早期排查上省了很多时间。控制台也会逐轮输出结果包含loss和准确率。你可以观察每轮下降速度如果第5轮loss已经降到0.5以下说明数据可学性强网络结构没有问题。4.3 测试集评估和混淆矩阵训练完拿测试集做最终评估YPred classify(net, XTestCell); acc mean(YPred YTestCat); fprintf(测试集准确率: %.2f%%\n, acc * 100); figure; confusionchart(YTestCat, YPred);这里有个容易踩的坑classify返回的类别顺序必须和你原来的YTestCat保持一致。由于我们训练和测试用的是同一个classNames定义这一步通常不会出错。混淆矩阵怎么看对角线上的数字越大越好非对角线就是误分类。如果某个类别的样本大量落到另一个类别里说明这两个类在特征空间上过于接近。以刚才的模拟数据为例理论上混淆矩阵的对角线应该非常干净。如果你看到某两类大面积混在一起多半是你生成模拟数据时中心向量设计得有问题或者特征数太少不足以区分这四个类。这时候就可以回头调中心向量的间距这也是模拟数据带来的“调试自由”——你可以随意控制问题难度。5. 真实踩坑记录与排查思路5.1 最常见的“输入维度不匹配”报错遇到类似Error using trainNetwork ... Invalid input datatype或Input size mismatch时优先检查三处。第一cell数组里每个数组是不是12×1。在命令行敲size(XTrainCell{1})如果输出是1 12说明转置漏了改成num2cell(XTrainNorm, 1)。第二输入层尺寸是不是[12 1 1]。如果你写成了[12 1]有些版本可能不报错但行为诡异干脆统一写三元素。第三标签类型class(XTrainCell)是不是cellclass(YTrainCat)是不是categorical。前者必须是cell数组后者必须是categorical缺一不可。曾经有位朋友跟我说他的代码报错结果查了半天是他把标签误存成了double矩阵还没转categorical。5.2 准确率低不是模型的锅当你发现训练曲线稳定下降但准确率还是低大概率是标签和数据源的关系没有建立起来。拿模拟数据马上可以验证如果你用的特征是rand(2000,12)产生的纯随机数而标签是某种规则那么样本和标签根本不存在可学习关系CNN当然学不出来。这不叫模型失败是数据不靠谱。真实数据场景下还要检查类别严重不平衡的情况。如果某一个类别占了80%的样本模型只要全预测成那一类就能拿到80%准确率但这毫无用处。一开始模拟数据用每类500个等比生成就是为了绕开这个问题。如果你把模拟数据改成每类数目差异极大的形式很快就能体会到这种“准确率假象”。所以看准确率之前先看一眼各类别的样本数。5.3 训练不稳定乱跳和NaNLoss在训练过程中剧烈震荡先检查学习率。0.01在这个模拟数据上是不大的但如果你直接把学习率改成0.1大概率loss跳到NaN。遇到NaN就果断调低学习率或者干脆换成adam优化器试一下。另外batchNormalizationLayer理论上对输入尺度不敏感但如果输入特征数值高达几百上千第一层卷积的梯度依然会被放大训练照样崩。归一化步骤放在数据组织之前就是为了省掉这种麻烦。如果loss在稳定下降但收敛很慢可以把epochs加大到50或者把MiniBatchSize减半都能加快收敛速度。这本质上是“步长”和“次数”的平衡问题不存在通解只能结合曲线微调。我个人的习惯是每次只改一个参数改完看曲线而不是一次性调三四个参数否则你根本不知道是哪个改动起了作用。5.4 模拟数据也会过拟合有人觉得模拟数据简单就不会过拟合其实不是。即使数据可分如果网络参数很多而训练数据有限模型同样可以把训练集的细节完全背下来。本例中网络参数不到2万2000个样本量epochs30不太会过拟合。但如果epochs开到几百你仍然能看到验证loss后面反弹。对策一个是早停另一个是加dropoutLayer或调高L2正则。更稳妥的办法是永远把测试集当成“真实验收”只在最后评估一次别拿它反复调参——调参用训练曲线和验证曲线足够测试集留在最后用。这里分享一个我自己的习惯训练结束后我会把net保存成mat文件顺手存一份参数说明和当日数据版本号。因为模拟数据测试阶段你可能每天改好几个版本一周后回来根本分不清哪个模型对应哪版数据。这个习惯不花什么时间但能省掉很多后期混乱。用模拟数据把这套流程完整跑一遍之后我个人最大的体会是深度学习的门槛常常不在“模型”而在“数据接口”。用Matlab做CNN分类最需要记牢的就是那三步——矩阵转置成特征方向、切成cell、标签转categorical。这三步在模拟数据上踩过一次坑后面换真实数据时你会条件反射地先检查它们。最后分享一个小技巧调好网络结构后把数据生成部分封装成一个函数输入换成真实数据表时只要保证表头顺序和特征定义一致后面的流程可以原样复用。你先用手里的模拟数据验证了能跑再迁移到真实场景踩坑成本会小非常多。