资讯动态

GAN-RNN时间序列缺失填补:递归生成对抗网络原理与TensorFlow复现

发布时间:2026/10/23 8:01:18 来源:尧图企业网站定制
简介为时间序列数据插补而设计的递归卷积 Seq2seq 生成对抗网络代码包面向有 TensorFlow 2 使用经验的数据科学家和机器学习工程师可迁移到网络流量、气象、电商等连续型序列的缺失值修复工作。项目围绕 Kaggle 的 Wikipedia Web 流量预测数据集给出了三种可执行方案基础递归卷积序列到序列模型、由生成器与判别器对抗训练的标准生成对抗网络以及在真实重构误差与对抗损失之间取得平衡的部分对抗模型配套 YAML 配置文件完整描述数据预处理、训练和测试的超参数Python 脚本按阶段组织便于读者理解数据装载、模型构架和评估思路。压缩包体积约 3.01MB主体为 Python 脚本与 YAML 配置信息内容紧凑且便于快速上手。目前已有 1428 人浏览学习适合希望掌握生成对抗网络在时间序列插补中落地细节的中高级开发者用作理解原理、对照实验或二次开发的参考。1. 递归GAN估算时间序列缺失GAN-RNN的架构选择与复现价值流量监控、传感器日志和运营指标里时间序列带着缺失值是常态不是异常。多数人第一反应是用前向填充或者线性插值补窟窿但这类方法只利用了缺口两侧的局部信息对周期性、趋势性和长程依赖几乎无能为力。GAN-RNN_Timeseries-imputation 这个项目把生成对抗网络和循环结构结合在一起用生成器学习整段序列的分布再用判别器逐点监督真假专门解决「非随机缺失、长缺血块」的时间序列估算问题。它在 Kaggle 的 Wikipedia Web 流量预测数据集上用 TensorFlow 2 完成实现训练好的模型可以直接复用。适合做时序预测、异常检测的数据从业者也适合正在学习 seq2seq 与对抗生成机制搭配的开发者。项目代码结构清晰模型定义、训练循环和数据切分三块边界分明可作为模板改造到自己的场景里。下面先从模型设计角度拆再给出可复现的训练流程和参数清单。2. 生成器与判别器的实现细节为什么递归GAN比纯CNN或纯RNN更适合估算2.1 掩码输入与缺失标记让模型知道「哪个位置是假的」时间序列估算任务里模型首先要区分「观测到的真实值」和「待填补的缺失值」。常见做法是构造一个与序列等长的二进制掩码向量1 表示该位置有观测值0 表示缺失。训练时把原始序列、掩码和噪声拼接喂给生成器。它的好处是让模型不靠猜测理解缺失位置而是通过显式标记学习缺失模式。def create_mask(series, missing_rate0.2): mask np.ones_like(series, dtypenp.float32) n len(series) missing_idx np.random.choice(n, sizeint(n * missing_rate), replaceFalse) mask[missing_idx] 0.0 return mask这段代码按固定比例随机生成缺失位置。注意这里的缺失率是训练时用的目的是模拟各种缺失占比下的估算效果。测试阶段要使用实际数据的缺失掩码不能用随机生成的掩码去冒充真实缺失位置。参数 missing_rate 控制在 0.1 到 0.3 之间比较合理过高会让生成器学不到可信的上下文过低则退化成一个简单回归问题。掩码输入是这类模型与普通序列预测模型最大的区别点。预测模型看到的是完整历史估算模型必须明确区分「哪些点不能信」。实现时用tf.concat([x, mask], axis-1)把两个向量拼接后作为输入特征维度变为原来的两倍后续 LSTM 层的输入尺寸要相应调整。2.2 生成器用 LSTM 编码器解码器从部分观测重建完整序列生成器的思路是先让编码器读取带掩码的输入序列把整个序列压缩成上下文向量再由解码器逐步生成缺失区域。这与 seq2seq 翻译模型的结构同源区别在于输出长度与输入长度相等且解码器每个时间步都要参考掩码决定输出是保留原值还是使用生成的估算值。def build_generator(seq_len, feature_dim, hidden_dim128): inputs tf.keras.Input(shape(seq_len, feature_dim)) mask_input tf.keras.Input(shape(seq_len, 1)) combined tf.keras.layers.Concatenate(axis-1)([inputs, mask_input]) encoder tf.keras.layers.LSTM(hidden_dim, return_sequencesTrue)(combined) encoder tf.keras.layers.LSTM(hidden_dim, return_sequencesFalse)(encoder) repeat tf.keras.layers.RepeatVector(seq_len)(encoder) decoder tf.keras.layers.LSTM(hidden_dim, return_sequencesTrue)(repeat) decoder tf.keras.layers.LSTM(hidden_dim, return_sequencesTrue)(decoder) output tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(1))(decoder) model tf.keras.Model(inputs[inputs, mask_input], outputsoutput) return model生成器实际输出的是整个序列的估算值不仅仅输出缺失位置。训练时通过计算「重建损失」让生成器在非缺失位置尽量贴近原始值在缺失位置则完全依赖对抗损失的信号来学习合理分布。隐藏层维度 hidden_dim 设为 128 在流量数据这类中等规模序列上表现均衡序列特别长的场景可以提高到 256但训练时间和显存占用会同步上升。2.3 判别器用双向LSTM加全连接输出局部与全局真伪判断判别器输入是「生成器输出的完整序列」或「带缺失标记的真实完整序列」任务是对整段序列做二分类的真伪判断。用双向 LSTM 而不是单向是因为缺失区域的合理性判断既依赖前面的上下文也依赖后面的观测值单向结构只能利用一半信息。def build_discriminator(seq_len, feature_dim, hidden_dim128): inputs tf.keras.Input(shape(seq_len, feature_dim)) x tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(hidden_dim, return_sequencesTrue))(inputs) x tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(hidden_dim, return_sequencesFalse))(x) x tf.keras.layers.Dropout(0.3)(x) output tf.keras.layers.Dense(1, activationsigmoid)(x) model tf.keras.Model(inputsinputs, outputsoutput) return model判别器的输出是一个标量概率值表示输入序列是真实数据的概率。这里不需要返回每个时间步的判断结果因为训练目标是区分整段序列的分布真伪逐点判断反而容易让判别器过度关注局部细节忽略全局趋势的一致性。2.4 判别器与生成器的训练节奏联合训练的稳定性控制GAN 训练的核心矛盾是生成器和判别器的能力不能差距太大。判别器太强生成器的梯度会迅速消失输出变成常数生成器太强判别器无法提供有效的学习信号。实际训练中常见做法是判别器每更新 1 次生成器更新 2 次让生成器有更多机会逼近真实分布。tf.function def train_step(x_batch, mask_batch, g_opt, d_opt, g_loss_fn, d_loss_fn): noise tf.random.normal(shapetf.shape(x_batch), mean0.0, stddev0.1) with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape: generated generator([x_batch, mask_batch], trainingTrue) real_output discriminator(x_batch, trainingTrue) fake_output discriminator(generated, trainingTrue) d_loss d_loss_fn(real_output, fake_output) g_loss g_loss_fn(fake_output, x_batch, mask_batch, generated) gradients_of_generator gen_tape.gradient(g_loss, generator.trainable_variables) gradients_of_discriminator disc_tape.gradient(d_loss, discriminator.trainable_variables) g_opt.apply_gradients(zip(gradients_of_generator, generator.trainable_variables)) d_opt.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))给真实输入加噪声是关键操作防止判别器仅凭微小数值差异就区分真假。训练过程中建议每 5 个 epoch 打印一次生成器的重构误差和判别器的准确率如果判别器准确率长期高于 0.95说明生成器已经跟不上需要降低判别器学习率或增大 dropout。3. Wikipedia 流量数据的预处理与窗口切分从原始日志到训练样本3.1 数据格式与清洗策略log 变换和缺失比例统计Wikipedia Web 流量数据集每行是一条页面的逐日访问量记录时间跨度数年。原始数值跨度极大从个位数到几十万直接送入 LSTM 会导致梯度更新不稳定。常规做法是先做log1p变换压缩量级再按全局均值方差做标准化。data np.load(wikipedia_traffic.npy) # 形状 (num_pages, seq_len) logged np.log1p(data) mean logged.mean(axis0, keepdimsTrue) std logged.std(axis0, keepdimsTrue) 1e-6 normalized (logged - mean) / std先 log 再标准化顺序不能反。如果先标准化再 log负值会让对数变换产生 NaN。缺失值在原始数据中通常用 NaN 表示需要先填充成 0 再做掩码标记不能直接送入模型。流量数据的缺失往往不是随机分布而是整段整段地缺失这种情况对模型的挑战更大也更能体现 RNN 结构的长程记忆能力。3.2 滑动窗口策略窗口长度与步长的选择逻辑训练样本通过滑动窗口从完整序列中截取。窗口越长模型能捕获的周期特征越多但训练样本数量越少且 LSTM 的长期依赖学习难度增加。项目默认窗口长度为 120 天对应约 4 个月的流量趋势能够覆盖周周期和月度波动。步长设为 10 天让相邻窗口有重叠增加训练样本数量。def create_sequences(data, window120, stride10): X, M [], [] for i in range(0, len(data) - window, stride): segment data[i:iwindow] mask np.ones_like(segment) mask[np.isnan(segment)] 0 segment np.nan_to_num(segment, nan0.0) X.append(segment) M.append(mask) return np.array(X), np.array(M)这段代码是通用的时序切窗逻辑。实际使用时要根据业务场景调整窗口长度流量数据用 120 天股票日线数据可以考虑 60 或 90 天传感器高频数据可以按小时数换算。窗口太长训练速度慢窗口太短模型看不到完整周期估算结果会偏向局部均值。3.3 训练集与验证集的切分按时间序列先后切不能随机切时间序列数据必须按时间顺序切分训练集用早期数据验证集用后期数据。随机切分会让模型偷看未来信息验证得分虚高。项目代码里直接按索引比例切分即可split_idx int(len(sequences) * 0.8) train_x, val_x sequences[:split_idx], sequences[split_idx:] train_m, val_m masks[:split_idx], masks[split_idx:]这里有一个容易被忽略的坑验证集的缺失掩码应该来自真实缺失标记而不是人为构造的模拟掩码。如果直接使用训练时的随机掩码函数生成验证掩码会高估模型的真实表现因为验证集里的「缺失」原本是有真实值的模型看到的是被完整信息包裹的人工缺口难度远低于真实场景。4. 训练超参与损失函数选择配置不当会导致模式崩溃4.1 生成器损失对抗损失与重建损失的加权生成器的损失由两部分构成判别器的对抗损失和重建损失。重建损失负责让非缺失位置的输出贴近原始值对抗损失负责让缺失位置的填补符合整体数据的分布特征。def generator_loss(fake_output, real_data, mask, generated): adversarial_loss tf.keras.losses.BinaryCrossentropy(from_logitsTrue)( tf.ones_like(fake_output), fake_output ) reconstruction_loss tf.reduce_mean(tf.square((generated - real_data) * mask)) total_loss adversarial_loss 10.0 * reconstruction_loss return total_loss, adversarial_loss, reconstruction_loss重建损失的权重设为 10目的是让非缺失位置的输出严格贴合真实值否则生成器会倾向于输出更「安全」的中间值看起来平均损失更低但缺失位置的分布特性会失真。要注意重建损失只对非缺失位置求均值所以代码中乘以了 mask —— 缺失位置 mask 为 0不会计入重构误差。如果这一处实现有误把缺失位置的误差也算进去模型最终会把输出推到接近 0 的位置来压平损失。判别器损失直接用标准二分类交叉熵。真实数据标签为 1生成数据标签为 0不采用梯度惩罚也能收敛但训练过程中每轮打印的准确率如果长期保持 0.98 以上就需要考虑加入标签平滑。4.2 优化器选择与学习率设置Adam 与梯度裁剪的组合推荐使用 Adam 优化器学习率设置在 1e-4 到 3e-4 之间。GAN 训练对学习率比普通监督学习更敏感学习率太高会让判别器快速过拟合训练集学习率太低则整个训练过程变得缓慢。generator_optimizer tf.keras.optimizers.Adam(learning_rate1e-4, beta_10.5, beta_20.9) discriminator_optimizer tf.keras.optimizers.Adam(learning_rate1e-4, beta_10.5, beta_20.9)beta_1 设置为 0.5 而不是默认的 0.9是 GAN 训练中的常见调整可以降低梯度更新的惯性让模型对生成器分布变化更敏感。梯度裁剪同样重要尤其在序列较长的场景下gradient_clip 1.0 gradients_of_generator [tf.clip_by_norm(g, gradient_clip) for g in gradients_of_generator]不裁剪梯度时LSTM 在长序列训练过程中容易出现梯度爆炸表现为损失函数出现 NaN 或者损失值突然跳到极大。裁剪阈值设 1.0 属于比较保守的选择能够稳定训练过程但可能略微延长收敛时间。4.3 Epoch 数量与早停策略看验证集重建误差而非对抗损失对抗训练的损失曲线不能直接作为评判标准因为生成器和判别器在不断博弈单边损失下降不代表整体模型变好。可靠的评估指标是验证集上的重建损失——只看非缺失位置计算生成值与真实值的 MSE。def evaluate_imputation(model, val_x, val_m): pred model.predict([val_x, val_m]) mask val_m sq_error np.square(pred - val_x) * mask mse sq_error.sum() / mask.sum() return mse早停逻辑就基于这个 MSE连续 10 个 epoch 没有下降就停止训练并回滚到最佳权重。项目默认训练 200 个 epoch实际使用中通常在 50 到 100 个 epoch 附近就能收敛。5. 避坑指南GAN-RNN时间序列估算中的常见问题与排查记录5.1 判别器准确率长期超过 0.97生成器输出趋向常数现象训练日志里判别器准确率稳定在 0.97 以上生成器输出各种输入都几乎相同缺失区域的填补值全部趋近于某个固定数值。原因判别器能力过强能够轻易区分生成数据和真实数据此时提供给生成器的梯度信号几乎为零生成器陷入局部最优。解决将判别器的学习率降低到生成器的十分之一或者每训练一次判别器就训练三次生成器。同时给判别器的输入加入标准差为 0.05 的高斯噪声。还可以在判别器的全连接层使用 label smoothing把真实标签从 1 改为 0.9。5.2 验证集重建损失很低但可视化发现填补区域明显偏离趋势现象评估脚本计算出的 MSE 数值很小但把生成结果画成折线图后发现缺失区域被填补成一条水平直线和前后趋势有明显断层。原因验证集的掩码设置有问题。常见做法是在验证时随机生成了掩码于是「缺失」位置仍是真实观测值计算出的训练信号模型学会了在非缺失位置输出原值后掩码位置的预测实际上变成了纯噪声而 MSE 计算时把非缺失位置的主导误差稀释了。解决验证时务必复用训练时使用的固定掩码或者在加载真实数据时记录缺失位置直接使用业务中的真实缺失标记。不要用随机掩码评估缺失估算效果。从那以后我每次做评估都用真实掩码重跑一遍验证流程再跟人工抽样对比避免被指标欺骗。5.3 损失函数突然出现 NaN且无法恢复现象训练进行到第 200 步左右时生成器损失直接变成 NaN后续无法恢复只能重启训练。原因数值溢出。输入数据存在极大峰值没有做 log 压缩或者生成器输出的值范围不受约束导致 LSTM 内部状态呈指数增长。解决检查输入数据是否全部经过 log1p 变换和标准化生成器最后一层不要用 tanh 或 sigmoid 激活直接保持线性输出即可。训练参数里建议加上tf.keras.backend.set_floatx(float32)来固定精度。5.4 填补结果整体偏小复现出来的序列方差低于原始数据现象填补后的序列走势大致合理但整体方差明显低于原始序列看起来所有值都被拉向均值方向。原因重建损失使用的是 L2 损失对离群值惩罚严重生成器倾向于输出接近均值的「保守」结果来降低总体误差在缺失区域尤其明显。解决把重建损失替换为 L1 损失MAE或者将重建损失权重从 10 调低到 3给对抗损失更多发挥空间。另一种方案是对非缺失位置使用 L2对缺失位置只使用对抗损失让生成器自由发挥数据的分布特征。6. 进阶验证思维与方法把「算出来的」变成「能信服的」数值指标只是第一步真正判断估算结果能不能用需要做差异可视化。把原始序列、掩码标记和生成结果画在同一张图上连续对比 20 个样本点。如果发现缺失区域填补值在边界处出现断崖式跳变说明模型没有学到平滑过渡能力如果填补值和前后趋势高度吻合说明模型的判断是可信的。第二个建议是支持缺失比例压力测试。通常训练时的 missing_rate 设置在 0.1 到 0.2但生产环境的实际缺失比例可能达到 30%。可以在验证阶段构造 multiple missing_rate 的测试集观察 MSE 随缺失率上升的曲线。如果缺失率从 0.1 升到 0.3 时 MSE 只增加 20%说明模型鲁棒如果成倍增加则需要考虑增加训练数据里高缺失率的样本比例。第三个技巧是用真实业务中的代表性子集检查。随便找几个典型页面或典型传感器节点看填补值是否符合该场景的物理直觉。这一步需要领域经验参与机器无法替代。模型训练完成后保存为 SavedModel 格式。后续业务系统加载模型时要做的是数据按同样顺序做 log1p、标准化、构造掩码调用模型输出再逆标准化、逆 log 变换还原成原始量纲。这一条链路当初让我吃了不少亏——复原顺序反了所有预测值全部对不上。从那以后我每次提交模型固化流程都强制走一遍「训练环境数据 → 测试环境数据 → 生产环境数据」的三段一致性校验确保预处理完全对齐。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑