简介
这篇笔记主要是记录了Deep Q-Learning Network的开发过程。开发环境是:Ubuntu18.04 、tensorflow-gpu 1.13.1 和 OpenAI gym
其中,这篇笔记记录了深度学习的开发环境。安装完成后,在虚拟环境执行pip install gym
安装界面环境。
强化学习的一个困难的地方,在于数据收集和环境描述。而 OpenAI的gym给我们提供了一个非常强大的虚拟环境,这样我们就可以专注于算法本身的开发了。
这篇笔记主要参考了:
- 论文: https://www.cs.toronto.edu/~vmnih/docs/dqn.pdf
- 博客:https://towardsdatascience.com/cartpole-introduction-to-reinforcement-learning-ed0eb5b58288
环境描述
基本环境可以参考:https://gym.openai.com/envs/CartPole-v1/
学习的目标是使得木棍在小车上树立的时间尽量长。action的选择只有向左或者是向右。环境会自动给出给出反馈,每一步后的得分,下一个局面的描述的状态,是否是结束。环境状态被gym自动封装成一个np.array
,可以通过有关的API获取信息。
在这个例子中,环境的描述是一个4维的向量,我们不必管这4维向量的意义,只需要知道有这个描述即可(当然,如果你感兴趣,可以深究)。每个环境,gym都封装了一分数reward。而且,如果是结束状态,gym会给出描述符。这些在下面的代码中会有说明。
算法介绍和说明
先给出基本算法描述,算法来自上面的参考连接:
这是一个最基本的Off-Policy借助Replay-Buffer和神经网络实现的算法。上面的
ϕ
\phi
ϕ,是表示一个连贯的输入,因为上述的算法是输入了一系列的图片。不过在这个例子中,可以把
ϕ
\phi
ϕ理解成仅仅输入当前的局面,即
s
t
=
ϕ
(
s
t
)
s_t=\phi(s_t)
st=ϕ(st)。之后会有exploration
的操作,这是为了随机的选取那些评估分数比较低,但是可能会有较好表现的行动。
Q
(
s
,
a
)
Q(s,a)
Q(s,a)表示一个Q-function,它的作用是给状态
s
s
s下的每个行动
a
a
a一个评估分数。实际操作中,
Q
Q
Q是一个神经网络,每个状态作为神经网络的输入,神经网络的输出是所有的行动
a
a
a的评估分数。
算法给出了 y i y_i yi的计算法则。对神经网络进行BP的时候,就根据这个公式来即可。每次从buffer中选取一个批次的数据,执行随机梯度下降SGD算法,即可进行修正。
建议仔细阅读原文。
一个说明点:在Deep-Q-Learning Nerwork (DQN)中,最后的输出层的激活函数都是线性的,而且损失函数是Mean Square Error (MSE)。即下面代码中提到的linear
,原因参考自这篇博客:
因为DQN需要计算的是采取某个行动后的评估值,所以最好是线性输出;而且使用MSE可以可以描述两个数据之间的差距,所以MSE是最佳的选择。这就处理一个回归问题。而对于分类问题,我们需要的是处理非线性的,因为最终的结果需要把数据进行概率的划分,一般使用softmax等的分类函数处理。对于DQN的内部隐藏层,一般采取Relu函数进行非线性映射,以增加函数可以表示的状态空间。
还有一个说明的地方,在损失函数中,我们实际需要反向传播的,只有QDN选择的那个动作与评估之间的误差,而其他动作的误差不需要传播。
先给出MSE公式:
M
S
E
=
1
N
∑
i
=
1
N
(
Y
i
−
Y
^
i
)
2
MSE = \frac{1}{N}\sum_{i=1}^N \left(Y_i-\hat{Y}_i\right)^2
MSE=N1i=1∑N(Yi−Y^i)2
举个例子:
Q
(
s
,
a
)
Q(s,a)
Q(s,a)输出[1.0, 0.8],记为q_values
。那么,这个表示动作0的分数是1.0,动作1的分数是0.8。这样来看,agent肯定会采取动作0。假设现在经过计算的该动作的分数是0.7,那么误差的绝对值是0.3,这是需要进行反向传播的。那么,问题是怎样进行反向传播呢?采取的技巧是这样的:新给出一个向量q_update
,等于
Q
(
s
,
a
)
Q(s,a)
Q(s,a)的输出[1.0, 0.8],因为我们之前存储了action,即知道是采取行动0,那么更新q_update
参数为[0.7, 0.8]。
因为反向传递使用MSE,计算方式是:
E
r
r
=
1
2
[
(
0.7
−
1.0
)
2
+
(
0.8
−
0.8
)
2
]
=
1
2
(
0.7
−
1.0
)
2
Err= \frac{1}{2}\left[\left(0.7-1.0\right)^2+\left(0.8-0.8\right)^2\right]=\frac{1}{2}(0.7-1.0)^2
Err=21[(0.7−1.0)2+(0.8−0.8)2]=21(0.7−1.0)2
这个方式很巧妙,只标出了具体行动的误差,其他方式在MSE计算中,都成为0了。可以参考代码好好理解。
代码实例
代码借助深度学习框架tensorflow进行实现。在1.13.1以及将来要发布的版本中,继承了keras接口。个人的看法是,尽量使用高阶API,开发效率高,不易出错,代码简单易懂。
这个例子中,使用了一个全连接神经网络。输入层是4维向量,表示当前状态。两个隐藏层,都包含24个神经元。输出层是2维的,表示向左或者向右采取行动。
强化学习算法Agent.py介绍
import tensorflow as tf
from tensorflow import keras
from collections import deque
import numpy as np
import random
MAX_LEN = 2000
BATCH_SIZE = 64
GAMMA = 0.95
EXPLORATION_DECAY = 0.995
EXPLORATION_MIN = 0.1
class Agent(object):
def __init__(self, input_space, output_space, lr=0.001, exploration=0.9):
self._model = keras.Sequential()
self._model.add(keras.layers.Dense(input_shape=(input_space,), units=24, activation=tf.nn.relu))
self._model.add(keras.layers.Dense(units=24, activation=tf.nn.relu))
# 注意这里输出层的激活函数是线性的!!!
self._model.add(keras.layers.Dense(units=output_space, activation='linear'))
self._model.compile(loss='mse', optimizer=keras.optimizers.Adam(lr))
self._replayBuffer = deque(maxlen=MAX_LEN) # replay buffer,最大200的容量
self._exploration = exploration
@property
def exploration(self):
return self._exploration
def add_data(self, state, action, reward, state_next, done):
self._replayBuffer.append((state, action, reward, state_next, done))
def act(self, state):
if np.random.uniform() <= self._exploration: # 随机走出一步
return np.random.randint(0, 2)
action = self._model.predict(state) # 使用神经网络评估的选择
return np.argmax(action[0])
def train_from_buffer(self):
if len(self._replayBuffer) < BATCH_SIZE:
return
batch = random.sample(self._replayBuffer, BATCH_SIZE) # 随机选取一个批次的数据
for state, action, reward, state_next, done in batch:
if done: # 对应论文中的分数更新
q_update = reward
else:
q_update = reward + GAMMA * np.amax(self._model.predict(state_next)[0])
q_values = self._model.predict(state) # 先赋值,为了减去不相关的行动得分
q_values[0][action] = q_update # 把采取了的行动的分数更新,那么只有这项在MSE中有效果
self._model.fit(state, q_values, verbose=0) # SGD训练模型
self._exploration *= EXPLORATION_DECAY
self._exploration = max(EXPLORATION_MIN, self._exploration)
train.py训练
import gym
from Agent import Agent
import numpy as np
import matplotlib.pyplot as plt
def train():
env = gym.make("CartPole-v1")
input_space = env.observation_space.shape[0]
output_space = env.action_space.n
print(input_space, output_space)
agent = Agent(input_space, output_space)
run = 0
x = []
y = []
while run < 100:
run += 1
state = env.reset()
state = np.reshape(state, [1, -1])
step = 0
while True:
step += 1 # 步数越多,相当于站立的时间越长,比较容易理解。
# env.render()
action = agent.act(state)
state_next, reward, done, _ = env.step(action)
reward = reward if not done else -reward # 棍子倒了,分数肯定是负数了
state_next = np.reshape(state_next, [1, -1])
agent.add_data(state, action, reward, state_next, done)
state = state_next
if done:
print("Run: " + str(run) + ", exploration: " +
str(agent.exploration) + ", score:" + str(step))
x.append(run)
y.append(step)
break
agent.train_from_buffer() # 每次都要执行训练
plt.plot(x, y)
plt.show()
if __name__ == "__main__":
train()
训练结果
首先声明一个地方,强化学习不同于监督学习,曲线是下降的。RL的曲线会波动的很厉害,不过如果模型好的话,大体上会是上升的。这里训练,我测试了多种激活函数。每个进行100局的测试。硬件条件限制,结果肯定是不太准确的,只能看一下大体趋势。
初始情况 batch-size到32,buffer-size到10000。然后把学习速率设置到0.001,看一下效果:
很明显的上升趋势,出现波动属于正常现象,而且20步之后平均分数能到150/。。所以是之前的学习速率设定的太高了。。。。。
在上述的基础上,使用batch-size=64,在此测试结果,得到下面的图像:
平均值更好一些,而且收敛速度相对较快