概要
本記事では、ヒューリスティックな確率モデルとベイズの定理を用いて、ブラックジャックの勝率を計算する方法を解説します。Python を使い、ライブラリに頼らずゼロから計算ロジックを実装することで、確率計算の裏側で何が起きているのかを理解することを目指します。
目次
- ヒューリスティックな確率モデルとは
- 定義と構成要素
- 例:ブラックジャックの勝率予測
- 正規化(Normalization)の概念
- Python による手計算での実装
- ベイズ的アプローチによる勝率計算
- より複雑なモデルを扱うには(ライブラリの紹介)
- 専門ライブラリの利点
1. ヒューリスティックな確率モデルとは
ヒューリスティックな確率モデルは、すでに持っているすべての情報をもとに、ある事象の確率を予測するためのアプローチです。
ここでいう「すべての情報」には、例えば次のようなものが含まれます:
- 観測変数(Observed Variables):直接測定や観測が可能な情報
- 隠れ変数(Hidden Variables / Latent Variables):直接は観測できないが、結果に影響を与えると考えられる要因
持っている情報をもとに累積確率を計算する場合は、まずそれぞれの状況における生の確率(事前確率)を求めます。
例えば、ある知識から「起こる確率」が 40%(0.4)とわかり、別の独立した知識からは 80%(0.8)とわかったとします。
この 2 つの知識を組み合わせた確率は、両方の確率を掛け合わせることで求められます。
0.4 × 0.8 = 0.32(32%)
ここで確率が下がったように見えるのは、これは全体の比率の一部にすぎないためです。
完全な比率を求めるには、「起こらない確率」も計算する必要があります。
それぞれの補数(起こらない確率)は 1 − 0.4 = 0.6 と 1 − 0.8 = 0.2 であり、これらを掛け合わせることで、両方とも「起こらない」確率が求められます。
0.6 × 0.2 = 0.12(12%)
この時点では数値が直感的にしっくりこないかもしれませんが、次にこれらを正規化して、すべての可能性に基づく割合に変換することで、より論理的に納得できる結果となります。
正規化(Normalization)の必要性
このような確率モデルでは、複数の確率を組み合わせて最終的な確率を求めますが、その計算結果は必ずしも「0〜1 の範囲で合計 1」になるとは限りません。
そのため、すべての候補の確率を合計が 1 になるようにスケーリングする処理が必要です。これを**正規化(Normalization)**と呼びます。
例:
生の(非正規化)確率 → Win: 0.32, Lose: 0.12
合計:0.32 + 0.12 = 0.44
このままでは確率として扱えないため、正規化します。
正規化後の確率:
- Win: 0.32 / 0.44 = 0.7272 (72.7% -切り上げ)
- Lose: 0.12 / 0.44 = 0.2727 (27.3% -切り上げ)
このように、各値を合計で割ることで、最終的な確率の合計が 1 になるように揃えます。
ブラックジャックの例で考える
まず、ブラックジャックの基本的なゴールを確認しましょう。プレイヤーとディーラーがカードをすべて引き終えた時点で、最終的にカードの合計点が 21 に近い方が勝ちとなります(ただし 21 を超えてはいけません)。
ヒューリスティックな考え方を使うと、複数の情報を組み合わせて、より正確な勝率を予測できます。
例えば、最初に「自分のカードだけを見たときの勝率」があるとします。しかし、これだけでは不十分です。ディーラーのカードも見えている場合、その情報を追加することで、最初の予測を更新し、より精度の高い勝率を計算することができます。
ここでは、あなたとディーラーのスキルレベルは同じだと仮定します。つまり、あるカード(例えば「A」)を持っているときの基本的な勝率は、どちらのプレイヤーにとっても同じです。この前提のもと、2 つの情報(自分のカードとディーラーのカード)を組み合わせて、最終的な勝率を導き出します。
- MyCard: 自分の最初のカード (例: 'A', '10', '5')
- DealerCard: ディーラーの最初のカード (例: 'A', '10', '5')
確率の定義
まず、各カードが単独でどれくらい強いかを定義します。これは「そのカードを持っている場合に勝つ確率」のようなものです。特に、エース(A)は 1 としても 11 としても使えるため、最も強力なカードとされています。
| カード | 勝率 |
|---|---|
| A | 0.60 |
| K | 0.55 |
| Q | 0.55 |
| J | 0.55 |
| 10 | 0.55 |
| 9 | 0.54 |
| 8 | 0.52 |
| 7 | 0.50 |
| 6 | 0.48 |
| 5 | 0.45 |
| 4 | 0.42 |
| 3 | 0.38 |
| 2 | 0.35 |
(この確率はあくまで例です)
正しい計算方法
2 つの独立した情報(自分のカードとディーラーのカード)から最終的な勝率を計算するには、以下のステップを踏みます。
ステップ 1: 各プレイヤーの勝敗確率を求める
まず、自分のカード (MyCard) とディーラーのカード (DealerCard) に基づいて、それぞれの「勝つ確率」と「負ける確率」を個別に計算します。
- 自分の勝率: P(Win | MyCard)
- 自分の負率: 1 - P(Win | MyCard)
- ディーラーの勝率: P(Win | DealerCard)
- ディーラーの負率: 1 - P(Win | DealerCard)
ステップ 2: 「自分が勝つシナリオ」と「自分が負けるシナリオ」の確率を計算する
次に、これらの確率を組み合わせて、ゲーム全体のシナリオを考えます。
-
自分が勝つシナリオ:
自分が勝ち、かつディーラーが負ける必要があります。この 2 つの独立した出来事が同時に起こる確率なので、それぞれの確率を掛け合わせます。
P'(Win) = P(Win | MyCard) * P(Lose | DealerCard) -
自分が負けるシナリオ:
自分が負け、かつディーラーが勝つ必要があります。同様に、それぞれの確率を掛け合わせます。
P'(Lose) = P(Lose | MyCard) * P(Win | DealerCard)
この時点で得られる P'(Win) と P'(Lose) は、まだ最終的な確率ではありません。これらは「生の」スコア、または「非正規化」確率と呼ばれます。
ステップ 3: 正規化して最終的な確率を求める
最後に、計算した「生の」スコアの合計が 1 になるように調整します。これを正規化 (Normalization) と呼びます。
- まず、生のスコアをすべて足し合わせます: Total = P'(Win) + P'(Lose)
- それぞれのスコアを合計で割ることで、最終的な確率を求めます。
- P(Win) = P'(Win) / Total
- P(Lose) = P'(Lose) / Total
このプロセスにより、すべての可能性を考慮した上で、最終的な勝率と負率を正しく求めることができます。
計算例:自分もディーラーも「A」の場合
では、具体的な数字で計算してみましょう。自分もディーラーもカードが「A」の場合、表から勝率は 0.60 です。
-
各プレイヤーの確率:
- 自分の勝率: 0.60, 自分の負率: 0.40
- ディーラーの勝率: 0.60, ディーラーの負率: 0.40
-
シナリオの確率:
- P'(Win) = 0.60 * 0.40 = 0.24
- P'(Lose) = 0.40 * 0.60 = 0.24
-
正規化:
- Total = 0.24 + 0.24 = 0.48
- P(Win) = 0.24 / 0.48 = 0.5
- P(Lose) = 0.24 / 0.48 = 0.5
結果として、勝率 50%、負率 50%となり、同じカードを持つ場合の直感的な結果と一致します。
隠れ変数:プレイヤーのスキルレベル
ここまでの計算ではカードの情報のみを使いましたが、実際には直接観測できない「隠れ変数」も結果に影響します。その代表例がプレイヤーのスキルレベルです。
例えば、100 回のゲームで各プレイヤーが何回勝つか(または負けるか)という一般的な勝率が分かっていれば、そのスキルレベルを確率として計算に加えることができます。このスキルという変数を、カードの確率と同じように掛け合わせることで、さらに精度の高い予測が可能です。
今回の例では簡単化のために省略しますが、このようにヒューリスティックなアプローチは複数の情報を柔軟に組み合わせられるのが強みです。
2. Python による手計算での実装
これまでに説明した計算ロジックは、専門のライブラリを使わなくても、シンプルな Python 関数として直接実装できます。
計算ロジックの解説
実装するコードは、以下のステップを忠実に実行します。
- カードの基本勝率を辞書として定義します。
-
calculate_win_probability 関数を作成します。
- この関数は、自分のカードとディーラーのカードを引数として受け取ります。
- 内部で非正規化確率の計算と正規化をすべて行い、最終的な勝率を返します。
- 関数を呼び出して結果を表示します。
シンプルな実装コード
python
1. カードの基本勝率を定義
card_probabilities = {
'A': 0.60, 'K': 0.55, 'Q': 0.55, 'J': 0.55, '10': 0.55,
'9': 0.54, '8': 0.52, '7': 0.50, '6': 0.48, '5': 0.45,
'4': 0.42, '3': 0.38, '2': 0.35
}
2. 計算ロジックを定義した関数
def calculate_win_probability(my_card, dealer_card, probabilities):
"""
2 つのカード情報から、正規化された勝率を計算します。
"""
p_win_my = probabilities[my_card]
p_win_dealer = probabilities[dealer_card]
raw_win = p_win_my * (1 - p_win_dealer)
raw_lose = (1 - p_win_my) * p_win_dealer
total_raw = raw_win + raw_lose
return raw_win / total_raw if total_raw > 0 else 0.5
3. 例(丸ごと)
以下のコードでユーザーからカード入力してから計算できます。
このコードを blackjack.py として保存し、ターミナルで python3 blackjack.py を実行してください。自分のカードとディーラーのカードを入力すると、正規化された勝率が表示されます。
# 1. カードの基本勝率を定義
card_probabilities = {
'A': 0.60, 'K': 0.55, 'Q': 0.55, 'J': 0.55, '10': 0.55,
'9': 0.54, '8': 0.52, '7': 0.50, '6': 0.48, '5': 0.45,
'4': 0.42, '3': 0.38, '2': 0.35
}
# 2. 計算ロジックを定義した関数
def calculate_win_probability(my_card, dealer_card, probabilities):
"""
2つのカード情報から、正規化された勝率を計算します。
"""
p_win_my = probabilities[my_card]
p_win_dealer = probabilities[dealer_card]
raw_win = p_win_my * (1 - p_win_dealer)
raw_lose = (1 - p_win_my) * p_win_dealer
total_raw = raw_win + raw_lose
return raw_win / total_raw if total_raw > 0 else 0.5
# 3. ユーザーからの入力を受け取り、計算と表示を繰り返す
while True:
my_card = input("自分のカードを入力してください: ").upper()
dealer_card = input("ディーラーのカードを入力してください: ").upper()
# 計算を実行
win_probability = calculate_win_probability(my_card, dealer_card, card_probabilities)
# 結果を表示
print(f"\n自分のカード: {my_card}, ディーラーのカード: {dealer_card}")
print(f" -> 正規化後の勝率: {win_probability:.2%}\n")
3. ベイズ的アプローチによる勝率計算
これまで、真の可能性を計算するための重要なピースを見落としていました。
もし私の勝率が60%だとしても、ディーラーがエースを引いた場合、私がエースを引く可能性は減りますね!
同様に、ディーラーが弱いカードを引けば、私がその弱いカードを引く可能性は減り、その分私の勝率は上がります。
より正確にするためには、ディーラーがエースを引いた確率を計算に組み込む必要があります。
ベイズの定理とは?
ベイズの定理は、以下の式で表されます。
P(H | E) = (P(E | H) * P(H)) / P(E)
-
P(H | E): 事後確率 (Posterior)
- 証拠 E が得られた後の、仮説 H の確率。私たちが求めたい「更新された確率」です。
-
P(H): 事前確率 (Prior)
- 証拠 E を得る前の、仮説 H の確率。私たちの初期の信念や知識です。
-
P(E | H): 尤度 (Likelihood)
- 仮説 H が真である場合に、証拠 E が観測される確率。
-
P(E): 証拠の確率 (Evidence)
- 証拠 E が観測される全体の確率。これは正規化のための定数として機能します。
ブラックジャックでの計算例
自分のカードが「A」だとわかっている状況を考えます。この時点での勝率は、前のセクションで使った表から 60% です。
これを事前確率とします。
- 仮説 (H): 自分がこのゲームに勝つ。
- 事前確率 P(H): P(Win) = 0.60
ここで、新しい情報として「ディーラーのアップカードが『6』だった」ことがわかりました。
この証拠 (E) を使って、勝率を更新してみましょう。
- 証拠 (E): ディーラーのカードが 6 である。
ベイズの定理を適用するために、必要な確率を整理します。
-
P(H) = P(Win) = 0.60
- すでに定義した、自分のカードが A である時点での勝率。
-
P(E | H) = P(Dealer has 6 | I Win)
- 「自分が勝つ」という条件下で、「ディーラーのカードが 6 である」確率。
- ディーラーが6を持っていることで私たちの勝率が約10%上昇すると仮定します。
正確に計算するのは複雑で、この例の範囲内で扱うと主旨がぼやけてしまうため、便宜的な値として設定しています。
(この値は、ディーラーの6がプレイヤーに有利な状況を生み、弱いカードが減り、ディーラーにとってバーストしやすくリスクを負うことを反映しています。)
-
P(E) = P(Dealer has 6)
- ディーラーのカードが 6 である全体の確率。デッキに 4 枚の 6 があるので、4/52 ≈ 0.077 です。
これらの値をベイズの定理の式に代入します。
P(Win | Dealer has 6) = (P(Dealer has 6 | Win) * P(Win)) / P(Dealer has 6)
= (0.10 * 0.60) / 0.077
= 0.06 / 0.077
≈ 0.779
結果の解釈
計算の結果、事後確率 P(H | E) は約 77.9% となりました。
- 更新前: 自分のカードが A という情報だけでは、勝率は 60% でした。
- 更新後: ディーラーのカードが 6 という新しい情報を加味した結果、勝率は 77.9% に上昇しました。
このように、ベイズの定理を使うことで、新しい情報が得られるたびに確率的な予測をより現実に近い形に更新していくことができます。これは、不確実な状況下で合理的な意思決定を行うための強力なツールとなります。
4. より複雑なモデルを扱うには(ライブラリの紹介)
今回の例のように、変数が少なく計算ロジックが明確な場合は、手計算による実装が非常に効果的です。しかし、より多くの変数が複雑に絡み合う問題を解きたい場合、専門のライブラリを利用するのが一般的です。
専門ライブラリの利点
確率モデルを扱うライブラリは、以下のようなタスクを自動化してくれます。
- モデル構築: 変数間の依存関係をグラフとして簡単に定義できます。
- 確率推論: ある事象が観測されたときに、他の事象の確率がどう変化するかを自動で計算します。
- 学習: データからモデルの構造や確率パラメータを学習することも可能です。
もし、より高度な確率モデリングに挑戦したい場合は、以下のようなライブラリの利用を検討してみてください。
-
pgmpy: Python で確率的グラフィカルモデルを扱うための代表的なライブラリです。モデル構築から推論まで幅広く対応しています。
- インストール: pip install pgmpy
- NumPyro や PyMC: より汎用的な確率的プログラミング言語(PPL)で、確率モデル全般を柔軟に記述できます。
まずは手計算で基本をしっかり理解し、その後でこれらのライブラリを使ってみると、その便利さをより深く実感できるでしょう。
補足: さらに学びを深めるために
より高度な確率推論の例を試してみたい方は、以下の GitHub リポジトリをご覧ください。
DNA 特性の確率計算
https://github.com/ChrisFletcher1989/dna_trait_calculation_agent
このプロジェクトでは、両親の遺伝子から特定の DNA 特性を持つ確率を計算します。
実装は Chris Fletcher 氏によるもので、ハーバード大学の CS50 AI コースの教材をもとにしています。
ぜひ フォーク(Fork) して、ご自身のシナリオで実験してみてください。
データや確率表を変更することで、異なる遺伝パターンをシミュレーションし、確率の変化を確認することができます。