Press "Enter" to skip to content

通过自我对弈训练一个智能体掌握井字棋

令人惊讶的是,软件代理永远不会对游戏感到厌倦。

啊!小学!这是我们学习宝贵技能的时候,比如识字、算术和玩井字游戏。

Photo by Solstice Hannan on Unsplash

在没有被老师发现的情况下与朋友进行井字游戏比赛是一门艺术。你必须在桌子下秘密传递游戏纸张,同时给人留下专心听课的印象。这种乐趣可能更多地来自于秘密行动而不是游戏本身。

我们无法教会软件代理在课堂上避免被抓,但是我们能训练一个代理来掌握这个游戏吗?

在我之前的文章中,我们研究了一个代理通过自我对战学习SumTo100游戏。那是一个简单的游戏,允许我们显示状态值,从而帮助我们建立对代理学习游戏的直觉。现在我们将挑战一个更大的状态空间,即井字游戏。

你可以在这个代码库中找到Python代码。执行训练的脚本是learn_tictactoe.sh:

#!/bin/bashdeclare -i NUMBER_OF_GAMES=30000declare -i NUMBER_OF_EPOCHS=5export PYTHONPATH='./'python preprocessing/generate_positions_expectations.py \ --outputDirectory=./learn_tictactoe/output_tictactoe_generate_positions_expectations_level0 \    --game=tictactoe \    --numberOfGames=$NUMBER_OF_GAMES \    --gamma=0.95 \    --randomSeed=1 \    --agentArchitecture=None \    --agentFilepath=None \    --opponentArchitecture=None \    --opponentFilepath=None \    --epsilons="[1.0]" \    --temperature=0 dataset_filepath="./learn_tictactoe/output_tictactoe_generate_positions_expectations_level0/dataset.csv" python train/train_agent.py \  $dataset_filepath \  --outputDirectory="./learn_tictactoe/output_tictactoe_train_agent_level1" \  --game=tictactoe \  --randomSeed=0 \  --validationRatio=0.2 \  --batchSize=64 \  --architecture=SaintAndre_1024 \  --dropoutRatio=0.5 \  --learningRate=0.0001 \  --weightDecay=0.00001 \  --numberOfEpochs=$NUMBER_OF_EPOCHS \  --startingNeuralNetworkFilepath=None   for level in {1..16}do dataset_filepath="./learn_tictactoe/output_tictactoe_generate_positions_expectations_level${level}/dataset.csv" python preprocessing/generate_positions_expectations.py \  --outputDirectory="./learn_tictactoe/output_tictactoe_generate_positions_expectations_level${level}" \  --game=tictactoe \  --numberOfGames=$NUMBER_OF_GAMES \  --gamma=0.95 \  --randomSeed=0 \  --agentArchitecture=SaintAndre_1024 \  --agentFilepath="./learn_tictactoe/output_tictactoe_train_agent_level${level}/SaintAndre_1024.pth" \  --opponentArchitecture=SaintAndre_1024 \  --opponentFilepath="./learn_tictactoe/output_tictactoe_train_agent_level${level}/SaintAndre_1024.pth" \  --epsilons="[0.5, 0.5, 0.1]" \  --temperature=0    declare -i next_level=$((level + 1)) python train/train_agent.py \  "./learn_tictactoe/output_tictactoe_generate_positions_expectations_level${level}/dataset.csv" \  --outputDirectory="./learn_tictactoe/output_tictactoe_train_agent_level${next_level}" \  --game=tictactoe \  --randomSeed=0 \  --validationRatio=0.2 \  --batchSize=64 \  --architecture=SaintAndre_1024 \  --dropoutRatio=0.5 \  --learningRate=0.0001 \  --weightDecay=0.00001 \  --numberOfEpochs=$NUMBER_OF_EPOCHS \  --startingNeuralNetworkFilepath="./learn_tictactoe/output_tictactoe_train_agent_level${level}/SaintAndre_1024.pth"  done

脚本循环调用两个程序:

  • generate_positions_expectations.py:模拟比赛并存储具有折扣预期回报的游戏状态。
  • train_agent.py:在最新生成的数据集上对神经网络进行几个时期的训练。

训练循环

代理通过生成比赛和训练来学习游戏,以预测比赛结果:

图1:比赛生成和神经网络训练的循环。图像由作者提供。

比赛生成

循环从模拟随机选手之间的比赛开始,即从给定游戏状态的合法行动列表中随机选择。

为什么我们要生成随机播放的比赛?

这个项目是关于通过自我对弈学习,所以我们不能给代理任何关于如何玩的先验信息。在第一个循环中,由于代理对好坏举动一无所知,比赛必须通过随机播放生成。

图2显示了随机选手之间的比赛示例:

图2:随机播放的井字棋比赛示例。图像由作者提供。

通过观察这场比赛,我们可以学到什么?从“X”玩家的角度来看,我们可以假设这是一个糟糕的例子,因为它以失败告终。我们不知道哪些举动导致了失败,所以我们假设“X”玩家做出的所有决策都是错误的。如果有些决策是正确的,我们会根据统计数据(其他模拟可能会经历类似的状态)来更正它们的预测状态值。

玩家“X”的最后一个动作被赋予值-1。其他动作会收到一个折扣负值,其值按照γ(gamma)∈ [0, 1]的几何递减因子向后推进到第一个动作。

图3:游戏状态的目标值。图像由作者提供。

从导致胜利的比赛中获得类似的正折扣值。从平局中抽取的状态被赋予值0。代理以第一和第二玩家的视角进行观察。

游戏状态作为张量

我们需要一个张量表示游戏状态。我们将使用一个[2x3x3]的张量,其中第一维表示通道(0表示“X”,1表示“O”),另外两个维度表示行和列。一个(行,列)方格的占用被编码为(通道,行,列)条目中的1。

图4:[2x3x3]张量表示的游戏状态。图像由作者提供。

通过比赛生成获得的(状态张量,目标值)对构成了每一轮神经网络将进行训练的数据集。数据集在循环开始时建立,在前几轮中已经发生的学习中获得优势。虽然第一轮纯随机游戏生成,但随后的轮次会逐渐生成更加真实的比赛。

注入随机性到游戏中

比赛生成的第一轮对抗是随机选手。随后的对抗是代理程序与自身对战(因此称为“自我对抗”)。代理程序配备有一个经过训练的回归神经网络,用于预测比赛结果,从而使其能够选择产生最高期望值的合法动作。为了促进多样性,代理程序基于ε-贪婪算法选择动作:以概率(1-ε)选择最佳动作;否则选择随机动作。

训练

图5显示了在最多17轮训练中五个时期的验证损失的演变:

图5:不同训练轮次的验证损失的演变。作者提供的图片。

我们可以看到,前几轮训练显示验证损失迅速减小,然后在均方误差损失约为0.2的水平上趋于平稳。这种趋势表明,代理程序的回归神经网络在预测自身对战的比赛结果时变得更加准确,从给定的游戏状态。由于双方动作都是非确定性的,比赛结果的可预测性是有限的。这解释了为什么验证损失在一些轮次后停止改善。

从一轮到下一轮的改进

对于SumTo100游戏,我们可以在一维网格上表示状态。然而,对于井字棋,我们不能直接显示状态值的演变。我们可以做的一件事是让代理程序与其先前版本对战,并观察胜利和失败之间的差异,以衡量改进程度。

使用ε = 0.5作为双方玩家的第一个动作,剩余比赛中使用ε = 0.1,每次比较运行1000场比赛,结果如下:

图6:代理程序与其先前版本的比较。作者提供的图片。

胜利次数超过失败次数(显示改进)直到第10轮训练。之后,代理程序没有在每一轮中改进。

测试

现在是时候看看我们的代理程序如何玩井字棋了!

拥有回归神经网络的一个有用特性是可以显示代理程序对每个合法动作的评估。让我们与代理程序对战,展示它如何判断自己的选项。

手动对战

代理程序先手,以’X’进行下棋:

图7:与代理程序对战,显示动作评估。作者提供的图片。

这就是你被一个无情的井字棋机器碾压的方式!

当我在(1, 0)的方格中放入’O’时,预期回报从0.142增加到0.419,我的命运就被决定了。

让我们看看当代理程序扮演第二个玩家时会发生什么:

图8:与代理程序对战,显示动作评估。作者提供的图片。

它没有掉入陷阱,比赛以平局结束。

与随机玩家的比赛

如果我们模拟与随机玩家进行大量比赛,结果如下:

图9:与随机玩家进行1000场比赛的结果。图片由作者提供。

在1000场比赛中(代理程序在一半的比赛中先手),代理程序赢了950场比赛,输掉了0场比赛,平局有50场。这并不能证明我们的代理程序在进行最优化的游戏,但它肯定已经达到了一个不错的水平。

结论

作为对《通过自我博弈训练代理程序掌握简单游戏》的后续,那个游戏容易破解且状态空间较小,我们使用了相同的技术来掌握井字棋。虽然这仍然是一个玩具问题,但井字棋的状态空间足够大,需要代理程序的回归神经网络来找到状态张量中的模式。这些模式允许对未见过的状态张量进行泛化。

代码可以在此存储库中获取。试试看,并告诉我你的想法!

Leave a Reply

Your email address will not be published. Required fields are marked *