重啟撲克機器人之路 -11:有時AI也是挺難搞的

更新於 發佈於 閱讀時間約 32 分鐘
raw-image

原本以為處理資料清理和特徵編碼會是個簡單的任務,實際做起來卻發現即使是與語言模型合作,也處處藏著意想不到的挑戰。

一開始在處理撲克牌的編碼時還算順利,將rank和suit轉換成數值讓機器學習模型可以訓練。但當我請語言模型幫我設計訓練模型時,卻發現它漏掉了一些我認為相當重要的特徵 - 比如每位玩家的stack size、完整的牌面資訊(它只單純使用了牌的數量而非具體的rank和suit),最重要的是它完全忽略了previous actions這個關鍵特徵,既使我不斷地重複要求將其放入其中。最後將每個步驟拆解到相當小,一步一步要求才完成。

這讓我想起之前在處理語言模型時的一個重要領悟:與其深入鑽研他提供的每一段程式碼細節,不如先確保程式能夠運作,即使可能還不是最理想的狀態。這種方式和我過去的開發習慣有很大的不同。以前我總是試圖完全理解每個function、每個action的邏輯,但這在與語言模型協作時反而成為了一種障礙 - 畢竟它的寫作邏輯和風格往往與我們習慣的不同,花太多時間深入理解反而可能是在浪費精力,特別是當那個方向最後證明是個死胡同的時候。

最初的測試結果:

__________________________________________________________________________________________________ Layer (type) Output Shape Param # Connected to ================================================================================================== seq_types_input (InputLayer) [(None, 20)] 0 __________________________________________________________________________________________________ seq_amounts_input (InputLayer) [(None, 20, 1)] 0 __________________________________________________________________________________________________ static_input (InputLayer) [(None, 384)] 0 __________________________________________________________________________________________________ action_type_embedding (Embeddin (None, 20, 16) 416 seq_types_input[0][0] __________________________________________________________________________________________________

...

Epoch 20/20 399180/399180 [==============================] - 136s 342us/sample
- loss: 0.6324 - acc: 0.6971 - val_loss: 0.7220 - val_acc: 0.6883 99795/99795 [==============================] - 8s 76us/sample - loss: 0.7220 - acc: 0.6883 Validation Loss: 0.7220 | Validation Accuracy: 0.6883

雖然單一行動的預測並不是我的主要目標(我更在意模型能否準確預測在特定情況下對手各種可能行動的機率分布),但這個結果至少證明了這個方向是可行的。即使我對機器學習還不夠熟悉,可能還看不懂很多統計指標的細節,但這是個好的開始。

這次的經驗再次證明,在使用語言模型輔助開發時,最重要的是先得到一個可以運作的版本,然後再逐步改進。這種方式不僅能讓開發更有效率,也能避免在可能是死路的方向上投入過多時間。

我模型訓練的程式碼:

#!/usr/bin/env python3

import json

import numpy as np

from sklearn.model_selection import train_test_split

import tensorflow as tf

from tensorflow.keras.models import Model

from tensorflow.keras.layers import (

Input, Dense, LSTM, Embedding, Concatenate, Dropout

)

from tensorflow.keras.optimizers import Adam



# =============================================================================

# 1. Helper Functions for Feature Encoding

# =============================================================================



def one_hot_round(round_no):

"""One-hot encode round number (1=preflop, 2=flop, 3=turn, 4=river)."""

vec = np.zeros(4)

if 1 <= round_no <= 4:

vec[round_no - 1] = 1

return vec



def one_hot_position(pos, max_players=10):

"""One-hot encode a player's position (an integer in [0, max_players-1])."""

vec = np.zeros(max_players)

if pos < max_players:

vec[pos] = 1

return vec



def card_to_onehot(card):

"""

Convert a card string (e.g., 'S4', 'HA', 'C10') to a 52-dim one-hot vector.

If the card is hidden (e.g., starts with 'X') it returns an all-zeros vector.

"""

ranks = ['A','2','3','4','5','6','7','8','9','10','J','Q','K']

suits = ['S','H','D','C']

onehot = np.zeros(52)

if card is None or card.upper().startswith("X"):

return onehot

suit = card[0]

rank = card[1:]

if suit in suits and rank in ranks:

suit_index = suits.index(suit)

rank_index = ranks.index(rank)

index = suit_index * 13 + rank_index

onehot[index] = 1

return onehot



def encode_board_cards(board_cards, max_cards=5):

"""

Encode the board cards as the concatenation of one-hot vectors (52 dims each).

Pads with zeros if there are fewer than max_cards.

"""

encoded = []

for card in board_cards:

encoded.append(card_to_onehot(card))

while len(encoded) < max_cards:

encoded.append(np.zeros(52))

return np.concatenate(encoded[:max_cards])



def encode_hole_cards(hole_cards):

"""

Encode the player's (or actor's) hole cards (expected to be a list of 2 cards)

as a concatenation of two 52-dim one-hot vectors.

"""

encoded = []

for card in hole_cards:

encoded.append(card_to_onehot(card))

while len(encoded) < 2:

encoded.append(np.zeros(52))

return np.concatenate(encoded[:2])



# =============================================================================

# 2. Process Each Snapshot into Model Inputs and Target

# =============================================================================



def process_snapshot(snapshot):

"""

From a snapshot dictionary, create:

- A vector of static features

- A sequence of previous actions (each with an action type and amount)

- The target (opponent's current action type)

**Static features include:**

- One-hot encoded round (4 dims)

- Pot size (1 dim; scaled)

- Blinds: small, big, ante (3 dims; scaled)

- Actor stack (1 dim; scaled)

- Actor position (one-hot, 10 dims)

- Number of players remaining (1 dim; scaled)

- Board cards (5 fixed cards × 52 dims = 260 dims)

- Actor hole cards (2 cards × 52 dims = 104 dims)

**Sequential features:**

For each previous action (up to a fixed max length) we use:

- action_type (integer; offset by +1 so that 0 is reserved for padding)

- action_sum (float; scaled)

"""

# --- Static features ---

# 1. Round (from current action)

round_no = int(snapshot["action"]["round"])

round_vec = one_hot_round(round_no)

# 2. Pot size (scale by 100)

pot_size = np.array([float(snapshot["pot_size"]) / 100.0])

# 3. Blinds and ante (scaled)

blinds = snapshot.get("blinds", {})

small_blind = float(blinds.get("small_blind", 0)) / 100.0

big_blind = float(blinds.get("big_blind", 0)) / 100.0

ante = float(blinds.get("ante", 0)) / 100.0

blinds_vec = np.array([small_blind, big_blind, ante])

# 4. Actor stack size (scale by 1000)

actor_stack = np.array([float(snapshot.get("actor_stack_size", 0)) / 1000.0])

# 5. Actor position (one-hot with dimension 10)

actor_pos = int(snapshot.get("actor_position", 0))

pos_vec = one_hot_position(actor_pos, max_players=10)

# 6. Number of players remaining (scale by 10)

players_remaining = np.array([float(snapshot.get("players_remaining", 0)) / 10.0])

# 7. Board cards (5 fixed cards)

board_vec = encode_board_cards(snapshot.get("board_cards", []), max_cards=5)

# 8. Actor hole cards (2 cards)

hole_cards_vec = encode_hole_cards(snapshot.get("actor_hole_cards", []))

# Concatenate all static features:

# Total dims: 4 + 1 + 3 + 1 + 10 + 1 + 260 + 104 = 384

static_features = np.concatenate([

round_vec, pot_size, blinds_vec, actor_stack, pos_vec,

players_remaining, board_vec, hole_cards_vec

])

# --- Sequential features ---

# For each previous action, we take:

# - action_type (offset by +1 so that 0 is our pad value)

# - action_sum (scaled by 100)

seq_actions = snapshot.get("previous_actions", [])

seq_types = []

seq_amounts = []

for action in seq_actions:

act_type = int(action.get("action_type", 0)) + 1 # reserve 0 for padding

act_sum = float(action.get("action_sum", 0)) / 100.0

seq_types.append(act_type)

seq_amounts.append(act_sum)

MAX_SEQ_LENGTH = 20 # maximum number of previous actions to consider

# Truncate if too long

seq_types = seq_types[:MAX_SEQ_LENGTH]

seq_amounts = seq_amounts[:MAX_SEQ_LENGTH]

# Pad sequences (pad type=0, which for action_type will be masked in the Embedding layer)

while len(seq_types) < MAX_SEQ_LENGTH:

seq_types.append(0)

seq_amounts.append(0.0)

seq_types = np.array(seq_types, dtype=np.int32)

seq_amounts = np.array(seq_amounts, dtype=np.float32).reshape((MAX_SEQ_LENGTH, 1))

# --- Target: Opponent's current action type (as integer) ---

target = int(snapshot["action"].get("action_type", 0))

return static_features, seq_types, seq_amounts, target



# =============================================================================

# 3. Load and Preprocess Data

# =============================================================================



def load_and_preprocess_data(json_filename):

"""

Load snapshot logs from a JSON file and create training arrays.

The JSON file is expected to be a list of snapshots.

"""

with open(json_filename, 'r') as f:

data = json.load(f)

static_features_list = []

seq_types_list = []

seq_amounts_list = []

targets = []

for snapshot in data:

static_feat, seq_types, seq_amounts, target = process_snapshot(snapshot)

static_features_list.append(static_feat)

seq_types_list.append(seq_types)

seq_amounts_list.append(seq_amounts)

targets.append(target)

X_static = np.stack(static_features_list) # shape: (N, 384)

X_seq_types = np.stack(seq_types_list) # shape: (N, MAX_SEQ_LENGTH)

X_seq_amounts = np.stack(seq_amounts_list) # shape: (N, MAX_SEQ_LENGTH, 1)

y = np.array(targets, dtype=np.int32) # shape: (N,)

return X_static, X_seq_types, X_seq_amounts, y



# Change the filename below to your JSON file produced by your XML parser.

JSON_FILENAME = 'logs.json'

X_static, X_seq_types, X_seq_amounts, y = load_and_preprocess_data(JSON_FILENAME)



# (Optional) Check the shapes of your training arrays:

print("X_static shape:", X_static.shape)

print("X_seq_types shape:", X_seq_types.shape)

print("X_seq_amounts shape:", X_seq_amounts.shape)

print("y shape:", y.shape)



# Split into training and validation sets

X_static_train, X_static_val, X_seq_types_train, X_seq_types_val, X_seq_amounts_train, X_seq_amounts_val, y_train, y_val = train_test_split(

X_static, X_seq_types, X_seq_amounts, y, test_size=0.2, random_state=42

)



# =============================================================================

# 4. Build the Keras Model

# =============================================================================



# Parameters for the sequential branch

MAX_SEQ_LENGTH = 20

# Adjust NUM_ACTION_TYPES based on your data (here we assume 25; update if needed)

NUM_ACTION_TYPES = 25

EMBEDDING_DIM = 16

NUM_ACTION_CLASSES = 30 # number of distinct action types to predict (update as needed)



# -- Static Input Branch --

static_input = Input(shape=(384,), name='static_input')

x_static = Dense(128, activation='relu')(static_input)

x_static = Dense(64, activation='relu')(x_static)



# -- Sequential Input Branch --

# Input for action types (integers; shape = (MAX_SEQ_LENGTH,))

seq_types_input = Input(shape=(MAX_SEQ_LENGTH,), dtype='int32', name='seq_types_input')

# Input for action amounts (floats; shape = (MAX_SEQ_LENGTH, 1))

seq_amounts_input = Input(shape=(MAX_SEQ_LENGTH, 1), dtype='float32', name='seq_amounts_input')



# Process action types with an Embedding layer.

# (We use mask_zero=True so that padded 0 values are ignored.)

x_seq_types = Embedding(

input_dim=NUM_ACTION_TYPES + 1, # +1 to reserve index 0 for padding

output_dim=EMBEDDING_DIM,

mask_zero=True,

name='action_type_embedding'

)(seq_types_input)



# Process the amounts with a simple dense layer (applied to each time step).

x_seq_amounts = Dense(8, activation='relu', name='amount_dense')(seq_amounts_input)



# Concatenate along the feature dimension: now each time step has (EMBEDDING_DIM + 8) features.

x_seq = Concatenate(name='seq_concat')([x_seq_types, x_seq_amounts])



# Process the concatenated sequence with an LSTM.

x_seq = LSTM(64, name='lstm_seq')(x_seq)



# -- Merge Both Branches --

x = Concatenate(name='merge')([x_static, x_seq])

x = Dense(64, activation='relu')(x)

x = Dropout(0.5)(x)

output = Dense(NUM_ACTION_CLASSES, activation='softmax', name='output')(x)



model = Model(

inputs=[static_input, seq_types_input, seq_amounts_input],

outputs=output

)



model.compile(

optimizer=Adam(learning_rate=1e-3),

loss='sparse_categorical_crossentropy',

metrics=['accuracy']

)



model.summary()



# =============================================================================

# 5. Train the Model

# =============================================================================



history = model.fit(

x={

'static_input': X_static_train,

'seq_types_input': X_seq_types_train,

'seq_amounts_input': X_seq_amounts_train

},

y=y_train,

validation_data=(

{

'static_input': X_static_val,

'seq_types_input': X_seq_types_val,

'seq_amounts_input': X_seq_amounts_val

},

y_val

),

epochs=20,

batch_size=32

)



# =============================================================================

# 6. Evaluate / Save the Model

# =============================================================================



loss, acc = model.evaluate(

x={

'static_input': X_static_val,

'seq_types_input': X_seq_types_val,

'seq_amounts_input': X_seq_amounts_val

},

y=y_val

)

print(f"Validation Loss: {loss:.4f} | Validation Accuracy: {acc:.4f}")



# Optionally, save your model:

model.save("opponent_model.h5")

avatar-img
2會員
12內容數
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
傑劉的沙龍 的其他內容
記錄了在開發撲克機器人時處理數據的心得,從最初對XML格式感到困惑,到現在能夠從容面對數據處理的轉變過程。反思了數據品質在機器學習project中的重要性,以及自己在程式開發能力上的進步
記錄了在分析200萬筆撲克歷史記錄時的思考過程,從最初被 Q-Learning 吸引,到理解其在不完整資訊遊戲中的侷限,最終決定轉向建立對手模型系統的過程。反映了在技術選擇時,如何在炫酷與實用之間找到平衡。
記錄了在開發撲克機器人時,從對機器學習模型的成功驗證,到意識到自己又回到solver策略老路的過程。最終決定改變方向,轉向分析實戰數據的心路歷程。
記錄了放棄使用大型語言模型作為撲克機器人核心的決定過程,以及新的混合策略方案的構思。文章探討了技術選擇的考量因素,並回顧了過去開發經驗帶來的啟發。
記錄了在開發撲克牌AI時,從機器學習到大型語言模型的技術選擇過程,以及對各種可能解決方案的思考與權衡。
記錄了在開發撲克牌辨識系統時遇到的按鈕辨識挑戰,以及從中學到的debug思維和版本控制重要性。文章分享了技術解決方案的演進過程,也反思了個人開發習慣需要改進的地方。
記錄了在開發撲克機器人時處理數據的心得,從最初對XML格式感到困惑,到現在能夠從容面對數據處理的轉變過程。反思了數據品質在機器學習project中的重要性,以及自己在程式開發能力上的進步
記錄了在分析200萬筆撲克歷史記錄時的思考過程,從最初被 Q-Learning 吸引,到理解其在不完整資訊遊戲中的侷限,最終決定轉向建立對手模型系統的過程。反映了在技術選擇時,如何在炫酷與實用之間找到平衡。
記錄了在開發撲克機器人時,從對機器學習模型的成功驗證,到意識到自己又回到solver策略老路的過程。最終決定改變方向,轉向分析實戰數據的心路歷程。
記錄了放棄使用大型語言模型作為撲克機器人核心的決定過程,以及新的混合策略方案的構思。文章探討了技術選擇的考量因素,並回顧了過去開發經驗帶來的啟發。
記錄了在開發撲克牌AI時,從機器學習到大型語言模型的技術選擇過程,以及對各種可能解決方案的思考與權衡。
記錄了在開發撲克牌辨識系統時遇到的按鈕辨識挑戰,以及從中學到的debug思維和版本控制重要性。文章分享了技術解決方案的演進過程,也反思了個人開發習慣需要改進的地方。
你可能也想看
Google News 追蹤
Thumbnail
身為一個小資女,一日之始在於起床。 每天早上起床,最先面對的就是被陽光曝曬的空間場景。 如何用既懶散又不失優雅的姿態完美的伸個懶腰後踮腳下床,著實是門學問。 重點不在於自己那一席披頭散髮,也不是因為打呵欠而扭曲的容顏。 而是在於陽光滲進空間的角度與濃度。 不能太多,直接曝曬像吸血鬼一樣花容
Thumbnail
用 AI 製作一張專屬巴黎奧運的紀念海報吧~
Thumbnail
AI繪圖要廣泛用於商用還有一大段路,還需要依賴人類的經驗判斷、調整,為什麼呢?
Thumbnail
透過玩桌遊的方式學習AI生成圖像技巧,在充滿樂趣的活動中,學會如何操作工具及生成圖像技巧。這款遊戲預計下個月將開設實體課程,適合所有對AI有興趣的人、AI繪圖新手及藝術愛好者。
Thumbnail
AlphaGo 的開發,讓人工智慧在圍棋的研究讓更多人被看到,也看到它成熟的結果。現代的圍棋教學和棋手訓練,也或多或少會借鏡各類的AI系統做學習。然而,教學的歷程,過度追求AI的棋步和棋法,有時會讓小朋友難以理解。一步登天的方式,有時反而會讓同學走得更坎坷。
Thumbnail
最新的AI趨勢讓人眼花撩亂,不知要如何開始學習?本文介紹了作者對AI的使用和體驗,以及各類AI工具以及推薦的選擇。最後強調了AI是一個很好用的工具,可以幫助人們節省時間並提高效率。鼓勵人們保持好奇心,不停止學習,並提出了對健康生活和開心生活的祝福。
Thumbnail
如何運用A I這個工具,以人為本,不是讓AI主導你的人生。
大語言模型能夠生成文本,因此被認為是生成式人工智慧的一種形式。 人工智慧的學科任務,是製作機器,使其能執行需要人類智慧才能執行的任務,例如理解語言,便是模式,做出決策。 除了大語言模型,人工智慧也包含了深度學習以及機器學習。 機器學習的學科任務,是透過演算法來實踐AI。 特別
Thumbnail
延續上週提到的,「有哪些不訓練模型的情況下,能夠強化語言模型的能力」,這堂課接續介紹其中第 3、4 個方法
Thumbnail
最近和朋友討論AI,朋友提到了跟上AI議題、學習AI工具的難點: 雖然知道有各種AI工具,但不知道哪裡會用得到。 工具演變這麼迅速,如果現在學,工具一下子又更新,就又得重新學習,好像永遠都跟不上。 如果AI幫我做了很多事情,那我要做什麼?
Thumbnail
大家最近從AI AlphaGo打敗棋王, 開始陸續新聞一直報導, 到最近不管是AI繪圖,AI Chatgpt,AI coplit...
Thumbnail
身為一個小資女,一日之始在於起床。 每天早上起床,最先面對的就是被陽光曝曬的空間場景。 如何用既懶散又不失優雅的姿態完美的伸個懶腰後踮腳下床,著實是門學問。 重點不在於自己那一席披頭散髮,也不是因為打呵欠而扭曲的容顏。 而是在於陽光滲進空間的角度與濃度。 不能太多,直接曝曬像吸血鬼一樣花容
Thumbnail
用 AI 製作一張專屬巴黎奧運的紀念海報吧~
Thumbnail
AI繪圖要廣泛用於商用還有一大段路,還需要依賴人類的經驗判斷、調整,為什麼呢?
Thumbnail
透過玩桌遊的方式學習AI生成圖像技巧,在充滿樂趣的活動中,學會如何操作工具及生成圖像技巧。這款遊戲預計下個月將開設實體課程,適合所有對AI有興趣的人、AI繪圖新手及藝術愛好者。
Thumbnail
AlphaGo 的開發,讓人工智慧在圍棋的研究讓更多人被看到,也看到它成熟的結果。現代的圍棋教學和棋手訓練,也或多或少會借鏡各類的AI系統做學習。然而,教學的歷程,過度追求AI的棋步和棋法,有時會讓小朋友難以理解。一步登天的方式,有時反而會讓同學走得更坎坷。
Thumbnail
最新的AI趨勢讓人眼花撩亂,不知要如何開始學習?本文介紹了作者對AI的使用和體驗,以及各類AI工具以及推薦的選擇。最後強調了AI是一個很好用的工具,可以幫助人們節省時間並提高效率。鼓勵人們保持好奇心,不停止學習,並提出了對健康生活和開心生活的祝福。
Thumbnail
如何運用A I這個工具,以人為本,不是讓AI主導你的人生。
大語言模型能夠生成文本,因此被認為是生成式人工智慧的一種形式。 人工智慧的學科任務,是製作機器,使其能執行需要人類智慧才能執行的任務,例如理解語言,便是模式,做出決策。 除了大語言模型,人工智慧也包含了深度學習以及機器學習。 機器學習的學科任務,是透過演算法來實踐AI。 特別
Thumbnail
延續上週提到的,「有哪些不訓練模型的情況下,能夠強化語言模型的能力」,這堂課接續介紹其中第 3、4 個方法
Thumbnail
最近和朋友討論AI,朋友提到了跟上AI議題、學習AI工具的難點: 雖然知道有各種AI工具,但不知道哪裡會用得到。 工具演變這麼迅速,如果現在學,工具一下子又更新,就又得重新學習,好像永遠都跟不上。 如果AI幫我做了很多事情,那我要做什麼?
Thumbnail
大家最近從AI AlphaGo打敗棋王, 開始陸續新聞一直報導, 到最近不管是AI繪圖,AI Chatgpt,AI coplit...