【PaddlePaddle】 强化学习(CartPole-v1)

介绍

这篇文章主要介绍如何使用PaddlePaddle Fluid实现强化学习,通过机器自我学习,完成一个经典的游戏CartPole-v1,这个游戏通过控制滑块左右移动,不让竖着的柱子掉下来,相关介绍请移步官网:CartPole。用强化学习通过在玩游戏的过程中受到的奖励或者惩罚,学习到一个模型。

游戏界面:
在这里插入图片描述
环境:ubuntu18.04
python版本:python3.6.7

注意,这个模型中会使用到gym模块,这个模块中包含着游戏CartPole-v1。但是这个模块只可以在python3.5及以上的版本中使用。

介绍Deep Q-Lreaning

Q-Learning

Q-Learning就是要学习在一个给定的state时,采取一个特定的行动,能得到的总奖励是什么。
一般用一个表格Q-table矩阵来记录每一组的state和action的值,每走一步就相应更新表格,更新的方法是Bellman Equation:
在这里插入图片描述
参数含义:
s 代表当前的状态,a 代表当前状态所采取的行动,
s’ 代表这个行动所引起的下一个状态,a’ 是这个新状态时采取的行动,
r 代表采取这个行动所得到的奖励 reward,γ 是 discount 因子,
由公式可以看出 s,a 对的 Q 值等于 即时奖励 + 未来奖励* discount。
γ 决定了未来奖励的重要性有多大,
比如说,我们到了一个状态,它虽然离目标状态远了一些,但是却离游戏失败远了一些,那这个状态的即时奖励就很小,但是未来奖励就很多。

算法步骤是:
1.初始化 Q-table全为 0
2.每一次遍历,随机选择一个状态作为起点
3.在当前状态 (S) 的所有可选的行动中选择一个 (a)
4.移动到下一个状态 (S’)
5.在新状态上选择 Q 值最大的那个行动 (a’)
6.用 Bellman Equation 更新 Q-table
7.将新状态设置为当前状态重复第 2~6 步
8.如果已经到了目标状态就结束

DQN

DQN全称是Deep Q-Learning,是将Q-Learning和深度学习相结合,不使用Q-table记录Q值,而是使用神经网络来预测Q值和下一步的东走,并通过不断更新神经网络从而学习到最佳的行动路径。
DeepMind 用DQN来玩电子游戏,他们将游戏画面的像素转换成深度神经网络的输入数据(状态s),用CNN(卷积神经网络)来预测动作a(a1,a2,a3 …), 和对应的Q(s, a1), Q(s, a2),Q(s, a3)…

记忆库和Fixed Q-target

DQN中有两个神经网络,第一个是参数相对固定的网络,我们称之为target-net,用来预测下一步的q-target的数值,另外一个网络用来反向传播,进行参数调整,用来获取q-eval评估。q_target的网络target_net的参数也会定期进行修剪和更新,更新q_target网络的参数就是直接将q_eval的参数复制过来并进行修剪。
我们的损失函数实际上就是q_target减q_eval的结果。
训练的数据是从记忆库中随机提取的,记忆库记录了每个状态和接下来的行动,奖励,和下个状态的结果(s,a,r,s’)。我们在python中使用deque,当数据存满的时候下一数据就会覆盖掉记忆库中的第一个数据。

代码思路

首先使用全连接搭建一个网络用来预测action,然后将网络分为两个,一个是反向传播更新参数,我们称之为state_model,一个是只进行前向传播用预测下一步的动作,我们称之为target。state_model喂入的是当前的状态,产生一个预测的Q1。target喂入的是下一个状态,产生的是的预测的Q‘,并用Bellman Equation公式计算出现在现在这个状态target对应的期望值(q_target)。而state_model产生的预测值Q’可以通过计算计算算出q_评估(q_eval)。则q_target和q_eval构成了损失函数。state_model进行参数的不断更新,目的就是为了达到target_net和Bellman Equation计算出的最大期望值。随后,q_eval的参数不断进行更新,生成的action也会不断被优化,使模型能自己预测出下一步应该怎么走,并且这样走能达到期望值最大。
还是不太理解的同学可以移步这里:【深度学习】对强化学习的理解(在CartPole-v1游戏下的强化学习),这篇文章我详细介绍了针对CartPole-v1游戏的强化学习算法思想。
在这期间,target-net网络参数定期根据state_model的参数进行复制,修剪。

模型的搭建

因为上面讲的挺详细了,而且代码中也有大量的注释,所以直接贴出代码。

# coding:utf-8
'''
created on February 16 23:07 2019

@author:lhy
'''
import numpy as np
import paddle.fluid as fluid
import random
import gym
from collections import deque
from paddle.fluid.param_attr import ParamAttr


# 定义一个简单的神经网络,用于输出预测的Q值,这个网络仅仅由3个全连接层组成,并为每个全连接层指定参数名称,用于之后更新指定的网络参数
def DQNetWork(ipt, variable_field):
    fc1 = fluid.layers.fc(input=ipt, size=24, act='relu', param_attr=ParamAttr(name='{}_fc1'.format(variable_field)),
                          bias_attr=ParamAttr(name='{}_fc1_b'.format(variable_field)))
    fc2 = fluid.layers.fc(input=fc1, size=24, act='relu', param_attr=ParamAttr(name='{}_fc2'.format(variable_field)),
                          bias_attr=ParamAttr(name='{}_fc2_b'.format(variable_field)))
    fc3 = fluid.layers.fc(input=fc2, size=2, param_attr=ParamAttr(name='{}_fc3'.format(variable_field)),
                          bias_attr=ParamAttr(name='{}_fc3_b'.format(variable_field)))
    return fc3


# 定义一个更新参数的函数,这个函数是通过指定参数名称,通过修剪参数来完成模型更新
def build_sync_target_network():
    # 首先获取所有的参数
    vars = list(fluid.default_main_program().list_vars())
    # 把两个网络的参数分开
    # 将x.name中不含GRAD关键字但是含有target关键字的参数挑选出来并组成一个列表
    # 在这里,lambda匿名函数传入的参数是vars里面的元素,这些元素挨个被传入到x中,并进行判断
    #这些元素作为参数进行后面的判断,把判断为True的元素整合成一个列表返回
    policy_vars = list(filter(lambda x: 'GRAD' not in x.name and 'policy' in x.name, vars))
    target_vars = list(filter(lambda x: 'GRAD' not in x.name and 'target' in x.name,vars))  
    #下面这句话表示在元素x的name属性上进行排序
    policy_vars.sort(key=lambda x: x.name) 
    target_vars.sort(key=lambda x: x.name)

    # 从主程序克隆一个程序用来更新参数
    sync_program = fluid.default_main_program().clone()
    with fluid.program_guard(sync_program):
        sync_ops = []
        for i, var in enumerate(policy_vars):
            sync_op = fluid.layers.assign(policy_vars[i], target_vars[i])  # 将两个模型的参数组合在一起,就是将target的参数更新
            sync_ops.append(sync_op)
    # 修剪参数,对不需要的label进行裁剪
    sync_program = sync_program._prune(sync_ops)  
    # 通过修剪参数的方式完成模型更新
    return sync_program


# 定义5个数据输出层
# state_data是当前游戏状态的数据输入层
# action_data是对游戏操作动作的数据输入层,只有两个动作0和1
# reward_data是当前游戏给出的奖励的数据输入层
# next_state_data是游戏下个状态的数据输入层
# done_data是游戏是否结束的数据输入层
# Q(当前状态S,当前动作A)=reward+贪婪因子*(max Q(s,a))s指当前行动引起的下个状态,a指的是新状态时采取的动作
state_data = fluid.layers.data(name='state', shape=[4], dtype='float32')
action_data = fluid.layers.data(name='action', shape=[1], dtype='int64')
reward_data = fluid.layers.data(name='reward', shape=[], dtype='float32')
next_state_data = fluid.layers.data(name='next_state', shape=[4], dtype='float32')
done_data = fluid.layers.data(name='done', shape=[], dtype='float32')

# 定义一些必要的训练参数,比如epsilon-greedy搜索策略的参数,以一定概率ϵ选择前面试验中平均收益最佳的item,以1-ϵ的概率选择其他item
batch_size = 32
num_episodes = 300
num_exploration_episodes = 100
max_len_episode = 1000
learning_rate = 1e-3
gamma = 1.0

initial_epsilon = 1.0
final_epsilon = 0.01

# 创建一个游戏
env = gym.make("CartPole-v1")
replay_buffer = deque(maxlen=10000)  # deque提供了两端都可以操作的序列,限制长度为10000,当有新的元素append进来时,会顶替掉最早进来的元素

# 定义一个网络模型,传入当前游戏状态的数据输入层,并指定参数名称包含policy字符串
# 因为最后一层是全连接层,所以输出的shape是(batch_size,2)
state_model = DQNetWork(state_data, 'policy')

# 这里从主程序克隆一个预测程序,用这个预测程序预测游戏的下一个动作
predict_program = fluid.default_main_program().clone()

# 定义损失函数
# 游戏操作的one_hot编码,如果是0的话,就是[[1,0]],如果是1的话,就是[[0,1]]
# action_onehot.shape=(batch_size,2)
action_onehot = fluid.layers.one_hot(action_data, 2)
# 将这一步的action和Q值进行hadamard乘积,得到当前状态下执行这个action得到的期望值
# elementwise_mul()函数是逐元素相乘算子,也就是hadamard乘积:Out=X⊙Y
# action_value.shape=(batch_size,2)
action_value = fluid.layers.elementwise_mul(action_onehot, state_model)  
# fluid.layers.reduce_sum在dim=1(行)维度上进行相加,使其与target的shape相同,目的是将shape从(batch_size,2)二维张量转变成(batch_size,)以为张量,因为此函数默认会消除dim维度
# pred_action_value.shape=(batch_size,)
pred_action_value = fluid.layers.reduce_sum(action_value, dim=1)  

# 经过神经网络得到下一个状态分别执行两个操作,得到的收益
# targetQ_predict_value.shape=(batch_size,2)
targetQ_predict_value = DQNetWork(next_state_data, 'target')  # load参数名称包含'target'的字符串
# 用贪心法找出dim=1(行)维度上较大的那个值,作为下一步可以获得的最大期望值,同样此函数默认会消除dim维度
# best_v.shape=(batch_size,)
best_v = fluid.layers.reduce_max(targetQ_predict_value, dim=1) 
# 设置不进行梯度下降,仅仅是预测期望值Q'的作用
best_v.stop_gradient = True  
# 进行当前期望值q-target的计算,使用Bellman Equation方法,其实就是当前状态下,执行这一步获得的收益加上未来期望值
# target.shape=(batch_size,)
target = reward_data + gamma * best_v * (1 - done_data)

# 使用的还是平方差损失函数,但是输入的不是简单的输入数据和标签
cost = fluid.layers.square_error_cost(pred_action_value, target)
avg_cost = fluid.layers.reduce_mean(cost)

# 获取一个更新参数的程序,用于之后执行更新参数
_sync_program = build_sync_target_network()

# 定义一个优化方法,还是使用Adam,优化速度快
optimizer = fluid.optimizer.AdamOptimizer(learning_rate=learning_rate, epsilon=1e-3)  # 分子分母同时加上1e-3,防止有0出现
opt = optimizer.minimize(avg_cost)

# 创建执行器
place = fluid.CPUPlace()
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())
epsilon = initial_epsilon

update_num = 0
# 开始玩游戏
for epsilon_id in range(num_episodes):
    # 初始化环境,获得初始状态
    state = env.reset()
    # 定义一个epsilon-greedy搜索策略,这个策略根据训练的进度,选择自动操作或者是模型预测的动作的概率
    epsilon = max(initial_epsilon * (num_exploration_episodes - epsilon_id) / num_exploration_episodes, final_epsilon)
    # 一局游戏的循环
    for t in range(max_len_episode):
        # 显示游戏界面
        env.render()
        # 扩展数组的形状,在axis位置添加一个维度
        state = np.expand_dims(state, axis=0) 
        # 进行epsilon-greedy搜索策略
        if random.random() < epsilon:
            # 以epsilon的概率选择随机下一步动作
            action = env.action_space.sample()
        else:
            # 使用模型预测作为结果下一步动作
            action = exe.run(predict_program, feed={'state': state.astype('float32')}, fetch_list=[state_model])[0]
            action = np.squeeze(action, axis=0)  # 删除所有单维度条数,比如将[[[1,2]]]变成[1,2]
            action = np.argmax(action)  # 取出最大值的索引

        # 让游戏执行动作,获得执行后的下一个状态,动作的奖励,游戏是否结束以及其他信息
        next_state, reward, done, info = env.step(action)
        # 如果游戏结束,就进行惩罚
        reward = -10 if done else reward
        # 记录下游戏输出的结果,作为以后训练的数据
        replay_buffer.append((state, action, reward, next_state, done))
        state = next_state

        # 如果游戏结束,就重新玩游戏
        if done:
            print('Pass:%d,epsilon"%f,score:%d' % (epsilon_id, epsilon, t))
            break

        # 如果收集的数据大于Batch的大小,就开始训练
        if len(replay_buffer) >= batch_size:
            batch_state, batch_action, batch_reward, batch_next_state, batch_done = [np.array(a, np.float32) for a in
                                                                                     zip(*random.sample(replay_buffer,
                                                                                                        batch_size))]

            # 更新参数,每200步更新一次target
            if update_num % 200 == 0:
                exe.run(program=_sync_program)
            update_num += 1

            # 调整数据维度
            batch_action = np.expand_dims(batch_action, axis=1)
            batch_next_state = np.expand_dims(batch_next_state, axis=1)
            '''
            最终每个输入变量的shape:
            batch_state.shape=(32,1,4)
            batch_action.shape=(32,1)
            batch_reward.shape=(32,)
            batch_next_state.shape=(32,1,4)
            batch_done.shape=(32,)
            '''
            # 执行训练
            exe.run(program=fluid.default_main_program(),
                    feed={'state': batch_state, 'action': batch_action.astype('int64'), 'reward': batch_reward,
                          'next_state': batch_next_state, 'done': batch_done}, fetch_list=[avg_cost])

运行结果

运行后,程序先是进行记忆库的填充,记忆库存储的内容到达了一定数量,就开始训练q_eval,也就是开始训练state_model的参数,让其尽量拟合target计算出的q_target,最后的效果如下图:
在这里插入图片描述
游戏最大分是500分,可以看到在后期,模型已经可以次次跑500分了。

  • 4
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值