假设一个人把手机固定在腰间,然后走路、上楼、下楼、坐下、站立或者躺下。手机里的加速度计和陀螺仪会不断记录身体运动。现在给你其中一小段传感器数据,让程序判断这个人在做什么,你会从哪里下手?
只看某一个时刻往往不够。坐着与站着在某些瞬间都很平稳,走路与上下楼也可能出现相似的振动。真正有用的信息,一部分藏在相邻几个采样点的局部起伏里,另一部分藏在整段动作的先后变化里。
这正是 CNN-RNN 混合模型适合处理的问题:CNN 先找局部模式,RNN 再把这些模式按顺序串起来,最后分类器从几个候选类别中选一个。但"先 CNN、再 RNN"还远远算不上讲懂。下面先不看网络结构名,直接跟着一条真实数据,看看它在每一站究竟变成了什么。
一、先说清楚:这到底是什么任务
本文做的是多分类。输入是一条已经截取好的动作片段,输出是"行走、上楼、下楼、坐着、站立、躺着"中的一个。它不是回归,因为输出不是房价、温度这类连续数值;它也不是时间序列预测,因为我们没有要求模型猜下一时刻的传感器读数。
"分类任务""输入有顺序""人体活动识别"是三个不同层面的概念。分类是要做什么;有顺序是数据怎样组织;人体活动识别只是一个具体应用。把这三件事分开,后面才能理解为什么同一个函数还可以处理 Iris 表格和 MNIST 图像。
二、一条原始数据长什么样
本文使用 UCI HAR 人体活动识别数据。原始研究让 30 名受试者把智能手机佩戴在腰部,完成 6 种日常活动,手机以 50 Hz 记录加速度和角速度等信号。
在本项目里,一条样本被整理成 9×128:9 表示同一时刻记录的 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 个时间点。走路时可能出现一次周期性起伏,上楼时可能出现另一种幅度和方向组合。一个局部窗口虽然看不到整段动作,却能发现"这里发生过一次怎样的变化"。
一维卷积就是一把反复使用的局部尺子。本文的 kernelSize=5,表示每次观察相邻 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 步
本文使用 poolSize=2 的最大池化。可以把相邻两个位置看成一组,从中保留更突出的响应。这样,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_005
MATLAB 代码中,rnnHidden=64 决定这本"记忆笔记"有多宽,OutputMode='last' 表示只把读完整段后的最后摘要交给分类器。隐藏单元太少,可能装不下足够的信息;太多则训练更慢,也更容易过拟合。
本文默认使用 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.9014,Macro-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 函数。设置好输入和参数后,真正启动模型只需要一行:
matlab
[foreData, foreDataTrain, net, info] = FunClassCNNRNN(X, Y, options);
这一行背后会依次完成输入轴适配、训练/验证/测试划分、训练集归一化、CNN-RNN 网络搭建、分类训练、验证集早停、最佳网络选择、三集合预测、分类指标计算和五张结果图的绘制与保存。
函数的三个输入是:X 为数值型输入数组;Y 为每条样本的类别标签;options 管理输入适配、数据划分、网络结构、训练过程、归一化和绘图。四个输出分别是:foreData 为测试集预测标签,foreDataTrain 为训练集预测标签,net 为训练好的网络,info 保存索引、验证预测、类别得分、各集合指标、混淆矩阵、训练记录、归一化参数、适配后的尺寸和完整选项。
以本文 HAR 数据为例,完整调用如下:
matlab
%% 1. 导入数据
load('har_activity_data.mat', 'X', 'Y', 'splitLabels');
%% 2. 输入适配与固定划分
options.sampleDimension = 'auto'; % 自动识别样本维
options.sequenceAxis = 2; % 单样本[9,128]的第2维为时间轴
options.splitLabels = splitLabels; % 1训练、2验证、3测试,按受试者隔离
options.rTrain = 0.80; % 无splitLabels时,训练+验证数据占80%
options.validationRatio = 0.15; % 验证集占训练+验证数据的15%
options.shuffle = true; % 训练批次打乱
options.seed = 42; % 固定随机过程,0表示不固定
%% 3. CNN-RNN 网络结构
options.networkType = 'LSTM'; % 可选LSTM、GRU、BiLSTM
options.convChannels = [32, 64]; % 两层卷积的输出通道数
options.kernelSize = 5; % 每次观察相邻5步,必须为正奇数
options.poolSize = 2; % 最大池化窗口,1表示不池化
options.rnnHidden = 64; % 循环层隐藏单元数
options.dropout = 0.20; % 分类头前随机失活比例,范围[0,1)
%% 4. 训练参数
options.solverName = 'adam'; % 可选adam、sgdm、rmsprop
options.maxEpochs = 35; % 最大训练轮数
options.learnRate = 0.001; % 初始学习率
options.batchSize = 64; % 批尺寸
options.earlyStoppingPatience = 8; % 验证损失连续8次不改善则停止,0关闭
options.learnRateSchedule = 'piecewise'; % 可选none或piecewise
options.learnRateDropPeriod = 15; % 每15轮降低一次学习率
options.learnRateDropFactor = 0.5; % 每次变为原来的50%
options.classWeight = 'auto'; % auto、none或每类权重数组
options.executionEnvironment = 'auto'; % auto、cpu或gpu
%% 5. 数据处理与图像输出
options.mapflag = 'on'; % 仅用训练集统计量进行标准化
options.figflag = 'on'; % 自动绘图并保存
options.showFigures = 'on'; % 本地运行时弹出图窗
options.caseName = 'HAR人体活动'; % 结果图文件名前缀
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 逐段降低。
classWeight='auto' 会根据训练集类别频数自动给少数类更高权重;类别本来均衡时也可设为 none。executionEnvironment 选择 CPU、GPU 或自动判断。mapflag 控制训练集标准化,figflag 控制是否生成图片,showFigures 控制图片是否在桌面弹出;图片即使不弹出,只要 figflag='on' 仍会保存到 figure 目录。
MATLAB 版可切换的 RNN
matlab
options.networkType = 'LSTM'; % 默认,门控记忆较完整
options.networkType = 'GRU'; % 结构更精简
options.networkType = 'BiLSTM'; % 双向读取,计算量和参数更多
三行实际只保留一行。当前 MATLAB 核心函数只接受这三个名称,不支持普通 RNN、BiRNN 或 BiGRU。
函数结束后,会自动保存收敛过程、测试集混淆矩阵、各类别指标、分类结果对比和训练/验证/测试指标五张图。前文解释的是这条数据旅程为什么成立;这一节解决的是怎样把同一套严谨流程真正跑起来。
如何获取完整代码
本文使用的完整 MATLAB 代码包括核心函数、HAR/Iris/MNIST 三个演示脚本、数据集、分类评价函数、详细参数说明和自动生成的可视化图表。
需要代码的同学可以在公众号后台回复关键词 CNN-RNN分类 获取,也可以通过菜单栏或留言联系。
参考资料
- UCI HAR 数据集官方页面:https://archive.ics.uci.edu/dataset/240/human%2Bactivity%2Brecogni-tion%2Busing%2Bsmartphones
- MATLAB 一维卷积层文档:https://www.mathworks.com/help/deeplearning/ref/nnet.cnn.layer.convolution1dlayer.html
- MATLAB LSTM 层文档:https://www.mathworks.com/help/deeplearning/ref/nnet.cnn.layer.lstmlayer.html