Pong是起源于1972年美国的一款模拟两个人打乒乓球的游戏,近几年常用于测试强化学习算法的性能。这篇文章主要记录如何用DQN实现玩Atari游戏中的Pong,希望对和我一样的小白有所帮助,文章最后附本文代码及参考代码。
torch = 1.8.0+cu111
Python = 3.8.5
环境配置见另一篇博客https://blog.csdn.net/libenfan/article/details/116396388?spm=1001.2014.3001.5502
代码主要包含四个部分:经验重放区ReplayMemory、DQN网络、DQNagent、训练器Trainer。
用于DQN的经验重放,包含的采样、存储、计算经验重放区长度三个方法。
# 定义一个元组表征经验存储的格式
Transition = namedtuple('Transion',
('state', 'action', 'next_state', 'reward'))
class ReplayMemory(object):
def __init__(self, capacity):
self.capacity = capacity
self.memory = []
self.position = 0
def push(self, *args):
if len(self.memory) < self.capacity:
self.memory.append(None)
self.memory[self.position] = Transition(*args)
self.position = (self.position + 1) % self.capacity #移动指针,经验池满了之后从最开始的位置开始将最近的经验存进经验池
def sample(self, batch_size):
return random.sample(self.memory, batch_size)# 从经验池中随机采样
def __len__(self):
return len(self.memory)
三层卷积层,两层线性连接层,(这里要注意卷积层输出的大小要能够与线性层的输入大小相匹配)。
由于需要对pong环境进行重写,因此DQN网络的输入大小在后面介绍pong环境重写的时候会说明。
class DQN(nn.Module):
def __init__(self, in_channels=4, n_actions=14):
"""
Initialize Deep Q Network
Args:
in_channels (int): number of input channels
n_actions (int): number of outputs
"""
super(DQN, self).__init__()
self.conv1 = nn.Conv2d(in_channels, 32, kernel_size=8, stride=4)
# self.bn1 = nn.BatchNorm2d(32)
self.conv2 = nn.Conv2d(32, 64, kernel_size=4, stride=2)
# self.bn2 = nn.BatchNorm2d(64)
self.conv3 = nn.Conv2d(64, 64, kernel_size=3, stride=1)
# self.bn3 = nn.BatchNorm2d(64)
self.fc4 = nn.Linear(7 * 7 * 64, 512)
self.head = nn.Linear(512, n_actions)
def forward(self, x):
x = x.float() / 255
x = F.relu(self.conv1(x))
x = F.relu(self.conv2(x))
x = F.relu(self.conv3(x))
x = F.relu(self.fc4(x.view(x.size(0), -1)))
return self.head(x)
定义一个自己的agent,agent需要包含动作选择、策略学习的方法。
class DQN_agent():
def __init__(self,in_channels=1, action_space=[], learning_rate=1e-4, memory_size=10000, epsilon=1):
self.in_channels = in_channels
self.action_space = action_space
self.action_dim = self.action_space.n
self.memory_buffer = ReplayMemory(memory_size)
self.stepdone = 0
self.DQN = DQN(self.in_channels, self.action_dim).cuda()
self.target_DQN = DQN(self.in_channels, self.action_dim).cuda()
# 加载之前训练好的模型,没有预训练好的模型时可以注释
self.DQN.load_state_dict(torch.load(madel_path))
self.target_DQN.load_state_dict(self.DQN.state_dict())
self.optimizer = optim.RMSprop(self.DQN.parameters(),lr=learning_rate, eps=0.001, alpha=0.95)
def select_action(self, state):
self.stepdone += 1
state = state.to(device)
epsilon = EPS_END + (EPS_START - EPS_END)* \
math.exp(-1. * self.stepdone / EPS_DECAY) # 随机选择动作系数epsilon 衰减,也可以使用固定的epsilon
# epsilon-greedy策略选择动作
if random.random()
用于训练DQNagent,包含获取当前状态、训练和绘制奖励曲线三个方法,输入参数包含环境(env)、agent(DQNagent)、训练的代数(n_episode)。
class Trainer():
def __init__(self, env, agent, n_episode):
self.env = env
self.n_episode = n_episode
self.agent = agent
# self.losslist = []
self.rewardlist = []
# 获取当前状态,将env返回的状态通过transpose调换轴后作为状态
def get_state(self,obs):
state = np.array(obs)
state = state.transpose((2, 0, 1)) #将2轴放在0轴之前
state = torch.from_numpy(state)
return state.unsqueeze(0) # 转化为四维的数据结构
# 训练智能体
def train(self):
for episode in range(self.n_episode):
obs = self.env.reset()
state = self.get_state(obs)
episode_reward = 0.0
# print('episode:',episode)
for t in count():
# print(state.shape)
action = self.agent.select_action(state)
if RENDER:
self.env.render()
obs,reward,done,info = self.env.step(action)
episode_reward += reward
if not done:
next_state = self.get_state(obs)
else:
next_state = None
# print(next_state.shape)
reward = torch.tensor([reward], device=device)
# 将四元组存到memory中
'''
state: batch_size channel h w size: batch_size * 4
action: size: batch_size * 1
next_state: batch_size channel h w size: batch_size * 4
reward: size: batch_size * 1
'''
self.agent.memory_buffer.push(state, action.to('cpu'), next_state, reward.to('cpu')) # 里面的数据都是Tensor
state = next_state
# 经验池满了之后开始学习
if self.agent.stepdone > INITIAL_MEMORY:
self.agent.learn()
if self.agent.stepdone % TARGET_UPDATE == 0:
self.agent.target_DQN.load_state_dict(self.agent.DQN.state_dict())
if done:
break
if episode % 20 == 0:
torch.save(self.agent.DQN.state_dict(), MODEL_STORE_PATH + '/' + "model/{}_episode{}.pt".format(modelname, episode))
print('Total steps: {} \t Episode: {}/{} \t Total reward: {}'.format(self.agent.stepdone, episode, t, episode_reward))
self.rewardlist.append(episode_reward)
self.env.close()
return
#绘制奖励曲线
def plot_reward(self):
plt.plot(self.rewardlist)
plt.xlabel("episode")
plt.ylabel("episode_reward")
plt.title('train_reward')
plt.show()
选择仿真环境,实例化智能体和训练器,并利用训练器对智能体进行训练 。
if __name__ == '__main__':
# create environment
env = gym.make("PongNoFrameskip-v4")
env = make_env(env)
action_space = env.action_space
state_channel = env.observation_space.shape[2]
agent = DQN_agent(in_channels = state_channel, action_space= action_space)
trainer = Trainer(env, agent, n_episode)
trainer.train()
env = make_env(env)
make_env()函数是参考代码作者对gym环境的重写,我去wrappers文件夹下研究了这个函数,其本质和baselines项目的make_atari()函数类似,用于定义自己需要的gym环境。
对于"PongNoFrameskip-v4"这个环境,下面给出参考代码中作者的make_env()函数及解析:
def make_env(env, stack_frames=True, episodic_life=True, clip_rewards=False, scale=False):
if episodic_life:
env = EpisodicLifeEnv(env) # 使得每次游戏玩家都只有一条命
env = NoopResetEnv(env, noop_max=30) # 通过随机抽取重置时的无操作数对初始状态进行采样。
# 相邻帧的画面相似度高需要跳过
env = MaxAndSkipEnv(env, skip=4) #跳过若干(skip)帧画面,并且取最后两帧像素值的最大值
# 如果需要开火则需要玩家按键开始
if 'FIRE' in env.unwrapped.get_action_meanings():
env = FireResetEnv(env)
# 图像处理函数,将原本的210x160x3的彩色图像编程84x84的灰度图像,符合网络结构,即线性层的输入大小对应的是84*84的图像经过卷积层后的输出
env = WarpFrame(env)
if stack_frames:
# 将最后四帧当成状态,
env = FrameStack(env, 4)
if clip_rewards:
# 将游戏的奖励符号化
# 主要是因为之前deepmind团队用一个网络同时玩49个游戏,各个游戏奖励值不一样,避免混淆
env = ClipRewardEnv(env)
return env
最后大家如果没有GPU,可以用Google免费的GPU平台Colab(需要VPN),使用起来比较简单,不过在Colab上面跑代码需要特别关注路径的问题。
希望这篇文章对于初学者能够有一些参考作用,有什么问题欢迎交流,期待大家一起进步。下面附上代码链接。
参考代码:https://github.com/jmichaux/dqn-pytorch
本文代码:https://github.com/libenfan/DQN_pong