首先郑重声明,这个赚不了钱!赚不了钱!赚不了钱!重要的话说三遍!
纯粹出于兴趣和技术做了个小实验,指望这个赚钱不太可能鸭!emmm,但可能会让你赔钱赔的少一点?
转载请注明出处:https://blog.csdn.net/aaronjny/article/details/103276212
以前从没买过彩票,前几天一时兴起,随机买了几注,然后兴致勃勃地等开奖。中奖序列出来后,比对一下,握草?!
果然没中~
然后我就在考虑机器选彩票的技术可行性。
首先,大乐透的中奖序列为35选5+12选2,每个球的选取是随机的,因此,想使用机器学习精准地预测出获奖序列是很难(或者说基本不可能)的。为什么这么说呢?我们可以类比于机器学习选股票。目前有很多机器学习应用在股票选择上的例子,并能够实现盈利。但机器学习选股和选彩票有几点显著差异:
所谓的概率分布指的是,假设彩票的中奖序列是完全随机产生的话,序列中每一个球在一次开奖过程中出现的概率应该是相同的(前区和后区要分开算),并且从时间序列上来看,连续的很多次开奖中,每一个球的出现与否也应当满足某种规律(当然,这是宏观上讲,实际上肯定不会严格满足,但能够体现某种趋势或倾向)。
而我们又不打算靠这个中大奖,目标不是预测出头等奖的开奖序列,而是尽可能多的预测出可能会出现在中奖序列中的球。听上去好像没什么区别,但在模型设计上能够体现出差异——如果是前者,我们的模型应该是一个分类器,从 C 35 5 ∗ C 12 2 C_{35}^5*C_{12}^2 C355∗C122种类别中预测出其中一个,但考虑数据集规模和概率问题,根本不现实;而后者,我们则设计一个多输入、多输出的模型,输入是七个球的时间序列,输出是每个位置出现某个球的概率,这样就靠谱的多~emmm,我们不求赚钱,少输点就行~!
行嘛,想了想好像没什么问题,那就开搞?于是,就有了这篇文章。
想训练个模型的话,第一步肯定是获取数据啦。
我在网上找了一下,很快从[江苏体彩网](https://www.js-lottery.com/Pla 
yZone/lottoData.html)找到了历史开奖记录,可以直接下载,文件格式为csv。
这就很舒服啦,不用写爬虫到处一点点爬了。把文件下载下来看一下,数据是以这种形式组织的:
很明显,最前面是期号,然后是前区的五个号码,最后是后区的两个号码。
ok,下面让我们来写一个下载数据集的脚本,以便于我们对数据进行更新:
# -*- coding: utf-8 -*-
# @File : update_data.py
# @Author: AaronJny
# @Date : 2019/10/29
# @Desc :
import requests
import settings
print('开始尝试从 {} 获取最新的大乐透数据...'.format(settings.LOTTO_DOWNLOAD_URL))
try:
resp = requests.get(settings.LOTTO_DOWNLOAD_URL)
if resp.status_code == 200:
# 解析数据,查看数据集中最新的数据期数
lines = resp.content.decode('utf-8').split('\n')
index = lines[0].replace('"', '').split(',')[0]
print('获取成功!开始更新文件...')
with open(settings.DATASET_PATH, 'wb') as f:
f.write(resp.content)
print('完成!当前最新期数为{}期,请确认期数是否正确!'.format(index))
else:
raise Exception('获取数据失败!')
except Exception as e:
print(e)
我把一些常量提取出来了,放到了settings.py
里,需要对照的话请看:
# -*- coding: utf-8 -*-
# @File : settings.py
# @Author: AaronJny
# @Date : 2019/10/29
# @Desc :
# 训练epochs数量
EPOCHS = 60
# 训练批大小
BATCH_SIZE = 128
# 输入的连续时间序列长度
MAX_STEPS = 256
# 前区号码种类数
FRONT_VOCAB_SIZE = 35
# 后区号码种类数
BACK_VOCAB_SIZE = 12
# dropout随机失活比例
DROPOUT_RATE = 0.5
# 长短期记忆网络单元数
LSTM_UNITS = 64
# 前区需要选择的号码数量
FRONT_SIZE = 5
# 后区需要选择的号码数量
BACK_SIZE = 2
# 保存训练好的参数的路径
CHECKPOINTS_PATH = 'checkpoints'
# 预测下期号码时使用的训练好的模型参数的路径,默认使用完整数据集训练出的模型
PREDICT_MODEL_PATH = '{}/model_checkpoint_x'.format(CHECKPOINTS_PATH)
# 预测的时候,预测几注彩票,默认5注
PREDICT_NUM = 5
# 数据集路径
DATASET_PATH = 'lotto.csv'
# 数据集下载地址
LOTTO_DOWNLOAD_URL = 'https://www.js-lottery.com/PlayZone/downLottoData.html'
# 大乐透中奖及奖金规则(没有考虑浮动情况,税前)
AWARD_RULES = {
(5, 2): 10000000,
(5, 1): 800691,
(5, 0): 10000,
(4, 2): 3000,
(4, 1): 300,
(3, 2): 200,
(4, 0): 100,
(3, 1): 15,
(2, 2): 15,
(3, 0): 5,
(2, 1): 5,
(1, 2): 5,
(0, 2): 5
}
我们尝试运行一下,看看效果:
很好,没有问题。
虽然数据已经获取到了,但显然,这个数据无法直接应用于训练。我们需要对数据做一下简单的处理。
现在,我们为中奖序列中的数字(或者说球)编一下号,从前往后它们的编号分别为1到7,其中1-5是前区的5个球,6-7是后区的2个球。
好的,我们现在先考虑另外一个问题,假如我们有近一年以来的气温数据,需要预测明天的气温,应该怎么做?
你可能会脱口而出,用循环神经网络做序列的预测。假设按顺序给这近一年来的气温分别编号为1-365,其中t1表示第一天的气温,t365表示今天的气温。气温的变化应该是有规律的(一般情况下),我们想让机器来学习这种规律。我们选定一个合适的时间长度,比如30天,然后将这30天的连续数据作为输入(x),将接下来一天的气温数据作为输出(y),就构成了一条数据。然后使用长度为31天的扫描框,对一年的数据进行一次遍历,我们就得到了一组数据集。用它进行训练,完成后,输入30天前到今天的气温序列,即可预测明天的气温。
回到这个问题上来,其实和预测气温差不多,我们使用连续若干期的球1的数据来预测下期球1的分布概率,球2-球7都是同样的方法。单从输入输出看来就是这样,实际上实现起来肯定会有更多的处理和优化,在这里不讨论,说到模型的时候再细说。
和预测气温的例子不同,气温预测时只有一种因子参与,就是当天的气温值。而在这个例子里,输入的是7个球,输出的也是7个概率分布,所以这是个多输入、多输出的模型。我准备使用keras来实现模型,按照keras的接口,我需要把输入数据处理成这个格式:
x={'x1': [球1序列1, 球1序列2, ... , 球1序列n], 'x2': [球2序列1, 球2序列2, ... , 球2序列n], ... , 'x7': [球7序列1, 球7序列2, ... , 球7序列n]}
相应的,输出数据整理成这个格式:
y={'y1': [序列1的下一期球1值, 序列2的下一期球1值, ... 序列n的下一期球1值], 'y2': [序列1的下一期球2值, 序列2的下一期球2值, ... 序列n的下一期球2值], ... ,'y7': [序列1的下一期球7值, 序列2的下一期球7值, ... 序列n的下一期球7值], }
开搞!
# -*- coding: utf-8 -*-
# @File : dataset.py
# @Author: AaronJny
# @Date : 2019/10/29
# @Desc : 对数据集进行相关处理
import time
import numpy as np
import settings
class LottoDataSet:
def __init__(self, path=settings.DATASET_PATH, train_data_rate=0.9, shuffle=True):
# 数据集路径
self.path = path
# 训练集占整体数据集的比例
self.train_data_rate = train_data_rate
# 是否打乱数据集顺序
self.shuffle = shuffle
# 训练集
self.train_np_x = {}
self.train_np_y = {}
# 测试集
self.test_np_x = {}
self.test_np_y = {}
# 加载并处理数据集
self.clean_data()
def load_data_from_path(self, path=None):
"""
从给定路径加载数据集
:param path: 数据集路径
:return: list,若干组开奖记录
"""
# 如果没有指定路径,就是用初始化实例时传递的path
if not path:
path = self.path
# 读取数据行
with open(path) as f:
# 因为csv里面最新的数据放在了最前面,所以我们需要颠倒一下
lines = f.readlines()[::-1]
# 排除空行
lines = [line.strip() for line in lines if line.strip()]
return lines
def clean_data(self):
"""
对数据进行清洗
:return:
"""
# 先从硬盘读取文件
lines = self.load_data_from_path()
# 去除引号,并使用逗号分割,将数据转成数组
x_nums = []
for line in lines:
# 下标0的位置是期号,直接去掉
nums = line.replace('"', '').split(',')[1:]
# 所有球的编号都减一,把1-35变成0-34,1-12变成0-11
# 这样便于后面做softmax
x_nums.append([int(x) - 1 for x in nums])
# 接着,把中奖序列中的七个数字拆开,按位置和时间纵轴组合,变成7组数据
num_seqs = {}
# 对于每一期的中奖序列
for line in x_nums:
# 对于一条中奖序列中的每一个数
for index, num in enumerate(line):
# 最后的数据格式{0: [1,2,3,4,...],1: [1,2,3,4,...],...,6: [1,2,3,4,...]}
num_seqs.setdefault(index, []).append(num)
# 根据时间序列,拆出来x和y数据集,每MAX_STEPS长度的连续序列构成一条数据的x,max_steps+1构成y
# 举例,假设MAX_STEPS=3,有序列[1,2,3,4,5,6],则[1,2,3->4],[2,3,4->5],[3,4,5->6]是组成的数据集
x = {}
y = {}
for index, seqs in num_seqs.items():
x[index] = []
y[index] = []
total = len(seqs)
# 序列的长度要求为MAX_STEPS,所以从MAX_STEPS处开始,而不是下标0处
for i in range(settings.MAX_STEPS, total, 1):
# 存放本条x序列的,存放的是数字形式
tmp_x = []
# 存放本条y值的,存放的one-hot形式,虽然y只是一个数,但one-hot形式也为list
if index < settings.FRONT_SIZE:
# 根据index判断当前号码属于前区还是后区,使用相关的号码种类数量来初始化one-hot向量
# 因为前区是35选5,后区是12选2,one-hot向量大小不同,所以要区别对待
tmp_y = [0 for _ in range(settings.FRONT_VOCAB_SIZE)]
else:
tmp_y = [0 for _ in range(settings.BACK_VOCAB_SIZE)]
# 将从i-MAX_STEPS到i(不包括i)的这一段长为MAX_STEPS的序列,逐个加入到tmp_x中
for j in range(i - settings.MAX_STEPS, i, 1):
tmp_x.append(seqs[j])
# 将这条记录添加到x数据集中
x[index].append(tmp_x)
# 修改y值的one-hot,并将标签加入到y数据集中
tmp_y[seqs[i]] = 1
y[index].append(tmp_y)
# y在前面已经是one-hot形式了,我们现在需要把x里面的数字也转成one-hot形式,并转成numpy的array类型
np_x = {}
np_y = {}
# 对应7个球构成的七组序列中的每一组
for i in range(settings.FRONT_SIZE + settings.BACK_SIZE):
# 获取样本数量
x_len = len(x[i])
# 根据球所处的前后区,分别进行初始化
if i < settings.FRONT_SIZE:
tmp_x = np.zeros((x_len, settings.MAX_STEPS, settings.FRONT_VOCAB_SIZE))
tmp_y = np.zeros((x_len, settings.FRONT_VOCAB_SIZE))
else:
tmp_x = np.zeros((x_len, settings.MAX_STEPS, settings.BACK_VOCAB_SIZE))
tmp_y = np.zeros((x_len, settings.BACK_VOCAB_SIZE))
# 分别利用x,y中的数据修改tmp_x和tmp_y
for j in range(x_len):
for k, num in enumerate(x[i][j]):
tmp_x[j][k][num] = 1
for k, num in enumerate(y[i][j]):
tmp_y[j][k] = num
# 然后将tmp_x和tmp_y按照球所处的位置,加入到np_x和np_y中
np_x['x{}'.format(i + 1)] = tmp_x
np_y['y{}'.format(i + 1)] = tmp_y
# ok,现在我们可以看一下数组的shape是否正确
# for i in range(settings.FRONT_SIZE + settings.BACK_SIZE):
# print(i + 1, np_x['x{}'.format(i + 1)].shape, np_y['y{}'.format(i + 1)].shape)
# 划分数据集
total_batch = len(np_x['x1']) # 总样本数
train_batch_num = int(total_batch * self.train_data_rate) # 训练样本数
train_np_x = {}
train_np_y = {}
test_np_x = {}
test_np_y = {}
for i in range(settings.FRONT_SIZE + settings.BACK_SIZE):
x_index = 'x{}'.format(i + 1)
y_index = 'y{}'.format(i + 1)
train_np_x[x_index] = np_x[x_index][:train_batch_num]
train_np_y[y_index] = np_y[y_index][:train_batch_num]
test_np_x[x_index] = np_x[x_index][train_batch_num:]
test_np_y[y_index] = np_y[y_index][train_batch_num:]
# 打乱训练数据
if self.shuffle:
random_seed = int(time.time())
# 使用相同的随机数种子,保证x和y的一一对应没有被破坏
for i in range(settings.FRONT_SIZE + settings.BACK_SIZE):
np.random.seed(random_seed)
np.random.shuffle(train_np_x['x{}'.format(i + 1)])
np.random.seed(random_seed)
np.random.shuffle(train_np_y['y{}'.format(i + 1)])
self.train_np_x = train_np_x
self.train_np_y = train_np_y
self.test_np_x = test_np_x
self.test_np_y = test_np_y
@property
def predict_data(self):
"""
模型训练好之后,获取预测下期彩票序列(未发生事件)时使用的输入数据.
数据处理方式与clean_data方法相似,但只返回最新的连续的MAX_STEPS期开奖序列的x值
:return:
"""
# 先从硬盘读取文件
lines = self.load_data_from_path()
# 去除引号,并使用逗号分割,将数据转成数组
x_nums = []
for line in lines:
# 下标0的位置是期号,直接去掉
nums = line.replace('"', '').split(',')[1:]
# 所有球的编号都减一,把1-35变成0-34,1-12变成0-11
# 这样便于后面做softmax
x_nums.append([int(x) - 1 for x in nums])
# 接着,把中奖序列中的七个数字拆开,按位置和时间纵轴组合,变成7组数据
num_seqs = {}
# 对于每一期的中奖序列
for line in x_nums:
# 对于一条中奖序列中的每一个数
for index, num in enumerate(line):
# 最后的数据格式{0: [1,2,3,4,...],1: [1,2,3,4,...],...,6: [1,2,3,4,...]}
num_seqs.setdefault(index, []).append(num)
# 根据时间序列,拆出来x和y数据集,每MAX_STEPS长度的连续序列构成一条数据的x,max_steps+1构成y
# 举例,假设MAX_STEPS=3,有序列[1,2,3,4,5,6],则[1,2,3->4],[2,3,4->5],[3,4,5->6]是组成的数据集
x = {}
for index, seqs in num_seqs.items():
x[index] = []
total = len(seqs)
# 存放本条x序列的,存放的是数字形式
tmp_x = []
# 将从i-MAX_STEPS到i(不包括i)的这一段长为MAX_STEPS的序列,逐个加入到tmp_x中
for j in range(total - settings.MAX_STEPS, total, 1):
tmp_x.append(seqs[j])
# 将这条记录添加到x数据集中
x[index].append(tmp_x)
# 我们现在需要把x里面的数字也转成one-hot形式,并转成numpy的array类型
np_x = {}
# 对应7个球构成的七组序列中的每一组
for i in range(settings.FRONT_SIZE + settings.BACK_SIZE):
# 获取样本数量
x_len = len(x[i])
# 根据球所处的前后区,分别进行初始化
if i < settings.FRONT_SIZE:
tmp_x = np.zeros((x_len, settings.MAX_STEPS, settings.FRONT_VOCAB_SIZE))
else:
tmp_x = np.zeros((x_len, settings.MAX_STEPS, settings.BACK_VOCAB_SIZE))
# 分别利用x,y中的数据修改tmp_x和tmp_y
for j in range(x_len):
for k, num in enumerate(x[i][j]):
tmp_x[j][k][num] = 1
# 然后将tmp_x和tmp_y按照球所处的位置,加入到np_x和np_y中
np_x['x{}'.format(i + 1)] = tmp_x
return np_x
需要提一下的是,虽然每个球上的编号都是数字,但我们不应该把它们当数字,因为它们的大小关系在开奖中没有任何意义,且在训练中可能会对模型产生干扰。因此我们选择使用one-hot形式以类别的方式来表示它们,而不是一个数值。
同时,为了softmax和one-hot方便,我将球上的数字都减了一,把1-35变成0-34,1-12变成0-11。
先给出模型示意图,再细说模型,可能会更清楚一些:
前面提到“和预测气温差不多,我们使用连续若干期的球1的数据来预测下期球1的分布概率,球2-球7都是同样的方法”,但因为这些球本身并不独立,比如球1开出了3,球2-5就不可能再开出3,而是在剩下的里面选。所以我们在预测最后的概率之前,对球1-5的中间层进行了拼接,再分别预测,这样模型可能会学习到每一期中前区的球之间的某种关系。对于球6和7,也做了类似操作。
而球1-5在前区,球6-7在后区,两者没什么关系,所以这两部分之间没有进行拼接。
另外,最后的输出预测层选择了softmax,值得说一下。严格来说,softmax对于这个问题来说,并不是一个很好地选择,因为开球应该是条件概率,比如球1开了5之后,开球2的概率计算应该是球1==5
条件下的条件概率,球3-5同理。但我最终还是选择了softmax,原因一是softmax实现起来更加简单,二是模型输出本身设计的就不是预测头等奖的完全正确序列,而是尽可能多的选中球,两者的区别前面提过。这样看来,softmax也还算合适,大不了重复了就使用轮盘赌法重新选。
其他的就没什么好说的了,模型示意图已经表现的很清楚了,更多的无非是层的选择罢了,直接看代码吧:
# -*- coding: utf-8 -*-
# @File : models.py
# @Author: AaronJny
# @Date : 2019/10/29
# @Desc : 建立深度学习模型
import keras
from keras import layers
from keras import models
import settings
# 这是一个多输入模型,inputs用来保存所有的输入层
inputs = []
# 这是一个多输出模型,outputs用来保存所有的输出层
outputs = []
# 前区的中间层列表,用于拼接
front_temps = []
# 后区的中间层
back_temps = []
# 处理前区的输入变换
for i in range(settings.FRONT_SIZE):
# 输入层
x_input = layers.Input((settings.MAX_STEPS, settings.FRONT_VOCAB_SIZE), name='x{}'.format(i + 1))
# 双向循环神经网络
x = layers.Bidirectional(layers.LSTM(settings.LSTM_UNITS, return_sequences=True))(x_input)
# 随机失活
x = layers.Dropout(rate=settings.DROPOUT_RATE)(x)
x = layers.Bidirectional(layers.LSTM(settings.LSTM_UNITS, return_sequences=True))(x)
x = layers.Dropout(rate=settings.DROPOUT_RATE)(x)
x = layers.TimeDistributed(layers.Dense(settings.FRONT_VOCAB_SIZE * 3))(x)
# 平铺
x = layers.Flatten()(x)
# 全连接
x = layers.Dense(settings.FRONT_VOCAB_SIZE * 3, activation='relu')(x)
# 保存输入层
inputs.append(x_input)
# 保存前区中间层
front_temps.append(x)
# 处理后区的输入和变换
for i in range(settings.BACK_SIZE):
# 输入层
x_input = layers.Input((settings.MAX_STEPS, settings.BACK_VOCAB_SIZE),
name='x{}'.format(i + 1 + settings.FRONT_SIZE))
# 双向循环神经网络
x = layers.Bidirectional(layers.LSTM(settings.LSTM_UNITS, return_sequences=True))(x_input)
# 随机失活
x = layers.Dropout(rate=settings.DROPOUT_RATE)(x)
x = layers.Bidirectional(layers.LSTM(settings.LSTM_UNITS, return_sequences=True))(x)
x = layers.Dropout(rate=settings.DROPOUT_RATE)(x)
x = layers.TimeDistributed(layers.Dense(settings.BACK_VOCAB_SIZE * 3))(x)
# 平铺
x = layers.Flatten()(x)
# 全连接
x = layers.Dense(settings.BACK_VOCAB_SIZE * 3, activation='relu')(x)
# 保存输入层
inputs.append(x_input)
# 保存后区中间层
back_temps.append(x)
# 连接
front_concat_layer = layers.concatenate(front_temps)
back_concat_layer = layers.concatenate(back_temps)
# 使用softmax计算分布概率
for i in range(settings.FRONT_SIZE):
x = layers.Dense(settings.FRONT_VOCAB_SIZE, activation='softmax', name='y{}'.format(i + 1))(front_concat_layer)
outputs.append(x)
for i in range(settings.BACK_SIZE):
x = layers.Dense(settings.BACK_VOCAB_SIZE, activation='softmax', name='y{}'.format(i + 1 + settings.FRONT_SIZE))(
back_concat_layer)
outputs.append(x)
# 创建模型
model = models.Model(inputs, outputs)
# 指定优化器和损失函数
model.compile(optimizer=keras.optimizers.Adam(),
loss=[keras.losses.categorical_crossentropy for __ in range(settings.FRONT_SIZE + settings.BACK_SIZE)],
loss_weights=[2, 2, 2, 2, 2, 1, 1])
# 查看网络结构
model.summary()
# 可视化模型,保存结构图
# from keras.utils import plot_model
# plot_model(model, to_file='model.png')
可视化模型部分,保存的就是上面那张模型图,因为需要额外的依赖,我就给注释掉了。如果确实需要执行的话,请自行安装相关依赖。
数据和模型都已经准备完毕,可以进行训练了。但这个模型不同于一般的分类模型,我们怎么来评估模型的效果呢?我选择的方法是——回测。
其实很简单,就是划分一部分数据(比如90%)作为训练集,训练模型,剩下的10%作为测试集。划分是按照时间顺序划分的,保证后面10%的数据绝不出现在训练集的结果数据或过程数据中。在使用训练集完成模型的训练后,我们对测试集进行预测,并按照预测结果购买彩票,计算支出和奖金,以最终的净收入的多少来衡量模型效果。
现在,我们需要编写一些工具方法,辅助我们完成回测。
# -*- coding: utf-8 -*-
# @File : utils.py
# @Author: AaronJny
# @Date : 2019/10/29
# @Desc : 对数据进行处理和操作的一些工具方法
import matplotlib.pyplot as plt
import numpy as np
import settings
def sample(preds, temperature=1.0):
"""
从给定的preds中随机选择一个下标。
当temperature固定时,preds中的值越大,选择其下标的概率就越大;
当temperature不固定时,
temperature越大,选择值小的下标的概率相对提高,
temperature越小,选择值大的下标的概率相对提高。
:param preds: 概率分布序列,其和为1.0
:param temperature: 当temperature==1.0时,相当于直接对preds进行轮盘赌法
:return:
"""
preds = np.asarray(preds).astype(np.float64)
preds = np.log(preds) / temperature
exp_preds = np.exp(preds)
preds = exp_preds / np.sum(exp_preds)
probas = np.random.multinomial(1, preds, 1)
return np.argmax(probas)
def search_award(front_match_num, back_match_num, cache={}):
"""
给定前后区命中数量,使用记忆化搜索查找并计算对应奖金
:param front_match_num: 前区命中数量
:param back_match_num: 后区命中数量
:param cache: 缓存用的记忆字典
:return:
"""
# 前后区都没有命中,奖金为0
if front_match_num == 0 and back_match_num == 0:
return 0
# 尝试直接从缓存里面获取奖金
award = cache.get((front_match_num, back_match_num), -1)
# 这里使用-1是为了避免0和None在判断上的混淆
# 如果缓存里面有,已经计算过了,就直接返回
if award != -1:
return award
# 尝试直接从中奖规则中获取奖金数量
award = settings.AWARD_RULES.get((front_match_num, back_match_num), -1)
if award == -1:
# 如果没找到,就先认为没中奖,然后将前区命中数量或后区命中数量减一,
# 递归查找,保留最大的中奖金额
award = 0
if front_match_num > 0:
award = search_award(front_match_num - 1, back_match_num)
if back_match_num > 0:
award = max(award, search_award(front_match_num, back_match_num - 1))
# 缓存下本次计算结果
cache[(front_match_num, back_match_num)] = award
# 返回奖金数额
return award
def lotto_calculate(winning_sequence, sequence_selected):
"""
给定中奖序列和选择的序列,计算获奖金额
:param winning_sequence:中奖序列
:param sequence_selected: 选择的序列
:return:
"""
# 前区命中数量
front_match = len(
set(winning_sequence[:settings.FRONT_SIZE]).intersection(set(sequence_selected[:settings.FRONT_SIZE])))
# 后区命中数量
back_match = len(
set(winning_sequence[settings.FRONT_SIZE:]).intersection(set(sequence_selected[settings.FRONT_SIZE:])))
# 计算奖金
award = search_award(front_match, back_match)
return award
def select_seqs(predicts):
"""
根据给定的概率分布,随机选择一种彩票序列
:param predicts:list[list] 每一个球的概率分布组成的列表
:return: list 彩票序列
"""
balls = []
# 对于每一种球
for predict in predicts:
try_cnt = 0
while True:
try_cnt += 1
# 根据预测结果随机选择一个
if try_cnt < 100:
ball = sample(predict)
else:
# 如果连续100次都是重复的,就等概率地从所有球里面选择一个
ball = sample([1. / len(predict) for __ in predict])
# 如果选重复了就重新选
if ball in balls:
# 序列不长,就没有使用set优化,直接用list了
continue
# 将球保存下来,跳出,开始选取下一个
balls.append(ball)
break
# 排序,前五个升序,后两个升序
balls = sorted(balls[:settings.FRONT_SIZE]) + sorted(balls[settings.FRONT_SIZE:])
return balls
def draw_graph(y):
"""
绘制给定列表y的折线图和趋势线
"""
# 横坐标,第几轮训练
x = list(range(len(y)))
# 拟合一次函数,返回函数参数
parameter = np.polyfit(x, y, 1)
# 拼接方程
f = np.poly1d(parameter)
# 绘制图像
plt.plot(x, f(x), "r--")
plt.plot(y)
plt.show()
数据集、模型和工具方法已经全部写好了,可以正式开始训练了。
我们将数据集按照训练集:测试集=9:1
的比例划分数据集,在训练集上训练模型,并使用测试集回测。我准备训练60轮,每一轮训练完成后,都会保存模型的参数,并进行回测。
在训练结束后,将所有回测结果,按时间顺序,绘制出折线图和趋势线。
# -*- coding: utf-8 -*-
# @File : train.py
# @Author: AaronJny
# @Date : 2019/10/29
# @Desc :
import os
import numpy as np
from models import model
from dataset import LottoDataSet
import settings
import utils
def simulate(test_np_x, test_np_y):
"""
模拟购买彩票,对测试数据进行回测
:param test_np_x: 测试数据输入
:param test_np_y: 测试数据输出
:return: 本次模拟的净收益
"""
# 获得的奖金总额
money_in = 0
# 买彩票花出去的钱总额
money_out = 0
# 预测
predicts = model.predict(test_np_x, batch_size=settings.BATCH_SIZE)
# 共有多少组数据
samples_num = len(test_np_x['x1'])
# 对于每一组数据
for j in range(samples_num):
# 这一期的真实开奖结果
outputs = []
for k in range(settings.FRONT_SIZE + settings.BACK_SIZE):
outputs.append(np.argmax(test_np_y['y{}'.format(k + 1)][j]))
# 每一期彩票买五注
money_out += 10
for k in range(5):
# 存放每个球的概率分布的list
probabilities = []
# 对于每一种球,将其概率分布加入到列表中去
for i in range(settings.FRONT_SIZE + settings.BACK_SIZE):
probabilities.append(predicts[i][j])
# 根据概率分布随机选择一个序列
balls = utils.select_seqs(probabilities)
# 计算奖金
award = utils.lotto_calculate(outputs, balls)
money_in += award
if award:
print('{} 中奖了,{}元! {}/{}'.format(j, award, money_in, money_out))
print('买彩票花费金钱共{}元,中奖金额共{}元,赚取{}元'.format(money_out, money_in, money_in - money_out))
return money_in - money_out
# 初始化数据集
lotto_dataset = LottoDataSet(train_data_rate=0.9)
# 创建保存权重的文件夹
if not os.path.exists(settings.CHECKPOINTS_PATH):
os.mkdir(settings.CHECKPOINTS_PATH)
# 开始训练
results = []
for epoch in range(1, settings.EPOCHS + 1):
model.fit(lotto_dataset.train_np_x, lotto_dataset.train_np_y, batch_size=settings.BATCH_SIZE, epochs=1)
# 保存当前权重
model.save_weights('{}/model_checkpoint_{}'.format(settings.CHECKPOINTS_PATH, epoch))
print('已训练完第{}轮,尝试模拟购买彩票...'.format(epoch))
results.append(simulate(lotto_dataset.test_np_x, lotto_dataset.test_np_y))
# 输出每一轮的模拟结果
print(results)
# 显示每一轮模拟结果的变化趋势
utils.draw_graph(results)
有几点需要注意:
回测时的输出大致是这样的(输出比较长,截取一部分):
已训练完第17轮,尝试模拟购买彩票...
6 中奖了,5元! 5/70
8 中奖了,5元! 10/90
8 中奖了,5元! 15/90
10 中奖了,5元! 20/110
12 中奖了,15元! 35/130
23 中奖了,5元! 40/240
24 中奖了,5元! 45/250
24 中奖了,5元! 50/250
25 中奖了,5元! 55/260
33 中奖了,5元! 60/340
36 中奖了,15元! 75/370
38 中奖了,5元! 80/390
41 中奖了,5元! 85/420
44 中奖了,5元! 90/450
46 中奖了,5元! 95/470
51 中奖了,15元! 110/520
51 中奖了,5元! 115/520
54 中奖了,5元! 120/550
61 中奖了,5元! 125/620
61 中奖了,5元! 130/620
62 中奖了,5元! 135/630
62 中奖了,5元! 140/630
67 中奖了,5元! 145/680
75 中奖了,5元! 150/760
76 中奖了,5元! 155/770
84 中奖了,5元! 160/850
87 中奖了,5元! 165/880
88 中奖了,5元! 170/890
88 中奖了,5元! 175/890
88 中奖了,15元! 190/890
90 中奖了,5元! 195/910
93 中奖了,5元! 200/940
96 中奖了,15元! 215/970
107 中奖了,5元! 220/1080
114 中奖了,5元! 225/1150
115 中奖了,5元! 230/1160
120 中奖了,100元! 330/1210
123 中奖了,5元! 335/1240
123 中奖了,200元! 535/1240
123 中奖了,15元! 550/1240
125 中奖了,5元! 555/1260
125 中奖了,5元! 560/1260
133 中奖了,5元! 565/1340
136 中奖了,5元! 570/1370
136 中奖了,5元! 575/1370
141 中奖了,5元! 580/1420
142 中奖了,15元! 595/1430
147 中奖了,5元! 600/1480
147 中奖了,5元! 605/1480
149 中奖了,15元! 620/1500
149 中奖了,5元! 625/1500
153 中奖了,5元! 630/1540
155 中奖了,15元! 645/1560
155 中奖了,5元! 650/1560
160 中奖了,15元! 665/1610
164 中奖了,5元! 670/1650
买彩票花费金钱共1660元,中奖金额共670元,赚取-990元
我试着跑了几次,给出几个我跑出来的结果:
[-1335, -1305, -1360, -1420, -1215, -1090, -1140, -1395, -1310, -1355, -1220, -1095, -1375, -1420, -1255, -1270, -730, -1155, -1360, -1330, -1140, -1090, -1030, -1340, -1060, -1150, -1285, -935, -1175, -1290, -1260, -1075, -1275, -1275, -870, -1275, -890, -1175, -1265, -1235, -1260, -1265, -1255, -1270, -1170, -660, -1015, -915, -1095, -850, -560, -890, -980, -670, -1185, -510, -1110, -470, -1180, -655]
这算是一个比较符合预期的结果?虽然还是在赔钱,但达到了我们的目的——少赔点钱=。=虽然一直在赔钱,但随着训练次数的增加,亏损金额在整体趋势上逐步减少。
[-1095, -1345, -1405, -1390, -1265, -1055, -970, -1375, -1205, -1365, -1260, -1305, -1120, -1280, -1125, -1370, -860, -1285, -1160, -1065, -1295, -1105, -765, -1160, -1055, -1180, -780, -1200, -1205, -760, -1075, -1105, -1130, -955, -1105, -1170, -1140, -915, -735, 8785, 9065, -1035, -1145, -635, -785, 8935, 799876, -995, -1010, -1130, -1125, -1170, -1255, -1035, -920, -935, -1090, -1330, 8930, -1370]
这个是比较容易血压升高的结果?有一次回测的过程中中了80万…因为80万跟其他回测结果差距太大了,所以图像上基本显示不出其他轮次的起伏了。我们把最高的结果减去79万,看一下其他轮次的趋势:
这个看起来就顺眼多了。
多次运行的结果可能差距明显,其原因分析如下:
两者综合起来,两次的运行结果可能天差地别。但从多次运行的整体来看,还是有一定规律的:
综上,我们的模型应该是起到了一定作用。
你可能说,这亏的也不少啊,我怎么看出来模型到底有没有效果呢?那我们写一个基线模型,来比较一下。
什么是基线模型?
emmm,怎么说呢,它指的是一个最基础、最简单的模型,它是从概率的角度上来说最糟糕的一个模型。可能解释的不是很清楚,我们直接看例子。
一般极限模型就是都是完全随机的。比如这个问题,我们需要从前区选出五个球,后区选出两个球,我们每个球都随机选择,这就是基线模型。emmm,它类似于彩票中心的机选方案?
我们来实现一下基线模型,并模拟多次购买彩票经历,看使用基线模型我们会亏多少:
# -*- coding: utf-8 -*-
# @File : random_show.py
# @Author: AaronJny
# @Date : 2019/10/29
# @Desc : 随机选择情况下的收益情况
import random
import numpy as np
from dataset import LottoDataSet
import utils
import settings
def get_one_random_sample():
"""
获取一种随机序列
:return:
"""
front_balls = list(range(settings.FRONT_VOCAB_SIZE))
back_balls = list(range(settings.BACK_VOCAB_SIZE))
return random.sample(front_balls, settings.FRONT_SIZE) + random.sample(back_balls, settings.BACK_SIZE)
def simulate(test_np_x, test_np_y):
# 获得的奖金总额
money_in = 0
# 买彩票花出去的钱总额
money_out = 0
# 共有多少组数据
samples_num = len(test_np_x['x1'])
# 对于每一组数据
for j in range(samples_num):
# 这一期的真实开奖结果
outputs = []
for k in range(settings.FRONT_SIZE + settings.BACK_SIZE):
outputs.append(np.argmax(test_np_y['y{}'.format(k + 1)][j]))
# 每一期彩票买五注
money_out += 10
for k in range(5):
balls = get_one_random_sample()
# 计算奖金
award = utils.lotto_calculate(outputs, balls)
money_in += award
print('买彩票花费金钱共{}元,中奖金额共{}元,赚取{}元'.format(money_out, money_in, money_in - money_out))
return money_in - money_out
dataset = LottoDataSet(train_data_rate=0.9)
# 随机买一百次,并记录每一次收入-支出的差值
results = []
for epoch in range(1, 101):
results.append(simulate(dataset.test_np_x, dataset.test_np_y))
# 去除最高的和最低的
results = sorted(results)[1:-1]
# 计算平均值
print('mean', sum(results) / len(results))
运行一下,输出的结果是这样的:
多次运行可以发现,最后的平均值绝大多数落在[-1400,-1200]之间,其中又以-1250左右最多。少数亏得更少或更多,极少数能够小赚。
这样比较下来,我们写的模型还是有用的?至少能少亏一点?滑稽.jpg
平时事情也比较多,所以模型只是大概调了一下,如果对此有兴趣的话,也可以在此基础上,再自行调一下参看看。
如果准备利用模型买彩票,可以分为两种情况:
MAX_STEPS
期到最近一期的数据序列,预测下一期序列。MAX_STEPS
期到最近一期的数据序列,预测下一期序列。两者的区别在于:
各有优缺点吧,酌情选择。但不管怎么样,我们都来实现一下完整训练的脚本:
# -*- coding: utf-8 -*-
# @File : train_with_whole_dataset.py
# @Author : AaronJny
# @Date : 2019/11/26
# @Desc : 使用全部数据集进行训练
import os
from models import model
from dataset import LottoDataSet
import settings
# 初始化数据集
lotto_dataset = LottoDataSet(train_data_rate=1)
# 创建保存权重的文件夹
if not os.path.exists(settings.CHECKPOINTS_PATH):
os.mkdir(settings.CHECKPOINTS_PATH)
# 开始训练
model.fit(lotto_dataset.train_np_x, lotto_dataset.train_np_y, batch_size=settings.BATCH_SIZE, epochs=settings.EPOCHS)
# 保存模型
model.save_weights('{}/model_checkpoint_x'.format(settings.CHECKPOINTS_PATH))
好的,不论你选择用哪种方法训练出的模型,都没有关系,我们来看看如何让模型帮我们选号码。
# -*- coding: utf-8 -*-
# @File : predict.py
# @Author : AaronJny
# @Date : 2019/11/26
# @Desc : 指定一个训练好的模型参数,让模型随机选出下期彩票号码
from dataset import LottoDataSet
from models import model
import settings
import utils
# 加载模型参数
model.load_weights(settings.PREDICT_MODEL_PATH)
# 构建数据集
lotto_dataset = LottoDataSet()
# 提取倒数第MAX_STEPS期到最近一期的数据,作为预测的输入
x = lotto_dataset.predict_data
# 开始预测
predicts = model.predict(x, batch_size=1)
# 存放选号结果的列表
result = []
# 存放每个球的概率分布的list
probabilities = [predict[0] for predict in predicts]
# print(probabilities)
# 总共要选出settings.PREDICT_NUM注彩票
for i in range(settings.PREDICT_NUM):
# 根据概率分布随机选择一个序列
balls = utils.select_seqs(probabilities)
# 加入到选号列表中,注意,我们需要把全部的数字+1,恢复原始的编号
result.append([ball + 1 for ball in balls])
# 输出要买的彩票序列
print('本次预测结果如下:')
for index, balls in enumerate(result, start=1):
print('第{}注 {}'.format(index, ' '.join(map(str, balls))))
模型默认加载使用完整数据作为训练集的模型参数,如果想要加载其他指定参数,直接修改settings中的PREDICT_MODEL_PATH
即可。
运行一下,模型输出:
本次预测结果如下:
第1注 2 5 19 25 26 1 11
第2注 2 5 19 26 28 1 12
第3注 1 5 19 21 26 7 11
第4注 2 5 19 26 28 1 11
第5注 1 5 19 21 26 8 11
emmm,我一会儿去买下看看能不能中……滑稽.jpg
好的,这次的分享就到这里了,应该没什么遗漏的吧?因为最近事情比较多,所以这篇文章和相关代码编写的时间跨度很长,内容也比较多,虽然我已经通读了几遍,但可能还是会漏下某些没发现的问题,请见谅。分享中涉及到的全部代码,都已经上传到了GitHub,戳这里 (https://github.com/AaronJny/lotto)。
还是要强调一下,这只是一个以技术研究为出发点的娱乐性质的小实验,所以请不要指望这个能帮你赚大钱(如果运气爆棚真的中了,那就当我没说……见面分一半?),能少赔点就不错啦。
最后,请珍惜钱财,远离彩票。小赌怡情,大赌伤身。知难而退,量力而行。