ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LSTM双色球预测:从数据清洗到模型部署的深度学习实战

LSTM双色球预测:从数据清洗到模型部署的深度学习实战 简介这是一份基于深度学习的LSTM算法双色球预测实战源码包面向计算机相关专业做大作业、毕业设计的学生以及需要项目实战练习的Python开发者。项目以LSTM时序建模为核心包含数据读取、数据集封装、模型定义、训练、预测与测试等完整流程难度适中适合作为毕业设计或课程设计的参考实现。压缩包共31个文件约29KB以py源码与pyc缓存文件为主另有yaml配置、xml工程配置、gitignore及红蓝球历史数据txt等结构与运行入口清晰训练与调用脚本层次分明下载后可结合configs.yaml快速复现。已有93人学习代码经过严格调试并获导师认可评审得分98分属于可直接运行的高分设计项目能够帮助读者快速理解LSTM在序列预测中的应用思路并在此基础上扩展自己的实验方案。1. 双色球预测为什么是个伪命题却值得你动手实现一遍拿 LSTM 算法去做双色球预测本质上是在“用一个擅长捕捉规律的模型去拟合一个尽量没有规律的随机抽样过程”。可这类项目这几年依然被反复提出——因为抛开中奖不谈它把数据清洗、序列建模、训练调参、结果评估完整串成了一趟能跑通的深度学习实战。这个标题里真正能落地的价值是让你把一个基于深度学习的 LSTM 双色球预测实战项目完整跑下来理解序列模型从数据到训练再到部署的每个动作它不是“发家致富指南”而是一趟动手深度学习的全流程练习。适合刚学完 RNN 和 LSTM、想找一个端到端小项目的人也适合想验证“模型是否真的学到了规律”的从业者。看完后你会获得一套可复现的 PyTorch 代码以及一套不会自欺欺人的验证方法。2. 先搭数据管线从开奖历史CSV到LSTM输入样本2.1 加载并清洗开奖数据最重要的不是模型而是日期排序双色球的历史开奖数据常见的发布格式是 CSV至少包含期号、开奖日期、6 个红球和 1 个蓝球。很多公开文件里还会带上销售额、奖池金额、一等奖注数等字段这些和号码预测无关直接丢掉。我一般只保留期号和 7 个号码列然后把每列强制转成整数再按期号排序。这里最容易翻车的是排序有些历史 CSV 按年份倒序排列有些按开奖日期乱序排列如果之前不做排序后面的滑动窗口会把时间顺序完全打乱模型看到的是一堆“未来混着过去”的序列。import pandas as pd def load_ssq(path): df pd.read_csv(path) # 常见历史表里至少有 7 个开奖号列名随站点不同会有变化按实际字段改 red_cols [fred{i} for i in range(1, 7)] blue_col blue df df[[lottery_id] red_cols [blue_col]].copy() df df.dropna() for c in red_cols [blue_col]: df[c] df[c].astype(int) df df.sort_values(lottery_id).reset_index(dropTrue) # 必须按时间升序 return df这段代码的关键在最后一行按期号升序排列。LSTM 的序列样本是按照时间窗口切出来的如果原始数据不是有序的窗口内就混入了时间上不相邻的号码模型学到的“时序依赖”全是假的。另外 dropna 要去掉缺失行真实历史表偶尔会出现空行或部分字段残缺不清理会在后续 astype(int) 时直接报错。如果你拿到的 CSV 列名不同只要保证 7 个号码列都进了 red_cols 和 blue_col 即可。2.2 用滑动窗口生成训练样本禁止用随机切分把整段历史号码拆成训练集时不能用 sklearn 的 train_test_split 随机切分。时间序列的随机切分会让模型在训练阶段见到未来数据验证集和测试集失去意义。正确做法是先按时间比例切三段再对每一段独立做滑动窗口。滑动窗口的含义是用最近 seq_len 期的 7 个号码作为特征预测下一期的 7 个号码。import numpy as np def make_windows(arr, seq_len20): # arr: shape (N, 7)7 列分别是 red1..red6, blue X, y [], [] for i in range(seq_len, len(arr)): X.append(arr[i - seq_len:i]) y.append(arr[i]) return np.stack(X), np.stack(y)窗口长度 seq_len 我一般取 20。原因后面第 4 章会细说这里只要知道窗口太短会丢失近期分布信息太长又会把老旧的号码分布也塞进来而且会让样本数量显著变少。数据量大约只有两千多期窗口取 50 时样本量会掉到两千以下过拟合风险更大。再强调一次切分要在原始序列上做先把原始 DataFrame 按 8:1:1 分成训练、验证、测试三段再分别调用 make_windows这样任何窗口都不会横跨两个集合测试集里的每一期都是模型在训练时看不到的时间段。2.3 归一化与标签编码为什么我坚持用分类头而不是回归双色球号码是离散的类别不是连续值。很多老代码把号码当成回归目标直接输出一个浮点数然后 round 成整数结果经常出现 0、 34、负数这类不合法号码。我一开始也这么写过后来彻底改成分类建模。具体做法是红球 1~33 映射成类别 0~32蓝球 1~16 映射成类别 0~15LSTM 的输出头用线性层产生类别 logits训练用交叉熵损失。这样输出的结果天然是合法号码范围只有“红球重复”这一个问题留到后续解码阶段处理。特征侧同样要做归一化把每个红球列除以 33蓝球列除以 16。注意这里不是用统计出的最大值而是用规则固定的取值范围所以即使先在整表上做归一化再切分也不存在信息泄漏。def normalize_features(arr): # arr: (N, 7) 原始号码 arr arr.astype(np.float32).copy() norm_vec np.array([33, 33, 33, 33, 33, 33, 16], dtypenp.float32) return arr / norm_vec这样处理之后特征全部落在 0 到 1 之间LSTM 的 tanh 激活能正常工作训练也更稳定。有些复现代码会再把特征减均值除标准差但在这种数据量很小的任务上没必要固定比值缩放已经够用。标签不参与归一化直接以整数类别形式进入交叉熵损失。这里有一个工程妥协双色球的红球本质上是“无序集合”但数据公布时按升序排列我把它当成了 6 个位置序列来建模。严格讲这不严谨却几乎所有公开复现都这么做我也沿用这种妥协因为目的是练习序列建模而不是证明这个预测真的可行。3. 用PyTorch实现LSTM双色球预测模型可从零复现的完整代码3.1 方案选型为什么序列基线模型中LSTM仍然最合适对比 RNN 和 CNN 时LSTM 的门控机制能缓解长序列里的梯度消失适合捕捉窗口内号码的“局部节奏”。CNN 更擅长局部模式但很难把期号之间的先后顺序编码进来Transformer 在这个数据规模下几乎必过拟合——两千个样本喂给自注意力模型很容易把训练期号码背下来。所以在“这个标题到底选什么模型”这个问题上我的答案是 LSTM而且要单向 LSTM。双向 LSTM 会同时看到窗口内“过去”和“未来”的隐藏状态在真正的时间序列预测里反而制造出虚假的未来信息效果并不比单向好。模型结构上我用两层 LSTM 加一个全连接头。层数再往上加参数量变大过拟合更快。隐藏单元 64 就够因为输入特征只有 7 维输出类别也只是 33 和 16容量过大的模型只会更快地“背题”。3.2 模型定义输入维度、隐藏层和双输出头输入维度是 7对应 6 个红球加 1 个蓝球。LSTM 的输出取最后一个时间步的隐藏状态作为整段窗口的压缩表示然后接入两个独立的输出头一个输出 6 个红球各自的 33 类 logits一个输出蓝球的 16 类 logits。import torch from torch import nn class LSTMPredictor(nn.Module): def __init__(self, input_dim7, hidden_dim64, num_layers2, n_red6, red_range33, blue_range16, dropout0.2): super().__init__() self.lstm nn.LSTM( input_dim, hidden_dim, num_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0 ) self.fc nn.Linear(hidden_dim, 64) self.red_head nn.Linear(64, n_red * red_range) self.blue_head nn.Linear(64, blue_range) self.relu nn.ReLU() def forward(self, x): # x: (batch, seq_len, 7) out, _ self.lstm(x) h out[:, -1, :] h self.relu(self.fc(h)) red_logits self.red_head(h).reshape(-1, 6, 33) blue_logits self.blue_head(h) return red_logits, blue_logits两个输出头的设计是这套模型的核心。红球头输出形状 (batch, 6, 33)第 0 维的 6 是红球位置第 1 维是 33 个候选号码的 logits。蓝球头只输出 (batch, 16)。把 6 个红球位置放在一个头里比拆成 6 个独立模型更省参数也保留了不同位置之间通过共享 LSTM 特征产生的相关性。dropout 只在层数大于 1 时生效这是 PyTorch 的实现限制注意默认启动条件。3.3 数据集封装与训练循环保存最佳训练权重而不是最后一轮训练循环里要做几件事封装 Dataset、用交叉熵计算红球和蓝球损失、验证集评估、按验证集表现保存最优权重。不要把训练结束时的权重作为最终结果——LSTM 在最后几个 epoch 通常已经过拟合验证集上的最佳权重才是模型的真正“后悔药”。import torch from torch.utils.data import Dataset, DataLoader class SSQDataset(Dataset): def __init__(self, X, y): self.X torch.FloatTensor(X) # y 前 6 列是红球类别最后一列是蓝球类别 self.red_y torch.LongTensor(y[:, :6]) self.blue_y torch.LongTensor(y[:, 6]) def __len__(self): return len(self.X) def __getitem__(self, idx): return self.X[idx], self.red_y[idx], self.blue_y[idx]损失计算时红球损失是 6 个位置的交叉熵之和蓝球损失单独计算。蓝球权重我一般取 0.2 到 0.5不让它主导整体损失。import torch.optim as optim SEQ_LEN 20 BATCH_SIZE 32 EPOCHS 40 LR 5e-4 model LSTMPredictor() optimizer optim.Adam(model.parameters(), lrLR, weight_decay1e-5) def evaluate(model, loader): model.eval() total_loss 0.0 red_criterion nn.CrossEntropyLoss() blue_criterion nn.CrossEntropyLoss() with torch.no_grad(): for xb, red_b, blue_b in loader: red_logits, blue_logits model(xb) loss red_criterion(red_logits.reshape(-1, 33), red_b.reshape(-1)) loss loss 0.25 * blue_criterion(blue_logits, blue_b) total_loss loss.item() return total_loss / max(len(loader), 1) train_loader DataLoader(SSQDataset(X_train, y_train), batch_sizeBATCH_SIZE, shuffleTrue) val_loader DataLoader(SSQDataset(X_val, y_val), batch_sizeBATCH_SIZE, shuffleFalse) best_val_loss float(inf) patience 10 wait 0 for epoch in range(EPOCHS): model.train() train_loss 0.0 for xb, red_b, blue_b in train_loader: optimizer.zero_grad() red_logits, blue_logits model(xb) loss nn.CrossEntropyLoss()(red_logits.reshape(-1, 33), red_b.reshape(-1)) loss loss 0.25 * nn.CrossEntropyLoss()(blue_logits, blue_b) loss.backward() optimizer.step() train_loss loss.item() val_loss evaluate(model, val_loader) print(fepoch {epoch1}: train {train_loss:.3f}, val {val_loss:.3f}) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model.pt) wait 0 else: wait 1 if wait patience: break代码里的 patience 是早停轮数。验证集 loss 连续 10 个 epoch 不下降就停止训练避免继续吃训练集里的噪声。训练时每轮都要把验证集跑完再决定是否保存当前权重这份代码保存的是验证 loss 最低时刻的权重而不是最后一次 epoch 的权重。训练样本量只有两千出头shuffleTrue 会让每个 batch 在每轮看到不同的窗口组合打乱顺序有助于优化器跳出局部极小这和时间序列“预测时不能 shuffle”是两回事。3.4 推理阶段给出合法号码红球解码去重模型输出的红球 logits 是 6 个位置的类别分布直接各自取 argmax 会出现同一个号码出现在两个位置。开奖规则不允许红球重复所以推理时要加一个掩码循环每次选出一个号码下一次把这个号码对应 logits 置为负无穷强制不重复。def decode_red(red_logits): # red_logits: (batch, 6, 33) red_preds [] with torch.no_grad(): for i in range(6): logits red_logits[:, i, :].clone() for chosen in red_preds: logits[:, chosen] -1e9 idx torch.argmax(logits, dim1) red_preds.append(idx) return torch.stack(red_preds, dim1)这段逻辑的关键是 mask 集合。第 i 个位置原本有自己的概率分布但那些已经选过的号码即使概率再高也要被强制压掉。实际使用中六个位置输出的 logits 可能都在偏好同一个号码mask 之后模型必须“退而求其次”。这种推理策略和自然语言生成里的重复惩罚类似双色球场景下就是硬约束。蓝球则简单得多直接对 16 类 logits 取 argmax 即可没有重复问题。4. 训练参数如何调序列长度、学习率和损失权重那点事4.1 关键超参数速查表与经验取值LSTM 调参里有一部分是“手感”和“玄学”但核心参数有清晰的边界。下面这张表是我在这个数据规模上反复跑出来的取值可以直接抄作业。参数常见范围推荐值说明seq_len10~5020窗口太长会吞掉样本量2000 期历史只够 20 窗口hidden_dim32~12864输入只有 7 维容量再大就开始背号码num_layers1~322 层有足够非线性3 层在小数据上过拟合明显dropout0.1~0.50.2只在 num_layers1 时生效batch_size16~6432太小梯度抖动大太大每个 epoch 迭代次数少lr1e-4~1e-35e-4大于 1e-3 时 loss 会瞬间崩成 NaN 或极度过拟合weight_decay0~1e-41e-5轻微 L2 约束能压住一部分过拟合early_stop patience5~1510验证 loss 连续 10 轮不降就停这些参数不是从一个“最佳配置”反推出来的而是多个随机种子下验证 loss 的平均表现。如果你换了数据源或者加入了额外特征建议只在推荐值附近小幅度移动不要一次性全改否则出了问题很难定位是哪个参数引发的。4.2 损失函数权重、优化器和学习率选择的工程逻辑红球和蓝球的类别空间大小不同6 个红球位置每一个都有 33 类蓝球只有 16 类。如果两个 loss 等权相加模型会更倾向于降低红球损失因为红球部分贡献的梯度更大、更容易优化蓝球预测得不到足够注意力。所以我在总损失里给蓝球乘了 0.25。这个权重不是固定的可以做一次小网格搜索0.1、0.2、0.5、1.0 四组各跑 5 个种子看验证集蓝球命中率和整体 loss 的折中。实际项目里我没有为它花太多时间0.2 到 0.5 之间就可以。优化器用 Adam 而不是 SGD。原因很简单这个任务的数据量太小SGD 对学习率太敏感调整成本高Adam 自带自适应步长在 5e-4 和 1e-4 之间都基本能收敛。weight_decay 我习惯开 1e-5不是为了特征选择而是防止 LSTM 的权重矩阵增长过快。学习率大于 1e-3 时训练 loss 可以掉到 0.01但验证 loss 会卡在一个高位不动这是典型的“一步跨进了过拟合陷阱”从 1e-4 起步虽然收敛慢但验证曲线更平稳。我的建议是先用 5e-4 跑 10 个 epoch 看趋势再把学习率降到 1e-4 继续微调。4.3 早停与验证监视捕捉过拟合的唯一稳定手段LSTM 在 2000 个样本上几乎必然过拟合。训练 loss 每轮都在下降甚至降到接近 0但验证 loss 在某个 epoch 后开始回升。这说明模型开始“背答案”把最近几期的号码记下来了。早停只是最后一道防线更应该做的是每个 epoch 结束都打印两边的 loss观察过拟合从哪里开始。我一般用两个信号判断是否过拟合训练 loss 与验证 loss 的 gap 是否持续拉大验证 loss 是否连续 5 轮没有创新低。满足任一条就把 patience 减小原计划 40 个 epoch 可能 15 轮就停了。这里有一个小技巧早停之后不要直接使用最后一轮的权重而是把早停过程中保存的 best_model.pt 重新加载继续做推理。虽然训练时有多余的 epoch但保存下来的权重是在验证集上峰值表现的版本这个“后悔药”至少不会让你交出一个已经恶化的模型。5. LSTM双色球预测避坑指南5次翻车的现象和排查办法5.1 预测出负数或大于33的号码分类任务不要反归一化现象模型输出的号码预测值出现 0、34、负数等不合法结果测试集上的“准确率”低到离谱。原因把号码当成回归问题输出层是单个神经元损失用 MSE最后又把模型输出做了反归一化。双色球号码是离散类别回归头根本不知道号码的取值范围是 1 到 33它只会在训练集分布附近乱抖一旦某个特征超出一丁点反归一化就越界。解决改成分类头红球输出 33 类 logits蓝球输出 16 类 logits用交叉熵训练推理时 argmax 得到类别索引再加 1 就是原始号码。如果坚持回归路线必须在输出层后面接 sigmoid 并缩放到 1 到 33但这样做的精度上限远低于分类头我不推荐。5.2 同一个红球出现两次增加推理掩码现象预测的 6 个红球里有重复号码比如“03、03、12、17、24、29”。原因6 个红球位置各自独立做 argmax没有做放回抽样下的不重复约束。位置 1 和位置 2 可能都认为 03 概率最高于是各自输出 03。解决使用上一章给出的 decode_red 函数在推理时把已选号码的 logits 置为 -1e9强制不重复。注意训练时不能做这个掩码因为训练用的是真实标签真实开奖结果里红球本身就是去重的只有推理阶段需要这个后处理。做了掩码之后某些样本的红球命中数可能不升反降这是正常现象因为六个位置共享同一个隐向量模型原本很可能就是在“赌”一个最热号码。5.3 训练loss很低但验证集一塌糊涂网络在背号码现象训练 loss 降到 0.05 以下验证 loss 却还在 3.0 以上预测结果偶尔完全复刻最近几期开奖号。原因模型容量相对数据量过大加上 LSTM 有很强的短程记忆能力它不需要学习任何泛化规律只要记住“最近 20 期之后是什么号码”就能把训练集背下来。验证集是时间上完全没见过的期次当然预测不准。解决先检查训练曲线如果训练 loss 下降速度过快、验证 loss 同步反弹立刻降容量hidden_dim 从 64 降到 32dropout 从 0.2 升到 0.4并加大早停 patience 到 15。同时用样本外命中数而不是训练 loss 来评判模型。这样至少能从“背题”退化成“学了一个平稳分布”。5.4 换随机种子结果完全不一样用多次实验取中位现象同一套代码只改随机种子红球平均命中数有时在 0.9、有时在 1.4差异比模型之间的差异还大。原因数据量太小模型对参数初始化和数据打乱顺序极其敏感。单次实验的指标没有任何统计意义只能说明“这一次训练恰好学到什么”。解决固定数据切分跑 5~10 个随机种子取平均命中数和标准差再去和随机基线对比。如果平均命中数只比随机基线高 0.1、标准差却有 0.4这个结果就约等于噪声。我在实际验证中会用 numpy 设十个固定 seed把每个 seed 的测试集预测结果存下来最后汇总分布。5.5 样本乱序导致窗口失效统一按期号排序现象训练 loss 波动很大验证集指标忽高忽低甚至模型能“预测”出历史早期数据。原因公开的 CSV 文件并不都是按期号升序排列。有些站点按年份倒序有些按开奖日期乱序一旦乱了长度为 20 的窗口里就会出现“未来期号”在“过去期号”前面模型学到的根本不是时间依赖。解决load_ssq 里保存前必须 sort_values(lottery_id)。排序之后再检查一次打印前 5 行期号是不是递增再打印窗口前 3 条样本的期号跨度。这一步花不了 1 分钟但能避免后面几小时的训练全部作废。顺便确认期号列没有重复值重复期号往往意味着同一期数据出现两次直接 drop_duplicates 处理。6. 如何验证模型确实没用再把同一套代码迁移到电力和销量预测6.1 把随机基线放在发布之前用命中数分布判断模型价值训练完模型第一件事不是看验证 loss而是和随机基线对比。随机基线的逻辑很简单从 33 个红球里不放回抽 6 个再从 16 个蓝球里抽 1 个各自的期望命中数非常稳定。如果 LSTM 的平均红球命中数没有显著高于随机基线那就可以直接下结论模型没有学到任何可泛化的规律。def random_baseline(n_periods, true_reds, true_blues, seed0): rng np.random.default_rng(seed) red_hits, blue_hits [], [] for i in range(n_periods): pred_red set(rng.choice(33, 6, replaceFalse) 1) pred_blue int(rng.choice(16, 1)) 1 red_hits.append(len(pred_red set(true_reds[i]))) blue_hits.append(int(pred_blue true_blues[i])) return np.mean(red_hits), np.mean(blue_hits)运行 1000 次随机模拟得到红球命中数的均值分布再把 LSTM 在测试集上的平均命中数放到这个分布里看分位。落在 30% 以下说明模型可能学出了反向信号落在 50% 附近说明不优于随机落在 80% 以上才值得进一步分析。以双色球为例纯随机抽 6 个红球命中数的期望是 36/33≈1.09如果你训练的模型均值也在 1.1 附近就不用再折腾超参数了。6.2 把输入特征和输出头换掉这套脚手架可以服务周期型序列这套代码的真正价值不在双色球而在它是一套可迁移的时间序列预测脚手架。把输入特征从“7 个号码”换成“24 小时的电力负荷、过去 30 天的商品销量”把输出头改成需要的预测维度训练循环完全不用动。唯一的前提是数据里有真实的自相关或周期性LSTM 才能兑现它的建模能力。我会先跑一遍随机基线再用同样的评估逻辑对比 LSTM这是我在这个项目里收获最大的习惯。雪花一样的训练曲线会骗人损失会骗人只有与随机分布的对比不会。如果哪天你接手新的序列预测任务记住先问一句“如果我不做任何学习直接猜能拿到多少分”希望这篇笔记能帮你少走这一段弯路。本文还有配套的精品资源点击获取
返回列表