重啟撲克機器人之路 -10:當數據處理變得不再可怕

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

今天開始著手處理對手模型訓練所需的數據。說實話,數據處理一直是我比較抗拒的部分 - 總覺得又繁瑣又無趣。但經過這幾年的經驗,我越來越清楚意識到:在機器學習中,數據的品質往往決定了成敗。就算你用了最先進的模型架構,餵進去的數據品質不佳,最終結果還是不會理想。

有趣的是,當我開始處理iPoker的XML格式牌局記錄時,發現事情並沒有想像中困難。回想幾年前第一次看到這些複雜的牌局記錄時,光是那些看不懂的程式碼就讓我望而卻步。但現在有了這幾年寫程式的經驗,再加上語言模型的協助,理解這些XML結構反而變得輕鬆許多。這種進步讓我感到欣慰 - 至少證明這幾年的摸索沒有白費。

目前的重點是要從這些牌局記錄中提取出有意義的數據,建立一個能預測玩家行為傾向的回歸模型。具體來說,就是分析在特定場景下(比如特定的牌面、底池大小、玩家位置等),玩家選擇raise、call或fold的機率分布。這讓我想到需要仔細梳理每個決策點的場景資訊,確保模型能"看到"玩家當下可見的所有資訊。

snapshot = { "round_no": round_no, "current_street": current_street, "blinds": blinds_info, "player_positions": positions, "player_stacks": stacks, "pot_size": pot_size, "board_cards": board_cards, "previous_actions": actions_history, "players_remaining": active_players, "is_button": is_button }

處理showdown資訊可能需要另外考慮 - 我還在思考是否該將其整合進同一個模型,還是分開處理會比較合適。這部分可能需要再多做一些研究和實驗。

經過今天的工作,我發現自己對數據處理的態度似乎有了微妙的改變。雖然還是不能說特別喜歡這個過程,但至少不再覺得那麼困難和可怕了。或許這就是所謂的成長吧 - 當你逐漸掌握了工具和方法,曾經令人生畏的事物也會變得平常。

這是我用來處理iPoker Hand History的程式碼:

import xml.etree.ElementTree as ET

import json

import re



def safe_float(text):

    """Convert a string to float after removing commas."""

    if text is None:

        return 0.0

    return float(re.sub(r",", "", text.strip()))



def parse_decision_logs(root, hero):

    """

    Parse the XML tree and, for every opponent decision (action) in rounds 1 and later

    (preflop and beyond), produce a snapshot log capturing the game state at the moment

    BEFORE the action is processed.

   

    In this snapshot:

      - The snapshot does NOT include any actions from round 0 (blinds/antes).

      - The re-indexed action counter is incremented for every action in rounds ≥ 1,

        so that no action gets a number of 0.

      - Only opponent actions are logged (the hero's actions are skipped).

      - The snapshot shows the acting opponent's hole cards (if available); the hero's

        or other players' hole cards are not revealed in the snapshot.

    """

    logs = []



    for game in root.findall('game'):

        general = game.find('general')

        if general is None:

            continue



        # --- Extract blinds and ante ---

        small_blind_elem = general.find('smallblind')

        big_blind_elem = general.find('bigblind')

        ante_elem = general.find('ante')

        small_blind = safe_float(small_blind_elem.text) if small_blind_elem is not None else 0.0

        big_blind = safe_float(big_blind_elem.text) if big_blind_elem is not None else 0.0

        ante = safe_float(ante_elem.text) if ante_elem is not None else 0.0



        # --- Process players ---

        players = {}        # key: player name, value: player info

        active_players = {} # tracks whether a player is still in the hand

        players_elem = general.find('players')

        if players_elem is None:

            continue



        for player_elem in players_elem.findall('player'):

            name = player_elem.attrib.get('name')

            seat = int(player_elem.attrib.get('seat'))

            is_dealer = (player_elem.attrib.get('dealer') == "1")

            chips = safe_float(player_elem.attrib.get('chips'))

            bet = safe_float(player_elem.attrib.get('bet'))

            win = safe_float(player_elem.attrib.get('win'))

            players[name] = {

                'seat': seat,

                'is_dealer': is_dealer,

                'chips': chips,

                'bet': bet,

                'win': win

            }

            active_players[name] = True



        # --- Determine relative positions ---

        # Sort players by seat number.

        sorted_players = sorted(players.items(), key=lambda item: item[1]['seat'])

        # Find the dealer (button). If none is marked, assume the first player is the dealer.

        dealer_index = None

        for i, (name, info) in enumerate(sorted_players):

            if info['is_dealer']:

                dealer_index = i

                break

        if dealer_index is None:

            dealer_index = 0



        button_player = sorted_players[dealer_index][0]

        small_blind_player = sorted_players[(dealer_index + 1) % len(sorted_players)][0]

        big_blind_player = sorted_players[(dealer_index + 2) % len(sorted_players)][0]



        # Assign relative position codes: small blind = 0, big blind = 1, button = 2, others = 3.

        player_positions = {}

        for name in players:

            if name == small_blind_player:

                players[name]['position_relative'] = 0

            elif name == big_blind_player:

                players[name]['position_relative'] = 1

            elif name == button_player:

                players[name]['position_relative'] = 2

            else:

                players[name]['position_relative'] = 3

            player_positions[name] = players[name]['position_relative']



        # Skip this game if the hero is not present.

        if hero not in players:

            continue



        # --- Initialize state variables ---

        pot_size = 0.0

        cumulative_actions = []  # will contain actions from rounds >= 1 only

        board_cards = []         # community cards seen so far

        pocket_cards = {}        # updated when encountering <cards type="Pocket">

        snapshot_action_counter = 0  # counter for actions in rounds >= 1



        # --- Process rounds in order (sorted by round number) ---

        rounds = game.findall('round')

        rounds = sorted(rounds, key=lambda r: int(r.attrib.get('no')))

        for r in rounds:

            round_no = int(r.attrib.get('no'))

            for elem in r:

                if elem.tag == 'cards':

                    card_type = elem.attrib.get('type')

                    if card_type == 'Pocket':

                        # Record pocket cards for the given player.

                        player = elem.attrib.get('player')

                        text = elem.text.strip() if elem.text else ""

                        cards = text.split()

                        # If cards are hidden (e.g., "X X"), record as unknown.

                        if any(card.upper().startswith("X") for card in cards):

                            pocket_cards[player] = ["unknown", "unknown"]

                        else:

                            pocket_cards[player] = cards

                    else:

                        # Community cards (flop, turn, river)

                        cards = elem.text.split() if elem.text else []

                        board_cards.extend(cards)

                elif elem.tag == 'action':

                    # Build an action dictionary.

                    action_details = {

                        'round': round_no,

                        'player': elem.attrib.get('player'),

                        'action_type': int(elem.attrib.get('type')),

                        'action_sum': safe_float(elem.attrib.get('sum'))

                    }



                    # For actions in round 0 (blinds/antes), update state only (do not record them).

                    if round_no < 1:

                        pot_size += action_details['action_sum']

                        # (If desired, you could update pocket_cards from round 0 as well.)

                        continue



                    # For actions in rounds >= 1, first assign a new re-indexed action number.

                    snapshot_action_counter += 1

                    # Copy the action_details and add the new action number.

                    action_details['action_no'] = snapshot_action_counter



                    # If the acting player is not the hero, produce a snapshot BEFORE processing the action.

                    # (The snapshot’s "previous_actions" will be the cumulative_actions so far.)

                    if action_details['player'] != hero:

                        is_button = (action_details['player'] == button_player)

                        actor_cards = pocket_cards.get(action_details['player'], ["unknown", "unknown"])

                        # Build the snapshot.

                        snapshot = {

                            "gamecode": game.attrib.get("gamecode"),

                            "round_no": round_no,

                            "current_street": (

                                "preflop" if round_no == 1 else

                                "flop" if round_no == 2 else

                                "turn" if round_no == 3 else

                                "river" if round_no == 4 else "unknown"

                            ),

                            "blinds": {

                                "small_blind": small_blind,

                                "big_blind": big_blind,

                                "ante": ante

                            },

                            "player_positions": player_positions,

                            "player_stacks": {name: players[name]['chips'] for name in players},

                            "pot_size": pot_size,  # state BEFORE processing this action

                            "board_cards": board_cards.copy(),

                            "previous_actions": cumulative_actions.copy(),  # only round>=1 actions so far

                            "action": action_details.copy(),

                            "players_remaining": sum(1 for active in active_players.values() if active),

                            "is_button": is_button,

                            "actor_hole_cards": pocket_cards.get(action_details['player'], ["unknown", "unknown"])

                        }

                        logs.append(snapshot)



                    # Now update the state: add this action to the cumulative history.

                    cumulative_actions.append(action_details)

                    pot_size += action_details['action_sum']

                    # Mark the actor as inactive if the action is a fold (we assume type==0 means fold).

                    if action_details['action_type'] == 0:

                        active_players[action_details['player']] = False



    return logs



if __name__ == '__main__':

    try:

        # Parse the XML file (adjust 'test.xml' to your filename)

        tree = ET.parse('test.xml')

        root = tree.getroot()



        # Determine the hero's nickname from the session-level <nickname> element.

        session_general = root.find('general')

        if session_general is not None and session_general.find('nickname') is not None:

            hero = session_general.find('nickname').text.strip()

        else:

            hero = ""  # fallback if not found



        # Parse opponent decision snapshots (skipping hero actions and round 0 actions)

        logs = parse_decision_logs(root, hero)

        print(json.dumps(logs, indent=4))

    except ET.ParseError as e:

        print("Error parsing XML file:", e)

留言
avatar-img
留言分享你的想法!
avatar-img
傑劉的沙龍
3會員
18內容數
傑劉的沙龍的其他內容
2025/03/16
記錄了對撲克數據庫程式碼的深入理解,以及如何通過精確的查詢獲得準確的分析結果。通過重新組織action type的分類,讓後續的數據分析變得更加高效。這個數據庫將是撲克機器人專案的重要組成部分,用於建立更精確的對手模型。
Thumbnail
2025/03/16
記錄了對撲克數據庫程式碼的深入理解,以及如何通過精確的查詢獲得準確的分析結果。通過重新組織action type的分類,讓後續的數據分析變得更加高效。這個數據庫將是撲克機器人專案的重要組成部分,用於建立更精確的對手模型。
Thumbnail
2025/03/14
記錄了在建構撲克數據庫過程中遇到的挑戰和收穫。探討了自建系統與現成工具的差異,以及如何確保數據準確性。同時反思了精確表達查詢需求的重要性,以及自建系統潛在的長期價值。
Thumbnail
2025/03/14
記錄了在建構撲克數據庫過程中遇到的挑戰和收穫。探討了自建系統與現成工具的差異,以及如何確保數據準確性。同時反思了精確表達查詢需求的重要性,以及自建系統潛在的長期價值。
Thumbnail
2025/03/13
記錄了在撲克機器人開發中從機器學習模型轉向建立自定義數據庫的過程,以及這個策略轉變背後的思考。通過分析真實玩家的行動分布,希望能訓練出更有效的撲克機器人。
Thumbnail
2025/03/13
記錄了在撲克機器人開發中從機器學習模型轉向建立自定義數據庫的過程,以及這個策略轉變背後的思考。通過分析真實玩家的行動分布,希望能訓練出更有效的撲克機器人。
Thumbnail
看更多
你可能也想看
Thumbnail
大家好,我是一名眼科醫師,也是一位孩子的媽 身為眼科醫師的我,我知道視力發展對孩子來說有多關鍵。 每到開學季時,診間便充斥著許多憂心忡忡的家屬。近年來看診中,兒童提早近視、眼睛疲勞的案例明顯增加,除了3C使用過度,最常被忽略的,就是照明品質。 然而作為一位媽媽,孩子能在安全、舒適的環境
Thumbnail
大家好,我是一名眼科醫師,也是一位孩子的媽 身為眼科醫師的我,我知道視力發展對孩子來說有多關鍵。 每到開學季時,診間便充斥著許多憂心忡忡的家屬。近年來看診中,兒童提早近視、眼睛疲勞的案例明顯增加,除了3C使用過度,最常被忽略的,就是照明品質。 然而作為一位媽媽,孩子能在安全、舒適的環境
Thumbnail
我的「媽」呀! 母親節即將到來,vocus 邀請你寫下屬於你的「媽」故事——不管是紀錄爆笑的日常,或是一直想對她表達的感謝,又或者,是你這輩子最想聽她說出的一句話。 也歡迎你曬出合照,分享照片背後的點點滴滴 ♥️ 透過創作,將這份情感表達出來吧!🥹
Thumbnail
我的「媽」呀! 母親節即將到來,vocus 邀請你寫下屬於你的「媽」故事——不管是紀錄爆笑的日常,或是一直想對她表達的感謝,又或者,是你這輩子最想聽她說出的一句話。 也歡迎你曬出合照,分享照片背後的點點滴滴 ♥️ 透過創作,將這份情感表達出來吧!🥹
Thumbnail
記錄了對撲克數據庫程式碼的深入理解,以及如何通過精確的查詢獲得準確的分析結果。通過重新組織action type的分類,讓後續的數據分析變得更加高效。這個數據庫將是撲克機器人專案的重要組成部分,用於建立更精確的對手模型。
Thumbnail
記錄了對撲克數據庫程式碼的深入理解,以及如何通過精確的查詢獲得準確的分析結果。通過重新組織action type的分類,讓後續的數據分析變得更加高效。這個數據庫將是撲克機器人專案的重要組成部分,用於建立更精確的對手模型。
Thumbnail
記錄了在建構撲克數據庫過程中遇到的挑戰和收穫。探討了自建系統與現成工具的差異,以及如何確保數據準確性。同時反思了精確表達查詢需求的重要性,以及自建系統潛在的長期價值。
Thumbnail
記錄了在建構撲克數據庫過程中遇到的挑戰和收穫。探討了自建系統與現成工具的差異,以及如何確保數據準確性。同時反思了精確表達查詢需求的重要性,以及自建系統潛在的長期價值。
Thumbnail
記錄了在撲克機器人開發中從機器學習模型轉向建立自定義數據庫的過程,以及這個策略轉變背後的思考。通過分析真實玩家的行動分布,希望能訓練出更有效的撲克機器人。
Thumbnail
記錄了在撲克機器人開發中從機器學習模型轉向建立自定義數據庫的過程,以及這個策略轉變背後的思考。通過分析真實玩家的行動分布,希望能訓練出更有效的撲克機器人。
Thumbnail
記錄了在開發過程中與LLM合作的經驗教訓,以及在資料處理和模型設計上的一些思考。特別強調了在開發過程中,有時看似繁瑣的基礎工作反而是最重要的。
Thumbnail
記錄了在開發過程中與LLM合作的經驗教訓,以及在資料處理和模型設計上的一些思考。特別強調了在開發過程中,有時看似繁瑣的基礎工作反而是最重要的。
Thumbnail
記錄了在改進數據處理過程中的一些突破,包括簡化行動分類、修正數據計算方式,以及從Random Forest到LSTM的轉變。同時也反思了過度追求完美反而可能限制進展的現象。
Thumbnail
記錄了在改進數據處理過程中的一些突破,包括簡化行動分類、修正數據計算方式,以及從Random Forest到LSTM的轉變。同時也反思了過度追求完美反而可能限制進展的現象。
Thumbnail
記錄了在開發撲克機器人對手模型時,如何與語言模型協作的心得,以及在這過程中對開發方法論的一些思考。特別強調了「先求有,再求好」的重要性,以及如何在保持開發效率和深入理解技術細節之間找到平衡。
Thumbnail
記錄了在開發撲克機器人對手模型時,如何與語言模型協作的心得,以及在這過程中對開發方法論的一些思考。特別強調了「先求有,再求好」的重要性,以及如何在保持開發效率和深入理解技術細節之間找到平衡。
Thumbnail
記錄了在開發撲克機器人時處理數據的心得,從最初對XML格式感到困惑,到現在能夠從容面對數據處理的轉變過程。反思了數據品質在機器學習project中的重要性,以及自己在程式開發能力上的進步
Thumbnail
記錄了在開發撲克機器人時處理數據的心得,從最初對XML格式感到困惑,到現在能夠從容面對數據處理的轉變過程。反思了數據品質在機器學習project中的重要性,以及自己在程式開發能力上的進步
Thumbnail
記錄了在分析200萬筆撲克歷史記錄時的思考過程,從最初被 Q-Learning 吸引,到理解其在不完整資訊遊戲中的侷限,最終決定轉向建立對手模型系統的過程。反映了在技術選擇時,如何在炫酷與實用之間找到平衡。
Thumbnail
記錄了在分析200萬筆撲克歷史記錄時的思考過程,從最初被 Q-Learning 吸引,到理解其在不完整資訊遊戲中的侷限,最終決定轉向建立對手模型系統的過程。反映了在技術選擇時,如何在炫酷與實用之間找到平衡。
Thumbnail
記錄了在開發撲克機器人時,從對機器學習模型的成功驗證,到意識到自己又回到solver策略老路的過程。最終決定改變方向,轉向分析實戰數據的心路歷程。
Thumbnail
記錄了在開發撲克機器人時,從對機器學習模型的成功驗證,到意識到自己又回到solver策略老路的過程。最終決定改變方向,轉向分析實戰數據的心路歷程。
Thumbnail
分享在撲克桌況辨識project中,從OCR文字辨識到程式碼重構的過程,以及如何在技術實作中尋找平衡點的心得。
Thumbnail
分享在撲克桌況辨識project中,從OCR文字辨識到程式碼重構的過程,以及如何在技術實作中尋找平衡點的心得。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News