简介:面向深度学习初学者与算法工程师,资源围绕一维卷积神经网络(1D CNN)处理序列数据展开,覆盖时间序列预测、文本分类、音频信号分析等典型场景,提供Python完整实现与训练好的模型文件。包内共25个文件,以17个h5模型权重为主,记录了不同训练轮次下的准确率(如0.62~0.75),配合2个py脚本可用于复现模型构建、训练与评估;另有5个txt说明文档和1个arff格式的WISDM转换数据,方便理解数据集结构及预处理流程。整个压缩包约31.67MB,结构清晰,适合对照学习一维卷积层的卷积、激活、池化、全连接等关键步骤。资源已获10808人学习下载,既可直接加载最佳h5模型快速验证效果,也可基于源码调整卷积核大小、层数与激活函数进行二次实验,是入门1D CNN序列建模的实用参考资料。
1. 一维CNN处理序列数据:为什么我先把LSTM放到一边
如果你手头是一段传感器振动波形,或者一组股票日线收盘价,第一反应往往是找LSTM。但我在最近两个序列预测项目里,都先把一维CNN跑成了基线:几十行Python代码、十几分钟就能看到可用结果,训练速度比LSTM快一大截,在小样本条件下反而不容易过拟合。一维CNN不是“穷人的LSTM”,它把CNN卷积神经网络的核心思路平移到了单条时间轴上,用若干个卷积核在时间窗口上滑动,同时提取局部形状。你可以在时间序列预测、时间序列异常检测、波形分类这些任务里,把它当成一个低成本、可复现、能快速验证想法的起点。这篇笔记会从滑窗构造、Conv1d建模、训练评估讲到五个高频踩坑点。适合有Python基础、想把序列数据真正跑通,但还没有系统用过一维CNN的读者。
2. 时间序列怎么喂给一维CNN:滑窗构造与数据归一化
CNN和LSTM最大的区别是输入结构。LSTM可以按时间步一个个喂进去,而Conv1d要求每个样本是一段固定长度的向量。你不能把一条任意长度的连续序列直接塞进卷积层,所以必须先通过滑窗,把长序列切成固定形状的样本,再把每个样本对应的未来一段值做成标签。这一步做不对,后面所有网络结构都是空谈。
2.1 为什么是滑窗:把序列变成样本
滑窗的意思很简单:从序列开头取一段长度为input_len的数据作为特征X,再取紧接着未来的horizon个点作为目标y,然后沿着时间轴向右挪动step个点,重复这个动作。
import numpy as np def make_windows(series, input_len=48, horizon=6, step=3): X, y = [], [] for i in range(0, len(series) - input_len - horizon + 1, step): X.append(series[i:i + input_len]) y.append(series[i + input_len:i + input_len + horizon]) return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32) # 造一段示例数据:趋势 + 正弦 + 噪声 raw = np.sin(np.linspace(0, 20, 1000)) + 0.1 * np.random.randn(1000) X, y = make_windows(raw, input_len=48, horizon=6, step=3) print("X:", X.shape, "y:", y.shape)循环结束条件里的+1是为了保证最后一个窗口的y不越界。X的形状是(样本数, input_len),y的形状是(样本数, horizon)。这里所有样本都按原始顺序排列,因为时间序列一旦打乱,相邻样本的先后关系就废了。后面训练模型前,还要把X从(batch, seq_len)改成(batch, 1, seq_len),这就是Conv1d需要的通道维度,后面第3章会详细说。
滑窗的参数选择直接影响效果。input_len至少要覆盖一个完整周期,如果数据是小时粒度、存在日周期,窗口小于24就看不到周期;如果数据是日粒度、存在周周期,窗口至少7。horizon取决于你的业务预测需求,想预测未来6个点就设6。step控制样本密度,设成1会让相邻两个窗口只差一个点,样本量爆炸且高度重复;我一般取input_len // 2或更稀疏,既保留足够样本,又减少重叠带来的过拟合。如果样本量不够,再逐步调小。
2.2 用Python构造监督学习样本的代码
上面那段就是最小可用的滑窗代码。真正上项目时,通常还要处理多变量、缺失值、以及归一化。多变量的思路是让series变成二维数组(时间步, 特征数),滑窗后X变成(样本数, input_len, 特征数),这样每个样本里不仅有时间顺序,还包含同一时刻的多个特征。一维CNN会把特征数当通道数处理,这比把特征硬拼成一维要合理得多。
构造样本时有一个容易忽略的点:紧挨着的两个样本窗口重叠度非常高。例如input_len=48时,step=1会让相邻两个样本有47个点完全一样,模型很容易从训练集“背住”这些重复模式,到验证集上就露馅。一种常见做法是把滑窗写成一个可配置类,便于后面做时间序列交叉验证和切分。
class SlidingWindowDataset: def __init__(self, series, input_len=48, horizon=6, step=3): self.series = np.asarray(series, dtype=np.float32) self.input_len = input_len self.horizon = horizon self.step = step def __len__(self): return (len(self.series) - self.input_len - self.horizon) // self.step + 1 def __getitem__(self, idx): start = idx * self.step x = self.series[start:start + self.input_len] y = self.series[start + self.input_len:start + self.input_len + self.horizon] return x, y这个类的__len__需要和前面函数里的循环边界完全一致,否则训练时DataLoader会越界。实际我用 PyTorch 时更喜欢这种写法,因为它天然支持懒加载,内存压力小。如果你构造的序列有几百万个点,一次性np.array可能吃掉好几个G内存,而数据集类只在每次__getitem__时切片,靠谱得多。
2.3 归一化:按训练集算,还是按窗口算
归一化这里有个时间序列专属的坑:不能像图像分类那样,在整个数据集上算出mean和std,然后一次性缩放。原因是你做的是预测任务,测试阶段是未来的数据,未来数据的均值在训练时是未知的。如果在全部数据上归一化,相当于把未来信息泄漏进了训练集,验证时不明显,上线后模型会突然变差。
常见做法有两种。第一种是全局归一化:只用训练集部分计算mean和std,然后把这个统计量同时应用到训练集、验证集和测试集。它适合相对平稳的序列,比如固定场景下的工业传感器数据。第二种是窗口内归一化:对每个滑窗独立减去该窗口均值、除以窗口标准差,标签也按同样的均值标准差缩放,预测后再还原。它对趋势和水平漂移更鲁棒,因为卷积核学到的是局部形态,而不是绝对数值。
def normalize_window(window, target): w_mean = window.mean() w_std = window.std() + 1e-5 return (window - w_mean) / w_std, (target - w_mean) / w_std # 在训练循环里这样调用 x_norm, y_norm = normalize_window(x, y)加1e-5是防止窗口内数值完全相等时标准差为0。窗口归一化也有代价:如果在做时间序列异常检测,全局均值的漂移本身就是重要信号,窗口归一化会把这种慢漂移抹掉;但如果你更关心预测短期形态,它带来的稳定性通常大于损失。
多变量序列归一化时,还要注意按特征维度分别归一化,不要把5个特征拉平后混在一起算均值。不同特征的量纲可能差几个数量级,混在一起算会导致数值大的特征主导损失,卷积核学不到小量纲特征的模式。每个特征通道单独算mean/std,送入模型前再在通道维度拼接。
注意:滑窗构造和归一化顺序不能反过来。一旦先归一化再滑窗,窗口边界信息会因为全局统计量出现泄漏;正确顺序是先按训练集统计量缩放,再做滑窗,或者先滑窗再做窗口内归一化。
3. 用PyTorch搭一个可落地的1D CNN:网络结构与参数选择
滑窗做完,数据已经是(样本, seq_len)的形状,下一步就是建网络。很多人初次接触一维CNN处理序列数据,会直接把二维CNN的代码抄过来换个Conv2d为Conv1d,结果要么报维度错误,要么训练出来一团糟。这里需要理解一维卷积到底在卷什么。
3.1 为什么用Conv1d,而不是把序列转成二维图
Conv1d的卷积核只在时间维度上滑动,输入形状必须是(batch, channels, seq_len)。这里的channels在图像里是RGB三个通道,在时间序列里就是特征数量。单变量序列只有1个通道,多变量序列可以有多个通道。卷积核在每个时间位置同时对所有通道做加权求和,输出一个或若干个滤波器响应。
这种结构天然适合时间序列中的局部模式。比如振动信号的异常冲击往往持续几个采样点,股价的短期反转往往围绕某根均线波动,卷积核能在3到7个点内捕捉这些局部形状。参数共享让同一套卷积核在序列不同位置复用,所以模型参数远少于全连接网络,也远少于LSTM。CNN和RNN的经典争论放在时间序列上,我的经验是:如果你的数据没有特别强的长距离依赖,先跑一维CNN,它的训练速度会快一个数量级,代码也更短。
一维CNN不是没有缺点。它没有显式的门控机制,想要看到很长的历史依赖,必须靠堆层、扩大卷积核或使用空洞卷积。所以很多项目会先用CNN做基线,效果不够再加LSTM或Transformer,这也符合先易后难的调参顺序。
3.2 最小可运行模型:两层Conv1d + 池化 + 全连接
给一个可以直接复制到Jupyter里的最小模型,输入是上一章滑窗得到的(batch, seq_len),输出是(batch, out_steps)。
import torch import torch.nn as nn class TimeSeriesCNN(nn.Module): def __init__(self, in_channels=1, seq_len=48, out_steps=6, kernel_size=3, hidden=32, dropout=0.1): super().__init__() self.conv1 = nn.Conv1d(in_channels, hidden, kernel_size=kernel_size, padding=kernel_size // 2) self.bn1 = nn.BatchNorm1d(hidden) self.conv2 = nn.Conv1d(hidden, hidden * 2, kernel_size=kernel_size, padding=kernel_size // 2) self.bn2 = nn.BatchNorm1d(hidden * 2) self.pool = nn.MaxPool1d(2, 2) self.relu = nn.ReLU() self.dropout = nn.Dropout(dropout) # 两层卷积 + 一次池化后,时间长度变为 seq_len // 2 self.fc = nn.Linear(hidden * 2 * (seq_len // 2), out_steps) def forward(self, x): # 输入形状是 (batch, seq_len, in_channels) x = x.transpose(1, 2) # 转成 (batch, in_channels, seq_len) x = self.relu(self.bn1(self.conv1(x))) x = self.relu(self.bn2(self.conv2(x))) x = self.pool(x) x = self.dropout(x) x = x.flatten(1) return self.fc(x)代码里最关键的一行是x.transpose(1, 2)。PyTorch中Conv1d默认输入是(batch, channels, seq_len),而我们滑窗出来的(batch, seq_len)补上通道维后通常是(batch, seq_len, 1),所以必须把通道维挪到第二位。如果在训练时报错“Expected 3D input”,90%是这一步没做。
kernel_size用了3,padding=kernel_size // 2是为了让卷积前后时间长度保持不变,这样两层卷积后依然是seq_len,再经过MaxPool1d(2, 2)变成seq_len // 2。全连接层的输入维度是hidden*2乘以seq_len // 2。窗口长度、池化次数一旦改了,这里要跟着改,后面第5章会讲一个更省心的写法。
BatchNorm1d放在激活之前,训练会比裸卷积稳定得多。时间序列数据的分布经常随环境变化,BatchNorm能让每一层的输入尺度相对稳定。dropout设0.1,防止模型在重复窗口上硬记。
3.3 必调参数:kernel_size、padding、dilation与感受野
Conv1d可调参数很多,但时间序列场景下真正需要盯住的主要是下面这几个。
| 参数名 | 常用值 | 说明 |
|---|---|---|
| kernel_size | 3, 5, 7 | 卷积核覆盖的时间点数,越大感受野越大,但容易把噪声学进去 |
| padding | kernel_size // 2 | 保持时间长度不变的常用做法,等效于“same” |
| dilation | 1, 2, 4 | 空洞卷积,在不增加参数的前提下扩大感受野 |
| stride | 1 | 卷积层通常为1,降采样交给池化 |
| in_channels | 1或特征数 | 单变量序列为1,多变量序列为特征数量 |
| out_channels | 32, 64 | 第一层卷积核数量,自己是个超参数 |
感受野决定了一个输出点能看到多长的输入区间。最简单估算方法是:一层卷积的感受野等于kernel_size,两层卷积加一起大约是kernel_size + (kernel_size - 1)乘以中间空洞系数。如果你只有两层的3卷积核,感受野大约5个点,很多周期信号根本看不全。解决办法不是把kernel加到15,而是通过dilation扩大感受野。比如第一层dilation=1、第二层dilation=2,就能在两层的深度下看到大约9个点,参数量不变。
时间序列里我很少用stride>1的卷积层,因为每步都做下采样会让网络的输出对时间对齐太敏感。降采样交给MaxPool1d或者AvgPool1d更稳妥。池化不是必须的,如果序列本身不长,比如窗口只有16,干脆不池化,全连接层直接接卷积输出,避免信息损失。
这里还涉及一个常见选择:要不要在第3章的网络里加入LSTM层?我的建议是先把纯一维CNN调通,再考虑混合。混合模型一旦出现问题,你很难判断是卷积部分还是循环部分在拖后腿。CNN作为特征提取器,后面接一个轻量LSTM的做法确实有效,但不要一开始就上。
4. 训练与评估:损失函数、验证切分和时序模拟的边界
网络搭好之后,训练流程也不能照搬图像分类。很多人拿着train_test_split(random_state=42)就开跑,结果验证loss低得惊人,上线后立刻翻车。时间序列的样本之间有时间先后关系,随机切分会把未来样本混进训练集,这种泄漏会造成一个看起来完美的黑匣子模型。
4.1 时间序列切分:不能按分类任务那样随机打乱
滑窗样本之间存在大量重叠,相邻样本可能只有step个点不同。如果随机切分,同一个时间段既出现在训练集又出现在验证集,模型相当于提前看过答案。正确做法是按时间顺序切分:前80%的数据做训练,后20%做验证。
需要调超参数时,用TimeSeriesSplit而不是普通K折交叉验证。
from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=4) X = np.random.randn(800, 48) y = np.random.randn(800, 6) for fold, (train_idx, val_idx) in enumerate(tscv.split(X)): X_train, X_val = X[train_idx], X[val_idx] y_train, y_val = y[train_idx], y[val_idx] print(f"fold {fold}: train {train_idx.min()}-{train_idx.max()}, " f"val {val_idx.min()}-{val_idx.max()}")TimeSeriesSplit不会打乱顺序,它把数据按时间分成连续块,每次用前面的k块做训练、后面的1块做验证。这能暴露模型在不同时间段的泛化能力。最终报告效果时,我习惯再用一次“只训练到某个时间点,预测未来”的严格切分,因为那才是最接近真实上线的场景。
4.2 训练循环:稳定复现的最小PyTorch代码
下面这段训练代码可以直接跑,已经包含了归一化占位、验证集评估和模型保存。
import torch from torch.utils.data import TensorDataset, DataLoader # X, y 来自滑窗数据集,假设已经按 8:2 时间切分 trainX = torch.from_numpy(X[:800]).float().unsqueeze(1) # (batch, 1, seq_len) trainY = torch.from_numpy(y[:800]).float() valX = torch.from_numpy(X[800:]).float().unsqueeze(1) valY = torch.from_numpy(y[800:]).float() model = TimeSeriesCNN(in_channels=1, seq_len=48, out_steps=6, hidden=32) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=5) epochs = 80 best_loss = float('inf') for epoch in range(epochs): model.train() loader = DataLoader(TensorDataset(trainX, trainY), batch_size=64, shuffle=False) running_loss = 0.0 for xb, yb in loader: optimizer.zero_grad() out = model(xb) loss = criterion(out, yb) loss.backward() optimizer.step() running_loss += loss.item() * xb.size(0) train_loss = running_loss / len(trainX) model.eval() with torch.no_grad(): val_pred = model(valX) val_loss = criterion(val_pred, valY).item() scheduler.step(val_loss) if val_loss < best_loss: best_loss = val_loss torch.save(model.state_dict(), 'best_cnn.pt') if (epoch + 1) % 10 == 0: print(f'{epoch+1:02d} train {train_loss:.5f} val {val_loss:.5f}')关键参数说明:学习率从1e-3开始,如果loss震荡,优先降到3e-4;shuffle=False是因为时间顺序本身不是需要打乱的信息,而且保持False更容易复现结果。ReduceLROnPlateau会在验证loss连续5个epoch不下降时把学习率减半,缓解训练后期震荡。
训练集和验证集归一化要在切分之前只基于训练集统计量完成,代码里注释已经标出。如果在真实项目里用窗口归一化,要在DataLoader里逐个样本做,不能统一处理好后缓存,否则后面验证集会引用训练窗口的均值。
4.3 评估指标:为什么不能只看RMSE
MSE和RMSE是默认损失,但时间序列预测里有个几乎必然出现的问题:如果预测值永远等于最后一个观测值,RMSE也可能很低。原因是很多真实序列的一阶自相关很高,直接把当前值复制到所有未来点,就已经是相当强的基线了。这时候要加一个和业务强相关的指标:方向命中率。
import numpy as np def direction_accuracy(pred, true): # pred 和 true 的形状都是 (样本数, 多步) pred_dir = np.sign(np.diff(pred, axis=1)) true_dir = np.sign(np.diff(true, axis=1)) return float((pred_dir == true_dir).mean())方向命中率只看预测值相对输入窗口最后一个点是否涨跌正确。它对均值回归特别敏感,如果模型只是在做平滑复制,方向命中率会明显偏低。预测多步时,可以分别计算每一步的方向命中率,比如第3步的方向命中率往往比第1步低,这能告诉你模型的有效预测范围到底多长。
时间序列异常检测场景里,评估指标又不一样。你通常不是直接看预测值误差,而是把预测误差和某个阈值比较。这时更多用F1分数、精确率和召回率,因为异常样本通常稀少。MSE会把几个极端误差放大,掩盖大量小幅误差,所以异常检测里我会改用MAE或HuberLoss。
criterion = nn.SmoothL1Loss() # HuberLoss,对离群点更稳健SmoothL1Loss在误差小的时候像MSE,误差大的时候像MAE,不会让单个异常样本主导梯度。这个损失在时间序列异常检测里比纯MSE好用得多。
5. 避坑:一维CNN处理时间序列的5个常见问题
这一章全部来自真实项目里的“翻车”记录,按照出现频率从高到低排列。每一条都是现象、原因、解决三层。你照着检查一遍,能省下好几天调试时间。
5.1 输入形状报错:Conv1d期待的到底是几维
现象:代码跑到model(xb)时直接报RuntimeError: Expected 3D input,而且提示输入是2维。
原因:很多人做完滑窗后拿到(batch, seq_len)就直接喂给模型,但Conv1d要求(batch, channels, seq_len)。单变量序列的channels就是1,这个维度不能省。
解决:在训练循环里给输入补一个通道维,或者在__getitem__阶段直接返回3维数组。
# 原输入形状 (batch, seq_len) x = x.unsqueeze(1) # 多变量输入形状 (batch, seq_len, features) x = x.transpose(1, 2)我建议统一在Dataset.__getitem__里返回(1, seq_len),这样后续模型不用关心数据处理细节。多变量时返回(features, seq_len)。
5.2 训练loss不稳,验证loss乱跳
现象:前10个epoch训练loss下降很快,之后开始震荡;验证loss忽高忽低,像随机游走。
原因:滑窗重叠度高,相邻样本高度相关,小批量内的样本彼此太像,梯度方向非常不稳定。另一个常见原因是学习率太大,Adam默认1e-3有时候对时间序列并不友好。
解决:把滑窗step从1调到3或5,降低样本重复度;学习率先降到3e-4;在网络里加一个BatchNorm1d。如果还不稳定,检查是不是每个batch里混入了量纲差异巨大的特征。多变量序列每个通道单独归一化,否则大数值通道会主导梯度更新。
5.3 预测曲线像是延迟复制,方向命中率不堪入目
现象:把预测值和真实值画在一张图里,发现预测曲线比真实曲线整体“晚”了一会儿,像把输入往后平移了一样。你拿到RMSE还过得去,但方向命中率不到50%,基本等于白做。
原因:序列自身一阶自相关太高,模型学到的最优策略就是把输入窗口最后一两个点复制到未来。这在纯回归指标下是最优的,但业务上毫无价值。也可能是输入窗口太短,没有覆盖完整周期,模型根本没有可用的周期性信息。
解决:对原始序列做一阶差分,让模型去预测变化量,而不是绝对值。
diff_series = np.diff(raw) X, y = make_windows(diff_series, input_len=48, horizon=6, step=3) # 预测得到的是未来的差分值,评估时需要累加还原: pred_cum = raw[input_len] + np.cumsum(pred_diff, axis=-1)差分之后,模型不再能靠复制当前值偷懒。如果预测目标本身是涨跌方向,直接预测分类标签也可能是更好的选择。这一步是时间序列预测里最常见的翻车点,宁可先差分,不要先叠网络。
5.4 训练loss很低,验证loss很高:过拟合是滑窗出来的
现象:训练集RMSE降到接近0,验证集RMSE是训练集的几倍,模型没什么泛化能力。
原因:step=1时相邻窗口几乎完全重叠,训练集信息高度冗余,模型很容易把特定时间段的绝对数值背下来。卷积核参数数并不算多,但冗余样本把训练变成了记忆任务。
解决:增加step,减少样本重叠。同时加噪声增广,让模型不能记住原始数值,只能学模式。我给序列数据做增广时,一般给输入加均值为0、标准差为该窗口数值标准差1%(或5%)的高斯噪声。
def add_noise(x, noise_std=0.01): noise = torch.randn_like(x) * x.std(dim=-1, keepdim=True) * noise_std return x + noise噪声只加在训练集的输入上,标签不变,验证集保持干净。数据增广配合Dropout,能明显缓解滑窗重叠带来的过拟合。
5.5 全连接层维度对不上:池化把序列长度算错了
现象:模型前向传播到flatten之后,Linear层报mat1 and mat2 shapes cannot be multiplied。
原因:网络里写了MaxPool1d(2, 2),但没注意到池化只在整除时才能把长度严格减半。如果seq_len是奇数,或者中间做了非对称padding,实际展平长度和hidden * 2 * (seq_len // 2)对不上。
解决:不用手算flatten长度,改用全局平均池化。
# 在最后一层卷积之后用 mean(dim=-1) x = self.relu(self.bn2(self.conv2(x))) x = x.mean(dim=-1) # 每个通道压成一个数,长度维度消失 self.fc = nn.Linear(hidden * 2, out_steps)全局平均池化把每个通道在时间维度上取平均,输出维度固定为hidden*2,和输入长度彻底解耦。这样换窗口长度时不用改网络结构,模型更普适。缺点是会丢失时间上的精确对齐信息,但作为全连接层之前的过渡,通常利大于弊。
6. 进阶:多变量输入、因果卷积与模型验证技巧
6.1 多变量特征放通道,而不是硬拼成一个数
多变量时间序列不只有一个特征,比如同时采集温度、压力、振动。一维CNN处理序列数据时,最自然的做法是把每个特征作为一个通道,让卷积核同时跨时间和跨特征做组合。
model = TimeSeriesCNN(in_channels=5, seq_len=48, out_steps=6) # forward 时输入形状 (batch, 5, 48)这比把5个特征上下拼成一条长序列更合理,因为卷积核会在每个时间步同时对5个特征加权,特征之间的交互是局部的、可解释的。注意每个通道单独归一化,否则量纲差异会让模型偏向大数值通道。
6.2 用因果卷积做在线预测
如果你做的是未来时间点的预测,用普通的中心padding会让第t个输出在训练时看到t+1乃至更后面的输入。离线分析无所谓,但一旦上线,数据还没发生,模型就会用“未来”做预测,表现和测试集完全脱节。常见做法是把padding改成kernel_size - 1,也就是所谓的因果卷积。卷积核在当前位置左侧补零,右侧不补,输出只依赖当前及之前的输入。
nn.Conv1d(in_channels, hidden, kernel_size=3, padding=2, dilation=1)当kernel_size=3时,padding=2可以配合后面的切片只取到seq_len - 1,但更简单的是自己用F.pad(x, (kernel_size - 1, 0))。我在做实时预测项目时一定用因果填充,它能避免一个非常隐蔽的黑匣子问题:训练指标漂亮,上线后第一条预测就走样。
6.3 验证技巧:滚动后测比固定切分更接近真实场景
固定切分只评估一个时间点的模型,滚动后测能评估模型在持续更新下的表现。常见做法是:训练到第T天,预测T+1到T+H,然后把这一段时间的真实值并入训练集,滚动到下一个窗口。用滑窗构造出来的样本,可以复用前面的SlidingWindowDataset来实现。
我自己每次跑完模型,还会画一张“预测 vs 真实 vs 输入窗口末尾”的对齐图。把每个预测点对应到输入窗口末尾之后,如果预测曲线明显贴着输入末尾走,方向命中率又低,就先去做差分或缩短horizon,而不是继续堆网络层数。这个习惯比任何指标都直接,救过我好几次。希望帮到你。
本文还有配套的精品资源,点击获取