
做一维信号二分类这件事我前前后后在Matlab里折腾了不少次从最早的语音清浊音判断到后来帮朋友处理心电图室性早搏的识别试过BP网络、SVM、LSTM最后真正用得顺手、稳定能出结果的还是CNN。很多人一提到CNN就想到图像觉得和语音、ECG这类一维信号不沾边实际上一维卷积处理时序信号是CNN的强项而且Matlab的Deep Learning Toolbox把从数据准备、网络搭建到训练评估整条链路都封装得很顺特别适合快速验证科研思路或者做课程项目。这篇文章我打算把一个完整的“Matlab CNN 一维信号二分类”工程从头到尾拆开讲包括数据怎么切窗、网络怎么搭、参数怎么调、源码怎么改以及我实际跑数据时踩过的几个坑。文章里给的代码不是那种跑不通的示意代码而是我尽量组织成可以直接套用的框架你只需要把自己的信号数据和标签按格式塞进去改几个参数就能跑起来。1. 项目全貌与方案选型为什么用Matlab又为什么用CNN1.1 这个工程到底要解决什么问题先说清楚这个项目的定位给定一段一维信号比如一段语音、一段心电、一段振动信号我们要让程序自动判断它属于哪一类而且只能是两类要么“是A”要么“是B”。典型的场景包括语音信号判断当前语音帧是清音还是浊音或者检测一段音频里有没有人声语音活动检测VAD。心电图信号从连续心电中切出的单个心拍判断它是正常窦性心拍还是异位搏动比如室性早搏PVC。工业振动信号判断某个轴承是正常状态还是故障状态典型的两分类是“正常/内圈故障”。肌电信号判断手势“握拳/张开”或肌肉“疲劳/不疲劳”。这类问题的共同点是原始输入不是一张图片而是一串按时间排列的数值点。很多初学者上来就会想那我要不要先做FFT变成频谱图然后再丢给图像CNN可以但没必要。直接对时域波形做一维卷积把特征自动学出来效果通常不差而且省掉了大量手动特征工程的时间。1.2 CNN凭什么能处理一维信号CNN的核心机制是局部感受野和权值共享这两个特性并不仅仅针对图像。对于一维信号来说每个时刻的数据点与其前后若干点之间本来就有很强的局部相关性比如语音信号里一个音素的能量主要集中在几十毫秒内心电信号里一次QRS波群的形态也就占据几十个采样点。用一个小卷积核沿着时间轴滑动就相当于让网络自动学习这些局部形态模式。把一维信号用二维CNN处理本质上就是一个“看着像图像”的技巧长度为L的一维信号reshape成L×1的矩阵通道数设为1这样就能直接套用Conv2D层。真正在Matlab里做这一步的人很多因为可以复用大量成熟的图像CNN经验而且在画网络图、可视化中间特征的时候也非常直观。当然Matlab也提供了convolution1dLayer但我在实际使用中反而是“二维卷积核高度等于窗口长度、宽度为1”的写法更方便排查问题也方便在层结构里做各种debug。1.3 为什么选Matlab而不是Python问得最多的问题就是这个。我的答案很直接如果是纯工程部署Python的生态确实更丰富但如果目标是“把算法跑通、把论文实验做出来、把课设交上去”Matlab在很多环节省事得多。下面这张对比表是我自己的体会数据预处理Matlab有Signal Processing Toolboxfilter、resample、findpeaks这些函数开箱即用Python需要scipy、numpy、librosa各种搭配而且采样率、数据类型稍不注意就一堆坑。网络搭建Matlab用layers数组一层一行像填表格一样语法对新手非常友好Python的PyTorch虽然灵活但需要理解nn.Module、forward这些概念。训练可视化trainNetwork自带训练进度图loss和accuracy实时更新动一下鼠标就能看Python要装tensorboard或者matplotlib自己画。部署小模型Matlab可以把训练好的网络直接转成Simulink模型或者生成C/C代码对嵌入式验证很方便。团队合作很多做信号处理的老师、课题组和公司老项目都是Matlab的你拿Python写一遍还得再翻译回来。当然Python的优势是开源模型多、社区资料多如果你要跑特别新的预训练模型Matlab确实跟不上。但就“CNN一维信号二分类”这个范围Matlab完全够用而且调试体验更顺。2. 数据准备从原始信号到可直接输入网络的训练集2.1 一维信号二分类的数据长什么样在开始搭网络之前先要搞清楚数据怎么组织。CNN训练需要的是“一堆样本 每个样本的标签”。对于一维信号一个样本就是一段长度为L的数值向量比如语音处理里常取25ms的帧采样率16kHz时就是400个点心电处理里以R峰为中心取前后各100个点一共201个点。所以我强烈建议你在动任何代码之前先把数据整理成下面这两种格式之一格式Acell数组方式XTrain是一个N×1的cell其中第i个元素是(L×1)的double列向量YTrain是N×1的categorical标签。格式B四维数组方式XTrain是一个L×1×1×N的四维double数组对应图像CNN的H×W×C×NYTrain同样是categorical标签。格式A看起来更像一维信号配合sequenceInputLayer使用格式B是把信号当成“单列灰度图”配合imageInputLayer使用。我后面给的源码主要基于格式B因为这种写法基于imageInputLayer兼容性和可视化都最好很多老版本Matlab也能跑。2.2 怎么把长信号切成训练样本实际项目里我们拿到的往往是一段很长的连续信号比如一条10分钟的心电记录、一段3分钟的语音文件不能整段丢给网络而且也没有那么多人工标注的边界。所以切窗是关键步骤我的建议是窗口长度不要贪长。语音帧取20~50ms即可心电单拍取R峰前后约0.4~0.6s振动信号取1~3个转频周期。窗口太长网络要学的东西太多反而忽略局部形态。滑动步长训练样本不足时用重叠滑窗来扩增。比如心电信号R峰间隔平均0.8s可以以R峰为中心截取0.5s窗口如果样本不够就在旁边再偏移几个采样点截取形成重叠扩增。标签对齐窗口中心落在什么位置就标这个窗口的类别。做心电二分类时如果窗口中心是正常R峰标为“正常”如果中心是早搏的宽大畸形QRS就标“异常”。集外划分重要的一点划分训练集/测试集时要以“患者”或“录音文件”为单位而不是把所有窗口混在一起随机划分否则同一个人的心拍既在训练集又在测试集识别率会虚高到95%以上实际落地时打回原形。我在做心电PVC识别时习惯以每例患者为单元比如10个患者的数据6个患者做训练、2个做验证、2个做测试这样才接近真实场景。2.3 预处理滤波、归一化和数据增强预处理不用做太狠因为CNN本身能学特征但三件事我建议一定要做去除基线漂移和工频干扰语音和ECG对低频漂移都很敏感用高通滤波比如0.5Hz~1Hz截止去掉基线用50Hz/60Hz陷波器去掉工频。这个直接在Matlab里用designfilt和filter函数完成。幅值归一化每个样本减去自身均值再除以自身标准差也就是z-score归一化。这一步特别重要因为不同录音的响度、不同导联的增益差异太大了不归一化的话网络会把音量高低当特征而不是形态。数据增强对于一维信号实用且不容易出错的增强方法有加随机小噪声给原始信号加上幅度为信号标准差5%~10%的高斯白噪声。时间缩放对一个样本的点数做轻微拉伸或压缩例如改变±5%的采样数再插值还原。幅值缩放随机乘以0.9~1.1的增益因子。注意一个细节数据增强必须放在训练集和测试集划分之后否则会引入数据泄漏。我见过有人先把整个数据集做增强再划分结果网络“记住”了同一条信号不同噪声版本的模式测试分数高得离谱。2.4 把一维信号“伪装”成图像输入的格式约定这是Matlab写代码时最关键的一个数据格式问题。如果你用的是imageInputLayer那输入数据的每个样本必须是一个H×W×C的数组对于一维信号我们把它设为L×1×1。把所有样本拼起来之后整份训练集就是一个L×1×1×N的四维数组这个维度顺序是Matlab训练函数默认要求的高度×宽度×通道×样本数。我踩过最大的坑就在这里。一开始我用cell数组存了一堆L×1的向量直接喂给imageInputLayer结果报维度不匹配。后来把数据转换成四维数组问题才解决。转换代码其实很简单dataCell {randn(400,1), randn(400,1)}; % 假设两个样本 labels categorical([0; 1]); % 转成 [400 1 1 2] 的数组 X cat(4, dataCell{:}); % 或者循环填充 X zeros(400, 1, 1, numel(dataCell)); for i 1:numel(dataCell) X(:,1,1,i) dataCell{i}; end如果不想这么麻烦也可以直接用cell数组配合sequenceInputLayer网络定义稍作调整。但我要提醒一句在Matlab的R2021之后sequenceInputLayer和后续层类型匹配有一定限制新老版本行为不完全一致所以对于小白先用四维数组的imageInputLayer路线最稳。3. 网络设计与关键参数调整3.1 基线网络架构卷积-池化-全连接CNN处理一维信号网络骨架其实非常固定输入层 → 卷积层 → 激活函数 → 池化层 → 重复若干次 → 全连接层 → Softmax → 分类层。这样一个架构对语音/心电/振动都能有不错的基线效果。我在项目里常用的基线结构是输入层imageInputLayer([L 1 1])其中L是窗口长度。第一段卷积核长度7~11滤波器数量16~32padding设为same保持时间长度不变加BatchNorm用ReLU再用最大池化做2倍下采样。第二段卷积核长度5~7滤波器数量32~64padding same加BatchNormReLU池化2倍下采样。第三段可选卷积核长度3~5滤波器数量64~128后面可以不池化或再用全局平均池化压到一维。分类头全连接层节点数64~128→ Dropout(0.5) → 全连接层(2) → Softmax → classificationLayer。下面是一个可以直接替换数据的完整网络定义窗口长度我以心电200个采样点为例windowLength 200; numFilters 32; layers [ imageInputLayer([windowLength 1 1], Name, input) convolution2dLayer([11 1], numFilters, Padding, same, Name, conv1) batchNormalizationLayer(Name, bn1) reluLayer(Name, relu1) maxPooling2dLayer([2 1], Stride, [2 1], Name, pool1) convolution2dLayer([7 1], numFilters * 2, Padding, same, Name, conv2) batchNormalizationLayer(Name, bn2) reluLayer(Name, relu2) maxPooling2dLayer([2 1], Stride, [2 1], Name, pool2) convolution2dLayer([5 1], numFilters * 4, Padding, same, Name, conv3) batchNormalizationLayer(Name, bn3) reluLayer(Name, relu3) maxPooling2dLayer([2 1], Stride, [2 1], Name, pool3) fullyConnectedLayer(128, Name, fc1) reluLayer(Name, relu_fc) dropoutLayer(0.5, Name, dropout) fullyConnectedLayer(2, Name, fc_out) softmaxLayer(Name, softmax) classificationLayer(Name, output) ];这里用convolution2dLayer([11 1]表示卷积核在时间方向是11个点在“宽度”方向是1这样作用在L×1的输入上就等效于一维卷积。pooling层同样用[2 1]只沿时间轴压缩。3.2 关键参数怎么定值越大越好吗网络参数是初学者最容易迷茫的地方。我给的基线数值不是随手写的背后都有自己的逻辑卷积核大小11、7、5逐段减小。第一层用大核是为了在原始波形上看更宽的局部形态比如心电的QRS波群本身宽度约80~120ms如果采样率250Hz那就是20~30个点第一层用[11 1]能覆盖差不多半个波群后面层逐层减小核尺寸是为了在更高层特征上做更精细的判别。核太小比如3也能跑但第一层感受野不足很多形态信息学不到。滤波器数量16/32/64这种指数增长是最常用的经验值。越深的层特征抽象程度越高需要更多的通道去表达不同模式。但要注意对于一维信号样本信息量比图像少很多通道数翻到128以上就很容易过拟合尤其是训练样本只有几千条的时候。池化为什么用最大池化、步长2最大池化在信号处理里相当于一种非线性下采样能保留局部区域里最“突出”的响应对小幅时移有容忍度。步长2是最常用的每一步信息减半和滤波器数量翻倍形成一种平衡很多经典CNN都这么干。步长太大信息损失太快步长太小又起不到降维作用。Dropout放哪里我放在第一个全连接层之后系数0.5。一维信号CNN网络参数量主要集中在全连接层所以dropout放在这里收益最大。卷积层后面一般不dropout因为卷积层参数共享本身还有一定正则化作用dropout更容易造成欠拟合。3.3 训练选项优化器、学习率、BatchSize和Epoch数Matlab的trainNetwork用trainingOptions配置训练参数。我长期使用的组合是这样options trainingOptions(adam, ... InitialLearnRate, 0.001, ... MaxEpochs, 30, ... MiniBatchSize, 64, ... ValidationData, {XValidation, YValidation}, ... ValidationFrequency, 20, ... Shuffle, every-epoch, ... Plots, training-progress, ... Verbose, true, ... LearnRateSchedule, piecewise, ... LearnRateDropFactor, 0.5, ... LearnRateDropPeriod, 10);这里逐项说理由优化器选adam还是sgdm对小规模一维信号分类adam收敛快、对学习率不那么敏感是省心选择。sgdm在调好学习率后泛化可能更好但需要更多epoch。我建议先adam拿到基线再用sgdm微调。初始学习率0.001这是CNN训练最常见的起点。学习率0.01往往震荡不收敛0.0001又太慢0.001是个安全区间。如果训练loss完全不动调到0.003试试。MiniBatchSize 64取决于数据量。样本数只有一两千时用32或16更好小batch噪声有利于摆脱局部最优点数据上万时64~128都行。BatchSize太大容易内存溢出尤其当窗口长度很长时。MaxEpochs 30一维信号CNN参数并不多30轮通常足够看到收敛趋势。我见过的坑是一上来就设1000epoch训练到50轮loss已经不变还在傻跑。ValidationData和ValidationFrequency验证集一定要有否则你就是蒙着眼训练。每20个iteration跑一次验证能实时看到是不是过拟合。Shuffle every-epoch每个epoch都重新打乱数据顺序避免网络学到样本顺序带来的伪规律。LearnRateSchedule piecewise每10轮学习率减半这是很务实的做法训练后期用更小步长微调权重。3.4 完整的Matlab源码框架替换数据即可跑我把从数据处理到评估输出的完整流程整理成一个框架自己用的时候也是在这个基础上改。这里以心电二分类为例但语音或其他信号只需改windowLength和预处理部分。% 1. 加载数据和标签 % 假设你已经把训练样本存在变量 dataTrain_cell 中每个元素是 [L x 1] double % 标签存在 labelTrain 中类型为 categorical例如 [0;1] 或 [正常;异常] % 测试集同理 load(trainData.mat); % 包含 dataTrain_cell, labelTrain load(testData.mat); % 包含 dataTest_cell, labelTest windowLength 200; % 和你的样本长度保持一致 % 2. 把cell转换成四维数组 XTrain zeros(windowLength, 1, 1, length(dataTrain_cell)); for i 1:length(dataTrain_cell) x dataTrain_cell{i}; x (x - mean(x)) / (std(x) eps); % z-score归一化 XTrain(:, 1, 1, i) x; end XTest zeros(windowLength, 1, 1, length(dataTest_cell)); for i 1:length(dataTest_cell) x dataTest_cell{i}; x (x - mean(x)) / (std(x) eps); XTest(:, 1, 1, i) x; end YTrain categorical(labelTrain); YTest categorical(labelTest); % 3. 定义网络 layers [ imageInputLayer([windowLength 1 1], Name, input) convolution2dLayer([11 1], 32, Padding, same, Name, conv1) batchNormalizationLayer(Name, bn1) reluLayer(Name, relu1) maxPooling2dLayer([2 1], Stride, [2 1], Name, pool1) convolution2dLayer([7 1], 64, Padding, same, Name, conv2) batchNormalizationLayer(Name, bn2) reluLayer(Name, relu2) maxPooling2dLayer([2 1], Stride, [2 1], Name, pool2) convolution2dLayer([5 1], 128, Padding, same, Name, conv3) batchNormalizationLayer(Name, bn3) reluLayer(Name, relu3) maxPooling2dLayer([2 1], Stride, [2 1], Name, pool3) fullyConnectedLayer(128, Name, fc1) reluLayer(Name, relu_fc) dropoutLayer(0.5, Name, dropout) fullyConnectedLayer(2, Name, fc_out) softmaxLayer(Name, softmax) classificationLayer(Name, output) ]; % 4. 可视化网络结构 analyzeNetwork(layers); % 5. 训练 options trainingOptions(adam, ... InitialLearnRate, 0.001, ... MaxEpochs, 30, ... MiniBatchSize, 64, ... Shuffle, every-epoch, ... Plots, training-progress, ... Verbose, true); net trainNetwork(XTrain, YTrain, layers, options); % 6. 测试 YPred classify(net, XTest); accuracy mean(YPred YTest); fprintf(测试集准确率: %.2f%%\n, accuracy * 100); % 混淆矩阵 figure; confusionchart(YTest, YPred);这段代码我在多个数据集上跑过数据量在几千到几万条时都能正常收敛。如果你的目标是语音清浊音判断、故障诊断之类只需要把load数据那一段换成自己的输入并把归一化处理好就行。4. 训练、验证与结果评估4.1 训练曲线怎么看loss和accuracy反映什么训练开始后Matlab会蹦出一个训练进度窗口里面有两条曲线一条是训练集的loss和accuracy一条是验证集的loss和accuracy。我的经验是不要只盯着准确率要看loss曲线的形状。理想情况训练loss单调下降并趋于平稳验证loss同步下降两者的差距不大。如果训练loss非常低验证loss却居高不下说明模型过拟合了把训练样本的噪声细节背了下来这时候增加数据增强、加大dropout、减小网络层数或滤波器数量。如果训练loss和验证loss都在下降但验证loss中途突然反弹说明学习率太大了模型在最优解附近震荡调小学习率或者让学习率衰减更快一点。如果loss从头到尾纹丝不动大概率是数据预处理出了问题比如归一化没做、标签category顺序反了、或者学习率太小可以先调大学习率到0.01试试若还是不动就要检查数据到底是什么样的了。4.2 评估只看准确率远远不够二分类模型如果只看整体准确率很容易被“骗”。比如正常心拍和异常心拍是9:1那么一个“永远预测正常”的模型准确率也有90%看起来很高实际上根本没学会识别异常。所以我评估二分类模型时至少会看这几个指标混淆矩阵confusionchart直接给出TP、FN、FP、TN四个数字一目了然。灵敏度异常样本里被正确预测的比例也就是 Recall TP / (TP FN)。这个指标告诉你“真正的异常你抓住了多少”。特异度正常样本里被正确识别为正常的比例Specificity TN / (TN FP)。F1分数精确率和召回率的调和平均对不平衡数据比准确率更稳健。这些指标在Matlab里都可以手动算比如C confusionmat(YTest, YPred); TP C(2,2); TN C(1,1); FP C(1,2); FN C(2,1); sensitivity TP / (TP FN); specificity TN / (TN FP); F1 2 * TP / (2 * TP FP FN);4.3 那些“虚高”的结果数据泄漏和划分陷阱我在处理一维信号时遇到过最坑的事就是在数据划分上偷懒导致结果虚高。一开始我图省事把同一个患者所有心拍混在一起随机划分训练集和测试集测试准确率一度到98%。后来换成“按患者划分”同一个患者的心拍不会同时出现在训练集和测试集里准确率一下子掉到89%。这个差距不是因为模型变差了而是因为一开始的评估方式不公平——同一个人的信号质量、基线漂移模式、电极位置都相近网络很大程度上是靠“认出这个人”来分类的而不是靠“认出这个心拍形态”。同样的问题也出现在语音上。如果你把同一个人说的多段语音切窗混在一起划分语音内容高度重叠测试集里也会出现和训练集几乎一样的句子识别率虚高。所以我的硬性要求是划分训练/验证/测试集时颗粒度要按“人”或“文件”划分而不是按“窗口”划分。另一个容易忽略的泄漏点是预处理。如果你对整段测试信号做了全局归一化然后把测试信号的一部分窗口拿去当训练数据那测试集的信息已经通过归一化参数泄漏到了训练过程中。正确做法是先切窗再对每个窗口独立做归一化或者只用训练集统计量做归一化测试集沿用训练集的均值方差。最简单的办法就是对每个窗口独立做z-score这样可以彻底避开这个问题。5. 常见问题与排查技巧实录5.1 网络一直不收敛loss卡住不动这类问题我排查的顺序是先打印或画出一两个样本确认信号本身不是全零、不是NaN、不是方差为0。很多数据采集时会有坏段切窗后正好切到一段静音或导联脱落模型学了个寂寞。检查标签是否真的只有两类且两类都有足够样本。如果某一类只有几十条另一类几千条网络会直接学成“全预测多数类”loss照样降不下来。检查输入数据和输入层维度是不是一致。imageInputLayer的维度写错了trainNetwork会报错但有时候你用的是sequenceInputLayercell数据里有几个样本长度不一致也会导致训练无法进行。试试Adam 学习率0.001、去掉BatchNorm、只用两层卷积先让模型在一个简化的网络上跑通再逐步加复杂度。还有一个很多人不知道的小细节分类层要求训练标签必须是一个不连续的categorical而不是double。我遇到过直接用double数组当标签导致训练报错的情况用categorical函数转一下就好。5.2 GPU内存不足或训练太慢怎么办如果你用的是经典卷积结构一维信号本身很容易算但数据量大时也可能出现内存问题。优先减少MiniBatchSize从64降到32甚至16这是最简单有效的办法。如果窗口长度很长比如每个样本有几千个点可以尝试压缩采样率或者只在关键频段保留信息窗口短了、内存占用自然就低。如果你的Matlab是CPU版本没有可用的GPU可以显式设置ExecutionEnvironment为cpu避免软件反复尝试初始化GPU却失败。纯CPU训练一维CNN通常也不慢几千个样本、30个epoch一般几分钟就能跑完。如果真的要跑大规模数据建议先把样本格式写成mat文件用tall数组和minibatchqueue处理但那种场景我觉得不如直接用Python框架了。5.3 过拟合明显验证集效果远差于训练集过拟合在一维信号里特别常见原因很简单信号样本之间高度相似比如同一段语音里相邻帧几乎一样同一个人的正常心拍形态也差不多。网络很容易把训练集里的个性学进去而不是学类别共性。我的应对手段按优先级排序数据增强加噪、缩放、平移增强后样本多样性增加过拟合通常能显著缓解。Dropout从0.3调到0.5甚至0.6观察验证集loss变化。减小网络复杂度滤波器数量减半去掉一个卷积段或把全连接层节点从128降到64。有时候模型太大学过头了砍掉一层反而效果更好。早停虽然没有直接函数但可以在验证loss连续若干个epoch不再下降时手动停止训练重新设置MaxEpochs或者直接在训练选项里降低Epoch数。5.4 Matlab版本和工具箱的兼容性问题Deep Learning Toolbox在不同版本里语法改动还是不小的。我在R2019b和R2022b上跑同一段代码就遇到过一些差异比如convolution2dLayer的Padding参数在老版本里只支持数值新版本才支持same字符串trainingOptions里ValidationFrequency以前的版本叫ValidationFrequency再早一点的版本是ValidationPatience。所以如果你照抄代码报错第一反应去查自己Matlab版本的文档而不是改网络结构。另一个容易忽略的点如果你没有安装Deep Learning ToolboxtrainNetwork、convolution2dLayer这些函数当然不存在。装好之后可以用ver命令确认是否有dltoolbox这个工具箱。5.5 关于我的一点个人体会做一维信号二分类最容易忽视的反而不是神经网络本身而是数据质量。我反复踩坑之后形成了一套固定的检查流程拿到数据先画波形、先听声音、先看频谱确认信号是正常的再谈建模。CNN只是工具它能弥补一部分预处理不到位的问题但弥补不了数据本身的脏乱差。建议你在训练之前把每个类别的数据各挑几条画在同一个图里肉眼对比一下两类信号有没有可分的趋势。如果人眼都看不出来区别那网络学出来大概率也是个随机水平。另外一个实用的小技巧训练完网络之后不要只盯着准确率把网络中间层的卷积核权重和特征图激活值可视化出来看看。Matlab里用activations函数可以很方便地提取中间层输出画成热图后你会发现网络确实在“关注”信号的某些局部形态——比如心电分类时它往往对QRS波群的宽度和幅度特别敏感。这个可视化过程既有利于debug写论文报告的时候还能当插图用一举两得。