关于DQN算法的详细讲解可以看一下科老师的公开课(公开课地址),可以很清晰的了解整个DQN算法的详细流程。
这里展示了DQN算法的伪代码:
DQN算法引入神经网络Q来拟合Q值,它有两大创新点:
经验回放是为了切断输入样本的相关性,并解决了样本利用率低的问题,具体做法是增加一个缓冲区(buffer)来存放与环境交互的数据,学习时再抽取batch来训练Q网络。
一组数据通常为 (s,a,r,s_),因此DQN是一个典型的off-policy算法。
固定Q目标是解决算法更新不平稳的问题,因为target_q值也是需要过一遍Q网络才能拿到的,而Q网络是在不定更新的,因此会造成算法的不平稳。举个例子,射箭是我们射击的兔子是在不断运动的,因此很难进行瞄准,而射击靶标时则更容易射中。
未解决这个问题,需要建立一个和Q网络一模一样的Target_Q网络,来输出target_q值,每隔一段时间再拷贝Q网络的参数到Target_Q网络。
熟悉监督学习的朋友都知道,神经网络学习的反向传递过程是需要计算网络预测值和label值的loss,而在强化学习中是没有label,我们则使用target_q值来作为label。
计算Q网络的预测值和target_q的loss,反向传递来优化神经网络,使其能更好的预测Q值。
这里使用的环境是gym中的 ‘CartPole-v0’,它有向左和向右两个action,保持杆子不倒且不超过边界的时间越长,获得的奖励越大。
代码的整体框架如下图:如果理解DQN算法按照这个框架可以很容易的实现算法。
主函数主要实现 run episode的过程,首先定义DQN的类,然后进行400个episode的循环,要注意的地方是:
我们每20个episode进行一下测试,也就是运行test_episode的函数,main函数的代码如下:
def main():
dqn = DQN()
print('\nCollecting experience...')
for i_episode in range(400):
s = env.reset()
ep_r = 0
while True:
env.render()
# 选择动作
a = dqn.choose_action(torch.unsqueeze(torch.FloatTensor(s), 0))
# 与环境交互
s_, r, done, info = env.step(a)
# 重新定义reward
x, x_dot, theta, theta_dot = s_
r1 = (env.x_threshold - abs(x)) / env.x_threshold - 0.8
r2 = (env.theta_threshold_radians - abs(theta)) / env.theta_threshold_radians - 0.5
r = r1 + r2
# 存入数据到经验池
dqn.store_transition(s, a, r, s_)
ep_r += r
# 存入一定的数据量再进行训练
if dqn.memory_counter > MEMORY_CAPACITY:
dqn.learn()
if done:
print('Ep: ', i_episode,
'| Ep_r: ', round(ep_r, 2))
if done:
break
s = s_ # 传递状态
# 每10个episode测试一下训练成果
if (i_episode > 200) and (i_episode % 10) == 0:
eval_reward = test_episode(env, dqn, render=True)
print('episode:{} e_greed:{} Test reward:{}'.format(i_episode, EPSILON, eval_reward))
神经网络就是常见的pytorch神经网络搭建方法,这里我们搭建了两层的神经网络:
代码如下:
class Net(nn.Module):
def __init__(self, ):
super(Net, self).__init__()
self.fc1 = nn.Linear(N_STATES, 50)
self.fc1.weight.data.normal_(0, 0.1) # initialization
self.out = nn.Linear(50, N_ACTIONS)
self.out.weight.data.normal_(0, 0.1) # initialization
def forward(self, x):
x = self.fc1(x)
x = F.relu(x)
actions_value = self.out(x)
return actions_value
DQN类的整体框架如下图:
e_greed策略选择动作的函数,有0.9的概率选择Q网络输出Q值最大的动作,0.1的概率随机选择动作,保持一定的探索。
是用来存放交互数据的,很简单,创建双端队列buffer,往里append数据就好了。
learn函数主要实现了三个功能:
DQN类的代码如下:
class DQN(object):
def __init__(self):
self.eval_net, self.target_net = Net(), Net()
self.learn_step_counter = 0 # for target updating
self.memory_counter = 0 # for storing memory
self.optimizer = torch.optim.Adam(self.eval_net.parameters(), lr=LR)
self.loss_func = nn.MSELoss()
self.buffer = collections.deque(maxlen=max_size) # 创建一个双端队列,用于储存经验池
def choose_action(self, obs):
if np.random.uniform() < EPSILON:
action = self.predict(obs) # 根据神经网络的输出来选择动作
else:
action = np.random.choice(N_ACTIONS) # 随机选择动作的策略
action = action if ENV_A_SHAPE == 0 else action.reshape(ENV_A_SHAPE)
return action
# 根据神经网络输出Q值选择动作
def predict(self,obs):
pred_Q = self.eval_net.forward(obs)
action = torch.max(pred_Q, 1)[1].data.numpy()
action = action[0] if ENV_A_SHAPE == 0 else action.reshape(ENV_A_SHAPE)
return action
# 经验池的存储函数
def store_transition(self, s, a, r, s_):
self.buffer.append((s,a,r,s_))
self.memory_counter += 1
def learn(self):
# target网络的更新,是每隔一段时间进行更新
if self.learn_step_counter % TARGET_REPLACE_ITER == 0:
self.target_net.load_state_dict(self.eval_net.state_dict())
self.learn_step_counter += 1
# 从经验池中取数据
mini_batch = random.sample(self.buffer,BATCH_SIZE)
obs_batch,action_batch,reward_batch,next_obs_batch,done_batch = [],[],[],[],[]
for experience in mini_batch:
s,a,r,s_p = experience
obs_batch.append(s)
action_batch.append(a)
reward_batch.append(r)
next_obs_batch.append(s_p)
# 转换数据的格式
obs_batch = np.array(obs_batch).reshape(-1, 4)
action_batch = np.array(action_batch).reshape(-1, 1)
reward_batch = np.array(reward_batch).reshape(-1,1)
next_obs_batch = np.array(next_obs_batch).reshape(-1, 4)
b_s = torch.FloatTensor(obs_batch)
b_a = torch.LongTensor(action_batch)
b_r = torch.FloatTensor(reward_batch)
b_s_ = torch.FloatTensor(next_obs_batch)
# 获取Q网络的输出值
pred_q = self.eval_net(b_s).gather(1, b_a)
# 从target_model中获取 max Q' 的值,用于计算target_Q
q_next = self.target_net(b_s_).detach() # 阻止梯度的传递
target_q = b_r + GAMMA * q_next.max(1)[0].view(32,1)
# 计算loss = predict_q - target_q
loss = self.loss_func(pred_q,target_q)
# 更新Q网络
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
test_episode函数过程和main函数中的过程有点类似,只不过它不再是用e_greed的方法来选择动作,而是用我们训练好的网络来直接输出动作,每次测试进行5个episode,求这5个episode的平均reward,代码如下:
def test_episode(env,agent,render=False):
eval_reward = []
for i in range(5):
obs = env.reset()
episode_reward = 0
while True:
action = agent.predict(torch.unsqueeze(torch.FloatTensor(obs),0)) # 选取动作全部由e-greed策略,不再有探索
next_obs,reward,done,_ = env.step(action)
episode_reward += reward
obs = next_obs
if render:
env.render()
if done:
break
eval_reward.append(episode_reward)
return np.mean(eval_reward)
在一开始的episode,杆子只能立起很小的一段时间,获得的reward也很低,但是在当训练到250个episode后,reward值会逐渐增大。