新闻详情

新闻详情

首页 / 资讯中心 / 详情

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

发布时间:2026/9/28 4:58:44来源:尧图网络
德州扑克人工智能算法优化:遗憾最小化与深度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, dtypenp.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, dtypenp.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), dtypenp.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(), pstrategy) 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], me0, regret_bufferregret_samples) policy_states.extend(self.collect_policy_states(root)) # 2. 训练 RegretNet train_regret_net(regret_samples, regret_epochs30) # 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_epochs20) # 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或512AveragePolicyNet用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批量大小1024AveragePolicyNet也用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_norm1.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确认没有破坏收敛性再切回五人桌或二人限制桌继续训练。这个流程看似多了一步实际是给整个训练管线装了刹车能拦下绝大多数的实现错误。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

ESP8266烧录AT固件总失败?排查这5类问题就够了 2026/9/28 6:02:29

ESP8266烧录AT固件总失败?排查这5类问题就够了

1. 烧录失败这件事,先别急着怀疑模块坏了ESP8266这颗芯片,玩物联网的、做智能家居的、搞串口透传的基本都绕不开。价格便宜、资料多、社区活跃,但真正上手第一关往往不是写代码,而是烧录AT固件。我见过太多人,模块刚到…

阅读更多 →
微信小程序养老服务平台:源码部署与实战讲解指南 2026/9/28 6:02:29

微信小程序养老服务平台:源码部署与实战讲解指南

从拿到这个项目标题到真正动手去做,中间其实隔着不少东西。市面上打着“微信小程序养老服务平台源码部署文档讲解”旗号的项目不少,但真正能落地、能跑通、能扛住老年用户场景的并不多。我最近完整梳理了一遍这类项目的交付内容和实施路径,把…

阅读更多 →
Tomcat生产环境部署全攻略:从JDK安装到JVM调优与排错 2026/9/28 6:02:29

Tomcat生产环境部署全攻略:从JDK安装到JVM调优与排错

部署Tomcat这事儿,说难不难,说简单也有一堆坑等着你。从下载解压到跑起来一个能用的Web服务,中间隔着JDK版本匹配、端口配置、JVM参数、自启动脚本、日志切割、前后端分离路由……任何一个环节偷懒,后面上线都可能给你颜色看。这篇…

阅读更多 →
电商GIF主图压缩实战:工具选择、参数调优与避坑指南 2026/9/28 6:02:29

电商GIF主图压缩实战:工具选择、参数调优与避坑指南

做电商的同行应该有同感,GIF主图这个事儿,看着简单,真正做起来是真麻烦。平台后台对主图体积有硬性限制,动辄几十MB的原始GIF根本传不上去;就算勉强传上去,首页加载慢、卡顿,买家没等图转完就划…

阅读更多 →
Java中学排课管理系统源码:课程设计、存储过程与前后端分离实战 2026/9/28 6:02:29

Java中学排课管理系统源码:课程设计、存储过程与前后端分离实战

简介:这是一份用于中学排课场景的JAVA源码工程,也是数据库课程设计常用的完整参考项目,适合正在做排课管理系统课设或需要学习数据库存储过程、参照完整性约束的同学。项目采用前后端分离结构,后端以JAVA为核心处理班级、课程、学…

阅读更多 →
Codex CLI 频繁 Reconnecting 怎么排查?先分清 WebSocket、SSE 与配置边界 2026/9/28 6:02:22

Codex CLI 频繁 Reconnecting 怎么排查?先分清 WebSocket、SSE 与配置边界

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉