强化学习开发黑白棋、五子棋游戏

这篇文章会从以下四个方面对“强化学习开发黑白棋、五子棋游戏”进行分析

一、总述

二、黑白棋游戏思路

三、五子棋游戏思路

四、分布式训练

------------------------------------------------------------------------------------------------------------------------------------

一、总述

黑白棋和五子棋属于比较简单的棋类游戏,用强化学习方法开发智能体的话,代码容易、训练时间耗费的也不大,单机gpu也可以,单机cpu也可以,速度差不了多少,因为涉及的网络并不太复杂。我训练黑白棋只花了40个小时,五子棋花费的时间多一些,3天,最终黑白棋训练的很好,但是五子棋训练的一般,和人类比起来还是有点差。要想改进五子棋的话,可以从特征、模型方面入手,因为我在训练五子棋智能体的时候,同时尝试了4个特征、11个特征,最终发现训练到相同的step的时候,11个特征的智能体要远好于4个特征的。我还尝试了将原有代码改成分布式的,采用tensorflow中的tf.train.Supervisor架构来进行训练模型,同时尝试了同步更新、异步更新。同步更新的话,可能是因为没有多个worker同时共享一个“训练数据队列”,最终导致训练效果不好,而且训练速度超慢,还没单机好。异步更新的话,速度大约提升了7倍,如果我的worker再多点的话,速度还会更加提升,但是效果就像是随机的一样,估计是因为我的训练数据还是太少了。

接下来会分别把训练黑白棋、五子棋时候的细节进行分析。

二、黑白棋游戏思路

1:黑白棋规则

黑白棋规则很简单,就是你下一个子后,在这个字的8个方向,前后左右、左前、右前、左后、右后,如果你的两个子之间包含有对方的子,那么这些方向上哪个方向是这样,就可以把对方的子翻转下成为你的字。最后比谁的子多谁就赢了。

2:总体思路 - 需要先定义的变量

定义一个棋盘对象,这个棋盘对象会有很多属性,比如长宽(棋盘的大小是决定训练模型时间长短的重要元素、也是决定你设计特征和模型复杂程度的重要元素)、两个玩家、棋盘状态、上一个状态、历史记录中的最后一步、棋盘上可落子位置

定义一个游戏对象,这个对象可以完成双方对弈(用于测试模型性能)、自我对弈(用户模型训练)

定义一个人类玩家对象,这个对象可以完成用户输入棋盘上要下子的坐标Index功能、获取这个index功能

定义一个类似于决策树的对象,我们称它为Mcts树,这个对象含有扩展自身的功能(利用给出来的数据扩展本身自己)、根据q值和u值选择自身叶子节点中最佳的一个选择、根据模拟对局的最终结果来更新整个树上每个节点的访问次数、q值、判断是否是叶子节点、判断是否是根节点

定义一个训练模型的网络对象,这个对象含有整个网络架构(输入的placeholder、网络结构、策略网络 能输出所有落子处的概率、值网络 输出赢面估计、损失函数、总损失、正则化、学习率、优化器、熵等)、加载模型的功能、前向计算的接口、估值网路接口 用于列举出来所有当前可落子处、单次训练步、保存模型、载入模型

定义一个macts树,有mcts推荐过程的接口(mcts一般含有4步,第一步是不断的在mcts树中搜索,直到找到一个叶子节点,第二步是利用当前棋盘的state得到可以下的几步棋,然后用这几步棋对Mcts树进行扩展,第三部是反向更新mcts树上面的所有q值 u值 还有每个节点的访问次数)、根据当前棋盘state得到所有可以下子的地方,用Move这一步去更新mcts树 裁剪掉无用的地方
定义一个mcts玩家,有设置玩家序号的接口、有充值玩家的mcts树的接口、还有和类似于人类玩家得到下一步该走那步棋的get_action落子决策函数

定义一个train.py文件,用于训练模型,他的构造函数就包含了上面所说的所有东西,还有一些训练模型需要的参数,比如batch_size、mcts树的迭代步数、迭代模型的对象、mcts玩家对象;数据增强函数;收集自我博弈的函数(用于充当训练集)、网络更新接口、模型评估接口、run函数(开始整个流程)

3:最重要的点,也是整个训练过程中最能体现强化学习步骤 mcts 的点

mcts树为了得到模型下一步应该走哪个index,会进行1000次下面的循环

在得到某个叶子节点后(代表模型可以落子的几个地方),会把当前state输入到最新的模型中得到可以下的几步棋(体现在mcts中就是几个新的叶子节点),同时得到模型的赢面估计值leaf_value,先判断当前游戏是否已经结束来更新leaf_value,然后用leaf_value来更新整个mcts树上每个节点的q u值,以及访问次数

1000次循环过后,在最上面的一层节点上找到模型应该下哪一步棋

4:特征讲解

(1):矩阵中当前玩家的已存在棋子处置为1,其他地方都是0

(2):矩阵中敌方玩家的已存在棋子处置为1,其他地方都是0

(3):游戏历史中最后一手的位置为1,其他地方都是0

(4):棋盘上全0全1,代表上一手是当前玩家还是敌方

3 4 两个特征看起来不起眼,但是在神经网络模型中这种特征很吃香,而且实际表明这两个特征也是很有用的。

三、五子棋游戏思路

五子棋的思路跟上面黑白棋比起来差不多一样,我主要做的更新是在特征这块,训练模型的时候,我同时尝试了上面4个特征和我11个特征的版本,然后看日志输出来的模拟下的棋局,发现在同样的step时,11个特征要明显比4个特征的版本好,虽然我训练出来的两种模型效果都很差

11个特征(上面4个就不再重复了)

还有就是历史棋局中的7步棋

改进模型的思路:实际上不管是搜索广告、还是强化学习训练模型,特征都是很重要的一部分,而特征最终影响的是样本,所以样本也是很重要的一部分。我的代码中样本是这样设计的,如果总共走了32步棋 一个棋局中,那么就有了32个样本,在下棋的时候,我就会把这样的数据收集起来,特征就是上面说的那些,label_tmp是当前玩家,棋局结束的时候得到的赢家a,然后把样本中label_tmp为a的全部置为1,剩下的为0

这样做实际上不太好,因为始终是模型在下棋,初期的时候模型下很烂,用这样的数据去更新模型,模型更新的很慢,而且还会有瓶颈,有兴趣的读者可以试着自己去编写规则,让每一步棋都变得像普通人一样高明,这样模型就能训练的比较好了,而且这个规则也不难些,后期我会把代码放出来

四、分布式训练

先说说我的整个实验成果,同步时更新、异步式更新都失败了,但是还是学习到了很多的东西,特别是分布式训练的知识,比如Tensorflow中的接口是哪些,训练的时候要注意些什么,分布式训练时候样本、模型的合理搭配,强化学习中同步时更新和异步式更新的速度差异、效果差异......

下面我再把我遇到的很多细节进行分享

1:首先是ps和worker之间的结束信号通信,worker结束的时候,应该通知ps也进行结束

关于这个问题,可以访问我的另一篇文章  https://blog.csdn.net/a1066196847/article/details/104024549

2:异步更新的结构

def train():
    # 先设置分布式环境。如果是worker0,就删掉模型路径,然后重建
    is_chief = (FLAGS.job_name == "worker" and FLAGS.task_index == 0)
    # Init cluster.
    ps_hosts = FLAGS.ps_hosts.split(",")
    worker_hosts = FLAGS.worker_hosts.split(",")
    # Create a cluster from the parameter server and worker hosts.
    cluster = tf.train.ClusterSpec({"ps": ps_hosts, "worker": worker_hosts})
    # Create and start a server for the local task.
    server = tf.train.Server(cluster, job_name=FLAGS.job_name, task_index=FLAGS.task_index)

    if FLAGS.job_name == "ps":
        ...

    elif FLAGS.job_name == "worker":
        ...

        # Assigns ops to the local worker by default.
        with tf.device(tf.train.replica_device_setter(worker_device="/job:worker/task:%d" % FLAGS.task_index,
                                                      cluster=cluster)):

            ...
            # 定义一个公共队列,然后所有worker一起消费,只用worker1来进行补充数据
            if(FLAGS.task_index == 1):
                ...
            else:
               ...

        if is_chief:
            # Add ops to save and restore all the variables.
            saver = tf.train.Saver(max_to_keep=50, write_version=saver_pb2.SaverDef.V2)
            

        init_op = tf.group(tf.local_variables_initializer(), tf.global_variables_initializer())
        sv = tf.train.Supervisor(is_chief=is_chief,
                                 init_op=init_op,
                                 summary_op=None,
                                 recovery_wait_secs=120,
                                 saver=None,
                                 global_step=global_step)

        with sv.managed_session(server.target) as sess:
            # 0节点 + 指定初始化模型 => 就从指定的路径地方初始化模型
            if is_chief and FLAGS.restore_model == "True":
                ...

            try:
                while not sv.should_stop() and (global_step_ < FLAGS.max_steps):

                    if is_chief:
                        ...

                    elif FLAGS.task_index == 1:
                        ...

                    else:

                    step += 1

            except tf.errors.OutOfRangeError:
                print_sys("train-%s except out of range." % (datetime.datetime.now()))
            finally:
                pass

            # worker与ps之间的通信

3:同步更新的架构

实际上同步更新跟上面异步比起来,就只有一个点的不同,那就是在训练模型那块,梯度更新的时候,需要有一些参数的设置。这个参数的功能是这样:假如你有8个worker,参数设置成4的话,那么这8个worker中必须有4个worker计算出来梯度的时候,才会用这4个worker计算出来的均值去更新ps中的参数

所以这样一来就慢多了,按理说效果会更好,但是我在尝试的时候,发现效果并没有达到很好的程度

4:异步更新、同步更新,我更推崇哪一个?

更推崇的是异步更新,因为同步更新的速度接受不了,所以读者应该把时间和精力都花在异步更新的优化上,比如优化特征、模型,还有训练架构(我尝试的方式是每个worker都用自己的data_buffer,也就是自己的样本库,这样实际上不太好,所有worker应该用同一个样本库)

六、后续会把两个游戏做成游戏网站,欢迎读者来试用

你可能感兴趣的:(AI爱好者社区)