这个项目用三篇文章进行介绍,各部分的内容如下:
项目实战:使用Deep Q Network(DQN)算法让机器学习玩游戏(一):总体介绍,游戏部分
项目实战:使用Deep Q Network(DQN)算法让机器学习玩游戏(二):算法部分
项目实战:使用Deep Q Network(DQN)算法让机器学习玩游戏(三):算法和游戏的交互部分,模型训练,模型评估,使用相同的算法和参数去玩另外一个不同的游戏
(三). 游戏和算法的交互部分
游戏和算法的交互流程主要是:玩家1将它的当前state输入DQN模型1,获得要采取的action。然后游戏执行这个action,并返回得到的reward和执行完这个动作之后的state。根据reward计算target Q value,并计算target Q value和predicted Q value之间的loss,然后更新模型1.
接着,玩家2在模型2中执行和上面一样的步骤,直到游戏结束。当游戏结束的时候,每一个玩家都会得到他们取得的结果的对应的reward。
什么时候存储经验数据
在第二部分中,我们介绍过会使用之前的经验数据来训练模型,在存储经验数据的时候,有两种方式可以选择。如下图所示。
一种是在玩家1执行完它的步骤就就马上把玩家1的数据存入经验queue中,第二种是在玩家2执行完动作后再把玩家1 的数据存入经验queue中。直观来看,很多人都会选择第一种方式,但是实际上应该选择第二种方式。因为当玩家1是agent时,玩家2是环境的一部分,只有当整个环境都更新完毕后,得到的reward才我们希望得到的reward。所以我们需要等玩家2把它的动作做完再计算玩家1的reward,再存储在经验队列中。
比如,在游戏的过程中,当玩家1执行完一个动作之后,如果这时游戏没有结束,根据reward的设置,玩家1的reward是0,如果按照第一种方法马上将玩家1的数据存储进去,那么它在这个状态下对应的reward是0. 接着,玩家2执行它的步骤,如果这时玩家2赢了,它得到了+1分,按照reward的设置,玩家1应该得到-1分。但是由于我们已经将0写入进去了,真正的reward-1将不会再被写入。如果我们采用第二种方法,在玩家2的动作执行完之后再写入玩家1的数据,那就能得到正确的结果。
系统的总体流程
系统的流程图如下所示:
1. 初始化游戏,开始一个新的游戏,获得游戏的图像并转换成像素信息。
2. 玩家1获得它的status,输入给模型1,从模型中得到它应该采取的action
3. 游戏部分在环境中执行这个动作,更新游戏。根据上面的分析,我们应该在玩家2执行完它的懂得做后再计算玩家1的reward。所以这时我们不将玩家1的数据写入经验队列。
4. 同样的,在玩家1执行完一个动作之后,玩家2的整个步骤完成,我们将玩家2的数据写入到经验队列2中。
5. 如果游戏结束,玩家1的数据将被写入经验队列1中。
6. 如果游戏没有结束,玩家1 进行学习并更新模型1.
7. 玩家2 获得它的status,然后输入给模型2, 得到它应该采取的行动。
8. 游戏部分执行这个动作并更新游戏。
9. 玩家1 完成了它的整个步骤。玩家1的数据被写入经验队列1中。
10. 如果游戏结束,将玩家2的数据写入经验队列2中
11. 如果游戏没有结束,玩家进行学习,并更新模型2.
12. 继续执行步骤2.
# 训练TRAIN_NUM轮游戏
for episode in range(TRAIN_NUM):
game_number += 1
step_p1_round = 0
step_p2_round = 0
print("--------------------- game numbers:", game_number, " ---------------------\n")
observation_0 = env.reset()
observation_0 = RL1.pre_process(observation_0, crop_size)
observation_0 = observation_0.reshape(60, 60, 1)
# 在一局游戏中
# player1 run first
print("player1 run first")
if play_mode == "AI_AI":
action_p1, epsilon_p1 = RL1.choose_action(observation_0, mode)
observation_p1_1, reward_m1_p1, reward_m1_p2, done_p1, info_p1 = env.make_move_p1(player_p1, action_p1, is_draw)
step_p1 += 1
step_p1_round += 1
observation_p1_1 = RL1.pre_process(observation_p1_1, crop_size)
observation_p1_1 = np.reshape(observation_p1_1, (60, 60, 1))
# s2
observation_p2 = observation_p1_1
observation_p2_finish = observation_p1_1
observation_p1 = observation_0
while True:
if play_mode == 'AI_AI':
# p2 a2
action_p2, epsilon_p2 = RL2.choose_action(observation_p2, mode)
# p2 m2, get s1_
observation_p1_finish, reward_m2_p1, reward_m2_p2, done_p2, info_p2 = env.make_move_p2(player_p2, action_p2, is_draw)
# ----------- p1 finish ----------------
step_p2 += 1
step_p2_round += 1
observation_p1_finish = RL1.pre_process(observation_p1_finish, crop_size)
observation_p1_finish = np.reshape(observation_p1_finish, (60, 60, 1))
# p1 save queue 1
if play_mode == 'AI_AI':
RL1.store_transition(observation_p1, action_p1, reward_m2_p1, observation_p1_finish)
reward_p1_total += reward_m2_p1
info_p1_total = np.sum([info_p1_total, info_p2[:4]], axis=0)
if done_p2:
# p2 save queue 2
if play_mode == 'AI_AI':
RL2.store_transition(observation_p2, action_p2, reward_m2_p2, observation_p1_finish)
reward_p2_total += reward_m2_p2
info_p2_total = np.sum([info_p2_total, info_p2[-4:]], axis=0)
break
# p2 learn
if step_p2 > start_learn_step and (play_mode == 'AI_AI'):
loss_p2 = RL2.learn(step_p2)
# s1
observation_p1 = observation_p1_finish
if play_mode == 'AI_AI':
# p1 a1
action_p1, epsilon_p1 = RL1.choose_action(observation_p1, mode)
# p1 m1 ,get s2_
observation_p2_finish, reward_m1_p1, reward_m1_p2, done_p1, info_p1 = env.make_move_p1(player_p1, action_p1, is_draw)
step_p1 += 1
step_p1_round += 1
# -------------- p2 finish -----------------
observation_p2_finish = RL1.pre_process(observation_p2_finish, crop_size)
observation_p2_finish = np.reshape(observation_p2_finish, (60, 60, 1))
# p2 save queue 2
if play_mode == 'AI_AI':
RL2.store_transition(observation_p2, action_p2, reward_m1_p2, observation_p2_finish)
reward_p2_total += reward_m1_p2
info_p2_total = np.sum([info_p2_total, info_p1[-4:]], axis=0)
if done_p1:
# p1 save queue 1
if play_mode == 'AI_AI':
RL1.store_transition(observation_p1, action_p1, reward_m1_p1, observation_p2_finish)
reward_p1_total += reward_m1_p1
info_p1_total = np.sum([info_p1_total, info_p1[:4]], axis=0)
break
# p1 learn
if step_p1 > start_learn_step and (play_mode == 'AI_AI'):
loss_p1 = RL1.learn(step_p1)
# s2
observation_p2 = observation_p2_finish
(四). 模型训练
我们用上面描述的方法对模型进行训练,玩家1先下,玩家2后下,一共训练了915000局。
由于需要的训练次数特别多,时间特别长,所以采用GPU进行训练。
下面分别是两个玩家的胜率,epsilon变化,illegal move数的图。
在胜率图中,在大约380000局之前,玩家1的胜率持续上升,而后小范围波动,玩家1的胜率大于在65%左右,玩家2的胜率大约在35%。在这个项目中,我们采用的是self-play方式,两个玩家的模型参数和网络结构都完全一样,唯一的差别是一个先手,一个后手。后来我们采用两个random玩家进行实验,发现在connect 4游戏中,先手的优势很明显。
在epsilon图中,epsilon最开始是1,以很大概率随机选择动作,然后epsilon逐步下降,经过大于60000局游戏后,下降到设置的最小值0.1,然后维持在最小值。
在illegal move图中,在大约前60000局游戏中,由于epsilon比较大,模型很大概率随机选择动作,所以非法动作的数量很大,当epsilon降低到很小的值时,大概率选择Q value最大的动作,非法动作的数量上升很慢,慢慢趋于平稳。
(五). 模型评估
评估方法
当模型训练完成之后,需要对模型的效果进行评估。由于需要对手的数据才能形成一个完整的游戏环境,所以我们有两种评估方法:和random 玩家进行比赛以及和人类玩家进行比赛。
和random 玩家进行比赛是每次对手都是随机得从7列中选择一列落子,它不会思考,水平比较差。
和人类玩家进行比赛在其他的算法中也用过,比如alphago就是和最顶级的选手进行比赛。但是我们的训练效果还达到不到那么好,而且也找不到最优秀的选手,所以就可以和普通水平的人类进行比赛。
评估的指标就是胜率。
由于random玩家是随机选择动作,为了消除随机性,我们增大比赛的次数,比如1000次,然后求平均值,来看最终的效果。
评估结果
和random玩家进行比赛
下面是用玩家1 训练的模型和一个random对手进行比赛的结果,一共进行了1000局比赛。
下图是胜率的结果,可以看出玩家1的模型能以大约87%的概率战胜随机玩家。
下面是两个玩家的illegal move的数量图,从结果可以看出,玩家1已经学习到要避免illegal move,所以它的illegal move数很少,但是由于epsilon的最小值是0.1,还存着在0.1的概率随机探索动作,所以还是有一些illegal move。相比而言,玩家2由于是随机玩家,它的illegal move数量很多。
和人类玩家进行比赛
当我们用训练好的模型和人类玩家进行比赛时,观察模型的下棋策略,我们发现,模型还战胜不了普通的人类玩家,因为它只会自己去连四连,还没有学会怎么去堵对手的棋,这样人类玩家就能很轻松地赢得比赛。
在评估中,模型只会在竖着的方向上连成4个,有时候会在横着的方向上连成4个,偶尔会在斜着的方向上连成4个。
经过分析,我们觉得原因可能是因为竖着的方向上连成4个的概率是最大的,只要对手不在这一列堵上(它下在其他6列都没有影响),就能连成4个。而在横着的方向上,对手只要在4列中随便堵掉一个就连不成四个。而斜着的就更难,因为首先还得把下面的棋先垒起来才能在斜着的方向上连成4个。
相比自己获胜,学会堵对手更难。因为当agent选择某个动作之后获胜,那它下次再碰到相同的场景就知道选择相同的动作。但是如果在某个动作之后对手胜利而它失败了,它需要去尝试其他6个可能的动作才知道如果去堵对手而避免自己失败。所以它需要更多的训练次数才能再碰到相同的场景。
由于我们的训练次数有限,所以模型还没有充分学习到所有的技能,可能还只是在学习竖着四连,横着四连和少量斜着四连的程度。在alphago的论文中,deep mind 团队使用了很多个gpu还有tpu去训练了按月计的时间,同时在第一阶段还采用了高水平的人类玩家的数据进行才达到能战胜人类的水平。
(六). 使用相同的算法和参数去玩另外一个不同的游戏
为了验证在不改变网络结构和参数的情况下,算法可能适用于另一个不同的游戏,我们开发了另外一个游戏:乒乓球。游戏界面如下,同样采用pygame开发。它也是一个两个玩家的游戏。两边白色板子是两个玩家的球拍,中间的白色方块是球,当球碰到球拍或者四周的墙壁,球会反方向弹回来。
为了更好的看出训练效果,我们稍微改变了乒乓球的游戏规则,我们的目标不是让它击败对手,而是让它学会如果准确地接住球。当球拍接住球时,reward为+1。 球拍没有接住球而让球碰到球拍侧的墙壁时,reward为-1. 胜率是指接住球的次数和总的游戏次数的比例。球拍有3个可能的动作,往上走,往下走,停止不动。
经过观察,我们发现乒乓球的训练速度比connect 4要快得多,所以把epsilon的衰减步数减少到10000。另外,由于乒乓球的可能动作是3,所以CNN网络的输出层神经元个数由7变成了3,其他所有的参数都不变。
训练结果
我们一共训练了20000局,下面是两个玩家的胜率图。可见,经过20000局的训练,两个玩家的胜率大约能达到70%左右,而且还在上升的趋势中。
评估结果
由于我们只是训练球拍接住球,所以我们不需要对手的数据就能完成评估,因为无论对手有没有接到球,球都会由球拍或者墙壁反弹回来。所以我们就用两个训练好的模型相互进行评估,一共运行了1000局游戏。评估结果如下图,从图中可以看出,两个玩家的模型都能以很高的概率接住球,大于94%和95%。
这个实验说明,在不改变网络结构和参数的情况下,这个方法能适用于不同的游戏。
Pong游戏,模型评估,模型训练的完整代码请见github: https://github.com/zm2229/use-DQN-to-play-a-simple-game。