化學(xué)習(xí)與DQN實(shí)戰(zhàn))
簡(jiǎn)介基于深度強(qiáng)化學(xué)習(xí)的德州撲克AI算法優(yōu)化項(xiàng)目面向計(jì)算機(jī)、電子信息工程、數(shù)學(xué)等專業(yè)的高校學(xué)生可作為課程設(shè)計(jì)、期末大作業(yè)或畢業(yè)設(shè)計(jì)的參考資料。項(xiàng)目以不完全信息博弈為場(chǎng)景重點(diǎn)展示強(qiáng)化學(xué)習(xí)算法在策略決策中的建模與優(yōu)化思路。壓縮包為zip格式共165個(gè)文件約13.96MB含58個(gè)py源碼文件、48個(gè)pth模型權(quán)重文件、18個(gè)txt說(shuō)明文件、18個(gè)csv訓(xùn)練記錄、16個(gè)pkl數(shù)據(jù)文件及若干json配置等目錄結(jié)構(gòu)清晰。目前已有325人學(xué)習(xí)瀏覽。除完整Python實(shí)現(xiàn)與項(xiàng)目說(shuō)明文檔外還附帶訓(xùn)練好的模型權(quán)重可直接加載運(yùn)行或繼續(xù)優(yōu)化csv與pkl文件記錄了訓(xùn)練中的性能變化與中間結(jié)果有助于復(fù)盤(pán)調(diào)參思路、分析收斂情況對(duì)復(fù)現(xiàn)或改進(jìn)德州撲克AI算法具有較強(qiáng)參考價(jià)值。1. 從隨機(jī)決策到策略收斂這份德州撲克 AI 源碼到底改了什么很多人下載德州撲克 AI 項(xiàng)目第一件事就是拉去和電腦對(duì)打贏了幾把就以為算法跑通了。這個(gè)項(xiàng)目如果只拿來(lái)玩就虧了。它不是一個(gè)能讓你在牌桌上穩(wěn)贏的“作弊器”而是一套完整的深度強(qiáng)化學(xué)習(xí)訓(xùn)練系統(tǒng)從牌局狀態(tài)提取、動(dòng)作離散化、獎(jiǎng)勵(lì)塑形到 DQN 網(wǎng)絡(luò)更新、經(jīng)驗(yàn)回放、目標(biāo)網(wǎng)絡(luò)同步再到自博弈式訓(xùn)練閉環(huán)源碼里全都有。適合三類人做畢業(yè)設(shè)計(jì)需要完整工科閉環(huán)的學(xué)生、想入門(mén)深度強(qiáng)化學(xué)習(xí)但不想只跑 CartPole 的開(kāi)發(fā)者、以及想在有限時(shí)間里把 DQN 調(diào)出真實(shí)博弈效果的算法愛(ài)好者。這套代碼的價(jià)值在于它把德州撲克里最麻煩的部分——不完整信息博弈——變成了一個(gè)可訓(xùn)練、可復(fù)現(xiàn)、可觀察收斂過(guò)程的工程任務(wù)。你從這份資源里能拿到的不只是會(huì)打牌的模型而是一套“怎么讓智能體在決策邊界模糊的場(chǎng)景里學(xué)會(huì)策略”的范本。2. 先建模再訓(xùn)練狀態(tài)特征、動(dòng)作空間與回報(bào)函數(shù)的設(shè)計(jì)2.1 特征工程是這套 AI 的第一道分水嶺德州撲克不像圍棋棋盤(pán)信息完全可見(jiàn)。玩家只能看到自己的兩張底牌和公共牌對(duì)手的牌是隱藏的還涉及加注、棄牌、詐唬等心理博弈。如果把原始牌面直接丟給網(wǎng)絡(luò)模型根本學(xué)不會(huì)。這個(gè)項(xiàng)目把每輪牌局的狀態(tài)提取成一組固定維度的向量我在閱讀源碼時(shí)梳理了核心特征組合手牌強(qiáng)度基于兩張底牌評(píng)估的初始牌力分?jǐn)?shù)歸一化到 0~1底池比例當(dāng)前底池與玩家籌碼量的比值反映風(fēng)險(xiǎn)敞口街位信息當(dāng)前是翻牌前、翻牌、轉(zhuǎn)牌還是河牌用 one-hot 編碼對(duì)手動(dòng)作歷史最近 N 輪對(duì)手的加注、跟注、棄牌行為的統(tǒng)計(jì)特征玩家自身籌碼量絕對(duì)值和相對(duì)底池的比例用于控制激進(jìn)程度特征維度在 20~40 之間具體取決于歷史窗口長(zhǎng)度。這樣設(shè)計(jì)的好處是把一局牌的動(dòng)態(tài)過(guò)程壓縮成一個(gè)固定形狀的張量網(wǎng)絡(luò)輸入層就不需要處理變長(zhǎng)序列整體訓(xùn)練效率提升明顯。特征歸一化是這套代碼比較細(xì)致的地方所有特征在進(jìn)網(wǎng)絡(luò)前都經(jīng)過(guò) min-max 或者 z-score 處理避免某一維數(shù)值過(guò)大導(dǎo)致梯度震蕩。2.2 動(dòng)作空間裁剪為什么不是所有合法動(dòng)作都保留德州撲克每輪合法動(dòng)作數(shù)量很多尤其是加注可以加任意數(shù)量籌碼。如果動(dòng)作空間是連續(xù)的DQN 根本沒(méi)法直接輸出 Q 值。這個(gè)項(xiàng)目對(duì)動(dòng)作空間做了離散化裁剪源碼里常見(jiàn)的是把動(dòng)作分成四類# 動(dòng)作空間定義示例 ACTION_FOLD 0 # 棄牌放棄本輪損失已投入的籌碼 ACTION_CHECK 1 # 過(guò)牌不追加籌碼等待下一家動(dòng)作 ACTION_CALL 2 # 跟注匹配當(dāng)前最高下注額 ACTION_RAISE 3 # 加注按底池比例增加下注如 0.5 倍底池代碼邏輯不難理解但關(guān)鍵在參數(shù)敏感性。加注比例如果設(shè)成固定值模型容易在激進(jìn)和保守之間跳變?nèi)绻O(shè)成動(dòng)態(tài)倍數(shù)區(qū)間比如 0.5~2 倍底池之間隨機(jī)選擇訓(xùn)練出的策略會(huì)更平滑。我自己的經(jīng)驗(yàn)是離散動(dòng)作空間寧多勿少但每多一個(gè)動(dòng)作網(wǎng)絡(luò)輸出維度就加一訓(xùn)練所需樣本量也成倍上漲。項(xiàng)目里默認(rèn)的四動(dòng)作方案是在收斂速度和策略表達(dá)力之間取平衡的做法。2.3 回報(bào)函數(shù)只贏底池不夠關(guān)鍵是抗風(fēng)險(xiǎn)如果只是“贏了給正獎(jiǎng)勵(lì)輸了給負(fù)獎(jiǎng)勵(lì)”模型會(huì)陷入一種局面拿到好牌就瘋狂加注拿到爛牌就棄牌完全不考慮詐唬和試探性跟注。這套代碼的回報(bào)函數(shù)寫(xiě)得相對(duì)精細(xì)它會(huì)綜合底池變化、對(duì)手籌碼消耗、牌局階段三個(gè)因素來(lái)設(shè)定即時(shí)獎(jiǎng)勵(lì)而不是單純用勝負(fù)結(jié)算。獎(jiǎng)勵(lì)塑形是這套項(xiàng)目的核心亮點(diǎn)之一。源碼里會(huì)把“從對(duì)手棄牌中贏下的底池”和“從攤牌中贏下的底池”分開(kāi)計(jì)算后續(xù)你會(huì)看到這樣分鍋對(duì)策略風(fēng)格的收斂很重要。另外還加了一個(gè)小懲罰項(xiàng)用于抑制每局無(wú)意義的頻繁加注這在自博弈階段非常關(guān)鍵——沒(méi)有懲罰機(jī)制的話兩個(gè) AI 會(huì)互相試探到天荒地老。3. 讓 DQN 真正跑起來(lái)訓(xùn)練主循環(huán)、經(jīng)驗(yàn)回放與目標(biāo)網(wǎng)絡(luò)細(xì)節(jié)3.1 訓(xùn)練主循環(huán)拆解每一步在做什么項(xiàng)目核心訓(xùn)練腳本采用的是 DQN 的標(biāo)準(zhǔn)流程但加了自博弈Self-Play的變體。訓(xùn)練主循環(huán)大致是當(dāng)前策略模型與歷史策略池中的對(duì)手模型對(duì)局對(duì)局過(guò)程中產(chǎn)生的狀態(tài)轉(zhuǎn)移數(shù)據(jù)存入經(jīng)驗(yàn)池再?gòu)闹胁蓸有∨扛庐?dāng)前網(wǎng)絡(luò)。整個(gè)過(guò)程反復(fù)迭代。# 訓(xùn)練主循環(huán)核心邏輯簡(jiǎn)化版 for episode in range(total_episodes): state env.reset() # 初始化一局牌獲取初始狀態(tài) while not episode_done: # epsilon-greedy 策略前期多探索后期多利用 action select_action(state, epsilon) next_state, reward, done env.step(action) store_transition(state, action, reward, next_state, done) if len(replay_buffer) batch_size: batch replay_buffer.sample(batch_size) loss update_q_network(batch) # 計(jì)算 TD 誤差并反向傳播 if episode % target_update_freq 0: sync_target_network() # 每 N 局同步一次目標(biāo)網(wǎng)絡(luò) state next_state epsilon max(min_epsilon, epsilon * epsilon_decay)這段代碼是項(xiàng)目的骨架。select_action里用到的 epsilon-greedy 有兩種常見(jiàn)實(shí)現(xiàn)線性衰減和指數(shù)衰減這個(gè)項(xiàng)目使用的是指數(shù)衰減權(quán)重更新更平滑但前期探索充分度稍低。store_transition寫(xiě)入的是五元組可以用deque維護(hù)固定容量滿了就覆蓋最舊數(shù)據(jù)。update_q_network是核心計(jì)算部分下面單獨(dú)展開(kāi)。3.2 優(yōu)先經(jīng)驗(yàn)回放不是配角它決定了收斂速度項(xiàng)目采用的經(jīng)驗(yàn)回放機(jī)制值得單獨(dú)說(shuō)。普通的 DQN 用均勻采樣但撲克局里大量數(shù)據(jù)是“平淡無(wú)奇”的——沒(méi)什么大底池、沒(méi)什么驚險(xiǎn)抉擇這類數(shù)據(jù)對(duì)學(xué)習(xí)幫助有限。這套代碼用了優(yōu)先經(jīng)驗(yàn)回放Prioritized Experience Replay, PER即每個(gè)轉(zhuǎn)移樣本帶一個(gè)優(yōu)先級(jí)權(quán)重TD 誤差越大下次被采樣到的概率越高。# 優(yōu)先經(jīng)驗(yàn)回放采樣邏輯 class PrioritizedReplayBuffer: def __init__(self, capacity, alpha0.6): self.buffer deque(maxlencapacity) self.alpha alpha # 優(yōu)先級(jí)指數(shù)0 為均勻采樣1 為完全按優(yōu)先級(jí) def push(self, transition, td_error): priority (abs(td_error) 1e-6) ** self.alpha self.buffer.append((transition, priority)) def sample(self, batch_size, beta0.4): # 按優(yōu)先級(jí)概率抽樣beta 用于重要性采樣權(quán)重修正 probs [p for _, p in self.buffer] total sum(probs) probabilities [p / total for p in probs] indices random.choices(range(len(self.buffer)), probabilities, kbatch_size) return [self.buffer[i][0] for i in indices], indices這里alpha參數(shù)決定優(yōu)先級(jí)的影響力beta是重要性采樣權(quán)重用來(lái)修正因?yàn)榉蔷鶆虿蓸訋?lái)的估計(jì)偏差。新手最容易踩的坑是把a(bǔ)lpha拉到 1.0 之上導(dǎo)致某些 TD 誤差大的樣本被反復(fù)采樣網(wǎng)絡(luò)陷入局部震蕩。我一般習(xí)慣設(shè)在 0.6 左右跑 2 萬(wàn)局之后逐步調(diào)高。需要說(shuō)明的是td_error本身就是計(jì)算 Q 值更新時(shí)附帶得出的中間量并不需要額外算一遍所以 PER 帶來(lái)的計(jì)算開(kāi)銷很小。這個(gè)項(xiàng)目在這一點(diǎn)上做得比較干凈值得學(xué)習(xí)。3.3 目標(biāo)網(wǎng)絡(luò)同步頻率與學(xué)習(xí)率兩個(gè)超參數(shù)決定成敗DQN 里最經(jīng)典的 hack 是目標(biāo)網(wǎng)絡(luò)Target Network。如果更新 Q 值用的還是當(dāng)前網(wǎng)絡(luò)的預(yù)測(cè)值就會(huì)出現(xiàn)“自己追自己尾巴”的問(wèn)題訓(xùn)練震蕩極其劇烈。項(xiàng)目的做法是維護(hù)一個(gè)結(jié)構(gòu)完全相同但參數(shù)延遲同步的網(wǎng)絡(luò)每target_update_freq步把當(dāng)前網(wǎng)絡(luò)權(quán)重復(fù)制過(guò)去。# 目標(biāo)網(wǎng)絡(luò)同步邏輯 if episode % target_update_freq 0: target_net.load_state_dict(policy_net.state_dict())target_update_freq設(shè)太小目標(biāo)網(wǎng)絡(luò)更新的太快會(huì)退化成沒(méi)有目標(biāo)網(wǎng)絡(luò)訓(xùn)練不穩(wěn)定設(shè)太大目標(biāo) Q 值過(guò)于陳舊收斂變慢。參數(shù)設(shè)置的常見(jiàn)區(qū)間是每 500~2000 局同步一次對(duì)應(yīng)到每步更新頻率在 mm 數(shù)量級(jí)。我在跑這個(gè)項(xiàng)目時(shí)習(xí)慣的做法是先固定學(xué)習(xí)率為 0.0001Adam 優(yōu)化器跑 5000 局觀察 loss 曲線如果 loss 波動(dòng)范圍超過(guò) 0.05再調(diào)小學(xué)習(xí)率或調(diào)大同步頻率。4. 避坑與優(yōu)化自博弈滯后、獎(jiǎng)勵(lì)爆炸與評(píng)估陷阱4.1 自博弈訓(xùn)練范式為什么 AI 和 AI 打容易跑偏這個(gè)項(xiàng)目采用了自博弈訓(xùn)練即當(dāng)前策略會(huì)和它自己的歷史版本對(duì)局。自博弈讓模型越來(lái)越適應(yīng)對(duì)手的行為模式訓(xùn)練出的策略往往非常鋒利但也容易“鉆空子”——比如發(fā)現(xiàn)某種固定套路能穩(wěn)定戰(zhàn)勝歷史弱對(duì)手后策略會(huì)朝那個(gè)方向過(guò)度收斂而不是學(xué)習(xí)通用打法。源碼里對(duì)此的常見(jiàn)處理是維護(hù)一個(gè)歷史對(duì)手池每次隨機(jī)抽取一個(gè)對(duì)手來(lái)對(duì)局而不是只和當(dāng)前最新策略打。這套代碼里歷史策略池的容量和替換策略是控制收斂質(zhì)量的旋鈕。策略池太大訓(xùn)練速度變慢太小又容易退化到“只針對(duì)當(dāng)前自己”的扎堆博弈。我跑出來(lái)的經(jīng)驗(yàn)是池子容量設(shè)為 8~16 個(gè)歷史版本、每 500 局替換一次為當(dāng)前策略效果比較穩(wěn)健。如果你的目標(biāo)是畢業(yè)設(shè)計(jì)展示不需要“打出最強(qiáng)風(fēng)格”就采用更保守的設(shè)置展示穩(wěn)定收斂的曲線。4.2 獎(jiǎng)勵(lì)數(shù)值漂移與梯度爆炸的觀察方法訓(xùn)練深度強(qiáng)化學(xué)習(xí)模型最怕的就是 loss 曲線炸掉或獎(jiǎng)勵(lì)數(shù)值越跑越大、沒(méi)有上限。德州撲克的獎(jiǎng)勵(lì)范圍本來(lái)就是可正可負(fù)的如果獎(jiǎng)勵(lì)塑形系數(shù)沒(méi)控制好模型會(huì)瘋狂挖掘獎(jiǎng)勵(lì)漏洞出現(xiàn)“每次翻牌前都全下”的極端策略。這個(gè)項(xiàng)目的一個(gè)隱蔽坑點(diǎn)就在這里由于自博弈對(duì)手的籌碼結(jié)構(gòu)也在變化同一手牌的絕對(duì)獎(jiǎng)勵(lì)數(shù)值在不同訓(xùn)練階段含義完全不同。解決思路在源碼的 reward normalization 部分有體現(xiàn)把每個(gè) episode 的總獎(jiǎng)勵(lì)除以一個(gè)滑動(dòng)平均的參考值讓模型學(xué)習(xí)的是“相對(duì)好壞”而不是“絕對(duì)數(shù)值”。如果你跑出來(lái)的 reward 曲線在 2 萬(wàn)局后還在持續(xù)爬升、沒(méi)有任何收斂跡象大概率是獎(jiǎng)勵(lì)塑形里沒(méi)有做歸一化或者學(xué)習(xí)率偏大。先停掉訓(xùn)練把 reward 歸一化打開(kāi)再跑。4.3 常見(jiàn)踩坑記錄現(xiàn)象、原因、解決現(xiàn)象訓(xùn)練 loss 持續(xù)下降但同一玩家跟 AI 對(duì)打時(shí) AI 表現(xiàn)毫無(wú)進(jìn)步甚至變蠢。原因獎(jiǎng)勵(lì)被歸一化后模型學(xué)會(huì)了最大化相對(duì)優(yōu)勢(shì)而非贏得真實(shí)牌局。自博弈對(duì)手池中整體水平也在提升但絕對(duì)勝率并沒(méi)有提高。解決定期在訓(xùn)練過(guò)程中凍結(jié)當(dāng)前策略和一組固定難度的基線對(duì)手比如隨機(jī)策略、保守策略做離線評(píng)估用外部勝率作為真實(shí)指標(biāo)而不是只看內(nèi)部 loss。現(xiàn)象程序報(bào)錯(cuò)IndexError: index out of range集中在特征提取模塊。原因某局牌對(duì)手?jǐn)?shù)量不足或底牌序列異常導(dǎo)致歷史動(dòng)作窗口的長(zhǎng)度無(wú)法填滿特征向量。解決在特征提取函數(shù)入口處加斷言檢查動(dòng)作歷史長(zhǎng)度是否達(dá)到history_length參數(shù)的最小值不足時(shí)用 0 向量補(bǔ)齊而不是截?cái)嗷蛱^(guò)?,F(xiàn)象GPU 利用率極低訓(xùn)練大部分時(shí)間都在等待。原因訓(xùn)練主循環(huán)里env.step是同步串行執(zhí)行的一局牌模擬完成才產(chǎn)生一批數(shù)據(jù)GPU 只能間歇性工作。解決改造為多環(huán)境并行采樣或者減少單局模擬的等待時(shí)間。改動(dòng)量不大核心是把env.step封裝進(jìn)multiprocessing池子用apply_async收集結(jié)果?,F(xiàn)象模型下載后加載權(quán)重直接報(bào)錯(cuò)state_dict鍵名不匹配。原因PyTorch 版本差異導(dǎo)致網(wǎng)絡(luò)層命名不同或者模型文件是用 GPU 訓(xùn)練、在 CPU 上加載時(shí)缺少map_location參數(shù)。解決加載時(shí)統(tǒng)一加model.load_state_dict(torch.load(path, map_locationcpu))并對(duì)比網(wǎng)絡(luò)構(gòu)造函數(shù)與保存文件的鍵名。這個(gè)坑基本是版本問(wèn)題引起的建議跑之前先打印權(quán)重文件里前三個(gè)鍵名核對(duì)。5. 把模型部署到對(duì)局測(cè)試離線評(píng)估與策略導(dǎo)出路徑訓(xùn)練完的模型不能只在訓(xùn)練腳本里自嗨要驗(yàn)證它到底有沒(méi)有學(xué)到東西最快的方法是寫(xiě)一個(gè)獨(dú)立的評(píng)估腳本讓訓(xùn)練好的策略與固定策略打幾百局統(tǒng)計(jì)勝率和平均底池收益。這個(gè)項(xiàng)目的模型保存通常包含兩個(gè)文件策略網(wǎng)絡(luò)權(quán)重和優(yōu)化器狀態(tài)權(quán)重文件可以直接加載到評(píng)估腳本里推理。# 離線評(píng)估核心代碼 model DQN(state_dim, action_dim).to(device) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() # 關(guān)閉 dropout 和 batch norm 的統(tǒng)計(jì)更新 win_count 0 chip_diff_total 0 for i in range(500): final_chips evaluate_one_round(model, opponentrandom) chip_diff final_chips - initial_chips chip_diff_total chip_diff if chip_diff 0: win_count 1 print(f勝率: {win_count / 500:.2%}, 平均籌碼差: {chip_diff_total / 500:.1f})評(píng)估腳本寫(xiě)好后可以多跑幾個(gè)固定策略做對(duì)比比如隨機(jī)策略、保守策略只跟注不加注、激進(jìn)策略拿到好牌全下。我實(shí)踐下來(lái)發(fā)現(xiàn)一個(gè)很反直覺(jué)的現(xiàn)象訓(xùn)練好的模型對(duì)隨機(jī)策略的勝率往往不是最高的反而是對(duì)激進(jìn)策略的勝率最高——因?yàn)榧みM(jìn)策略會(huì)頻繁送籌碼。如果你用隨機(jī)策略做基線勝率數(shù)字可能不好看但模型本身是沒(méi)問(wèn)題的所以評(píng)估基線的選擇要謹(jǐn)慎。如果要進(jìn)一步把這個(gè)模型用到其他對(duì)局環(huán)境中需要把模型推理邏輯從訓(xùn)練腳本中剝離。目前這份資源的模型輸出是四種動(dòng)作的 Q 值只要在外部環(huán)境里把環(huán)境的狀態(tài)轉(zhuǎn)化為同樣的特征向量格式就能解釋模型的輸出hard coding 的加注幅度參數(shù)也需要檢查因?yàn)橥獠恳?guī)則可能不允許 0.5 倍底池的加注。我一般會(huì)寫(xiě)一個(gè)interact.py腳本把模型封裝成一個(gè)純函數(shù)輸入特征向量輸出動(dòng)作索引這樣就徹底解耦了訓(xùn)練和部署。我吃過(guò)虧的地方在于模型在訓(xùn)練環(huán)境里跑得風(fēng)生水起換到規(guī)則稍有不同的環(huán)境里就完全失效——后來(lái)才發(fā)現(xiàn)是特征編碼里的“底池比例”計(jì)算方式變了底池包含的籌碼口徑不同模型看到的輸入和訓(xùn)練時(shí)完全不是一回事。從那以后我每次評(píng)估都會(huì)強(qiáng)制先在環(huán)境接口層面過(guò)一遍特征向量對(duì)比代碼確保訓(xùn)練和推理走的是同一套編碼邏輯再談勝率和收益。這份資源的好處就在于特征提取代碼是完整封裝好的直接調(diào)用、別自己重寫(xiě)就不會(huì)踩這個(gè)坑。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取