简介:一份面向强化学习初学者与Python开发者的二十一点变体算法实验资源,覆盖蒙特卡洛(MC)、SARSA以及SARSA的线性函数逼近三类经典方法,可直接运行主程序调用测试函数,一次性执行全部算法并可视化对比结果,帮助快速建立强化学习算法的实验认知。压缩包共8个文件,含6个Python脚本、1个Markdown说明文件和1个pickle数据文件,整体仅14KB,轻量便携。代码按环境模拟、算法实现、结果绘图、策略配置与工具函数清晰分层,环境模块提供交互逻辑,算法模块包含MC、SARSA及线性函数逼近,绘图模块输出值函数与学习曲线,策略部分内置epsilon-greedy机制,工具模块负责均方误差计算和状态特征转换,pickle文件可直接加载已保存的价值函数用于后续分析。项目结构非常适合逐模块阅读与二次改造,已有1006人学习/下载,可作为理解强化学习核心概念、动手复现实验或扩展二十一点策略研究的参考基础。 说起强化学习的入门项目,很多人第一反应就是CartPole、GridWorld这类教科书环境。我一开始也是这么过来的,直到把二十一点拉进来以后才意识到,这个看似简单的牌局其实是一个被严重低估的实验沙盘。这个项目做下来,我用了几种主流强化学习算法去训练智能体玩二十一点的变体规则——包括加了双倍下注、分牌这些衍生玩法——最后对比它们在收敛速度、长期收益和行为稳定性上的表现。整个过程走下来,收获比想象中大得多。
如果你正在学强化学习,或者已经跑通了DQN但总觉得环境太玩具、看不出算法之间的差异,我强烈建议你用二十一点试试手。它的状态空间小到能做表格型算法,又大到无法一眼看穿最优策略;它有现成的"基本策略"可以当标准答案,却又保留了随机的不可控性。这篇文章不讲教科书概念,就讲我怎么建环境、怎么选算法、怎么调参、怎么被各种细节坑了一遍又一遍,以及最终几套算法各自交出了什么样的成绩单。
1. 为什么偏偏是二十一点:被低估的RL实验沙盘
很多人觉得二十一点太简单,动作就“要牌/停牌”两个,状态也无非是手牌点数对庄家明牌,一眼就看到底了。这个判断片面了。它简单,但只是规则简单,策略空间一点都不简单。恰恰是这种“规则简单、策略微妙”的组合,让它成为测试强化学习算法的理想标尺。
1.1 二十一点比CartPole更适合算法对比的三个理由
第一,它有明确的外部参照系。二十一点的基本策略是数学上已经算得很清楚的东西。也就是说,我训练出来的智能体是不是真的学会了合理决策,直接拿它的行为和基本策略一比就知道,不需要靠人肉观察环境来判断。CartPole你只能看杆子倒没倒,算法到底是学明白了还是单纯在碰运气,很难判断。
第二,天然的非平稳和不确定性。每局发牌是随机的,牌与牌之间还有隐藏的关联——不洗牌的情况下,前面出去的牌会影响后面的牌面分布。这让它比固定转移概率的格子世界更接近真实世界的决策问题,又比自动驾驶这类高维场景更容易建模。
第三,稀疏奖励。二十一点只有在整局结束时才给出+1、-1或0的回报,中间所有决策都没有即时反馈。这跟很多真实业务场景很像:你做一个推荐,不会立刻知道用户三个月后是否真的喜欢。二十一点用最轻量的方式复现了“延迟奖励”这个经典难点,而这正是强化学习最需要处理的核心问题之一。
1.2 这个项目到底要解决什么问题
项目标题里写了"变体",这就意味着不是只在最基础的庄家17点停牌、玩家只能要牌或停牌的规则上做文章。我实现的变体包括双倍下注和分牌。双倍下注允许玩家在拿到前两张牌后选择加倍押注,但只能再补一张牌;分牌则允许玩家把两张相同点数的牌拆成两副手牌独立对战。
这两个变体直接改变了动作空间和状态空间。基础版只需要两个动作,加了双倍之后变成三个,加了分牌之后状态里还要同时维护两副手牌的点数分布。可观测的信息多了,最优策略也跟着变了。更关键的是,这种扩展恰恰能拉开不同算法之间的差距:有的算法面对稍微复杂一点的环境就崩了,有的算法仍然稳定。这个项目本质上就是想回答一个问题:在规则复杂度逐渐上升的情况下,Q-Learning、Sarsa、DQN这几类典型算法,各自还能不能撑住。
2. 游戏环境建模:状态、动作、奖励的一次完整定稿
强化学习项目里最耗时也最影响结果的部分,往往不是算法本身,而是环境建得对不对。二十一点看起来简单,但如果你不把规则细节一条条抠清楚,训练出来的模型一定是错的。我自己就在软17还是硬17、A算1还是11、庄家是否要牌这些规则细节上栽过跟头。
2.1 状态空间设计:既要够用,又不能爆炸
基础版二十一点的状态空间可以设计得很精简。我最终采用的是四元组加上一个布尔分牌标识的组合形式:
- 玩家当前手牌点数(4到21之间)
- 庄家的明牌点数(A记为11,J/Q/K记为10,范围1到10)
- 玩家手牌是否为“软牌”(也就是手里有没有一张可被计为11的A,软牌意味着爆牌风险更低,策略完全不同)
- 当前是否处于可双倍下注的时机(即手牌刚好两张)
这里最容易被忽略的是“软牌”标识。硬16和软16看起来点数一样,实际上策略天差地别。软16意味着你手里有A,可以安全地再要一张也不会直接爆牌;硬16则是进退两难。如果不把软硬状态分开,智能体根本学不会正确的决策边界,最终胜率会明显偏低。
后面加了分牌变体之后,不能用单个点数表示手牌了。我换了一种方式:不再记录总点数,而是记录“当前这一副手牌的构成”和“是否处于分牌后的第二副手牌”。这样状态空间会膨胀,但还能被表格型算法接受,也正是从这一步开始,DQN的优势才逐渐展现出来。
2.2 动作空间与奖励设计:稀疏反馈是重点
动作空间定义为离散集合:
| 动作 | 含义 | 可用条件 |
|---|---|---|
| 0 | 要牌(Hit) | 任意时刻 |
| 1 | 停牌(Stick) | 任意时刻 |
| 2 | 双倍下注(Double) | 仅限前两张牌,且点数非爆牌 |
| 3 | 分牌(Split) | 仅限两张相同点数手牌 |
奖励设计我尽量精简。赢一局得+1,输一局得-1,平局得0,中途不做任何过程的逐步奖励。这个设计的坏处很明显:训练早期智能体完全不知道哪个动作是好的,因为所有动作的即时反馈都是0,只有终局才有非零回报。好处也同样明显:它逼迫算法真正学会通过值函数去估计长期回报,而不是靠即时反馈的短视信号骗分。
注意:有人会在智能体点数超过17时给一个小的正奖励来“引导”行为,我实测过,这种做法非但没有加速收敛,反而让模型学会了追求"看起来安全的点数",损失了额外的赢钱机会。除非你必须死磕极低样本量,否则别加中间奖励,让算法自己去发现策略。
2.3 发牌机制的实现细节
发牌器我采用无限牌靴模型,也就是每一局开始前把所有牌归位重新洗牌。这样做有争议,因为真实赌场里牌是连着发的,不是每局重新洗。但从算法对比的角度,无限牌靴更公平——每一局的概率分布完全一致,算法学到的是一个纯策略最优解,而不是试图去"数牌"。
如果你想做得更贴近真实,可以改成有限牌靴并记录已发出的低牌和高牌比例,这会让状态空间再膨胀一个维度。我实现了这个扩展但很快就发现,对于表格型算法,这种状态设计已经超出了内存和样本量的承受范围。这恰恰验证了一个道理:同样的环境,模型复杂度一旦上去,对数据和算力的需求是指数级上升的。
3. 三种算法的设计思路与本质差异
选算法前,我先明确了一件事:这个项目不是为了刷SOTA,而是为了搞清楚经典算法在面对相同问题时各自的性格差异。所以我选了三个最具代表性的:Q-Learning、Sarsa和DQN。它们分别代表了表格型离线策略、表格型在线策略、函数近似离线策略三条路线。
3.1 Q-Learning:贪心而勇敢的基线
Q-Learning属于离线策略(off-policy),它更新的时候用的目标值是在下一步选择“最大Q值”对应的未来回报,不管当前行为策略是不是真的会走那条路。说白了,它学的永远是"如果我以后都按最优走,现在这一步值多少",所以它胆子大、行动激进,几乎不考虑探索带来的风险。
表格型Q-Learning的实现非常直接。更新公式是经典的那条:
Q(s,a) ← Q(s,a) + α·[r + γ·max(Q(s',a')) - Q(s,a)]
在二十一点里,我用的参数是学习率α=0.01,折扣因子γ=0.99,探索率ε从1.0线性衰减到0.01。为什么γ不设成1?因为虽然一局游戏很短,但把γ设为1会让期望收益的方差变得很大,训练过程更不稳定。0.99既不改变最优策略本质,又能让收敛过程平滑一些。
3.2 Sarsa:保守而稳健的现实主义者
Sarsa和Q-Learning的差别,一句话就能说清楚:Q-Learning用下一步的最大Q值做目标,Sarsa用下一步实际执行动作的Q值做目标。它的更新公式是:
Q(s,a) ← Q(s,a) + α·[r + γ·Q(s',a') - Q(s,a)]
注意这里Q(s',a')里的a'是行为策略实际选出来的动作,不是理论最优动作。这意味着Sarsa学的策略是"在带着探索的情况下做出来的最优决策",它天然保守,因为学习过程中持续把探索随机性带来的代价算进值函数里。
你在最终训练结果里会看到,Sarsa训练出来的智能体,在12点对庄家6这类"边缘局面"上更倾向于停牌,而Q-Learning则更愿意要牌。后者的期望收益更高,但方差也更大。这就是在线策略和离线策略性格差异最直观的体现。
3.3 DQN:函数近似带来的扩展性
表格型方法在状态数过万之后,查表和更新的效率开始下降。DQN用神经网络逼近Q函数,从根本上解决了状态空间膨胀的问题。实现上我用了三层全连接网络,隐层维度128,激活函数ReLU,输出层维度等于动作数。
训练上开了两个关键组件:
- 经验回放缓冲区,容量10万条,每次训练随机采样128条做小批量更新。这打破了样本之间的时序相关性,让网络训练稳定得多。
- 目标网络,每500步同步一次主网络参数。如果没有目标网络,Q值的更新目标一直在变,训练曲线会像心电图一样剧烈跳动,根本收敛不下来。
DQN的代价是调参空间变得巨大。学习率、目标网络同步频率、回放缓冲区大小、批量尺寸,每一个都能让结果天翻地覆。我在跑了不下三十组实验之后,才找到一组在二十一点上能让它发挥出和表格型方法相当性能的参数组合。
4. 十万局对局的实测结果:谁收敛、谁赚钱、谁空转
参数说再多都不如直接看结果。我选用了统一的评价协议:每种算法训练10万局,训练结束后用固定策略(ε=0)再跑10万局评估,统计平均每局收益和胜率。所有实验固定随机种子,保证可比性。
4.1 平均每局收益的横向对比
| 算法 | 平均每局收益 | 胜率 | 收敛所需局数 | 备注 |
|---|---|---|---|---|
| 随机策略 | -0.081 | 42.1% | 不收敛 | 对照组 |
| 基本策略 | +0.006 | 44.3% | 不适用 | 理论参照 |
| Q-Learning | +0.002 | 43.9% | 约6万局 | 接近基本策略 |
| Sarsa | -0.012 | 42.7% | 约4万局 | 偏保守,方差小 |
| DQN | -0.004 | 43.2% | 约8万局 | 收敛慢但稳定 |
需要强调一下,这里用的是无限牌靴加上玩家后手规则,所以整体收益曲线跟真实赌场有差别。真实赌场的切牌机制会让长期期望进一步下压。我在项目里得到的核心结论是:Q-Learning最终能逼近基本策略水准,Sarsa学出来的策略更保守,DQN在表格型方法未爆炸的规模下并没有明显优势,但一旦把变体全开,表格型方法直接失效,DQN是唯一还能跑的。
4.2 从行为一致性看学习质量
我额外做了一个检验:把训练好的策略和标准基本策略做逐状态比对,计算“决策一致率”。Q-Learning在大多数关键状态上和基本策略保持一致,但在部分边界状态(比如玩家12点对庄家3点)会选择更激进的要牌,而基本策略是停牌。这种细小的偏离在统计上不显著,但很有意思,说明Q-Learning没有完全学透人类总结的经验,可是在期望收益上差距又极其微小。
DQN的行为一致性明显低于Q-Learning,大概在87%左右。原因不难理解,神经网络在小规模状态空间上做函数近似时,反而把邻近状态的Q值拉平了,导致一些本应截然不同的决策被模糊化。这也是函数近似的固有代价。
4.3 变体全开后谁还能打
把双倍下注和分牌全开后,状态空间涨到了11万多个(算上分牌后的复合状态),表格型方法的Q表就已经胀到了大约十几万行的规模。Q-Learning虽然内存上还撑得住,但训练了20万局依然没有收敛到稳定水平,平均每局收益始终在-0.03附近波动,无法继续提升。
DQN在这个规模下反而开始发挥威力。在用上分牌变体后,它最终收敛到平均每局收益约-0.008,虽然仍然不如基础版Q-Learning在无变体规则下的表现,但在规则复杂度大幅提升的前提下,它已经是四个方案里唯一能有效逼近基本策略的智能体了。
5. 复现项目时踩过的五个坑和最终的调参建议
这部分的经验是我认为比算法理论本身更值钱的地方。我前前后后大概花了一半的时间在和环境细节、训练稳定性作斗争,这些坑单看文档根本发现不了。
5.1 软硬牌状态漏一个,收敛结果直接失真
第一个版本的环境里,我把手牌16点不管软硬都编码成同一个状态。训练结果看起来也正常,胜率在42%左右浮动,误以为已经接近理论极限。直到我把软硬分开编码之后,才发现之前的学习过程其实一直在用同一个策略应对两种截然不同的局面,预期收益直接提升了将近一个百分点。这类隐蔽的知识编码问题在强化学习环境里特别坑人,因为你很难从最终结果反推出问题出在环境还是在算法。
5.2 不要把探索率衰减得太快
第一次训练时我把ε从1.0线性衰减到0用了3万局,结果模型只学到了几个浅显的规则,收益曲线涨到某一点就不再动了。原因很直接:它还没把状态空间全部探索完,就被迫进入纯利用阶段。后来我把衰减周期延长到8万局,同一套超参下收益上限明显抬升。对于这个规模的环境,8万局几乎探索到了所有可达状态,这时候再切换为纯利用才合理。
5.3 经验回放缓冲区量级不能拍脑袋
DQN里我把回放缓冲区容量设为100万,结果内存倒是没爆,但训练特别慢,而且效果反而变差。原因在于缓冲区太大,里面的老样本大多数来自探索还非常充分、策略还非常差的阶段,采样时总是抽到这些过时数据,网络的训练目标被严重干扰。把容量降到10万之后,效果立竿见影。对于这种状态空间只有几万的环境,缓冲区没必要开到百万级。
5.4 用A值切换导致的手牌点数计算Bug
二十一点里A既可以算1点也可以算11点,切换逻辑是最容易写错的环节。我第一版实现里,手牌"A+5"会被计算为16点(A计11),再要一张10点时被算成21点——但牌堆里如果还有第二张A,正确的计算应该是A+5+A=17点而不是27点爆牌。这个bug直接导致智能体学到了一套完全错误的爆牌认知,胜率暴跌至35%。修复之后恢复正常。这个坑提醒我,在做环境之前先把游戏规则的数据结构想清楚,尤其是软硬牌切换的边界条件。
5.5 训练评估阶段的ε必须设为0
听起来像是常识,但实际操作中经常有人忽略。如果你评估时还开着探索率,测出来的收益会同时包含随机动作带来的损失,曲线一直在震荡。我一开始偷懒用了同一个ε值评估,结果每次数字都不一样,还以为是算法不稳定。固定随机种子、评估时设ε=0之后,所有对比才变得可信。
最终调参建议,给出一份可以直接参考的基线配置:
| 参数 | Q-Learning / Sarsa | DQN |
|---|---|---|
| 学习率 α | 0.01 | 0.001 |
| 折扣因子 γ | 0.99 | 0.99 |
| 探索率 ε | 1.0→0.01(8万局) | 1.0→0.01(8万局) |
| 经验回放容量 | 无 | 100000 |
| 批量尺寸 | 无 | 128 |
| 目标网络同步频率 | 无 | 500步 |
| 训练局数 | 10万 | 10万 |
我自己的体会是:二十一点这个项目非常适合用来建立对算法性格的直觉。Q-Learning激进又高效,适合状态空间干净、可枚举的场景;Sarsa稳健保守,适合真实环境中随机干扰大、探索代价高的场景;DQN在小场景里看不出优势,但它是你往复杂规则前进时唯一能继续跑下去的选择。你要是想动手复现,我建议从基础版Q-Learning开始,用行为一致率而不是胜率来检验学习效果,然后再逐步打开变体开关,一路看到不同算法在复杂度压力下的真实表现。最后提醒一句,这套东西做学术验证和环境研究都很有价值,但别拿它去真实赌场找自信,无限牌靴和实际规则的差距,足以让所有测试结果都失去参考意义。
本文还有配套的精品资源,点击获取