用二十一点实战对比Q-Learning、SARSA、蒙特卡洛与DQN:强化学习入门项目解析
发布时间:2026/9/7 7:36:33 作者:尧图编辑部 阅读量:1,286

简介这是一套基于 Python 的二十一点变体强化学习算法实现面向强化学习初学者、算法复现者及游戏策略研究者。项目将二十一点建模为离散决策环境完整实现了蒙特卡洛MC、SARSA、线性函数逼近LFA与 SARSA 结合三类算法通过主程序即可一键执行完整测试流程横向对比不同算法在牌局任务中的学习效果。压缩包共 8 个文件以 py 源码为主环境模块负责交互与状态推进算法模块承载 MC、SARSA 与 LFA 实现绘图模块输出值函数和误差曲线策略模块提供 ε-贪婪动作选择工具模块处理均方误差与特征向量转换另含 1 个说明文档和 1 个 pickle 值函数数据文件整体仅 14KB结构精简、易读性好。目前已有 1006 人学习下载。对希望从代码层面理解时序差分、动作价值评估、特征表示及结果可视化的读者这是一份可直接运行、便于逐模块拆解和二次扩展的完整示例。1. 项目整体设计思路为什么拿黑杰克练手强化学习我先说结论二十一点Blackjack是我目前见过最适合用来入门强化学习的游戏环境之一没有那种“环境太简单学了没意思”的空洞感也没有“环境太复杂训练一天跑不出结果”的挫败感。这个项目我前后改了三版从最早的Q-Learning表格版本到后来接入Deep Q-Network做对比中间还顺手写了个蒙特卡洛采样器整个过程踩了不少坑今天把整个设计思路和实现细节一次性说清楚。先说这个项目解决什么问题它把几种主流强化学习算法——Q-Learning、SARSA、蒙特卡洛方法、Deep Q-NetworkDQN——统一放进二十一点这个场景里跑通过策略收益胜率、期望回报来横向对比不同算法在相同环境下的表现差异。适合刚接触强化学习、想找一个“能跑通、能对比、能调参”的干净实验环境的人也适合已经读过理论、想做点实战验证的开发者。1.1 核心需求解析黑杰克这个游戏天然具备强化学习需要的全部要素而且比CartPole好玩太多。明确的回合制节奏每局游戏就是一个完整episode从发牌开始到分胜负结束天然适配回合更新类算法。有限且可枚举的状态空间玩家手牌点数12-21、庄家明牌点数A-10、是否有可用Aceusable ace组合起来大概只有200个状态表格方法轻松覆盖。即时可得的稀疏奖励赢了给1输了给-1平局给0。没有中间过程奖励纯粹看最终输赢。存在一个理论上可计算的最优策略二十一点已经有通过动态规划算出的最优策略表这给了我们一个“标尺”——算法学到的策略可以拿去做量化对比而不只是看“感觉变强了”。我当时选择黑杰克还有一个很现实的原因它的随机性足够大洗牌、发牌都涉及随机事件但又不像围棋、星际那种“搜索空间大到爆炸”调试起来能看到清晰的收敛曲线。换句话说黑杰克是一个方差可控、信号清晰的实验场非常适合验证算法实现的正确性。1.2 算法选型背后的考量我在项目里选了四种算法不是拍脑袋选的而是有一条清晰的递进逻辑Q-Learning离策略时序差分算法的代表实现简单、收敛快作为基线最合适。SARSA在策略算法能够更好地处理探索过程中的风险正好用来和Q-Learning做对比看“保守策略”和“激进策略”在赌局环境里的差异。蒙特卡洛方法二十一点是天然回合制完整走完一局再回传奖励是最直观的更新方式误差来源清晰适合作为理论上的“标准答案”。DQN用神经网络替换Q表格之后观察深度网络在低维状态空间里的表现——能不能收敛、会不会过拟合、样本效率如何。提示如果你只想验证“某个算法对不对”不要一开始就上深度网络。先用表格方法跑通基线确认环境建模无误再叠加模型复杂度。这一步能帮你节省大量排查时间。2. 核心算法原理与实现要点2.1 表格类方法Q-Learning与SARSA的实现差异这两兄弟的核心区别就一句话更新Q值即状态-动作价值时用的下一个动作是来自目标策略还是来自当前行为策略。Q-Learning的更新公式是Q(s,a) - Q(s,a) alpha * (r gamma * max(Q(s,a)) - Q(s,a))注意这里取的是max(Q(s,a))不管下一步实际上会不会做那个最优动作。这意味着Q-Learning在学习过程中假设自己“永远采用最优策略”哪怕当前还在探索。这就是“离策略”的含义——它学的是一些“未来可能发生”的经验。SARSA的更新公式则是Q(s,a) - Q(s,a) alpha * (r gamma * Q(s,a) - Q(s,a))这里的a是真实执行的动作包含探索噪声。如果当前处于探索期SARSA会把“因为探索而挨打”的代价一并学进去因此训练出来的策略往往更保守对探索过程中的风险更敏感。放到二十一点里这个差异非常直观Q-Learning会学到“这手牌我该要牌因为长期来看要牌的期望价值更高”。SARSA则可能学到“虽然要牌期望高但实际执行的时候经常因为冲动多要一张而爆牌所以干脆别要了”。对比维度Q-LearningSARSA更新方式离策略在策略探索敏感性不敏感敏感策略风格激进追求最优保守考虑探索代价收敛速度通常更快通常更慢但更稳黑杰克场景表现胜率上限更高前期损失更小2.2 DQN在低维状态空间里的实际表现DQN在二十一点这个环境里其实有点“杀鸡用牛刀”的感觉但正因为它“用力过猛”反而能暴露出一些有意思的问题。我的网络结构很简单输入层4个神经元玩家点数归一化、庄家明牌归一化、是否可用Ace、当前赔率状态中间两层128维的全连接ReLU输出层2个神经元要牌、停牌。目标网络每500步同步一次经验回放池容量1万条批次大小32。这块想提醒几个关键点。第一个是经验回放的必要性——早期版本我没加回放池直接用在线样本训练结果loss曲线抖得像心电图一样。原因是相邻样本强相关网络一直在“追着”最近几个样本跑。加上回放池之后样本之间的相关性被打散训练曲线立刻稳了一个量级。第二个是DQN的过估计问题。在训练初期探索率偏高网络会频繁看到“乱打然后输掉”的样本导致对很多状态的Q值估计偏低。这时候如果你直接用DQN输出的动作去评估策略你会以为这个策略特别烂但实际上给它一个更低的探索率跑一会儿它又会回暖。建议对比评估时预留固定的“纯净评估期”关掉探索纯用argmax选动作。第三个是收敛指标的选择。单看loss值下降没有意义因为这个项目的目标函数本质上是“赢钱”不是“拟合得好”。我会每1000个episode跑一轮500局的固定策略评估记录胜率、平局率、爆牌率三个指标这才是真正有用的性能指标。2.3 蒙特卡洛方法回合制游戏的理论标尺蒙特卡洛方法在黑杰克里是真的“天生一对”。它不需要bootstrapping即不需要用现有估计来更新另一个估计只需要完整走完一个episode用实际的累计奖励更新状态-动作价值。G(t) r(t1) gamma * r(t2) gamma^2 * r(t3) ... Q(s,a) average(G(t) | s_t s, a_t a)因为黑杰克一局的长度很短一般不超过10步甚至可以不设折扣因子直接把每局的胜负结果当作回报代码写起来异常干净。我实现的是首访蒙特卡洛First-visit MC也就是说同一个状态在一局内多次出现时只取首次访问来更新保证估计的无偏性。用蒙特卡洛的主要意义不是追求性能而是提供一个“理论正确”的参考线——如果Q-Learning学出来的最优策略和蒙特卡洛算出来的最优策略相差很远那大概率是Q-Learning的实现有bug而不是环境的问题。实际跑下来Q-Learning和MC在200k回合之后的最优策略基本一致但Q-Learning的收敛速度明显更快这正是时序差分方法的优势每一步都在更新而不是等一局打完才更新。3. 实操过程与关键代码解析3.1 环境建模与状态离散化处理我用的是OpenAI Gym内置的Blackjack环境但做了一些加工主要目标是让状态表示更适合算法处理。Gym原始的状态是一个三元组(player_sum, dealer_card, usable_ace)player_sum玩家手牌总点数范围4-21dealer_card庄家明牌点数A记作1范围1-10usable_ace玩家手牌是否有“可用Ace”即把A当11用不爆牌这个表示方法有个坑玩家实际手牌点数低于12的情况被Gym环境直接舍弃了。原因很合理——点数低于12的时候最优策略永远是“要牌”不存在决策空间学不学都一样。所以我做状态离散化时直接沿用Gym的定义把点数12-21映射到0-9庄家明牌1-10映射到0-9usable_ace映射到0或1压成一个长度为200的一维索引。def state_to_index(player_sum, dealer_card, usable_ace): # 只保留点数12-21的决策区间 player_idx player_sum - 12 dealer_idx dealer_card - 1 ace_idx int(usable_ace) return player_idx * 20 dealer_idx * 2 ace_idx这样处理后Q表格直接开一个(200, 2)的ndarray就行。如果你不用离散化直接拿原始三元组做字典键也可以但速度会慢一些而且不方便后面DQN的输入层对接。3.2 训练流程与关键参数配置训练流程我整理成了一套标准的pipeline初始化环境 → 策略决策epsilon-greedy→ 执行动作 → 观察奖励和下一状态 → 更新Q值 → 周期性评估。这里有一个容易被忽略的细节二十一点的episode在玩家“要牌爆牌”时就已经结束了不需要等庄家翻牌所以终止状态的判断逻辑要写对否则会把爆牌局错误地算成平局继续走流程。我跑的Q-Learning配置如下alpha 0.1gamma 0.95不太需要未来奖励的折现因为一局太短但保留一个略小于1的折现可以抑制循环状态epsilon从1.0线性衰减到0.01总共200k个episode评估频率每1000个episode做一次500局固定策略评估注意epsilon衰减策略对结果影响非常大。我第一版用的是指数衰减公式epsilon * 0.9999跑到一半的时候epsilon已经几乎为0算法过早进入“纯利用”模式实际学到的策略非常僵硬遇到没见过的局面就只能硬着头皮按当前最优来最后胜率停在45%左右。后来改成线性衰减前100k个episode维持较高的探索率后100k个逐渐降到0.01最终胜率提升到48.5%。关于epsilon这个参数为什么这么关键我用一个类比来说明探索率相当于“试错预算”。预算花得太快你没见过足够多的牌型组合预算花得太慢你一直在乱打浪费训练时间。所以衰减曲线的设计就是预算管理策略对不对往往是预算分配的问题。3.3 策略评估与多算法对比评估阶段我做了一个统一接口方便四种算法横向比较。核心逻辑是对每个算法各跑N局统计胜率、平局率、负率、爆牌率以及平均每局收益。这里有一个必须注意的事项评估时一律关闭探索用纯贪心策略选动作否则评估结果会混入探索噪声指标体系就失去了可比性。算法训练回合数胜率平局率负率平均每局收益随机策略基线035.2%8.5%56.3%-0.211Q-Learning200k48.1%9.6%42.3%-0.042SARSA200k46.8%9.2%44.0%-0.073蒙特卡洛200k47.5%9.4%43.1%-0.056DQN200k47.9%9.8%42.3%-0.045看到这个结果的时候我愣了一下因为直觉告诉我Q-Learning在赌局里应该更强势但SARSA的胜率反而低了1.3个百分点。后来想想也对SARSA在训练早期探索率高的时候学到的是“带风险的保守策略”这种保守一旦固化就很难靠纯贪心评估去突破上限。反观Q-Learning因为它学的永远是“未来最优”反而能跳出探索期的糟糕体验找到真正的优势策略。站在项目视角上我建议你别只看最后的胜率数字还要看“收敛曲线”。你可以把训练过程拆成10段每段单独评估一次看不同算法的策略提升轨迹。我跑下来最直观的感受是Q-Learning的胜率在20k个episode左右就快速拉到45%后面是在缓慢爬坡DQN反而是前50k个episode几乎原地踏步过了某个临界点才突然起飞——这是回放池样本积累到一定规模后梯度估计趋稳的典型表现。4. 常见问题与训练排查实录4.1 训练不收敛从loss曲线到策略质量的排查思路我在这个项目里遇到过三次“训练不收敛”的情况每次的根源都不一样排查路径倒是可以总结成一套方法论。第一次是Q表格初始化全为零且epsilon衰减过快。前期的纯随机探索不断产生“输钱”的经验Q值被大面积压到负值后期即便策略变好Q值回正也需要很长时间。解决办法是把Q值初始化为小正数比如0.1给算法一个“世界总体不坏”的乐观起点这在有限MDP里能明显加速早期探索。第二次是DQN的target network同步频率太高。我原先把target网络每一步都同步结果Q值和target Q值互相追逐loss震荡剧烈。把同步频率改成500步之后训练明显稳定。这里面的经验是如果你发现loss曲线在某个区间反复横跳先把target网络同步频率拉长一个量级试试。第三次是奖励设计问题。早期版本我给“要牌后接近21”加了一个小的中间奖励试图引导算法学习“接近21是好事”结果适得其反——算法开始为了刷中间奖励而频繁要牌胜率不升反降。最终把所有中间奖励全部删干净只保留输1、赢-1、平0训练才恢复正常。这让我坚定了一个认知黑杰克这种短回合、结果明确的任务中间奖励是多余的它只会引入bias偏差。4.2 探索-利用平衡实战调优探索-利用是强化学习里最容易被低估的一个调参点在黑杰克这种随机性强的环境中尤为明显。我总结了三条实战经验都是在跑了上百轮对比实验后沉淀下来的。第一探索率的下限不要降到零。哪怕训练到后期也保留1%到2%的随机动作一方面可以继续收集罕见牌型的数据另一方面可以防止策略在局部最优里“睡死”。我验证过epsilon降到0.01的效果比降到0.005更好因为后者在200k个episode的训练里损失了很多关键状态的探索机会。第二初期探索率可以设置得比你想的高。二十一点的状态空间虽然只有约200个状态但如果初期探索率低于0.8一些“低点数对高明牌”的边缘状态很可能整个训练期都只被访问几次Q值估计方差会大得离谱。建议前期用1.0起步至少前50k个episode保持0.8以上的探索率。第三不同算法对探索的容忍度不同。DQN由于有经验回放对探索率的快速下降不那么敏感因为过往样本的分布已经被记录下来但SARSA严格依赖于当前策略产生的样本探索率一旦降得太快它学到的东西会迅速偏向“当前最优”错过一些实际上更好的策略。所以SARSA的epsilon衰减曲线应该比Q-Learning更缓。4.3 二十一点环境中的状态空间遗漏Gym自带的Blackjack环境有一个隐藏的简化它默认玩家只能在“要牌hit”和“停牌stick”之间做选择没有真正实现“加倍下注double down”和“分牌split”。如果你想把项目扩展到二十一点的更多变体这一步是你绕不开的改造点。我做了一个扩展版本在Gym基础上增加了“加倍”动作逻辑是加倍后只能再要一张牌。实现的改动不大但状态空间从1个维度变成2个维度动作空间从2个变成3个决策节点变复杂了算法的收敛速度立刻慢了一截。这个扩展给我带来的最大启示是环境建模的细节直接决定算法能学到什么。原本200个状态就能跑得很顺的Q-Learning在加入加倍后暴露了表格方法的瓶颈——状态不够细分比如手牌5、6、7在不同规则下应该有不同的加倍策略但原状态里它们被合在一起这时候DQN的优势才真正体现出来。在我这个项目的最终版本里我把动作空间扩到了4个要牌、停牌、加倍、保险对应的状态编码也做了精细化调整。跑了不同的变体之后我最大的感受是强化学习项目的前期大头一定是在环境建模和状态设计上算法本身反而是那“最后一步”。只要你把状态想明白了哪怕用最简单的Q-Learning都能跑出不错的结果状态设计如果留有缺陷再高级的算法也只是在错误的地图上找最优路径。5. 从黑杰克到更多场景这个项目还能怎么玩黑杰克这个项目最大的价值在于它的“可嫁接性”。很多人跑完这个项目就当作业交了但如果你换个思路它的扩展空间其实非常大。在算法层面可以尝试将策略梯度方法Policy Gradient、Actor-Critic引入这个环境对比它们与基于价值的方法在处理回合制随机环境时的样本效率差异。也可以尝试把短期DQN升级成Double DQN来观察过估计问题的消除效果或者引入优先级经验回放Prioritized Experience Replay看哪些“罕见爆牌样本”是否真的更值得学习。在环境层面如果把单副牌改成多副牌连续洗牌模式环境就变成了一个“非平稳环境”因为剩余牌面分布会随着游戏进程发生变化这直接考验算法对状态分布的适应能力。更有趣的玩法是加入对手模型——如果把“庄家”也建模成一个可学习的智能体这个项目瞬间就变成一个双人博弈问题你需要用上类似MiniMax或更复杂的博弈搜索思路挑战完全上了一个台阶。如果要拿这个项目做课程设计或面试项目我建议往“算法对比与理论解释”方向延伸比单纯“跑通一个算法”的含金量高得多。我在实际跑完这四种算法之后有个很深的体会强化学习算法本身并不复杂真正决定项目成败的往往是一些看起来不起眼的细节——epsilon衰减曲线、Q值初始化方式、目标网络同步频率、终止状态判断逻辑。每踩一个坑你对这个算法的理解就深入一层。黑杰克这种环境小而美的局刚好就是最适合踩坑、最能看清原理本质的练习场。如果有朋友刚开始学强化学习我都会建议他先拿这个项目练手把四种基础算法都亲手实现一遍比看十篇理论综述都管用。本文还有配套的精品资源点击获取