GAN实战:用PyTorch生成6+1彩票号码的完整流程

GAN实战:用PyTorch生成6+1彩票号码的完整流程 简介面向机器学习、深度学习与时间序列预测方向的开发者这份资源以蔬菜销量数据为场景演示如何用生成对抗网络实现序列预测最终生成“菜票号码”。资源共7个文件由6个Python脚本和1个xlsx数据文件构成压缩包仅67KB。其中数据加载、生成器/判别器结构、损失函数、训练与测试等模块均有独立脚本xlsx文件提供原始蔬菜销量数据整体轻量、模块清晰便于在真实数据上快速复现。已有158人学习下载。使用这套代码读者可以了解生成对抗网络处理数值序列的完整流程包括数据清洗与离散化、对抗训练迭代、MSE/MAE等指标对比以及如何将生成结果同传统时间序列方法结合提升预测稳定性与泛化能力。对于希望探索生成式模型在销量预测、异常检测等场景应用的学习者这是一份结构清晰、可直接运行的参考实现适合作为入门练习或项目基线。 先用一句话说明白这个项目做的事是把常见61菜票玩法的号码组合丢给 GAN 网络去学然后让训练好的生成器批量产出和真实号码分布形态相近的新号码。注意是“相近”不是“预测”更不是“中奖密码”——菜票开奖本质是独立随机事件如果谁告诉你 AI 能预测请直接拉黑。但 GAN 在这个任务上确实是个很有意思的练手场景。数据维度不高、训练快、效果可以用肉眼直接判断非常适合刚接触对抗生成网络的人做完整实践也适合老手想换个数据集找找 GAN 训练手感。本文按我实际跑通的流程写包括数据构造、网络设计、训练参数、常见坑代码可以完整复制改改就能用。1. 项目背景与方案选型1.1 为什么想到用 GAN 做号码生成61 型菜票号码本身是一个多维离散分布问题6 个红球范围 1~33不重复1 个蓝球范围 1~16。每个号码都有明确边界球与球之间存在“不重复”这种约束关系历史数据也容易获取。这类任务天然适合测试生成模型的分布学习能力。如果用普通程序生成号码规则写死就行但这没有任何学习过程。GAN 的价值在于它不依赖显式的概率公式而是通过生成器和判别器的对抗博弈自动逼近真实号码的联合分布。换句话说训练完成后生成器学到的不是某个号码出现的概率表而是“一注号码整体应该长什么样”的隐式规律。1.2 GAN、VAE 和纯随机采样如何取舍我在动手之前其实对比过三条技术路线纯随机采样、VAE、GAN。结果如下方案优点缺点适合场景纯随机采样实现简单零训练成本完全不学习数据分布无法体现任何“生成”概念快速生成测试数据VAE有显式隐空间生成可控对离散取值、强边界的数据拟合效果一般容易平均化连续图像、风格编码GAN能拟合隐式分布样本质量高训练不稳定容易模式崩溃需要调参分布学习、样本生成、本项目场景我的结论是菜票号码虽然有规则边界但整体分布并不复杂用 GAN 做既是可行的又是最能体现“对抗式生成”核心思想的一个小型载体。训练速度快也不用准备大显卡CPU 都能跑起来。2. 数据准备与预处理2.1 构造训练数据集为了保证项目可复现我选择直接用规则采样生成 2 万注模拟真实分布的号码。这里的规则就是 61 玩法的硬性规定红球从 1 到 33 中不放回抽取 6 个并升序排列蓝球从 1 到 16 中随机抽取 1 个。import numpy as np def simulate_ticket(num_samples): data [] for _ in range(num_samples): red np.random.choice(np.arange(1, 34), size6, replaceFalse) red.sort() blue np.random.randint(1, 17) data.append(np.concatenate([red, [blue]])) return np.array(data, dtypenp.float32) tickets simulate_ticket(20000) print(tickets.shape) # (20000, 7)如果你手头有真实历史开奖数据也可以替换这一段转成同格式的二维数组即可。不过我的建议是先用模拟数据跑通全流程因为模拟数据分布足够均匀方便后续判断 GAN 是否真的学到了东西。2.2 归一化与 DataLoader 封装GAN 的生成器输出层一般用 tanh 激活函数输出范围是 [-1, 1]。如果直接拿范围在 1~33 的原始数值去训练判别器会简单粗暴地依据数值大小判断真假而不是依据分布形态训练很容易走偏。所以数据必须先归一化。归一化公式很简单把红球从 [1, 33] 映射到 [-1, 1]蓝球从 [1, 16] 映射到 [-1, 1]。def norm_data(tickets): normed tickets.copy().astype(np.float32) normed[:, :6] (normed[:, :6] - 1) / 32 * 2 - 1 normed[:, 6] (normed[:, 6] - 1) / 15 * 2 - 1 return normed def denorm_data(normed): data normed.copy() data[:, :6] (data[:, :6] 1) / 2 * 32 1 data[:, 6] (data[:, 6] 1) / 2 * 15 1 return data norm_tickets norm_data(tickets)然后把归一化后的数组封装成 DataLoader。这里用 PyTorch 的标准流程固定好随机种子保证可复现。import torch from torch.utils.data import Dataset, DataLoader class TicketDataset(Dataset): def __init__(self, data): self.data torch.from_numpy(data) def __len__(self): return len(self.data) def __getitem__(self, idx): return self.data[idx] dataset TicketDataset(norm_tickets) dataloader DataLoader(dataset, batch_size128, shuffleTrue, drop_lastTrue)3. 网络结构与训练配置3.1 生成器结构设计生成器的输入是 100 维高斯噪声向量中间经过两个全连接隐藏层最后输出 7 维向量也就是一注号码的归一化表示。import torch.nn as nn class Generator(nn.Module): def __init__(self, z_dim100, out_dim7): super().__init__() self.net nn.Sequential( nn.Linear(z_dim, 256), nn.BatchNorm1d(256), nn.ReLU(True), nn.Linear(256, 512), nn.BatchNorm1d(512), nn.ReLU(True), nn.Linear(512, out_dim), nn.Tanh() ) def forward(self, z): return self.net(z)有三个细节是我试过之后确认有效的。第一BatchNorm 一定要加否则生成器容易在训练中期输出崩溃所有样本变成同一组数值。第二输出层必须用 Tanh把输出限制在 [-1, 1]这样生成的号码不会出现大规模越界。第三隐藏层宽度我选 256 和 512这个规模对这个任务来说已经足够再大反而容易过拟合。3.2 判别器结构设计判别器的输入是 7 维向量输出一个 logit 值。输出层不用 Sigmoid配合 PyTorch 的 BCEWithLogitsLoss 在数值上更稳定。class Discriminator(nn.Module): def __init__(self, in_dim7): super().__init__() self.net nn.Sequential( nn.Linear(in_dim, 128), nn.LeakyReLU(0.2, inplaceTrue), nn.Linear(128, 64), nn.LeakyReLU(0.2, inplaceTrue), nn.Linear(64, 1) ) def forward(self, x): return self.net(x).squeeze(1)判别器这边我刻意保持轻量级。LeakyReLU 的负数斜率固定设成 0.2这是 DCGAN 系列模型里的常用经验值。它比普通 ReLU 更适合判别器因为即使输入是负值也有梯度不容易让神经元坏死。3.3 损失函数与超参配置损失函数采用二元交叉熵。真实样本的标签用的是 0.9 而不是 1生成样本的标签用的是 0.1 而不是 0这就是标签平滑。目的是防止判别器过于自信让梯度更温和减少模式崩溃的可能。超参数取值说明z_dim100生成器输入噪声维度batch_size128每批样本数lr0.0002生成器和判别器统一学习率betas(0.5, 0.999)Adam 优化器系数偏保守epochs200训练轮数label_smooth_real0.9真实样本标签label_smooth_fake0.1生成样本标签学习率 2e-4 和 Adam 的 betas(0.5, 0.999) 是 GAN 训练里的经典组合尤其 betas 第一项设为 0.5 是为了让梯度更新没那么激进稳定对抗过程。4. 训练核心代码与过程解读4.1 完整训练循环实现生成器和判别器交替更新。这里采用每轮先更新判别器、再更新生成器的顺序二者各更新一次保持相对平衡。criterion nn.BCEWithLogitsLoss() G Generator() D Discriminator() opt_g torch.optim.Adam(G.parameters(), lr0.0002, betas(0.5, 0.999)) opt_d torch.optim.Adam(D.parameters(), lr0.0002, betas(0.5, 0.999)) epochs 200 for epoch in range(epochs): for i, real in enumerate(dataloader): current_batch real.size(0) # 训练判别器 z torch.randn(current_batch, 100) fake G(z) real_label torch.ones(current_batch) * 0.9 fake_label torch.zeros(current_batch) * 0.1 loss_d criterion(D(real), real_label) criterion(D(fake.detach()), fake_label) opt_d.zero_grad() loss_d.backward() opt_d.step() # 训练生成器 z torch.randn(current_batch, 100) fake G(z) loss_g criterion(D(fake), torch.ones(current_batch)) opt_g.zero_grad() loss_g.backward() opt_g.step() if (epoch 1) % 20 0: print(fEpoch {epoch 1}, D loss: {loss_d.item():.4f}, G loss: {loss_g.item():.4f})特别提醒两个容易写错的地方。首先是 fake.detach()训练判别器时不能把梯度传到生成器否则生成器参数会被偷跑更新。其次是生成器训练时要重新采样一遍噪声 z不能用判别器阶段用过的那批否则生成器容易过拟合到当前这批固定输入。4.2 训练过程中的现象观察我在跑的时候前 20 轮判别器损失下降非常快因为它很容易识别出生成器还在乱输出的号码。这时候生成的号码完全是乱的红球经常飞出 1~33 的区间边界。到第 40 到 60 轮生成器的损失开始稳步下降样本逐渐收敛到合法范围内但偶尔还会出现红球重复。第 100 轮之后肉眼观察生成结果已经有了明显改善号码基本都在合法区间红球分布比较均匀蓝球也是。这里要说一句单个 epoch 的 loss 数值不能只看大小因为 GAN 是博弈D loss 和 G loss 即使平稳也有可能只是局部均衡。更可靠的判断方式是每隔一段训练轮数固定一批噪声生成若干注号码直接打印出来看分布是否合理。5. 常见问题与排查技巧5.1 模式崩溃生成器只输出少数固定组合这是 GAN 训练里最让人头疼的问题。现象表现为训练后期生成器只输出几组相似甚至完全相同的号码多样性彻底丧失。我踩坑后的解决办法是优先做三件事。一是降低判别器的学习率比如从 2e-4 降到 1e-4让判别器不要强得过分二是把真实标签从 1 改成 0.9让判别器不要对“哪些是真样本”过于自信三是生成器每轮可以多更新一到两次提前一点“追赶”判别器。5.2 生成的号码越界或红球重复连续值输出的生成器天然无法保证号码完全满足 61 的玩法规则。在我的项目里训练到后期越界问题基本消失因为 tanh 限制住了输出范围。但取整之后仍可能出现临界情况比如红球归一化值为接近 1 时反归一化后可能得到 33 或 34。我的处理方案是反归一化之后用 np.clip 把号码强行拉回合法范围然后用一个简单去重函数修正重复红球。def fix_reds(red_input): reds np.round(red_input).astype(int) reds np.clip(reds, 1, 33) uniq np.unique(reds) if len(uniq) 6: return np.sort(reds) pool list(set(np.arange(1, 34)) - set(reds)) for i in range(6): if reds[i] in reds[:i]: reds[i] int(pool.pop()) return np.sort(reds)需要说明的是这种后处理会让生成样本与原始连续值输出存在细微偏差。如果只是做技术研究建议同时保留修正前后的数据观察后处理对分布的影响。5.3 训练不收敛、loss 剧烈震荡出现这种情况我一般按这个顺序排查可能原因判断方法解决办法学习率过高D loss 和 G loss 剧烈跳变学习率降到 1e-4 或 5e-5批次太小每个 batch 的梯度方差大batch_size 提到 256随机种子未固定每次结果相差巨大全局固定随机种子判别器太强G loss 一直涨、D loss 接近 0降低 D 学习率或增加 G 更新次数我最常犯的错误是一上来就套用大模型的训练参数。这个任务只有 7 维输出模型非常小通常不需要太强的判别器。把判别器降为 64 维隐藏层也能收敛得更平滑。6. 效果评估与后续扩展6.1 从分布角度看生成效果训练结束后可以批量生成 1 万注号码统计红球和蓝球的出现频率。理想情况下每种红球的出现频率应该接近 10000 * 6 / 33 约等于 1818 次蓝球应该是 10000 / 16 约等于 625 次。如果频率集中在某几个号码上说明模式崩溃还没解决。def evaluate_generator(G, n10000, z_dim100): G.eval() red_hist np.zeros(34) blue_hist np.zeros(17) with torch.no_grad(): z torch.randn(n, z_dim) gen_norm G(z).numpy() gen_val denorm_data(gen_norm) for row in gen_val: reds np.round(row[:6]).astype(int) reds np.clip(reds, 1, 33) for r in reds: red_hist[r] 1 b int(np.clip(round(row[6]), 1, 16)) blue_hist[b] 1 return red_hist[1:], blue_hist[1:]我实测下来训练充分后红球频率基本能稳定在 1700 到 1900 之间蓝球频率在 580 到 670 之间。这个结果说明模型的分布学习是有效的。6.2 可以继续做的优化方向这个项目做完后我顺手列了几个值得深入的方向。第一把每个球的输出从连续值改成离散分类。也就是红球单独过一个 33 类的 Softmax蓝球单独过一个 16 类的 Softmax配合 Gumbel-Softmax 做可微采样这样生成结果天然满足范围和重复约束不再需要后处理。第二把标准 GAN 换成 WGAN-GP。WGAN-GP 用 Wasserstein 距离代替 Jensen-Shannon 散度训练稳定性显著提升基本告别模式崩溃问题。代价是代码和梯度惩罚的计算复杂一些。第三把号码看成序列用 LSTM 或 Transformer 做序列 GAN。61 号码本质上是一个长度为 7 的离散序列红球和蓝球有先后顺序关系。序列模型能更自然地建模这种结构。我在实操中最大的感受是GAN 的训练问题不会因为数据维度低而消失反而更容易暴露。一个 7 维小数据项目如果不做标签平滑、不设固定随机种子、不观察生成样本照样会陷入模式崩溃。把这种小项目调稳了比直接跑一个大模型更有收获。至少下次再碰到高维数据你就知道该从哪几个方向下手了。本文还有配套的精品资源点击获取