
简介这份资源面向希望深入理解序列生成与对抗学习的开发者与研究人员提供SeqGAN的Python完整源码与配套数据帮助解决传统GAN在文本、音频等序列数据上难以建模时间依赖与顺序性的问题。压缩包共13个文件约5.75MB以6个py源码文件为核心涵盖生成器、判别器、rollout与训练主流程另含2个pkl模型参数文件、2张训练曲线图、1个txt实验日志、1个md说明文档及1个zip归档结构清晰便于按模块阅读。已有393人学习下载。项目完整呈现两阶段训练思路先以预言机正样本和最大似然估计做监督预训练再通过对抗训练与策略梯度交替优化生成器与判别器读者可据此掌握序列奖励函数、损失设计与训练循环的实现细节并借助实验日志与曲线图观察收敛过程为强化学习、序列建模与对抗学习的交叉应用提供可复现的实践参考。1. 从一张手写数字图说起SeqGAN 到底在生成什么如果你手头有一批序列数据——离散的 token、音符、点击流、代码行——想训一个生成模型大概率会先试 RNN 加最大似然。结果往往是这样loss 降得挺好看采样出来的东西却像复读机翻来覆去就那几句。原因不玄学最大似然在每一步都逼模型拟合 teacher forcing 下的真实前缀可推理时喂进去的是模型自己吐出来的 token分布一旦偏一点误差就滚雪球。更麻烦的是序列生成的评价指标BLEU、ROUGE、人工打分根本不可导你没法直接拿它当 loss 反传。SeqGAN 就是冲着这个矛盾来的。它把生成器当成一个强化学习里的策略生成一个完整序列等于走完一条动作轨迹判别器负责判断这条轨迹像不像真数据判别器的输出当作 reward 回传给生成器。判别器本身用 CNN 做二分类生成器用 LSTM 做自回归采样。这套结构在 2017 年前后是序列生成的一个标志性方案今天拿它当对抗式序列生成的入门骨架依然合适——代码量不大逻辑闭环清晰能跑通就说明你把「离散采样 策略梯度 判别器打分」这条链路吃透了。这份资源是一套 Python 完整源码加配套数据核心就是 SeqGAN 的训练与采样流程。适合两类人一类是想搞懂对抗式序列生成到底怎么落地、不想只看公式的另一类是手里有序列数据、想拿一个能改能跑的 baseline 做实验的。下面我按「先跑通、再拆解、最后避坑」的顺序把这份源码拆开讲。2. 环境与数据准备把 SeqGAN 跑起来的第一公里2.1 依赖版本与目录结构这份源码是纯 Python 实现没有依赖冷门框架常见做法是用 PyTorch 或 TensorFlow 其一。我拿到手第一件事是确认版本因为序列模型对 API 变动很敏感。下面是我一般会先跑的检查脚本用来确认环境是否齐活# 检查 Python 与关键库版本避免 API 不兼容 python --version python -c import torch; print(torch, torch.__version__) python -c import numpy; print(numpy, numpy.__version__) python -c import nltk; print(nltk, nltk.__version__)逻辑说明先确认 Python 主版本再逐个 import 关键库并打印版本。参数上torch 建议 1.x 以上numpy 不要低于 1.19nltk 主要用于数据预处理里的分词或 BLEU 计算。如果 import 直接报错说明依赖没装全先补依赖再谈训练。目录结构上这类工程通常长这样data/放原始语料generator.py和discriminator.py分别是生成器和判别器rollout.py负责蒙特卡洛采样train.py是主训练入口utils.py放数据加载和指标。你拿到手先别急着改代码先按这个结构对一遍文件是否齐全。2.2 数据格式与预处理SeqGAN 吃的是离散序列输入通常是一段段 token id。数据准备这一步最容易翻车因为格式不对训练直接崩。常见做法是把语料转成「每行一条序列、token 用空格分隔」的文本再用词表映射成 id。下面是我常用的预处理片段# 构建词表并把语料转成 id 序列 from collections import Counter def build_vocab(corpus, min_freq1): counter Counter() for line in corpus: counter.update(line.strip().split()) # 保留出现次数达标的 token0 留给 padding vocab {w: i 1 for i, (w, c) in enumerate(counter.items()) if c min_freq} vocab[pad] 0 return vocab def encode(corpus, vocab, max_len20): seqs [] for line in corpus: ids [vocab.get(w, 0) for w in line.strip().split()][:max_len] # 不足 max_len 的用 0 补齐保证 batch 内等长 ids [0] * (max_len - len(ids)) seqs.append(ids) return seqs逻辑说明build_vocab统计词频并建立 token 到 id 的映射id 从 1 开始0 专门留给 padding这样后续算 loss 时可以用 mask 把 padding 排除。encode负责截断和补齐max_len是关键参数——设太小会丢信息设太大显存吃紧且 padding 比例高。我一般先统计语料长度分布取 95 分位作为max_len。提示如果你的数据是中文分词这步别偷懒直接按字符切和按词切效果差别很大先确认源码里用的是哪种。2.3 首次运行与冒烟测试环境齐了、数据转好了别一上来就开全量训练。我习惯先跑一个极小配置做冒烟测试确认前向、反向、采样三条路都通# 小样本冒烟测试1 个 epoch小 batch确认流程不报错 python train.py --data data/sample.txt --epochs 1 --batch_size 16 --max_len 20 --gpu 0逻辑说明--epochs 1只跑一轮--batch_size 16降低显存压力--max_len 20和预处理保持一致。这一步的目标不是看效果是看有没有 shape 不匹配、device 不一致、loss 变 NaN 这类硬伤。跑通了再放大配置能省下大量无效等待。3. 生成器与判别器的对抗逻辑reward 是怎么回传的3.1 生成器LSTM 策略网络生成器本质是一个自回归的 LSTM。每一步输入当前 token 和隐藏状态输出下一个 token 的概率分布采样得到动作再把动作喂回下一步。它和普通语言模型的区别在于训练信号不是拿交叉熵直接监督而是等整条序列生成完由判别器给一个分数再用策略梯度更新。import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim): super().__init__() self.embed nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, batch_firstTrue) self.fc nn.Linear(hidden_dim, vocab_size) def forward(self, x, hiddenNone): # x: [batch, seq_len] 的 token id emb self.embed(x) out, hidden self.lstm(emb, hidden) logits self.fc(out) return logits, hidden逻辑说明embedding把离散 id 变成稠密向量padding_idx0保证 padding 不参与梯度。LSTM的batch_firstTrue让输入维度是[batch, seq_len, dim]这是后面接全连接层的前提。fc把隐藏状态映射回词表大小得到每个位置的 logits。参数上embed_dim一般 64 到 256hidden_dim128 到 512太大在小数据上必过拟合。3.2 判别器CNN 二分类打分器判别器用一维卷积对整条序列做二分类输出「真」的概率。它比 LSTM 判别器训练更快且对局部 n-gram 模式敏感正好补上生成器只看全局的短板。class Discriminator(nn.Module): def __init__(self, vocab_size, embed_dim, num_filters, kernel_sizes): super().__init__() self.embed nn.Embedding(vocab_size, embed_dim, padding_idx0) # 多尺度卷积核捕捉不同长度的局部模式 self.convs nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, k) for k in kernel_sizes ]) self.fc nn.Linear(num_filters * len(kernel_sizes), 1) def forward(self, x): emb self.embed(x).transpose(1, 2) # [B, dim, L] feats [torch.relu(c(emb)).max(dim2)[0] for c in self.convs] cat torch.cat(feats, dim1) return torch.sigmoid(self.fc(cat))逻辑说明transpose(1, 2)把维度从[B, L, dim]转成[B, dim, L]因为Conv1d要求通道在前。每个卷积核做完后取max做全局池化把变长序列压成定长向量。kernel_sizes常见取[1, 2, 3, 4, 5]覆盖不同 n-gram 长度。最后sigmoid输出 0 到 1 的概率。3.3 蒙特卡洛 rollout中间步骤的 reward 从哪来这是 SeqGAN 最核心也最容易写错的地方。生成器每生成一个 token 就更新一次是不行的因为判别器只能对完整序列打分。但如果等整条序列生成完再给 reward中间步骤就没有信用分配。SeqGAN 的解法是蒙特卡洛搜索对每个中间状态用当前策略把剩余序列补全多次取判别器打分的平均作为该步 reward。def get_reward(gen, dis, prefix, rollout_num16, max_len20): # prefix: 已生成的前缀 [1, cur_len] rewards [] for _ in range(rollout_num): seq prefix.clone() hidden None # 用当前策略补全剩余部分 while seq.size(1) max_len: logits, hidden gen(seq, hidden) next_token torch.multinomial( torch.softmax(logits[:, -1, :], dim-1), 1) seq torch.cat([seq, next_token], dim1) rewards.append(dis(seq).item()) return sum(rewards) / len(rewards)逻辑说明rollout_num是蒙特卡洛采样次数越大 reward 估计越准但计算量线性增长常见取 16。torch.multinomial按概率采样而不是取 argmax保证探索性。每次补全都从当前前缀出发用同一个生成器这样 reward 才反映当前策略的真实水平。这里如果写成贪心解码reward 会偏乐观训练容易崩。3.4 策略梯度更新把 reward 变成 loss拿到每步 reward 后用策略梯度更新生成器。核心是loss -reward * log_probreward 高的动作提高概率低的压低。def generator_loss(log_probs, rewards): # log_probs: 每步的 log 概率 [seq_len] # rewards: 每步的 reward [seq_len] loss 0 for lp, r in zip(log_probs, rewards): loss -lp * r return loss逻辑说明log_probs是采样时记录的对数概率rewards是 rollout 得到的分数。负号是因为优化器默认做梯度下降而我们要最大化期望 reward。实际实现里常加一个 baseline比如 reward 均值降低方差否则训练抖动会很大。这一步是血泪经验不加 baselineloss 曲线能抖到你怀疑人生。4. 训练调参与常见翻车排查4.1 判别器太强导致生成器梯度消失现象训练几轮后生成器 loss 不再下降采样结果全是高频词或重复 token。原因判别器收敛太快对生成样本一律给接近 0 的 reward生成器拿不到有效梯度。解决降低判别器学习率或对判别器做标签平滑也可以每训 k 轮判别器才训一次生成器。我一般把判别器 lr 设成生成器的 1/2 到 1/5。4.2 reward 方差过大训练不收敛现象loss 剧烈震荡采样质量时好时坏。原因蒙特卡洛 rollout 次数太少reward 估计噪声大。解决把rollout_num从 16 提到 32 或 64同时引入 reward 均值作为 baseline。代价是训练变慢但稳定性提升明显。4.3 padding 参与 loss 计算现象模型学会生成大量 padding token序列有效长度越来越短。原因算 loss 时没做 maskpadding 位置的 log_prob 也被计入。解决在计算log_probs和 reward 时用 mask 把 padding 位置排除确保只有有效 token 参与更新。4.4 显存溢出与 batch 设置现象训练中途 OOM。原因max_len或batch_size设太大rollout 阶段又要保存多份中间状态。解决先降batch_size再考虑降max_lenrollout 时用torch.no_grad()包住补全过程能省不少显存。4.5 采样结果全是同一句话现象不管输入什么前缀生成结果高度雷同。原因模式崩溃生成器找到了一个能骗过判别器的固定输出。解决提高采样温度增加多样性或在 reward 里加入去重惩罚。温度参数一般从 1.0 开始调低于 0.7 会明显变保守。5. 进阶玩法用 BLEU 和自采样验证生成质量训练跑通只是开始怎么判断生成质量是真提升还是过拟合得靠指标。SeqGAN 原论文用 BLEU 衡量生成序列和真实序列的 n-gram 重合度这个思路今天依然好用。下面是我常用的验证脚本from nltk.translate.bleu_score import sentence_bleu def evaluate_bleu(gen, real_seqs, vocab, num_samples200, max_len20): inv_vocab {v: k for k, v in vocab.items()} scores [] for _ in range(num_samples): # 从起始 token 开始自回归采样 seq torch.tensor([[vocab.get(start, 1)]]) hidden None while seq.size(1) max_len: logits, hidden gen(seq, hidden) next_token torch.multinomial( torch.softmax(logits[:, -1, :], dim-1), 1) seq torch.cat([seq, next_token], dim1) gen_tokens [inv_vocab.get(i, unk) for i in seq[0].tolist()] # 和真实序列逐条比 BLEU ref [[inv_vocab.get(i, unk) for i in real_seqs[0]]] scores.append(sentence_bleu(ref, gen_tokens)) return sum(scores) / len(scores)逻辑说明inv_vocab把 id 映射回 token 方便算 BLEU。采样时从起始 token 出发逐步生成直到max_len。sentence_bleu的参考集这里简化成取一条真实序列实际用的时候应该对每条生成序列匹配最相近的真实序列否则 BLEU 会偏低。num_samples取 200 到 500 之间太少指标不稳。除了 BLEU我还会做自采样检查固定随机种子生成 20 条序列人眼看有没有重复、有没有语法崩坏。指标是冷的人眼是热的两个都过才算数。参数上温度设 1.0 看多样性设 0.8 看质量上限对比着看能判断模型是保守还是发散。有个容易忽略的点验证集要和训练集严格分开。我见过有人拿训练语料算 BLEU分数高得离谱一换新数据就原形毕露。从那以后我每次评估都强制走一遍数据隔离检查确认验证集没混进训练流程。希望这份拆解帮到你源码和数据拿下去先跑冒烟测试再按章节里的参数逐项调别一上来就全量开训。本文还有配套的精品资源点击获取