简介:这份PDF资源是一篇关于Elman神经网络结合粒子群优化算法预测社会消费品零售总额的学术论文,适合经济学、数据分析、机器学习等领域的研究者、研究生及从业者参考。资源为单文件PDF格式,压缩包约2.63MB,内容从Elman网络的结构原理、粒子群优化算法的权重辨识与寻优机制,到社会消费品零售总额的建模与预测流程均有完整阐述。已有84人学习下载。通过阅读可掌握递归神经网络在非线性时间序列预测中的建模思路、PSO全局搜索优化神经网络权重参数的实现方法,以及完整的公式推导与实验设计细节,对开展经济数据建模或深度学习预测研究具有直接的参考价值。论文还附有网络结构图、算法流程图与关键公式,便于读者对照复现。
1. 社会消费品零售总额预测,为什么偏偏选Elman神经网络
做社会消费品零售总额的月度预测,很多人一上来就堆LSTM,结果训练集上拟合得漂漂亮亮,一到真实月份就翻车。我后来把模型换成Elman神经网络,反而在这种只有几百个点的中短序列上稳住了。Elman是一种带反馈连接的递归神经网络,结构比LSTM简单得多,但对月度经济指标这类“周期明显、样本量小、噪声还不小”的数据,往往比堆参数的深度模型更实用。这篇文章会把Elman从结构原理讲到数据切分、模型搭建、参数调试和避坑,最后给出小波去噪和多步外推的完整做法,适合做宏观指标预测、商品零售趋势分析的从业者照着复现。
2. 从前馈到递归:Elman神经网络的结构、记忆机制与选型理由
2.1 Elman的结构:把上一个时刻的隐含状态也当成输入
先看结构。Elman神经网络是Jeffrey Elman在1990年提出的简单循环网络,也叫SRN。它比普通前馈神经网络多了一层“上下文层”,这层存的是隐含层上一个时刻的输出。当前时刻计算时,除了输入层给进来的特征,上下文层的值也会一起送进隐含层,相当于网络有了一小段短期记忆。
x_t → [输入层] ↓ h_t = f( W_in·x_t + W_ctx·h_{t-1} + b_h ) ← 隐含层 ↓ [上下文层] ← 存 h_{t-1} ↓ y_t = g( W_out·h_t + b_out ) ← 输出层这个结构里记住两件事。第一,隐含层状态h_t同时依赖当前输入x_t和上一个时刻的隐含状态h_{t-1},这就是“递归”二字的来源。第二,上下文层不是独立训练出来的参数,它就是上一步隐含状态的一份拷贝,训练时通过反向传播算法一起更新。
所以Elman本质上是一个带时间记忆的前馈网络,不是那种能记住“遥远过去”的复杂模型。它对短周期、近邻依赖强的序列特别合适,比如月度社零数据里的年度季节性、相邻月份之间的惯性。
我一般会用“展开”的方式理解它:把时间维拆开,每走一步,网络接收当前输入和上一步的输出。展开后它看起来像一个很深的网络,但每一层的权重是共享的。这也是为什么很多人把它和“循环神经网络”画等号。
2.2 对比LSTM、前馈神经网络:社零月度数据为什么选Elman
做预测选型时,最容易纠结的就是“都用神经网络,我到底选哪个”。我拿社零月度数据这个场景,把Elman、前馈神经网络和LSTM放在同一张表里对比过:
| 对比维度 | 前馈神经网络 | Elman神经网络 | LSTM |
|---|---|---|---|
| 是否建模时间依赖 | 否,输入输出独立 | 是,依赖上一步隐含状态 | 是,依赖长时间跨度的状态 |
| 记忆范围 | 无 | 短期,约几个到十几个时间步 | 长短期兼顾,有门控机制 |
| 参数量 | 少 | 少,只比前馈多一个上下文权重矩阵 | 多,有输入门、遗忘门、输出门 |
| 训练难度 | 低 | 低到中等,容易收敛 | 高,调参空间大,容易过拟合 |
| 适合序列长度 | 不适合时间序列 | 几十到几百个点的中短序列 | 上千点的长序列 |
| 对样本量的要求 | 低 | 低,几百个点就能训 | 高,样本少了容易学到噪声 |
社会消费品零售总额的月度数据,公开可用的往往只有十几年到二十年的月度数,也就是一百多到两百多个数据点。这种体量下,LSTM三个门加上各种隐藏参数,很容易把噪声也学进去,训练集损失很低,测试集上却一塌糊涂。前馈网络结构太“平”,不认时间顺序,硬把月度序列当成独立样本输入,基本等于丢掉了月份之间的惯性信息。
Elman正好卡在中间:参数量不大,又认得时间顺序。它在月度数据上的经验表现是——能抓住相邻月份的惯性、能表现年度季节性,又不会像LSTM那样动不动就过拟合。这也解释了为什么“小波elman神经网络”的组合在宏观经济指标预测里经常出现:小波负责降噪和拆趋势,Elman负责时序记忆,两件事各管各的。
2.3 选型的一个朴素标准:序列多长、噪声多大、样本多少
我判断一个序列到底适不适合用Elman,就看三个指标:时间跨度、样本量、噪声水平。
时间跨度看的是“记忆长度”。如果序列的周期是一年,那么Elman的上下文层反正只保留上一个时刻的状态,它记住的不是“去年的今天”,而是“上个月的值”。它能学到年度季节性的途径,是网络参数自己把12步之前的依赖编码进权重里,实际能力有限。所以经验上说,周期超过6到12个月的序列,直接裸用Elman效果会打折,通常需要先用小波或差分把趋势和季节性处理掉。
样本量方面,Elman比较能抗住“样本少”。我做过最小规模的测试,七八十个数据点、切出五六十个训练样本,Elman依然能训出一个不太离谱的预测模型,换成LSTM基本就是一团乱码。原因是Elman参数少,优化空间小,不容易掉进过拟合的坑。
噪声水平则决定了要不要做预处理。社零月度数据春节效应明显,不同年份的春节月份错位很大,如果不做对齐或小波平滑,Elman这种短期记忆模型会被单月异常值带偏。后面第6章会讲具体怎么用小波分解把噪声拆出去。
一句话收住选型逻辑:序列短、样本少、有周期,先试Elman;序列长、样本多、依赖长期趋势,再上LSTM;如果输入特征之间没有时间关系,那就用普通前馈网络,别把Elman往上凑。
3. 把社零月度序列做成训练样本:窗口切分、归一化与数据划分
3.1 原始月度数长什么样,要处理成什么形式
社会消费品零售总额的公开月度数据,一般是“年份+月份+当月零售总额”三列,单位是亿元。原始序列看起来就是一条带明显季节波动的曲线:每年年底和春节前冲高,春节月份回落,年中平稳。
Elman没法直接吃这种原始序列。它要求的输入是二维的:每个样本是一段连续时间窗口内的数值,标签是窗口之后的一个或几个数值。也就是说,要把一条时间序列转换成“X=过去12个月,y=下个月”这种监督学习样本。这一步在时间序列预测里叫滑动窗口,也叫lookback。
我习惯先把数据读进来做基础检查。至少要看三件事:有没有缺失月份、有没有异常跳变、是不是按时间升序排列。社零数据偶尔会有口径调整和历史修正,缺失月份不能直接补零,一般用前后月份均值填充;异常跳变要对照当时的宏观事件判断要不要保留,不能一删了之。
3.2 窗口切分代码:lookback取多少、horizon取多少
下面是最核心的样本构造逻辑。给定一条原始序列,用滚动窗口切成“过去lookback个月预测未来horizon个月”。lookback一般不小于一个完整年度周期,因为社零有季节性,输入窗口里至少要包含一个“去年同月”,模型才有条件学到年度效应。
import numpy as np def make_samples(data, lookback=12, horizon=1): """把一维时间序列切成监督学习样本。 data : 一维数组,按时间升序排列的月度值 lookback: 每个样本用多少个过去月份作为输入 horizon : 预测未来多少个月 返回 X: (样本数, lookback),y: (样本数, horizon) """ X, y = [], [] for i in range(len(data) - lookback - horizon + 1): X.append(data[i : i + lookback]) y.append(data[i + lookback : i + lookback + horizon]) return np.array(X), np.array(y)这段代码的逻辑是:从序列开头开始,每次向后移动一个月,截取连续lookback个月作为特征,紧接着的horizon个月作为标签。移动一步就是一个样本,所以样本数等于“总长度 - lookback - horizon + 1”。
参数上,我做月度预测时默认lookback=12,horizon=1。原因不复杂:社零数据有年度季节性,窗口小于12个月会漏掉“去年同期”这个最强特征;大于12个月像24个月,样本数会少掉12个,对小样本数据集来说得不偿失。horizon=1代表做单步预测,如果想一次预测未来3个月,就把horizon设为3,但要注意标签维度变成3,后面的输出层也要跟着改。
这里有一个容易忽略的细节:X和y的最后一个维度对齐问题。单步预测时y的shape是(样本数, 1)还是(样本数,)会影响后续损失函数计算,我习惯统一reshape成(样本数, horizon),避免在训练时报维度不匹配的错。
3.3 归一化与数据划分:不要把测试集信息泄漏进训练集
归一化在时间序列预测里不是可选项,是必选项。Elman的隐含层激活函数默认是tanh,输入值落在太大或太小的范围里,tanh很容易直接饱和,梯度传不回去,网络怎么训都学不动。社零总额动辄几千亿,不归一化的话,第一轮前向传播算出来的损失就是天文数字。
常见做法是用MinMaxScaler把序列压到0到1之间。关键坑在于:一定要先切分训练集和测试集,再分别做归一化,而且只能用训练集的min和max去变换测试集。如果先对整个序列做归一化再切分,测试集的信息就已经渗进了训练数据的分布里,评估结果会变得虚高,这也是前面说的“数据泄漏”。
from sklearn.preprocessing import MinMaxScaler from sklearn.model_selection import train_test_split # data 为原始月度序列,按时间升序 scaler = MinMaxScaler() data_norm = scaler.fit_transform(data.reshape(-1, 1)).flatten() # 关键:按时间顺序切分,不能随机打乱 train_data = data_norm[: int(len(data_norm) * 0.8)] test_data = data_norm[int(len(data_norm) * 0.8) :] # 分别在训练段和测试段上构造样本 X_train, y_train = make_samples(train_data, lookback=12, horizon=1) X_test, y_test = make_samples(test_data, lookback=12, horizon=1)这里train_test_split都没有出场,原因是时间序列天然不能随机切分。随机切分会把“上个月”和“下个月”同时塞进训练集和测试集,模型等于提前偷看到了答案。我用的是最朴素的比例切分:前80%做训练,后20%做测试。样本量大的时候可以用TimeSeriesSplit做多折滚动验证,但社零这种数据量,简单留出法更稳,多折反而会把有限的测试样本切成碎片。
归一化的另一个细节是:预测完之后,所有评价指标都要在原始尺度上算。模型输出的是0到1之间的归一化值,直接拿它算MAPE没有意义,必须先scaler.inverse_transform还原成亿元单位,再和真实值对比。这个还原操作如果漏了,你看到的“误差率5%”可能是归一化空间里的值,实际换算成亿元之后就失真了。
4. 用PyTorch复现一个Elman预测模型:构建、训练与回测全流程
4.1 用PyTorch把Elman写出来:一个RNNCell加一个全连接层
PyTorch没有单独的“Elman”类,但它的nn.RNN默认就是Elman结构。PyTorch源码里对RNN的说明写得很清楚:ht = tanh(x_t·W_ih^T + b_ih + h_{t-1}·W_hh^T + b_hh),这正是Elman的定义。所以搭建时不需要自己写循环逻辑,直接用RNN层承接序列,再接一个全连接层输出预测值就行。
import torch import torch.nn as nn class ElmanNet(nn.Module): """基于RNNCell实现的Elman神经网络。 input_size : 每个时间步输入的特征维度,这里固定为1(当月零售额) hidden_size: 隐含层节点数,决定网络的记忆容量 output_size: 输出维度,等于预测步数horizon """ def __init__(self, input_size=1, hidden_size=16, output_size=1): super().__init__() self.rnn = nn.RNN(input_size, hidden_size, batch_first=True) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x: (batch, lookback, input_size) out, _ = self.rnn(x) # out: (batch, lookback, hidden_size) last = out[:, -1, :] # 取最后一个时间步的隐含状态 return self.fc(last) # (batch, output_size)逻辑上分三步看。第一步,输入x的shape是(batch, 12, 1),代表一批样本,每个样本是12个月的月度值。第二步,RNN按时间顺序逐个读取这12个值,内部不断更新隐含状态,输出序列out保存了每一个时间步的隐含状态。第三步,我们要预测的是“第12个月之后的下一个月”,所以只取最后一个时间步的隐含状态,也就是out[:, -1, :],然后通过全连接层映射到预测值。
hidden_size是这里最值得调的参数。它太小,网络记不住足够的历史信息,预测结果会偏向均值;它太大,参数量上升,小样本下容易过拟合。我对社零月度数据的经验范围是8到32,起步设16,先跑一轮看训练曲线再调。input_size固定为1,因为我们只用零售总额本身做单变量预测;如果你想加入CPI、居民收入等外生变量,这里要改成特征数量,同时X的构造也要从二维变成三维。
4.2 训练循环、学习率与早停:让损失曲线真正降下去
模型搭好之后,训练环节有几个参数直接决定成败:损失函数、优化器、学习率、epoch数量和早停策略。回归预测用MSELoss,优化器用Adam,学习率从0.001起步。这些属于常规配置,但“常规”不等于不会翻车,下面这段训练代码里我加了最关键的早停判断。
def train_model(model, X_train, y_train, X_val, y_val, epochs=300, lr=0.001, patience=30): criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=lr) best_val_loss = float("inf") wait = 0 for epoch in range(epochs): model.train() optimizer.zero_grad() # 转为Tensor并保持浮点类型;lookback和batch维度要放对 xb = torch.tensor(X_train, dtype=torch.float32).unsqueeze(-1) yb = torch.tensor(y_train, dtype=torch.float32) pred = model(xb) loss = criterion(pred, yb) loss.backward() optimizer.step() # 验证集上计算当前误差,判断是否早停 model.eval() with torch.no_grad(): xv = torch.tensor(X_val, dtype=torch.float32).unsqueeze(-1) yv = torch.tensor(y_val, dtype=torch.float32) val_loss = criterion(model(xv), yv).item() if val_loss < best_val_loss: best_val_loss = val_loss wait = 0 else: wait += 1 if wait >= patience: break return model这段代码的关键不在前向传播,而在最后那几行早停逻辑。每个epoch结束后都在验证集上算一次损失,如果连续patience个epoch验证损失都没有刷新最低值,就认为模型开始过拟合了,直接停掉。这个机制对Elman这类小模型尤其重要,因为它收敛快,一旦开始过拟合,曲线翘起来的速度也快。
参数上,epochs给300只是一个上限,配合patience=30,绝大多数时候训练在一两百轮内就会自己停。学习率方面,0.001是Adam配小模型的稳妥起点;如果损失曲线震荡得厉害,降到0.0005;如果下降得跟蜗牛爬一样,可以试0.003,但再大就容易发散了。还有一个细节:输入X_train要unsqueeze(-1),因为RNN要求输入最后一维是特征维,哪怕只有一个特征也得显式标出来,不然PyTorch会直接报维度错误。
4.3 回测评估:用滚动起点验证模型的真实水平
训练完的模型不能只看训练集损失。时间序列预测最常见的错误就是在历史数据上反复调参,然后拿同一段测试集说“准确率很高”,这种测试集已经被污染了。我习惯的做法是滚动回测:每次把训练窗口向后推几个月,重新训练模型,预测下一个未知月份,最后把所有预测值串成一条线,和真实值统一对比。
def backtest(data_norm, lookback=12, horizon=1, step=1): """滚动回测:每次用截至t的数据训练,预测t+1,然后窗口后移。""" predictions = [] actuals = [] # 最后留出至少lookback+horizon个点作为测试区 for start in range(lookback, len(data_norm) - horizon, step): train_part = data_norm[:start] test_part = data_norm[start:] X_tr, y_tr = make_samples(train_part, lookback, horizon) # 这里用上面训练函数的一个简化版,目标只在回测中评估 model = ElmanNet(input_size=1, hidden_size=16, output_size=horizon) model = train_model(model, X_tr, y_tr, X_tr[-20:], y_tr[-20:], epochs=200, patience=20) X_te, _ = make_samples(test_part, lookback, horizon) with torch.no_grad(): xb = torch.tensor(X_te[:1], dtype=torch.float32).unsqueeze(-1) pred = model(xb).item() # 用训练段的scaler还原到原始尺度 pred_raw = scaler.inverse_transform([[pred]])[0][0] actual_raw = scaler.inverse_transform([[test_part[horizon - 1]]])[0][0] predictions.append(pred_raw) actuals.append(actual_raw) return predictions, actuals回测的核心思想是“每次只用当时已知的数据做训练”。start每向后移一步,训练集就变长一点,模型重新训练一次,预测下一个完全没见过的月份。这个过程的代价是训练次数多,但换来的是接近真实的预测能力评估。社零数据量小,一次回测大概训练几十个模型,普通笔记本几秒钟就能跑完,完全不用担心计算开销。
回测结果里我一般同时看两个数:与真实值对比的MAPE,以及预测序列和真实序列的相关系数。相关系数这个指标容易被人忽略,但实际很有用——它反映的是模型能不能跟住趋势方向。社零预测里方向对不对,往往比数值差一点点更影响业务决策。
5. Elman训练避坑实录:收敛慢、过拟合与梯度问题的排查清单
5.1 收敛很慢或完全不动:先查归一化和学习率
现象:训练了上百轮,loss还挂在0.5以上下不去,或者从一开始就没怎么变过。
原因有两个常见来源。第一个是没做归一化,或者归一化范围不对。输入值停留在几千亿的数量级,tanh激活函数在绝对值大于2之后就完全饱和了,梯度趋近于零,反向传播什么都没传回去,网络当然学不动。第二个是学习率设置得太小或太大。太小了每一轮参数只挪一点点,表现为缓慢下降甚至停滞;太大了loss直接震荡,看起来像随机乱跳。
解决:先确认输入已经压到0到1区间,再打印前几轮的loss变化。如果loss从非常大的值快速掉下来,说明学习率可能偏大,调小到0.0005再试;如果loss平稳但下降极慢,把学习率调到0.003。另外把hidden_size从16临时调到4,如果训练速度明显变快,说明不是结构问题而是数据或超参问题,这也是一个很有效的排查手法。
5.2 预测结果是一条平直线:模型学到的只是均值
现象:训练曲线很漂亮,测试集预测结果却是一条横线,每个月份的预测值都差不多,完全看不出波动。这种情况是我在做社零预测时遇到最多的坑。
原因:模型在拟合过程中发现,与其逐月捕捉波动,不如输出一个接近序列平均值的常量,这样整体损失反而最低。这在统计学上叫“回归到均值”,Elman的tanh非线性在输入范围太窄、信息量不足的时候,特别容易走到这个局部最优解里。
解决分三步。第一步,检查归一化后的数据分布,如果大部分值集中在0.2到0.4这种窄区间,信息熵太低,先放大数据的变化幅度。第二步,把hidden_size调大一点,从16调到24或32,增加网络的拟合容量。第三步,检查lookback是不是太短,我用过lookback=6的配置,模型根本看不到年度季节性,预测出来就是一根扁平的线,改成12之后立刻有了曲线形状。
5.3 过拟合:训练集误差很低、验证集误差翘起来
现象:训练loss一路降到0.001以下,验证集loss先降后升,最终比训练集高出一个数量级,预测值在回测区间里剧烈摆动。
原因:Elman参数少,但扛不住样本量本身很小。社零这种一两百个点的序列,如果hidden_size设到64以上,网络就有足够的容量把训练样本里的每一个噪声点都记住,包括春节错位、口径调整带来的异常跳变。
解决:早停是第一道防线,patience设20到30,验证集不刷新就直接停。第二道防线是减小hidden_size,我做过对比,同样的数据,hidden_size从32降到16,验证集误差能降一半。第三道是增加训练样本的有效性,lookback从12提到24也能帮助稳定,代价是样本数量减少。实在不行,给输入加一点高斯噪声,比如在归一化后的X上叠加标准差的0.01倍,能让模型不那么容易死记硬背。
5.4 测试集泄露:把归一化fit到了全量数据上
现象:回测误差率漂亮得不可思议,MAPE只有1%不到,但模型一上线预测未来,误差直接破10%。
原因:做数据预处理时先对全序列做了scaler.fit_transform,再来切分训练集和测试集。这样一来,测试集的min和max已经参与过归一化参数的估计,相当于模型在训练时“间接见过”测试集的范围。更隐蔽的是,如果测试集里有极端值,整个序列归一化后训练集被压缩,模型学到的分布实际上已经被测试集扭曲了。
解决:严格遵循“先切分、再分别归一化”的顺序,测试集只能用训练集的scaler做transform,永远不要重新fit。这个坑我翻过两次车,现在写代码的时候会把scaler.fit这一行注释掉,只在训练段上调用,每次跑完都检查一遍scaler.data_min_和scaler.data_max_是从哪段数据里算出来的。
5.5 时间顺序被破坏:随机切分和数据打乱
现象:训练loss很低,测试集表现却稳定地差,而且每次运行结果波动很大。
原因:用sklearn的train_test_split默认shuffle=True切分数据,或者训练时把DataLoader的shuffle打开。时间序列一旦被打乱,相邻月份的依赖关系就断了,模型学到的“时间记忆”全是混乱的。更麻烦的是,随机切分会让测试样本混进训练集,指标虚高,但你发现不了,因为每次的结果都不一样。
解决:时间序列的切分必须按时间顺序,用data[:n]和data[n:]这种切片方式,不用train_test_split。训练时如果需要打乱批量顺序,只能打乱batch之间的顺序,不能打乱batch内部的月份顺序。PyTorch的DataLoader里shuffle=True只对batch的索引生效,不会破坏单个样本内部的lookback序列,这一层可以做,但前提是样本本身已经按时间构造好。如果不想纠结,就直接不用DataLoader,用全批量梯度下降,社零这种数据量完全能承受。
6. 让预测更可信:小波去噪、多步外推与误差校验
6.1 小波分解与Elman组合:先拆趋势再预测
社零月度数据里有两类成分混在一起:一类是反映消费趋势和年度周期的低频成分,另一类是春节扰动、促销活动带来的高频噪声。Elman直接把这两样混在一起学,容易顾此失彼。“小波elman神经网络”这个方向的常用做法,就是先对序列做小波分解,把高频细节分出来单独处理或直接平滑,让Elman只负责低频趋势的预测,最后再重构回原始尺度。
import pywt def wavelet_denoise(data, wavelet="db4", level=3): """小波分解去噪:保留低频趋势分量,抑制高频噪声。 data : 归一化后的一维数组 level : 分解层数,层数越高,去掉的高频成分越多 """ coeffs = pywt.wavedec(data, wavelet, level=level) # coeffs[0]是低频近似分量,coeffs[1:]是各层高频细节 # 各层细节按统一阈值软阈值处理 threshold = 0.1 coeffs_denoised = [coeffs[0]] for c in coeffs[1:]: coeffs_denoised.append(pywt.threshold(c, threshold, mode="soft")) return pywt.waverec(coeffs_denoised, wavelet)小波去噪的参数有两个要调。wavelet选db4还是db8,区别不大,db4是常用起点。level层数决定分解的粗细,level=3表示把序列拆成低频加三层高频细节,月度数据一般取2到4层。threshold则要谨慎,设得太大会把真实的季节波动也抹掉,设得太小等于没去噪。我一般先把分解后的各层细节画出来看一遍,再定threshold,不盲目套0.1。
去噪之后,用处理过的低频序列替换原始序列进入第4章的模型流程。这样做的好处是Elman学的是相对平滑的趋势,预测曲线不那么毛躁。注意两个细节:小波重构出来的序列长度可能和原始序列差几个点,做窗口切分前要检查对齐;小波分解是在归一化之前还是之后做,我习惯在归一化之后做,因为小波阈值对幅度更敏感,归一化后阈值更可控。
6.2 多步外推:直接多输出与滚动预测怎么选
生产环境里很少只预测下一个月,通常要预测未来3个月或6个月。这里有两条路线。
第一条是直接多输出,把输出层换成output_size=3,标签用y的3个月窗口,训练时一次输出3个预测值。优点是推理快,但缺点很明确:模型越往后预测越偏向均值,第三个月的预测误差通常明显大于第一个月。
第二条是滚动预测,保留output_size=1,每次预测下一个月,把这个预测值拼到输入序列末尾,再预测下下个月。这条路线的问题在于误差会累积,预测值一旦偏了,后面的输入就被带偏了。
我的建议是社零做多步外推时用混合法:用滚动预测的方式推到3个月,但每次向前推之前,都用一个较新的小波去噪序列重建输入窗口,别让上一步的误差原封不动地传给下一步。3个月以内的滚动预测误差累积还不太明显,超过6个月就建议重训模型了,因为宏观环境变化之后,旧样本对未来的参考价值会快速衰减。
6.3 误差校验:MAPE、RMSE和残差自相关
最后一步,不校验等于没做。我每次回测完都会计算三个指标:MAPE看平均百分比误差,RMSE看大偏差的惩罚程度,残差自相关看模型是不是漏掉了时间结构。
def evaluate(preds, actuals): preds = np.array(preds) actuals = np.array(actuals) mape = np.mean(np.abs((actuals - preds) / actuals)) * 100 rmse = np.sqrt(np.mean((actuals - preds) ** 2)) # 残差自相关:如果残差还有明显自相关,说明模型漏掉了时间信息 residuals = actuals - preds autocorr = np.corrcoef(residuals[:-1], residuals[1:])[0, 1] return {"MAPE": mape, "RMSE": rmse, "残差自相关": autocorr}MAPE能直接和业务说“平均误差百分之几”,但要注意社零售额有季节波峰,波峰月份的绝对基数大,MAPE容易显得很好看。RMSE会在个别异常月份上被拉高,如果MAPE很低、RMSE很高,说明模型在正常月份很好,在春节等异常月份翻车,这时候需要回去处理节假日效应而不是调模型。残差自相关是很多人忽略的检查,如果这个值接近0.3以上,说明模型预测完剩下的误差仍然和上一期有关,时间信息没有学干净,该回头看看lookback是不是太短。
现在回看整个流程,最值得记下的一条教训是:Elman神经网络在社零这类中小规模月度数据上,真没必要一上来就堆重型网络。先把数据切分和归一化的边界守住,把hidden_size控制在16附近,再用小波分解把噪声拆出来,这一套下来预测精度会比那些复杂模型更可靠。我做过很多次日度、周度数据在Elman上直接翻车的经历,关键不是模型不行,是序列长度和数据形态根本不适合。数据只有几十个点还硬要用深度模型,那就是拿锤子找钉子。
希望帮到你。
本文还有配套的精品资源,点击获取