前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >AlphaGo增强式学习算法:实现‘高手指点’特效

AlphaGo增强式学习算法:实现‘高手指点’特效

作者头像
望月从良
发布2019-08-05 14:38:19
4070
发布2019-08-05 14:38:19
举报
文章被收录于专栏:Coding迪斯尼

我们在学习过程中离开不了老师的指导,老师除了传授知识外,另外一个很重要的作用是指出问题。我们或多或少有这样的经验,在训练某种技能时一开始进步很快,但不久就进入瓶颈期,这段时期无论你做什么都很难产生明显的突破。此时如果有个水平较高,经验老到的老师给你指条出路,或是告诉你哪一步做出了,你根据它的指导去践行后,水平又会出现新的提升。

本节我们就要把这种‘高手指点’的效应引入到围棋机器人的训练过程中从而提升机器人的训练效果。在前面章节说到基于政策的训练算法时,我们先让两个机器人自我对弈,假设下了200步棋后机器人A获得了胜利,然后我们会把A下的200步棋和对应的棋盘状况收集起来作为训练数据,把棋盘当做输入,把每一步落子当做结果,训练网络在给定棋盘状况时,学会按照给定结果去落子。这种方法其实是告诉网络说,这200步落子方式都是好走法,事实上这200步落子中,可能只有十几步是好棋,其他的都是臭棋,因此以前的训练方法实际上会让网络把某些臭棋当做好棋来学习。

本节我们要引入一个”高手“,它会告诉网络这200步棋中,哪几步是好棋,哪几步是坏棋,这样的话网络在训练时就不会被坏棋误导,从而得到更好的训练效果。本节我们要找到一种算法,它能评估那200步棋的好坏,从而将里面的好棋和坏棋区分开来。

我们如何评估一步走法的好坏呢。设想你最喜欢的NBA球员是库里,比赛到了第四节快结束时库里投了一个三分球,那么他这次投篮成功价值有多好呢?这取决于当前比分状况,如果库里所在球队与对手的差距在命中前是77比78,命中后变成80比78,那么这记三分球就非常重要,如果此时比分是77比110,那么这记三分球就显得无足轻重,所以这记三分球的重要程度取决于当前局面的具体情况。

同样的道理,一步棋是好是坏也取决于当前的棋盘状况。因此我们需要先对当前局面进行评估,假设s代表当前棋盘,我们有一个函数V,V(s)返回当前棋盘对机器人A的有利程度,V(s)的值越接近1表明情况对机器人A越有利,越接近-1表明棋盘对机器人A越不利。这个函数与前几节的评估函数Q(s,a)很像,不同在于V(s)评估行动前局面的好坏,Q(s,a)评估采取行动后局面的好坏。

于是我们评估一步棋是好是坏时可以这么计算A = Q(s,a) - V(s)。数值A表示机器人落子后所获得的优势,如果原来局面有利,但走了一步臭棋,A的值变小,也就是局面相比于原来变得不利了,这时A是负值。问题在于Q(s,a)如何计算,如果对弈200步后机器人A获得了胜利,那么s对应着两百步棋的棋盘,a对应200步走法,那么我们直接将Q(s,a)设置为1,如果最终输了,我们就将Q(s,a)设置成-1.

当然此时我们也不知道如何计算V(s),但我们可以用网络训练处准确评估V(s)的能力。我们看一个具体例子,在一开局时s对应空棋盘,所以V(s)=0,它表示当前局面对机器人A没有好处,当然也没有坏处。直到下完200步后,如果机器人A输了,由此A下完第一步棋后,对应的Q(s,a)=-1,于是我们得出A=-1-0=-1,由此认为在空棋盘状况下,第一步落子让机器人A处于非常不利的局面

假设在对弈快结束时,V(s)对棋盘进行评估后的值是0.95,最后机器人A赢了,那么机器人A在给定棋盘上落子后的得分Q(s,a)就设定为1,于是得出它落子后收获的优势A=1-0.95=0.05。如果A最后输了,那么在给定棋盘上A落子后收获的优势就是A=-1-0.95=-1.95。通过对当前棋盘状况的评估,在结合落子后的结果,我们就可以从数值上判断这步棋的好坏。

现在问题是,我们如何计算数值A,我们通过代码实现来讲解它的计算过程。我们前面几节曾经使用一个类ExperienceCollector来记录机器人对弈的信息。如果机器人A走了200步棋,那么该类就会记录200步棋的走法,以及每步棋落子时对应的棋盘以及最后的胜负结果,这里我们需要让它增加记录一种信息,也就是V(s),记录当前棋盘对机器人A的好坏,相关修改如下:

代码语言:javascript
复制
class  ExperienceCollector:
    def  __init__(self):
        self.states = []  #记录每一步棋对应的棋盘
        self.actions = []  #记录每一步落子
        self.rewards = []  #记录最终结果
        self.advatanges = []  #记录棋盘对应机器人的好坏

        self._current_episode_states = []  #记录当前对弈所产生的棋盘状况
        self._current_episode_actions = []  #记录当前对弈过程每一步落子
        self._current_episode_estimaged_values = [] #记录当前对弈过程所产生棋盘状况对机器人的有利程度

相比于以前的代码,我们增加了棋盘局面对机器人优劣的评估信息。在对弈过程中,每次落子,该类就得记录相应信息,代码如下:

代码语言:javascript
复制
def  record_decision(self, state, action, estimated_value = 0):
        self._current_episode_states.append(state)  #记录落子前棋盘状态
        self._current_episode_actions.append(action)  #记录机器人的落子步骤
        self._current_episode_estimated_values.append(estimated_value)  #记录当前棋盘对机器人好坏的评估

在对弈结束后,该类的complete_episode函数会被调用,此时我们就可以计算相应的数值:

代码语言:javascript
复制
def  complete_episode(self, reward):  #如果胜利reward的值为1,失败则值为-1
        num_states = len(self._current_episode_states)
        self.states += self._current_episode_states
        self.actions += self._current_episode_actions
        self.rewards += [reward for _ in range(num_states)]

        for i in range(num_states):#根据最终结果计算每个棋盘状况的优劣
            advantage = reward - self._current_episode_estimated_values[i]
            self.advantages.append(advantage)

        self._current_episode_states = []
        self._current_episode_actions = []
        self._current_episode_estimated_values = []
    def  combine_experience(collectors):
        combined_states = np.concatenate([np.array(c.states) for c in collectors])
        combined_actions = np.concatenate([np.array(c.actions) for c in collectors])
        combined_rewards = np.concatenate([np.array(c.rewards) for c in collectors])
        combined_advatages = np.concatenate([np.array(c.advantages) for c in collectors])

        return ExperienceBuffer(combined_states, combined_actions,
                               combined_rewards, combined_advantages)

现在问题是,我们如何计算V(s)呢,此时我们需要训练一个神经网络来完成这个功能。当网络的输入当前棋盘状态后,它将输出两个结果,一个是当前所有可落子方式的胜率评估,一个是当前棋盘对应的期望收益。接着我们看看网络的代码设计:

代码语言:javascript
复制
from keras import Model
from keras.layers import Conv2D, Dense, Flatten, Input

board_input = Input(shape=encoder.shape(), name = 'board_input')
conv1 = Conv2D(64, (3, 3), padding = 'same', activation = 'relu')(board_input)
conv2 = Con2D(64, (3,3), padding = 'same', activation = 'relu')(conv1)
conv3 = Conv2D(64, (3, 3), padding = 'same', activation = 'relu')(conv2)
flat = Flatten()(conv3)
processed_board = Dense(512)(flat)
policy_hidden_layer = Dense(512, activation = 'relu')(processed_board)
policy_output = Dense(encoder.num_poits(), activation = 'softmax')(policy_hidden_layer) #给出每个落子位置的赢率
value_hidden_layer = Dense(512, activation = 'relu')(processed_board)
value_output = Dense(1, activation = 'tanh')(value_hidden_layer) #给出当前棋盘形势优劣的评估也就是V(s)
model = Model(inputs = board_input, outputs = (policy_output, value_output))

代码构造的网络会使用三个卷积层对棋盘进行扫描,最后给出两个结果,一个是当前棋盘下所有可以落子位置的赢率,第二个是当前棋盘对机器人优劣的量化评估,这里我们可以看到,当我们想计算某个数值,但又不知道如何以明确的方式或没有具体步骤或算法去计算时,我们就通过神经网络来完成对数值的评估,上面网络形成的结构如下:

接着我们看看机器人如何使用这个网络来决定落子位置:

代码语言:javascript
复制
class  ACAgent(Agent):
    def  select_move(self, game_state):
        num_moves = self.encoder.board_width * self.encoder.board_height
        board_tensor = self.encoder.encode(game_state)
        X = np.array([board_tensor])
        actions, values = self.model.predict(X) #根据当前棋盘评估落子赢率以及评估棋盘对本机器人的优劣
        mov_probs = actions[0] #获得所有可落子位置的赢率
        estimated_value = values[0][0] #获得棋盘优劣的评估值
        eps = 1e-6
        move_probs = np.clip(move_probs, eps, 1 - eps) #将概率过大或过下的落子位置忽略掉
        move_probs = move_probs / np.sum(move_probs)

        candidates = np.arange(num_moves)
        #根据所有位置赢率的大小随机抽样,赢率大的落子位置被抽到排在前面的概率就大
        ranked_moves = np.random.choice(candidates, num_moves, replace = False, p = move_probs)
        for point_index in ranked_moves:  #从抽样中找到能合法落子的步骤
            point = self.encoder.decode_point_index(point_idx)
            move = goboard.Move.play(point)
            move_is_valid = game_state.is_valid_move(move)
            fills_own_eye = is_point_an_eye(game_state.board, point, game_state.next_player)
            if move_is_valid and not fills_own_eye:
                if self.collector is not None: #记录下当前落子和棋盘状况
                    if self.collector.record_decision(state = board_tensor,
                                                     action = point_idx,
                                                     estimated_value = estimated_value):
                    return goboard_Move.play(point)
        return goboard.Move.pass_turn()

接下来还有一个很重要的问题是,我们如何训练网络,让它知道怎么评估当前棋盘的优劣。假设机器人A下了200步棋,最后赢得胜利,那么我们把200步棋对应的棋盘优势值全部设置成1,然后训练时把棋盘输入网络,让网络调整自己内部参数,以便它自己输出的优劣评估值尽可能接近1,如果机器人A最终输了,那么200步棋对应的棋盘优势值全部设置成-1,于是网络在接收棋盘后,调整内部参数让输出的评估值尽可能接近-1,相应代码如下:

代码语言:javascript
复制
 def  train(self, experience, lr = 0.1, batch_size = 128):
        opt = SGD(lr = lr)
        #因为网络有两个输出所有要对两头分别使用不同损失函数
        self.model.compile(optimizer = opt, loss = ['categorical_crossentropy', 'mse'])
        #左边输出的重要性是右边的2倍
        loss_weights = [1.0 , 0.5]

        n = experience.states.shape[0]
        num_moves = self.encoder.num_points()
        policy_target = np.zeros((n, num_moves))
        value_target = np.zeros((n))
        for i in range(n):
            action = experience.action[i]
            policy_target[i][action] = experience.advantages[i]
            reward = experience.rewards[i]
            value_target[i] = reward
        self.model.fit(experience.states, [policy_target, value_target],
                      batch_size = batch_size, epochs = 1)

上面所给代码就是本节所说的”高手指点“原理。网络内部参数在训练时受到两个方面影响,一是给定棋盘状态后网络选择的落子位置与棋盘对应的获得胜利的落子位置的差异,二是网络给出的棋盘优劣评估与我们在train函数中设置的棋盘优劣分值差异的影响。在train函数中,我们将所有棋盘的优劣分值设置成1或-1,这取决于网络在对弈时是胜利还是失败。

上面的代码需要足够的算力才能运行,普通电脑不可能训练出合适的网络,因此我们可以通过代码来了解算法原理,在没有GPU算力下,上面的代码依然难以通过运行来得以检验。

更多视频讲解和调试演示请点击‘阅读原文’。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Coding迪斯尼 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档