news 2026/9/28 4:58:40

德州扑克人工智能算法优化:遗憾最小化与深度CFR训练实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
德州扑克人工智能算法优化:遗憾最小化与深度CFR训练实践

简介:这份资源围绕基于Python深度强化学习的德州扑克AI算法优化展开,核心agent位于“实验环境/agents/DeepCFRagent3.py”,由DeepCFR改进而来。资源在Limit与NoLimit Leduc Holdem Poker上用exploitability衡量与纳什均衡的距离,并与CFR、CFR+、MCCFR、DeepCFR等主流算法对比;在规模较大的Limit Holdem Poker中使用对战RandomAgent的reward评估,兼顾理论收敛质量与实际对战收益。适合希望系统学习强化学习与博弈论算法的小白或进阶学习者,也可用于毕业设计、课程设计、大作业或工程实训。包体共166个文件,以58个py源码、48个pth模型权重、18个csv评估数据、16个pkl数据文件为主,另有txt、json、md说明等辅助内容,压缩包约14MB,目录分层清晰。所有数据与脚本按实验环境分区,便于复现与二次开发。已有139人学习下载。通过完整agent实现、多算法对比框架、训练模型和评估数据,读者可以复现实验、理解DeepCFR改进思路,并可迁移到更大规模扑克环境中继续优化。

1. 德州扑克AI不是让DQN硬学:为什么遗憾最小化比奖励最大化更关键

如果照着围棋的思路把DQN直接丢到一局德州扑克上,你大概率得到的不是AI,而是一个疯狂诈唬的黑匣子。德州扑克AI的难点不在算力,而在信息不完全:你看不到对手底牌,同样的可观察状态背后对应着无数种真实牌局,价值函数天然不是一个确定函数。标题里的“算法优化”因此不指换更大网络,而是在遗憾最小化框架里改善样本效率与收敛,把CFR扩展到深层状态空间,用自博弈逼近纳什均衡。这篇文章面向有Python与深度强化学习基础、想把手上的知识真正落到一个零和博弈场景的工程师,从编码、搭建到踩坑按实操顺序展开。

2. 把一局牌变成强化学习能吃的状态:状态编码、动作抽象与奖励设计

2.1 为什么不完美信息让DQN直接失灵

很多从“python入门”走过来的朋友,第一反应是把牌面、底池、下注轮次拼成一个向量,然后丢给DQN。这个思路在完美信息游戏里成立,在德州扑克里会碰壁:DQN学的是“当前状态下的期望回报”,但扑克里一个状态并不对应一个真实世界。你看到公共牌是A-K-7,对手可能拿AA,也可能拿72o;这两个世界的正确应对完全不同。传统DQN只能在这些可能性上平均,结果就是策略变得又软又犹豫。

真正在德州扑克AI里被验证的路线是反事实遗憾最小化(CFR)家族。DeepStack、Libratus这些公开方案的核心都是先在受限动作空间里做CFR类迭代,再用深度网络压缩状态规模。深度强化学习在这里的角色不是“端到端学一个价值函数”,而是让CFR能扩展到人类无法全量遍历的牌局空间。这个定位搞清楚,后面的网络设计才不会跑偏。

2.2 用Python写状态编码:手牌、公牌与下注史怎么进网络

我一般把一局牌编码成三块:手牌、公牌、下注史。手牌和公牌都用52维one-hot;下注史按四条街分别记录双方累计下注。这里的关键坑是手牌和公牌可能指向同一张牌,写特征时一定要先去重,否则网络会以为同一张牌出现了两次。

import numpy as np SUITS = ['s', 'h', 'd', 'c'] RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A'] def card_index(card: str) -> int: return RANKS.index(card[0]) * 4 + SUITS.index(card[1]) def street_bet_features(bet_history: dict) -> np.ndarray: # 每条街存 [本方总下注, 对方总下注] feats = np.zeros(8, dtype=np.float32) for i, street in enumerate(['preflop', 'flop', 'turn', 'river']): if street in bet_history: feats[i * 2] = bet_history[street][0] feats[i * 2 + 1] = bet_history[street][1] return feats def encode_info_state(hole_cards, board_cards, bet_history) -> np.ndarray: vec = np.zeros(52 + 8, dtype=np.float32) seen = set() for c in hole_cards + board_cards: idx = card_index(c) if idx in seen: continue seen.add(idx) vec[idx] = 1.0 vec[52:] = street_bet_features(bet_history) return vec

这段代码的输出是一个60维向量,plant到Torch或TF里就是一行Linear(60, 256)的事。注意下注金额我建议归一化到筹码量的比例值,不要直接用绝对数,不然深码局和浅码局的特征分布差异会干扰训练。

2.3 动作离散化与合法性掩码:fold当合法动作才给奖励

无限注德扑的下注尺寸理论上连续,落地时必须离散化。我的做法是把每条街限制成固定档位,常见配置是fold / check_call / half_pot / pot / all_in五档。这里有个坑容易被新手忽略:当前无人下注时fold是不合法动作,check_call也不该拆成两个动作。所以动作空间必须配合合法性掩码使用,否则网络会学到“在免费看牌时主动弃牌”这种自杀策略。

ACTIONS = ['fold', 'check_call', 'half_pot', 'pot', 'all_in'] def build_action_mask(state) -> np.ndarray: legal = ['check_call', 'half_pot', 'pot', 'all_in'] if state.current_bet > 0: legal.insert(0, 'fold') mask = np.zeros(len(ACTIONS), dtype=np.float32) for i, act in enumerate(ACTIONS): mask[i] = 1.0 if act in legal else 0.0 return mask def masked_softmax(logits: np.ndarray, mask: np.ndarray) -> np.ndarray: logits = np.where(mask == 1.0, logits, -1e9) logits = logits - np.max(logits) exp = np.exp(logits) return exp / exp.sum()

动作档位越细,信息集数量越大。我用五档起步,先把整条训练管线跑通,再回头调动作抽象。ali的尺寸选择会在第4章专门展开,因为它直接决定训练时间量级。

2.4 稀疏奖励与反事实价值:为什么不要给每一步加模拟奖励

很多做Dota/星际RL的同学习惯设计中间奖励来缓解稀疏问题。在CFR框架里,这条路最好不要走。在德扑里,中间奖励没有天然的定义:翻牌前加注可能是价值也可能是诈唬,单步收益无法反映长期策略质量。CFR的效用函数直接取牌局终点的筹码收益,每一步的“遗憾”由反事实价值自动产生,不需要人为设计。

如果你一定要走NFSP或Actor-Critic这类深度强化学习路线,我建议同样只使用终局稀疏奖励,配合经验回放和对抗采样。给“赢得底池”加1分这种伪奖励会让模型变成只看眼前利益的下注机器,胜率上不去还难调试。

3. 用Python搭一个Deep CFR训练骨架:从后悔匹配到自博弈

3.1 后悔匹配是CFR的心脏

CFR的核心不是神经网络,而是一个非常朴素的更新法则:对每个信息集、每个动作,累计“如果当初选这个动作,比实际选的动作多赢多少”,然后按正遗憾的比例生成下一轮策略。先把这个函数写对,后面接网络才有意义。

def regret_matching(regrets: np.ndarray) -> np.ndarray: positive = np.maximum(regrets, 0.0) total = positive.sum() if total < 1e-12: return np.full_like(regrets, 1.0 / len(regrets)) return positive / total

参数说明:regrets是当前信息集下所有动作的遗憾值向量,正遗憾的动作才有概率被选中。分母小于阈值时返回均匀策略,这个兜底很关键,否则会出现除以零。这段代码虽然短,但它在整个训练管线里会被调用几十万次,性能上建议用纯numpy实现,别在Python循环里逐元素算。

3.2 用PyTorch定义RegretNet和AveragePolicyNet

官方Deep CFR类方案维护两个网络:一个预测“某个信息集下某个动作的遗憾值”,另一个输出“最终应该执行的平均策略”。两个网络必须分开,因为遗憾值和策略分布是两回事:遗憾值告诉你怎么修正,平均策略告诉你最终怎么打。

import torch import torch.nn as nn class RegretNet(nn.Module): def __init__(self, state_dim: int, num_actions: int): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim + num_actions, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 1), ) def forward(self, state: torch.Tensor, action_onehot: torch.Tensor) -> torch.Tensor: x = torch.cat([state, action_onehot], dim=-1) return self.net(x).squeeze(-1) class AveragePolicyNet(nn.Module): def __init__(self, state_dim: int, num_actions: int): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, num_actions), ) def forward(self, state: torch.Tensor, mask: torch.Tensor) -> torch.Tensor: logits = self.net(state) logits = logits.masked_fill(mask == 0, -1e9) return torch.softmax(logits, dim=-1)

RegretNet把动作拼成one-hot后输出一个标量,本质是在做回归;AveragePolicyNet只吃状态特征,输出经过masked softmax的动作概率。两个网络的结构刻意不一样宽,RegretNet需要更宽的容量来拟合更复杂的映射,AveragePolicyNet要窄一些,具体原因在第4章展开。

3.3 MCCFR采样:把对局遍历变成regret样本

完整CFR需要遍历整棵博弈树,德扑状态空间远超这个量级,所以用蒙特卡洛外部采样(MCCFR)替代:当前玩家全量遍历所有动作,对手动作按策略概率采样。这样每次只推进一条对手分支,训练数据量大幅下降。

def traverse(self, node, reach, me: int, regret_buffer: list): if node.is_terminal(): return node.utility(me) if node.is_chance(): return self.traverse(node.sample_chance(), reach, me, regret_buffer) cur = node.current_player() if cur == me: info_state = node.info_state_feature() legal = node.legal_actions() strategy = self.get_strategy(info_state, legal) node_value = 0.0 for action in legal: child_reach = reach.copy() child_reach[cur] *= strategy[action] action_value = self.traverse( node.child(action), child_reach, me, regret_buffer ) # 反事实到达概率:排除当前玩家的到达概率 cf_reach = np.prod( [r for p, r in enumerate(reach) if p != cur] ) sampled_regret = cf_reach * (action_value - node_value) regret_buffer.append((info_state, action, sampled_regret)) node_value += strategy[action] * action_value return node_value else: # 对手节点:采样单条分支 strategy = self.get_strategy( node.info_state_feature(), node.legal_actions() ) action = np.random.choice(node.legal_actions(), p=strategy) child_reach = reach.copy() child_reach[cur] *= strategy[action] return self.traverse(node.child(action), child_reach, me, regret_buffer)

逻辑说明:当前玩家是me时,每个合法动作都做一次递归,并把该动作的sampled_regret写入buffer;对手节点则只采样一条分支,这是计算量能压下来的关键。node_value是当前策略下的期望收益,action_value - node_value度量“换成这个动作能改进多少”。这个版本是教学骨架,生产上还要补充重要性权重,否则采样偏差会随迭代累积。

3.4 最小训练循环:采样、训练网络、更新策略

把前面几块拼起来,训练的骨架就是:跑一批对局,攒regret样本;训练RegretNet;再用RegretNet的输出生成平均策略目标,训练AveragePolicyNet。每轮迭代都要重复这个过程。

for iteration in range(total_iterations): # 1. 采样阶段 regret_samples = [] policy_states = [] for _ in range(episodes_per_iter): root = new_hand() self.traverse(root, [1.0, 1.0], me=0, regret_buffer=regret_samples) policy_states.extend(self.collect_policy_states(root)) # 2. 训练 RegretNet train_regret_net(regret_samples, regret_epochs=30) # 3. 生成平均策略目标并训练 AveragePolicyNet policy_targets = [] for info_state, mask in policy_states: regrets = regret_net( torch.tensor(info_state).unsqueeze(0), torch.eye(num_actions).unsqueeze(0), ).squeeze(0).detach().numpy() target = regret_matching(regrets, mask) policy_targets.append((info_state, target)) train_average_policy(policy_targets, policy_epochs=20) # 4. 每 50 轮做一次评估 if iteration % 50 == 0: log(iteration, evaluate_exploitability(agent))

参数说明:episodes_per_iter我通常设100到500,太少则regret估计方差大,太多则单轮训练耗时过长;regret_epochs和policy_epochs不需要太大,因为每轮迭代的监督信号本身就在变动,硬拟合反而会记住上一轮的噪声。

4. 算法优化先调这四个参数:迭代策略、网络容量、学习率与动作抽象

4.1 CFR+与线性加权的迭代策略

优化算法第一步不是换网络结构,而是换更新规则。经典CFR对负遗憾直接保留,CFR+则是把负遗憾直接截断为0,同时平均策略按迭代次序线性加权。这两个小改动在德扑场景里能明显加速收敛。

def cfr_plus_update(regret_sum, increment): return np.maximum(regret_sum + increment, 0.0) def linear_weight(iteration: int) -> float: return float(max(iteration, 0))

逻辑说明:CFR+的本质是“过去的错误不再惩罚”,让策略更激进地朝当前最优方向走;线性加权则是让后期的策略对平均结果有更大影响,避免早期随机探索污染最终策略。实现时注意:regret_sum要在遍历过程中累加,平均策略的权重也要同步按linear_weight累加,否则CFR+的优势体现不出来。

4.2 两个网络分开配容量:别让平均策略网络太宽

我踩过最典型的一个坑,是给AveragePolicyNet用了和RegretNet一样宽的256结构。结果训练曲线看起来在收敛,实际评估时策略表现忽好忽坏。原因是平均策略网络太宽,把每轮迭代的采样噪声当成规律背了下来。

常见的做法是RegretNet用256或512,AveragePolicyNet用128到256。RegretNet要做的是细粒度回归,容量不够就欠拟合;AveragePolicyNet学的是各动作的长期平均概率,容量过高就过拟合短期波动。另一个附带好处是AveragePolicyNet参数少,在每轮生成策略目标时推理更快,训练循环整体节奏更跟得上。

4.3 非平稳目标下的学习率与批量大小

RegretNet的回归目标每轮迭代都在变,同一个信息集这轮的标签和下轮可能差很多。这种非平稳回归问题,学习率设置得过高会让网络在“追新标签”和“忘掉旧知识”之间震荡。

参数常见范围影响
RegretNet学习率1e-4 到 1e-3过高则exploitability曲线反复跳动
AveragePolicyNet学习率1e-4 到 1e-3过高则平均策略失去“平均”意义
regret批量大小512 到 2048小批量在非平稳目标下梯度噪声过大
每轮训练epoch20 到 50太多会过拟合上一轮标签

我一般的做法是RegretNet用Adam、学习率3e-4,批量大小1024;AveragePolicyNet也用Adam但学习率降到1e-4。如果exploitability曲线出现锯齿状震荡,先降学习率,而不是加大网络。

4.4 动作抽象粒度与信息集规模的平衡

动作抽象是整个优化里性价比最高的杠杆。动作档位越多,信息集数量呈组合级增长,但策略的理论上限也越高。这里必须做一个明确取舍,我建议按游戏规模决定。

动作方案每条街档位数适用阶段训练成本策略上限
三档:fold/check_call/all_in3调通链路低低
五档:fold/check_call/half_pot/pot/all_in5正式训练中中
七档:增加3/4_pot、overbet7冲刺上限高边际递减

先无脑用五档跑通全流程,确认网络和采样代码没有bug后,再把half_pot替换成更细的档位。很多优化效果不明显,问题不在算法,而在动作抽象太粗,网络根本没机会表达正确策略。

5. 德州扑克DRL训练避坑:4个最常见的收敛翻车现场

5.1 Loss突然变成NaN且不再恢复

现象:训练到某个迭代轮次,RegretNet的MSE loss变成NaN,后续无论怎么调学习率都救不回来。

原因:一般有两个来源。一是AveragePolicyNet在masked softmax之前没有对非法动作做足够低的屏蔽值,导致某次前向计算出现inf减去inf;二是regret样本中出现极端大的cf_reach,乘出来的回归目标超过浮点范围。

解决:对RegretNet的输出做梯度裁剪,nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0);同时把masked softmax里的屏蔽值从-1e9改成-1e6,避免和float32的精度边界纠缠。另外建议给regret target做一次离群值截断,超过均值±5倍标准差的样本直接丢弃。

5.2 策略塌缩成只跟注不弃牌

现象:训练完成后,AveragePolicyNet在绝大多数信息集上输出fold概率接近0,面对加注永远跟注或再加注。

原因:Rregret的稀疏性。在很多信息集上,fold动作从未被真正采样到正遗憾,RegretMatching按正遗憾分布分配概率,fold天然得不到概率。AveragePolicyNet学到的supervision里fold目标全是0,自然就塌缩。

解决:在MCCFR遍历时对动作引入epsilon探索,保证fold这类低频动作用最小概率被采样到。我通常在每个决策节点做strategy = 0.95 * strategy + 0.05 * uniform。也可以对average policy的训练目标做标签平滑,给fold一个很小的本底概率,比如0.02,防止网络输出硬0。

5.3 对局胜率高但打真人就翻车

现象:自博弈评估胜率到了60%以上,换成固定基线或人手动评估时,策略变得极易被剥削。

原因:自博弈对手和自己是同一个策略,两边会共同收敛到一种非均衡的“互相对付”模式。胜率高只说明它比曾经的自己强,不能说明它接近纳什均衡。

解决:正确的评估指标是exploitability,而不是胜率。实践中我会每隔若干轮暂停训练,用当前策略对阵一个固定的、偏紧的规则策略,混入评估流程。如果对阵规则策略的EV始终偏低,说明你的自博弈路线出了偏差。这个问题在我接触的团队里出现频率极高,几乎人手一个。

5.4 训练时间爆炸:一天一夜只跑了几千手

现象:代码逻辑没问题,但单次迭代时间超长,迭代数千轮后exploitability纹丝不动。

原因:绝大多数情况是对手节点没有做采样,整棵博弈树被全量遍历;或者动作抽象里存在一个档位从未被触发,但每条路径仍在遍历。另一个常见原因是经验回放缓冲太小,导致网络反复拟合同一批样本,训练曲线看起来在动,实则在原地打转。

解决:首先检查MCCFR的对手节点是否真的只采样了一条分支,这是加速的关键。其次把RegretNet和AveragePolicyNet的训练数据池做大,我一般保留最近200到500轮的样本,保证每轮训练数据的分布变化足够平滑。训练是体力活,合理的batch和数据保留策略比调网络结构见效快得多。

6. 用exploitability和数据分布验证策略质量:先跑Kuhn扑克给代码“验光”

6.1 为什么exploitability比胜率可信

在二人零和博弈里,一个策略的exploitability指“如果对手已知你的完整策略并专门寻找最优反制策略,你能损失多少”。平均两个玩家的exploitability越小,说明策略越接近纳什均衡。德州扑克算不了全局最优反制,但在受限动作空间内可以用同样的CFR类算法估算一条best response,得到有参考价值的利用度曲线。

我一般每隔50轮训练记录一次exploitability,如果曲线稳定下降,说明策略在走向均衡;如果曲线横盘,说明训练分布有问题。看胜率曲线容易产生虚假安全感,exploitability曲线是更诚实的反馈。

6.2 先用小规模信息集验证代码正确性

完整德扑的训练周期太长,直接上手难以判断代码是否有bug。常见做法是先跑Kuhn扑克:3张牌的简化版德州扑克,状态空间极小,可以用穷举CFR求出理论均衡。把同一个Deep CFR训练骨架套到Kuhn扑克上,如果实现正确,平均策略会向理论最优策略收敛,exploitability应快速降到接近0。

python train_deep_cfr.py --game kuhn_poker --episodes_per_iter 100 \ --regret_epochs 30 --policy_epochs 20 --total_iterations 500

跑通的标志不是loss低,而是exploitability降到阈值以下并维持稳定。Kuhn扑克理论均衡是已知的,如果网络策略与理论均衡的概率分布对不上,那一定是采样逻辑或网络target生成有问题。先花一天把这条链路跑稳,比直接跑完整德扑省一周的调试时间。

我的习惯是每次改动采样器或网络结构,先跑500轮Kuhn确认没有破坏收敛性,再切回五人桌或二人限制桌继续训练。这个流程看似多了一步,实际是给整个训练管线装了刹车,能拦下绝大多数的实现错误。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 4:58:33

德州扑克AI深度强化学习优化:算法选型、奖励塑形与实战避坑

简介&#xff1a;基于Python深度强化学习的德州扑克AI算法优化项目&#xff0c;面向希望系统学习强化学习与博弈算法的小白及进阶学习者&#xff0c;适合作为毕业设计、课程设计、大作业、工程实训或初期项目立项。项目以自行改进的DeepCFR agent为核心&#xff0c;在Leduc&…

作者头像 李华
网站建设 2026/9/28 4:55:52

Python开发必会的十个高效技巧

Python 写起来简单&#xff0c;但写得好和写得快是两回事。很多开发者停留在“能跑就行”的阶段&#xff0c;代码冗长、效率低下。下面十个技巧&#xff0c;每一个都能让你的代码更短、更快、更 Pythonic。1. 列表推导式替代循环 appendpython复制下载# 不推荐 squares [] for…

作者头像 李华
网站建设 2026/9/28 4:55:29

研发各场景下的提示词Prompt模板

我为大家整理了七个主要的场景, 并且为每一个场景都提供了能够起到很高效率的作用和提示词的模板。1. 完成需求方面的分析工作, 并且展开系统设计这一部分的内容。把那些模棱两可的产品创意&#xff0c;转换成清清楚楚具体技术计划、还有数据库结构安排、或者直接明确 API 接口…

作者头像 李华
网站建设 2026/9/28 4:55:17

Go微内核+35个扩展:我给自己的AI桌面助手做了一次推倒重写

给自己的 AI 助手做大重构&#xff1a;Go 微内核 35 个扩展&#xff0c;内核只干 6 件事本文是《自制桌面 AI 助手》系列的第二篇。上一篇介绍的 0.4.x 版本是一个"单体"架构&#xff1a;所有功能都塞在一个 exe 里&#xff0c;改一个小功能就要整体重新编译发布。这…

作者头像 李华