文章目录
Q-learning中的过高估计
普通的 DQN 算法通常会导致对值的过高估计(overestimation)。传统 DQN 优化的 TD 误差目标为
r
+
γ
max
a
′
Q
ω
−
(
s
′
,
a
′
)
r+\gamma\max_{a^{\prime}}Q_{\omega^{-}}\left(s^{\prime},a^{\prime}\right)
r+γa′maxQω−(s′,a′)
其中
max
a
′
Q
ω
−
(
s
′
,
a
′
)
\max_{a^{\prime}}Q_{\omega^{-}}\left(s^{\prime},a^{\prime}\right)
maxa′Qω−(s′,a′)可以变为这种形式
Q
ω
−
(
s
′
,
arg
max
a
′
Q
ω
−
(
s
′
,
a
′
)
)
Q_{\omega^-}\left(s',\arg\max_{a'}Q_{\omega^-}\left(s',a'\right)\right)
Qω−(s′,arga′maxQω−(s′,a′))即
m
a
x
max
max操作实际可以被拆解为两部分:首先选取状态
s
′
s'
s′下的最优动作
a
∗
=
arg
max
a
′
Q
ω
−
(
s
′
,
a
′
)
a^*=\arg\max_{a^{\prime}}Q_{\omega^-}\left(s^{\prime},a^{\prime}\right)
a∗=argmaxa′Qω−(s′,a′),接着计算该动作对应的价值
Q
ω
−
(
s
′
,
a
∗
)
Q_{\omega^{-}}\left(s^{\prime},a^{*}\right)
Qω−(s′,a∗)。但
m
a
x
max
max 操作使得 𝑄 函数的值越来越大,甚至高于真实值。原因如下:假设有随机变量
X
1
X_1
X1,
X
2
X_2
X2,取
m
a
x
max
max之后的期望大于等于先取期望再取
m
a
x
max
max操作,
E
[
max
(
X
1
,
X
2
)
]
≥
max
(
E
[
X
1
]
,
E
[
X
2
]
)
\mathbb{E}[\max(X_1,X_2)]\geq\max(\mathbb{E}[X_1],\mathbb{E}[X_2])
E[max(X1,X2)]≥max(E[X1],E[X2])𝑄 函数的值被视作在状态 𝑠′,动作 𝑎′ 下的回报期望值。于是有
max
a
′
∈
A
Q
θ
′
(
s
t
+
1
,
a
′
)
=
Q
θ
′
(
s
t
+
1
,
arg
max
a
′
Q
θ
′
(
s
t
+
1
,
a
′
)
)
=
E
[
R
∣
s
t
+
1
,
arg
max
a
′
Q
θ
′
(
s
t
+
1
,
a
′
)
,
θ
′
]
≥
max
(
E
[
R
∣
s
t
+
1
,
a
1
,
θ
′
]
,
E
[
R
∣
s
t
+
1
,
a
2
,
θ
′
]
,
⋯
)
,
a
i
∈
A
\begin{aligned} \max_{a'\in A}Q_{\theta'}(s_{t+1},a')& =Q_{\theta^\prime}(s_{t+1},\arg\max_{a^\prime}Q_{\theta^\prime}(s_{t+1},a^\prime)) \\ &=\mathbb{E}[R|s_{t+1},\arg\max_{a^{\prime}}Q_{\theta^{\prime}}(s_{t+1},a^{\prime}),\theta^{\prime}] \\ &\geq\max(\mathbb{E}[R|s_{t+1},a_1,\theta^{\prime}],\mathbb{E}[R|s_{t+1},a_2,\theta^{\prime}],\cdots),a_i\in A \end{aligned}
a′∈AmaxQθ′(st+1,a′)=Qθ′(st+1,arga′maxQθ′(st+1,a′))=E[R∣st+1,arga′maxQθ′(st+1,a′),θ′]≥max(E[R∣st+1,a1,θ′],E[R∣st+1,a2,θ′],⋯),ai∈A
举个例子:
如图所示,x轴为状态,有10个候选行动;左列紫线是真实价值函数,绿点是训练数据点,绿线是拟合的价值函数。中间黑色虚线是取最大值后的估计。可以看到由于数据点的分布,使得拟合出的价值函数无法与真实的价值函数吻合,甚至在部分区域产生了明显的偏差。’
此外,Q函数的过高估计程度随着候选行动数量增大变得更严重.下图中𝑄′函数是另一组独立训练的价值函数。可以看到红色部分的误差明显大于蓝色。
Double DQN
为了解决这一问题,Double DQN 算法提出利用两个独立训练的神经网络估算 max a ′ Q ∗ ( s ′ , a ′ ) \max_{a^{\prime}}Q_{*}\left(s^{\prime},a^{\prime}\right) maxa′Q∗(s′,a′)。具体做法是将原有的 Q ω − ( s ′ , arg max a ′ Q ω − ( s ′ , a ′ ) ) Q_{\omega^-}\left(s',\arg\max_{a'}Q_{\omega^-}\left(s',a'\right)\right) Qω−(s′,argmaxa′Qω−(s′,a′))更改为 Q ω − ( s ′ , arg max a ′ Q ω ( s ′ , a ′ ) ) Q_{\omega^-}\left(s',\arg\max_{a'}Q_{\omega}\left(s',a'\right)\right) Qω−(s′,argmaxa′Qω(s′,a′)),即利用一套神经网络 Q ω Q_{\omega} Qω的输出选取价值最大的动作,但在使用该动作的价值时,用另一套神经网络 Q ω − Q_{\omega^-} Qω−计算该动作的价值。这样,即使其中一套神经网络的某个动作存在比较严重的过高估计问题,由于另一套神经网络的存在,这个动作最终使用的值不会存在很大的过高估计问题。
在传统的 DQN 算法中,本来就存在两套 Q Q Q函数的神经网络——目标网络和训练网络,只不过 Q ω − ( s ′ , arg max a ′ Q ω − ( s ′ , a ′ ) ) Q_{\omega^-}\left(s',\arg\max_{a'}Q_{\omega^-}\left(s',a'\right)\right) Qω−(s′,argmaxa′Qω−(s′,a′))的计算只用到了其中的目标网络,那么我们恰好可以直接将训练网络作为 Double DQN 算法中的第一套神经网络来选取动作,将目标网络作为第二套神经网络计算值,这便是 Double DQN 的主要思想。由于在 DQN 算法中将训练网络的参数记为 ω \omega ω,将目标网络的参数记为 ω − \omega^- ω−,这与本节中 Double DQN 的两套神经网络的参数是统一的,因此,我们可以直接写出如下 Double DQN 的优化目标: r + γ Q ω − ( s ′ , arg max a ′ Q ω ( s ′ , a ′ ) ) r+\gamma Q_{\omega^-}\left(s^{\prime},\arg\max_{a^{\prime}}Q_{\omega}\left(s^{\prime},a^{\prime}\right)\right) r+γQω−(s′,arga′maxQω(s′,a′))
Double DQN代码实践
Pendulum环境
Pendulum环境文档
本节采用的环境是倒立摆(Inverted Pendulum),该环境下有一个处于随机位置的倒立摆。环境的状态包括倒立摆角度的正弦值
s
i
n
θ
sin\theta
sinθ,余弦值
c
o
s
θ
cos\theta
cosθ,角速度
θ
˙
\dot\theta
θ˙;动作为对倒立摆施加的力矩
τ
\tau
τ.每一步都会根据当前倒立摆的状态的好坏给予智能体不同的奖励,该环境的奖励函数为
r
=
−
(
t
h
e
t
a
2
+
0.1
∗
t
h
e
t
a
d
t
2
+
0.001
∗
t
o
r
q
u
e
2
)
r = -(theta^2 + 0.1 * theta_dt^2 + 0.001 * torque^2)
r=−(theta2+0.1∗thetadt2+0.001∗torque2).倒立摆向上保持直立不动时奖励为 0,倒立摆在其他位置时奖励为负数(最大回报为零,倒立摆是垂直的,速度为零,没有施加扭矩)。环境本身没有终止状态,运行 200 步后游戏自动结束。
Pendulum环境的状态空间
标号 | 名称 | 最小值 | 最大值 |
---|---|---|---|
0 | x = cos(theta) | -1.0 | 1.0 |
1 | y = sin(theta) | -1.0 | 1.0 |
2 | Angular Velocity | -8.0 | 8.0 |
Pendulum环境的动作空间
标号 | 名称 | 最小值 | 最大值 |
---|---|---|---|
0 | 力矩 | -2.0 | 2.0 |
力矩大小是在 [ − 2 , 2 ] [-2,2] [−2,2]范围内的连续值。为了能够应用 DQN,我们需要采用离散化动作的技巧。例如,下面的代码将连续的动作空间离散为 11 个动作。动作 [ 0 , 1 , 2 , … , 9 , 10 ] [0,1,2,\dots,9,10] [0,1,2,…,9,10]分别代表力矩为 [ − 2 , − 1.6 , − 1.2 , … , 1.2 , 1.6 , 2.0 ] [-2,-1.6,-1.2,\dots,1.2,1.6,2.0] [−2,−1.6,−1.2,…,1.2,1.6,2.0]。
代码
PS:
torch.max()[0], 只返回最大值的每个数
troch.max()[1], 只返回最大值的每个索引
class DQN:
''' DQN算法 '''
def __init__(self, state_dim, hidden_dim, action_dim, learning_rate, gamma,
epsilon, target_update_rate, device, numOfEpisodes, env,
buffer_size, minimal_size, batch_size, DQNtype="DQN"):
self.action_dim = action_dim
# Q网络
self.q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device)
# 目标网络
self.target_q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device)
# 使用Adam优化器
self.optimizer = torch.optim.Adam(self.q_net.parameters(),
lr=learning_rate)
self.gamma = gamma
self.epsilon = epsilon
# 目标网络更新频率
self.target_update_rate = target_update_rate
# 计数器,记录更新次数
self.count = 0
self.device = device
self.numOfEpisodes = numOfEpisodes
self.env = env
self.buffer_size = buffer_size
self.minimal_size = minimal_size
self.batch_size = batch_size
self.DQNtype = DQNtype
# Choose A from S using policy derived from Q (e.g., epsilon-greedy)
def ChooseAction(self, state):
if np.random.random() < self.epsilon:
action = np.random.randint(self.action_dim)
else:
state = torch.tensor(np.array([state]), dtype=torch.float).to(self.device)
action = self.q_net(state).argmax().item()
return action
def Update(self, transition_dict):
states = torch.tensor(transition_dict['states'],
dtype=torch.float).to(self.device)
actions = torch.tensor(transition_dict['actions']).view(-1, 1).to(self.device)
rewards = torch.tensor(transition_dict['rewards'],
dtype=torch.float).view(-1, 1).to(self.device)
next_states = torch.tensor(transition_dict['next_states'],
dtype=torch.float).to(self.device)
terminateds = torch.tensor(transition_dict['terminateds'],
dtype=torch.float).view(-1, 1).to(self.device)
truncateds = torch.tensor(transition_dict['truncateds'],
dtype=torch.float).view(-1, 1).to(self.device)
#Q值
q_values = self.q_net(states).gather(1, actions)
# 下个状态的最大Q值
if self.DQNtype == "DoubleDQN":
max_action = self.q_net(next_states).max(1)[1].view(-1, 1)
max_next_q_values = self.target_q_net(next_states).gather(1, max_action)
else: # DQN的情况
max_next_q_values = self.target_q_net(next_states).max(1)[0].view(-1, 1)
# TD误差目标
q_targets = rewards + self.gamma * max_next_q_values * (1 - terminateds + truncateds)
# 均方误差损失函数
dqn_loss = torch.mean(F.mse_loss(q_values, q_targets))
# PyTorch中默认梯度会累积,这里需要显式将梯度置为0
self.optimizer.zero_grad()
# 反向传播更新参数
dqn_loss.backward()
self.optimizer.step()
if self.count % self.target_update_rate == 0:
self.target_q_net.load_state_dict(
self.q_net.state_dict()) # 更新目标网络
self.count += 1
def max_q_value(self, state):
state = torch.tensor(np.array([state]), dtype=torch.float).to(self.device)
return self.q_net(state).max().item()
# 离散动作转回连续的函数
def dis_to_con(self, discrete_action, env, action_dim):
action_lowbound = env.action_space.low[0]
actoin_upbound = env.action_space.high[0]
return action_lowbound + (discrete_action / (action_dim - 1)) \
* (actoin_upbound - action_lowbound)
def DQNtrain(self):
replay_buffer = util.ReplayBuffer(self.buffer_size)
returnList = []
max_q_valueList = []
max_q_value = 0
for i in range(10):
with tqdm(total=int(self.numOfEpisodes / 10), desc='Iteration %d' % i) as pbar:
for episode in range(int(self.numOfEpisodes / 10)):
# initialize state
state, info = self.env.reset()
terminated = False
truncated = False
episodeReward = 0
# Loop for each step of episode:
while (not terminated) or (not truncated):
action = self.ChooseAction(state)
# 平滑处理
max_q_value = self.max_q_value(state) * 0.005 + max_q_value * 0.995
max_q_valueList.append(max_q_value)
action_continuous = self.dis_to_con(action, self.env, self.action_dim)
next_state, reward, terminated, truncated, info = self.env.step([action_continuous])
replay_buffer.add(state, action, reward, next_state, terminated, truncated)
if terminated or truncated:
break
state = next_state
episodeReward += reward
# 当buffer数据的数量超过一定值后,才进行Q网络训练
if replay_buffer.size() > self.minimal_size:
b_s, b_a, b_r, b_ns, b_te, b_tr = replay_buffer.sample(self.batch_size)
transition_dict = {
'states': b_s,
'actions': b_a,
'next_states': b_ns,
'rewards': b_r,
'terminateds': b_te,
'truncateds': b_tr
}
self.Update(transition_dict)
returnList.append(episodeReward)
if (episode + 1) % 10 == 0: # 每10条序列打印一下这10条序列的平均回报
pbar.set_postfix({
'episode':
'%d' % (self.numOfEpisodes / 10 * i + episode + 1),
'return':
'%.3f' % np.mean(returnList[-10:])
})
pbar.update(1)
return returnList, max_q_valueList
结果
如图所示,DQN相比DoubleDQN能够获得更多的回报。
如图所示,DQN算法会产生过高估计(超越黄色虚线0),甚至部分Q值超过了10。而DoubleDQN算法则能有效缓解过高估计这一问题。
Dueling DQN
Dueling DQN 是 DQN 另一种的改进算法,它在传统 DQN 的基础上只进行了微小的改动,但却能大幅提升 DQN 的表现。在强化学习中,我们将状态动作价值函数
Q
Q
Q减去状态价值函数
V
V
V的结果定义为优势函数
A
A
A,即
A
(
s
,
a
)
=
Q
(
s
,
a
)
−
V
(
s
)
A(s,a)=Q(s,a)-V(s)
A(s,a)=Q(s,a)−V(s).在同一个状态下,所有动作的优势值之和为 0,因为所有动作的动作价值的期望就是这个状态的状态价值。据此,在 Dueling DQN 中,Q 网络被建模为:
Q
η
,
α
,
β
(
s
,
a
)
=
V
η
,
α
(
s
)
+
A
η
,
β
(
s
,
a
)
Q_{\eta,\alpha,\beta}(s,a)=V_{\eta,\alpha}(s)+A_{\eta,\beta}(s,a)
Qη,α,β(s,a)=Vη,α(s)+Aη,β(s,a)
其中,
V
η
,
α
(
s
)
V_{\eta,\alpha}(s)
Vη,α(s)为状态价值函数,而
A
η
,
β
(
s
,
a
)
A_{\eta,\beta}(s,a)
Aη,β(s,a)则为该状态下采取不同动作的优势函数,表示采取不同动作的差异性;
η
\eta
η是状态价值函数和优势函数共享的网络参数,一般用在神经网络中,用来提取特征的前几层;而
α
,
β
\alpha,\beta
α,β分别为状态价值函数和优势函数的参数。
不同的Advantage聚合形式:
- Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) − max a ′ A η , β ( s , a ′ ) Q_{\eta,\alpha,\beta}(s,a)=V_{\eta,\alpha}(s)+A_{\eta,\beta}(s,a)-\max_{a'}A_{\eta,\beta}\left(s,a'\right) Qη,α,β(s,a)=Vη,α(s)+Aη,β(s,a)−a′maxAη,β(s,a′)
- Q η , α , β ( s , a ) = V η , α ( s ) + A η , β ( s , a ) − 1 ∣ A ∣ ∑ a ′ A η , β ( s , a ′ ) Q_{\eta,\alpha,\beta}(s,a)=V_{\eta,\alpha}(s)+A_{\eta,\beta}(s,a)-\frac{1}{|\mathcal{A}|}\sum_{a'}A_{\eta,\beta}\left(s,a'\right) Qη,α,β(s,a)=Vη,α(s)+Aη,β(s,a)−∣A∣1a′∑Aη,β(s,a′)
Dueling DQN网络结构
Dueling DQN优点
- 能够处理与动作关联较小的状态
- Dueling DQN 能更高效学习状态价值函数。每一次更新时, V V V函数都会被更新,这也会影响到其他动作的 Q Q Q值。而传统的 DQN 只会更新某个动作的 Q Q Q值,其他动作的 Q Q Q值就不会更新。因此,Dueling DQN 能够更加频繁、准确地学习状态价值函数。
Dueling DQN 代码实践
class VAnet(torch.nn.Module):
''' 只有一层隐藏层的A网络和V网络 '''
def __init__(self, state_dim, hidden_dim, action_dim):
super(VAnet, self).__init__()
# 共享网络部分
self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
self.fc_A = torch.nn.Linear(hidden_dim, action_dim)
self.fc_V = torch.nn.Linear(hidden_dim, 1)
def forward(self, x):
A = self.fc_A(F.relu(self.fc1(x)))
V = self.fc_V(F.relu((self.fc1(x))))
Q = A + V - A.mean(1).view(-1, 1)
return Q
class DQN:
''' DQN算法 '''
def __init__(self, state_dim, hidden_dim, action_dim, learning_rate, gamma,
epsilon, target_update_rate, device, numOfEpisodes, env,
buffer_size, minimal_size, batch_size, DQNtype="DQN"):
self.action_dim = action_dim
if DQNtype == "DuelingDQN":
self.q_net = VAnet(state_dim, hidden_dim, self.action_dim).to(device)
self.target_q_net = VAnet(state_dim, hidden_dim, self.action_dim).to(device)
else:
# Q网络
self.q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device)
# 目标网络
self.target_q_net = Qnet(state_dim, hidden_dim, self.action_dim).to(device)
# 使用Adam优化器
self.optimizer = torch.optim.Adam(self.q_net.parameters(),
lr=learning_rate)
self.gamma = gamma
self.epsilon = epsilon
# 目标网络更新频率
self.target_update_rate = target_update_rate
# 计数器,记录更新次数
self.count = 0
self.device = device
self.numOfEpisodes = numOfEpisodes
self.env = env
self.buffer_size = buffer_size
self.minimal_size = minimal_size
self.batch_size = batch_size
self.DQNtype = DQNtype
结果
同样,DQN相比Dueling DQN能够获得更多的回报。
Dueling DQN可以缓解过高估计这一问题。
参考
[1] 伯禹AI
[2] https://www.davidsilver.uk/teaching/
[3] 动手学强化学习
[4] Reinforcement Learning