在OpenAI CartPole上实现Q-Learning强化学习算法
1. OpenAI Gym简介
OpenAI Gym是一个用于开发和比较强化学习算法(简称RL算法)的工具包,其中封装了由简单到复杂的各种游戏环境,Gym与其他的数值计算库兼容,如TensorFlow,支持Python语言。
2. CartPole环境说明
使用CartPoleV1环境。
环境描述: CartPoleV1可视化画面如图1所示,此游戏通过控制载体左右移动来保持载体上的杆子平衡,运载体无摩擦地支撑杆子。
动作: 2个动作(施加0和1分别对应向左向右推动运载体)。
状态: 4个:位置
[
−
4.8000002
e
+
00
,
4.8000002
e
+
00
]
[-4.8000002e+00, 4.8000002e+00]
[−4.8000002e+00,4.8000002e+00]、移动速度
[
−
3.4028235
e
+
38
,
3.4028235
e
+
38
]
[-3.4028235e+38, 3.4028235e+38]
[−3.4028235e+38,3.4028235e+38]、杆子偏转角度
[
−
4.1887903
e
−
01
,
4.1887903
e
−
01
]
[-4.1887903e-01, 4.1887903e-01]
[−4.1887903e−01,4.1887903e−01]、杆子角速度
[
−
3.4028235
e
+
38
,
3.4028235
e
+
38
]
[-3.4028235e+38, 3.4028235e+38]
[−3.4028235e+38,3.4028235e+38]。区间是对应状态的取值范围。
终止条件: 杆子的摇摆幅度超过了垂直方向15°或运载体偏移初始位置超过2.4个单位。
3. Q-Learning算法原理
3.1 原理
有一个由状态
s
s
s描述的环境
(
s
∈
S
,
S
是
所
有
可
能
状
态
的
集
合
)
(s∈S,S 是所有可能状态的集合)
(s∈S,S是所有可能状态的集合),一个能够执行动作
a
a
a的
a
g
e
n
t
agent
agent
(
a
∈
A
,
A
是
所
有
可
能
动
作
的
集
合
)
(a∈A,A 是所有可能动作的集合)
(a∈A,A是所有可能动作的集合),智能体的动作致使智能体从一个状态转移到另外一个状态。智能体的行为会得到奖励,而智能体的目标就是最大化奖励。
Q-Learning算法的核心是如表1所示的表格。
Action 1 | Action 2 | |
---|---|---|
State 1 | 0.4 | 5 |
State 1 | -3 | 1.2 |
… | … | … |
State N | 0.9 | 3 |
当游戏角色处于某个状态,游戏有
2
2
2个操作可选,而表格中就是执行相应动作后得到奖励的预测值。我们选择预测奖励值最大的动作执行。例如:若某时刻游戏角色处于
S
t
a
t
e
1
State 1
State1,在
S
t
a
t
e
1
State 1
State1下执行
A
c
t
i
o
n
1
Action 1
Action1可得到
0.4
0.4
0.4的奖励值,执行
A
c
t
i
o
n
2
Action 2
Action2可得到
5
5
5的奖励值,因为
0.4
<
5
0.4 < 5
0.4<5,所以选择
A
c
t
i
o
n
2
Action 2
Action2执行。
显然Q-Learning算法的关键就是如何得到这样的表格。
我们先随机初始化表格,然后再根据公式
Q
(
s
,
a
)
=
l
r
∗
(
r
e
w
a
r
d
+
γ
∗
m
a
x
{
Q
(
s
+
1
,
∗
)
}
−
Q
(
s
,
a
)
)
\begin{aligned} Q(s, a) = lr * (reward + \gamma * max\{Q(s+1, *)\} - Q(s, a)) \end{aligned}
Q(s,a)=lr∗(reward+γ∗max{Q(s+1,∗)}−Q(s,a))
计算,其中
Q
(
s
,
a
)
Q(s,a)
Q(s,a)表示在状态
s
s
s下执行动作
a
a
a获得的奖励预测值(表格中的Q值);
r
e
w
a
r
d
reward
reward是在状态
s
s
s下执行动作
a
a
a获得的实际奖励值;
m
a
x
{
Q
(
s
+
1
,
∗
)
}
max\{Q(s+1, *)\}
max{Q(s+1,∗)}表示在状态
s
s
s下执行最优动作变到状态
s
+
1
s+1
s+1,状态
s
+
1
s+1
s+1能够获取的最大的奖励预测值(即表格中状态
s
+
1
s+1
s+1对应行数值的最大值);
l
r
lr
lr为学习率;
γ
\gamma
γ为衰减系数,
γ
\gamma
γ越小,说明越重视眼前的奖励,
γ
\gamma
γ越大,说明越重视未来的奖励。
3.2 连续状态离散化
由于CartPoleV1的4个状态取值都是连续值,无法创建行数有限的表格。因此,需要先对状态进行离散化。首先限定每个状态量的取值区间,这是因为如移动速度取值虽然位于 [ − 3.4028235 e + 38 , 3.4028235 e + 38 ] [-3.4028235e+38, 3.4028235e+38] [−3.4028235e+38,3.4028235e+38]中,但实际上移动速度的取值大部分都在 [ − 3.0 , 3.0 ] [-3.0, 3.0] [−3.0,3.0]中。因此,我们先限定取值区间。以移动速度为例,先限定其取值区间为 [ − 3.0 , 3.0 ] [-3.0, 3.0] [−3.0,3.0],然后再把区间 [ − 3.0 , 3.0 ] [-3.0, 3.0] [−3.0,3.0]平均分成 n n n份,这样连同小于 − 3.0 -3.0 −3.0和大于 3.0 3.0 3.0共得到 n + 2 n+2 n+2个子区间,将移动速度转化为位于区间的下标。若移动速度为 − 4 -4 −4,则离散化后为 0 0 0。
4. 代码
代码包含三部分: Q t a b l e Qtable Qtable, e n v i r o n m e n t environment environment, C a r t P o l e − Q L e a r n i n g CartPole-QLearning CartPole−QLearning。
# -*- coding: utf-8 -*-
"""
Created on Thu Dec 24 18:37:48 2020
@author: qiqi
"""
import numpy as np
class Qtable:
def __init__(self, action_num=2, state_num=4, s_split_num=6):
self.action_num = action_num
self.s_split_num = s_split_num
self.table = np.random.uniform(low=0, high=1, size=(s_split_num**state_num, action_num))
def __cal_idx(self, state_decode):
idx = 0
length = len(list(state_decode))
for i in range(length):
idx += state_decode[length-i-1] * (self.s_split_num**i)
return idx
def update(self, state_decode, action, reward, lr, GAMMA, max_q_next):
#根据编码后的状态核动作,更新表格
idx = self.__cal_idx(state_decode)
self.table[idx, action] += lr * (reward + GAMMA * max_q_next - self.table[idx, action])
def get_Q(self, state_decode, action):
#state_decode为编码后的状态,action为动作。返回state_decode状态下执行action动作的Q值
idx = self.__cal_idx(state_decode)
return self.table[idx, action]
def get_best_action(self, state_decode):
#state_decode为编码后的状态,action为动作
#返回state_decode状态能获得最大Q值的动作
idx = self.__cal_idx(state_decode)
return np.argmax(self.table[idx])
def get_max_q(self, state_decode):
#state_decode为编码后状态,返回state_decode状态能获得最大Q值
idx = self.__cal_idx(state_decode)
return float(np.max(self.table[idx]))
# -*- coding: utf-8 -*-
"""
Created on Sat Dec 26 15:09:12 2020
@author: qiqi
"""
import numpy as np
class environment:
def __init__(self, env, low, high, s_split_num):
#low和high,分别是各个状态取值最低值和最高值
#s_split_num是一个整数,表示将一个连续状态转化为s_split_num个离散状态
self.env = env
self.low = low
self.high = high
self.s_split_num = s_split_num
def __decode(self, state):
#将连续状态离散化编码,返回一个一维数组
#state是一个列表,包含n个浮点数,每个浮点数表示一个状态变量取值
#一维数组每个元素表示一个离散化后的状态变量
st_decode = []
for i in range(len(state)):
a = np.linspace(self.low[i], self.high[i], self.s_split_num-1)
idx = np.digitize(state[i], a)
st_decode.append(idx)
return np.array(st_decode)
def step(self, action):
#游戏运行一步,返回当前状态,奖励,是否死亡,调试信息
s_t, reward, done, info = self.env.step(int(action))
s_t = self.__decode(s_t)
return s_t, reward, done, info
# -*- coding: utf-8 -*-
"""
Created on Sat Dec 26 15:07:19 2020
@author: qiqi
"""
import gym
import time
import random
import numpy as np
from Q_table import Qtable
from Environment import environment
def train(episode, env):
s_split_num = 6
Q_table = Qtable(action_num=2, state_num=4, s_split_num=s_split_num)
low = [-2.4, -3.0, -0.5, -2.0]
high = [2.4, 3.0, 0.5, 2.0]
envir = environment(env=env, low=low, high=high, s_split_num=s_split_num)
step_num_list = [200]
for e in range(episode):
start = time.time()
epsilon = 10 / (e + 1)
_ = env.reset()
env.render()
action = env.action_space.sample() #随机从动作空间中选取动作
s_t, reward, done, info = envir.step(action) #根据动作获取下一步的信息
step_num = 1
while not done:
env.render() #环境展示
if random.random() <= epsilon:
#探索,随机选择一个动作
action = env.action_space.sample()
else:
#利用,根据表中数据选择能获得最大Q值的动作
action = Q_table.get_best_action(s_t)
s_t1, reward, done, info = envir.step(action)
if done:
reward = 1 if step_num > np.max(step_num_list) else -1
else:
reward = 0
step_num += 1
max_q_next = Q_table.get_max_q(s_t1)
Q_table.update(state_decode=s_t, action=action, reward=reward,
lr=0.5, GAMMA=0.99, max_q_next=max_q_next)
s_t = s_t1
end = time.time()
print('Episode:{0:d}'.format(e),
' time:{0:.4f}'.format(end-start),
' step_num:{0:d}'.format(step_num),
)
step_num_list.append(step_num)
if e % 1000 == 0:
np.save('step_num_'+str(e)+'.npy', np.array(step_num_list))
if __name__ == '__main__':
env = gym.make('CartPole-v1').unwrapped
train(episode=1000000, env=env)
5. 结果
奖励函数的定义方法对Q-Learning算法的效果有很大影响,这里使用了 3 3 3种奖励函数进行实验。
5.1 固定阈值
r e w a r d = { − 1 , 角 色 死 亡 且 得 分 小 于 200 0 , 角 色 未 死 亡 1 , 角 色 死 亡 且 得 分 不 小 于 200 \begin{aligned} reward=\left \{\begin{array}{ll} -1, & 角色死亡且得分小于200\\ 0, &角色未死亡\\ 1, & 角色死亡且得分不小于200 \end{array} \right. \end{aligned} reward=⎩⎨⎧−1,0,1,角色死亡且得分小于200角色未死亡角色死亡且得分不小于200
以此作为奖励函数得到的游戏训练过程如图2所示。图2中横轴代表训练迭代轮次,纵轴代表得分。
5.2 历史最高分
r e w a r d = { − 1 , 角 色 死 亡 且 得 分 小 于 历 史 最 高 分 0 , 角 色 未 死 亡 1 , 角 色 死 亡 且 得 分 不 小 于 历 史 最 高 分 \begin{aligned} reward=\left \{\begin{array}{ll} -1, & 角色死亡且得分小于历史最高分\\ 0, &角色未死亡\\ 1, & 角色死亡且得分不小于历史最高分 \end{array} \right. \end{aligned} reward=⎩⎨⎧−1,0,1,角色死亡且得分小于历史最高分角色未死亡角色死亡且得分不小于历史最高分
以此作为奖励函数得到的游戏训练过程如图3所示。
5.3 平均分数
r e w a r d = { − 1 , 角 色 死 亡 且 得 分 小 于 平 均 分 数 0 , 角 色 未 死 亡 1 , 角 色 死 亡 且 得 分 不 小 于 平 均 分 数 \begin{aligned} reward=\left \{\begin{array}{ll} -1, & 角色死亡且得分小于平均分数\\ 0, &角色未死亡\\ 1, & 角色死亡且得分不小于平均分数 \end{array} \right. \end{aligned} reward=⎩⎨⎧−1,0,1,角色死亡且得分小于平均分数角色未死亡角色死亡且得分不小于平均分数
以此作为奖励函数得到的游戏训练过程如图4所示。
5.4 结果分析
通过对比 3 3 3种不同奖励函数可以发现,历史最高分效果最好,使用该奖励函数训练智能体最高得分超过了 500 500 500万,但却不稳定。相比而言,固定阈值和平均分数两种奖励函数较为稳定,使用固定阈值则得分稳定在设定的阈值 200 200 200附近,平均分数则稳定在一个较低的水平。