DeepCFR算法优化与复现:从遗憾最小化到exploitability评估

发布时间:2026/10/8 5:55:10
DeepCFR算法优化与复现:从遗憾最小化到exploitability评估 简介针对基于Python深度强化学习的德州扑克AI算法优化这套完整工程面向希望从入门到进阶掌握深度强化学习的开发者与研究者特别适合作为毕设、课程设计、大作业或工程实训项目也可供教师作为教学案例。核心智能体为DeepCFRagent3.py由DeepCFR改进而来系统对比了CFR、CFR、MCCFR与DeepCFR四种算法在Limit与Nolimit Leduc holdem环境中以exploitability衡量策略与纳什均衡的差距作为评估指标在规模更大的Limit holdem环境中则以与RandomAgent对战的奖励值评估策略强度实验数据完整。压缩包共166个文件包含58个Python脚本、48个模型权重pth、18个CSV结果表、18个TXT说明与16个PKL文件整体约14MB目录组织清晰便于按代码、模型和实验数据逐层查阅。目前已有139人学习下载读者可结合论文与代码复现对比实验理解从CFR类传统算法到DeepCFR深度强化学习的优化路径也可直接改造智能体用于其他不完全信息博弈研究。1. 德州扑克AI不是玄学DeepCFR的纳什距离与可复现改进一份基于Python深度强化学习的德州扑克AI算法优化资源值不值花时间去复现先看它敢用什么指标说话。德州扑克AI的强弱判断和多数人想的不一样不看它赢了多少而看它被对手剥削的空间有多大。exploitability衡量当前策略与纳什均衡的距离数值越小对手能榨取的收益越少这才是博弈类AI真正要优化的目标。这份资源把agent落在/实验环境/agents/DeepCFRagent3.py从DeepCFR改进而来在Leduc与CFR、CFR、MCCFR、DeepCFR逐一对比小规模场景用exploitabilityLimit Holdem上用与随机Agent对战的reward评估。适合拿来做毕设、课程设计或工程实训也适合想弄懂深度强化学习怎么落地到不完美信息博弈的新手——不需要GPUCPU就能把Leduc跑完。2. 从遗憾最小化到DeepCFR先拆原理黑匣子再谈DeepCFRagent3的四处改进2.1 遗憾值是什么CFR在用“后悔”修正策略CFR的全称是Counterfactual Regret Minimization反事实遗憾最小化。它优化的不是胜率而是“后悔值”在某个信息集上你选择了动作A事后发现如果当时选B能拿到更高收益这个差额就是你对该动作的遗憾。在不完美信息博弈里你既不知道对手底牌也不在自己没走到的世界线上所以这个遗憾必须带“反事实”修饰——用当前策略和对手策略的假想收益差来计算而不是用实际牌局的输赢。遗憾值在经典CFR里是表驱动的为每个信息集的每个动作维护一个数值一轮一轮迭代更新。更新之后用Regret Matching规则把正遗憾部分归一化成下一轮的动作概率迭代到后期策略会逼近纳什均衡。表驱动在Leduc Holdem上没有问题因为这个环境的牌型少、两轮下注、单挑规则信息集规模在可枚举范围内。资源把Leduc当成核心验证场景就是因为它小到可以精确算均衡大到不会像toy game那样失真。后续的CFR和MCCFR也是在同一框架下做的改进CFR把负遗憾清零再用线性权重平均策略收敛明显更快MCCFR用蒙特卡洛采样替代全遍历把计算量降下来但引入随机性。这两条路正是DeepCFR改造的两条主线更快的平均策略更新、更省的采样过程。理解这个谱系后再看DeepCFRagent3就不会把它的改动当成孤立技巧。2.2 DeepCFR的神经网络化遗憾值从表格变成回归目标DeepCFR要解决的核心问题是CFR表格无法扩展。真实扑克游戏的信息集数量是天文数字表驱动的遗憾值既存不下也遍历不完。DeepCFR的决定性改动是让神经网络来拟合遗憾值函数和平均策略函数训练数据来自MCCFR式的外部采样。具体做法是每次迭代采样器从公共状态出发按当前策略模拟双方动作记录访问到的信息集和动作随后用外部采样计算该信息集下各动作的采样遗憾作为回归目标喂给遗憾网络。平均策略由另一个策略网络维护评估时直接将当前信息集特征输入策略网络得到动作概率不再查表。即使某个信息集没被访问过网络也能基于相似特征给出合理估计这是它比MCCFR更稳的根本原因。两个网络各司其职遗憾网络输出的是“在状态s选动作a的遗憾估计”策略网络输出的是“平均策略下动作a的概率”。外部采样只负责生成轨迹不参与网络参数更新。这也是后来很多DeepCFR变体继续修改采样器的原因——采样质量直接决定遗憾网络的回归目标干不干净。Leduc虽然是可枚举场景但DeepCFR的网络化设计让它在不可枚举场景里也有迁移可能这是算法本身的价值所在。提示DeepCFR的训练目标是遗憾值不是Q值。如果看到有人把DeepCFR和DQN混为一谈说明他还没分清“策略迭代”和“价值迭代”两条技术路线。2.3 DeepCFRagent3的四处改动Navg、特征增强、双率分离、探索噪声agent文件在/实验环境/agents/DeepCFRagent3.py和DeepCFRagent放在同一目录下训练入口可以直接切换对比。结合Leduc场景的复现经验把改进归纳成四处。第一处平均策略从朴素平均改成Navg。朴素平均把所有历史策略等权相加理论对但收敛慢Navg用指数衰减权重让策略网络更重视近期训练成果。常见的tau取值在0.99到0.999之间tau越大历史权重衰减越慢。改成Navg后Leduc上exploitability降到0.1以下的轮次大约前置三成。第二处信息集特征增强。DeepCFR原始输入是手牌独热编码加下注历史对Leduc这种有公共牌配对的游戏来说不够用。把“底牌是否成对”“公共牌是否与底牌配对”“当前下注档位”都编码进特征向量遗憾网络的拟合难度会明显下降代价极小。def build_info_set_features(hole_cards, board_cards, bet_history): # 底牌独热编码 hole_enc encode_cards(hole_cards) # 成对检测Leduc里对子是最强牌型直接作为强特征 pair_flag float(hole_cards[0].rank hole_cards[1].rank) # 公共牌配对检测 board_pair float(any( c.rank hole_cards[0].rank or c.rank hole_cards[1].rank for c in board_cards )) # 下注档位离散化Limit Leduc只有pass/bet两档 bet_bucket discretize_bet(bet_history, buckets[0, 2, 4, 6]) return np.concatenate([hole_enc, [pair_flag, board_pair], bet_bucket])这段特征构建是把天然存在的牌型信息直接交给网络先编码底牌再叠加两个配对标志位最后把下注历史按金额离散成档位向量。注意Limit Leduc下注档位很少Nolimit则必须拆pot、half-pot、min-bet、all-in几档否则网络很难区分不同下注尺寸的语义。第三处遗憾网络和策略网络使用不同学习率。原版DeepCFR两个网络共用一套优化器参数但遗憾网络面对的回归目标波动大学习率太高容易震荡策略网络更新的是平均策略收敛节奏可以略激进。常见设置是遗憾网络1e-4、策略网络3e-4策略网络更新频率再降到遗憾网络的一半。第四处加入epsilon-贪心探索。MCCFR的采样过程本身带随机性但采样轨迹偏向当前策略下概率高的动作低概率但关键的下注动作容易被漏掉信息集覆盖率不够时exploitability曲线会在平台期卡住。加一层epsilon0.1的探索相当于每轮采样有10%的动作完全不按当前策略走把信息集覆盖范围撑开。在Nolimit Leduc上这个参数更敏感动作空间大靠纯策略采样很难覆盖全。改进点DeepCFRDeepCFRagent3平均策略聚合朴素平均Navg指数加权特征向量基础独热编码增强配对特征下注档位双网络学习率共用一套遗憾网络1e-4策略网络3e-4探索项无epsilon0.1贪心探索四处改进合起来就是资源对比实验中DeepCFRagent3在Leduc场景的exploitability收敛值低于原版DeepCFR且没有额外遍历成本。这些改进不需要换框架、不需要加算力全部在agent文件内完成这也是这份资源适合做课程设计和毕设的原因——改动小而可解释。3. 评估指标的分工Leduc为什么用exploitabilityLimit Holdem为什么用Reward3.1 exploitability的计算思路先求最优应对再算剥削空间exploitability的字面意思是可剥削性——对手如果拥有完美针对你的策略能从你身上榨出多少额外收益。它的定义是当前策略在面对Best Response最优应对时与原均衡价值的期望差距。计算分三步固定当前策略遍历所有信息集求解每个信息集下与当前策略对抗的最优动作把这些动作产生的期望收益差汇总得到总可剥削值。def compute_exploitability(game, avg_strategy): # 1. 固定平均策略构造最优应对策略 br game.best_response(avg_strategy) # 2. 用对手的最优应对重新遍历博弈树 # 返回当前策略面对BR时的期望收益差 exploitable game.evaluate(avg_strategy, br) return exploitable这段代码的逻辑是best_response要求在给定对手策略时遍历所有可能私有信息计算最优动作Leduc可枚举所以能精确算evaluate则用前向遍历求出双方期望收益差差的绝对值越低说明当前策略越接近均衡。Limit Holdem只有RandomAgent对战的reward也是因为这一步best_response根本枚举不完。3.2 三场景的指标分工不是偷懒是信息集规模不允许这组场景设计是资源里最容易被忽略、也最值得学习的部分确认了Limit Leduc用exploitability做绝对度量同时把Nolimit Leduc也用exploitability评估代价是先离散化下注档位。按这个思路延续Limit Holdem场景则使用与RandomAgent对战的Reward作为评估指标。场景评估指标原因Limit Leduc Holdemexploitability信息集可枚举能精确求解Best Response指标有绝对意义Nolimit Leduc Holdemexploitability离散化下注档位连续动作空间先离散化到pot/half-pot/min-bet/all-in再走同一套BR流程Limit Holdem与RandomAgent对战的Reward信息集数量过大BR不可解改用固定对手下的平均收益衡量相对强弱Nolimit Leduc的离散化档位设计直接影响评估可信度。档位数太少会把不同下注尺寸混成一个语义档位数太多又会让BR枚举复杂度上升。比较稳妥的做法是离散成一个比较小的集合BET_BUCKETS [0, 2, 4, 6, 8, 999] # 0check, 2min-bet, 4half-pot, 6pot, 8all-in def discretize_bet(bet_size): for i, bound in enumerate(BET_BUCKETS): if bet_size bound: return i return len(BET_BUCKETS) - 1这段逻辑很简单但作用关键必须覆盖所有可能下注尺寸尤其是all-in要有一个兜底档位。如果离散化上限设小了Nolimit的极限下注语义会被截断算出来的exploitability会虚高各算法之间的差距也没有说服力。3.3 对比组怎么设置才公平同一轮次、同一间隔、同一评估器资源里对比了CFR、CFR、MCCFR、DeepCFR和DeepCFRagent3五个agent的训练轮次和评估间隔必须一致。CFR系列是确定性遍历CFR用正遗憾和线性平均MCCFR是DeepCFR的采样基础DeepCFR验证网络化泛化能力DeepCFRagent3在它之上验证改进。如果给某个agent多跑几千轮再比结果就失去对照意义。同一评估器也很重要exploitability计算代码必须同一套不能为CFR写一个BR实现、为DeepCFR另写一个。我见过不少对比实验在评估器上翻车结果不是算法不行而是两个评估器对“当前策略”的取法不同。正确的做法是把平均策略单独导出成一份动作概率表五个agent共用同一个evaluate函数。4. 复现一份能跑的DeepCFR环境准备、训练入口与performance.csv读取4.1 环境与目录结构CPU就能跑别被“深度强化学习”吓住这份资源用Python实现核心依赖是PyTorch 1.13或2.x、numpy、pandas、matplotlib。Leduc规模不大普通笔记本CPU就能跑不需要GPU。注意torch版本差异会影响自动求导行为建议固定到2.0以上避免遇到老版本算图缓存的问题。动手跑之前先把路径对齐。资源里的agent在/实验环境/agents/DeepCFRagent3.py训练脚本和数据日志一般也在实验环境目录下performance.csv就是评估结果记录。先把目录结构看一遍确认agents目录下有哪些可 import 的模块再把游戏环境和agent的 import 路径理顺这一步能避免后面一半的报错。4.2 训练入口一份脚本切换全部对比agent训练入口值得单独写清楚。最省事的方式是把训练和评估放在一个脚本里用agent名字作参数切换。下面这份代码可以直接套进资源目录使用# train_leduc.py import csv from agents import CFR, CFRPlus, MCCFR, DeepCFR, DeepCFRagent3 AGENTS { cfr: CFR, cfr_plus: CFRPlus, mccfr: MCCFR, deepcfr: DeepCFR, deepcfr3: DeepCFRagent3, } def train(game, agent_name, iterations10000, eval_interval100): agent AGENTS[agent_name](game) rows [] for it in range(iterations): # 一次迭代 一批对手牌轨迹的外部采样 agent.sample_and_update(num_samples2, epsilon0.1) if it % eval_interval 0: # 计算当前平均策略的可剥削值 exp compute_exploitability(game, agent.avg_strategy) rows.append([it, agent_name, exp]) print(it, exp) with open(performance.csv, w, newline) as fp: writer csv.writer(fp) writer.writerow([iteration, agent, exploitability]) writer.writerows(rows) if __name__ __main__: game load_leduc(limitTrue) train(game, deepcfr3)这段代码的逻辑sample_and_update一次迭代做一批外部采样并更新两个网络num_samples2表示每个公共状态采两对私有手牌epsilon0.1对应改进点里的探索项每100轮算一次exploitability并写入CSV。AGENTS字典是切换对比组的关键跑完一个agent换另一个名字即可前提是所有agent暴露相同的sample_and_update接口。4.3 评估环节平均策略要在训练循环外算exploitability计算必须放在训练循环外部并且使用平均策略副本。直接对agent的当前策略网络做评估会高估收敛速度——DeepCFR的当前策略和平均策略往往差距不小只有平均策略才是最终交付物。常见做法是每eval_interval个iteration把策略网络权重导出成一份纯numpy的动作概率表再传给compute_exploitability。一个容易忽略的细节是采样数和迭代数的关系num_samples调大每轮遍历的轨迹更多遗憾网络的回归目标更稳但单轮耗时线性上升。Leduc场景下num_samples2、iterations10000是比较平衡的设置跑完大约需要几十分钟到两小时取决于机器单核性能。4.4 读performance.csv先看列名再画曲线训练完的资源会产出一份performance.csv。不同实验记录的列名可能不同最常见的是iteration、agent、exploitability三列Limit Holdem场景则可能换成reward列。读取时先打印前几行确认列名再按列名操作import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(performance.csv) print(df.head()) # 先看列名和行格式 df[ma] df[exploitability].rolling(20, min_periods1).mean() plt.plot(df[iteration], df[exploitability], alpha0.3, labelraw) plt.plot(df[iteration], df[ma], lw2, labelrolling mean) plt.legend() plt.show()rolling窗口按训练轮次取20即可如果是Limit Holdem的reward数据把exploitability列换成reward列同样适用。先dropna再画图避免中断产生的空行把曲线截断。5. 避坑手册DeepCFR训练与评估的五个踩坑记录5.1 五条真实踩坑记录平均策略不收敛exploitability先降后升现象前500轮曲线正常下降之后开始震荡上升最终停在比DeepCFR基线还高的位置。原因策略网络更新频率和遗憾网络没分离平均策略被近期高方差更新带偏Navg里历史权重衰减过快也会加重这个问题。解决把策略网络更新频率降到遗憾网络的四分之一或者把Navg的tau从0.99改为0.999给历史策略更多缓冲。随机种子固定了结果还是不可复现现象固定seed后连续跑两次exploitability曲线差异明显。原因MCCFR的采样过程本身就依赖随机数Python的random、numpy的RNG、torch的RNG是三套独立的随机源只固定torch.manual_seed没用。解决三套随机源全部固定并且在同一配置下跑3到5次取中位数。资源里所有对比组都应该按这个方式报告单次结果不能代表算法真实水平。Limit Holdem硬套exploitabilityBR遍历直接跑不完现象评估脚本跑了一个小时没有输出或者输出的数值巨大。原因Limit Holdem信息集数量达到10的12次方量级best_response的枚举过程不可行。解决严格遵守资源的评估分工Limit Holdem场景改用与RandomAgent对战的reward奖励统计均值加方差不要试图在完整Holdem上算exploitability。Nolimit Leduc评估结果抖动剧烈现象每次评估exploitability的波动超过0.2曲线像噪声。原因Nolimit下注动作空间是连续的没有离散化档位BR无法在连续空间里枚举出稳定最应对。解决先按pot、half-pot、min-bet、all-in离散化动作空间再走同一套BR流程。离散化后曲线平滑度会有明显改善。performance.csv出现NaN和不完整行现象读取CSV时发现某些行exploitability为空画图时曲线断开。原因训练进程在评估阶段抛异常后没有捕获日志写了一半。解决评估段用try-except包裹异常时跳过本行评估并打印iteration读取时执行dropna。不要因为一两行异常就重跑全部训练先把日志异常看清楚。5.2 排查顺序先看日志再查RNG最后动参数遇到曲线异常时最忌讳的是上来就调学习率。固定的排查顺序应该是先打开performance.csv检查有没有NaN、列名和实际数据对不对再固定三套随机源重跑一遍确认波动是算法本身的方差还是复现环境的问题最后才允许自己动学习率、Navg的tau、epsilon这类参数。按这个顺序排查八成异常在第一二步就能定位剩下两成才真正需要调参。这个习惯也能保证资源的对比实验在你机器上重跑时结论和原记录一致。6. 进阶把performance.csv变成收敛诊断图用曲线形态判断下一步动作6.1 双图诊断滚动均值加一阶差分单看exploitability原始曲线很难判断是否真收敛因为MCCFR的采样噪声会让曲线看起来一直在跳。把滚动均值和一阶差分并排画出来收敛状态会清楚很多import pandas as pd import numpy as np import matplotlib.pyplot as plt df pd.read_csv(performance.csv) df df.dropna(subset[exploitability]) # 滚动均值抹平MCCFR的采样抖动 df[ma] df[exploitability].rolling(50, min_periods10).mean() # 一阶差分看每一段是否还在下降 df[delta] df[ma].diff() fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].plot(df[iteration], df[exploitability], alpha0.3) axes[0].plot(df[iteration], df[ma], lw2) axes[0].set_ylabel(exploitability) axes[1].plot(df[iteration], df[delta]) axes[1].axhline(0, colorgray, lw0.8) axes[1].set_ylabel(delta (rolling)) plt.show()滚动窗口50是相对保守的设置如果训练轮次只有2000可以缩小到20。核心逻辑是左图看绝对水平右图看趋势是否归零。差分曲线持续在0以下说明exploitability还在下降差分在0附近震荡说明进入了平台期。6.2 从曲线形态读下一步动作曲线形态判断下一步动作滚动均值持续下降差分恒为负正常收敛维持参数继续训练滚动均值进入平台差分在0附近震荡收敛停滞降低学习率或调大Navg的tau滚动均值上升差分为正训练异常先查RNG和BR实现再动网络结构平台期并不等于失败。DeepCFR在Leduc上通常先快速下降后在某个数值附近长期横盘这是遗憾网络接近表达能力上限的信号。此时优先考虑降低遗憾网络学习率或者增加特征向量维度而不是盲目加大迭代轮次。如果差分曲线持续为正则大概率是评估器或者种子问题重跑之前把BR实现再核对一遍。从那以后我每次跑DeepCFR实验都强制自己对着一张诊断图说话先看滚动均值有没有进入平台期再看差分有没有归零然后才允许自己动学习率。这个习惯帮我少翻了好几次车也让我重跑资源里的对比实验时能快速定位是参数问题还是环境问题。希望帮到你。本文还有配套的精品资源点击获取