戰(zhàn):不完美信息博弈的環(huán)境搭建與自對弈訓(xùn)練)
簡介斗地主RL模型1 是一份聚焦斗地主人工智能的PDF技術(shù)資料面向研究強(qiáng)化學(xué)習(xí)、不完整信息博弈或棋牌AI的開發(fā)者。資源以1個(gè)PDF文件壓縮為1.12MB系統(tǒng)梳理了斗地主RL建模的關(guān)鍵難點(diǎn)行動(dòng)空間龐大時(shí)的BFS搜索與Top100牌組集合、不同身份與階段的動(dòng)作價(jià)值估計(jì)、對手手牌推斷等并完整給出叫地主模型、斗地主模型、評(píng)估器模型的三模型架構(gòu)以及兩個(gè)核心建模假設(shè)、重要性采樣、stacked-attention與transformer cross channels等實(shí)現(xiàn)細(xì)節(jié)。資料還包含實(shí)驗(yàn)環(huán)境、無數(shù)據(jù)自對抗訓(xùn)練、訓(xùn)練時(shí)長和TODO清單便于讀者理解從任務(wù)分析到模型構(gòu)建的完整思路。目前已有222人學(xué)習(xí)適合希望借鑒實(shí)戰(zhàn)方案設(shè)計(jì)深度強(qiáng)化學(xué)習(xí)智能體的工程師參考。1. 斗地主RL模型不完美信息博弈為什么讓強(qiáng)化學(xué)習(xí)集體翻車斗地主 RL 模型核心不是“用強(qiáng)化學(xué)習(xí)把牌打完”而是解決一類典型問題三人博弈、兩副手牌不可見、合法動(dòng)作組合爆炸、自對弈目標(biāo)還在隨對手漂移。很多人照著 AlphaGo 的思路搭了一套 RL樹搜索結(jié)果在手牌不全可見、對手還是隊(duì)友的牌局里完全跑不動(dòng)訓(xùn)練損失像心電圖一樣抖。我見過不少工程項(xiàng)目在環(huán)境、特征和獎(jiǎng)勵(lì)設(shè)計(jì)上先后翻車真正能落地的斗地主 RL 模型往往是“規(guī)則引擎 靜態(tài)特征 蒙特卡洛自對弈”這套輕量組合。后面按一線落地順序拆開講先搭環(huán)境與合法動(dòng)作再定動(dòng)作編碼與獎(jiǎng)勵(lì)然后選算法訓(xùn)練最后給校驗(yàn)方法和關(guān)鍵踩坑適合已經(jīng)有 RL 基礎(chǔ)、正在選型或已經(jīng)訓(xùn)不動(dòng)的工程師照著復(fù)現(xiàn)。2. 搭建環(huán)境與狀態(tài)表征先讓一局牌能算清賬2.1 為什么斗地主環(huán)境不能直接拿來用很多人第一步就去找現(xiàn)成的斗地主環(huán)境走上正軌的不多。Gym 生態(tài)里不是沒有撲克環(huán)境但大多集中在德州、二十一一點(diǎn)這類固定回合的游戲斗地主里“順子、連對、飛機(jī)、四帶二、春天翻倍”這些規(guī)則實(shí)現(xiàn)不完整的環(huán)境一抓一大把。判斷一個(gè)環(huán)境合不合格不是看它能不能跑完一局而是看兩種場景手里有 3456789 時(shí)能不能拆出順子和單張兩種合法動(dòng)作上家出了三個(gè) 2 帶一對你三個(gè) A 能不能合法壓住。常見的做法是拿開源斗地主解析庫做底子自己補(bǔ)環(huán)境接口或者干脆自己花半天時(shí)間重寫一版規(guī)則判斷畢竟 54 張牌的邏輯并不復(fù)雜后面訓(xùn)練出了問題也容易定位。自研或改造時(shí)環(huán)境至少要暴露 reset、step、get_legal_actions、is_terminal 四個(gè)接口缺一個(gè)后面都會(huì)卡手。這個(gè)階段最容易忽視的是“重復(fù)動(dòng)作”問題。同一個(gè)手牌組合既可以被枚舉成三帶一也可以被枚舉成單張加三張的單牌拆分如果不做去重模型明明拿著同樣的牌卻被送進(jìn)兩個(gè)不同動(dòng)作 ID梯度會(huì)互相打架。所以動(dòng)作枚舉時(shí)一定要按“牌型 主牌點(diǎn)數(shù) 副牌點(diǎn)數(shù)”三元組去重只保留一組動(dòng)作 ID。這一步做得干凈后面合法動(dòng)作掩碼才不容易翻車。2.2 狀態(tài)表征手牌、歷史與未知牌的三段式編碼斗地主的觀測核心是把自己的 17 張手牌、另兩家打出的歷史牌、目前場上沒出現(xiàn)過的牌以及當(dāng)前輪次按序拼成一個(gè)定長向量。最樸素的做法是把 15 種點(diǎn)數(shù)3 到 A、2、小王、大王配 4 種花色編碼成 15×4 的 0/1 矩陣標(biāo)記“我手里有沒有這張牌”。第二段是兩家對手分別累計(jì)打出的牌同樣用 15×4 的矩陣記錄誰打的放哪個(gè)通道不能混在一起否則模型分不清哪張是上家管的、哪張是下家送的。第三段是全場出現(xiàn)過的牌減去手牌和歷史牌的差值這部分其實(shí)隱含了另兩家手牌的分布信息統(tǒng)計(jì)出來直接拼進(jìn)特征里。我一般會(huì)把當(dāng)前輪到誰出牌也做成 3 維 one-hot再帶一個(gè)歸一化到 [0,1] 的輪次數(shù)。整條特征拼完大約兩百維MLP 完全吃得消。不建議直接用 54 維 one-hot 表示手牌因?yàn)槟菢訒?huì)讓模型自己從稀疏向量里做加法去還原張數(shù)信息白白浪費(fèi)擬合能力收斂也慢。def encode_state(hand_cards, played_by_me, played_by_opp1, played_by_opp2, current_player, round_index): # 15 種點(diǎn)數(shù)按 3,4,5,6,7,8,9,10,J,Q,K,A,2,小王,大王 排列 # 4 種花色對應(yīng) 0~3不區(qū)分大小只做占位 hand np.zeros((15, 4), dtypenp.float32) for card in hand_cards: rank rank_to_idx[card.rank] # 0~14 hand[rank, card.suit] 1.0 hist1 np.zeros((15, 4), dtypenp.float32) for card in played_by_opp1: hist1[rank_to_idx[card.rank], card.suit] 1.0 hist2 np.zeros((15, 4), dtypenp.float32) for card in played_by_opp2: hist2[rank_to_idx[card.rank], card.suit] 1.0 unseen np.clip(1.0 - hand - hist1 - hist2, 0.0, 1.0) # 未出現(xiàn)的牌張數(shù)統(tǒng)計(jì) turn np.zeros(3, dtypenp.float32) turn[current_player] 1.0 return np.concatenate([ hand.flatten(), hist1.flatten(), hist2.flatten(), unseen.flatten(), turn, [round_index / 20.0], ])這段代碼的邏輯說明unseen 是“互補(bǔ)信息”通道它告訴網(wǎng)絡(luò)另外兩家手里大概率捏著什么牌型。手牌和歷史牌都是離散事實(shí)只有 unseen 是推斷但它對斗地主特別重要比如 unseen 里 A 數(shù)量多模型就該知道對子 A 很可能被拆在兩家手里。rank_to_idx 的映射順序要固定訓(xùn)練和推理必須用同一張表round_index 除以 20 是把輪次壓到 0~1 區(qū)間防止特征尺度把 MLP 的第一層權(quán)重帶到極端值。另一個(gè)注意點(diǎn)是 unseen 的減法如果直接用整副牌減去手牌減歷史牌會(huì)出現(xiàn)負(fù)數(shù)所以 np.clip 保底。2.3 合法動(dòng)作生成枚舉牌型與掩碼實(shí)現(xiàn)斗地主的動(dòng)作空間不是固定的而是每一輪根據(jù)當(dāng)前手牌和上家牌實(shí)時(shí)生成的。不少初學(xué)項(xiàng)目把動(dòng)作輸出設(shè)計(jì)成固定幾百維每輪把非法動(dòng)作概率置零結(jié)果模型在大部分非法動(dòng)作上學(xué)到一堆噪聲。正確做法是先把當(dāng)前手牌能組成的全部合法牌型枚舉出來再用上一手牌過濾出能壓住的部分最后把動(dòng)作 ID 列表轉(zhuǎn)成一份掩碼網(wǎng)絡(luò)只在掩碼上選動(dòng)作。def generate_legal_actions(hand, last_moveNone): # hand: 元素為 (rank, suit) 的列表 # last_move: (action_type, main_rank, kicker_rank) 或 None count_by_rank defaultdict(int) for card in hand: count_by_rank[card.rank] 1 actions [] # 基礎(chǔ)牌型單張、對子、三條、炸彈 for rank, cnt in count_by_rank.items(): if cnt 1: actions.append((single, rank, -1)) if cnt 2: actions.append((pair, rank, -1)) if cnt 3: actions.append((triple, rank, -1)) if cnt 4: actions.append((bomb, rank, -1)) # 順子從 3 到 A連續(xù)長度 5枚舉起點(diǎn)和終點(diǎn) # 連對、飛機(jī)、三帶一/三帶二、四帶二同理按 rank 連續(xù)段生成 # 去重并過濾上家出牌后只保留嚴(yán)格更大的動(dòng)作 actions list(set(actions)) if last_move is not None: actions [a for a in actions if beat(a, last_move)] # 動(dòng)作 ID 映射每種牌型一個(gè)固定區(qū)間例如 single 0~14, pair 15~29 # 順子/連對/飛機(jī)用 hash(起點(diǎn),長度) 落到預(yù)留段 return [action_to_id[a] for a in actions]這段代碼把動(dòng)作枚舉縮小到“從手牌聚合結(jié)果出發(fā)”不需要逐張遞歸。真正的工程難點(diǎn)在順子、連對、飛機(jī)這三種跨度動(dòng)作。常見誤區(qū)是只枚舉最長順子但手牌 3456789模型必須既能出全長順子也能出 34567 單組順子否則策略空間被強(qiáng)行截?cái)嗤瑯?4567 和 45678 如果都合法就必須各算一個(gè)動(dòng)作不能用“長度相同視為等價(jià)”來偷懶。action_to_id 要提前把單個(gè)、對子、三條這類基礎(chǔ)牌型固定在低區(qū)間把順子、連對這類組合牌型映射到高區(qū)間保證動(dòng)作空間不隨著手牌變化而改變位置訓(xùn)練時(shí)掩碼才能對齊。3. 動(dòng)作編碼與獎(jiǎng)勵(lì)塑形把牌感裝進(jìn)網(wǎng)絡(luò)3.1 獨(dú)熱編碼為何收斂不動(dòng)斗地主的動(dòng)作空間不像連續(xù)控制那樣天然帶結(jié)構(gòu)。如果策略頭直接把全部動(dòng)作 ID 打成幾百維獨(dú)熱模型看到的是互相獨(dú)立的符號(hào)對子 3 和對子 4 沒有任何共享表征三帶一和三帶二也被當(dāng)作完全不同的東西。它需要從零開始數(shù)據(jù)學(xué)出“對子 3 和對子 4 只是點(diǎn)數(shù)不同、行為同樣是把兩張牌一起出”的規(guī)律。一個(gè) 15 點(diǎn)數(shù)的斗地主牌型和點(diǎn)數(shù)之間的組合關(guān)系足以讓模型訓(xùn)練前期浪費(fèi)大量時(shí)間。更麻煩的是合法動(dòng)作掩碼每輪都在變獨(dú)熱輸出層會(huì)把概率分散到幾百個(gè)位置上梯度信號(hào)被拉得特別稀薄訓(xùn)練起來像玄學(xué)。這個(gè)問題的本質(zhì)是模型缺少先驗(yàn)。Deep Q 系列方法能在圍棋上收斂得益于棋盤天然的空間局部性而斗地主的牌型關(guān)系是離散組合的。給策略頭喂入先驗(yàn)是比堆網(wǎng)絡(luò)深度更直接有效的手段。3.2 分槽位動(dòng)作頭與先驗(yàn)偏置可解釋的合法出牌引導(dǎo)一種被驗(yàn)證有效的做法是把動(dòng)作輸出頭從“一個(gè)大 softmax”拆成“牌型、主牌、副牌”三個(gè)槽位。牌型槽位輸出當(dāng)前動(dòng)作屬于哪種類型主牌槽位輸出是哪個(gè)點(diǎn)數(shù)的牌副牌槽位只在三帶一、三帶二這類動(dòng)作里生效。三個(gè)槽位分別算 logits最后再合成完整動(dòng)作空間。貝葉斯先驗(yàn)體現(xiàn)在手牌里沒有某張牌這個(gè)點(diǎn)數(shù)的 logits 就應(yīng)該被壓到極低手里有三張 A三帶一的 A 主牌槽位就應(yīng)該有更高的基數(shù)。這個(gè)先驗(yàn)不需要網(wǎng)絡(luò)訓(xùn)練學(xué)初始化時(shí)直接用手牌掩碼乘一個(gè)加權(quán)系數(shù)加上去即可。def policy_head(features, hand_mask, last_move_mask): # features: 狀態(tài)編碼輸出例如 128 維 # hand_mask: (15,) 每張主牌是否可用 # last_move_mask: 預(yù)生成的動(dòng)作級(jí)掩碼shape 與完整動(dòng)作空間一致 x F.relu(fc1(features)) # 128 - 256 type_logits fc_type(x) # 牌型槽位長度 牌型總數(shù) rank_logits fc_rank(x) # 主牌槽位長度 15 kicker_logits fc_kicker(x) # 副牌槽位長度 15 # 先驗(yàn)偏置手里真正有的牌對應(yīng)槽位 logits 加一個(gè)固定正數(shù) prior_bias 0.8 rank_logits rank_logits hand_mask * prior_bias kicker_logits kicker_logits hand_mask * prior_bias # 組合成完整動(dòng)作空間并套用合法動(dòng)作掩碼 logits combine_slots(type_logits, rank_logits, kicker_logits) logits torch.where(last_move_mask 0, logits, torch.full_like(logits, -1e9)) return F.log_softmax(logits, dim-1)邏輯說明hand_mask 是 2.2 節(jié)里 hand 矩陣按點(diǎn)數(shù)聚合后的 15 維向量代表“我手里有沒有這張牌”。prior_bias 取 0.8作用是讓網(wǎng)絡(luò)一開始就偏向選自己手里有的牌而不是靠海量探索去發(fā)現(xiàn)這個(gè)規(guī)律。combine_slots 把三個(gè)槽位的外積映射回完整動(dòng)作 ID 空間對每個(gè)動(dòng)作 ID反查它是哪種牌型、主牌點(diǎn)數(shù)、副牌點(diǎn)數(shù)再把三個(gè) logits 相加。這個(gè)映射表必須和生成合法動(dòng)作的 action_to_id 用同一套半點(diǎn)不能差。torch.where 的寫法比直接把合法位置置一個(gè)大正數(shù)更穩(wěn)非法位置變成 -1e9softmax 后概率接近 0合法位置數(shù)值不變。這個(gè)掩碼在訓(xùn)練時(shí)用上一輪 last_move 生成推理時(shí)也要走同一份代碼保持完全一致。3.3 獎(jiǎng)勵(lì)塑形一局只有勝負(fù)其實(shí)就夠了斗地主訓(xùn)練里最常見的翻車是加中間獎(jiǎng)勵(lì)。有的人給“每一步壓住對手”加正獎(jiǎng)勵(lì)有的人給“自己手牌剩得少”加負(fù)獎(jiǎng)勵(lì)結(jié)果模型要么變成不出牌的龜縮流要么變成死攥炸彈不放的自私鬼。原因是這些塑形信號(hào)與最終勝負(fù)并不完全一致智能體總會(huì)找到一條“最小風(fēng)險(xiǎn)”路徑去刷中間分?jǐn)?shù)而不是為終局勝利服務(wù)。我一般只按整局結(jié)束給 ±1農(nóng)民陣營贏兩個(gè)農(nóng)民各得 1地主贏地主 1兩個(gè)農(nóng)民各 -1。不在任何中間步驟給獎(jiǎng)勵(lì)這是 DMC 類方法能在斗地主上站穩(wěn)的關(guān)鍵前提。如果想讓模型學(xué)到“盡快走完手牌”的意識(shí)可以做一個(gè)溫和的附加項(xiàng)終局時(shí)給勝方的額外 0.1 作為打牌速度加分但要先在固定對手上開一組對照實(shí)驗(yàn)確認(rèn)它不會(huì)扭曲最終勝負(fù)。相比之下剩一張牌和剩五張牌之間的“剩余牌數(shù)”懲罰看似合理其實(shí)會(huì)讓模型學(xué)成“留一張單牌不出拖到底”——局部最優(yōu)整體崩盤。4. 算法選型與自對弈訓(xùn)練從 DMC 到蒙特卡洛回歸4.1 三個(gè)候選方案怎么選斗地主 RL 常見實(shí)現(xiàn)路徑有三條NFSPNeural Fictitious Self-Play、DMCDeep Monte Carlo、以及“模型池 蒙特卡洛回傳”的簡化自對弈。NFSP 需要維護(hù)凍結(jié)的過去策略平均網(wǎng)絡(luò)和兩套回放緩沖用于反事實(shí)推理工程復(fù)雜度高適合真正打比賽級(jí)別的系統(tǒng)它的收益在不完美信息天梯里其實(shí)最明顯但團(tuán)隊(duì)如果沒有兩周以上的調(diào)參時(shí)間很容易在平均策略和當(dāng)前策略之間調(diào)崩。DMC 最簡潔整局采樣終局勝負(fù)回傳等價(jià)于把一步的 TD 目標(biāo)全部換成整局真實(shí)回報(bào)訓(xùn)練穩(wěn)定性好。第三種是大多數(shù)團(tuán)隊(duì)最終實(shí)際采用的形態(tài)保留 DMC 的蒙特卡洛回傳同時(shí)維護(hù)一個(gè)歷史模型池持續(xù)輪換對手抑制過擬合。方法核心思想適合場景NFSP維護(hù)平均策略網(wǎng)絡(luò)做反事實(shí)推理有大量調(diào)參時(shí)間、追求頂級(jí)牌力DMC整局蒙特卡洛回歸不用 TD bootstrap快速驗(yàn)證特征和獎(jiǎng)勵(lì)設(shè)計(jì)模型池蒙特卡洛定期換對手多智能體交替大多數(shù)工程項(xiàng)目的最終形態(tài)沒有足夠算力時(shí)不要硬上 NFSP。簡化自對弈先把特征和獎(jiǎng)勵(lì)驗(yàn)證明白后面再換復(fù)雜方法也不遲。4.2 主干訓(xùn)練循環(huán)整局采樣 端到端回傳在這個(gè)環(huán)節(jié)我用的模式是隨機(jī)洗牌開局確定性記錄當(dāng)前智能體和固定的對手池打完整局用終局勝負(fù)回傳更新價(jià)值網(wǎng)絡(luò)。下面這段偽代碼是能直接進(jìn)項(xiàng)目的主干循環(huán)細(xì)節(jié)按自己環(huán)境接口微調(diào)即可。for episode in range(total_episodes): # 每 500 局從模型池里換一次對手池里保存過去多個(gè) checkpoints if episode % 500 0: opponent pick_opponent_from_pool() seed episode * 7919 13 # 同類實(shí)驗(yàn)固定種子可復(fù)現(xiàn) trajectory, result play_one_episode(env, current_actor, opponent, seed) # 農(nóng)民陣營共享勝負(fù)兩個(gè)農(nóng)民同贏同輸 reward 1.0 if current_actor_won(result) else -1.0 # 整條軌跡每步都存同一個(gè) reward這就是 DMC 的核心 for obs, mask, act in trajectory: replay.append(Transition(obs, mask, act, reward)) if len(replay) 1000: batch replay.sample(batch_size256) pred_q q_net(batch.obs, batch.mask).gather(1, batch.act) loss F.mse_loss(pred_q, batch.reward) optim.zero_grad() loss.backward() optim.step()邏輯說明注意這些轉(zhuǎn)移全部共享同一個(gè)終點(diǎn) reward這正是 DMC 與 DQN 的差異。DQN 會(huì)用 Q(s) 做 bootstrap 目標(biāo)在斗地主這種玩家手牌分布復(fù)雜的環(huán)境里早期 Q 值本身充滿噪聲bootstrap 會(huì)讓誤差逐輪放大直接使用整局真實(shí)勝負(fù)等于讓每一步的決策都直接面向最終目標(biāo)做監(jiān)督。seed 固定是為了讓同一模型在不同實(shí)驗(yàn)里能對照復(fù)現(xiàn)如果種子不確定你根本分不清是模型進(jìn)步還是起手牌變好。模型池的作用在第 5 章會(huì)展開它可以防止策略只贏一個(gè)固定對手。參數(shù)上學(xué)習(xí)率 1e-4、batch 256 是一個(gè)穩(wěn)妥起點(diǎn)從 0.01 起步會(huì)把 loss 打到 NaN 的概率大幅升高。temperature 退火從 0.3 降到 0.05探索與利用的節(jié)奏參照常見自對弈做法。4.3 要不要顯式建模對手手牌有一類方案會(huì)額外輸出“另兩家手牌的概率分布”用輔助任務(wù)做對手建模。對斗地主來說這層預(yù)測是個(gè)黑匣子兩張不可見手牌的分布范圍極大預(yù)測誤差會(huì)被強(qiáng)化學(xué)習(xí)目標(biāo)放大而且輔助任務(wù)梯度會(huì)跟主任務(wù)搶網(wǎng)絡(luò)容量。我的常見做法是不單獨(dú)建模在第 2.2 節(jié)的狀態(tài)表征里已經(jīng)用 unseen 向量攜帶了“未出現(xiàn)牌張數(shù)統(tǒng)計(jì)”這已經(jīng)是一種隱式建模只有當(dāng)模型在純自對弈里進(jìn)入平臺(tái)期才考慮加對手牌型預(yù)測并且用“只在特征提取器上回傳、不讓策略頭被輔助任務(wù)污染”的方式接進(jìn)去否則容易變成兩個(gè)任務(wù)互相打架。5. 斗地主RL訓(xùn)練避坑記錄五條血淚經(jīng)驗(yàn)5.1 三個(gè)智能體同時(shí)訓(xùn)練損失不降反升現(xiàn)象讓三個(gè) RL 智能體在同一個(gè)環(huán)境里同時(shí)更新訓(xùn)了兩千局損失曲線像心電圖一樣抖動(dòng)誰也沒學(xué)會(huì)出牌邏輯。原因三個(gè)策略都在變每個(gè)智能體的對手都在動(dòng)Q 值永遠(yuǎn)在追趕一個(gè)不存在靜止目標(biāo)價(jià)值函數(shù)沒有可收斂的固定點(diǎn)。解決固定兩個(gè)對手可以一個(gè)是隨機(jī)策略一個(gè)是歷史 checkpoint只更新當(dāng)前智能體升級(jí)到模型池后也要保證池里模型是不參與梯度更新的凍結(jié)版本。如果想跑三方聯(lián)合進(jìn)化必須用聯(lián)盟訓(xùn)練里的優(yōu)先級(jí)采樣那一套普通項(xiàng)目直接三人同步訓(xùn)練幾乎必崩。5.2 合法動(dòng)作掩碼導(dǎo)致 loss 變成 NaN現(xiàn)象加 -1e9 掩碼后第一輪訓(xùn)練輸出 NaN。原因mask 里合法位置被置成 0導(dǎo)致所有 logits 都被抹掉softmax 分母為 0更隱蔽的是 torch.where 的寫法里非法位置的數(shù)量覆蓋了整個(gè)動(dòng)作空間合法位置全是 -1e9。解決千萬別把 mask 直接乘到 logits 上要用加法掩碼并且寫一個(gè)計(jì)數(shù)器檢查每個(gè) batch 里是否至少有一個(gè)動(dòng)作位置的掩碼為 1。還要確認(rèn)“pass”這個(gè)動(dòng)作永遠(yuǎn)合法手里任何牌都能不出否則在對手出牌后模型會(huì)有輪次完全沒動(dòng)作可選。5.3 中途獎(jiǎng)勵(lì)讓模型變成龜縮流現(xiàn)象加了“每一步壓住對手獎(jiǎng)勵(lì) 0.1”后模型學(xué)會(huì)了不出牌、不接牌全靠隊(duì)友把牌走完自己最后才出。原因這個(gè)中間獎(jiǎng)勵(lì)與最終勝負(fù)不一致模型發(fā)現(xiàn)“不動(dòng)”是風(fēng)險(xiǎn)最低的路徑同時(shí)還能撿到少量分?jǐn)?shù)于是為了刷分犧牲終局。解決刪掉所有中間獎(jiǎng)勵(lì)只讓終局勝負(fù)做唯一監(jiān)督如果一定要給進(jìn)度信號(hào)把它放進(jìn)終局判定的附加項(xiàng)里并且做一個(gè) A/B 對照驗(yàn)證添加項(xiàng)后對固定對手的勝率不降再保留。5.4 過擬合到固定對手換人即翻車現(xiàn)象在固定對手上勝率到了 60%換上另一個(gè)風(fēng)格不同的對手勝率跌到 30%。原因智能體記住了訓(xùn)練對手的出牌習(xí)慣比如“對子壓到 K 就收手”“手里有小王優(yōu)先接單”這種習(xí)慣并不能泛化到所有玩家。解決訓(xùn)練中定期從模型池里隨機(jī)采樣對手每隔五百局換一次評(píng)估時(shí)保留一組“從未參與訓(xùn)練”的對手模型用它單獨(dú)測勝率這才是模型真實(shí)牌力的標(biāo)尺。一個(gè)合格的模型池至少要有五到八個(gè)不同階段的模型讓當(dāng)前策略不斷面對陌生打法。5.5 發(fā)牌種子方差太大評(píng)估玄學(xué)現(xiàn)象同一個(gè)模型用不同隨機(jī)種子各跑一百局勝率在 45% 到 65% 之間跳來跳去。原因斗地主起手牌影響特別大炸彈位置和手牌張數(shù)分布會(huì)讓牌局產(chǎn)生極端結(jié)果只跑少量局?jǐn)?shù)時(shí)勝率指標(biāo)完全被運(yùn)氣覆蓋。解決評(píng)估時(shí)固定一組預(yù)生成的發(fā)牌種子比如 1000 個(gè)多個(gè)對手模型上取平均訓(xùn)練時(shí)不要看單局日志單獨(dú)保留最近 500 局移動(dòng)平均勝率拿這個(gè)值判斷是否在進(jìn)步。這條經(jīng)驗(yàn)我翻了兩次車之后才落實(shí)成固定流程。6. 用人類出牌記錄做監(jiān)督預(yù)熱讓RL模型起步更穩(wěn)6.1 用歷史對局先教會(huì)模型“像個(gè)正常人”隨機(jī)初始化的策略在斗地主里連基本出牌邏輯都要摸索訓(xùn)練前期大量局?jǐn)?shù)被浪費(fèi)在“學(xué)會(huì)不出錯(cuò)牌”這件事上。常見做法是收集人類斗地主的出牌記錄把每一步的狀態(tài)、合法掩碼、人類動(dòng)作拼成監(jiān)督數(shù)據(jù)集先做模仿學(xué)習(xí)預(yù)熱。這個(gè)預(yù)熱不需要太多數(shù)據(jù)幾十上百局完整對局的去重動(dòng)作就能看出效果核心收益是讓模型在進(jìn)入 RL 前已經(jīng)知道“接牌大、拆牌穩(wěn)、單牌要留著壓”這類人類基本牌感后續(xù) RL 只需要在局部策略上做微調(diào)。# 監(jiān)督預(yù)熱復(fù)用斗地主 RL 模型的策略頭換成交叉熵?fù)p失 for batch in human_data_loader: obs, mask, human_action batch # human_action 必須合法動(dòng)作 ID log_probs policy_head(obs, mask) # 復(fù)用強(qiáng)化學(xué)習(xí)同一份代碼 loss F.nll_loss(log_probs, human_action) optim.zero_grad() loss.backward() optim.step()邏輯說明這里的關(guān)鍵是復(fù)用第 3.2 節(jié)同一個(gè)策略頭代碼不要另起模型否則 RL 階段冷啟動(dòng)的收益就歸零。熱啟動(dòng)學(xué)習(xí)率用 1e-4batch 取 128 或 256訓(xùn)練 2000 到 5000 步后停止停得太早預(yù)熱不足停太久模型會(huì)被人類打法鎖死失去 RL 自對弈探索的余地。預(yù)熱結(jié)束后切回 RL 訓(xùn)練循環(huán)時(shí)把探索溫度調(diào)高到 0.3 左右讓模型先跳出純模仿再逐步退火到 0.05。6.2 三個(gè)驗(yàn)證視角動(dòng)作合法性、固定種子勝率與關(guān)鍵決策復(fù)盤驗(yàn)證一個(gè)斗地主 RL 模型我不會(huì)只看訓(xùn)練損失。第一步是統(tǒng)計(jì)測試局里非法動(dòng)作數(shù)量必須為 0這能確認(rèn)動(dòng)作掩碼和策略頭的一致性第二步是固定 1000 個(gè)發(fā)牌種子和從未參與訓(xùn)練的對手模型池各跑若干局取平均勝率這個(gè)數(shù)字才值得寫進(jìn)實(shí)驗(yàn)記錄第三步是回放幾局關(guān)鍵決策比如對手剩兩張牌時(shí)模型會(huì)不會(huì)拆對子壓牌隊(duì)友明顯缺門時(shí)模型會(huì)不會(huì)主動(dòng)送牌。模型能在這三關(guān)都站住才算真正可以進(jìn)天梯或業(yè)務(wù)場景。我自己習(xí)慣是任何新實(shí)驗(yàn)先寫合法動(dòng)作的單元測試再進(jìn)監(jiān)督預(yù)熱最后才碰強(qiáng)化學(xué)習(xí)目標(biāo)這套順序就算后邊 RL 目標(biāo)調(diào)崩了策略也不至于徹底放飛。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取