6.4 DQN的优化与改进
Deep Q-Network (DQN) 是一种强化学习算法,最初由DeepMind提出,并已经在各种领域获得了成功。然而,DQN 在实际应用中仍然面临一些挑战,因此有许多优化和改进的技术,以增强其性能和稳定性。
6.4.1 Double DQN
Double DQN(Double Deep Q-Network)是对标准的深度 Q 学习(DQN)算法的一种改进,旨在解决 Q 值高估问题。在标准 DQN 中,用于选择最佳动作的 Q-network 可能会高估 Q 值,导致不稳定的训练和低效的策略。Double DQN 引入了一种机制,通过使用两个独立的 Q-network 来减小 Q 值的高估。Double DQN 的工作原理如下:
- 两个 Q-networks:Double DQN 使用两个神经网络,通常称为 Q1-network 和 Q2-network。这两个网络具有相同的架构,但参数是独立的。
- 动作选择和 Q 值估计:在每个训练步骤中,Double DQN 使用 Q1-network 来选择最佳动作(具有最高估计的 Q 值),然后使用 Q2-network 来估计所选动作的 Q 值。
- 目标 Q 值计算:与标准 DQN 不同,Double DQN 在计算目标 Q 值时使用了 Q1-network。具体来说,对于下一个状态,它使用 Q1-network 来选择最佳动作,然后使用 Q2-network 来估计该动作的 Q 值。这个目标 Q 值用于更新 Q1-network。
- 目标 Q 值更新:Double DQN 使用目标 Q 值计算来更新 Q1-network 的权重,而不是使用 Q1-network 自身的估计。
Double DQN的主要优点是它减小了 Q 值高估的影响,提高了训练的稳定性。这有助于在强化学习任务中更快地获得更好的策略。例如下面是一个创建Double DQN网络的例子。
实例6-6:创建Double DQN网络(源码路径:daima\6\dou.py)
实例文件dou.py的主要实现代码如下所示。
import torch
import torch.nn as nn
import torch.optim as optim
import random
class DoubleDQN(nn.Module):
def __init__(self, input_dim, output_dim):
super(DoubleDQN, self).__init__()
self.fc1 = nn.Linear(input_dim, 64)
self.fc2 = nn.Linear(64, 64)
self.fc3 = nn.Linear(64, output_dim)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x
# 创建两个 Q-networks
input_dim = 4 # 输入状态的维度
output_dim = 2 # 输出动作的维度
q1_network = DoubleDQN(input_dim, output_dim)
q2_network = DoubleDQN(input_dim, output_dim)
# 定义优化器
optimizer_q1 = optim.Adam(q1_network.parameters(), lr=0.001)
optimizer_q2 = optim.Adam(q2_network.parameters(), lr=0.001)
6.4.2 Dueling DQN
Dueling DQN 是一种改进的深度 Q 网络(DQN)算法,旨在更好地估计状态-动作值函数 Q(s, a)。与标准的 DQN 不同,Dueling DQN 将 Q(s, a) 分解为状态值函数 V(s) 和优势函数 A(s, a),这有助于更好地理解和估计状态-动作值函数。例如下面是一个 Dueling DQN 的例子,演示了使用自定义的迷宫环境来演示Dueling DQN的过程。
实例6-7:使用Dueling DQN解决自定义迷宫导航问题(源码路径:daima\6\Dueling.py)
实例文件Dueling.py的主要实现代码如下所示。
# 创建一个简单的自定义迷宫环境
class CustomMaze:
def __init__(self):
self.grid = np.array([
[0, 0, 0, 1, 0],
[0, 1, 0, 1, 0],
[0, 1, 0, 0, 0],
[0, 1, 0, 1, 0],
[0, 0, 0, 0, 0]
])
self.start = (0, 0)
self.goal = (4, 4)
self.current_pos = self.start
def reset(self):
self.current_pos = self.start
return self.current_pos
def step(self, action):
if action == 0: # 上
new_pos = (self.current_pos[0] - 1, self.current_pos[1])
elif action == 1: # 下
new_pos = (self.current_pos[0] + 1, self.current_pos[1])
elif action == 2: # 左
new_pos = (self.current_pos[0], self.current_pos[1] - 1)
elif action == 3: # 右
new_pos = (self.current_pos[0], self.current_pos[1] + 1)
if 0 <= new_pos[0] < 5 and 0 <= new_pos[1] < 5 and self.grid[new_pos[0], new_pos[1]] != 1:
self.current_pos = new_pos
if self.current_pos == self.goal:
done = True
reward = 1.0
else:
done = False
reward = 0.0
return self.current_pos, reward, done
# 定义Dueling DQN模型
class DuelingDQN(tf.keras.Model):
def __init__(self, num_actions):
super(DuelingDQN, self).__init__()
self.dense1 = tf.keras.layers.Dense(128, activation='relu')
self.advantage = tf.keras.layers.Dense(num_actions, activation='linear')
self.value = tf.keras.layers.Dense(1, activation='linear')
def call(self, state):
x = self.dense1(state)
advantage = self.advantage(x)
value = self.value(x)
q_values = value + (advantage - tf.reduce_mean(advantage, axis=1, keepdims=True))
return q_values
# 定义经验回放缓冲区
class ReplayBuffer:
def __init__(self, capacity):
self.capacity = capacity
self.buffer = []
self.position = 0
def push(self, state, action, reward, next_state, done):
if len(self.buffer) < self.capacity:
self.buffer.append(None)
self.buffer[self.position] = (state, action, reward, next_state, done)
self.position = (self.position + 1) % self.capacity
def sample(self, batch_size):
batch = random.sample(self.buffer, batch_size)
state, action, reward, next_state, done = map(np.array, zip(*batch))
return state, action, reward, next_state, done
# 定义epsilon-greedy策略
def epsilon_greedy_policy(q_values, epsilon):
if np.random.rand() < epsilon:
return np.random.randint(len(q_values))
else:
return np.argmax(q_values)
# 定义Dueling DQN代理
class DuelingDQNAgent:
def __init__(self, num_actions, epsilon=0.1, gamma=0.99, lr=0.001, batch_size=64, buffer_capacity=10000):
self.num_actions = num_actions
self.epsilon = epsilon
self.gamma = gamma
self.batch_size = batch_size
self.buffer = ReplayBuffer(buffer_capacity)
self.q_network = DuelingDQN(num_actions)
self.target_network = DuelingDQN(num_actions)
self.optimizer = tf.optimizers.Adam(learning_rate=lr)
self.update_target_network()
def update_target_network(self):
self.target_network.set_weights(self.q_network.get_weights())
def train(self):
if len(self.buffer.buffer) < self.batch_size:
return
state, action, reward, next_state, done = self.buffer.sample(self.batch_size)
with tf.GradientTape() as tape:
q_values = self.q_network(state)
target_q_values = self.target_network(next_state)
target_max_q_values = tf.reduce_max(target_q_values, axis=1)
target_q = reward + (1.0 - done) * self.gamma * tf.expand_dims(target_max_q_values, axis=1)
action_masks = tf.one_hot(action, self.num_actions)
predicted_q_values = tf.reduce_sum(q_values * action_masks, axis=1)
loss = tf.reduce_mean(tf.square(target_q - predicted_q_values))
gradients = tape.gradient(loss, self.q_network.trainable_variables)
self.optimizer.apply_gradients(zip(gradients, self.q_network.trainable_variables))
def act(self, state):
q_values = self.q_network(state)
return epsilon_greedy_policy(q_values[0], self.epsilon)
# 训练Dueling DQN代理解决自定义迷宫导航问题
if __name__ == "__main__":
env = CustomMaze()
num_actions = 4 # 上、下、左、右四个动作
agent = DuelingDQNAgent(num_actions)
num_episodes = 1000
for episode in range(num_episodes):
state = env.reset()
state = np.array([state])
total_reward = 0.0
while True:
action = agent.act(state)
next_state, reward, done = env.step(action)
next_state = np.array([next_state])
agent.buffer.push(state, action, reward, next_state, done)
state = next_state
total_reward += reward
if done:
break
agent.train()
agent.update_target_network()
print(f"Episode: {episode + 1}, Total Reward: {total_reward}")
上述实现了一个强化学习任务,使用Dueling DQN算法来训练一个代理(agent)解决一个自定义的迷宫导航问题。具体实现流程如下所示:
- 类CustomMaze定义了一个自定义的迷宫环境。这个迷宫由一个二维网格表示,其中0表示可以通过的路径,1表示障碍物。代理的目标是从起始位置出发,到达目标位置(终点)。reset 方法用于重置环境状态,step 方法用于执行代理的动作并返回下一个状态、奖励和终止条件。
- 类DuelingDQN定义了Dueling DQN模型,它包括一个全连接层(dense1),一个表示优势函数(advantage)的全连接层,以及一个表示值函数(value)的全连接层。call 方法根据输入状态计算Q值,通过将优势函数和值函数的输出进行组合来获得最终的Q值。
- 类ReplayBuffer实现了经验回放缓冲区,用于存储代理的经验样本,以便后续训练。push 方法用于添加经验样本,sample 方法用于随机抽样小批量样本。
- epsilon_greedy_policy 是一个策略函数,根据Q值和epsilon值决定代理的行动。它以一定的概率随机选择动作,以便在探索和利用之间进行权衡。
- 类DuelingDQNAgent 定义了Dueling DQN代理。它包括一个Q网络(q_network)和一个目标网络(target_network),以及一些超参数。train 方法用于训练代理,包括计算损失、更新Q网络权重和目标网络同步。act 方法用于根据当前状态选择动作。
- 在if __name__ == "__main__": 中,创建了自定义迷宫环境,实例化了Dueling DQN代理,并对其进行了多个Episode的训练。在每个Episode中,代理根据当前状态选择动作,执行动作,收集经验,计算损失并进行训练。训练过程中,代理的目标是学习如何在迷宫中导航以最大化总奖励。最后,代码打印了每个Episode的总奖励,以跟踪代理的性能改善。这个训练过程会一直重复,直到达到指定的Episode数。
运行上述代码后会进行多次迭代的训练,每次迭代都会打印当前迭代的信息,包括当前的Episode号和该Episode的总奖励。会输出类似以下内容的信息:
Episode: 1, Total Reward: 0.0
Episode: 2, Total Reward: 0.0
Episode: 3, Total Reward: 0.0
...
Episode: 999, Total Reward: 1.0
Episode: 1000, Total Reward: 1.0
在每个Episode中,代理会尝试在自定义迷宫中导航,迭代会继续进行直到达到目标或遇到终止条件。总奖励是代理在该Episode中获得的奖励总和。在这个例子中,当代理成功到达目标时,它会获得1.0的奖励。我们可以根据输出的信息来监视训练的进展,以及代理在解决自定义迷宫导航问题上的性能如何改善。
6.4.3 Prioritized Experience Replay
Prioritized Experience Replay(PER)是一种经验回放机制,用于改善深度强化学习中的样本选择和训练效率。传统的经验回放方法随机选择样本进行训练,但这可能导致一些重要的经验被低概率选择,从而影响训练效果。PER通过引入优先级来解决这个问题,它将重要的经验样本更频繁地用于训练,从而提高学习的效率。Prioritized Experience Replay的关键知识点如下:
- 优先级分配:每个经验样本都被分配一个优先级,通常根据代理的预测误差(TD误差)来确定优先级。更大的误差通常表示更重要的样本,因此具有更高的优先级。
- 样本选择:在进行经验回放时,根据样本的优先级来选择样本。具有较高优先级的样本被选择的概率更高,以确保重要经验被更频繁地使用。
- 重要性采样权重:为了保持样本选择的公平性,引入了重要性采样权重,以纠正样本选择的偏差。这些权重确保代理学习不受到优先级的影响。
- 优先级更新:当代理训练后,根据预测误差的变化来更新经验样本的优先级。更重要的样本会得到更高的优先级,以适应学习进展。
- 经验存储:PER通常使用一种数据结构(通常是二叉树)来存储经验样本和对应的优先级。这使得样本选择和优先级更新的效率更高。
PER的主要优势在于它能够提高样本的有效利用率,加速学习的收敛速度,并提高深度强化学习算法的性能。然而,PER也引入了一些挑战,如需要额外的超参数调整和复杂性增加。
在实现PER时,通常需要考虑如何平衡优先级采样的偏差和方差,以及如何合理地设置优先级的初始化值。PER是许多强化学习算法中的重要改进之一,例如在DQN(深度Q网络)和其变种中广泛使用。下面是一个简单的Prioritized Experience Replay(PER)的示例,不涉及任何特定的环境,只是演示了PER的基本概念。
实例6-7:创建一个简单的PrioritizedReplayBuffer(源码路径:daima\6\pe.py)
实例文件pe.py的主要实现代码如下所示。
import random
import numpy as np
# 定义经验回放缓冲区类
class PrioritizedReplayBuffer:
def __init__(self, capacity):
self.capacity = capacity
self.buffer = []
self.priorities = np.zeros(capacity, dtype=np.float32)
self.position = 0
def push(self, experience, priority):
if len(self.buffer) < self.capacity:
self.buffer.append(None)
self.priorities[self.position] = priority
self.buffer[self.position] = experience
self.position = (self.position + 1) % self.capacity
def sample(self, batch_size):
priorities = self.priorities[:len(self.buffer)]
prob = priorities / priorities.sum()
indices = np.random.choice(len(self.buffer), batch_size, p=prob)
samples = [self.buffer[idx] for idx in indices]
return samples, indices
# 创建一个简单的经验元组类
class Experience:
def __init__(self, state, action, reward, next_state, done):
self.state = state
self.action = action
self.reward = reward
self.next_state = next_state
self.done = done
# 创建一个PrioritizedReplayBuffer对象
buffer_capacity = 10000
buffer = PrioritizedReplayBuffer(buffer_capacity)
# 添加一些虚拟经验元组到缓冲区中
for i in range(100):
state = np.random.rand(4) # 代表状态的简化示例
action = np.random.randint(2) # 0或1
reward = np.random.rand() # 随机奖励
next_state = np.random.rand(4) # 代表下一个状态的简化示例
done = False # 这里示例中没有终止条件
experience = Experience(state, action, reward, next_state, done)
buffer.push(experience, priority=1.0)
# 从缓冲区中采样一批经验元组
batch_size = 64
samples, indices = buffer.sample(batch_size)
# 打印采样的经验元组
for sample in samples:
print(f"State: {sample.state}, Action: {sample.action}, Reward: {sample.reward}, Next State: {sample.next_state}, Done: {sample.done}")
# 打印被选中的样本索引
print("Selected Indices:", indices)
上述代码创建了一个简单的PrioritizedReplayBuffer,然后向其中添加了虚拟的经验元组。然后,它从缓冲区中采样一批经验元组,并打印出采样的内容以及被选中的样本索引。执行后会输出:
State: [0.06553838 0.79793838 0.83191107 0.20735608], Action: 1, Reward: 0.5546894149936815, Next State: [0.05097349 0.33601156 0.11562567 0.85548575], Done: False
State: [0.36528259 0.6671806 0.08346176 0.54937993], Action: 0, Reward: 0.5950038059630215, Next State: [0.16240237 0.80662676 0.62011377 0.98715585], Done: False
State: [0.33058664 0.57697594 0.00883182 0.37177207], Action: 0, Reward: 0.9942192797183348, Next State: [0.4768401 0.75973517 0.49737887 0.58094162], Done: False
######省略部分输出
State: [0.05007888 0.71787515 0.81902615 0.24681485], Action: 1, Reward: 0.2851874124135627, Next State: [0.6792876 0.00236248 0.0588185 0.12687168], Done: False
State: [0.91436844 0.18640032 0.44182336 0.6882996 ], Action: 0, Reward: 0.15472129635299292, Next State: [0.41523829 0.49050478 0.88478906 0.4958476 ], Done: False
Selected Indices: [20 19 94 15 46 91 98 4 71 74 56 35 79 34 18 25 1 55 95 0 56 55 95 16
66 47 8 32 62 98 61 95 14 5 23 87 3 4 23 48 91 81 8 16 41 14 44 66
51 0 33 4 9 11 49 37 74 59 71 15 38 91 48 22]
由此可见,已经成功运行了这个简单的Prioritized Experience Replay(PER)示例,并从缓冲区中采样了一批经验元组。输出显示了采样的经验元组,以及被选中的样本索引。