思考

我们先回顾一下Q-learning算法 

 我们上次末尾说001-005都是离散环境下的结果,但是在连续空间中怎么使用。观察Q-learning算法,其主要原理就是维护了一个Q_table表格,用来存储动作状态价值,连续条件下可以用函数拟合Q-(其实DQN只适合离散动作,连续状态)代替表格。这就是后面的DQN(deep-Q-Net)的主要思想

环境搭建--cartpole

智能体的任务是通过左右移动保持车上的杆竖直。结束条件满足以下之一:

  1. 杆的倾斜角度过大
  2. 车子里初始位置左右的偏离过大
  3. 坚持时间到达200贞

状态空间和动作空间给出来,

状态空间(s)--每一维连续

维度 意义 最小值 最大值
0 车的位置 -2.4 2.4
1 车的速度 不限 不限
2 杆的角度 -41.8 41.8
3 杆尖端的速度 不限 不限

动作空间(a)--离散

标号 动作
0 左移
1 右移

DQN

Q网络

用神经网络用来拟合函数 Q  (也即是Q_table表格),那么该函数的输入是 s ,a ,输出应该是标量Q: 表示状态s下采取动作a 的价值 。拟合函数 Q 时使用的参数是 w,那么每一个状态s 下所有可能动作 a 的Q 值表示为 Q_{w}(s,a).-------这里区分 w和 \pi的区别,没有什么关系,w是拟合Q的一个参数(从这里可以看出左边状态是连续的,动作是离散的,动作连续后续会讲)

当前网络-只有输入没有输出

 有网络就有损失函数,怎么计算,我们容易想到Q-learning 的更新规则

Q(s, a) \leftarrow Q(s, a)+\alpha\left[r+\gamma \max _{a^{\prime} \in \mathcal{A}} Q\left(s^{\prime}, a^{\prime}\right)-Q(s, a)\right]

在Q-learning 算法中(004)我们知道r_{t}+\gamma V\left(s_{t+1}\right)-V\left(s_{t}\right)-------------r+\gamma \max _{a^{\prime} \in \mathcal{A}} Q\left(s^{\prime}, a^{\prime}\right)-Q(s, a)是时序差分误差,换一个角度想,什么时候Q达到了最优,也就是说时序误差为0的时候一样,也就说Q(s, a)r+\gamma \max _{a^{\prime} \in \mathcal{A}} Q\left(s^{\prime}, a^{\prime}\right)靠近就行了

所以Q网络的损失函数:

$\omega^*=\arg\min_\omega\frac{1}{2N}\sum_{i=1}^N\left[Q_\omega\left(s_i,a_i\right)-\left(r_i+\gamma\max_{a^{\prime}}Q_\omega\left(s_i^{\prime},a^{\prime}\right)\right)\right]^2$

(1/2为了求导方便)

怎么收集数据呢:和Q-learning 一样使用不完全贪婪策略来平衡和利用,将收集到的数据存储起来后续使用。

同时DQN还有两个重要的模块:经验回放和目标网络

经验回放

也就是将每次使用不完全策略与环境互动后得到的四元组数据(s,a,s',r),存储起来,放到回放缓冲区里,训练Q的时候再从中采样若干数据来进行训练。

举例:游戏超级马里奥里,训练数据的时候第一关和第三关的动作时状是随时间变化的一个数据(上一个时刻决定写一个时刻),也就是说不满足独立同分布,但是如果训练的时候你在回头去采集已经使用过的数据(多次使用数据),这些数据就不是随时间变化的了,就满足独立同分布了。————非独立同分布的数据会使神经网络拟合到最近的训练数据上

目标网络

我们再来看一下损失函数

$\omega^*=\arg\min_\omega\frac{1}{2N}\sum_{i=1}^N\left[Q_\omega\left(s_i,a_i\right)-\left(r_i+\gamma\max_{a^{\prime}}Q_\omega\left(s_i^{\prime},a^{\prime}\right)\right)\right]^2$

我们可以发现Q_\omega\left(s_i,a_i\right)\left(r_i+\gamma\max_{a^{\prime}}Q_\omega\left(s_i^{\prime},a^{\prime}\right)\right)中的Q_\omega\left(s_i^{\prime},a^{\prime}\right)都是神经网络输出的结果,我们会发现神经网络输出Q_\omega\left(s_i,a_i\right)的参数w^{1}Q_\omega\left(s_i^{\prime},a^{\prime}\right)的参数w^{2}是不一样的,也就说神经网络的更新会导致损失函数的波动,解决办法,固定住Q,引入新的Q

具体方案:

  1. Q_\omega\left(s_i,a_i\right)计算使用原来的训练网络
  2. \left(r_i+\gamma\max_{a^{\prime}}Q_\omega\left(s_i^{\prime},a^{\prime}\right)\right)中的Q_{w^{-}}\left(s_i^{\prime},a^{\prime}\right)的计算使用引入的新网络目标网络来更新,目标网络使用训练网络一套比较旧得参数,每镉C步与训练网络同步一次

伪代码

  • 用随机得网络参数w初始化网络Q_\omega\left(s_i,a_i\right)
  • 复制相同的参数w^{-}\leftarrow w来初始化目标网络Q_{w^{-}}\left(s_i^{\prime},a^{\prime}\right)
  • 初始化经验回放池R
  • for  序列 e=1 \rightarrow E do:
    • 获取环境初始状态 s_{1}
      • for 时间步 t=1 \rightarrow T do
        • 根据当前网络Q_\omega\left(s_i,a_i\right)以不完全贪婪策略选择动作a_{t}
        • 执行动作a_{t},获得回报 r_{t},环境状态变为s_{t+1} --- 四元组数据已经得到
        • 将四元组数据存储到 R
        • 若R中数据足够,采样 N个数据$\{(s_{i},a_{i},r_{i},s_{i+1})\}_{i=1,\ldots,N}$
        • 对于每个数据,用目标网络计算\left(r_i+\gamma\max_{a^{\prime}}Q_\omega\left(s_i^{\prime},a^{\prime}\right)\right)
        • 最小化目标损失函数\frac{1}{2N}\sum_{i=1}^N\left[Q_\omega\left(s_i,a_i\right)-\left(r_i+\gamma\max_{a^{\prime}}Q_\omega\left(s_i^{\prime},a^{\prime}\right)\right)\right]^2
        • 用损失函数更新训练网络
        • 更新目标网络(C步)
      • end for 
  • end for

代码

引入相关库

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

定义回放池

class ReplayBuffer:
    ''' 经验回放池 '''
    def __init__(self, capacity):
        self.buffer = collections.deque(maxlen=capacity)  # 队列,先进先出

    def add(self, state, action, reward, next_state, done):  # 将数据加入buffer
        self.buffer.append((state, action, reward, next_state, done))

    def sample(self, batch_size):  # 从buffer中采样数据,数量为batch_size,返回类型是numpy
        transitions = random.sample(self.buffer, batch_size)
        state, action, reward, next_state, done = zip(*transitions)
        return np.array(state), action, reward, np.array(next_state), done

    def size(self):  # 目前buffer中数据的数量
        return len(self.buffer)

定义Q网络

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))  # 隐藏层使用ReLU激活函数
        return self.fc2(x)

基本组件完成

class DQN:
    ''' DQN算法 '''
    def __init__(self, state_dim, hidden_dim, action_dim, learning_rate, gamma,
                 epsilon, target_update, device):
        self.action_dim = action_dim
        #初始化训练Q网络
        self.q_net = Qnet(state_dim, hidden_dim,
                          self.action_dim).to(device)  # Q网络
        # 初始化目标网络
        self.target_q_net = Qnet(state_dim, hidden_dim,
                                 self.action_dim).to(device)
        # 使用Adam优化器
        #参数第一项获得网络的优化参数,并使用损失函数的梯度用adam来更新
        self.optimizer = torch.optim.Adam(self.q_net.parameters(),
                                          lr=learning_rate)
        self.gamma = gamma  # 折扣因子
        self.epsilon = epsilon  # epsilon-贪婪策略
        self.target_update = target_update  # 目标网络更新频率
        self.count = 0  # 计数器,记录更新次数
        self.device = device

    def take_action(self, state):  # epsilon-贪婪策略采取动作
        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 update(self, transition_dict):
        #读出状态并将他们变成tensor张量
        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值
        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 = 2e-3
num_episodes = 500
#128个神经元
hidden_dim = 128
gamma = 0.98
epsilon = 0.01
target_update = 10
buffer_size = 10000
minimal_size = 500
batch_size = 64
device = torch.device("cuda") if torch.cuda.is_available() else torch.device(
    "cpu")

#环境初始化
env_name = 'CartPole-v0'
env = gym.make(env_name)
random.seed(0)
np.random.seed(0)
env.seed(0)
torch.manual_seed(0)

#创建经验回放池实例
replay_buffer = ReplayBuffer(buffer_size)
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n
#创建一个DQN智能体实例
agent = DQN(state_dim, hidden_dim, action_dim, lr, gamma, epsilon,
            target_update, device)

return_list = []
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)
                next_state, reward, done, _ = env.step(action)
                replay_buffer.add(state, action, reward, next_state, done)
                state = next_state
                episode_return += reward
                # 当buffer数据的数量超过一定值后,才进行Q网络训练
                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
                    }
                    #update就是更新网络,可以看到目标网络不仅使用了训练网络相同的结构
                    #而且还是用了相同的参数
                    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)

 可视化

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

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

要会手写

 可以看到DQN收敛的快

扩展

智能体玩游戏,输入:游戏图像--图像作为状态
解决方法:将最近的几针图像作为一起输入为状态,用卷积层来提取图像特征

图像识别+环境反馈+最有动作(卧槽Fps外挂)

class ConvolutionalQnet(torch.nn.Module):
    ''' 加入卷积层的Q网络 '''
    def __init__(self, action_dim, in_channels=4):
        super(ConvolutionalQnet, self).__init__()
        self.conv1 = torch.nn.Conv2d(in_channels, 32, kernel_size=8, stride=4)
        self.conv2 = torch.nn.Conv2d(32, 64, kernel_size=4, stride=2)
        self.conv3 = torch.nn.Conv2d(64, 64, kernel_size=3, stride=1)
        #以上卷积层用来提取网络
        self.fc4 = torch.nn.Linear(7 * 7 * 64, 512)
        self.head = torch.nn.Linear(512, action_dim)

    def forward(self, x):
        x = x / 255
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        x = F.relu(self.conv3(x))
        x = F.relu(self.fc4(x))
        return self.head(x)

DQN改进算法

为什么改进:因为传统的DQN算法容易出现估计Q值过高的情况

这里给出结论,具体证明自己查资料:Q值的过高估计随动作空间大小m的增加而增加,也就是说在动作选择数更多的环境中,Q值的过高估计会很严重。

回想一下:

训练网络:由神经网络拟合输出,并不是使用

Q(s, a) \leftarrow Q(s, a)+\alpha\left[r+\gamma \max _{a^{\prime} \in \mathcal{A}} Q\left(s^{\prime}, a^{\prime}\right)-Q(s, a)\right]

而是使用网络输出的,Q(s,a;w),其中w是训练网络的参数,通过更新w来使得Q值能更好的的逼近真实值。

目标网络:结构和训练网络相同,但是参数更新频率较低。计算r+\gamma \max _{a^{\prime} \in \mathcal{A}} Q\left(s^{\prime}, a^{\prime};w\right)

如果使用上述的结论,Q值越来越大,正向误差越来越大

解决方法

先改写一个式子

r+\gamma \max _{a^{\prime} \in \mathcal{A}} Q\left(s^{\prime}, a^{\prime};w\right)     

上式中目标网络实际更新的是max这一块,更新方式如下主要代码

#更新的时候不仅更新网络结构,还更新了网络参数,而这些网络参数都是由采样四元组训练出来的
        if self.count % self.target_update == 0:
            self.target_q_net.load_state_dict(
                self.q_net.state_dict())  # 更新目标网络
        self.count += 1
  # 当buffer数据的数量超过一定值后,才进行Q网络训练
                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
                    }
                    #update就是更新网络,可以看到目标网络不仅使用了训练网络相同的结构
                    #而且还是用了相同的参数
                    agent.update(transition_dict)
        q_values = self.q_net(states).gather(1, actions)  # Q值
        # 下个状态的最大Q值
        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))  # 均方误差损失函数

修改公式为:(r并没有算进去)

Q_{\omega^{-}}\left(s^{\prime}, \arg \max _{a^{\prime}} Q_{\omega^{-}}\left(s^{\prime}, a^{\prime}\right)\right)

实际来说可以拆解两部分:先选取状态s' 下的最优动作a',接着计算该动作的对应价值 Q(s',a').----------注意在实际的目标网络更新中,并不是这样,直接输出这个,看上面的代码就知道(但是取最大值的过程已经隐含了先选动作,在计算价值两个步骤)

解决方法就是找到两个独立训练的神经网络来估算Q_{\omega^{-}}\left(s^{\prime}, \arg \max _{a^{\prime}} Q_{\omega^{-}}\left(s^{\prime}, a^{\prime}\right)\right),一个神经网络用来选取价值最大的动作,计算该动作的价值的时候用另一个神经网络来计算。这样的话即时一个大也能两个平衡一下。

思考

DQN本来就有两个网络,所以训练网络用来选取价值最大的动作,目标网络用来计算动作的价值

Double DQN(改进的目标网络和训练网络)

优化目标:

r+\gamma Q_{\omega^{-}}\left(s^{\prime}, \underset{a^{\prime}}{\arg \max } Q_\omega\left(s^{\prime}, a^{\prime}\right)\right)

显然,与DQN--(目标网络选取动作并计算价值)相比,Double DQN---(训练网络选取动作,目标网络计算价值)区别在于计算状态 s' 下的Q 值

环境搭建

倒立摆:奖励函数:-\left(\theta^2+0.1 \dot{\theta}^2+0.001 a^2\right),向上保持直立不动奖励为0,其他位置奖励为负数,本身没有终止状态,运行200步游戏结束

 状态空间

标号 名称 最小值 最大值
0 正弦值 -1.0 1.0
1 余弦值 -1.0 1.0
2 角速度 -8.0 8.0

动作空间(-2到2离散化处理)

标号 动作 最小值 最大值
0 力矩 -2.0 2.0
代码
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)

在DQN基础上修改

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
        #DQN类型
        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-v0'
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

开始训练DQN

random.seed(0)
np.random.seed(0)
env.seed(0)
torch.manual_seed(0)
replay_buffer = rl_utils.ReplayBuffer(buffer_size)
#默认为DQN
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()

结果

 看一下Double DQN

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()

结果--缓解了Q值过高

Dueling DQN

原理(改进的是训练网络)

 定义一个优势函数A = 状态动作价值函数Q - 状态价值函数V

即:A(s,a) = Q(s,a)-V(s)

明白:在同一个状态下,所有动作的优势和=0,因为所有动作的动作价值的期望就是这个状态的状态价值,所以;

$Q_{\eta,\alpha,\beta}(s,a)=V_{\eta,\alpha}(s)+A_{\eta,\beta}(s,a)$

其中,\eta是状态价值函数和优势函数共享的网络参数,一般用在神经网络中,用来提取特征的前几层

\alpha ,\beta分别为状态价值函数,优势函数的参数。

也就是说,神经网络不输出Q值,而是输出状态价值函数和优势函数,在求和得到Q值。如下图所示网络结构

 为什么要分别建模,如下图所示:优势函数可以观察出不同动作导致的差异,比如超车的时候需要知道从哪边超车更好

缺点

$Q_{\eta,\alpha,\beta}(s,a)=V_{\eta,\alpha}(s)+A_{\eta,\beta}(s,a)$里面的V,S值不确定,会导致训练不稳定。

Dueling DQN 强制最优动作的优势函数实际输出为0,

即:$Q_{\eta,\alpha,\beta}(s,a)=V_{\eta,\alpha}(s)+A_{\eta,\beta}(s,a)-\max_{a^{\prime}}A_{\eta,\beta}\left(s,a^{\prime}\right)$

并且要求;        V(s)=max_{a}Q(s,a)

就可以保证V值建模的唯一性。

举个例子:(deepseek生成)

 实际使用Dueling DQN中,还可以使用平均代替最大化操作,减少过度乐观估计。

Dueling DQN每次更新的是V和Q。 DQN只更新Q。一句话,状态价值本来就会影响动作价值。

代码

定义复合神经网络(代码结构都一样在原有基础上修改就行)

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 = V + A - A.mean(1).view(-1, 1)  # Q值由V值和A值计算得到
        return Q


class DQN:
    ''' DQN算法,包括Double DQN和Dueling 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
        if dqn_type == 'DuelingDQN':  # Dueling DQN采取不一样的网络框架
            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:
            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):
        #给定状态下的最大Q值
        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)
        if self.dqn_type == '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:
            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)
        dqn_loss = torch.mean(F.mse_loss(q_values, q_targets))
        self.optimizer.zero_grad()
        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


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, 'DuelingDQN')
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('Dueling 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('Dueling DQN on {}'.format(env_name))
plt.show()

结果:

Dueling DQN 在多个动作选择下的学习更加稳定 ,也就是说动作的差异性了解的比较好

总结

Double DQN :缓解了DQN中对Q值的过高估计

Dueling DQN:能够很好地学习到不同动作的差异性,动作空间大时非常有效

 这可以作为创新点来发掘,记住从DQN到Double DQN (解决现有问题Q值过高)和 Dueling DQN(解决不同动作模式的差别)的转变。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐