手把手教你训练一个神经网络,打爆21点!(附代码&链接)
来源:大数据文摘
本文介绍了代码模拟玩21点,在用朴素策略试验后转向了深度学习。
21点又名黑杰克(Blackjack),起源于法国,已流传到世界各地,有着悠久的历史。
随着互联网的发展, 二十一点开始走向网络时代,现在世界各地的赌场都可以看到二十一点。该游戏由2到6个人玩,使用除大小王之外的52张牌,游戏者的目标是使手中牌的点数之和不超过21点且尽量大。
Medium上一位程序员小哥就尝试用代码模拟玩21点,在用朴素策略试验后,小哥又将目光转向了深度学习,一起来看看吧。
上次我们开发了代码来模拟玩21点游戏,并发现了在这类赌局中获胜的关键因素。让我们先来快速回顾一下:
1、赌场的优势在于可以要求21点的玩家(根据不完整的信息)先于庄家拿牌,这使得玩家面临爆牌(bust,玩家的牌总点数超过21)的风险,有些玩家们甚至可能在庄家还未拿牌前就已经自爆了。
2、当玩家的手中牌总点数在12到16之间,并且比庄家点数小时尤为危险(玩家有可能在拿到下一张牌时自爆)。在这种情况下,如果庄家最终的总点数为大,则玩家要么继续拿牌要么放弃拿牌(停牌)。从下图可以清晰看到,总点数在12到16这一区间时玩家赢的几率最低(我们称之为“绝望之谷”)。
赢得赌局或打平的几率随玩家手牌总点数的变化(总点数为21时是必赢,概率为1)
3、最后,我们发现“仅在完全不会自爆时才要牌”的朴素策略就可极大地提高击败赌场的几率,因为这种策略将自爆的风险完全转移给了赌场。
如果你不熟悉21点游戏,那可以看我的上一篇文章,里面介绍了怎么玩以及相应的游戏规则。
文章链接:
https://towardsdatascience.com/lets-play-blackjack-with-python-913ec66c732f \t _blank
深度学习能做得更好吗?
这篇文章的主旨在于弄清楚是否可以通过深度学习找到比上面提到的朴素策略更好的策略。我们将:
用我们上次开发的21点模拟器生成数据(进行少量修正使其更适合用作训练算法)
编码并训练神经网络玩21点(最优情况下)
图解简单神经网络
在进入训练过程前,让我们先来快速讨论一下使用神经网络的利弊。神经网络是一种高度灵活的算法——就像软粘土一样,神经网络可以自我调整或进行少量转换就能适用于不同的数据集。通过神经网络可以轻松处理像线性回归这样比较刻板的问题。此外,网络层和神经元还能学习深藏于数据中的非线性关系。
但是,神经网络的这种多功能性需要一定的代价,因为它是一个黑匣子。与回归不同,在回归中我们可以通过查看回归系数来了解模型如何做出决策,而神经网络则缺乏这种透明度。同时,神经网络也存在过拟合的风险,就是对数据过度拟合以至于无法对样本数据进行很好的泛化。这些缺点虽不足以使我们放弃使用神经网络,但却值得我们牢记并设计防范措施。
生成训练数据
在训练神经网络前,我们首先需要弄清楚如何构造训练数据,这样训练出的模型才有意义。
我们想要预测什么?在我看来,我们的目标变量有两个候选参数:
输掉赌局的概率。在这种情况下,我们可能希望模型告诉我们失败的可能性是多少。再说一次,这只有在我们可以增加或减少赌注的情况下才有用,而这在21点中不适用。
实际上,我们更希望我们的神经网络能够给出正确的行为,拿牌还是停牌。因此,我们的目标变量应为“究竟是拿牌还是停牌”。
我花了一段时间才找出分析目标变量的最佳方法。下文阐述了我找到的方法。
我们需要一种让神经网络知道给定行为是否正确的方法。这种方法不需要确保万无一失,只需要大体正确即可。因此,我判断给定行为是否正确的方法是模拟一场21点游戏:将牌分发给玩家和庄家,检查是否有人拿到了21点,决定一个拿牌的行为(拿牌或停牌),如此模拟游戏直到结束并记录结果。由于模拟玩家每次只做出一个决定,因此我们可以通过他的输赢来评估该决定的质量:
如果玩家要牌并获胜,那拿牌(Y=1)是正确的决定。
如果玩家要牌但输了赌局,那停牌(Y=0)是正确的决定。
如果玩家不要牌并获胜,那停牌(Y=0)是正确的决定。
如果玩家不要牌但输了赌局,那拿牌(Y=1)是正确的决定。
我们基于此规则训练模型,其输出是对于拿牌还是停牌的预测。这次的代码与上次相似,因此在这里我不做详细介绍。
GitHub链接:
https://github.com/yiuhyuk/blackjack
代码的主要功能:
庄家的明牌(另一张暗牌,牌面朝下)
玩家手里牌点数总和
玩家手里有没有A
玩家的决定(拿牌还是停牌)
目标变量为由上述逻辑定义的正确决策。
训练神经网络
我们的神经网络将使用Keras(开源神经网络库)。先看模块导入:
再为训练神经网络构建输入变量。变量feature_list是包含上面提到的特征(X变量)名的一组列表。数据集model_df储存了所有21点模拟器产生的数据。
# Set up variables for neural netfeature_list = [i for i in model_df.columns if i not in ['dealer_card','Y','lose','correct_action'] ]train_X = np.array(model_df[feature_list])train_Y = np.array(model_df['correct_action']).reshape(-1,1)
用于实例化和训练神经网络的代码其实非常简单。第一行创建了一个序贯型神经网络,即多个网络层的线性堆叠。后面的代码对我们的模型逐层添加网络层(这里的Dense定义了最简单的网络层,即一大堆神经元),这里的数值16,128是指神经元的个数。
对于最后一层,我们需要选择一个激活函数。这个函数把神经网络的原始输出转变成某些能被我们理解的东西。最后一层有两点值得注意:第一,它只包含一个神经元,因为我们是在两个可能的结果间进行预测(二分类问题);第二,使用sigmoid激活函数,因为我们希望我们的神经网络像逻辑回归一样,在拿牌(Y=1)还是停牌(Y=0)之间进行预测 —— 或者说,我们希望知道正确行为是拿牌的概率。
最后两行代码告诉我们的神经网络用什么样的损失函数(二元交叉熵是一种用于概率输出分类模型的损失函数),并调整模型来拟合我们的数据。我没有花太多时间来调整网络层数或神经元个数,但如果你想要尝试我的代码,我觉得这可以作为模型改进的方向。
检查模型的表现
一种快速确定我们的模型是否有价值的方法是使用ROC曲线。
请查看链接:
https://towardsdatascience.com/roc-curves-and-the-efficient-frontier-7bfa1daf1d9c)
ROC曲线可以告诉我们,模型在权衡利益(真阳率True Positive Rate)和代价(假阳率False Positive Rate)时表现如何 —— 曲线下的面积越大,模型表现越好。
下图是我们的21点神经网络的ROC曲线 —— 这看上去似乎比随机猜测(红色虚线)要好不少。曲线下的面积,即AUC,达到0.73是明显高于随机猜测的AUC(0.5)。
21点神经网络的ROC曲线
我是用训练数据来画ROC曲线的。通常,我们希望用验证或测试数据去画,但在这个例子里,我们知道只要我们的样本数量足够大,它对于整体就是有代表意义的(假设21点的游戏规则不变)。并且我们还可以期望我们的模型有很好的泛化能力(任何新数据都有和我们训练数据一样的基本统计特性)。
是时候大显身手了!
在我们的神经网络正式开始21点之前,我们需要给它一个决策规则。请记住,神经网络最后一层中的sigmoid 激活函数会将输出变成“正确的行为是拿牌”的概率。因此我们需要一个决策规则,在给定这个概率的情况下,我们决定是否拿牌。
下面的函数用来确定这个决策规则,model_decision 函数的通过输入神经网络所需要的特征集来做出预测,并将作出的预测与预先给定的阈值进行比较,来决定是否拿牌。在这里我将阈值定为0.52,因为从上次的尝试中我们可以发现爆牌是21点玩家面临的最大风险。因而将0.52作为拿牌的阈值会使我们的模型选择拿牌的可能性小一些,于是爆牌的可能性也相应减小。
def model_decision(model, player_sum, has_ace, dealer_card_num): input_array = np.array([player_sum, 0, has_ace, dealer_card_num]).reshape(1,-1) predict_correct = model.predict(input_array) if predict_correct >= 0.52: return 1 else: return 0def model_decision(model, player_sum, has_ace, dealer_card_num): input_array = np.array([player_sum, 0, has_ace, dealer_card_num]).reshape(1,-1) predict_correct = model.predict(input_array) if predict_correct >= 0.52: return 1 else: return 0
现在我们需要将上面的函数加入代码中是否决定拿牌的部分。所以当我们需要决定是否拿牌时,神经网络会基于庄家的明牌、玩家手中牌的总点数以及玩家是否有A这三个特征来做出决策。
我们的模型表现不俗!
最后,让我们将神经网络模型的表现与朴素策略模型和随机模型进行比较。有几点值得注意:
我对每种策略类型(神经网络、朴素、随机)都进行了约 300,000 次21点游戏模拟。
朴素策略仅仅在爆牌的概率为零时进行拿牌(在玩家手中牌的总点数小于12时拿牌,总点数大于等于12时停牌)。
随机策略是指抛硬币的结果是正面朝上时选择拿牌,否则不拿。如果选择拿牌后没有爆牌,则继续抛硬币并重复该过程。
让我们看看神经网络是否找到了更优的策略。下表展示了各种策略类型的结果分布。从中我有两个发现:首先,我们的神经网络仅输掉了不到一半的游戏(49%)。 虽然很难说我们会最终获胜,但对于一场赔率固定的游戏来说,这是相当不错的结果了;其次,神经网络实际上并不比会比朴素策略带来更多的胜局,而是能够更频繁地将对手逼成平局。
各种策略类型的结果
我们还可以观察不同策略在一些重要的特征(庄家的明牌和玩家手中牌的总点数)中的表现。 首先,让我们看一下庄家的明牌对我们三种策略获胜或平局的概率影响。 在下图中,如果庄家的明牌点数较小,神经网络的表现和朴素策略相差不大。 但是,当庄家的明牌点数较大(大于等于7)时,神经网络的表现明显更好。
获胜或平局的概率随庄家明牌点数的变化(柱形越长概率越大!)
我们还可以看看获胜或平局的概率如何随玩家初始手牌的总点数而变化。结果看起来非常棒,无论玩家初始手牌的总点数是多少,和另外两种策略相比,我们的神经网络都能表现一样好甚至更好。 反观朴素策略, 它在绝望之谷(玩家初始手牌的总点数介于12到16之间)的表现甚至不如随机策略。毫无疑问,神经网络具有更佳的表现。
获胜或平局的概率随玩家初始手牌总点数的变化(柱形越长概率越大!)
接下来的图说明了神经网络是如何胜过朴素策略的。根据我们的代码,哪怕玩家存在极小的爆牌风险,朴素策略都不愿意冒险选择拿牌。另一方面,在玩家初始手牌的总点数为12、13、14或15时,神经网络更倾向于选择拿牌。这种细微变化的决策和计算风险的能力似乎是神经网络优于朴素策略的原因。
神经网络与朴素策略选择拿牌的趋势随玩家初始手牌总点数的变化
我们可以看看当玩家手牌总点数在12到16之间,时神经网络做了什么来尝试改善我们的朴素策略(尽量少输钱给赌场)。
当庄家的明牌点数较大(8、9或10)时,神经网络非常倾向于拿牌。但即使庄家的明牌点数较小(例如3),神经网络在60%的情况下仍然选择拿牌,这是因为神经网络在做出决定时会考虑其可使用的所有特征。因此,我们无法轻易将其决策提炼为一些简单的经验法则。
神经网络选择拿牌的频率随庄家明牌的变化
结论
希望这篇文章对于使用机器学习协助实现生活中的决策给出了合适的解释。训练自己的模型时,请牢记以下几点(无论是决策树、回归还是神经网络):
是否通过预测目标变量,就能解决眼前的问题? 在开始收集数据和建立模型之前,至关重要的是要确保你选择了正确的预测目标。
实际数据与训练数据会有什么不同?如果两者差别很大,那么网络模型可能不是解决问题的正确答案。至少我们必须意识到这一点,并采取措施,例如模型的正则化和严格(并诚实)的验证以及测试集基准的选择。
不明白决策如何形成,就无法理解和利用模型训练过程中未包含的测试数据来理智地检查模型的决策。
最后我想就21点这个游戏说几句。我可能接下来一段时间不会再讨论有关赌博的话题了(我想探索的话题太多了)。 但如果有人有兴趣继续探讨这个话题(无论是否使用我的代码),可以考虑进行一些有趣的扩展:
可以尝试通过更优化的神经网络结构来改善模型,或者添加用于拆分牌A的代码(我没有将其构建到原始模拟器中),或者选择比我使用的基本特征集更优的特征集。
为模型添加计算总点数的能力,并观察总点数在一副牌和六副牌(拉斯维加斯标准)时是怎样影响模型表现的。
链接:
https://towardsdatascience.com/teaching-a-neural-net-to-play-blackjack-8ec5f39809e2
编辑:于腾凯
校对:王欣
— 完 —