
简介一套面向多变量时序场景的CNN-RNN多特征分类项目实例基于MATLAB完成从数据合成、网络搭建到训练评估的端到端实现。资源聚焦CNN在局部模式提取与RNN/LSTM在长程依赖建模上的互补优势适合希望将深度学习用于工业监测、金融量化、医疗生理信号等序列分类任务的工程师与研究者。文档完整覆盖项目背景、目标意义、挑战解决方案、模型架构与代码示例具体涉及1D卷积编码、双向LSTM聚合、类别不平衡处理与多指标评估并包含GUI设计说明可帮助读者快速将方案迁移到自己的业务数据上。压缩包内含1个docx文档大小约97KB内容按项目背景、挑战方案、模型架构、训练评估等模块清晰组织便于查阅。目前已有81人学习作为一套紧凑而完整的端到端工程模板能在较短时间内提供从理论到实践的完整参考。1. 在 MATLAB 里搭 CNN-RNN 前先把多特征分类这件事想清楚用 MATLAB 做机器学习分类最容易让人一上来就懵的是多特征分类预测里的“特征”和“时间”到底怎么摆进网络。同样是传感器采集的数据表格里每行是一个时刻、每列是一个特征但到了 sequenceInputLayer 面前维度顺序必须换成“特征维 × 时间维 × 样本数”三者错一个trainNetwork 的报错信息能把人绕进去半天。CNN-RNN 这个组合要解决的正是这类“多条特征序列 → 输出一个类别标签”的任务卷积神经网络CNN负责抓局部模式循环神经网络RNN这里一般用 LSTM负责梳理时序依赖两者串联后比单独用其中任何一种都稳。下面按这类项目最常见的落地顺序走一遍先拆结构再组织数据给出可复现的训练脚本和一个基于 uifigure 的 GUI 预测工具最后落到验证、调参和排错。适合已经能熟练写 MATLAB 脚本、但对 Deep Learning Toolbox 还不熟的人。2. CNN-RNN 结构解析多特征输入如何变成分类输出2.1 为什么是 CNN-RNN 组合而不是单独用 CNN 或 LSTM先想清楚一个问题标题里的“多特征”到底指什么。常见场景是每个样本包含多条同步采样的时间序列比如设备的温度、振动、电流三个测点每个测点是一条序列特征数就是通道数每条序列的长度就是时间步数。这种数据单独喂给 CNN卷积核虽然能同时扫过多个通道但对前后时间步的长期依赖不敏感单独喂给 LSTM每个时间步都做门控计算训练慢而且容易把局部的突变模式平均掉。CNN-RNN 的做法是先让卷积层在时间维上做滑窗把相邻几个时间步的局部模式提炼成更紧凑的特征再用 LSTM 对提炼后的特征序列建模。这样卷积层输出的每一列仍对应一个时间步LSTM 看到的还是时序但每一步的信息密度更高。还有一个工程上的理由MATLAB 的 Deep Learning Toolbox 对 1D-CNN 加 LSTM 的原生支持很完善不需要像其他框架那样自己拼数据管道。卷积层、池化层、LSTM 层、分类层都是现成的层对象直接数组拼接就能定义网络。这在快速验证“某个多特征分类问题适不适合用深度学习”时成本很低。2.2 多特征输入在 MATLAB 里的维度摆放特征维在前还是时间维在前这是新手最容易踩的坑。MATLAB 的 sequenceInputLayer 期待单个样本的形状是“特征数 × 时间步数”而不是“时间步数 × 特征数”。一批样本则是在这个矩阵后面再加一维变成“特征数 × 时间步数 × 样本数”。下表总结了不同数据形态对应的输入层选择数据形态建议输入层张量尺寸要求多条等长时间序列一个类别标签sequenceInputLayer特征数 × 时间步数 × 样本数不等长序列sequenceInputLayer cell 数组每个 cell 为 特征数 × 时间步数纯表格特征、无时序featureInputLayer样本数 × 特征数这里仅讨论等长序列这一最常见情形。如果原始数据是“每个时间点一行、每列一个特征”的表格必须先用 transpose 把 特征数 放到第一维再 reshape 成三维数组。动量词都对不上时训练脚本会直接报维度错误而不是训练完再给你一个差结果。2.3 一个可直接起步的 CNN-RNN 网络布局与超参数我一般会先用一个比较浅的结构跑通正确性再逐步加深。对 5 个特征、序列长度 128、3 分类的任务起步布局可以是这样sequenceInputLayer(5)声明特征数为 5convolution1dLayer(3, 16, Padding, same)16 个卷积核每个覆盖 3 个时间步reluLayer maxPooling1dLayer(2, Stride, 2)下采样序列长度减半再重复一组 32 个卷积核的卷积池化lstmLayer(32, OutputMode, last)32 个隐藏单元只取最后一个时间步的输出dropoutLayer(0.2) 降低过拟合fullyConnectedLayer(3) softmaxLayer classificationLayer。第一层卷积核大小我习惯取 3因为时间序列的局部模式一般不会跨越太多时间步LSTM 隐藏单元先给 32够用再看 loss 曲线往上加。注意池化会改变序列长度卷积层加上 Padding,same 后长度不变池化每次让它减半128 进过两层池化变成 32LSTM 接收 32 个时间步信息仍然完整。3. MATLAB 数据准备从原始特征到 CNN-RNN 可用的数值序列3.1 用合成数据生成 5 特征时间序列和类别标签没有外接数据时用合成数据走通全流程是最稳妥的验证方式。下面这段代码生成 600 个样本每个样本是 5 个特征、长度 128 的时间序列分成 3 类。类别不同基础波形频率不同每个特征再叠加不同相位的谐波和噪声模拟真实多传感器信号。rng(42); % 固定随机种子保证结果可复现 numFeatures 5; % 特征数通道数 seqLen 128; % 时间步数 numSamples 600; % 样本总数 numClasses 3; % 类别数 X zeros(numFeatures, seqLen, numSamples); % 特征数×时间步×样本数 Y zeros(numSamples, 1); for s 1:numSamples label randi(numClasses); % 随机指定类别 t linspace(0, 2*pi*label, seqLen); base sin(t label); % 类别相关的基础波形 for f 1:numFeatures X(f, :, s) base 0.5*sin(3*t f) 0.1*randn(1, seqLen); end Y(s) label; end Y categorical(Y); % 转成分类标签类型逻辑说明外层循环生成样本内层循环生成 5 条特征序列。X 的每一页对应一个样本每页的第 f 行是第 f 个特征在 128 个时间步上的取值。Y 是样本对应的类别标签必须转成 categorical分类层才能接受。参数说明randi 负责随机分配标签让三类样本数量大致均衡sin 的第三个参数控制噪声幅度实际数据里噪声水平会根据信噪比调整。随机种子 rng(42) 保证下次运行结果一致调试阶段强烈建议保留。3.2 划分训练集/验证集并做 zscore 归一化数据集要先拆成三份训练、验证、测试。验证集在训练过程中用来观察是否过拟合测试集只在全部训练结束后用一次。idx randperm(numSamples); numTrain floor(0.7 * numSamples); numVal floor(0.15 * numSamples); idxTrain idx(1:numTrain); idxVal idx(numTrain1:numTrainnumVal); idxTest idx(numTrainnumVal1:end); XTrain X(:, :, idxTrain); YTrain Y(idxTrain); XVal X(:, :, idxVal); YVal Y(idxVal); XTest X(:, :, idxTest); YTest Y(idxTest);归一化有两种做法一种是直接在 sequenceInputLayer 上设 Normalization, zscore让工具箱在训练时自动按训练集统计量标准化另一种是手动计算均值方差。手动做法要保留训练集的 mu 和 sigma后面 GUI 预测新样本时用同一组统计量mu mean(XTrain(:)); sigma std(XTrain(:)); XTrain (XTrain - mu) / sigma; XVal (XVal - mu) / sigma; XTest (XTest - mu) / sigma;注意验证集和测试集必须用训练集的统计量不能单独算。很多人在这一步偷懒导致训练集和测试集分布不一致模型在验证集上虚高、在线上数据上翻车。如果特征之间的量纲差异很大可以改成每个特征各自归一化做法是沿第二维调用 mean(XTrain, 2)。3.3 把连续测量记录滑窗成样本序列工程里拿到的原始数据往往是一长段连续记录不是现成的等长样本。常见的做法是开一个固定长度的窗口以固定步长滑动每个窗口形成一个样本窗口之间有重叠相当于数据增强样本量能成倍增加。win 128; % 窗口长度等于网络输入的时间步数 step 32; % 滑动步长步长小于窗口时窗口重叠 rawData randn(5, 10000); % 模拟 5 特征、10000 个时间步的连续记录 numWindows floor((size(rawData,2) - win) / step) 1; Xwin zeros(5, win, numWindows); for k 1:numWindows startIdx (k-1)*step 1; Xwin(:, :, k) rawData(:, startIdx:startIdxwin-1); end滑窗有两个超参数窗口长度决定单个样本能看到多长的上下文步长决定样本重叠度。窗口太短LSTM 学不到长期依赖重叠太多相邻样本高度相关训练集和验证集的信息会有泄漏评估结果会偏乐观。如果数据量足够步长可以取窗口长度的一半甚至更长数据量少时再用小步长做增强。4. 用 Deep Learning Toolbox 定义 CNN-RNN 并训练模型4.1 用 layer 数组定义 CNN-RNNconvolution1dLayer 到 lstmLayer网络定义在 MATLAB 里就是按顺序拼一个 layer 数组每一层的输出是下一层的输入。下面是完整定义直接对应第 2.3 节的布局。numClasses 3; layers [ sequenceInputLayer(numFeatures, Normalization, zscore) convolution1dLayer(3, 16, Padding, same, Stride, 1) reluLayer maxPooling1dLayer(2, Stride, 2) convolution1dLayer(3, 32, Padding, same) reluLayer maxPooling1dLayer(2, Stride, 2) lstmLayer(32, OutputMode, last) dropoutLayer(0.2) fullyConnectedLayer(numClasses) softmaxLayer classificationLayer ];逻辑说明sequenceInputLayer 指定特征数并让工具箱在训练时自动做 zscore 归一化两个卷积层分别提取 16 个和 32 个局部特征每层后面接 relu 激活函数再接步长为 2 的池化层把序列长度从 128 降到 32LSTM 层设置 OutputMode, last只把最后一个时间步的结果传给全连接层dropout 在训练时随机屏蔽 20% 的神经元抑制过拟合最后的 fullyConnectedLayer 输出 3 个分数softmax 转成概率classificationLayer 负责计算损失。参数说明convolution1dLayer 的第一个参数是滤波器长度 3第二个是滤波器个数 16LSTM 的 32 是隐藏单元数不是时间步数两者经常被混淆。4.2 trainingOptions 的 5 个关键参数学习率、批次、轮数与验证集训练选项直接决定模型能不能收敛、会不会过拟合。下面是这组数据上比较稳的参数options trainingOptions(adam, ... MaxEpochs, 100, ... MiniBatchSize, 32, ... InitialLearnRate, 0.005, ... Shuffle, every-epoch, ... ValidationData, {XVal, YVal}, ... ValidationFrequency, 10, ... Plots, training-progress, ... Verbose, 1);参数说明adam 是自适应学习率的优化器对序列任务一般比 sgdm 稳InitialLearnRate 0.005 在 0.001 到 0.01 之间loss 振荡就往下调一半MiniBatchSize 32 是显存和梯度稳定性之间的折中ValidationFrequency 10 表示每 10 个迭代计算一次验证集准确率。MaxEpochs 100 是上限实际训练中如果验证集损失提前停止下降没必要真跑满 100 轮。训练进度图打开后重点看验证集损失和训练集损失之间的距离差距持续拉大就是过拟合信号。4.3 训练、保存模型并在测试集上做预测模型训练和评估的完整主脚本如下包含训练、保存、测试三件事。net trainNetwork(XTrain, YTrain, layers, options); save(cnn_rnn_net.mat, net); pred classify(net, XTest); acc mean(pred YTest); fprintf(测试集准确率: %.2f%%\n, acc*100); figure; confusionchart(YTest, pred);逻辑说明trainNetwork 接收训练数据和标签、网络结构、训练选项三个输入返回训练好的网络对象 netsave 把 net 存成 .mat 文件供后续 GUI 直接加载classify 返回每个测试样本的预测类别与 YTest 逐元素比较后求平均值得到准确率confusionchart 画出混淆矩阵对角线上的值越大说明分类越可靠。如果准确率在合成数据上低于 90%优先检查前面数据准备的维度方向而不是急着加深网络。5. 把训练好的 CNN-RNN 封装成 GUI 预测工具5.1 用 uifigure 写一个最小可用的 CNN-RNN 预测界面训练好的模型对业务人员来说只是个黑盒GUI 的意义在于把“加载数据、加载模型、得到预测”这三步收敛成几个按钮。用脚本方式创建界面最简单下面这段代码在 MATLAB R2018a 之后的版本都能直接运行。function predictGUI() fig uifigure(Name, CNN-RNN 多特征分类预测, Position, [200 200 680 420]); g uigridlayout(fig, [3 2]); uilabel(g, Text, 测试数据(.mat文件):); btnData uibutton(g, Text, 选择数据文件, ButtonPushedFcn, (src,event) chooseData(src)); uilabel(g, Text, 训练好的模型文件:); btnModel uibutton(g, Text, 加载模型并预测, ButtonPushedFcn, (src,event) runPredict(src)); txtResult uitextarea(g, Value, {预测结果会显示在这里}); txtResult.Layout.Row 3; txtResult.Layout.Column [1 2]; testData []; function chooseData(src) [file, path] uigetfile(*.mat, 选择测试数据); if isequal(file, 0), return; end d load(fullfile(path, file)); if isfield(d, XTest), testData d.XTest; else, testData d.X; end txtResult.Value sprintf(已加载测试样本当前尺寸%d 特征 × %d 时间步, size(testData,1), size(testData,2)); end function runPredict(src) if isempty(testData) txtResult.Value 请先加载测试数据; return; end [file, path] uigetfile(*.mat, 选择训练好的模型); if isequal(file, 0), return; end m load(fullfile(path, file)); pred classify(m.net, testData); txtResult.Value cellstr(char(pred)); end end逻辑说明外层函数创建窗口和控件chooseData 和 runPredict 是嵌套函数可以直接访问 testData 和 txtResult。uigetfile 弹窗让用户选择 .mat 文件load 读入后按字段名取数据classify 输出预测标签再显示到文本框。注意嵌套函数必须每个都用 end 收尾文件保存为 predictor.m 后在命令行直接输入 predictGUI 即可启动。按钮回调里 src 和 event 是固定占位参数即使不用也不能省略。5.2 在 App Designer 中重写同样的回调逻辑脚本 GUI 适合快速验证正式交付给同事或客户时App Designer 的界面更整齐组件拖拽布局也更不容易写错。核心逻辑完全一致区别只在两处控件回调函数的签名是固定的共享数据不再依赖嵌套函数作用域而是存在 app 的属性里。在 App Designer 的设计视图中放一个按钮双击后生成的回调骨架是function RunPredictionButtonPushed(app, event) if isempty(app.TestData) app.ResultTextArea.Value 请先加载测试数据; return; end [file, path] uigetfile(*.mat, 选择训练好的模型); if isequal(file, 0), return; end m load(fullfile(path, file)); pred classify(m.net, app.TestData); app.ResultTextArea.Value cellstr(char(pred)); endapp.TestData 是你在代码视图里手动添加的属性属性面板里新建一个字段即可类型不用声明。脚本 GUI 里靠嵌套函数共享的变量到了 App Designer 里全部改用 app.xxx 这种属性引用。弄清楚这一层对应关系从脚本 GUI 迁移到 App Designer 只是复制粘贴的问题。另外 App Designer 的组件默认会用 uifigure 容器与上文的 uifigure 控件完全兼容。5.3 GUI 里要处理的输入校验和归一化一致性问题GUI 最容易忽略的是“预测时的新样本要不要归一化、怎么归一化”。如果训练时用的是 sequenceInputLayer 自带的 zscore 选项那工具箱会自动用训练集的均值方差处理新数据不需要额外操心但如果训练前手动对 X 做了 (X - mu)/sigmaGUI 里必须保存当时的 mu 和 sigma预测前对新样本做完全相同的变换。建议训练主脚本在保存 net 时把统计量一起存进去save(cnn_rnn_net.mat, net, mu, sigma);GUI 里加载模型后先判断文件里有没有 mu、sigma 字段有就先对 testData 做归一化再 classify。另一个要校验的是输入维度classify 拿到一个维度对不上的数组时报错信息只说尺寸不匹配不会告诉你是特征数错了还是时间步错了。所以在 runPredict 里最好加一行判断特征维不等于 size(net.Layers(1).InputSize, 1) 时直接提示而不是把原始报错抛给用户。6. 验证、调参与排错让 CNN-RNN 在测试集上稳定工作6.1 用混淆矩阵看逐类表现而不是只看准确率整体准确率会掩盖类别不均衡的问题。三分类里其中一类占了 90%模型全部预测成那一类准确率也有 90%但对业务来说毫无价值。confusionchart 画出的矩阵能直接看到每一类的召回率。另外可以输出每一类的精确率和召回率cm confusionmat(YTest, pred); precision diag(cm) ./ sum(cm, 1); recall diag(cm) ./ sum(cm, 2);观察矩阵时如果某一类经常被误分成另一类说明这两类的特征模式太接近要么增加这一类别的样本量要么检查是不是特征里混入了与类别无关的噪声。6.2 三个常见报错与相应的调参路径第一个是维度报错。错误信息形如“Expected input to be of size [5 128]”原因就是 sequenceInputLayer 的输入尺寸和实际传入矩阵对不上检查数据的特征数、时间步长和层定义是否一致。第二个是训练 loss 不下降。这时先看训练集准确率如果训练集上也很低把 InitialLearnRate 从 0.005 调到 0.001或把 LSTM 隐藏单元从 32 加到 64如果训练集高、验证集低把 dropout 从 0.2 提高到 0.5并减小 MaxEpochs。第三个是 GPU 显存不足。最简单的办法是把 MiniBatchSize 从 32 降到 16 或 8因为 batch 越大反向传播中间缓存占用越高。这三个问题按“数据维度 → 学习率/结构 → 显存”的顺序排查能覆盖训练阶段绝大多数失败场景。本文还有配套的精品资源点击获取