一、DQN
DQN原理DQN
第一次提出的论文Playing Atari with Deep Reinforcement Learning
作者提出了用深度学习模型来拟合价值函数,结合Q-Learning进行训练的深度强化学习算法DQN,并且在Atari 2600上进行了验证。
在2015的Human-level control through deep reinforcement learning加入Target Network
一般DQN算法完整流程:
车杆环境实现DQN代码:
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import gym
BATCH_SIZE=32
LR=0.01
EPSILON=0.9
GAMMA=0.9
TARGET_REPLACE_ITER=100
MEMORY_CAPACITY=2000
env=gym.make("CartPole-v1",render_mode="human")
N_ACTIONS=env.action_space.n
N_STATES=env.observation_space.shape[0]
拟合 Q ( s , a ) Q(s,a) Q(s,a)的 Q Q Q网络
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)
self.out=nn.Linear(50,N_ACTIONS)
self.out.weight.data.normal_(0,0.1)
def forward(self,x):
x=F.relu(self.fc1(x))
actions_value=self.out(x)
return actions_value
定义DQN算法:
class DQN(object):
def __init__(self):
self.eval_net,self.target_net=Net(),Net()
self.learn_step_counter=0
self.memory_counter=0
self.memory=np.zeros((MEMORY_CAPACITY,N_STATES*2+2))
self.optimizer=torch.optim.Adam(self.eval_net.parameters(),lr=LR)
self.loss_func=nn.MSELoss()
def choose_action(self,x):
x=torch.unsqueeze(torch.FloatTensor(x),0)
if np.random.uniform()<EPSILON:#greedy
actions_value=self.eval_net.forward(x)
action=torch.max(actions_value,1)[1].data.numpy()
action=action[0]
else:#随机
action=np.random.randint(0,N_ACTIONS)
return action
def store_transition(self,s,a,r,s_):
transition=np.hstack((s,[a,r],s_))
index=self.memory_counter%MEMORY_CAPACITY
self.memory[index,:]=transition
self.memory_counter+=1
def learn(self):
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
sample_index=np.random.choice(MEMORY_CAPACITY,BATCH_SIZE)
b_memory=self.memory[sample_index,:]
b_s=torch.FloatTensor(b_memory[:,:N_STATES])
b_a=torch.LongTensor(b_memory[:,N_STATES:N_STATES+1].astype(int))
b_r=torch.FloatTensor(b_memory[:,N_STATES+1:N_STATES+2])
b_s_=torch.FloatTensor(b_memory[:,-N_STATES:])
q_eval=self.eval_net(b_s).gather(1,b_a)
q_next=self.target_net(b_s_).detach()
q_target=b_r+GAMMA*q_next.max(1)[0].view(BATCH_SIZE,1)
loss=self.loss_func(q_eval,q_target)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
dqn=DQN()
for i in range(400):
print('<<<<<<<Episode:%s'%i)
s=env.reset()
episode_reward_sum=0
while True:
a=dqn.choose_action(s)
s_,r,done,info=env.step(a)
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
new_r=r1+r2
dqn.store_transition(s,a,new_r,s_)
episode_reward_sum+=new_r
s=s_
if dqn.memory_counter>MEMORY_CAPACITY:
dqn.learn()
if done:
print('episode%s---reward_sum:%s'%(i,round(episode_reward_sum,2)))
break
二、DDQN
论文Deep Reinforcement Learning with Double Q-learning
高估问题(overestimation) 一直是强化学习中存在的问题,但是一直以来其来源有多种说法,一是归因于不灵活的函数近似(Thrun和Schwartz, 1993),一是来自噪声(van Hasselt, 2010, 2011)。通过实验也表明,高估问题的普遍性相较之前认识的要普遍的多,需要找到方法缓解其影响。
因为神经网络等估计方法存在误差,导致估计出的在某一固定状态
s
s
s下,不同动作的动作价值函数
Q
(
A
t
=
a
i
,
S
t
=
s
)
Q(A_t=a_i, S_t=s)
Q(At=ai,St=s)相对真实值
Q
∗
(
A
t
=
a
i
,
S
t
=
s
)
Q_*(A_t=a_i, S_t=s)
Q∗(At=ai,St=s)估计误差。假设估计噪声为高斯,无偏均值为0,误差可正可负,到此并无大碍,但是接下来的更新时的操作计算TD-target的时候,使用了max操作,这导致在这一步的误差始终为正的,造成正的偏差,这就是导致高估的原因。
上图表示在实现上,Q 值往往是被高估的。红色锯齿状的一直在变的线表示 Q 函数对不同的状态估计的平均 Q 值。DQN预估出来的值远比真实值大,且大很多,在每一个游戏中都是这样。
蓝色的锯齿状的线是 DDQN 的 Q 网络所估测出来的 Q 值,蓝色的无锯齿状的线是真正的Q 值,它们是比较接近的。我们用 DDQN 得出的真正的 Q 值在都是比原来的深度 Q 网络高的,代表 DDQN 学习出来的策略比较强,所以实际上得到的奖励是比较大的。虽然一般的深度 Q 网络的 Q 网络高估了自己会得到的奖励,但实际上它得到的奖励是比较低的。
因为实际在训练的时候我们要让左式与右式(目标)越接近越好。但目标的值
很容易被设得太高,因为在计算目标的时候,我们实际上在做的,是看哪一个
a
a
a可以得到最大的
Q
Q
Q 值,就把它加上去变成目标。
Q
(
s
t
,
a
t
)
⟷
r
t
+
max
a
Q
(
s
t
+
1
,
a
)
\begin{aligned}Q\left(s_t,a_t\right)\longleftrightarrow r_t+\max_aQ\left(s_{t+1},a\right)\end{aligned}
Q(st,at)⟷rt+amaxQ(st+1,a)
假设我们现在有 4 个动作,本来它们得到的
Q
Q
Q 值都是差不多的,它们得到的奖励也是差不多的。但是在估计的时候,网络是有误差的。如图所示,假设是第一个动作被高估了,绿色代表是被高估的量,智能体就会选这个动作,就会选这个高估的
Q
Q
Q 值来加上
r
t
r_t
rt 来当作目标。如果第四个动作被高估了,智能体就会选第四个动作来加上
r
t
r_t
rt 当作目标。所以智能体总是会选那个
Q
Q
Q值被高估的动作,总是会选奖励被高估的动作的
Q
Q
Q 值当作最大的结果去加上
r
t
r_t
rt 当作目标,所以目标值总是太大。
解决办法:
在 DDQN 里面,选动作的
Q
Q
Q 函数与计算值的
Q
Q
Q 函数不是同一个。在原来的DQN里面,我们穷举所有的
a
a
a,把每一个
a
a
a 都代入
Q
Q
Q 函数,看哪一个
a
a
a 可以得到的
Q
Q
Q 值最高,就把那个
Q
Q
Q 值加上
r
t
r_t
rt。
在 DDQN 里面有两个
Q
Q
Q 网络,第一个
Q
Q
Q 网络
Q
Q
Q 决定哪一个动作的
Q
Q
Q 值最大(我们把所有的
a
a
a 代入
Q
Q
Q 函数中,看看哪一个
a
a
a 的
Q
Q
Q 值最大)。我们决定动作以后,
Q
Q
Q 值是用
Q
′
Q′
Q′ 算出来的。
如式所示,假设我们有两个
Q
Q
Q 函数——
Q
Q
Q 和
Q
′
Q′
Q′,如果
Q
Q
Q 高估了它选出来的动作
a
a
a,只要
Q
′
Q′
Q′没有高估动作
a
a
a 的值,算出来的就还是正常的值。假设
Q
′
Q′
Q′ 高估了某一个动作的值,也是没问题的,因为只要
Q
Q
Q 不选这个动作就可以,这就是 DDQN 神奇的地方。
Q
(
s
t
,
a
t
)
⟷
r
t
+
Q
′
(
s
t
+
1
,
arg
max
a
Q
(
s
t
+
1
,
a
)
)
Q\left(s_{t},a_{t}\right)\longleftrightarrow r_{t}+Q^{\prime}\left(s_{t+1},\arg\max_{a}Q\left(s_{t+1},a\right)\right)
Q(st,at)⟷rt+Q′(st+1,argamaxQ(st+1,a))
我们动手实现的时候,有两个 Q Q Q 网络:会更新的 Q Q Q 网络和目标 Q Q Q 网络。所以在 DDQN 里面,我们会用会更新参数的 Q Q Q 网络去选动作,用目标 Q Q Q 网络(固定住的网络)计算值。
DDQN改动部分:
只需改动计算target时计算方法,用我们每次更新的Q网络去挑选最大Q值的action,挑选出action之后用Target Q网络输出下个状态此动作Q值,去计算target
q_eval=self.eval_net(b_s).gather(1,b_a)
q_next_eval=self.eval_net(b_s_).detach()
q_next_target = self.target_net(b_s_).detach()
max_action=q_next_eval.max(1)[1].view(BATCH_SIZE,1)
q_next=q_next_target.gather(1,max_action)
q_target=b_r+GAMMA*q_next
loss=self.loss_func(q_eval,q_target)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
原DQN:
q_eval=self.eval_net(b_s).gather(1,b_a)
q_next=self.target_net(b_s_).detach()
q_target=b_r+GAMMA*q_next.max(1)[0].view(BATCH_SIZE,1)
loss=self.loss_func(q_eval,q_target)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
三、Dueling DQN
论文Dueling Network Architectures for Deep Reinforcement Learning
相较于原来的DQN,它唯一的差别是改变了网络的架构。
Q
Q
Q 网络输入状态,输出的是每一个动作的
Q
Q
Q 值。如图所示,原来的DQN直
接输出
Q
Q
Q 值,Dueling DQN不直接输出
Q
Q
Q 值,而是分成两条路径运算。第一条路径会输出一个标量
V
(
s
)
V (s)
V(s),因为它与输入
s
s
s 是有关系的,所以称为
V
(
s
)
V (s)
V(s)。第二条路径会输出一个向量
A
(
s
,
a
)
A(s, a)
A(s,a),它的每一个动作都有一个值。我们再把
V
(
s
)
V (s)
V(s) 和
A
(
s
,
a
)
A(s, a)
A(s,a) 加起来就可以得到
Q
Q
Q 值
Q
(
s
,
a
)
Q(s, a)
Q(s,a)。
Q
(
s
,
a
)
=
V
(
s
)
+
A
(
s
,
a
)
\boldsymbol{Q}(s,\boldsymbol{a})=V(\boldsymbol{s})+\boldsymbol{A}(\boldsymbol{s},\boldsymbol{a})
Q(s,a)=V(s)+A(s,a)
假设我们在训练网络的时候,目标是希望 Q 表格中第一行第二列的值变成 4,第二行第二列的值变成 0。但是我们实际上能修改的并不是 Q 值,能修改的是 V (s) 与 A(s, a) 的值。根据网络的参数,V (s) 与 A(s, a) 的值输出以后,就直接把它们加起来,所以其实不是修改 Q 值。在学习网络的时候,假设我们希望 Q 表格中的 3 增加 1 变成 4、−1 增加 1 变成 0。最后我们在训练网络的时候,我们可能就不用修改 A(s, a) 的值,就修改 V (s) 的值,把 V (s) 的值从 0 变成 1。从 0 变成 1 有什么好处呢?本来只想修改两个值,但 Q 表格中的第三个值也被修改了:−2 变成了 −1。所以有可能我们在某一个状态下,只采样到这两个动作,没采样到第三个动作,但也可以更改第三个动作的 Q 值。这样的好处就是我们不需要把所有的状态-动作对都采样,可以用比较高效的方式去估计 Q 值。因为有时候我们更新的时候,不一定是更新 Q 表格,而是只更新了 V (s),但更新 V (s) 的时候,只要修改 V (s) 的值,Q 表格的值也会被修改。Dueling DQN是一个使用数据比较有效率的方法。
可能会有人认为使用Dueling DQN会有一个问题,Dueling DQN最后学习的结果可能是这样的:智能体就学到 V (s) 等于 0,A(s, a) 等于 Q,使用任何Dueling DQN就没有任何好处,就和原来的DQN一样。为了避免这个问题出现,实际上我们要给 A(s, a) 一些约束,让 A(s, a) 的更新比较麻烦,让网络倾向于使用 V (s) 来解决问题。例如,我们有不同的约束,一个最直觉的约束是必须要让 A(s, a) 的每一列的和都是 0,所以看我这边举的例子,列的和都是 0。如果这边列的和都是 0,我们就可以把 V (s) 的值想成是上面 Q 的每一列的平均值。这个平均值,加上 A(s, a) 的值才会变成是 Q 的值。
实现时,我们要给这个 A(s, a) 一个约束。假设有 3 个动作,输出的向量是
,我们在把 A(s, a) 与 V (s) 加起来之前,先进行归一化(normalization)。计算这三个动作输出均值,并将每一个动作减去均值。这样 A(s, a) 就会有比较大的约束,网络就会给它一些好处,让它倾向于去更新 V (s) 的值,这就是Dueling DQN。
Dueling DQN改动部分:
只需改动拟合Q值的网络结构
class DuelingNet(nn.Module):
def __init__(self):
super(DuelingNet, self).__init__()
self.fc1 = nn.Linear(N_STATES, 50)
self.fc1_advantage = nn.Linear(50, N_ACTIONS)
self.fc1_value = nn.Linear(50, 1)
self.fc1.weight.data.normal_(0, 0.1)
self.fc1_advantage.weight.data.normal_(0, 0.1)
self.fc1_value.weight.data.normal_(0, 0.1)
def forward(self, x):
x = F.relu(self.fc1(x))
adv = self.fc1_advantage(x)
val = self.fc1_value(x)
q_values = val + (adv - adv.mean(dim=1, keepdim=True))
return q_values
原DQN:
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)
self.out=nn.Linear(50,N_ACTIONS)
self.out.weight.data.normal_(0,0.1)
def forward(self,x):
x=F.relu(self.fc1(x))
actions_value=self.out(x)
return actions_value
三个网络结果
还在学习如何优化,欢迎提出宝贵意见!