RL 实践(4)—— 二维滚球环境【DQN Double DQN Dueling DQN】

  • 本文介绍如何用 DQN 及它的两个改进 Double DQN & Dueling DQN 解二维滚球问题,这个环境可以看做 gym Maze2d 的简单版本
  • 参考:《动手学强化学习》
  • 完整代码下载:5_[Gym Custom] RollingBall (DQN and Double DQN and Dueling DQN)

文章目录

  • 1. 二维滚球环境
    • 1.1 环境介绍
    • 1.2 代码实现
  • 2. 使用 DQN 系列方法求解
    • 2.1 DQN
      • 2.1.1 算法原理
      • 2.1.2 代码实现
      • 2.1.3 性能
    • 2.2 Double DQN
      • 2.2.1 算法原理
      • 2.2.2 代码实现
      • 2.2.3 性能
    • 2.3 Dueling DQN
      • 2.3.1 算法原理
      • 2.3.2 代码实现
      • 2.3.3 性能
  • 3. 总结

1. 二维滚球环境

1.1 环境介绍

  • 想象二维平面上的一个滚球,对它施加水平和竖直方向的两个力,滚球就会在加速度作用下运动起来,当球碰到平面边缘时会发生完全弹性碰撞,我们希望滚球在力的作用下尽快到达目标位置
    在这里插入图片描述

  • 此环境的状态空间为

    维度意义取值范围
    0滚球 x 轴坐标 [ 0 , width ] [0,\space \text{width}] [0, width]
    1滚球 y 轴坐标 [ 0 , height ] [0,\space \text{height}] [0, height]
    2滚球 x 轴速度 [ − 5.0 , 5.0 ] [-5.0,\space 5.0] [5.0, 5.0]
    3滚球 y 轴速度 [ − 5.0 , 5.0 ] [-5.0,\space 5.0] [5.0, 5.0]

    动作空间为

    维度意义取值范围
    0施加在滚球 x 轴方向的力 [ − 1.0 , 1.0 ] [-1.0,\space 1.0] [1.0, 1.0]
    1施加在滚球 y 轴方向的力 [ − 1.0 , 1.0 ] [-1.0,\space 1.0] [1.0, 1.0]

    奖励函数为

    事件奖励值
    到达目标位置 300.0 300.0 300.0
    发生反弹 − 10.0 -10.0 10.0
    移动一步 − 2.0 -2.0 2.0

1.2 代码实现

  • 通过 gym 的环境自定义方法实现以上二维滚球环境,具体的环境自定义方法可以参考:RL gym 环境(2)—— 自定义环境

    值得一提的是,借助 chatgpt 可以大幅提高这类工具代码的编写效率,以下代码有 80% 都是 chatgpt 自动生成的

  • 代码实现
    import gym
    from gym import spaces
    import numpy as np
    import pygame
    import timeclass RollingBall(gym.Env):metadata = {"render_modes": ["human", "rgb_array"],     # 支持的渲染模式,'rgb_array' 仅用于手动交互"render_fps": 500,}                         # 渲染帧率def __init__(self, render_mode="human", width=10, height=10, show_epi=False):self.max_speed = 5.0self.width = widthself.height = heightself.show_epi = show_epiself.action_space = spaces.Box(low=-1.0, high=1.0, shape=(2,), dtype=np.float64)self.observation_space = spaces.Box(low=np.array([0.0, 0.0, -self.max_speed, -self.max_speed]), high=np.array([width, height, self.max_speed, self.max_speed]),dtype=np.float64)self.velocity = np.zeros(2, dtype=np.float64)self.mass = 0.005self.time_step = 0.01# 奖励参数self.rewards = {'step':-2.0, 'bounce':-10.0, 'goal':300.0}# 起止位置self.target_position = np.array([self.width*0.8, self.height*0.8], dtype=np.float32)self.start_position = np.array([width*0.2, height*0.2], dtype=np.float64)self.position = self.start_position.copy()# 渲染相关self.render_width = 300self.render_height = 300self.scale = self.render_width / self.widthself.window = None# 用于存储滚球经过的轨迹self.trajectory = []# 渲染模式支持 'human' 或 'rgb_array'assert render_mode is None or render_mode in self.metadata["render_modes"]self.render_mode = render_mode# 渲染模式为 render_mode == 'human' 时用于渲染窗口的组件self.window = Noneself.clock = Nonedef _get_obs(self):return np.hstack((self.position, self.velocity))def _get_info(self):return {}def step(self, action):# 计算加速度#force = action * self.massacceleration = action / self.mass# 更新速度和位置self.velocity += acceleration * self.time_stepself.velocity = np.clip(self.velocity, -self.max_speed, self.max_speed)self.position += self.velocity * self.time_step# 计算奖励reward = self.rewards['step']# 处理边界碰撞reward = self._handle_boundary_collision(reward)# 检查是否到达目标状态terminated, truncated = False, Falseif self._is_goal_reached():terminated = Truereward += self.rewards['goal']  # 到达目标状态的奖励obs, info = self._get_obs(), self._get_info()self.trajectory.append(obs.copy())  # 记录滚球轨迹return obs, reward, terminated, truncated, infodef reset(self, seed=None, options=None):# 通过 super 初始化并使用基类的 self.np_random 随机数生成器super().reset(seed=seed)# 重置滚球位置、速度、轨迹self.position = self.start_position.copy()self.velocity = np.zeros(2, dtype=np.float64)self.trajectory = []return self._get_obs(), self._get_info()def _handle_boundary_collision(self, reward):if self.position[0] <= 0:self.position[0] = 0self.velocity[0] *= -1reward += self.rewards['bounce']elif self.position[0] >= self.width:self.position[0] = self.widthself.velocity[0] *= -1reward += self.rewards['bounce']if self.position[1] <= 0:self.position[1] = 0self.velocity[1] *= -1reward += self.rewards['bounce']elif self.position[1] >= self.height:self.position[1] = self.heightself.velocity[1] *= -1reward += self.rewards['bounce']return rewarddef _is_goal_reached(self):# 检查是否到达目标状态(例如,滚球到达特定位置)# 这里只做了一个简单的判断,可根据需要进行修改distance = np.linalg.norm(self.position - self.target_position)return distance < 1.0  # 判断距离是否小于阈值def render(self):if self.render_mode not in ["rgb_array", "human"]:raise Falseself._render_frame()def _render_frame(self):canvas = pygame.Surface((self.render_width, self.render_height))canvas.fill((255, 255, 255))    # 背景白色if self.window is None and self.render_mode == "human":pygame.init()pygame.display.init()self.window = pygame.display.set_mode((self.render_width, self.render_height))if self.clock is None and self.render_mode == "human":self.clock = pygame.time.Clock()# 绘制目标位置target_position_render = self._convert_to_render_coordinate(self.target_position)pygame.draw.circle(canvas, (100, 100, 200), target_position_render, 20)# 绘制球的位置ball_position_render = self._convert_to_render_coordinate(self.position)pygame.draw.circle(canvas, (0, 0, 255), ball_position_render, 10)# 绘制滚球轨迹if self.show_epi:for i in range(len(self.trajectory)-1):position_from = self.trajectory[i]position_to = self.trajectory[i+1]position_from = self._convert_to_render_coordinate(position_from)position_to = self._convert_to_render_coordinate(position_to)color = int(230 * (i / len(self.trajectory)))  # 根据轨迹时间确定颜色深浅pygame.draw.lines(canvas, (color, color, color), False, [position_from, position_to], width=3)# 'human' 渲染模式下会弹出窗口if self.render_mode == "human":# The following line copies our drawings from `canvas` to the visible windowself.window.blit(canvas, canvas.get_rect())pygame.event.pump()pygame.display.update()# We need to ensure that human-rendering occurs at the predefined framerate.# The following line will automatically add a delay to keep the framerate stable.self.clock.tick(self.metadata["render_fps"])# 'rgb_array' 渲染模式下画面会转换为像素 ndarray 形式返回,适用于用 CNN 进行状态观测的情况,为避免影响观测不要渲染价值颜色和策略else:return np.transpose(np.array(pygame.surfarray.pixels3d(canvas)), axes=(1, 0, 2))def close(self):if self.window is not None:pygame.quit()def _convert_to_render_coordinate(self, position):return int(position[0] * self.scale), int(self.render_height - position[1] * self.scale)
    
  • 由于 DQN 类方法都只能用于离散动作空间,我们进一步编写动作包装类,将原生的二维连续动作离散化并拉平为一维离散动作空间
    class DiscreteActionWrapper(gym.ActionWrapper):''' 将 RollingBall 环境的二维连续动作空间离散化为二维离散动作空间 '''def __init__(self, env, bins):super().__init__(env)bin_width = 2.0 / binsself.action_space = spaces.MultiDiscrete([bins, bins]) self.action_mapping = {i : -1+(i+0.5)*bin_width for i in range(bins)}def action(self, action):# 用向量化函数实现高效 action 映射vectorized_func = np.vectorize(lambda x: self.action_mapping[x])    result = vectorized_func(action)action = np.array(result)return actionclass FlattenActionSpaceWrapper(gym.ActionWrapper):''' 将多维离散动作空间拉平成一维动作空间 '''def __init__(self, env):super(FlattenActionSpaceWrapper, self).__init__(env)new_size = 1for dim in self.env.action_space.nvec:new_size *= dimself.action_space = spaces.Discrete(new_size)def action(self, action):orig_action = []for dim in reversed(self.env.action_space.nvec):orig_action.append(action % dim)action //= dimorig_action.reverse()return np.array(orig_action)
    
  • 随机策略测试代码
    import os
    import sys
    base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
    sys.path.append(base_path)import numpy as np
    import time
    from gym.utils.env_checker import check_env
    from environment.Env_RollingBall import RollingBall, DiscreteActionWrapper, FlattenActionSpaceWrapper
    from gym.wrappers import TimeLimit env = RollingBall(render_mode='human', width=5, height=5, show_epi=True)    
    env = FlattenActionSpaceWrapper(DiscreteActionWrapper(env, 5))
    env = TimeLimit(env, 100)
    check_env(env.unwrapped)    # 检查环境是否符合 gym 规范
    env.action_space.seed(10)
    observation, _ = env.reset(seed=10)# 测试环境
    for i in range(100):while True:action = env.action_space.sample()#action = 19state, reward, terminated, truncated, _ = env.step(action)if terminated or truncated:env.reset()breaktime.sleep(0.01)env.render()# 关闭环境渲染
    env.close()
    

2. 使用 DQN 系列方法求解

2.1 DQN

2.1.1 算法原理

  • DQN 方法的出发点是将 Q-Learning 扩展到连续、高维状态空间中,这些情况下无法构造 Q-Learning 中的 Q 表。DQN
    使用函数近似方法来拟合无限大的 Q 表,该网络输入一个状态,输出各个动作的 Q 价值
    在这里插入图片描述
    注意到网络的输出个数为动作空间尺寸,因此 DQN 方法仅适用于离散动作空间的环境。在训练使用 ω \omega ω 参数化的 DQN 网络 Q ω Q_\omega Qω 时,我们通过优化关于 TD error 的 mse loss,来让价值估计靠近 TD target
    ω ∗ = arg ⁡ min ⁡ ω 1 2 N ∑ i = 1 N [ ( r i + γ max ⁡ a ′ Q ω ( s i ′ , a ′ ) ) − Q ω ( s i , a i ) ] 2 \omega^{*}=\arg \min _{\omega} \frac{1}{2 N} \sum_{i=1}^{N}\left[\left(r_{i}+\gamma \max _{a^{\prime}} Q_{\omega}\left(s_{i}^{\prime}, a^{\prime}\right)\right)-Q_{\omega}\left(s_{i}, a_{i}\right)\right]^{2} ω=argωmin2N1i=1N[(ri+γamaxQω(si,a))Qω(si,ai)]2 其中 N N N 为 batch size
  • 如果 online 地使当前交互得到的 transition 样本去更新网络参数,会导致 训练不稳定数据相关性强数据利用率低 等问题。这里的核心矛盾在于 DQN 的训练方式本质是监督学习,而 on-policy RL 框架违背了监督学习基本的 i.i.d 原则。为了解决这个问题,DQN 引入了 经验重放 方法,维护一个 transition 缓冲区,将每次从环境中采样得到的 transition 四元组数据 ( s , a , r , s ′ ) (s,a,r,s') (s,a,r,s) 存储到回放缓冲区中,训练 Q Q Q 网络的时候再从回放缓冲区中随机采样若干数据来进行训练
    在这里插入图片描述
    这样可以增强训练数据的 i.i.d 性质,并且增加样本利用率
  • 使用经验重放机制仍然不足以稳定训练。注意到 DQN 训练的本质还是 Q-Learning 的 TD boostrap 迭代方法,分析上面的损失函数,我们是在使用由 DQN 生成的优化目标 TD target 来优化 DQN 网络。这就导致优化目标随着训练进行不断变化,违背了监督学习的 i.i.d 原则,导致训练不稳定。为了解决这一问题,进一步引入目标网络,它的结构、输入输出等和 DQN 完全一致,每隔一段时间,就把其网络参数 ω − \omega^- ω 更新为 DQN 网络参数 ω \omega ω,其唯一的作用就是给出 TD target r i + γ max ⁡ a ′ Q ω − ( s i ′ , a ′ ) r_{i}+\gamma \max _{a^{\prime}} Q_{\omega^{\pmb{-}}}\left(s_{i}^{\prime}, a^{\prime}\right) ri+γamaxQω(si,a)两次参数更新之间目标网络被冻结,这样就能给出平稳的优化目标,让训练更稳定。除此以外,目标网络部分打破了 DQN 自身的 Bootstrapping 操作,一定程度上缓解了 Q 价值高估的问题。
  • DQN 论文的详细解读见:论文理解【RL经典】 —— 【DQN】Human-level control through deep reinforcement learning

2.1.2 代码实现

  • 经验重放缓冲区(replay buffer)
    class ReplayBuffer:''' 经验回放池 '''def __init__(self, capacity):self.buffer = collections.deque(maxlen=capacity)        # 先进先出队列def add(self, state, action, reward, next_state, done):  self.buffer.append((state, action, reward, next_state, done))def sample(self, batch_size):  transitions = random.sample(self.buffer, batch_size)state, action, reward, next_state, done = zip(*transitions)return np.array(state), np.array(action), reward, np.array(next_state), donedef size(self): return len(self.buffer)
    
  • DQN 网络
    class Q_Net(torch.nn.Module):''' Q 网络是一个两层 MLP, 用于 DQN 和 Double DQN '''def __init__(self, input_dim, hidden_dim, output_dim):super().__init__()self.fc1 = torch.nn.Linear(input_dim, hidden_dim)self.fc2 = torch.nn.Linear(hidden_dim, output_dim)def forward(self, x):x = F.relu(self.fc1(x)) return self.fc2(x)
    
  • DQN agent
    class DQN(torch.nn.Module):''' DQN算法 '''def __init__(self, state_dim, hidden_dim, action_dim, action_range, lr, gamma, epsilon, target_update, device, seed=None):super().__init__()self.action_dim = action_dimself.state_dim = state_dimself.hidden_dim = hidden_dimself.action_range = action_range        # action 取值范围self.gamma = gamma                      # 折扣因子self.epsilon = epsilon                  # epsilon-greedyself.target_update = target_update      # 目标网络更新频率self.count = 0                          # Q_Net 更新计数self.rng = np.random.RandomState(seed)  # agent 使用的随机数生成器self.device = device                # Q 网络self.q_net = Q_Net(state_dim, hidden_dim, action_range).to(device)  # 目标网络self.target_q_net = Q_Net(state_dim, hidden_dim, action_range).to(device)# 使用Adam优化器self.optimizer = torch.optim.Adam(self.q_net.parameters(), lr=lr)def max_q_value_of_given_state(self, state):state = torch.tensor(state, dtype=torch.float).to(self.device)return self.q_net(state).max().item()def take_action(self, state):  ''' 按照 epsilon-greedy 策略采样动作 '''if self.rng.random() < self.epsilon:action = self.rng.randint(self.action_range)else:state = torch.tensor(state, dtype=torch.float).to(self.device)action = self.q_net(state).argmax().item()return actiondef update(self, transition_dict):states = torch.tensor(transition_dict['states'], dtype=torch.float).to(self.device)                             # (bsz, state_dim)next_states = torch.tensor(transition_dict['next_states'], dtype=torch.float).to(self.device)                   # (bsz, state_dim)actions = torch.tensor(transition_dict['actions'], dtype=torch.int64).view(-1, 1).to(self.device)               # (bsz, act_dim)rewards = torch.tensor(transition_dict['rewards'], dtype=torch.float).view(-1, 1).to(self.device).squeeze()     # (bsz, )dones = torch.tensor(transition_dict['dones'], dtype=torch.float).view(-1, 1).to(self.device).squeeze()         # (bsz, )q_values = self.q_net(states).gather(dim=1, index=actions).squeeze()                # (bsz, )max_next_q_values = self.target_q_net(next_states).max(axis=1)[0]                   # (bsz, )q_targets = rewards + self.gamma * max_next_q_values * (1 - dones)                  # (bsz, )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:# 按一定间隔更新 target 网络参数self.target_q_net.load_state_dict(self.q_net.state_dict())  self.count += 1
    
  • 训练并绘制性能曲线
    if __name__ == "__main__":def moving_average(a, window_size):''' 生成序列 a 的滑动平均序列 '''cumulative_sum = np.cumsum(np.insert(a, 0, 0)) middle = (cumulative_sum[window_size:] - cumulative_sum[:-window_size]) / window_sizer = np.arange(1, window_size-1, 2)begin = np.cumsum(a[:window_size-1])[::2] / rend = (np.cumsum(a[:-window_size:-1])[::2] / r)[::-1]return np.concatenate((begin, middle, end))def set_seed(env, seed=42):''' 设置随机种子 '''env.action_space.seed(seed)env.reset(seed=seed)random.seed(seed)np.random.seed(seed)torch.manual_seed(seed)state_dim = 4                               # 环境观测维度action_dim = 1                              # 环境动作维度action_bins = 10                            # 动作离散 bins 数量action_range = action_bins * action_bins    # 环境动作空间大小hidden_dim = 32lr = 1e-3num_episodes = 1000gamma = 0.99epsilon_start = 0.01epsilon_end = 0.001target_update = 1000buffer_size = 10000minimal_size = 5000batch_size = 128device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")# build environmentenv = RollingBall(width=5, height=5, show_epi=True)    env = FlattenActionSpaceWrapper(DiscreteActionWrapper(env, bins=10))env = TimeLimit(env, 100)check_env(env.unwrapped)            # 检查环境是否符合 gym 规范set_seed(env, seed=42)              # build agentreplay_buffer = ReplayBuffer(buffer_size)agent = DQN(state_dim, hidden_dim, action_dim, action_range, lr, gamma, epsilon_start, target_update, device)# 随机动作来填充 replay bufferstate, _ = env.reset()while replay_buffer.size() <= minimal_size:action = env.action_space.sample()next_state, reward, terminated, truncated, _ = env.step(action)replay_buffer.add(state, action, reward, next_state, done=terminated or truncated)if terminated or truncated:env.render()state, _ = env.reset()#print(replay_buffer.size())# 开始训练return_list = []max_q_value_list = []max_q_value = 0for i in range(20):with tqdm(total=int(num_episodes / 20), desc='Iteration %d' % i) as pbar:for i_episode in range(int(num_episodes / 20)):episode_return = 0state, _ = env.reset()while True:# 保存经过状态的最大Q值max_q_value = agent.max_q_value_of_given_state(state) * 0.005 + max_q_value * 0.995 # 平滑处理max_q_value_list.append(max_q_value)                                    # 选择动作移动一步action = agent.take_action(state)next_state, reward, terminated, truncated, _ = env.step(action)# 更新replay_bufferreplay_buffer.add(state, action, reward, next_state, done=terminated or truncated)# 当buffer数据的数量超过一定值后,才进行Q网络训练assert replay_buffer.size() > minimal_sizeb_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)state = next_stateepisode_return += rewardif terminated or truncated:env.render()break#env.render()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)#env.render()agent.epsilon += (epsilon_end - epsilon_start) / 10# show policy performencemv_return_list = moving_average(return_list, 29)episodes_list = list(range(len(return_list)))plt.figure(figsize=(12,8))plt.plot(episodes_list, return_list, label='raw', alpha=0.5)plt.plot(episodes_list, mv_return_list, label='moving ave')plt.xlabel('Episodes')plt.ylabel('Returns')plt.title(f'{agent._get_name()} on RollingBall')plt.legend()plt.savefig(f'./result/{agent._get_name()}.png')plt.show()# show Max Q value during trainingframes_list = list(range(len(max_q_value_list)))plt.plot(frames_list, max_q_value_list)plt.axhline(max(max_q_value_list), c='orange', ls='--')plt.xlabel('Frames')plt.ylabel('Max Q_value')plt.title(f'{agent._get_name()} on RollingBall')plt.savefig(f'./result/{agent._get_name()}_MaxQ.png')plt.show()
    

2.1.3 性能

在这里插入图片描述

2.2 Double DQN

2.2.1 算法原理

  • Bellman Optimal Equation 中有最大化 max ⁡ \max max 操作,这会导致价值函数的高估,而且高估会被 bootstrap 机制不断加剧在这里插入图片描述 最终我们得到的是真实 Q ∗ Q^* Q 的有偏估计。其实高估本身没什么,但关键是高估是不均匀的,如果某个 ( s , a ) (s,a) (s,a) 被迭代计算更多,那么由于 bootstrap 机制其价值也被高估更多,显然 replay buffer 中的 ( s , a ) (s,a) (s,a) 分布是不均匀的,很可能某个次优动作就变成最优动作了,这会导致 agent 性能下降

  • 我们可以对 Q Q Q 值的过高估计做简化的定量分析。假设在状态 s s s 下所有动作的期望回报均无差异,即 Q ∗ ( s , a ) = V ∗ ( s ) Q^*(s,a)=V^*(s) Q(s,a)=V(s)(此设置是为了定量分析所简化的情形,实际上不同动作的期望回报通常会存在差异);假设神经网络估算误差 Q ω − ( s , a ) − Q ∗ ( s , a ) Q_{\omega^{\pmb{-}}}(s,a)-Q^*(s,a) Qω(s,a)Q(s,a) 服从 [ − 1 , 1 ] [-1,1] [1,1] 之间的均匀独立同分布;假设动作空间大小为 m m m。那么,对于任意状态,有 E [ max ⁡ a Q ω − ( s , a ) − max ⁡ a ′ Q ∗ ( s , a ′ ) ] = m − 1 m + 1 \mathbb{E}\left[\max _{a} Q_{\omega^{\pmb{-}}}(s, a)-\max _{a^{\prime}} Q_{*}\left(s, a^{\prime}\right)\right]=\frac{m-1}{m+1} E[amaxQω(s,a)amaxQ(s,a)]=m+1m1动作空间越大时, Q Q Q 值过高估计越严重

    在这里插入图片描述

  • Q Q Q 价值过估计问题在表格型的 Q-learning 中也存在,一个解决方案是把选择动作和计算价值分开处理,这种方法称为 Double Q-learning ,详见强化学习笔记(6)—— 无模型(model-free)control问题 5.4 节。Double DQN 方法模仿 Double Q-learning 的思路处理了 DQN 的价值高估问题,它做的改动其实非常小,观察 TD target 公式
    y = r i + γ max ⁡ a ′ Q ω − ( s i ′ , a ′ ) y=r_{i}+\gamma \max _{a^{\prime}} Q_{\omega^{\pmb{-}}}\left(s_{i}^{\prime}, a^{\prime}\right) y=ri+γamaxQω(si,a) 它可以看作选择最优动作 a ∗ a^* a 和计算 TD target y y y 两步

    1. 原始 DQN 中,两步都用目标网络完成,即
      a ∗ = arg max ⁡ a ′ Q ω − ( s i ′ , a ′ ) y = r + γ Q ω − ( s i ′ , a ∗ ) a^* = \argmax_{a'}Q_{\omega^{\pmb{-}}}\left(s_{i}^{\prime}, a^{\prime}\right) \\ y=r+\gamma Q_{\omega^{\pmb{-}}}\left(s_{i}^{\prime}, a^*\right) a=aargmaxQω(si,a)y=r+γQω(si,a)
    2. Double DQN 中,第一步用 DQN 完成,第二步用目标网络完成,即
      a ∗ = arg max ⁡ a ′ Q ω ( s i ′ , a ′ ) y = r + γ Q ω − ( s i ′ , a ∗ ) a^* = \argmax_{a'}Q_{\omega}\left(s_{i}^{\prime}, a^{\prime}\right) \\ y=r+\gamma Q_{\omega^{\pmb{-}}}\left(s_{i}^{\prime}, a^*\right) a=aargmaxQω(si,a)y=r+γQω(si,a)

    显然有(左边来自 Double DQN,右边来自 DQN)
    Q ω − ( s i ′ , arg max ⁡ a ′ Q ω ( s i ′ , a ′ ) ) ≤ max ⁡ a ′ Q ω − ( s i ′ , a ′ ) Q_{\omega^{\pmb{-}}}\left(s_{i}^{\prime}, \argmax_{a'}Q_{\omega}\left(s_{i}^{\prime}, a^{\prime}\right)\right) \leq \max_{a^\prime}Q_{\omega^{\pmb{-}}}(s_{i}^{\prime}, a^{\prime}) Qω(si,aargmaxQω(si,a))amaxQω(si,a) 因此 Double DQN 得到的估计值比 DQN 更小一些。总的来看,DDQN 不但缓解了最大化导致的偏差,还和 DQN 一样部分缓解了 Bootstrapping 导致的偏差,因此其价值估计更准确

2.2.2 代码实现

  • 继承原始 DQN 类,修改其中的 update 方法即可
    class Double_DQN(DQN):''' Double DQN算法 '''        def update(self, transition_dict):states = torch.tensor(transition_dict['states'], dtype=torch.float).to(self.device)                             # (bsz, state_dim)next_states = torch.tensor(transition_dict['next_states'], dtype=torch.float).to(self.device)                   # (bsz, state_dim)actions = torch.tensor(transition_dict['actions'], dtype=torch.int64).view(-1, 1).to(self.device)               # (bsz, act_dim)rewards = torch.tensor(transition_dict['rewards'], dtype=torch.float).view(-1, 1).to(self.device).squeeze()     # (bsz, )dones = torch.tensor(transition_dict['dones'], dtype=torch.float).view(-1, 1).to(self.device).squeeze()         # (bsz, )q_values = self.q_net(states).gather(dim=1, index=actions).squeeze()                # (bsz, )max_action = self.q_net(next_states).max(axis=1)[1]                                 # (bsz, )max_next_q_values = self.target_q_net(next_states).gather(dim=1, index=max_action.unsqueeze(1)).squeeze()             q_targets = rewards + self.gamma * max_next_q_values * (1 - dones)                  # (bsz, )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:# 按一定间隔更新 target 网络参数self.target_q_net.load_state_dict(self.q_net.state_dict())  self.count += 1
    
  • 其余代码都可以维持不变

2.2.3 性能

在这里插入图片描述

  • 注意到 Double DQN 的最大 Q Q Q 价值估计相比普通 DQN 减少很多,说明值过高估计的问题得到了很大缓解

2.3 Dueling DQN

2.3.1 算法原理

  • RL 中 Q ( s , a ) Q(s,a) Q(s,a) 价值可以拆分为 V ( s ) V(s) V(s) 价值和动作优势 A ( s , a ) A(s,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)+A(s,a) 根据价值函数定义 V ( s ) = E a [ Q ( s , a ) ] V(s) = \mathbb{E}_a\left[Q(s,a)\right] V(s)=Ea[Q(s,a)],在同一个状态下所有动作的优势值之和 ∑ a A ( s , a ) = 0 \sum_aA(s,a)=0 aA(s,a)=0
  • DQN 和 Double DQN 都是直接对 Q Q Q 函数进行建模,而 Dueling DQN 对 A A A V V V 分别建模,通过组合二者来计算 Q Q Q 函数。这样做的好处在于:某些情境下智能体只会关注状态的价值,而并不关心不同动作导致的差异,此时将二者分开建模能够使智能体更好地处理与动作关联较小的状态
    在这里插入图片描述
    如图所示的驾驶车辆游戏中,agent 注意力集中的部位被显示为橙色,当智能体前面没有车时,车辆自身动作并没有太大差异,此时智能体更关注状态(道路尽头位置)的价值,而当智能体前面有车时(智能体需要超车),智能体开始关注不同动作优势值的差异。
  • 为了同时拟合 V ( s ) V(s) V(s) A ( s , a ) A(s,a) A(s,a)将原始的 Q ( s , a ) Q(s,a) Q(s,a) 改成一个共享隐藏层的双头 mlp,这可以理解为二者共享输入的状态特征,最后通过不同的线性组合参数组合得到 A A A V V V,公式表示如下
    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) 这里有一个对于值和值建模不唯一性的问题。例如,对于同样的 Q Q Q 值,如果将 V V V 值加上任意大小的常数 C C C,再将所有 A A A 值减去 C C C,则得到的 Q Q Q 值依然不变,这就导致了训练的不稳定性。为了解决这一问题,Dueling DQN 强制最优动作的优势函数的实际输出为 0,即
    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^{\prime}} A_{\eta, \beta}\left(s, a^{\prime}\right) Qη,α,β(s,a)=Vη,α(s)+Aη,β(s,a)amaxAη,β(s,a) 此时 V ( s ) = max ⁡ a Q ( s , a ) V(s)=\max_a Q(s,a) V(s)=maxaQ(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^{\prime}} A_{\eta, \beta}\left(s, a^{\prime}\right) Qη,α,β(s,a)=Vη,α(s)+Aη,β(s,a)A1aAη,β(s,a) 此时 V ( s ) = 1 ∣ A ∣ ∑ a ′ Q ( s , a ′ ) V(s)=\frac{1}{|\mathcal{A}|} \sum_{a^{\prime}} Q\left(s, a^{\prime}\right) V(s)=A1aQ(s,a)虽然它不再满足贝尔曼最优方程,但实际应用时更加稳定
  • Dueling DQN 能更高效学习状态价值函数。每一次更新时, V V V 函数都会被更新,这也会影响到其他动作的 Q Q Q 值。而传统的 DQN 只会更新某个动作的 Q Q Q 值,其他动作的值就不会更新。由 Dueling DQN 的原理可知,随着动作空间的增大,Dueling DQN 相比于 DQN 的优势更为明显

2.3.2 代码实现

  • 实现同时拟合 V V V A A A 的双头网络
    class VA_Net(torch.nn.Module):''' VA 网络是一个两层双头 MLP, 仅用于 Dueling DQN '''def __init__(self, input_dim, hidden_dim, output_dim):super(VA_Net, self).__init__()self.fc1 = torch.nn.Linear(input_dim, hidden_dim)   # 共享网络部分self.fc_A = torch.nn.Linear(hidden_dim, output_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().item()                         # Q值由V值和A值计算得到return Q
    ``
    
  • 继承原始 DQN 类,在初始化时重新将 q_nettarget_q_net 重新指向 VA_Net 对象,并新建优化器
    class Dueling_DQN(DQN):''' Dueling DQN 算法 '''            def __init__(self, state_dim, hidden_dim, action_dim, action_range, lr, gamma, epsilon, target_update, device, seed=None):super().__init__(state_dim, hidden_dim, action_dim, action_range, lr, gamma, epsilon, target_update, device, seed)# Q 网络self.q_net = VA_Net(state_dim, hidden_dim, action_range).to(device)  # 目标网络self.target_q_net = VA_Net(state_dim, hidden_dim, action_range).to(device)# 使用Adam优化器self.optimizer = torch.optim.Adam(self.q_net.parameters(), lr=lr)
    
  • 其余代码都可以维持不变

2.3.3 性能

在这里插入图片描述

3. 总结

  • 本文讲解了 DQN 算法及其两个容易实现的变式 —— Double DQN 和 Dueling DQN
    1. DQN 的主要思想是用一个神经网络来拟合最优 Q Q Q 函数,利用 Q-learning 的思想,通过优化 TD error 的 MSE 损失进行参数更新。为了保证训练的稳定性和高效性,DQN 算法引入了经验回放和目标网络两大模块
    2. Double DQN 将 TD target 的构造分成最优动作选取和提取价值两个部分,第一步用 DQN 完成,第二步用目标网络完成,缓解了 DQN 中对值的过高估计问题
    3. Dueling DQN 将对 Q Q Q 函数的建模拆分成对 V V V A A A 两部分进行建模,使智能体更好地处理与动作关联较小的状态,更高效学习状态价值函数。当动作空间很大时相对 DQN 有更大优势

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/50764.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Android】ARCore Sceneform 框架入门开发示例

本文只是作者在工作中涉及到的一点 ARCore 的知识进行了记录&#xff0c;深度和广度都非常有限&#xff0c;仅做入门学习用。如有错误烦请指正。 目录 文章目录 目录ARCore 基础AR 概念ARCore 概念ARCore 中的坐标系 Sceneform 开发Sceneform 开发步骤Sceneform Demo 实例Scen…

ArcGIS土地利用数据制备、分析及基于FLUS模型土地利用预测实践技术

土地资源对人类至关重要&#xff0c;土地是人类赖以生存和发展的物质基础&#xff0c;是一切生产和一起存在的源泉。利用现代化的技术手段及时、准确地获取土地利用现状&#xff0c;以及充分认识土地利用和土地覆盖变化规律&#xff0c;能极大地提高制定土地利用规划的科学性和…

土地覆盖/土地利用简介及数据集

1 简介 土地覆盖&#xff1a;地球表面当前所具有的自然和人为影响所形成的覆盖物&#xff0c;是地球表面的自然状态&#xff0c;如森林、草场、农田、土壤、冰川、湖泊、沼泽湿地及道路等。 土地利用&#xff1a;是人类在生产活动中为达到一定的经济效益、社会效益和生态效益…

从灰度图到地形图

序 大概就是根据一个灰度图&#xff0c;生成一个地形。 分两步来实现吧&#xff1b;首先&#xff0c;用随机数生成地形&#xff1b;然后&#xff0c;根据灰度图生成地形。 小白&#xff0c;没啥基础&#xff0c;所以只能慢慢来。 参考&#xff1a; 【萌新图形学】地形网格生成入…

Fragstats的土地利用景观格局分析

土地利用以及景观格局是当前全球环境变化研究的重要组成部分及核心内容&#xff0c;其对区域的可持续发展以及区域土地管理有非常重要的意义。通过对土地利用时空变化规律进行分析可以更好的了解土地利用变化的过程和机制&#xff0c;并且通过调整人类社会经济活动&#xff0c;…

土壤类型、土壤质地、土壤养分空间分布

引言 全国土壤类型、质地、养分及变化等信息产品分为土壤类型数据、土壤质地数据、土壤养分数据及土壤变化数据等。该类产品是基于野外调查和实地采样&#xff0c;结合历史数据&#xff0c;建立全国土壤类型、质地、养分及变化等数据集&#xff0c;同时做成多种尺度的栅格数据。…

第三版全球干旱指数和潜在蒸散数据发布

Robert J. Zomer &#xff1b;JianchuXu&#xff1b;AntonioTrabucco(Kunming Institute of Botany, Chinese Academy of Science&#xff1b;Euro-Mediterranean Center on Climate Change, IAFES Division, Sassari, Italy) 摘要 潜在蒸散(Potential evapotranspiration&…

制作(土壤侵蚀度图,土地利用图、生态系统类型图,生境分布图)遥感解译模型方法.

最新《环境影响评价技术导则生态影响HJ19—2022》于2022年1月15日发布&#xff0c;2022-07-01正式实施&#xff0c;新导则颁布后&#xff0c;要求生态现状评价内容中基本图件构成包含&#xff1a;项目区域地理位置图、工程平面图、调查样方、样线、点位、断面等布设图、土地利用…

ArcGIS中的土地利用变化分析详解

本篇主要是针对矢量数据的分析。 一、不同时期的土地利用矢量数据&#xff0c;如何分析其图形及属性变化&#xff1f; 土地利用图&#xff08;左图为1993年&#xff0c;右图为2003年&#xff09; 思路如下&#xff1a; 可以先对2个图层进行Union操作&#xff0c;然后在结果中…

ArcGis实战:土地利用变化矩阵与土地利用变化图制作

目录 一、数据下载 &#xff08;1&#xff09;下载网站 &#xff08;2&#xff09;账号注册 &#xff08;3&#xff09;数据下载 二、图像预处理 (1)准备研究区矢量图层 (2)图像镶嵌 1.添加数据 2.去除黑边&#xff08;复制栅格&#xff09; 3.图像拼接&#xff08;图…

基于GMT绘制行政区划图

基于GMT6软件&#xff0c;在VS Code上简单绘制单独省份行政区划图&#xff0c;以广西为例。 数据源&#xff1a;中国地理数据空间集&#xff0c;中国国界省界数据。 set GMT_SESSION_NAME97401 gmt begin GDO pnggmt set MAP_GRID_PEN_PRIMARY 0.25p,gray,2_2gmt coast -R103…

GPT模型应用丨遥感云大数据在灾害、水体与湿地领域典型案例实践

​ ​ ​ ​ 第一部分 基础实践 一 平台及基础开发平台 GEE平台及典型应用案例介绍&#xff1b; GEE开发环境及常用数据资源介绍&#xff1b; ChatGPT、文心一言等GPT模型介绍 JavaScript基础简介&#xff1b; GEE遥感云重要概念与典型数据分析流程&#xff1b; …

Google Earth Engine(GEE)下载全球土壤砂粒(Sand)含量数据

数据介绍&#xff1a; SoilGrids&#xff0c;是一个全球土壤属性地图&#xff0c;现在可以通过GEE下载。 SoilGrids是ISRIC–世界土壤信息的一个项目&#xff0c;旨在提供一个全球一致的、由数据驱动的系统&#xff0c;以预测土壤性质&#xff0c;并将其作为一项重要指标。该项…

利用ArcGIS做一张土地利用现状图

利用ArcGIS做一张土地利用现状图 首先我们打开ArcGIS加载进来三调数据&#xff0c;这是镇的数据&#xff0c;现在我们需要村的行政边界。首先打开属性表添加一个新的字段命名“行政代码”字段属性为文本&#xff0c;文本长度保持默认。 在行政代码列打开字段计算器输入Left(…

PIE-engine 教程 ——荒漠化程度提取案例分析(陕北区域)

这里我们利用ndvi进行荒漠化处理&#xff0c;我们这里将ndvi小于0.1的地方进行淹没掉&#xff0c;将剩余部分作为作为荒漠化的区域。这里选择时间的筛选我们将4月到10月的的时间作为研究时间。这里我们有几个函数需要需要先了解一下&#xff0c;首先是我们了解画polygon的函数&…

【乌鲁木齐】基于ArcGIS、ENVI、InVEST、FRAGSTATS等多技术融合提升环境、生态、水文、土地、土壤、农业、大气等领域应用

【原文链接】&#xff1a;【乌鲁木齐】基于ArcGIS、ENVI、InVEST、FRAGSTATS等多技术融合提升环境、生态、水文、土地、土壤、农业、大气https://mp.weixin.qq.com/s?__bizMzU5NTkyMzcxNw&mid2247545692&idx1&sn1934aa8be717557c6c97c6b5e2a49151&chksmfe68f5…

自然资源(国土)部门通用坐标TXT转SHP的FME方法

一、任务来源 经常遇到提供了TXT坐标需要转成矢量&#xff0c;然后去跟其他数据做分析统计的情况&#xff0c;最典型的就是原国土资源部门&#xff08;现在的自然资源部门&#xff09;在各种系统中用到的TXT坐标格式&#xff0c;它们的本质爱基本都是相同的&#xff0c;有的根据…

电商评论文本情感分类(中文文本分类+中文词云图)(第一部分-textcnn)

电商评论文本情感分类(中文文本分类中文词云图) 第一部分 第二部分Bert部分 本项目包含&#xff1a; 1.中文文本处理 2.中文词云图绘制 3.中文词嵌入 4.基于textcnn的中文文本分类&#xff08;Test_Acc89.2000&#xff09; 5.基于bert的中文文本分类&#xff08;Test_Acc0.…

详解升讯威在线客服系统前端多国语言实现技术:原生支持葡文、印尼文、土耳其文、俄文

我在业余时间开发维护了一款免费开源的升讯威在线客服系统&#xff0c;也收获了许多用户。对我来说&#xff0c;只要能获得用户的认可&#xff0c;就是我最大的动力。 越来越多的用户向我提出需求&#xff0c;希望为访客端增加更多的界面语言&#xff0c;如葡文、印尼文、土耳其…

【NLP论文翻译】基于显著性感知主题建模的面向主题的客户服务口语对话摘要

本博客为博主论文阅读记录&#xff0c;原论文和github地址如下&#xff1a; 原论文下载&#xff1a;https://ojs.aaai.org/index.php/AAAI/article/view/17723/17530 代码&#xff1a;https://github.com/RowitZou/topic-dialog-summ 本篇博客所介绍论文为AAAI 2021论文 NLP领域…