はじめに
突然ですが、東北大学の大関先生が開催している「量子アニーリングを利用した組み合わせ最適化問題の解法に関するワークショップ」をご存知でしょうか?
こちら2021年5月からYouTubeライブで開催されているんですが、とにかく熱量がハンパないんです。
1時間半の講義だと聞いていたのに、3時間越えは当たり前(むしろ短い)、気づけば日を跨いでいるという..。先生の体力はどうなっているんでしょう。
このワークショップには高校生、大学生、社会人、さらには高校生の母親など、多様性に富んだメンバーが参加しており、かくいう私も時には3倍速を駆使しながらこのワークショップに参加しています。自分が高校生の頃なんて、夜はギャグマンガ日和を読むくらいしか考えつきませんでしたが、今時の高校生は偉いですね!
さて、本題です。
今回の記事では、このワークショップで私たちのチームが取り組んでいる「プロ野球中継ぎ酷使問題」について紹介したいと思います。内容はアップデートし次第、改訂していく予定です。
ここからは真面目に書きます。
中継ぎ酷使問題とは
下図は2021/6/6のYahoo!News記事をテキストマイニングしたものです。
日本プロ野球界の中継ぎ投手事情を表す言葉として、”過多”、”離脱”、”過酷”といったネガティブな言葉が散見されますね..。
私も一野球ファンとして長年プロ野球を楽しんでおりますが、毎年のように怪我や不調に苦しむ中継ぎ投手が発生している現状については心苦しく思っていました。
「中継ぎ投手の負担が少ない運用方法を考えられないものだろうか」
そんな折、大関先生の講座に参加した事で「あれ..この課題は量子アニーリングで解決できるのでは..」とピンときて、問題の定式化に臨むことにしました。
問題の設定
以降では中継ぎ投手の負担を減らすべく、はじめに問題設定を行います。
この記事を書いている時点(2021/6/14)では、まだコンセプト検証段階、といった程度です。
適宜更新していこうと思いますので、仮定が甘い部分はご容赦ください。
前提条件1
- 先発投手は6イニングを投げ、残り3イニングを3人の中継ぎ投手でリレーする
前提条件2
- 6イニング終了時に(味方の追加点も加味し)ΔX点の点差がある。(今回はΔx>0のみを扱う)
前提条件3
- 各中継ぎ投手には、上限イニング数と失点の期待値が存在 (dict型情報を格納)
この問題の目標
- 各投手の上限イニング数を守りつつ、勝利数が最大になるように中継ぎ投手の組み合わせを考える。
上限イニング数を定めることで、投げすぎリスクを事前に回避できそうですね。そしてその制約の中で最も勝利数を上げるための投手組み合わせを決定していくことになります。
制約式について
量子アニーリングを利用してこの問題を解くためには上記ルールをうまい具合に定式化する必要があります。量子アニーリングとはなんぞや?という方はぜひ大関先生の熱血YouTube配信を見てほしい..。
恐れ多くも一言で言えば、「組み合わせ最適化問題」を量子的なふるまいを利用して解く手法といったところでしょうか。0-1の組み合わせが無数にあるような問題に強い、といった認識をしております。
先ほどの制約式を行列の形で表すと以下のような図になります。
行列要素(i,j) = 1というのは投手ID=iの人が試合ID=jの日に登板する(1イニング投げる)という意味ですね。
各行の総和が3となるように、また各列の総和は投手ごとの上限イニングとなるように制約式をたてていきます。
余談ですが、試合IDを例えば過去のペナントレース結果と紐付け、さらに連投禁止制約まで加えられるとさらに面白くなると思います。ただ、現状そこは未考慮です。
コードはこんな感じ
pyquboを使って先ほどの行列を定式化すると、以下のようになります。
from pyqubo import Array, Constraint, solve_qubo
x = Array.create('x', shape=(N,len(pitcher)), vartype='BINARY')
# 第一項 (制約項)を定義します。 各試合、3投手まで
H_A_Row = Constraint(sum((3-sum(x[v,i] for i in range(len(pitcher))))**2 for v in range(N)), label='HA_Row')
# 第二項 (制約項)を定義します。 各投手決められたイニングまで
H_A_Col = Constraint(sum((pitcher[v][2]-sum(x[i,v] for i in range(N)))**2 for v in range(len(pitcher))), label='HA_Col')
そして目的関数はこのように設定しました。
#目的関数を定義します。
H_B = sum((N_array[v]-1 - sum(x[v,i]*pitcher[i][1] for i in range(len(pitcher))))**2 for v in range(N))
ここには1つ工夫があって、1点差に近づけるように投手を選ぶよう目的関数を設定しています。
そうすることで僅差の時と大差の時で投手運用が変わると期待しました。
解いてみた
上記の制約式、目的関数を1つにまとめ、実際に解いてみました。
from openjij import SQASampler
## ハミルトニアン全体を定義します。
Q = 3*H_A_Row+3*H_A_Col+H_B
## モデルをコンパイルします。
model = Q.compile()
qubo, offset = model.to_qubo()
sampler = SQASampler()
raw_solution = sampler.sample_qubo(qubo)
print(raw_solution)
得られた結果をもう少し分かりやすくするために、なんやかんやしていきます。
# 得られた結果をデコードします。
decoded_sample = model.decode_sample(raw_solution.first.sample, vartype="BINARY")
# さらに解を見やすくする処理を追加します。
# .array(変数名, 要素番号)で希望する要素の値を抽出することができます。
x_solution = {}
to_list = []
for i in range(N):#(n):
x_solution[i] = {}
for j in range(len(pitcher)):
x_solution[i][j] = decoded_sample.array('x', (i, j))
if decoded_sample.array('x', (i, j))==1:
to_list.append(j)
# 制約式から逸脱していないかのチェック。ダメな時はFalseが返ってくる
print(decoded_sample.constraints())
print(decoded_sample.constraints(only_broken=True))
最後のprint文で制約式を守れているのか否かを確認できます。今回は制約を守れていますね。
(その後もう少しだけ後処理し)可視化した結果が以下のようになります。
なるほど、どの点差でも負けにならないように投手運用できてそうですね..
では実際に誰が投げたのか、以下の通りとなりました。
既にお気づきの方もいるかと思いますが、私は阪神ファンなのでタイガースのピッチャーで試しています。
絶対的守護神のスアレス投手、実績のある岩崎投手は僅差での登板が目立ちますね。2点差になると多少運用に柔軟性が入ってきて、5点差にもなると若手投手の抜擢が目立ちます。結構いい感じに投手の組み合わせができていそうです!
まとめと今後の課題
まとめ
- 1点差勝利を目指し、僅差の時は優秀な投手を、大差の時は若手が投げるような計画ができた
- イニング数などの制約を満たしながら計算できることがわかった
今後の課題
- 勝敗の見せ方は、もう少し確率的に、幅を持って示せた方がよいだろう
- Δxの分布は実データを活用すべき。連投問題にも着手したい
- アプリケーションにまで落とし込みたい etc.
思ったよりいい感じの結果になったので、勢いで記事にもしてしまいました。アップデートしていき、矢野監督にも売り込んでいきたいですね。
終わりに
今更ですが、この内容はこのリンクからYouTubeでも観ることができます。
本発表以外にも面白いテーマで検討されている方がたくさんいるので、ご覧になると面白いはずです!
11時間超のロングムービーですが..笑
ご覧頂きありがとうございました!