创建时间: 2024年11月4日 21:05

作者: 蜡笔大新
笔记类别: 强化学习
标签: Deep Reinforcement Learning, Policy Optimization, Q-Learning
状态: 完成

简介

Double DQN是深度强化学习中的一种改进算法,它解决了传统DQN(深度Q网络)中的过估计问题。传统DQN在某些情况下会高估Q值,导致学习效果不佳。Double DQN通过引入两个网络来缓解这个问题。

原理

  • 使用两个结构相同但参数不同的神经网络:当前网络和目标网络。
  • 当前网络用于选择动作,目标网络用于评估动作的价值。
  • 这种分离减少了过度乐观估计的可能性。

步骤

  1. 使用当前网络,输入当前状态 $s_t$ 根据贪婪策略获得动作 $a_t$ 和对应的Q值 $Q_1$
  2. 当前状态 $s_t$ 根据选择的动作 $a_t$ 到达下一个状态 $s_{t+1}$
  3. 将下一个状态 $s_{t+1}$ 输入当前网络,选择最大Q值的动作 $a'$
  4. 将下一个状态 $s_{t+1}$ 输入目标网络,获得动作 $a'$ 对应的Q值 $Q_2$
  5. 将 $Q_1$ 和 $Q_2$ 计算TD-Error,通过梯度下降法更新当前网络
  6. 一段时间后,目标网络慢慢向当前网络靠拢

区别

在普通DQN中,当前网络目标网络更像是两种并行的网络,目标网络只是当前网络参数较为稳定的版本,用于减缓目标漂移;而在Double DQN中,它们更像是互相合作的队友当前网络负责提供动作,目标网络负责评估这个动作的质量,并反馈给当前网络。

当前网络

  1. 当前网络和普通DQN一样生成当前状态的不同动作的Q值,通过贪婪策略选择动作(大概率Q值最大,小概率随机探索)。
  2. 当前网络比普通DQN多了一步,将下一个状态输入进去,得到包含所有动作的Q值向量,选取下一个状态Q值最大的最佳动作。

目标网络

  1. 它不再直接通过贝尔曼公式计算当前状态的最大Q值,而是对当前网络选择的动作进行评估。
  2. 具体而言,将下一状态输入目标网络,使用目标网络生成的Q值中,与当前网络选择的动作对应的Q值作为  $Q{\prime}$ (目标Q值)。
  3. 通过计算TD误差(TD-Error)评估模型的质量,优化目标是最小化TD误差,并通过反向传播更新当前网络的参数。

优点

Double DQN的优点主要体现在以下几个方面:

  • 更准确的Q值估计:通过分离动作选择和评估,有效减少了Q值的过估计问题。
  • 提高学习稳定性:两个网络的协作机制使得学习过程更加稳定,减少了价值估计的波动。
  • 更好的泛化能力:由于估计更准确,Double DQN在面对新的或未见过的状态时表现更好。
  • 更快的收敛速度:相比传统DQN,Double DQN通常能更快地达到最优策略。
  • 在复杂环境中表现出色:特别是在状态空间较大、动作选择复杂的环境中,Double DQN往往能取得更好的效果。

代码

环境

环境是倒立摆(Inverted Pendulum),在gym中对应的版本是Pendulum-v1。力矩大小是在[-2,2]范围内的连续值。由于DQN只能处理离散动作环境,因此我们无法直接用 DQN 来处理倒立摆环境,但倒立摆环境可以比较方便地验证 DQN 对Q值的过高估计:倒立摆环境下 Q值的最大估计应为0(倒立摆向上保持直立时能选取的最大值),Q值出现大于0的情况则说明出现了过高估计。为了能够应用DQN,我们采用离散化动作的技巧。例如,下面的代码将连续的动作空间离散为11个动作。动作 [0,1,2,...,9,10]分别代表力矩为[-2,-1.6, 1.2,..., 1.2, 1.6, 2]。

Double_DQN与DQN区别不大,主要改变的地方在于:

if self.dqn_type == 'DoubleDQN':  # DQN与Double DQN的区别
    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)

总代码:

import random
import gym
import numpy as np
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
import rl_utils
from tqdm import tqdm

class Qnet(torch.nn.Module):
    """ 只有一层隐藏层的Q网络 """

    def __init__(self, state_dim, hidden_dim, action_dim):
        super(Qnet, self).__init__()
        self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
        self.fc2 = torch.nn.Linear(hidden_dim, action_dim)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        return self.fc2(x)

class DQN:
    """ DQN算法,包括Double DQN """

    def __init__(self, state_dim, hidden_dim, action_dim, learning_rate, gamma, epsilon, target_update, device,
                 dqn_type='VanillaDQN'):
        self.action_dim = action_dim
        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)
        self.optimizer = torch.optim.Adam(self.q_net.parameters(), lr=learning_rate)
        self.gamma = gamma
        self.epsilon = epsilon
        self.target_update = target_update
        self.count = 0
        self.dqn_type = dqn_type
        self.device = device

    def take_action(self, state):
        if np.random.random() < self.epsilon:
            action = np.random.randint(self.action_dim)
        else:
            state = torch.tensor([state], dtype=torch.float).to(self.device)
            action = self.q_net(state).argmax().item()
        return action

    def max_q_value(self, state):
        state = torch.tensor([state], dtype=torch.float).to(self.device)
        return self.q_net(state).max().item()

    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)
        dones = torch.tensor(transition_dict['dones'], dtype=torch.float).view(-1, 1).to(self.device)

        q_values = self.q_net(states).gather(1, actions)  # Q值
        # 下个状态的最大Q值
        if self.dqn_type == 'DoubleDQN':  # DQN与Double DQN的区别
            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)
        q_targets = rewards + self.gamma * max_next_q_values * (1 - dones)  # TD误差目标
        dqn_loss = torch.mean(F.mse_loss(q_values, q_targets))  # 均方误差损失函数
        self.optimizer.zero_grad()  # PyTorch中默认梯度会累积,这里需要显式将梯度置为0
        dqn_loss.backward()  # 反向传播更新参数
        self.optimizer.step()

        if self.count % self.target_update == 0:
            self.target_q_net.load_state_dict(self.q_net.state_dict())  # 更新目标网络
        self.count += 1

lr = 1e-2
num_episodes = 200
hidden_dim = 128
gamma = 0.98
epsilon = 0.01
target_update = 50
buffer_size = 5000
minimal_size = 1000
batch_size = 64
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")

env_name = 'Pendulum-v1'
env = gym.make(env_name)
state_dim = env.observation_space.shape[0]
action_dim = 11  # 将连续动作分成11个离散动作

def dis_to_con(discrete_action, env, action_dim):  # 离散动作转回连续的函数
    action_lowbound = env.action_space.low[0]  # 连续动作的最小值
    action_upbound = env.action_space.high[0]  # 连续动作的最大值
    return action_lowbound + (discrete_action / (action_dim - 1)) * (action_upbound - action_lowbound)

def train_DQN(agent, env, num_episodes, replay_buffer, minimal_size, batch_size):
    return_list = []
    max_q_value_list = []
    max_q_value = 0
    for i in range(10):
        with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
            for i_episode in range(int(num_episodes / 10)):
                episode_return = 0
                state = env.reset()
                done = False
                while not done:
                    action = agent.take_action(state)
                    max_q_value = agent.max_q_value(state) * 0.005 + max_q_value * 0.995  # 平滑处理
                    max_q_value_list.append(max_q_value)  # 保存每个状态的最大Q值
                    action_continuous = dis_to_con(action, env, agent.action_dim)
                    next_state, reward, done, _ = env.step([action_continuous])
                    replay_buffer.add(state, action, reward, next_state, done)
                    state = next_state
                    episode_return += reward
                    if replay_buffer.size() > minimal_size:
                        b_s, b_a, b_r, b_ns, b_d = replay_buffer.sample(batch_size)
                        transition_dict = {
                            'states': b_s,
                            'actions': b_a,
                            'next_states': b_ns,
                            'rewards': b_r,
                            'dones': b_d
                        }
                        agent.update(transition_dict)
                return_list.append(episode_return)
                if (i_episode + 1) % 10 == 0:
                    pbar.set_postfix({
                        'episode': '%d' % (num_episodes / 10 * i + i_episode + 1),
                        'return': '%.3f' % np.mean(return_list[-10:])
                    })
                pbar.update(1)
    return return_list, max_q_value_list

random.seed(0)
np.random.seed(0)
env.seed(0)
torch.manual_seed(0)
replay_buffer = rl_utils.ReplayBuffer(buffer_size)
agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon, target_update, device)
return_list, max_q_value_list = train_DQN(agent, env, num_episodes, replay_buffer, minimal_size, batch_size)

episodes_list = list(range(len(return_list)))
mv_return = rl_utils.moving_average(return_list, 5)
plt.plot(episodes_list, mv_return)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('DQN on {}'.format(env_name))
plt.show()

frames_list = list(range(len(max_q_value_list)))
plt.plot(frames_list, max_q_value_list)
plt.axhline(0, c='orange', ls='--')
plt.axhline(10, c='red', ls='--')
plt.xlabel('Frames')
plt.ylabel('Q value')
plt.title('DQN on {}'.format(env_name))
plt.show()

random.seed(0)
np.random.seed(0)
env.seed(0)
torch.manual_seed(0)
replay_buffer = rl_utils.ReplayBuffer(buffer_size)
agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon, target_update, device, 'DoubleDQN')
return_list, max_q_value_list = train_DQN(agent, env, num_episodes, replay_buffer, minimal_size, batch_size)

episodes_list = list(range(len(return_list)))
mv_return = rl_utils.moving_average(return_list, 5)
plt.plot(episodes_list, mv_return)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('Double DQN on {}'.format(env_name))
plt.show()

frames_list = list(range(len(max_q_value_list)))
plt.plot(frames_list, max_q_value_list)
plt.axhline(0, c='orange', ls='--')
plt.axhline(10, c='red', ls='--')
plt.xlabel('Frames')
plt.ylabel('Q value')
plt.title('Double DQN on {}'.format(env_name))
plt.show()

100轮结果对比

总体奖励回报

预估Q值

总体奖励回报上,DQN和Double DQN都在训练中提升了表现,总体奖励相差不大。而在预估Q值时,DQN明显表现出了过度估计Q值(理论上不会出现正的Q值),Double DQN能够更准确的估计Q值。

1000轮结果对比

总体奖励回报

预估Q值

实验结果显示,在这个游戏中,两种网络获得的奖励基本持平,但DQN表现更稳定。在Q值预估方面,虽然两种网络都出现了大于0的情况,DQN明显更频繁地过高估计Q值,而Double DQN在一定程度上缓解了这个问题。值得注意的是,为了使两种DQN能够运行,原本的连续动作被改为离散动作,这可能对实验结果产生了一定影响。

最后修改:2024 年 11 月 25 日
如果觉得我的文章对你有用,请随意赞赏