本文记录了我初次使用DQN训练agent完成Atari游戏之"Breakout-v0"的过程。整个过程仿照DeepMind在nature发表的论文"Human-level control through deep reinforcement learning"。
首先确定"Breakout-v0"的动作空间和状态空间,其状态是游戏截图。
env = gym.make('Breakout-v0')
print(env.observation_space) # Box(0, 255, (210, 160, 3), uint8)
print(env.action_space) # Discrete(4)
env.reset()
observation = env.reset()
print(observation.shape) # (210, 160, 3) h, w, c
由上可以知道,"Breakout-v0"的状态空间和动作空间的基本情况:
上述两个空间都是gym
中的抽象类space
的实例化。space
有两个常用的方法:
contains(x)
:检查"x"是否属于该空间;sample()
:随机从空间中返回一个采样值。env = gym.make('Breakout-v0')
action_space = env.action_space
print(action_space.contains(2)) # True
print(action_space.sample()) # 3
gym
的第一步通常是使用make
得到一个游戏环境,如"Breakout-v0"。这个环境是抽象类env
的实例化,通常具有以下属性和方法:
observation_space
:环境的状态空间;action_space
:环境的动作空间;reset()
:重置环境为初始状态,并且返回该初始状态;step()
:agent与环境交互的接口。它会接受一个动作,然后返回更新后的环境状态、奖励、结束标识符以及其它相关游戏信息。env = gym.make('Breakout-v0')
env.reset()
observation, reward, done, info = env.step(env.action_space.sample())
print(observation) # (210, 160, 3)
print(reward) # scalar
print(done) # bool
print(info) # {'ale.lives': 5}
gym
除了上述功能外,还有一个强大的自定义工具Wrapper
。Wrapper
是env
的子类,其构造函数只接受一个参数:env
的实例化对象。通过这个抽象类,我们可以对env
的功能进行一些扩展。比如,可以自定义step()
或reset()
,改变环境的状态输出;也可以对奖励进行一些处理等等。
此外,为了处理更加特殊的情况,gym
还对Wrapper
扩展了三个子类,使其可以单独对状态、动作和奖励进行处理。
ObservationWrapper
:observation(obs)
ActionWrapper
:action(act)
RewardWrapper
:reward(rew)
接下来以ActionWrapper
为例,说明如何使用该类对动作进行处理。以下代码是ActionWrapper
的源码。可以发现,ActionWrapper
的step()
并没有直接接受输入的action,而是先使用action()
进行了某些处理。 但是处理方法并没有定义,这里就需要我们对该方法根据使用需要重新定义该函数。
# gym源码
class ActionWrapper(Wrapper):
def reset(self, **kwargs):
return self.env.reset(**kwargs)
def step(self, action):
return self.env.step(self.action(action))
def action(self, action):
raise NotImplementedError
def reverse_action(self, action):
raise NotImplementedError
以下是对action()
的重定义,用定义好的ActionWrapper
封装env后,程序就会在使用step()
时自动调用action()
对输入动作进行处理。
# 自定义动作
class RandomActionWrapper(gym.ActionWrapper):
def __init__(self, env, epsilon=0.1):
super(RandomActionWrapper, self).__init__(env)
self.epsilon = epsilon
def action(self, action):
if random() > self.epsilon:
print("it is random!")
return self.env.action_space.sample()
else:
return action
一些常见Wrapper解读举例和解读了常见的Wrapper
,这些Wrapper
就是DeepMind在论文中提出的一些处理过程。
除此之外,其实通过gym.make()
获得的环境本身也是经过封装的。使用env.unwrapper
可以获得最原始的环境。比如,使用gym.make('CartPole-v0')
获得的环境存在最大迭代次数为200次的设置,但是env.unwrapper
就没有这种限制了。
env.unwrapper
存在一些方法可以让我们理解该环境的一些内容:
“env.unwrapped.get_action_meanings()”:返回字符列表,表示动作空间每一个对应动作的含义;
env = gym.make("Breakout-v0")
env.action_space # Discrete(4) [0, 1, 2, 3]
env.unwrapped.get_action_meanings() # ['NOOP', 'FIRE', 'RIGHT', 'LEFT']
如果你想记录agent的训练过程,那么Monitor
是一个很好的工具。只需要对环境做一个简单的封装,Monitor
就可以实现很多有用的功能。但需要注意的是,使用该工具必须要确保系统具有FFmpeg
功能,否则程序会报错。最简单的做法就是安装FFmpeg
。在Windows环境下,可以使用命令conda install -c conda-forge ffmpeg
进行安装。
env = gym.make("Breakout-v0")
# argument2: save path
# argument3(force): if force=False,then the save folder shouldn't exist.
env = gym.wrappers.Monitor(env, "recording", force=True)
DQN中的一个重要机制就是经验回放机制,它是为了打破训练数据之间的相关性而专门设置的。在深度学习中,通常会假设训练数据是相互独立的。但是在强化学习中,这种假设并不成立。因为,它的学习资料是各组相互关联的MDP序列。
为了打破训练数据之间的相关性,DQN会专门设置一个经验池,用于储存经历过的状态序列 ( x i , a i , r i + 1 , x i + 1 ) (x_i,a_i,r_{i+1},x_{i+1}) (xi,ai,ri+1,xi+1)。然后,DQN会从经验池中随机抽取"batch_size"组序列用于神经网络的训练。在提出DQN的论文中,把经验池的大小设置为1,000,000,即可以存放这个数量的序列。
First, we use a technique known as experience replay23 in which we store the agent’s experiences at each time-step, e t = ( s t , a t , r t , s t + 1 ) e_t=(s_t,a_t,r_t,s_{t+1}) et=(st,at,rt,st+1), in a data set D t = e 1 , … , e t D_t={e_1,…,e_t} Dt=e1,…,et, pooled over many episodes (where the end of an episode occurs when a terminal state is reached) into a replay memory.
经验回放池需要实现以下功能:
关于第三个功能,即把k帧图像重构为一组状态输入,存在以下三种情况特殊处理:
由于经验池的容量是固定的,当经验池填满后,算法会根据先进先出的原则移除最早的数据。因此,对于第2种情况,我们需要进行经验的首尾拼接。
关于剩余2种情况,一种方法是用0填充不足的图像;当然,也可以不处理这种情况。
下面详细分析以上几种特殊情况的处理方法(注:从后往前检测):
situation 1 (no situation 3)
假设需要4帧图像,但是目前经验池中只有2帧,则取该2帧图像,剩余2帧图像用0补足。
situation 1 (with situation 3)
假设需要4帧图像,但是目前经验池中只有3帧。取停止标志后的那些帧,剩余的用0补足。
situation 2 (no situation 3)
假设需要4帧图像,但是前2帧是经验池的最后两帧,后2帧是经验池的开始2帧。将这4帧拼接。
situation 2 (no situation 3)
假设需要4帧图像,但是前2帧是经验池的最后两帧,后2帧是经验池的开始2帧。
停止标志出现在前2帧
取后两帧和停止标志后的那些帧,剩余的用0补足。
停止标志出现在后2帧
取停止标志后的那些帧,剩余的用0补足。
situation 3
取停止标志后的那些帧,剩余的用0补足。
DQN与其它使用神经网络训练强化学习的方法不同,它把target网络和预测网络分开了。两个网络采样相同的网络结构,但是target网络不参与模型的训练,而是每隔一定时间加载预测网络的参数。
The second modification to online Q-learning aimed at further improving the stability of our method with neural networks is to use a separate network for generating the targets yj in the Q-learning update. More precisely, every C updates we clone the network Q to obtain a target network Q ^ \hat{Q} Q^ and use Q ^ \hat{Q} Q^ for generating the Q-learning targets y j y_j yj for the following C updates to Q.
这样处理可以让算法更加稳定,减少策略更新时的振荡。
DeepMind在论文中还提出了很多可以提高算法性能的trick。这些数据处理的过程通常都可以使用Wrapper
对环境进行改写,上文的重组k帧图像的过程也可以放在Wrapper
中处理。
下面简要介绍DeepMind的一些小技巧:
其它内容可以查看DeepMind发表的论文"Human-level control through deep reinforcement learning"。
关于DQN的代码实现,我放在了这里。我是基于Pytorch实现的,torch版本为1.8.1。由于本人没有专门跑深度学习的配置,因此只跑了900000次就结束了。从结果看,agent的得到的平均奖励还是有明显提升的。