CNN-LSTM多特征二分类:基于MATLAB的工程实现与数据替换指南
2026/9/16 14:07:56 网站建设 项目流程

简介:面向需要快速搭建序列数据分类模型的MATLAB用户,这份基于卷积神经网络与长短期记忆网络(CNN-LSTM)的分类预测代码可直接用于多特征输入、单输出的二分类及多分类场景。模型融合CNN的空间特征提取能力与LSTM的时间依赖建模能力,对视频分析、动作识别、气象预报等时序任务具有参考价值。程序内注释详细,替换为自己的Excel数据集后即可运行,并能输出分类效果图、迭代优化图和混淆矩阵图,便于验证模型性能。压缩包共9个文件,包含MATLAB主程序、数据集表格、结果图像及说明文本,整体仅322KB,轻量易部署。目前已有250人学习下载,适合具备基础MATLAB经验、希望快速上手CNN-LSTM分类任务的开发者参考使用。

1. CNN-LSTM 能做什么:这个 MATLAB 工程解决的是哪类问题

多特征输入、单输出、二分类,这三个词放在一起,几乎就是工业场景里最常见的表格型数据建模需求:传感器采集十几个特征,预测设备是否故障;财务指标输入,判断客户是否违约;脑电或振动信号特征,给出状态标签。传统做法不是随机森林就是 XGBoost,但如果数据本身是带时间顺序的序列,或者特征之间存在局部关联,树模型会丢掉顺序信息。这个项目把 CNN 和 LSTM 串成一条流水线:CNN 负责在时间维度上做局部特征提取,LSTM 负责学习前后依赖,最后接一个全连接层输出分类。我拆了一遍这个工程,结构比我预想的干净,main.m 一个入口跑完训练、验证和画图,替换数据就能用,适合刚入门深度学习的 MATLAB 用户,也适合想快速验证 CNN-LSTM 在自己数据集上效果的研究生和工程师。我下面的拆解会按“数据怎么组织、CNN 怎么写、LSTM 怎么接、训练怎么配、替换数据时坑在哪”的顺序来写,每一段都会给出可以直接落到 MATLAB 2019 及以上版本跑的代码。

2. 读懂工程文件结构,先把数据维度对齐

这个压缩包里的东西不算多,但每个文件都对应一个明确阶段:main.m是全部流程的入口,数据集.xlsx是原始输入,1.png2.png3.png4.png是运行过程中保存的效果图,结果文件夹里放着训练完成后的详细输出。我实际拆解时首先做的不是读代码,而是先把数据打开看形状,因为这个工程最大的隐含约束在于:CNN-LSTM 对输入维度极其敏感,原数据集的行列顺序和网络层的输入要求必须对齐,否则代码跑不通。

文件/目录作用我关注的点
main.m主脚本,数据读取到画图全流程数据读取方式、网络层定义
数据集.xlsx特征和标签,多列特征 + 单列标签特征列数、标签是 0/1 还是类别单词
1.png / 2.png分类效果图真实标签和预测标签的对比展示
3.png迭代优化图观察 loss 是否收敛
4.png混淆矩阵图看哪些类别容易被混淆
结果训练过程的输出和保存的模型验证准确率、混淆矩阵等

数据集的组织方式我见过两种:一种是行是样本、列是特征、最后一列是标签;另一种是行是时间步、列是特征、标签独立存放。这个工程我按前一种来读,这也是表格数据最常见的形式。拿到数据第一步不要直接送进网络,先用sizehead确认行列,然后做归一化。常见做法是 z-score 标准化,但有一点容易错:标准化系数必须在训练集上计算,再应用到测试集,不能整个数据集统一求均值和方差,那样会造成信息泄漏,测试集评估结果虚高。

data = readmatrix('数据集.xlsx'); X = data(:, 1:end-1); % 多特征输入 Y = data(:, end); % 单输出标签 idx = randperm(size(X,1)); % 打乱顺序 split = round(0.8 * length(idx)); Xtrain = X(idx(1:split), :); Ytrain = Y(idx(1:split), :); Xtest = X(idx(split+1:end), :); Ytest = Y(idx(split+1:end), :); % 在训练集上计算均值和标准差 mu = mean(Xtrain); sg = std(Xtrain); Xtrain = (Xtrain - mu) ./ sg; Xtest = (Xtest - mu) ./ sg;

代码逻辑上先按 8:2 划分训练测试集,再对特征矩阵做标准化。musg是 1×特征数的向量,用到测试集上时是广播运算。注意 MATLAB 的std默认是除以 n-1 的样本标准差,如果你的数据某个特征方差为 0,这里会出现 NaN,需要在标准化前先删掉常数特征列。做完这一步后,数据形状是“样本数 × 特征数”,但这个形状还不能直接喂给序列网络。

MATLABtrainNetwork对序列输入要求的是 cell 数组,每个 cell 里是一个特征数 × 时间步的矩阵。时间步这个维度是很多人第一次用这个工程会卡住的地方:如果原始数据是一行一个样本的表格,每个样本只有一个时刻,那么输入就是特征数 × 1的单步序列;如果你的数据本身就是按时间先后排列的多变量序列,比如振动信号按采样点排列,那就要把连续 K 个时刻组成一个滑窗,每个窗口作为一个样本。后面第 5 章我再单独讲滑窗怎么做,先把单样本单时刻的情况跑通。

% 转换为 cell 数组:每个样本是一个 特征数×1 的序列 XtrainSeq = cell(size(Xtrain,1), 1); for i = 1:size(Xtrain,1) XtrainSeq{i} = Xtrain(i,:)'; % 转置成 特征数×1 end XtestSeq = cell(size(Xtest,1), 1); for i = 1:size(Xtest,1) XtestSeq{i} = Xtest(i,:)'; end % 标签转 categorical,分类网络要求类别型标签 YtrainCat = categorical(Ytrain); YtestCat = categorical(Ytest);

这里最关键的是循环里的转置操作:原始数据一行是 1×特征数,转置后变成 特征数×1,这才是sequenceInputLayer期望的输入格式。标签必须转成categorical类型,原因在于classificationLayer的损失函数要求类别索引或者类别标签,数值型的 0/1 直接作为 double 传入会在训练时报维度不匹配。完成这一步,数据流水线就通了,接下来才能开始搭网络层。

3. CNN 特征提取模块:为什么用卷积层处理多特征输入

这个工程里 CNN 的作用不是图像识别,而是对特征序列做局部感知。多特征输入的一个天然问题是特征之间存在不同尺度的关联,比如传感器数据里温度、压力、振动三个通道在相邻采样点上的组合模式往往比单点值更有判别力。一维卷积在时间轴上滑动,每次覆盖一个小窗口,窗口内的特征组合会被压缩成一个抽象值。这样做的直接收益是:LSTM 后面看到的输入已经不再是原始噪声特征,而是经过卷积核提炼的局部模式。

convolution1dLayer是 MATLAB Deep Learning Toolbox 里从 R2019a 开始支持的一维卷积层,这个工程的版本要求正好对齐。定义网络时我通常这样组织:

layers = [ sequenceInputLayer(numFeatures) % 输入层,numFeatures 为特征数 convolution1dLayer(3, 16, 'Padding', 'same') % 卷积核长度3,16个滤波器 reluLayer() maxPooling1dLayer(2, 'Stride', 2) % 池化窗口2,步长2 convolution1dLayer(3, 32, 'Padding', 'same') reluLayer() maxPooling1dLayer(2, 'Stride', 2) ];

我先解释卷积层参数的含义:convolution1dLayer(3, 16)里第一个参数 3 是卷积核长度,代表每次看连续 3 个时间步;第二个参数 16 是滤波器数量,也就是输出通道数。Padding设为'same'可以让卷积前后序列长度不变,不至于每过一层时间维度就缩短。池化层的作用是降采样,maxPooling1dLayer(2, 'Stride', 2)把序列长度减半,保留窗口内的最大值。这里用到两个卷积池化对,是因为一层卷积往往只能捕捉短距离局部模式,堆叠两层之后,第二层卷积的感受野会覆盖到更长的原始序列区域。

卷积核长度 3 是一个比较稳妥的默认值,对应“只看相邻三个时间点”。如果你的特征序列有更强的周期性,比如电网负荷数据每 24 个点一个周期,可以把这个参数调大到 5 或 7。滤波器数量 16 到 32 是轻量配置,特征数本身不多时,继续加宽只会增加参数量,效果提升有限。这一层设计的关键约束在下游:每个卷积池化对会把序列长度除以 2,两个对就是除以 4,所以进入 LSTM 层时序列剩余长度是原始长度的四分之一。如果原始序列很短,比如只有 8 个时间步,池化两次后只剩 2 步,LSTM 能学到的时序依赖就非常有限,这种情况下应当把池化层的Stride改成 1 或者去掉一个池化层。

CNN 部分提取到的特征还需要做一个维度调整再交给 LSTM。MATLAB 在处理序列数据时,卷积层输出的数据格式是序列长度 × 通道数,这个格式可以直接作为 LSTM 层的输入,前提是内部做了自动处理。这里有一个容易踩的坑:如果你的 MATLAB 版本低于 R2019a,convolution1dLayer不存在,会直接报错说“未定义函数或变量”,提示去查文档,其实就是工具箱版本不够新,不是代码写错。我之前在 R2018b 上试过一次,只能改用convolution2dLayer仿造一维效果,绕了一圈不如直接升级版本,这也是工程注释里强调“要求 2019 及以上版本”的直接原因。

网络定义完整看是这样的:输入层指定特征数,经过两层卷积池化提取局部特征,然后进入 LSTM 层学习时间依赖。特征数这个占位符在写代码时要用实际数据的列数替换,如果不想写死,可以在读取数据后直接用size(Xtrain, 2)赋值。我一般会在工程里保留这个变量,方便换数据集时不用改网络结构代码。

4. LSTM 层与训练配置:时序依赖建模和输出层设计

CNN 把局部模式提炼完之后,LSTM 负责捕捉这些模式在时间顺序上的依赖关系。LSTM 的核心机制是门控:输入门决定当前信息有多少写入记忆单元,遗忘门决定上一时刻的记忆保留多少,输出门控制记忆如何影响当前输出。对这种多特征二分类任务,LSTM 层不需要太宽,隐藏单元太多反而容易在训练集上过拟合,尤其是在样本量只有几百条的时候。

LSTM 层接上输出层的代码一般是这样的:

lstmLayers = [ lstmLayer(64, 'OutputMode', 'last') % 64个隐藏单元,只输出最后时刻 fullyConnectedLayer(2) % 二分类,输出2个神经元 softmaxLayer() classificationLayer() ];

lstmLayer(64)的第一个参数是隐藏单元数。隐藏单元数量可以理解为 LSTM 记忆容量,64 是一个中间值,特征少样本少可以降到 32,特征多序列长可以加到 128。'OutputMode', 'last'是分类任务的标准配置:整个序列经过 LSTM 处理后,只取最后一个时间步的隐藏状态作为整个序列的特征表示,然后过全连接层。如果是序列到序列的任务,比如逐点预测,才用'sequence'。全连接层的 2 对应两个类别,二分类只有 0 和 1 两个输出,softmax 把 2 个输出转成概率,概率大的那个类别就是预测结果。如果做多分类,只需要把fullyConnectedLayer(2)的数字改成类别数,其余不用动。

CNN 和 LSTM 两层结构合并后,训练配置是决定这个工程能不能收敛的关键。我在 main.m 里看到训练参数的写法和下面这段基本一致,它是训练效果图 3.png 能否正常生成的决定因素,同时也是最容易出问题的部分:

options = trainingOptions('adam', ... 'MaxEpochs', 100, ... 'MiniBatchSize', 32, ... 'InitialLearnRate', 0.005, ... 'ValidationData', {XtestSeq, YtestCat}, ... 'ValidationFrequency', 20, ... 'Plots', 'training-progress', ... 'Verbose', false); net = trainNetwork(XtrainSeq, YtrainCat, [layers; lstmLayers], options);

训练选项参数的具体意义如下:adam优化器适用于大多数表格数据场景,比传统 SGDM 收敛更平稳;MaxEpochs100 表示整个训练集迭代 100 轮,数据量小时建议不超过 200,否则过拟合;MiniBatchSize32 是每次迭代送入的样本数,设置原则是能被训练样本数整除,否则最后一个批次会不足;InitialLearnRate0.005 对 CNN-LSTM 这种量级的网络来说适中,偏大会导致 loss 振荡不收敛,偏小收敛太慢;ValidationData传入测试集,每个ValidationFrequency(这里是 20 次迭代)在验证集上算一次准确率,这是 3.png 迭代优化图里验证曲线数据的来源。Verbose设为 false 是为了不在命令行里刷大量训练日志,只看图。

训练完成后要做两件事:在测试集上预测并计算准确率,同时画出混淆矩阵。准确率是最直观的指标,但二分类任务只看准确率不够,正负样本不平衡时准确率会骗人。比如 95% 的样本是类别 0、5% 是类别 1,全猜 0 也有 95% 准确率,所以必须看混淆矩阵。

YPred = classify(net, XtestSeq); acc = sum(YPred == YtestCat) / numel(YtestCat); disp(['测试集准确率: ', num2str(acc)]); % 混淆矩阵 figure; confusionchart(YtestCat, YPred);

classify自动完成前向传播并返回类别标签,最后一行confusionchart生成的就是 4.png。混淆矩阵对角线的数值表示分类正确的样本数,非对角线上的数字表示误分类情况,比如类别 0 被预测成类别 1 的样本数。如果看到非对角线数值很高,再去看预测错误的样本具体是哪一个,往往能发现特征分布重叠的区域,这是后续做特征工程的入口。整个训练输出和混淆矩阵数据会保存到结果文件夹,方便多次实验横向对比。

训练不收敛是这类工程最常见的卡壳点,现象是training-progress图里训练 loss 下降一段后开始振荡,或者干脆不下降。常见原因按概率排序:学习率太大(降到 0.001 或 0.0005 试);数据没有打乱导致同类别样本连续出现(randperm顺序打乱时每个类别的样本要均匀分布);输入数据里有 NaN 或 Inf(isnan(sum(X,2))检查一下);标签类别不平衡(用classCounts = countcats(Ycat)看每类数量)。这些检查项中,前两个对训练曲线的影响最显著,建议调试顺序从学习率开始,改一次跑一次,别一次性改多个参数,否则你不知道是哪个改动起的效果。

5. 替换你自己的数据时,按这个顺序验证,能少走一半弯路

拿到这套工程直接换上自己的 Excel 数据,第一次跑通的人大概三分之一,剩下三分之二的问题都集中在几个固定环节。我建议按下面的顺序逐个检查,每步都做一次轻量验证,比一次改到底再跑快得多。

第一步,验证数据读取后的形状。readmatrix读 xlsx 文件时如果文件里有文本表头,读进来会变成 NaN 矩阵,后面所有计算全乱。读完后第一句disp(size(data)),行列数要符合预期,最好再disp(data(1:3, :))看前几行具体数值。第二步,确认标签类型。categorical(Ytrain)要求 Y 是数值向量,如果你的标签是“正常”“故障”这类文本,先映射成 0/1 数字再转 categorical;直接用文本也能转,但confusionchart显示类别名时顺序可能和预期不一致。第三步,跑通训练和预测的完整流程,用原始工程自带的数据集.xlsx先跑通,确认环境没问题,再换自己的数据。如果自带数据能出图、换了自己数据就不行,问题一定出在数据组织和维度上,不在网络结构。

第四步,也是这个工程最有意思的扩展点:如果你的数据是真·时间序列,不要用前面提到的单步输入方式,而是用滑窗把历史信息拼进去。举个例子,振动传感器每隔一分钟采一次,判断设备故障,单看某一分钟的 5 个特征值很难判断,但如果把最近 10 分钟的特征堆叠起来,故障前的变化趋势就会显现。滑窗构造方法如下:

function Xwin = makeWindow(X, winSize) n = size(X, 1); nFeat = size(X, 2); Xwin = zeros(n - winSize + 1, nFeat * winSize); for t = 1:n - winSize + 1 seg = X(t:t+winSize-1, :); Xwin(t, :) = seg(:)'; % 展平成一行 end end

这段代码winSize是窗口长度,比如 10,代表每个样本包含连续 10 个时间步;seg(:)'把 10 行 5 列的子矩阵展平成一行 50 个值,作为新的特征向量。窗口大小选择一般遵循一个原则:你希望网络看到多长历史,就设多大。窗口越大信息越完整,但样本数会减少(原来的 n 个时间点只剩 n-winSize+1 个窗口),特征维度也会成倍增加,训练时间变长。滑窗构造好之后,把每个窗口当成一个样本,后面的流程回到第 2 章的标准化和网络训练部分就行。检验滑窗是否有效的粗办法是:先用 winSize=1 跑一次作为 baseline,再逐步增大窗口,如果准确率没有明显提升,说明时序信息对这个任务帮助有限,可以继续用单步输入减少计算量。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询