【终审稿】CNN-RNN 通用分类算法及 MATLAB 实现

【终审稿】CNN-RNN 通用分类算法及 MATLAB 实现 假设一个人把手机固定在腰间然后走路、上楼、下楼、坐下、站立或者躺下。手机里的加速度计和陀螺仪会不断记录身体运动。现在给你其中一小段传感器数据让程序判断这个人在做什么你会从哪里下手只看某一个时刻往往不够。坐着与站着在某些瞬间都很平稳走路与上下楼也可能出现相似的振动。真正有用的信息一部分藏在相邻几个采样点的局部起伏里另一部分藏在整段动作的先后变化里。这正是 CNN-RNN 混合模型适合处理的问题CNN 先找局部模式RNN 再把这些模式按顺序串起来最后分类器从几个候选类别中选一个。但“先 CNN、再 RNN”还远远算不上讲懂。下面先不看网络结构名直接跟着一条真实数据看看它在每一站究竟变成了什么。一、先说清楚这到底是什么任务本文做的是多分类。输入是一条已经截取好的动作片段输出是“行走、上楼、下楼、坐着、站立、躺着”中的一个。它不是回归因为输出不是房价、温度这类连续数值它也不是时间序列预测因为我们没有要求模型猜下一时刻的传感器读数。“分类任务”“输入有顺序”“人体活动识别”是三个不同层面的概念。分类是要做什么有顺序是数据怎样组织人体活动识别只是一个具体应用。把这三件事分开后面才能理解为什么同一个函数还可以处理 Iris 表格和 MNIST 图像。二、一条原始数据长什么样本文使用 UCI HAR 人体活动识别数据。原始研究让 30 名受试者把智能手机佩戴在腰部完成 6 种日常活动手机以 50 Hz 记录加速度和角速度等信号。在本项目里一条样本被整理成9×1289 表示同一时刻记录的 9 路传感器信号128 表示这段窗口里连续的 128 个时间点。一个标签对应整条样本而不是给 128 个时间点分别贴标签。可以把它想成一本很薄的乐谱纵向有 9 行横向有 128 拍。模型既要比较同一拍的多路信号也要沿着横向读完整段变化。图中先看主路径9路信号×128步经过局部扫描、序列压缩和顺序记忆最后进入 6 类出口。接下来每一节只拆其中一个动作拆完后再回到这条主路。三、第一道关不同形状的数据怎样进入同一个模型网络层不会自动理解哪一维是样本、哪一维有先后顺序。对 HAR 来说完整数组是2700×9×128第一维是 2700 条样本拿出一条后单样本是9×128其中第二维才是时间轴。本项目先做输入适配。sampleDimension指出整批数据中的样本维sequenceAxis指出单条样本内部要按顺序读取的轴。其余维度被合并成每一步的特征。HAR 适配后仍是“9 个特征×128 步”但这个步骤把各维的含义明确固定下来。这就是“通用分类”的关键但通用不等于随便。Iris 的一行有 4 个特征可以把这 4 个位置当作一条短序列MNIST 的单张图是28×28可以指定宽度轴或高度轴为序列轴把另一维并入每一步的特征。轴选错时程序可能仍然能运行但模型读到的顺序已经变了。适配器现在交给 CNN 的是一条含 128 步的序列每一步有 9 个数。接下来要解决的问题是如何从这些连续数值中找出短促而重复的动作痕迹四、CNN先在相邻位置里找“局部动作”假设只截取连续 5 个时间点。走路时可能出现一次周期性起伏上楼时可能出现另一种幅度和方向组合。一个局部窗口虽然看不到整段动作却能发现“这里发生过一次怎样的变化”。一维卷积就是一把反复使用的局部尺子。本文的kernelSize5表示每次观察相邻 5 步它从序列左侧开始向右滑动并在每个位置生成新的局部特征。相同的检测规则会在整段序列上重复使用所以网络不必为第 1 步和第 100 步分别学习两套规则。KHMD_IMAGE_003第一层卷积把原来的 9 路输入变成 32 路局部特征尺寸从9×128变为32×128。这里的 32 不是时间点而是 32 种由网络自己学到的局部观察方式。第二层卷积继续组合这些初级模式得到64×128。因为使用了 same padding卷积前后仍然是 128 步。换句话说CNN 改变的是“每一步怎样描述”还没有缩短整段序列。CNN 的边界也很明确。卷积核太小可能看不到完整的局部动作卷积核太大又会增加参数并模糊局部性。convChannels越大网络能容纳的模式越多但数据不够时也更容易记住训练样本。到这里局部模式已经找到了但 RNN 如果直接读取全部 128 步计算量仍然较大。于是中间还安排了一次压缩。五、池化把 128 步压成 64 步本文使用poolSize2的最大池化。可以把相邻两个位置看成一组从中保留更突出的响应。这样64×128会变成64×64局部特征仍有 64 路序列长度从 128 减半为 64。KHMD_IMAGE_004压缩后的序列更短后面的 LSTM 读起来更省时间也能减少一些局部抖动。但是池化不是免费午餐。窗口过大时短暂但重要的峰值可能被吞掉。对于本来就很短的数据应把poolSize设为 1等价于跳过池化。现在每条样本已经变成 64 步每一步有 64 个 CNN 特征。下一棒才轮到 RNN。六、LSTM把局部特征按顺序串成整段记忆CNN 回答的是“这一小段像什么”LSTM 要回答的是“这些小段按当前顺序连起来整体像什么”。它从第 1 步读到第 64 步每读一步都会更新手中的记忆。普通循环网络容易在长序列中把较早的信息逐渐冲淡。LSTM 增加了受控的记忆通道可以决定哪些信息继续保留、哪些旧信息应该忘掉、当前新信息写入多少。这里不展开公式只抓住真正的数据接力LSTM 每次收到的是 CNN 产生的 64 维局部特征读完 64 步后留下一个 64 维的整段摘要。KHMD_IMAGE_005MATLAB 代码中rnnHidden64决定这本“记忆笔记”有多宽OutputModelast表示只把读完整段后的最后摘要交给分类器。隐藏单元太少可能装不下足够的信息太多则训练更慢也更容易过拟合。本文默认使用 LSTM。当前 MATLAB 核心函数还支持 GRU 和 BiLSTM。GRU 的结构更精简BiLSTM 会从两个方向阅读整段输入参数和计算量也更大。它们不是按名字就能排出高低必须在相同划分下比较验证集表现。七、64 维摘要怎样变成一个类别LSTM 输出的 64 维摘要仍然不是“行走”或“坐着”。全连接层会把它转换成 6 个类别分数每个分数对应一个候选活动。Softmax 再把这些分数整理成便于比较的相对可能性最高者就是本次预测类别。如果真实标签是“上楼”模型却把“下楼”分数排在最高训练程序就会得到一个较大的错误。这个错误通过网络向前追溯先调整分类层再调整 LSTM最后调整 CNN 的局部检测规则。许多批次反复进行后网络逐渐学会哪些局部模式和顺序组合更能区分 6 种活动。不过模型不能一边看最终考试答案一边调参。训练集、验证集和测试集必须各司其职。八、数据怎么分结果才不“作弊”本项目的 2700 条样本不是随意随机切分而是按受试者隔离1500 条训练、480 条验证、720 条测试。训练集负责更新参数验证集观察模型是否开始过拟合并选择最佳训练轮次测试集一直封存到训练结束最后只做一次评价。按人隔离很重要。同一个人的走路节奏、手机佩戴角度可能在不同窗口中重复出现。如果同一受试者同时进入训练和测试模型可能只是认出了这个人的习惯测试结果会显得过分乐观。KHMD_IMAGE_006归一化也遵循同样原则。程序只用训练集计算每路特征的均值和标准差然后把同一把尺子应用到验证集和测试集。若先用全部数据计算归一化参数再去划分集合测试信息就已经提前渗进训练流程。九、MATLAB 实跑结果怎么看本文使用默认的两层 CNN、单层 LSTM 和受试者隔离划分运行完整程序。MATLAB 版测试集 Accuracy 为 0.9014Macro-F1 为 0.9009。这个数字说明 720 条测试样本中约九成被正确分类但它不能告诉我们错误集中在哪些类别。KHMD_IMAGE_007先看收敛图。训练与验证损失在前期都快速下降验证准确率随后稳定在约 0.91 附近训练损失继续降低而验证损失后期有轻微回升。这提示模型已经学到主要规律但继续追求更低训练损失不会自动带来更好的泛化因此需要验证集和提前停止。KHMD_IMAGE_008混淆矩阵要沿对角线看正确分类离开对角线的数字就是具体错误。动态动作总体较稳行走、上楼、下楼大多落在对角线上躺着 120 条全部识别正确。最明显的问题在坐着与站立之间18 条坐着被判为站立30 条站立被判为坐着。这符合数据直觉——两种静态姿态的短时间传感器波形本来就更接近。KHMD_IMAGE_009分类别指标进一步确认了这一点。坐着和站立的 F1 明显低于其他类别而躺着接近 1。Macro-F1 会先分别计算每一类再做平均因此它比只看总体 Accuracy 更能暴露弱类。KHMD_IMAGE_010最后看三个集合的指标差距。训练集通常高于验证和测试这是正常现象如果差距继续扩大就要考虑减少网络容量、增大 dropout、加强数据覆盖或更早停止。本文结果能证明流程有效但不能证明 CNN-RNN 对任何数据都会得到同样精度。十、为什么说它能处理“任意维度”这里的“任意维度”指输入适配方式而不是承诺任何数据都适合 CNN-RNN。本项目还实际运行了 Iris 表格和 MNIST 图像。Iris 将 4 个特征视作一条短序列MNIST 选择图像的一条空间轴为序列轴另一条轴成为每一步的特征。三个案例共用FunClassCNNRNN。这件事并不等于模型对所有数据都有意义。模型是否合适取决于所选序列轴是否真的存在稳定的相邻关系。完全无序的列被强行排成序列时RNN 的顺序假设未必带来好处。十一、什么时候适合什么时候不适合CNN-RNN 更适合这样的数据单条样本内部存在局部相邻模式同时这些局部模式的先后顺序又会影响类别。例如传感器片段、语音片段、心电信号、光谱或按某个空间轴展开的图像。如果样本极少、特征没有自然顺序先尝试树模型、SVM 或简单全连接网络可能更稳。如果只靠一个很短的局部模式就能完成分类RNN 可能是多余成本如果需要精确保留每个瞬间过强池化也会损伤信息。排错时应先检查轴和划分再调网络。训练和验证都很差可能是学习率、输入方向或模型容量不合适训练很好、验证很差多半是过拟合或数据分布差异只有某些类别差则应看混淆矩阵、类别数量和原始信号是否真的可分。十二、“一行代码”完成 CNN-RNN 通用分类原理讲明白之后真正自己动手时麻烦才刚刚开始。你需要确认样本维和序列轴需要划分训练集、验证集和测试集还要避免归一化时偷看测试集。CNN 与 LSTM 之间的尺寸要逐层核对训练后还要恢复验证集表现最好的网络计算混淆矩阵、Accuracy、Precision、Recall、F1并把结果画出来。任何一个环节处理不严谨程序可能不报错结果却不可信。为了让大家把精力放在数据和模型本身而不是反复拼接这些固定流程我把整套过程封装成了FunClassCNNRNN函数。设置好输入和参数后真正启动模型只需要一行[foreData,foreDataTrain,net,info]FunClassCNNRNN(X,Y,options);这一行背后会依次完成输入轴适配、训练/验证/测试划分、训练集归一化、CNN-RNN 网络搭建、分类训练、验证集早停、最佳网络选择、三集合预测、分类指标计算和五张结果图的绘制与保存。函数的三个输入是X为数值型输入数组Y为每条样本的类别标签options管理输入适配、数据划分、网络结构、训练过程、归一化和绘图。四个输出分别是foreData为测试集预测标签foreDataTrain为训练集预测标签net为训练好的网络info保存索引、验证预测、类别得分、各集合指标、混淆矩阵、训练记录、归一化参数、适配后的尺寸和完整选项。以本文 HAR 数据为例完整调用如下%% 1. 导入数据load(har_activity_data.mat,X,Y,splitLabels);%% 2. 输入适配与固定划分options.sampleDimensionauto;% 自动识别样本维options.sequenceAxis2;% 单样本[9,128]的第2维为时间轴options.splitLabelssplitLabels;% 1训练、2验证、3测试按受试者隔离options.rTrain0.80;% 无splitLabels时训练验证数据占80%options.validationRatio0.15;% 验证集占训练验证数据的15%options.shuffletrue;% 训练批次打乱options.seed42;% 固定随机过程0表示不固定%% 3. CNN-RNN 网络结构options.networkTypeLSTM;% 可选LSTM、GRU、BiLSTMoptions.convChannels[32,64];% 两层卷积的输出通道数options.kernelSize5;% 每次观察相邻5步必须为正奇数options.poolSize2;% 最大池化窗口1表示不池化options.rnnHidden64;% 循环层隐藏单元数options.dropout0.20;% 分类头前随机失活比例范围[0,1)%% 4. 训练参数options.solverNameadam;% 可选adam、sgdm、rmspropoptions.maxEpochs35;% 最大训练轮数options.learnRate0.001;% 初始学习率options.batchSize64;% 批尺寸options.earlyStoppingPatience8;% 验证损失连续8次不改善则停止0关闭options.learnRateSchedulepiecewise;% 可选none或piecewiseoptions.learnRateDropPeriod15;% 每15轮降低一次学习率options.learnRateDropFactor0.5;% 每次变为原来的50%options.classWeightauto;% auto、none或每类权重数组options.executionEnvironmentauto;% auto、cpu或gpu%% 5. 数据处理与图像输出options.mapflagon;% 仅用训练集统计量进行标准化options.figflagon;% 自动绘图并保存options.showFigureson;% 本地运行时弹出图窗options.caseNameHAR人体活动;% 结果图文件名前缀options.classNames{行走,上楼,下楼,坐着,站立,躺着};%% 6. 一行开始训练、分类、评估和绘图[foreData,foreDataTrain,net,info]FunClassCNNRNN(X,Y,options);这些参数看起来不少但可以分组理解。sampleDimension和sequenceAxis决定“数据怎样读”前者指向整批数据的样本维后者在单条样本内部指定有顺序的轴。splitLabels适合按人员、设备或批次固定划分未提供它时rTrain先隔离最终测试集validationRatio再从训练验证数据中划出验证集。shuffle只适合可独立打乱的样本seed用于复现实验。convChannels、kernelSize、poolSize、rnnHidden和dropout管模型容量。通道数和隐藏单元增大后能表达更多模式也更耗时、更容易过拟合卷积核决定局部观察范围池化窗口决定压缩程度dropout 只在训练时随机屏蔽部分特征过拟合时可适当增加但太大会让模型学不动。maxEpochs是训练上限不代表一定跑满。learnRate太大容易震荡太小则收敛慢batchSize增大通常更稳定但占用更多内存earlyStoppingPatience让验证集控制何时停止。启用piecewise后学习率会按learnRateDropPeriod和learnRateDropFactor逐段降低。classWeightauto会根据训练集类别频数自动给少数类更高权重类别本来均衡时也可设为none。executionEnvironment选择 CPU、GPU 或自动判断。mapflag控制训练集标准化figflag控制是否生成图片showFigures控制图片是否在桌面弹出图片即使不弹出只要figflagon仍会保存到figure目录。MATLAB 版可切换的 RNNoptions.networkTypeLSTM;% 默认门控记忆较完整options.networkTypeGRU;% 结构更精简options.networkTypeBiLSTM;% 双向读取计算量和参数更多三行实际只保留一行。当前 MATLAB 核心函数只接受这三个名称不支持普通 RNN、BiRNN 或 BiGRU。函数结束后会自动保存收敛过程、测试集混淆矩阵、各类别指标、分类结果对比和训练/验证/测试指标五张图。前文解释的是这条数据旅程为什么成立这一节解决的是怎样把同一套严谨流程真正跑起来。如何获取完整代码本文使用的完整 MATLAB 代码包括核心函数、HAR/Iris/MNIST 三个演示脚本、数据集、分类评价函数、详细参数说明和自动生成的可视化图表。需要代码的同学可以在公众号后台回复关键词CNN-RNN分类获取也可以通过菜单栏或留言联系。参考资料UCI HAR 数据集官方页面https://archive.ics.uci.edu/dataset/240/human%2Bactivity%2Brecogni-tion%2Busing%2BsmartphonesMATLAB 一维卷积层文档https://www.mathworks.com/help/deeplearning/ref/nnet.cnn.layer.convolution1dlayer.htmlMATLAB LSTM 层文档https://www.mathworks.com/help/deeplearning/ref/nnet.cnn.layer.lstmlayer.html