1. 这个项目到底在做什么
第一次看到“CNN-GRU多特征分类预测”这个名字,很多人第一反应是“又是论文里那套模型拼接”,实际真不是。我最早接触这套方案是在一个工业设备状态监测的活儿里,传感器同时采集温度、振动、电流、压力一共8个维度的信号,每个样本是200个时间步长的一段一维序列,要分成“正常”、“轻度异常”、“故障”三个状态。这就把CNN-GRU多特征分类预测的典型场景说清楚了:多路特征同时输入,利用历史时间窗口数据,输出离散的类别标签。
多特征分类预测这几个字拆开看,其实涵盖了三层意思:
- 多特征:每一个时间点上采集的不止一路信号,而是多个维度同时记录,通道之间有关联也有冗余;
- 分类:输出不是连续数值,而是几个离散的类别标签,比如设备状态、故障类型、行为类别;
- 预测:不是拿当前时刻的瞬时值直接判断,而是利用一段历史窗口的时序变化趋势做推理。
那为什么偏偏选CNN-GRU这个组合?我在实际对比中发现,单独用CNN处理序列数据,卷积核能提取局部模式,但对长距离时间依赖的建模能力偏弱;单独用GRU或LSTM这类递归网络,虽然能记住历史信息,但对局部特征的自动提取和降噪能力不如CNN来得直接。两个串在一起,等于先让CNN做一轮“特征初筛”,把序列里最关键的局部模式抓出来,再交给GRU做时间维度上的综合分析,最终通过全连接层和Softmax输出分类概率。这一套在Matlab里实现起来并不复杂,关键是理解每一层到底在干什么,以及输入数据该按什么格式送进去。
1.1 什么样的数据适合这套方案
不是所有数据都需要上CNN-GRU,选模型前先看看手里的数据结构。这套方案最适合的数据形态是“多通道一维时间序列”。具体来说,数据应该满足以下几个条件:
- 每个样本都是一段等长的时间序列,长度统一,比如200个点、500个点;
- 每个时间点上同时记录多个特征维度,比如8个传感器通道;
- 样本有明确的类别标签,用于监督训练;
- 样本之间相互独立,不存在训练集与测试集之间的数据泄露。
如果你手里的数据是一张二维表格,每一行是一个样本、每一列是一个特征,并且特征之间没有明显的时序顺序,那直接套CNN-GRU反而画蛇添足。这种结构化表格数据用随机森林或梯度提升树效果更好。只有当特征之间存在先后顺序、时间依赖或局部关联,才值得把数据重排成序列形式送进CNN-GRU。
我遇到过不少用户,拿着传统表格数据来问能不能跑这个模型。我的回答通常是:可以跑,但前提是你要把样本构造成时间窗口形式。比如一根长度为5000的连续信号,按每200个点切一个窗口、滑动步长50,就能得到一批序列样本。每个窗口内的200个点就是时间步,窗口内每路传感器的值就是特征通道。
1.2 相比传统方法的优势在哪
过去做多特征分类,常规做法是手工提取特征:均值、峰值、方差、峭度、功率谱能量、小波系数等等,然后拼成特征向量送进SVM或决策树。这一套流程最大的问题是,特征工程非常依赖经验,换一个场景、换一组传感器,原来好用的特征就不一定灵了。而且当特征维度变多、数据量变大时,手工特征的表达力明显不够。
CNN-GRU这类深度学习方案绕开了手工特征设计,直接把原始时间序列作为输入,让网络在训练过程中自己学习哪些模式是有区分度的。CNN的卷积核会在底层学到类似“上升沿”、“尖峰”、“周期波动”这样的基础模式,GRU则在高一层学这些模式随时间组合的规律。整个过程是端到端的,从原始数据到分类结果一气呵成。
另外,多特征之间的交叉信息也能被自动利用。比如电流波形出现毛刺同时伴随温度缓慢上升,这两个通道单独看都不明显,但联合起来就是故障前兆。手工特征很难穷举这种组合,深度学习模型却能在训练中自动捕捉到。这也是为什么在故障诊断、行为识别、医疗信号分类这些场景里,CNN-GRU这类模型往往比传统方法有更高的准确率上限。
2. 模型架构为什么这样搭
CNN-GRU的典型结构可以概括为“卷积特征提取 + 循环时序建模 + 全连接分类”三个板块。下面我把每一块的作用和参数设计思路拆开讲。
2.1 CNN部分在做什么
CNN部分通常由一维卷积层、激活函数和池化层堆叠而成。一维卷积核沿着时间轴滑动,每次覆盖一小段局部窗口,与窗口内的数值做加权求和,从而提取局部模式。这和图像处理里的二维卷积本质相同,只是把滑动方向从两个方向缩减为一个方向。
以卷积核大小为3为例,它每次看3个连续时间点,计算它们的加权和,得到一个输出值。卷积核滑动到下一个位置,再算下一个输出值。这个过程相当于用一个“模式探测器”在整个时间序列上扫描。第一个卷积层可能学到“短时上升”、“短时下降”、“瞬时尖峰”这类基础模式,第二个卷积层则把这些基础模式组合成“先尖峰后衰减”、“持续上升再平稳”这类更高阶的模式。
激活函数通常选ReLU,作用是给网络引入非线性。没有非线性激活函数的卷积层,无论堆多少层都等价于一个线性变换,那深度模型就没有意义了。池化层的作用是降采样,把相邻几个位置的输出合并成一个值,既能减少后续计算量,又能增强模型对时间微小偏移的鲁棒性。比如最大池化取窗口内最大值,信号稍微平移一两个点,池化结果变化不大。
在Matlab里,一维卷积对应的是convolution1dLayer。它比二维卷积更好用,因为输入不需要手动重排成图像格式,直接接受序列输入即可。
2.2 GRU部分在做什么
GRU全称是门控循环单元,是LSTM的一种简化变体。它内部有两个门:更新门和重置门。更新门决定历史信息保留多少、新信息融入多少;重置门决定过去的状态有多少被用来计算当前候选状态。相比LSTM的三个门,GRU参数量更少,训练更快,在小数据集上往往表现更稳。
GRU处理的是一个时间步一个时间步的序列。在每个时间步,它接收当前时刻的输入向量和上一时刻的隐状态,经过门控计算得到新的隐状态。这个隐状态就像网络的“记忆”,保存了截止到当前时刻的历史信息。循环网络之所以适合时序建模,正是因为它能够在处理后面数据时仍然“记得”前面发生了什么。
放在CNN-GRU架构里,CNN输出的特征序列仍然保留着时间顺序,GRU再接过来逐时间步读取,从而建模完整的时序依赖。GRU层有一个关键参数叫OutputMode,常见两个取值:last和sequence。做分类任务时用last,只取最后一个时间步的隐状态,相当于把整段序列的信息压缩成一个固定长度的向量,再送给全连接层;做序列到序列的任务时用sequence,每个时间步都输出,主要用于预测或生成任务。
还有一点值得注意:GRU单元的个数决定了记忆容量。设成32、64、128都有可能,具体根据序列长度和数据复杂度调整。序列越长、模式越复杂,需要的单元数越多,但也不是越多越好。超过实际需要就会出现冗余,训练变慢且过拟合风险增加。
2.3 完整的网络结构长什么样
我把一个经典的CNN-GRU分类网络按层列出来,并标出每一层的输入输出尺寸变化。假设输入样本是8个特征通道、200个时间步长的一维序列。
| 层 | 类型 | 输出维度 | 说明 |
|---|---|---|---|
| 1 | sequenceInputLayer | 8×200 | 接受8通道、200时间步的序列输入 |
| 2 | convolution1dLayer | 16通道×200 | 卷积核大小3,Padding为same,输出16个特征图 |
| 3 | reluLayer | 不变 | 引入非线性 |
| 4 | maxPooling1dLayer | 16×100 | 池化窗口2、步长2,时间长度减半 |
| 5 | convolution1dLayer | 32×100 | 第二层卷积,输出32个特征图 |
| 6 | reluLayer | 不变 | 非线性激活 |
| 7 | gruLayer | 64×100 或 64×1 | 64个GRU单元,OutputMode设为last则得到64×1 |
| 8 | dropoutLayer | 不变 | 随机丢弃部分神经元,抑制过拟合 |
| 9 | fullyConnectedLayer | 3 | 输出节点数等于类别数 |
| 10 | softmaxLayer | 3 | 将分数转换为概率 |
| 11 | classificationLayer | 类标签 | 计算分类交叉熵损失 |
尺寸变化里有一个容易忽略的细节:第一层卷积使用Padding为same时,时间长度保持200不变;经过一个池化层后变成100;第二层卷积后时间长度依然是100;GRU层无论输入序列长度是多少,只要OutputMode设为last,最终输出都是64×1的向量。
这个结构只是基线,实际用的时候可以根据数据情况调整。比如序列特别长,可以加第三层卷积或增加池化层;类别很多,最后一层全连接的输出节点对应调整;数据量小,则适当减少卷积核数量和GRU单元数。
3. Matlab实现全流程
Matlab的Deep Learning Toolbox提供了完整的深度学习层定义和训练接口,不需要额外安装第三方框架。下面我从数据准备开始,一步步讲清楚完整操作流程。
3.1 环境准备与数据预处理
实现CNN-GRU多特征分类,Matlab版本建议R2020a以上,因为convolution1dLayer是R2020a引入的。如果版本低一些,可以用convolution2dLayer替代,把一维序列重排成单通道图像形式,但操作会麻烦不少,所以建议直接上高版本。
数据预处理是很多人容易忽视的环节。原始数据通常存在量纲差异,比如电流值在0到10之间,振动值可能是几百,温度可能是几十度。如果直接送进网络,数值范围大的特征会主导梯度更新,导致训练不稳定。解决方法是做归一化,最简单的就是Z-score标准化,也就是把每个特征通道的均值减掉、除以标准差。
在Matlab里可以这样写:
% 假设 rawData 是 numSamples×numFeatures×numTimeSteps 的三维数组 numFeatures = size(rawData, 2); numTimeSteps = size(rawData, 3); % 按特征通道计算均值和标准差 mu = mean(rawData(:,:,1), 1); % 如果数据是三维,先重塑成二维再逐个通道计算更稳妥 rawReshaped = reshape(rawData, [], numFeatures); mu = mean(rawReshaped, 1); sigma = std(rawReshaped, 0, 1); % 标准化 rawNorm = (rawRaw - mu) ./ sigma标准化以后,每个特征通道的均值接近0、标准差接近1。这样做还有一个额外好处:不同传感器通道之间的数值差异被抹平,网络可以更公平地利用每个通道的信息。
接下来是把三维数组转换成Matlab深度学习训练所需的格式。sequenceInputLayer接受的是cell数组,每个cell元素是一个numFeatures×numTimeSteps的矩阵,代表一个样本序列。
numSamples = size(rawData, 1); X = cell(numSamples, 1); for i = 1:numSamples X{i} = squeeze(rawData(i, :, :)); % 得到 numFeatures × numTimeSteps 矩阵 end标签部分也需要注意。Matlab的classificationLayer要求标签是categorical类型,不能直接用数值。
Y = categorical(labelVector); % labelVector 是 1、2、3 这样编码的类别标签预处理完成后,按比例划分训练集和测试集。常用划分比例为8:2或7:3。划分时注意随机打乱,但要保证类别比例在训练集和测试集中大致一致,避免某一类全落在测试集里。
3.2 网络搭建代码精讲
网络搭建最直接的方式是用layer数组按顺序定义每一层。下面给出一份可直接运行的代码:
numFeatures = 8; % 特征通道数 numClasses = 3; % 类别数 layers = [ sequenceInputLayer(numFeatures, 'Name', 'input') convolution1dLayer(3, 16, 'Padding', 'same', 'Name', 'conv1') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') maxPooling1dLayer(2, 'Stride', 2, 'Name', 'pool1') convolution1dLayer(3, 32, 'Padding', 'same', 'Name', 'conv2') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') maxPooling1dLayer(2, 'Stride', 2, 'Name', 'pool2') gruLayer(64, 'OutputMode', 'last', 'Name', 'gru') dropoutLayer(0.3, 'Name', 'dropout') fullyConnectedLayer(numClasses, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ];每一层的名称不是随便起的,给层命名有两个实际好处:一是后续可以用analyzeNetwork检查网络结构时更容易定位问题;二是如果要用深度学习网络设计器做可视化调整,名称清晰会方便很多。
这里我加了两个batchNormalizationLayer,这是很多简化版教程不会提到的细节。批归一化的作用是在每次训练迭代时对层输入做归一化,让数据分布保持稳定,从而加速收敛。实测下来,加了批归一化之后,同样的迭代次数,准确率能提升好几个百分点,尤其是当输入信号本身幅度变化较大的时候,效果非常明显。
池化层我用了两层,每层把时间长度减半。假设输入200个时间步,经过两层池化后变成50个时间步。这个降采样比例对大多数中等长度序列是合适的。如果你的序列特别短,比如只有50个时间步,那两层池化后只剩12个点左右,信息损失太大,这时应该只保留一层池化或者干脆不用池化。
dropoutLayer放在了GRU层之后,概率设为0.3。Dropout的作用是训练时随机丢弃一部分神经元的输出,逼迫网络学习更鲁棒的特征表达,是抑制过拟合最直接的手段之一。对于中小规模数据集,0.2到0.5之间是比较常见的取值范围。数据集越小,dropout比例可以适当调高一些。
3.3 训练选项与训练过程
网络定义好之后,用trainNetwork函数训练。训练选项通过trainingOptions设置:
options = trainingOptions('adam', ... 'MaxEpochs', 50, ... 'MiniBatchSize', 32, ... 'InitialLearnRate', 0.001, ... 'ValidationData', {XValidation, YValidation}, ... 'ValidationFrequency', 20, ... 'Plots', 'training-progress', ... 'Verbose', true);优化器选adam是当前序列分类任务中最稳妥的选择。SGD虽然经典,但对学习率敏感,收敛也更慢;adam相当于给每个参数自适应调整学习率,对小批量数据、不同尺度的特征都表现得比较平衡。
MiniBatchSize决定了每次迭代用多少样本计算梯度。32是一个比较温和的默认值。如果GPU显存足够、数据量大,可以设成64或128,训练速度会更快;如果显存紧张,设成16也行。需要注意的是,分辨率很低的小批量会让梯度噪声变大,训练曲线可能抖动得更厉害。
学习率0.001是adam在分类任务里的常见起点。判断学习率是否合适的直观方法是观察训练曲线:如果损失曲线下降非常缓慢,可以适当调大学习率;如果损失值出现剧烈震荡甚至NaN,说明学习率偏大,需要调小。
Plots设为training-progress后,训练过程会弹出一个实时曲线窗口,展示准确率和损失随迭代次数的变化。我强烈建议第一次运行保持这个选项打开,因为曲线形态能告诉你很多问题:
- 训练准确率快速上升但验证准确率停滞,说明过拟合已经开始了;
- 损失曲线平坦无下降,可能是学习率太低或者数据没有归一化;
- 验证损失先降后升,是典型的过拟合信号,需要加强正则化。
训练中还有一个容易踩坑的地方:ValidationData的格式必须和训练数据完全一致,也是cell数组加categorical标签。如果验证集的数据预处理和训练集不一致,比如用了不同的归一化参数,验证结果会失真,甚至出现准确率大幅波动。
训练完成后,模型保存在net变量中。实际使用的时候可以用classify函数对测试集的每个样本输出预测标签,用predict函数输出每个类别的概率分数。
3.4 分类效果评估
训练结束后,不能只看训练曲线就完事,必须用独立的测试集做评估。基础评估是计算整体准确率:
YPred = classify(net, XTest); accuracy = mean(YPred == YTest); fprintf('测试集准确率: %.2f%%\n', accuracy * 100);对于类别不平衡的数据,只看整体准确率是远远不够的。假设三个类别占比是90%、5%、5%,模型把后两类全分错,整体准确率仍然有90%。这种情况下更应该看混淆矩阵,逐类分析哪些类别之间容易被混淆。
figure; plotconfusion(YTest, YPred);plotconfusion会生成一个测试集真实标签与预测标签的交叉矩阵。对角线上的数字是正确分类的数量,非对角线上的数字是错误分类的数量。通过混淆矩阵能直观看出,比如A类经常被错分成B类,说明这两类样本的特征模式确实有重叠,可能需要增加更细的时序特征,或者扩充样本量。
如果做学术研究或工程报告,还可以计算每类的精确率、召回率和F1分数。精确率是预测为该类的样本中有多少是真正属于该类的;召回率是该类样本中有多少被正确找出来了;F1是两者的调和平均。在Matlab中可以用confusionmat函数配合手写计算:
C = confusionmat(YTest, YPred); precision = diag(C) ./ sum(C, 2); recall = diag(C) ./ sum(C, 1)'; F1 = 2 * precision .* recall ./ (precision + recall);评估完成之后,模型可以保存下来供后续使用:
save('cnn_gru_classifier.mat', 'net');后续加载模型时只需要:
load('cnn_gru_classifier.mat', 'net');4. 数据替换的操作要点
这个项目如果要做成通用工具,最常碰到的问题就是“换一套数据怎么跑”。很多人在这一步卡住,明明代码没改,换成自己的数据就各种报错。下面把数据替换的关键点梳理一遍。
4.1 数据格式规范
换数据之前,先按前面说的格式要求整理数据。最常见的格式是excel表格或csv文件,每列代表一个特征通道,每行代表一个时间点。但分类预测需要的是“多段样本”,不是一整条长信号。所以首先要做的就是样本切分。
假设你有一段长度为10000个采样点的多通道信号,共8个通道,需要切成200个时间步长的样本。按滑动窗口方式切分,步长设为50或100,这样能得到大量有重叠的样本。切分代码可以这样写:
function [X, Y] = slidingWindowSegment(data, labels, windowLength, step) % data: 一个矩阵,time × channels % labels: 每个时间点对应的类别标签,time × 1 % windowLength: 窗口长度 % step: 滑动步长 numTimePoints = size(data, 1); numChannels = size(data, 2); segments = {}; segLabels = []; idx = 1; for start = 1:step:(numTimePoints - windowLength + 1) segment = data(start:start+windowLength-1, :)'; % channels × windowLength segments{idx} = segment; % 取窗口内出现次数最多的标签作为该窗口的标签 segLabels(idx) = mode(labels(start:start+windowLength-1)); idx = idx + 1; end X = segments'; Y = categorical(segLabels'); end这里有一个处理细节:窗口标签的确定方式。如果窗口全部落在同一个标签区间内,直接取该标签即可;如果窗口跨了两个标签区间,可以取窗口内最多出现的标签,也可以取窗口末端时刻的标签,具体取决于业务含义。在故障诊断场景里,我用窗口末端时刻的标签通常更合理,因为越接近当前时刻的状态对判断越有参考价值。
4.2 维度对齐和网络参数调整
换数据后最容易报的错是维度不匹配。网络第一层设的numFeatures是8,你的新数据如果是6个通道,运行trainNetwork就会直接报错。解决办法是让numFeatures跟着数据走:
[numSamples, numFeatures, ~] = size(rawData);或者在读入数据后:
numFeatures = size(X{1}, 1);classify阶段也一样,测试集的每个样本必须是numFeatures×windowLength的矩阵,不能多一维也不能少一维。
除了特征通道数,序列长度也需要重新审视。你的新数据如果每个样本是500个时间步而不是200个,那么网络里池化层叠加后的输出长度会不同,但GRU层会自适应处理任意长度的输入,所以这种情况倒不影响网络结构。不过,序列长度大幅增加意味着GRU承担的时间步变多了,如果不够用,可以增加GRU单元数,比如从64调到128。
还有一个常见问题是标签类别的数量变了。原来3类,新数据5类,最后一层fullyConnectedLayer的输出节点必须改成5。这个值也可以从标签里自动获取:
numClasses = numel(unique(YTrain));训练选项也要根据新数据量做调整。数据量从几千变成几万,MaxEpochs可以适度下调,因为更多样本意味着每个epoch内见到的数据更多,收敛更快;反过来说,数据量只有几百,MaxEpochs就要调大,同时把dropout比例调高,防止过拟合。
4.3 归一化参数的保存与复用
数据替换时最容易被忽略的坑是归一化参数的处理。训练集上计算的mu和sigma必须保存下来,测试时使用同一批参数做标准化,绝对不能用测试集自己的均值标准差重新算一遍。原因很简单:测试集被用来评估模型在“没见过”的数据上的表现,如果把测试集的统计信息混入预处理过程,相当于间接让模型“偷看”了测试集的分布,评估结果会虚高。
保存归一化参数的写法:
save('normalization_params.mat', 'mu', 'sigma');部署时加载进来,先标准化再分类:
load('normalization_params.mat', 'mu', 'sigma'); rawTest = (rawData - mu) ./ sigma;这一点在实际工程中非常重要。有一次我在做跨设备故障诊断时,直接用目标设备的数据重新计算了归一化参数,结果验证准确率出奇地高,但到了新采集的数据上就垮掉。排查了好久才发现是归一化参数不一致导致的,真实世界的数据分布不可能和训练集完全一致,必须用训练集固定的统计量去做变换。
5. 常见问题和排查技巧
下面把我在跑CNN-GRU多特征分类时遇到最多的问题整理成速查表,每一条都是实际操作中排查过的经验。
5.1 维度不匹配报错
报错信息通常含有“input size mismatch”或“expected input to be...”。这类错误90%出在数据格式上。排查顺序如下:
- 检查X的每个cell元素是否是double类型的二维矩阵;
- 检查矩阵的维度顺序是否是特征数×时间步长,很多人拿到的数据是时间步×特征数,需要转置;
- 检查测试集和训练集的特征通道数是否一致;
- 检查标签是否是categorical类型。
一个绕不开的操作是:cell数组里每个样本矩阵的大小必须一致。如果有某个样本长度跟其他样本不同,trainNetwork直接报错。这时候需要检查滑窗切分是否在边界处理上出了问题,比如最后一段长度不足窗口长度,被直接舍弃或者被单独保留成了一个短样本。
5.2 训练速度慢和显存不足
如果训练一个epoch要等很久,优先检查有没有用上GPU。trainNetwork在可用时会自动选择GPU设备,但如果GPU显存不足,有时会退回CPU训练,速度慢到无法接受。在训练选项里可以显式指定执行环境:
options = trainingOptions('adam', 'ExecutionEnvironment', 'auto');auto模式会自动选择最优设备,也可以手动指定'gpu'或'cpu'。
显存不足(out of memory)时,优先降低MiniBatchSize。这是最直接有效的办法。把32改成16,显存占用能下降一半左右。如果还是不够,减少GRU单元数或精简卷积核数量同样能降低内存占用。
还有一个常被忽略的点:Matlab的图形界面本身会占用不少显存,尤其是开着training-progress绘图窗口的时候。如果显存非常紧张,可以把Plots选项设为'none',训练结束后再单独做评估绘图。
5.3 准确率低和过拟合
准确率一直提不上去,先从数据处理层面找原因。第一,确认标签没有错标。第二,确认样本切分时没有把同一段数据的重叠部分同时分进训练集和测试集,这会导致结果虚高但真实泛化能力差。第三,确认数据没有时间顺序混乱,比如用未来的数据预测过去的状态,这种数据泄露在时间序列里特别隐蔽。
过拟合的判断方法前面提过:训练准确率接近100%,验证集准确率明显落后且在下降。解决办法按优先级排列:
- 增加dropout比例,从0.3提高到0.5;
- 降低模型复杂度,减少卷积核数量或GRU单元数;
- 增加训练数据量,通过更多样的样本或数据增强手段;
- 提前停止训练,通过验证损失监控,在验证损失开始回升时终止训练。
Matlab提供了'ValidationPatience'参数,可以在validation loss不再改善时自动停止训练。默认值是Inf,也就是不启用。建议把它设成10或20,配合ValidationData一起使用,能有效避免后期过拟合浪费训练时间。
5.4 类别不平衡问题
当不同类别的样本数量差异很大时,模型会偏向预测多数类。除了前面说的用混淆矩阵做诊断,还有几个实际可操作的手段。
第一个手段是分层抽样。训练集和测试集的划分不要用简单的randperm随机洗牌,而是按类别比例分别抽样,保证少类别的样本在训练集中占的比例和原始数据一致。Matlab里可以按类别做索引拆分:
for c = 1:numClasses idxClass = find(labelVector == c); numTrainClass = round(0.8 * numel(idxClass)); idxClassShuffled = idxClass(randperm(numel(idxClass))); trainIdx = [trainIdx; idxClassShuffled(1:numTrainClass)]; testIdx = [testIdx; idxClassShuffled(numTrainClass+1:end)]; end第二个手段是类别加权。Matlab的classificationLayer默认对所有类别等权处理,如果数据不平衡,可以在训练前给少数类更高的权重,让分类器更关注这些样本。具体做法是把classificationLayer替换成带权重的自定义分类层,或在数据层面增加少数类样本的重复采样。
第三个手段是数据扩增。对时间序列来说,可以给少数类样本添加轻微噪声、时间缩放、小幅平移,生成更多样的样本。这个办法在故障诊断项目里实测有效,尤其是振动数据,在原始信号上叠加标准差为0.01到0.05的高斯噪声,就能把模型泛化能力提高不少。
另外一个细节是,如果测试集的类别分布和训练集差异很大,整体准确率指标会很有误导性。这时必须在报告里同时给出每类的精确率、召回率和F1分数,才能客观反映模型在少数类上的表现。有一次我处理某个故障数据集时,最优的故障类别识别率只有70%,但整体准确率却显示了92%,如果只汇报整体准确率,实际问题就被掩盖了。
6. 一些个人经验总结
最后说几句实操层面的体会。
CNN-GRU这套组合在Matlab里跑通并不难,真正花时间的是数据整理和调参。我个人的工作流程是:先拿一小部分数据(比如200个样本)快速跑通训练流程,确认网络能收敛、分类结果大体合理之后,再切换到全量数据上正式训练。这个小规模试跑能极大节省调试时间,因为网络结构错误、数据格式问题在第一轮就会暴露,没必要等全量数据训练到一半才发现。
超参数调整方面,我习惯按这个顺序来做:先定MiniBatchSize和初始学习率,再调卷积核数量和GRU单元数,最后调dropout和批归一化。学习率是影响收敛速度最敏感的参数,优先确定合理范围;模型容量决定了表达能力的天花板,在准确率不够时优先增加;正则化参数用来抑制过拟合,在验证集表现跟不上训练集的时候再动它。
数据替换服务这块,我接手过不少“代码有了但换数据不会用”的咨询。大部分人卡住的原因都是数据格式和标签定义不统一。如果你的数据本身就是单独的文件,每个样本一个文件,那建议先写一个统一的读取脚本,把所有文件批量导入到一个结构化变量里,再做后续处理。这个步骤最好在项目开始的第一个小时就完成,因为它决定了后面所有流程的输入形态。
Matlab的深度学习工具箱这几年迭代很快,从早期的LSTM为主,到现在原生支持一维卷积、GRU、注意力机制,已经能覆盖大多数工程场景。对于多特征时间序列分类,CNN-GRU在精度和训练成本之间是一个性价比相当高的平衡点。如果你手里正好有一批多通道、带标签的时序数据,又不知道该从哪个模型入手,这套方案值得先跑一版看看效果。