在上一篇文章强化学习——DQN介绍 中我们详细介绍了DQN 的来源,以及对于强化学习难以收敛的问题DQN算法提出的两个处理方法:经验回放和固定目标值。这篇文章我们就用代码来实现 DQN 算法
本算法以及以后文章要介绍的算法都会使用 由 O p e n A I OpenAI OpenAI 推出的 G y m Gym Gym仿真环境, G y m Gym Gym 是一个研究和开发强化学习相关算法的仿真平台,了许多问题和环境(或游戏)的接口,而用户无需过多了解游戏的内部实现,通过简单地调用就可以用来测试和仿真,并兼容常见的数值运算库如 T e n s o r F l o w TensorFlow TensorFlow 。
import gym
env = gym.make('CartPole-v1')
env.reset()
for _ in range(1000):
env.render()
env.step(env.action_space.sample()) # take a random action
env.close()
运行结果如下:
以上代码中可以看出,gym
的核心接口是Env
。作为统一的环境接口,Env
包含下面几个核心方法:
reset(self)
:重置环境的状态,返回观察。如果回合结束,就要调用此函数,重置环境信息step(self, action)
:执行动作 action
推进一个时间步长,返回observation
,reward
, done
, info
。
observation
表示环境观测,也就是state
reward
表示获得的奖励done
表示当前回个是否结束info
返回一些诊断信息,一般不经常用render(self, mode=‘human’, close=False)
:重新绘制环境的一帧。close(self)
:关闭环境,并清除内存。以上代码首先导入gym
库,第2行创建CartPole-v01
环境,并在第3行重置环境状态。在 for 循环中进行1000个时间步长(*timestep)的控制,第5行刷新每个时间步长环境画面,第6行对当前环境状态采取一个随机动作(0或1),最后第7行循环结束后关闭仿真环境。
CartPole 是gym提供的一个基础的环境,即车杆游戏,游戏里面有一个小车,上有竖着一根杆子,每次重置后的初始状态会有所不同。小车需要左右移动来保持杆子竖直,为了保证游戏继续进行需要满足以下两个条件:
对于 CartPole-v1
环境,其动作是两个离散的动作左移(0)和右移(1),环境包括小车位置、小车速度、杆子夹角及角变化率四个变量。如下代码所示:
import gym
env = gym.make('CartPole-v0')
print(env.action_space) # Discrete(2)
observation = env.reset()
print(observation) # [-0.0390601 -0.04725411 0.0466889 0.02129675]
下面以CartPole-v1
环境为例,来介绍 DQN 的实现
class ReplayBuffer:
def __init__(self, capacity=10000):
self.capacity = capacity
self.buffer = []
self.position = 0
def push(self, state, action, reward, next_state, done):
if len(self.buffer) < self.capacity:
self.buffer.append(None)
self.buffer[self.position] = (state, action, reward, next_state, done)
self.position = int((self.position + 1) % self.capacity)
def sample(self, batch_size = args.batch_size):
batch = random.sample(self.buffer, batch_size)
state, action, reward, next_state, done = map(np.stack, zip(*batch))
return state, action, reward, next_state, done
首先定义一个经验回放池,其容量为 10000,函数 push 就是把智能体与环境交互的到的信息添加到经验池中,这里使用的循环队列的实现方式,注意 position 指针的运算。当需要用数据来更新算法 时,使用 sample 从经验队列中随机挑选 一个 batch_size 的数据,使用 zip 函数把每一条数据打包到一起:
zip: a=[1,2], b=[2,3], zip(a,b) => [(1, 2), (2, 3)]
然后对每一列数据使用 stack 函数转化为列表后返回
本系列强化学习的代码,都是使用的 tensorlayer
,就是对 tensorflow
做了一些封装,使其更加易用,重点是还专门为强化学习内置了一些接口,下面是官网介绍:
TensorLayer 是为研究人员和工程师设计的一款基于Google TensorFlow开发的深度学习与强化学习库。 它提供高级别的(Higher-Level)深度学习API,这样不仅可以加快研究人员的实验速度,也能够减少工程师在实际开发当中的重复工作。 TensorLayer非常易于修改和扩展,这使它可以同时用于机器学习的研究与应用。
定义网络模型:
def create_model(input_state_shape):
input_layer = tl.layers.Input(input_state_shape)
layer_1 = tl.layers.Dense(n_units=32, act=tf.nn.relu)(input_layer)
layer_2 = tl.layers.Dense(n_units=16, act=tf.nn.relu)(layer_1)
output_layer = tl.layers.Dense(n_units=self.action_dim)(layer_2)
return tl.models.Model(inputs=input_layer, outputs=output_layer)
self.model = create_model([None, self.state_dim])
self.target_model = create_model([None, self.state_dim])
self.model.train()
self.target_model.eval()
可以看到tensorlayer 使用起来与tensorflow 大同小异,只要有tensorflow基础一眼就能明白,在上面代码中我们定义一个函数用来生成网络模型。然后创建一个当前网络model
和一个目标网络target_model
,我们知道DQN中的目标网络是起到一个“靶子”的作用,用来评估当前的 target 值,所以我们把它设置为评估模式,调用 eval()
函数即可。而 model
网络是我们要训练的网络,调用函数 train()
设置为训练模式。
for episode in range(train_episodes):
total_reward, done = 0, False
while not done:
action = self.choose_action(state)
next_state, reward, done, _ = self.env.step(action)
self.buffer.push(state, action, reward, next_state, done)
total_reward += reward
state = next_state
# self.render()
if len(self.buffer.buffer) > args.batch_size:
self.replay()
self.target_update()
关于与环境交互过程在上面已经介绍过了,这里重点看 第 10 行的 if 语句,当经验池的长度大于一个batch_size
时,就开始调用replay()
函数来更新网络 model
的网络参数,然后调用target_update()
函数把 model
网络参数复制给 target_model
网络。
def replay(self):
for _ in range(10):
states, actions, rewards, next_states, done = self.buffer.sample()
# compute the target value for the sample tuple
# target [batch_size, action_dim]
# target represents the current fitting level
target = self.target_model(states).numpy()
next_q_value = tf.reduce_max(self.target_model(next_states), axis=1)
target_q = rewards + (1 - done) * args.gamma * next_q_value
target[range(args.batch_size), actions] = target_q
with tf.GradientTape() as tape:
q_pred = self.model(states)
loss = tf.losses.mean_squared_error(target, q_pred)
grads = tape.gradient(loss, self.model.trainable_weights)
self.model_optim.apply_gradients(zip(grads, self.model.trainable_weights))
这部分应该就是 DQN 的核心代码了,在replay()
函数中,我们循环更新更新当前网络十次,目的就是改变两个网络的更新频率,有利于网络收敛。
具体的更新部分:我们知道,DQN就是把Q-Learning中的Q表格换成了神经网络,两者之间有很多 相似之处,我们可以类比Q-Learning 的更新方式。对于Q表格形式,我们获取某一个状态的动作价值Q是直接通过下标得到的,那么在神经网络中就需要把状态输入神经网络,经过前向计算得到。
Δ w = α ( r + γ m a x a ′ Q ^ ( s ′ , a ′ , w ) − Q ^ ( s , a , w ) ) ⋅ ∇ w Q ^ ( s , a , w ) \Delta w = \alpha (r + \gamma\;max_{a'}\; \hat{Q}(s', a', w) - \hat{Q}{(s, a, w)})\cdot \nabla_w\hat{Q}{(s, a, w)} Δw=α(r+γmaxa′Q^(s′,a′,w)−Q^(s,a,w))⋅∇wQ^(s,a,w)
第三行首先获取一个batch_size
的数据,这个过程称为 sample
。第7行我们首先获取当前的动作价值,target 表示的是根据当前的网络参数计算得到的动作价值。然后第8行先获取当前网络参数下 的下一个状态的所有动作,然后使用reduce_max()
函数找出最大的动作价值。然后第9行和第10行利用下一个状态最大的动作价值来计算出 target_q
,也就是 r + γ m a x a ′ Q ^ ( s ′ , a ′ , w ) r + \gamma\;max_{a'}\; \hat{Q}(s', a', w) r+γmaxa′Q^(s′,a′,w) 部分,然后更新target
。注意上面我们计算target时一直在使用 target_model
网络,target网络只有在评估网络状态时才使用。
接着我们使用 q_pred = self.model(states)
网络获取当前 网络的状态,也就是 公式中的 Q ^ ( s , a , w ) \hat{Q}{(s, a, w)} Q^(s,a,w) ,利用MSE函数计算其损失函数,最后更新 model
网络。
完整代码请参考强化学习——DQN代码地址 还请给个 star
,谢谢各位了
虽然 DQN 提出的这两个解决方案不错,但是仍然还有问题没有解决,比如:
对应第一个问题的改进就是 Double DQN ,第二个问题的改进是 Dueling DQN。他们都属与DQN的改进版,我们下篇文章介绍。