kurosame’s diary

フロントエンド中心です

ロト6 の当選番号を機械学習を使って予測する

はじめに

機械学習Python もまともに触ったことないし、数式にも馴染みがないけど
時代に取り残されないために勉強がてら scikit-learn を使ってロト6 の当選番号を予測してみます

一応この本の 8 章までは読んでます www.oreilly.co.jp

ロト6 を選んだのは、なんとなく実装が簡単そうだったからです

ちなみに「ロト6 機械学習」でググっても予測は難しいと言ってる人がほとんどなので、予測の難易度はかなり高いと思われます
あくまで勉強の一環です


ロト6 概要

一応ロト6 の簡単な概要

  • 1〜43 の中から 6 つの数字を選び、抽選結果の数字と一致している数によって当選金が分配される
  • 1 等約 2 億円(約と言ってるのは当選人数やキャリーオーバーによって変動するため)
  • 年中無休で購入可能
  • 抽選日は毎週月曜と木曜

機械学習モデルの選択

回帰モデルで訓練データが少なくてもいけそうなやつがいいかなって思ってましたが、ググってるとサポートベクターマシンSVM)で予測されてる方が何人かいたので scikit-learn の SVR クラスを使うことにしました

カーネルトリックを使ってみたいと思って、訓練データが少ない場合は、ガウス RBF カーネルが良さそうなのでそれを使います
訓練データは過去のロト6 の当選番号全てです
アウトプットは次回の抽選回の当選番号予測です
また、ボーナス数字の予測はしません


訓練データの作成

本数字 1〜6 をそれぞれ別の訓練データとして分割しています
最初は手っ取り早く過去の当選番号を全て 1 つの訓練データとして訓練させようとしてましたが、やめました

訓練データを作成するプログラム

import pandas as pd

url = 'http://sougaku.com/loto6/download/loto6.zip'

train = pd.read_csv(url, encoding='cp932')

X = train['抽せん回']
X_train = X[:, np.newaxis]
y_train = [train['本数字1'].values,
           train['本数字2'].values,
           train['本数字3'].values,
           train['本数字4'].values,
           train['本数字5'].values,
           train['本数字6'].values]

next_round = X.tail(1).values[0] + 1

X_train は過去の抽選回全て
y_train は本数字ごとの過去の当選番号全て
next_round は予測を行う回(つまり次回の抽選回)
です


学習と予測

以下のコードは本数字 1 のみを学習(fit)して、予測(predict)するプログラムです
本数字は 6 つあるので、ループするなどして計 6 回学習と予測を実行します

from sklearn.svm import SVR

svr_rbf = SVR(kernel='rbf', gamma=..., C=..., epsilon=...)
svr_rbf.fit(X_train, y_train[0]).predict(np.array([[next_round]]))

γ、C、ε の 3 つのハイパーパラメータを調整して実行します


ハイパーパラメータの最適化

scikit-learn のグリッドサーチを使えば簡単にできます
SVMSVR のハイパーパラメータの範囲は広いので 2 のべき乗の値をパラメータの候補として設定するそうです

パラメータ 候補の範囲 候補の数 Python で書くと
γ 2^{-20} 〜 2^{10} 31 np.logspace(-20, 10, 31, base=2)
C 2^{-5} 〜 2^{10} 16 np.logspace(-5, 10, 16, base=2)
ε 2^{-10} 〜 2^0 11 np.logspace(-10, 0, 11, base=2)

だいぶ絞られましたが、それでも 3 つのパラメータの組み合わせは31 x 16 x 11 = 5456通りもあります

以下のコードは本数字 1 のみを対象にグリッドサーチを使い、5456 通りのパラメータの組み合わせ全てを学習・評価し、精度が良いものを出力する例です
ただし、学習データの量にもよりますが、かなり時間がかかる(むしろ終わらない)ため、ある程度 5456 通りから更に候補を絞る必要は出てきます

param_grid = {
    'kernel': ['rbf'],
    'gamma': np.logspace(-20, 10, 31, base=2),
    'C': np.logspace(-5, 10, 16, base=2),
    'epsilon': np.logspace(-10, 0, 11, base=2)
}

grid_search = GridSearchCV(SVR(), param_grid, iid=False, cv=5)
### かなり時間がかかるため、パラメータの候補を更に絞った方が良い ###
grid_search.fit(X_train, y_train[0])
gs = grid_search.best_params_

print('grid-search result: {}'.format(gs))
# grid-search result: {'C': 32.0, 'epsilon': 0.5, 'gamma': 1.0, 'kernel': 'rbf'}

予測と当選結果確認をスケジューリング

毎回プログラムを実行したり当選結果を確認するのは面倒なので、ある程度自動化しておきます

以下のコードは、ロト6 の当選番号が載ってるサイトから抽選回、当選番号、ボーナス番号をスクレイピングしています

import bs4
import re
from urllib.request import urlopen

url = 'http://www.takarakuji-loto.jp/tousenp.html'

html = bs4.BeautifulSoup(urlopen(url).read(), 'html.parser')
section = html.find('section', class_='tousenno')
tbody = section.find_all('tbody')

current_round = re.match('第[0-9]+回', section.find('div').string).group()
winning_number = [i.get('alt') for i in tbody[0].find_all('img')]
bonus_number = tbody[1].find('img').get('alt')

以下のコードは、CircleCI のスケジューリング機能を使ってロト6 の当選番号を予測するプログラム(svr.py)を呼ぶ predict ジョブと当選結果をスクレイピングするプログラム(res.py)を呼ぶ result ジョブを設定しています
毎週月曜と木曜が抽選日なので、predict ジョブは前日の日曜日と水曜日に実行し、result ジョブは翌日の火曜日と金曜日に実行するようにしています

references:
  commands:
    setup-docker: &setup-docker
      docker:
        - image: kurosame/circleci-python

version: 2
jobs:
  predict:
    <<: *setup-docker
    steps:
      - checkout
      - run:
          name: Predict
          command: python3 svr.py
  result:
    <<: *setup-docker
    steps:
      - checkout
      - run:
          name: Get result
          command: python3 res.py

workflows:
  version: 2
  nightly-predict:
    triggers:
      - schedule:
          cron: '00 0 * * 0,3'
          filters:
            branches:
              only:
                - master
    jobs:
      - predict
  nightly-result:
    triggers:
      - schedule:
          cron: '00 0 * * 2,5'
          filters:
            branches:
              only:
                - master
    jobs:
      - result

CircleCI で使っている Docker は以下です
Python3 とプログラム実行に必要なライブラリをインストールしたイメージを使っています hub.docker.com

また、各プログラムの最後で Slack に通知するように実装しています

import slackweb

url = 'https://hooks.slack.com/services/...' # SlackのWebhook URL
slackweb.Slack(url).notify(text='ここに予測結果とか当選番号とか色々設定しておく')

流れ的には

  1. 日曜日と水曜日に predict ジョブが cron 実行され、Slack に予測結果を通知
  2. ロト6 を購入
  3. 月曜日と木曜日に抽選が行われる
  4. 火曜日と金曜日に result ジョブが cron 実行され、Slack に当選結果を通知

さいごに

現在 2 回予測結果でロト6 を購入しましたが外れました(2 回とも 2 個までは数字は的中しました)
とりあえずランダムで買った時とあまり結果が変わらないなーって思ったらやめます
ちなみに次は競馬予測をやってみようかなと思ってます