ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Matlab中用CNN做单输入单输出时间序列预测的完整实践指南

Matlab中用CNN做单输入单输出时间序列预测的完整实践指南 上个月我在做一个设备振动信号的预测任务输入过去20个采样点的振动幅值预测下一个采样点的数值。这就是典型的单输入单输出时间序列预测——只有一个特征序列作为输入输出也只是一个未来值。我一开始用的ARIMA后来同时试了LSTM最后在Matlab 2018B环境下用卷积神经网络把整个流程跑通并稳定输出结果。这篇内容就来整理一下选型思路、窗口化数据怎么造、CNN网络怎么搭、训练参数怎么调以及那些在书上遇不到但实际跑起来一定会踩的坑。适合所有需要做单变量时序预测、想用CNN又不想用Python、打算在Matlab里快速出结果的朋友。1. 为什么用CNN做时序预测单输入单输出问题的本质与选型逻辑1.1 单输入单输出到底在预测什么时间序列建模里有很多种范式最常见的是多输入多输出或者多步预测。单输入单输出指的是过去一段连续时间的历史数值只来自一条序列模型的输出也只有一个未来时刻的数值。打个比方你有一段每天记录的温度数据现在想用前7天的温度预测第8天的温度这就是单输入单输出。它不涉及其他辅助特征也不涉及一次性输出未来7天。这个问题的难点不在于网络结构有多复杂而在于如何把连续的时间切片变成适合CNN卷积操作的学习样本。很多人第一次接触CNN做时间序列时习惯性把一维序列直接丢给网络结果Matlab报维度错误。原因在于CNN通常被设计用来处理图像或具备空间结构的二维数据哪怕是一维卷积也需要明确告诉网络时间步所在的维度。我在这个项目里把问题定义成给定长度T的一段历史窗口单通道预测T1时刻的数值。窗口大小T是可调的超参数CNN在这个窗口上做局部特征提取。换句话说模型学习的是窗口内模式到下一个值的映射而不是整个全局序列的长依赖关系。这一点非常关键直接决定了后续窗口怎么切、卷积核怎么选。1.2 对比ARIMA与LSTMCNN的不可替代之处选型的时候我先后排除了两个方案原因是它们在“工业现场落地”这个诉求上存在短板。ARIMA模型适合平稳序列和线性关系但振动信号、风速、负荷这类数据往往包含非线性成分ARIMA强行建模会带来较大的残差而且它对异常值特别敏感预处理压力大。LSTM是处理时间序列的另一个常见选择但LSTM需要非常细致地调初始学习率、隐藏单元数、时间步展开方式而且当可用样本量只有几万条时LSTM过拟合速度非常快。相比之下CNN有几个明显优势一是参数共享让参数量远小于全连接网络和同量级的LSTM训练更快二是卷积操作天然擅长提取局部形状比如峰值、上升沿、周期性拐点三是CNN对训练数据的规模要求相对宽松在中等样本量下也能得到稳定结果。我并不是说CNN一定比LSTM好但对“单变量、单步预测、数据量有限、要求快速上线”这类场景CNN在Matlab里的实现成本和部署成本都更低。尤其在2018B之后深度学习的训练流程稳定compact模型的预测延迟也很低。1.3 什么样的数据适合用CNN做预测CNN不是万能的。如果原始信号只有线性趋势而没有局部模式CNN提取出来的特征可能不如直接用一次差分加线性回归。我的经验是具有以下特点的序列比较适合CNN序列中存在重复出现的局部形态比如周期波动、脉冲、平台期当前值与过去有限长度内的值关系紧密且超出该长度的历史影响微弱样本量至少在一千以上否则卷积层难以学到稳定的滤波器。如果反过来序列是纯随机游走或者存在极长程依赖比如今天的状态受到100天前的状态影响那么固定窗口的CNN大概率表现平庸此时应该考虑引入序列记忆机制或直接改用其他模型。我实际测试过一个设备温度序列窗口从20拉到100后预测误差显著下降说明这个序列的有效记忆长度在几十个时间步内但拉到300后误差反而上升原因是很远的历史信息变成了噪声。2. 数据窗口化把一维序列变成CNN能吃的样本集2.1 滑动窗口长度的选择依据窗口长度是模型效果的第一影响因素比网络层数还要敏感。我一开始凭直觉设了个10后来分别测试了15、20、30、50结论是窗口长度至少要覆盖一个完整的局部模式周期。比如信号有大约50个采样点的周期窗口至少要50如果窗口只有10卷积层看到的始终是支离破碎的片段很难学到有意义的特征。确定窗口长度有两个实用方法一是做lag相关分析计算当前值与前k个时间点的自相关系数观察系数衰减到接近0的位置二是直接做网格搜索在CPU上跑几组短训练用验证集误差选择。我推荐第二种因为自相关只能描述线性相关性而CNN可以捕捉非线性模式窗口长度和自相关显著长度不一定一致。窗口长度也影响样本数量。对于长度为N的序列窗口长度为W时能构造的样本数是N-W。如果W太大样本数会变少容易过拟合W太小则欠拟合。我常用的范围是W在全序列长度的5%到20%之间具体根据信号周期微调。2.2 训练集/验证集/测试集的切分顺序切分数据集时最容易犯的一个错误是随机抽样划分。时间序列的样本之间天然存在顺序关联如果随机打乱相当于把未来的信息混进了训练集验证结果会虚高部署时大打折扣。我在项目里严格按时间顺序切分前70%作为训练集紧接着15%作为验证集最后15%作为测试集。再补充一点验证集的作用是选择超参数和监控过拟合测试集只允许在最终模型确定后使用一次。很多人拿测试集反复调参调完再测这其实已经泄漏了测试信息最后报告的误差自然很乐观。我在实际跑代码时会把测试集单独留在一边等所有参数都定下来再做一次“盲测”。对于单输入单输出预测切分时还要保证每个输入窗口都完整落在对应的子集内。用代码做窗口化时直接对原始序列索引做截取就可以不需要像某些模型那样担心状态记忆跨越训练边界因为CNN本身不维护跨窗口的状态。2.3 归一化必须在训练之前完成CNN对数据尺度极其敏感。如果原始序列的量纲是几千而网络初始化的权重范围在零点几第一轮训练很容易发散。我在第一次跑的时候就因为没做归一化损失直接输出了NaN。后来在Matlab的imageInputLayer里设置Normalization,none然后在外部手动做z-score归一化。归一化的核心原则是只能使用训练集计算统计量。也就是先用训练集求出均值和标准差然后把验证集、测试集代入同一个均值和标准差完成变换。不该在整个序列上一次性计算因为那样会把验证集和测试集的分布信息“提前”泄露给训练过程。这个细节很多人会忽略而且它不会导致训练报错只会让测试指标虚好。我曾在论文复现时发现某份公开代码就是这么做的验证误差漂亮得奇怪处理之后就正常了。如果数据数值范围很小且稳定比如都在0到1的量级也可以不做归一化但我还是建议做因为即使量级差不多卷积层的梯度也会更平稳。3. Matlab 2018B版本下的CNN网络搭建实操3.1 版本与深度学习工具箱检查标题里写了“Matlab 2018B及以上版本”不是随便一说。在2018B中Deep Learning Toolbox已经包含trainNetwork、predict等核心函数可以支持CNN的回归任务。更早的版本比如2017a里神经网络功能相对简陋跑这种图像输入形式的回归会比较痛苦。启动代码第一步是检查工具箱是否可用ver(deep)如果输出里没有deep这一行说明深度学习工具箱没装。另外建议运行gpuDevice确认是否有可用的GPU。没有GPU也可以用CPU训练但速度会慢很多我实际测试在CPU上训练一个小型CNN100轮大概需要五分钟GPU上只需要几十秒。Matlab会自动选择执行环境如果想强制用CPU就设置trainingOptions中的ExecutionEnvironment,cpu。另一个版本相关的点是2018B里还没有我现在想用的convolution1dLayer那个层是后续版本才引入的。为了兼容我的做法是用convolution2dLayer模拟一维卷积把卷积核的第二维设为1。这样代码在2018B及以上任何版本都能跑不用改。如果你用的是2019A以后也可以换用convolution1dLayer但我不建议为了“用新层”而破坏版本兼容性除非你确定队友和服务器都升级了。3.2 手动搭建convolution2d网络层我的网络结构整体如下这个结构是基于回归任务定制化的winSize 20; numFilters1 16; numFilters2 32; layers [ imageInputLayer([winSize 1 1], Name, input) convolution2dLayer([3 1], numFilters1, Padding, same, Name, conv1) reluLayer(Name, relu1) maxPooling2dLayer([2 1], Stride, [2 1], Name, pool1) convolution2dLayer([3 1], numFilters2, Padding, same, Name, conv2) reluLayer(Name, relu2) fullyConnectedLayer(1, Name, fc) regressionLayer(Name, output) ];几个关键设计理由imageInputLayer的尺寸是[winSize 1 1]。winSize是窗口长度第一个1表示宽度第二个1表示通道数。因为只有一个特征通道数就是1。这相当于把每条样本看作一个宽高比 winSize:1 的单通道图像。卷积核用[3 1]而不是[3 3]目的是只在时间维度上滑动不做跨特征卷积。因为是单特征宽度方向只有一个像素核的宽度必须写成1否则会超出边界报错。Padding我用same这样经过卷积后时间维长度不变方便后续叠加。如果用valid每过一层序列长度会缩小网络设计起来会很麻烦。maxPooling2dLayer的池化窗口是[2 1]也就是在时间方向上做二分之一降采样宽度方向不动。这一步能加大感受野减少计算量。输出层直接用fullyConnectedLayer(1)配合regressionLayer这是一个标准的单输出回归头。如果你发现这个网络太深或者太浅可以按自己的情况加减卷积层。但我的建议是先用这个基础结构跑通基线再决定是否加深。别一上来就堆5层卷积数据量不够时这样只会让过拟合更严重。3.3 把样本格式对齐到[特征, 时间步, 样本]Matlab的trainNetwork要求训练输入X是四维数组顺序是[高度, 宽度, 通道, 样本]。对我们这个例子高度等于winSize宽度等于1通道等于1所以X的大小必须是winSize×1×1×N。很多第一次接触的人会漏掉最后两维直接给一个winSize×N的矩阵trainNetwork会报错说输入维度不匹配。下面是我用的窗口化代码% data 是 Ntotal x 1 的原始序列列向量 winSize 20; N length(data) - winSize; X zeros(winSize, 1, 1, N); Y zeros(N, 1); for i 1:N X(:, :, 1, i) data(i : i winSize - 1); Y(i) data(i winSize); end循环完之后X是四维数组Y是N×1列向量。接下来把X转成适合单个CNN网络输入的格式。注意如果data是行向量最好先转成列向量不然索引时容易搞混。划分训练/验证/测试时直接把X的第四维按比例截取就行trainRatio 0.7; valRatio 0.15; trainNum round(trainRatio * N); valNum round(valRatio * N); XTrain X(:, :, :, 1:trainNum); YTrain Y(1:trainNum); XVal X(:, :, :, trainNum1:trainNumvalNum); YVal Y(trainNum1:trainNumvalNum); XTest X(:, :, :, trainNumvalNum1:end); YTest Y(trainNumvalNum1:end);用这种顺序切片严格保持了时间先后关系。4. 训练参数设置与过拟合处理4.1 trainingOptions中的关键参数训练选项直接决定了模型能不能收敛。我用的配置如下options trainingOptions(adam, ... MaxEpochs, 100, ... MiniBatchSize, 64, ... InitialLearnRate, 0.001, ... Shuffle, once, ... L2Regularization, 0.0001, ... ValidationData, {XVal, YVal}, ... ValidationFrequency, 10, ... Verbose, true, ... Plots, training-progress);这里面的几个参数我解释一下求解器选adam它在回归任务上收敛速度快对学习率的敏感度低。如果换sgdm往往需要手动调momentum和学习率比较麻烦。InitialLearnRate0.001是我的默认起点。学习率太大Loss会震荡太小训练进展缓慢。遇到不收敛优先把学习率降到0.0005再试。MiniBatchSize设为64。样本量只有几千的时候64是性价比比较高的选择。如果显存不够会报错可以降到32。Shuffle设为once意思是训练前随机打乱整个数据集一次之后每个epoch不再改变。这个问题要特别说明mid训练时shuffle样本并不会破坏时序因果性因为CNN不像状态模型那样有跨样本的依赖每个样本本身就是一个完整窗口。但如果你设成every-epoch虽然Matlab也能跑结果会有一点点随机波动复现性差所以我习惯用once。ValidationFrequency10每10次迭代检查一次验证集损失这样能实时观察有没有过拟合。Plotstraining-progress可以弹出训练进度窗口在线看到损失曲线。在服务器无显示环境下要改成none。4.2 验证集监控与早停trainingOptions里有一个不常被提到的参数ValidationPatience默认值是5。意思是在验证集损失经过MaxEpochs轮后如果不再下降训练自动终止。但注意这个“验证集损失不再下降”的判定是按iteration频率的不是按epoch。我在实际使用时喜欢把ValidationPatience调成10这样可以给模型更多挣扎的空间避免过早停下导致欠拟合。如果你发现训练在30轮就停了但训练损失还在下降说明ValidationPatience设置得太小或者学习率偏大。Matlab训练结束时net是训练过程中验证集损失最优的那一次模型不一定是最后一步的模型。所以不用自己额外做模型快照直接用trainNetwork输出的net就行。这一点对回归任务尤其可靠不用手动写early stopping。如果我想更精细地控制会把ValidationData去掉自己写测试逻辑但正常情况下没必要。4.3 常见的过拟合信号和解决办法过拟合在这个任务里最明显的信号是训练损失降到几乎为0验证损失却显著高于训练损失。我在第一次跑这个网络时训练损失稳定在0.01附近验证损失却在0.35左右徘徊典型的过拟合。解决办法按优先级依次是增大L2Regularization从0.0001调到0.001效果立竿见影减少网络深度把第二个卷积层去掉只留一个卷积层加一个全连接层减少numFilters从32、32降到16、16增加Dropout层或在全连接层前加dropoutLayer保留概率0.8生成更多样本本质上需要更长的历史数据或做数据增强比如对窗口添加轻微噪声。实际操作中我先调整L2正则和网络宽度基本能解决。除非数据总量很少比如不到500个窗口Dropout能帮上大忙。Matlab的dropoutLayer在训练时随机丢弃节点预测时自动保留全部节点不需要额外处理。最后提醒一个看似奇怪但很常见的训练坑如果损失一开始就NaN先检查X里是否有NaN或Inf再检查归一化目标Y是否包含极端值。这两类脏数据在时间序列里非常隐蔽比如传感器丢包被填成了NaN窗口化之后整个样本就废了。我在代码里会先跑一遍any(isnan(data))来排查。5. 预测效果评估与单步/多步预测扩展5.1 三个评估指标RMSE、MAE、MAPE训练完成后用predict函数获得测试集预测值YPred predict(net, XTest);注意predict返回的可能是cell数组也可能直接是数值数组取决于输入形式。用数值数组时直接看维度如果不是N×1可以用cell2mat转换。我运行Matlab 2018B时predict返回的是单列数值不需要额外处理。不过如果网络输出维度带batch稳妥起见还是先size(YPred)看一眼。评估指标我一般看三个分别反映不同方面RMSE均方根误差单位与原始数据一致对大误差惩罚较重。适合判断模型在极端情况下的表现。MAE平均绝对误差更稳定直观。MAPE平均绝对百分比误差无量纲方便跨序列比较。但如果原始序列存在接近0的值MAPE会被放大甚至无穷大这时候我改用对称MAPE。计算代码rmse sqrt(mean((YPred - YTest).^2)); mae mean(abs(YPred - YTest)); mape mean(abs((YPred - YTest) ./ YTest)) * 100;我那个项目最终的RMSE大概在原始量程的2%左右MAE比RMSE低了近三分之一说明大部分误差集中在小幅偏差上偶发大偏差存在但不多。如果RMSE远大于MAE优先检查是不是某个单点预测偏离特别大这种点常出现在信号跳变沿上。5.2 从单步到滚动多步预测的实现这个标题虽然只要求单输入单输出但实际业务往往需要连续预测未来多步。最朴素的方法就是滚动预测把第T1步的预测值拼接到真实历史后面作为新的输入窗口再预测T2步。在Matlab里可以这样写YPredRoll zeros(steps, 1); currentWindow XTest(:, :, 1, 1); % 第一个测试窗口winSize x 1 for t 1:steps netOut predict(net, {currentWindow}); % 需要适配网络输入格式 yPred netOut(1); YPredRoll(t) yPred; currentWindow [currentWindow(2:end); yPred]; end滚动法最大的问题是误差累积。预测值一旦偏离真实值后续输入就带着这个偏差越往后越偏。所以对于多步预测不能直接把前几步的误差指标当做整个模型水平。我这里建议如果只预测1步用上面的单值模型如果预测步数超过5步最好改用序列到序列结构或者把目标改成多步输出。每次滚动预测时需要把currentWindow的维度保持为winSize×1。如果你的输入层是winSize×1×1则还要加两个维度可以用reshape或permute完成。5.3 后续优化残差校准与混合模型在基线模型跑通之后我通常留一个优化阶段不一定每个项目都做但性价比很高。一个有效做法是残差校准先用CNN预测得到初步结果再用ARIMA对残差序列建模最后把两个预测值相加。CNN负责非线性局部模式ARIMA负责线性趋势部分组合后的精度通常比单一模型高一些。Matlab里ARIMA可以用Econometrics Toolbox实现代码量不多。另外一个值得尝试的方向是增加时间特征。虽然是单输入单输出但可以把序列的滞后差分、移动平均、窗口最大最小作为附加特征拼到输入里让CNN有更多线索。不过这会破坏“单输入”的严格定义所以在标题场景下我只作为可选扩展来提。最后提一个实用小技巧训练完成后用Matlab的analyzeNetwork(net)查看网络结构它能直观展示每一层的输出尺寸帮助检查全连接层前的维度是否合理。我在调网络结构时就靠它快速定位维度错误比盯着变量逐一排查快得多。我的习惯是每次修改完网络结构先跑一次analyzeNetwork再训练能省下大量调试时间。这个流程走通了你在Matlab 2018B及以上的环境里用CNN做单输入单输出的时间序列预测应该不会再被基础问题卡住。
返回列表