拓冰建站拓冰建站
首页 / 资讯中心 / 正文

DeepCFR算法实战:从反事实遗憾到神经网络逼近与调参技巧

简介这份以Python深度强化学习优化德州扑克AI算法为主题的资源围绕DeepCFRagent3.py智能体实现展开面向需要完成毕设、课程设计或工程实训的算法学习者可解决传统CFR系列算法在复杂牌局中计算开销大、策略收敛慢的实际痛点。资源系统比较了CFR、CFR、MCCFR、DeepCFR等经典方法通过exploitability量化与纳什均衡的差距并在Limit与NoLimit Leduc Holdem、Limit Holdem等场景中采用reward对抗评估完整呈现从算法改进到实验验证的科研思路。压缩包共166个文件容量14MB以58个Python脚本为核心辅以48个pth模型权重、18个csv实验记录、18个txt说明、16个pkl数据以及json/xlsx等文档结构清晰便于按目录检索。目前已有139人学习下载借助源码、权重与实验数据可快速复现对比实验、观察训练曲线并在此基础上展开进一步优化。资源中附带的实验数据覆盖多种牌局规模为后续算法研究提供了可复用基线。1. 德州扑克AI为什么难在“不完美信息”德州扑克最难的不是牌的随机性而是对手底牌不可见造成的“不完美信息”。状态树规模大、奖励极度稀疏普通强化学习直接训练容易崩。CFR家族算法通过反事实遗憾逐步逼近纳什均衡但传统CFR需要维护两张大表状态稍大就爆内存。DeepCFR用神经网络替代表格把这个问题的可解规模往上推了一大截。这篇文章以我拆过的DeepCFRagent3为线索讲清楚DeepCFR的核心机制、实验评估口径以及限注和无限注在评估上的不同选择最终落到几个能直接落地的调参技巧。2. DeepCFR在算啥反事实遗憾、神经网络与目标逼近2.1 反事实遗憾CFR的更新到底在做什么CFR的核心不是去拟合最优策略而是迭代计算“如果我在某个信息集选了别的动作能多赢多少”。这个差值被记作反事实遗憾值。传统做法是在每个信息集维护一张表每个动作一行值每轮遍历后按正遗憾比例重新算策略。下面这个伪代码体现了最核心的更新逻辑# cfr_update.py def cfr_update(info_set, action, reach_prob, chance_prob): strategy info_set.get_strategy() # 反事实值假设当前信息集到达该动作的期望收益 cfv_action compute_cfv(info_set, action, reach_prob, chance_prob) # 期望值按当前策略加权的平均反事实值 cfv_avg sum( strategy[a] * compute_cfv(info_set, a, reach_prob, chance_prob) for a in info_set.actions ) # 反事实遗憾按到达概率加权累加 info_set.regret[action] reach_prob * chance_prob * (cfv_action - cfv_avg) # 同时累计策略和用于最后输出平均策略 info_set.strategy_sum[action] reach_prob * strategy[action]这里的关键参数是reach_prob和chance_prob前者表示当前玩家能到达该信息集的概率后者表示公共牌和发牌器等随机节点给出当前结果概率。二者相乘得到的是采样概率权重所以遗憾值的累加其实是“期望遗憾”的无偏估计。不把这两个概率乘进去相当于默认所有信息集到达概率一样而在德州扑克里深位置和盲注位置的决策价值天然不同这会让策略偏掉。CFR 的差异在于把负遗憾截断成 0并引入线性平均权重MCCFR 则用采样代替完整遍历减小单轮计算量。这些改进思路都保留在 DeepCFR 中只是把“维护表”换成了“训练网络”。2.2 DeepCFR 的两个神经网络各负责什么DeepCFR 把 CFR 的表格拆成两个网络一个是遗憾网络regret_net输入当前信息集的特征输出每个动作的遗憾值另一个是平均策略网络avg_policy_net输入同样特征输出动作概率分布。它们的训练目标不同所以损失函数不能混用。下面是一个常见的更新片段# deepcfr_train.py optimizer_r.zero_grad() regret_pred regret_net(state_features) regret_loss torch.mean((regret_pred - regret_targets) ** 2) regret_loss.backward() optimizer_r.step() if step % avg_update_interval 0: optimizer_a.zero_grad() log_probs torch.log_softmax(avg_policy_net(state_features), dim-1) avg_loss -torch.mean(torch.sum(regret_targets.detach() * log_probs, dim-1)) avg_loss.backward() optimizer_a.step()注意平均策略网络的更新目标不是策略的交叉熵而是让平均策略尽可能贴合遗憾值分布。PyTorch 里必须用detach()把遗憾网络梯度隔断否则两个网络会互相拉扯训练早期直接发散。regret_loss用 MSE 其实是一种简化因为遗憾值不是标签而是通过采样生成的估计值有些实现会改用 Huber Loss对小样本带来的异常值更稳。在 OpenSpiel 或自制 Python 引擎里state_features通常包含玩家手里的牌编码、公共牌编码、下注轮次、筹码量、各玩家已下注金额等。这里不建议直接丢 one-hot 矩阵会让网络参数量爆炸收敛非常慢。2.3 DeepCFRagent3 改进在哪网络容量和采样效率我读/实验环境/agents/DeepCFRagent3.py时最直观的感受是它没有改动 DeepCFR 的整体框架而是把工程细节做扎实了。与基础版对比变化集中在三个方向网络结构加了归一化和残差连接遗憾网络和平均策略网络使用不同的学习率以及对外部采样到的样本做重要性加权。下面这张表可以把差异看得更清楚项目DeepCFRagentDeepCFRagent3网络结构两层 MLPReLU三层 MLP LayerNorm梯度裁剪无max_grad_norm2.0遗憾网络学习率与策略网络相同相对策略网络放大 2 倍样本缓存单一大 buffer按遍历深度分层采样平均策略更新每个 batch 都更新每 16 个 batch 延迟更新LayerNorm 的作用是让特征在批量较小时也不至于出现某些维度数值过大或过小。梯度裁剪是我建议所有复现者都加上的DeepCFR 训练时遗憾目标经常出现极端值比如某个动作前几轮完全没被采样突然来一个高遗憾梯度一步能把网络推到全是 NaN。另外按遍历深度分层采样是 agent3 一个比较聪明的点——靠近根节点的信息集出现的次数少但重要性高靠近终止节点的信息集出现频繁但单个对整体策略影响小混在一起训练时后者会淹没前者的梯度信号。# deepcfragent3_layernorm.py import torch.nn as nn class RegretNet(nn.Module): def __init__(self, input_dim, hidden_dim, action_dim): super().__init__() self.block nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.LayerNorm(hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.LayerNorm(hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, action_dim) ) def forward(self, x): return self.block(x)LayerNorm放在激活函数之前而不是之后是我在调试时发现的一个重要细节。放在 ReLU 之后的归一化会让负值直接丢失损失函数反向传播时梯度也很容易消失。如果你看到的实现里把 LayerNorm 放在激活后多数情况是直接迁移了 NLP 的做法在 CFR 这种小批量回归任务里效果不稳定。3. 在 Python 实验环境中复现评估exploitability 与 reward 两条线3.1 实验目录里的 Agent 结构摘要中给出的 agent 位于/实验环境/agents/DeepCFRagent3.py这说明实验环境是一个可复现的工程目录而不是单文件脚本。典型结构是agents/存放参与对比的智能体games/实现扑克规则evaluation/放评估脚本。需要确认的是DeepCFRagent3是否继承了DeepCFRagent的接口我在类似项目里通常会要求两个类暴露相同方法# agents/base_agent.py class BaseAgent: def step(self, observation, legal_actions) - int: raise NotImplementedError def train(self, game, iterations) - dict: raise NotImplementedError def eval(self, game, n_episodes) - float: raise NotImplementedError这样评估脚本才能无差别调用不同 agent。如果继承的时候只覆盖了train而忘了给step传入平均策略网络最容易踩的坑是训练完的 agent 依旧用随机初始化网络在做决策exploitability 看起来完全没有下降。3.2 exploitability 怎么算出来的exploitability 衡量的是当前策略和纳什均衡之间的距离。直观理解是让一个能看穿你全部策略的最优反策略和你的策略对阵联游戏整体期望收益不为零的部分就是可被利用的漏洞。在 Limit Leduc Holdem 和 NoLimit Leduc Holdem 中因为博弈树规模小可以用遍历法精确计算 exploitability所以不需要随机很强的对手。在 OpenSpiel 里计算非常直接python -c from open_spiel.python import policy from open_spiel.python.algorithms import exploitability env pyspiel.load_game(leduc_holdem) policy policy.PolicyFromAgent(agent) print(exploitability.exploitability(env, policy)) 但自建环境没有现成接口时需要自己实现 best response 计算。我的经验是不要求一开始就写通用版本先在 Limit Leduc 上把规则写死验证结果符合预期后再扩展。exploitability 的单位是“平均到底池每注的净收益”通常接近 0 表示策略越接近纳什均衡。需要注意是“nash conv”还是“per-info-set”的表达不同论文用的单位不同对比时务必对齐。3.3 训练与评估参数我复现这类实验时默认用下面的参数组合作为起点然后根据 Leduc 环境的速度做调整参数推荐值影响traversal_iterations1000每次评估前遍历的博弈树次数regret_net_hidden128隐藏层维度过小欠拟合avg_policy_lr0.01平均策略网络学习率regret_lr0.02遗憾网络学习率通常比策略网络高buffer_size20000经验缓存容量太大占用内存batch_size512训练批次小批量样本方差大avg_update_interval16平均策略网络延迟更新步数之前我跑过一组对比把batch_size从 512 降到 64 后exploitability 收敛速度明显变慢但内存占用也显著下降。在只能使用 CPU 训练的环境里小批量反而可以用更频繁的网络更新弥补所以这两个指标要放在一起调。4. Limit 与 NoLimit 的评估口径不是一个 exploitability 走天下4.1 Leduc 两种版本的规则差异Leduc Holdem 是专门为学术实验设计的简化德州扑克只有两种点数、三张公共牌庄家和盲注逻辑固定。Limit 版本规定每轮只能加注固定大小动作空间有限exploitability 可被精确计算NoLimit 版本允许下注任意筹码量动作空间从离散几项扩展为连续或细粒度离散值。我拆开看这两个环境时发现最恼人的不是动作变多而是下注量作为特征进入网络后和手牌特征量级完全不同。对 DeepCFR 这类算法来说特征归一化做不好exploitability 曲线会在某个水准上震荡很久。维度Limit LeducNoLimit Leduc下注动作固定 1 注或 2 注任意筹码量动作离散化3 种动作通常用分桶离散到 10~20 个动作exploitability可精确计算可精确计算但计算成本更高网络输入变化下注特征稳定下注量特征跨度大我一般在 NoLimit 环境里先对动作做对数分桶因为筹码量本身具有近似对数分布用线性分桶会导致大额下注被过度细化。4.2 为什么完整 Limit Holdem 不比较 exploitability摘要里特别提到“Limit holdem poker 环境过大使用与 RandomAgent 作战的 reward 作为评估指标”这里的重点在于完整德州扑克状态树规模太大无法在可接受时间内精确计算 best response。工程上通常退而求其次用对战表现估值让 agent 与固定随机策略对手连续对局一定局数用平均 reward 衡量提升幅度。需要注意用 reward 评估时随机对手的版本必须固定否则不同批次实验结果没有可比性。# evaluate_against_random.py import random def evaluate(agent, env, episodes500): total_reward 0.0 for _ in range(episodes): state env.new_initial_state() reset_player_state(env, random) while not state.is_terminal(): if state.is_chance_node(): state.apply_action(random.choice(state.legal_actions())) else: player state.current_player() action agent.step(state, state.legal_actions()) state.apply_action(action) total_reward state.player_reward(agent_uid) return total_reward / episodesepisodes太小时reward 波动会掩盖改进效果我的经验是至少 500 局并且在报告结果时记录方差而不是只给一个平均值。只挑一次最优结果写进实验报告在评审阶段很容易被质疑。4.3 用 exploitability 与 reward 共同判断收敛对 Leduc 版本exploitability 是金标准但 reward 也不是完全没有价值。DeepCFRagent3 训练后期可能出现 exploitability 持续下降但 reward 不涨的情况这是因为策略逐渐接近均衡后对随机行动者的胜率增量变得很小两者都进入平台期。我一般会同时记录这两个指标如果 exploitability 下降而 reward 也在下降说明策略正在走向一个过度保守的均衡解如果两者都停滞再考虑调学习率或增加遍历次数。5. DeepCFR 调优技巧让 exploitability 降得快一点5.1 用 warmup 和梯度裁剪稳定训练初期DeepCFR 的状态特征中包含下注金额和筹码量不同量纲特征在训练初期很容易让损失值剧烈波动。给 regret_net 加一个学习率 warmup前 500 步从 0 线性升到目标值能显著减少曲线前期的尖刺。结合之前 agent3 里的梯度裁剪效果更明显# train_with_warmup.py lr_scale min(1.0, step / 500) regret_optimizer.param_groups[0][lr] base_regret_lr * lr_scale avg_optimizer.param_groups[0][lr] base_avg_lr * lr_scale这里 warmup 只作用于优化器的param_groups而不是重新赋值网络层。另外如果你在训练日志里看到regret_loss降到很小但 exploitability 还在上升多半是平均策略网络更新频率太高导致它过早陷入某个局部策略。5.2 平均策略网络延迟更新的窗口选择avg_update_interval的取法决定平均策略是否平滑。我测过几组数据每隔 1 步更新时平均策略波动大exploitability 后期震荡每 16 步更新曲线明显平滑每 64 步更新则收敛变慢但最终值更稳定。建议从 16 起步观察 exploitability 曲线连续多轮不降时把间隔提高到 32 或 64通常比直接调学习率更有效。另一个容易被忽略的细节是缓存中每个动作的采样次数差异动作fold在早轮次出现频率远低于call如果不对样本做加权fold 的遗憾估计方差会很大最终策略会过分激进。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门