让 Q 值估计更准确:从 DQN 到 Double DQN 的改进方案

简介: 本文深入剖析DQN的过估计偏差根源,系统讲解Double DQN(解耦动作选择与评估)、Dueling DQN(分离状态值与动作优势)、优先经验回放(按TD误差智能采样)三大核心改进,并用PyTorch从零实现,最后对比CleanRL专业实现,助你扎实掌握强化学习进阶技巧。

DQN 用

max Q(s',a')

计算目标值,等于在挑 Q 值最高的动作,但是这些动作中包括了那些因为估计噪声而被高估的动作,素以就会产生过估计偏差,直接后果是训练不稳定、策略次优。

这篇文章要解决的就是这个问题,内容包括:DQN 为什么会过估计、Double DQN 怎么把动作选择和评估拆开、Dueling DQN 怎么分离状态值和动作优势、优先经验回放如何让采样更聪明,以及用 PyTorch 从头实现这些改进。最后还会介绍一个 CleanRL 的专业实现。

过估计问题

DQN 的目标值如下:

 y = r + γ·maxₐ' Q(s', a'; θ⁻)

问题就在于,同一个网络既负责选动作(a* = argmax Q),又负责评估这个动作的价值。Q 值本身是带噪声的估计所以有时候噪声会让差动作的 Q 值偏高,取 max 操作天然偏向选那些被高估的动作。

数学上有个直观的解释:

 E[max(X₁, X₂, ..., Xₙ)] ≥ max(E[X₁], E[X₂], ..., E[Xₙ])

最大值的期望总是大于等于期望的最大值,这是凸函数的 Jensen 不等式。

过估计会导致收敛变慢,智能体把时间浪费在探索那些被高估的动作上。其次是策略质量打折扣,高噪声的动作可能比真正好的动作更受青睐。更糟的是过估计会不断累积,导致训练发散。泛化能力也会受损——在状态空间的噪声区域,智能体会表现得过于自信。

Double DQN:把选择和评估拆开

标准 DQN 一个网络干两件事:

 a* = argmaxₐ' Q(s', a'; θ⁻)  # 选最佳动作  
 y = r + γ · Q(s', a*; θ⁻)    # 评估这个动作(同一个网络)

Double DQN 用两个网络,各管一件:

 a* = argmaxₐ' Q(s', a'; θ)  # 用当前网络选  
 y = r + γ · Q(s', a*; θ⁻)   # 用目标网络评估

当前网络(θ)选动作,目标网络(θ⁻)评估。两个网络的误差不相关这样最大化偏差就被打破了。

为什么有效呢?

假设当前网络把动作 a 的价值估高了,目标网络(参数不同)大概率不会犯同样的错。误差相互独立,倾向于抵消而非累加。

最通俗的解释就是DQN 像是自己给菜打分、自己挑菜吃,这样烂菜可能就混进来了,而Double DQN 让朋友打分、你来挑,两边的误差对冲掉了。

  Standard DQN:  E[Q(s, argmaxₐ Q(s,a))] ≥ maxₐ E[Q(s,a)]   (有偏)  
 Double DQN:    E[Q₂(s, argmaxₐ Q₁(s,a))] ≈ maxₐ E[Q(s,a)]  (无偏)

从 DQN 到 Double DQN,只需要改一行:

 # DQN 目标  
next_q_values=target_network(next_states).max(1)[0]  
target=rewards+gamma*next_q_values* (1-dones)  

# Double DQN 目标  
next_actions=current_network(next_states).argmax(1)  # <- 用当前网络选  
next_q_values=target_network(next_states).gather(1, next_actions.unsqueeze(1))  # <- 用目标网络评估  
 target=rewards+gamma*next_q_values.squeeze() * (1-dones)

就这一行改动极小,效果却很明显。

实现:Double DQN

扩展 DQN Agent

 classDoubleDQNAgent(DQNAgent):  
    """  
    Double DQN: 通过解耦动作选择和评估来减少过估计偏差。  
    """  

    def__init__(self, *args, **kwargs):  
        """  
        初始化 Double DQN agent。  
        从 DQN 继承所有内容,只改变目标计算。  
        """  
        super().__init__(*args, **kwargs)  

    defupdate(self) ->Dict[str, float]:  
        """  
        执行 Double DQN 更新。  

        Returns:  
            metrics: 训练指标  
        """  
        iflen(self.replay_buffer) <self.batch_size:  
            return {}  

        # 采样批次  
        states, actions, rewards, next_states, dones=self.replay_buffer.sample(  
            self.batch_size  
        )  

        states=states.to(self.device)  
        actions=actions.to(self.device)  
        rewards=rewards.to(self.device)  
        next_states=next_states.to(self.device)  
        dones=dones.to(self.device)  

        # 当前 Q 值 Q(s,a;θ)  
        current_q_values=self.q_network(states).gather(1, actions.unsqueeze(1))  

        # Double DQN 目标计算  
        withtorch.no_grad():  
            # 使用当前网络选择动作  
            next_actions=self.q_network(next_states).argmax(1)  

            # 使用目标网络评估动作  
            next_q_values=self.target_network(next_states).gather(  
                1, next_actions.unsqueeze(1)  
            ).squeeze()  

            # 计算目标  
            target_q_values=rewards+ (1-dones) *self.gamma*next_q_values  

        # 计算损失  
        loss=F.mse_loss(current_q_values.squeeze(), target_q_values)  

        # 梯度下降  
        self.optimizer.zero_grad()  
        loss.backward()  
        torch.nn.utils.clip_grad_norm_(self.q_network.parameters(), max_norm=10.0)  
        self.optimizer.step()  

        self.training_step+=1  

        return {  
            'loss': loss.item(),  
            'q_mean': current_q_values.mean().item(),  
            'q_std': current_q_values.std().item(),  
            'target_q_mean': target_q_values.mean().item()  
         }

训练函数:

 deftrain_double_dqn(  
    env_name: str,  
    n_episodes: int=1000,  
    max_steps: int=500,  
    train_freq: int=1,  
    eval_frequency: int=50,  
    eval_episodes: int=10,  
    verbose: bool=True,  
    **kwargs  
) ->Tuple:  
    """  
    训练 Double DQN agent(使用 DoubleDQNAgent 而不是 DQNAgent)。  
    """  
    # 与 train_dqn 相同但使用 DoubleDQNAgent  
    env=gym.make(env_name)  
    eval_env=gym.make(env_name)  

    state_dim=env.observation_space.shape[0]  
    action_dim=env.action_space.n  

    # 使用 DoubleDQNAgent  
    agent=DoubleDQNAgent(  
        state_dim=state_dim,  
        action_dim=action_dim,  
        **kwargs  
    )  

    # 训练循环(与 DQN 相同)  
    stats= {  
        'episode_rewards': [],  
        'episode_lengths': [],  
        'losses': [],  
        'q_values': [],  
        'target_q_values': [],  
        'eval_rewards': [],  
        'eval_episodes': [],  
        'epsilons': []  
    }  

    print(f"Training Double DQN on {env_name}")  
    print(f"State dim: {state_dim}, Action dim: {action_dim}")  
    print("="*70)  

    forepisodeinrange(n_episodes):  
        state, _=env.reset()  
        episode_reward=0  
        episode_length=0  
        episode_metrics= []  

        forstepinrange(max_steps):  
            action=agent.select_action(state, training=True)  
            next_state, reward, terminated, truncated, _=env.step(action)  
            done=terminatedortruncated  

            agent.store_transition(state, action, reward, next_state, done)  

            ifstep%train_freq==0:  
                metrics=agent.update()  
                ifmetrics:  
                    episode_metrics.append(metrics)  

            episode_reward+=reward  
            episode_length+=1  
            state=next_state  

            ifdone:  
                break  

        # 更新目标网络  
        if (episode+1) %kwargs.get('target_update_freq', 10) ==0:  
            agent.update_target_network()  

        agent.decay_epsilon()  

        # 存储统计信息  
        stats['episode_rewards'].append(episode_reward)  
        stats['episode_lengths'].append(episode_length)  
        stats['epsilons'].append(agent.epsilon)  

        ifepisode_metrics:  
            stats['losses'].append(np.mean([m['loss'] forminepisode_metrics]))  
            stats['q_values'].append(np.mean([m['q_mean'] forminepisode_metrics]))  
            stats['target_q_values'].append(np.mean([m['target_q_mean'] forminepisode_metrics]))  

        # 评估  
        if (episode+1) %eval_frequency==0:  
            eval_reward=evaluate_dqn(eval_env, agent, eval_episodes)  
            stats['eval_rewards'].append(eval_reward)  
            stats['eval_episodes'].append(episode+1)  

            ifverbose:  
                avg_reward=np.mean(stats['episode_rewards'][-50:])  
                avg_loss=np.mean(stats['losses'][-50:]) ifstats['losses'] else0  
                avg_q=np.mean(stats['q_values'][-50:]) ifstats['q_values'] else0  

                print(f"Episode {episode+1:4d} | "  
                      f"Reward: {avg_reward:7.2f} | "  
                      f"Eval: {eval_reward:7.2f} | "  
                      f"Loss: {avg_loss:7.4f} | "  
                      f"Q: {avg_q:6.2f} | "  
                      f"ε: {agent.epsilon:.3f}")  

    env.close()  
    eval_env.close()  

    print("="*70)  
    print("Training complete!")  

     returnagent, stats

LunarLander-v3

 # 训练 Double DQN  
if__name__=="__main__":  
    device='cuda'iftorch.cuda.is_available() else'cpu'  

    agent_ddqn, stats_ddqn=train_double_dqn(  
        env_name='LunarLander-v3',  
        n_episodes=4000,  
        max_steps=1000,  
        learning_rate=5e-4,  
        gamma=0.99,  
        epsilon_start=1.0,  
        epsilon_end=0.01,  
        epsilon_decay=0.9995,  
        buffer_capacity=100000,  
        batch_size=128,  
        target_update_freq=20,  
        train_freq=4,  
        eval_frequency=100,  
        eval_episodes=10,  
        hidden_dims=[256, 256],  
        device=device,  
        verbose=True  
    )  

    # 保存模型  
     agent_ddqn.save('doubledqn_lunar_lander.pth')

输出:

  Training Double DQN on LunarLander-v3  
State dim: 8, Action dim: 4  
======================================================================  
Episode  100 | Reward: -155.24 | Eval: -885.72 | Loss: 52.9057 | Q:   0.20 | ε: 0.951  
Episode  200 | Reward: -148.85 | Eval:  -85.94 | Loss: 37.2449 | Q:   2.14 | ε: 0.905  
Episode  300 | Reward: -111.61 | Eval: -172.48 | Loss: 37.4279 | Q:   3.52 | ε: 0.861  
Episode  400 | Reward:  -99.21 | Eval: -198.43 | Loss: 41.5296 | Q:   8.15 | ε: 0.819  
Episode  500 | Reward:  -80.75 | Eval: -103.26 | Loss: 56.2701 | Q:  11.70 | ε: 0.779  
...  
Episode 3200 | Reward:  102.04 | Eval:  159.71 | Loss: 16.5263 | Q:  27.94 | ε: 0.202  
Episode 3300 | Reward:  140.37 | Eval:  191.79 | Loss: 22.5564 | Q:  29.81 | ε: 0.192  
Episode 3400 | Reward:  114.08 | Eval:  269.40 | Loss: 23.2846 | Q:  32.40 | ε: 0.183  
Episode 3500 | Reward:  166.33 | Eval:  244.32 | Loss: 21.8558 | Q:  32.51 | ε: 0.174  
Episode 3600 | Reward:  150.80 | Eval:  265.42 | Loss: 21.6430 | Q:  33.18 | ε: 0.165  
Episode 3700 | Reward:  148.59 | Eval:  239.56 | Loss: 23.8328 | Q:  34.65 | ε: 0.157  
Episode 3800 | Reward:  162.82 | Eval:  233.36 | Loss: 28.3445 | Q:  37.46 | ε: 0.149  
Episode 3900 | Reward:  177.70 | Eval:  259.99 | Loss: 36.2971 | Q:  40.22 | ε: 0.142  
Episode 4000 | Reward:  156.60 | Eval:  251.17 | Loss: 46.7266 | Q:  42.15 | ε: 0.135  
======================================================================  
 Training complete!

Dueling DQN:分离值和优势

很多状态下,选哪个动作其实差别不大。CartPole 里杆子刚好平衡时,向左向右都行;开车走直线方向盘微调的结果差不多;LunarLander 离地面还远的时候,引擎怎么喷影响也有限。

标准 DQN 对每个动作单独学 Q(s,a),把网络容量浪费在冗余信息上。Dueling DQN 的思路是把 Q 拆成两部分:V(s) 表示"这个状态本身值多少",A(s,a) 表示"这个动作比平均水平好多少"。

架构如下

 标准 DQN:  
 Input -> Hidden Layers -> Q(s,a₁), Q(s,a₂), ..., Q(s,aₙ)  

Dueling DQN:  
                       |-> Value Stream -> V(s)  
Input -> Shared Layers |  
                       |-> Advantage Stream -> A(s,a₁), A(s,a₂), ..., A(s,aₙ)  

 Q(s,a) = V(s) + (A(s,a) - mean(A(s,·)))

为什么要减去均值?不减的话,任何常数加到 V 再从 A 减掉,得到的 Q 完全一样,网络学不出唯一解。

数学表达如下:

 Q(s,a) = V(s) + A(s,a) - (1/|A|)·Σₐ' A(s,a')

也可以用 max 代替 mean:

 Q(s,a) = V(s) + A(s,a) - maxₐ' A(s,a')

实践中 max 版本有时效果更好。

举个例子:V(s) = 10,好动作的 A 是 +5,差动作的 A 是 -3,平均优势 = (+5-3)/2 = +1。那么 Q(s, 好动作) = 10 + 5 - 1 = 14,Q(s, 差动作) = 10 - 3 - 1 = 6。

实现

 classDuelingQNetwork(nn.Module):  
    """  
    Dueling DQN 架构,分离值和优势。  

    理论: Q(s,a) = V(s) + A(s,a) - mean(A(s,·))  
    """  

    def__init__(  
        self,  
        state_dim: int,  
        action_dim: int,  
        hidden_dims: List[int] = [128, 128]  
    ):  
        """  
        初始化 Dueling Q 网络。  

        Args:  
            state_dim: 状态空间维度  
            action_dim: 动作数量  
            hidden_dims: 共享层大小  
        """  
        super(DuelingQNetwork, self).__init__()  

        self.state_dim=state_dim  
        self.action_dim=action_dim  

        # 共享特征提取器  
        shared_layers= []  
        input_dim=state_dim  

        forhidden_diminhidden_dims:  
            shared_layers.append(nn.Linear(input_dim, hidden_dim))  
            shared_layers.append(nn.ReLU())  
            input_dim=hidden_dim  

        self.shared_network=nn.Sequential(*shared_layers)  

        # 值流: V(s) = 状态的标量值  
        self.value_stream=nn.Sequential(  
            nn.Linear(hidden_dims[-1], 128),  
            nn.ReLU(),  
            nn.Linear(128, 1)  
        )  

        # 优势流: A(s,a) = 每个动作的优势  
        self.advantage_stream=nn.Sequential(  
            nn.Linear(hidden_dims[-1], 128),  
            nn.ReLU(),  
            nn.Linear(128, action_dim)  
        )  

        # 初始化权重  
        self.apply(self._init_weights)  

    def_init_weights(self, module):  
        """初始化网络权重。"""  
        ifisinstance(module, nn.Linear):  
            nn.init.kaiming_normal_(module.weight, nonlinearity='relu')  
            nn.init.constant_(module.bias, 0.0)  

    defforward(self, state: torch.Tensor) ->torch.Tensor:  
        """  
        通过 dueling 架构的前向传播。  

        Args:  
            state: 状态批次, 形状 (batch_size, state_dim)  

        Returns:  
            q_values: 所有动作的 Q(s,a), 形状 (batch_size, action_dim)  
        """  
        # 共享特征  
        features=self.shared_network(state)  

        # 值: V(s) -> 形状 (batch_size, 1)  
        value=self.value_stream(features)  

        # 优势: A(s,a) -> 形状 (batch_size, action_dim)  
        advantages=self.advantage_stream(features)  

        # 组合: Q(s,a) = V(s) + A(s,a) - mean(A(s,·))  
        q_values=value+advantages-advantages.mean(dim=1, keepdim=True)  

        returnq_values  

    defget_action(self, state: np.ndarray, epsilon: float=0.0) ->int:  
        """  
        使用 ε-greedy 策略选择动作。  
        """  
        ifrandom.random() <epsilon:  
            returnrandom.randint(0, self.action_dim-1)  
        else:  
            withtorch.no_grad():  
                state_tensor=torch.FloatTensor(state).unsqueeze(0).to(  
                    next(self.parameters()).device  
                )  
                q_values=self.forward(state_tensor)  
                 returnq_values.argmax(dim=1).item()

Dueling 架构的好处:在动作影响不大的状态下学得更好,梯度流动更通畅所以收敛更快,值估计也更稳健。

还可以把两种改进叠在一起,做成Double Dueling DQN

 classDoubleDuelingDQNAgent(DoubleDQNAgent):  
    """  
    结合 Double DQN 和 Dueling DQN 的智能体。  
    """  

    def__init__(  
        self,  
        state_dim: int,  
        action_dim: int,  
        hidden_dims: List[int] = [128, 128],  
        **kwargs  
    ):  
        """  
        初始化 Double Dueling DQN 智能体。  
        使用 DuelingQNetwork 而不是标准 QNetwork。  
        """  
        # 暂不调用 super().__init__()  
        # 我们需要以不同方式设置网络  

        self.state_dim=state_dim  
        self.action_dim=action_dim  
        self.gamma=kwargs.get('gamma', 0.99)  
        self.batch_size=kwargs.get('batch_size', 64)  
        self.target_update_freq=kwargs.get('target_update_freq', 10)  
        self.device=torch.device(kwargs.get('device', 'cpu'))  

        # 探索  
        self.epsilon=kwargs.get('epsilon_start', 1.0)  
        self.epsilon_end=kwargs.get('epsilon_end', 0.01)  
        self.epsilon_decay=kwargs.get('epsilon_decay', 0.995)  

        # 使用 Dueling 架构  
        self.q_network=DuelingQNetwork(  
            state_dim, action_dim, hidden_dims  
        ).to(self.device)  

        self.target_network=DuelingQNetwork(  
            state_dim, action_dim, hidden_dims  
        ).to(self.device)  

        self.target_network.load_state_dict(self.q_network.state_dict())  
        self.target_network.eval()  

        # 优化器  
        learning_rate=kwargs.get('learning_rate', 1e-3)  
        self.optimizer=torch.optim.Adam(self.q_network.parameters(), lr=learning_rate)  

        # 回放缓冲区  
        buffer_capacity=kwargs.get('buffer_capacity', 100000)  
        self.replay_buffer=ReplayBuffer(buffer_capacity)  

        # 统计  
        self.episode_count=0  
        self.training_step=0  

     # update() 方法继承自 DoubleDQNAgent

优先经验回放

不是所有经验都同等有价值。TD 误差大的转换说明预测偏离现实,能学到东西;TD 误差小的转换说明已经学得差不多了再采到也没多大用。

均匀采样把所有转换一视同仁,浪费了学习机会。优先经验回放的思路是:让重要的转换被采到的概率更高。

优先级怎么算

 pᵢ = |δᵢ| + ε  

 其中:  
 δᵢ = r + γ·max Q(s',a') - Q(s,a)   (TD 误差)  
 ε = 小常数,保证所有转换都有被采到的可能

采样概率:

  P(i) = pᵢ^α / Σⱼ pⱼ^α  

 α 控制优先化程度:  
 α = 0 -> 退化成均匀采样  
 α = 1 -> 完全按优先级比例采样

优先采样改了数据分布,会引入偏差。所以解决办法是用重要性采样比率来加权更新:

 wᵢ = (N · P(i))^(-β)  

 β 控制校正力度:  
 β = 0 -> 不校正  
 β = 1 -> 完全校正

通常 β 从 0.4 开始,随训练逐渐增大到 1.0。

实现

 classPrioritizedReplayBuffer:  
    """  
    优先经验回放缓冲区。  

    理论: 按 TD 误差比例采样转换。  
    我们可以从中学到更多的转换会被更频繁地采样。  
    """  

    def__init__(self, capacity: int, alpha: float=0.6, beta: float=0.4):  
        """  
        Args:  
            capacity: 缓冲区最大容量  
            alpha: 优先化指数(0=均匀, 1=比例)  
            beta: 重要性采样指数(退火到 1.0)  
        """  
        self.capacity=capacity  
        self.alpha=alpha  
        self.beta=beta  
        self.beta_increment=0.001  # 随时间退火 beta  

        self.buffer= []  
        self.priorities=np.zeros(capacity, dtype=np.float32)  
        self.position=0  

    defpush(self, state, action, reward, next_state, done):  
        """  
        以最大优先级添加转换。  

        理论: 新转换获得最大优先级(会很快被采样)。  
        它们的实际优先级在首次 TD 误差计算后更新。  
        """  
        max_priority=self.priorities.max() ifself.bufferelse1.0  

        iflen(self.buffer) <self.capacity:  
            self.buffer.append((state, action, reward, next_state, done))  
        else:  
            self.buffer[self.position] = (state, action, reward, next_state, done)  

        self.priorities[self.position] =max_priority  
        self.position= (self.position+1) %self.capacity  

    defsample(self, batch_size: int):  
        """  
        按优先级比例采样批次。  

        Returns:  
            batch: 采样的转换  
            indices: 采样转换的索引(用于优先级更新)  
            weights: 重要性采样权重  
        """  
        iflen(self.buffer) ==self.capacity:  
            priorities=self.priorities  
        else:  
            priorities=self.priorities[:len(self.buffer)]  

        # 计算采样概率  
        probs=priorities**self.alpha  
        probs/=probs.sum()  

        # 采样索引  
        indices=np.random.choice(len(self.buffer), batch_size, p=probs, replace=False)  

        # 获取转换  
        batch= [self.buffer[idx] foridxinindices]  

        # 计算重要性采样权重  
        total=len(self.buffer)  
        weights= (total*probs[indices]) ** (-self.beta)  
        weights/=weights.max()  # 归一化以保持稳定性  

        # 退火 beta  
        self.beta=min(1.0, self.beta+self.beta_increment)  

        # 转换为 tensor  
        states, actions, rewards, next_states, dones=zip(*batch)  

        states=torch.FloatTensor(np.array(states))  
        actions=torch.LongTensor(actions)  
        rewards=torch.FloatTensor(rewards)  
        next_states=torch.FloatTensor(np.array(next_states))  
        dones=torch.FloatTensor(dones)  
        weights=torch.FloatTensor(weights)  

        return (states, actions, rewards, next_states, dones), indices, weights  

    defupdate_priorities(self, indices, td_errors):  
        """  
        根据 TD 误差更新优先级。  

        Args:  
            indices: 采样转换的索引  
            td_errors: 那些转换的 TD 误差  
        """  
        foridx, td_errorinzip(indices, td_errors):  
            self.priorities[idx] =abs(td_error) +1e-6  

    def__len__(self):  
         returnlen(self.buffer)

生产环境会用 sum-tree 数据结构,采样复杂度是 O(log N) 而不是这里的 O(N)。这个简化版本以可读性为优先。

DQN 变体对比

几个变体各自解决什么问题呢?

DQN 是基线,用单一网络选动作、评估动作。它引入了目标网络来稳定"移动目标"问题,但容易过估计 Q 值,噪声让智能体去追逐根本不存在的"幽灵奖励"。

Double DQN 把选和评拆开。在线网络选动作,目标网络评估价值。实测下来能有效压低不切实际的 Q 值,学习曲线明显更平滑。

Dueling DQN 换了网络架构,单独学 V(s) 和 A(s,a)。它的核心认知是:很多状态下具体动作的影响不大。在 LunarLander 这种存在大量"冗余动作"的环境里,样本效率提升明显——不用为每次引擎脉冲都重新学状态值。

Double Dueling DQN 把两边的好处结合起来,既减少估计噪声,又提高表示效率。实测中这个组合最稳健,达到峰值性能的速度和可靠性都优于单一改进。

实践建议

变体选择对比

Double DQN 跑得比 DQN 还差?可能是训练不够长(Double DQN 起步偶尔慢一点),或者目标网络更新太频繁,或者学习率偏高。这时可以将训练时间翻倍,target_update_freq 调大,学习率砍 2-5 倍。

Dueling 架构没带来改善?可能是环境本身不适合(所有状态都很关键),或者网络太小,或者值流/优势流太浅。需要对网络加宽加深,确认环境里确实有"中性"状态。

PER 导致不稳定?可能是 β 退火太快、α 设太高、重要性采样权重没归一化。可以减慢 β 增量、α 降到 0.4-0.6、确认权重做了归一化。

首选 Double DQN 起步,代码改动极小,收益明确,没有额外复杂度。

什么时候加 Dueling:状态值比动作优势更重要的环境,大量状态下动作值差不多,需要更快收敛。

什么时候加 PER:样本效率至关重要,有算力预算(PER 比均匀采样慢),奖励稀疏(帮助关注少见的成功经验)。

最后Rainbow 把六项改进叠在一起:Double DQN、Dueling DQN、优先经验回放、多步学习(n-step returns)、分布式 RL(C51)、噪声网络(参数空间探索)。

多步学习把 1-step TD 换成 n-step 回报:

 # 1-step TD:  
 y = rₜ + γ·max Q(sₜ₊₁, a)  

 # n-step:  
 y = rₜ + γ·rₜ₊₁ + γ²·rₜ₊₂ + ... + γⁿ·max Q(sₜ₊ₙ, a)

好处是信用分配更清晰,学习更快。

小结

这篇文章从 DQN 的过估计问题讲起,沿着 Double DQN、Dueling 架构、优先经验回放等等介绍下来,每种改进对应一个具体的失败模式:max 算子的偏差、低效的状态-动作表示、浪费的均匀采样。

从头实现这些方法,能搞清楚它们为什么有效;很多"高级" RL 算法不过是简单想法的组合,理解这些想法本身才是真正可扩展的东西。

https://avoid.overfit.cn/post/4c5835f419d840b0acb0a1eb72f92b6f
作者: Jugal Gajjar

目录
相关文章
|
2天前
|
人工智能 自然语言处理 Shell
🦞 如何在 Moltbot 配置阿里云百炼 API
本教程指导用户在开源AI助手Clawdbot中集成阿里云百炼API,涵盖安装Clawdbot、获取百炼API Key、配置环境变量与模型参数、验证调用等完整流程,支持Qwen3-max thinking (Qwen3-Max-2026-01-23)/Qwen - Plus等主流模型,助力本地化智能自动化。
🦞 如何在 Moltbot 配置阿里云百炼 API
|
6天前
|
人工智能 API 开发者
Claude Code 国内保姆级使用指南:实测 GLM-4.7 与 Claude Opus 4.5 全方案解
Claude Code是Anthropic推出的编程AI代理工具。2026年国内开发者可通过配置`ANTHROPIC_BASE_URL`实现本地化接入:①极速平替——用Qwen Code v0.5.0或GLM-4.7,毫秒响应,适合日常编码;②满血原版——经灵芽API中转调用Claude Opus 4.5,胜任复杂架构与深度推理。
|
10天前
|
JSON API 数据格式
OpenCode入门使用教程
本教程介绍如何通过安装OpenCode并配置Canopy Wave API来使用开源模型。首先全局安装OpenCode,然后设置API密钥并创建配置文件,最后在控制台中连接模型并开始交互。
4633 8
|
16天前
|
人工智能 JavaScript Linux
【Claude Code 全攻略】终端AI编程助手从入门到进阶(2026最新版)
Claude Code是Anthropic推出的终端原生AI编程助手,支持40+语言、200k超长上下文,无需切换IDE即可实现代码生成、调试、项目导航与自动化任务。本文详解其安装配置、四大核心功能及进阶技巧,助你全面提升开发效率,搭配GitHub Copilot使用更佳。
10442 22
|
3天前
|
人工智能 自然语言处理 Cloud Native
大模型应用落地实战:从Clawdbot到实在Agent,如何构建企业级自动化闭环?
2026年初,开源AI Agent Clawdbot爆火,以“自由意志”打破被动交互,寄生社交软件主动服务。它解决“听与说”,却缺“手与脚”:硅谷Manus走API原生路线,云端自主执行;中国实在Agent则用屏幕语义理解,在封闭系统中精准操作。三者协同,正构建AI真正干活的三位一体生态。
2370 9
|
1天前
|
存储 安全 数据库
使用 Docker 部署 Clawdbot(官方推荐方式)
Clawdbot 是一款开源、本地运行的个人AI助手,支持 WhatsApp、Telegram、Slack 等十余种通信渠道,兼容 macOS/iOS/Android,可渲染实时 Canvas 界面。本文提供基于 Docker Compose 的生产级部署指南,涵盖安全配置、持久化、备份、监控等关键运维实践(官方无预构建镜像,需源码本地构建)。
1317 3
|
1天前
|
机器人 API 数据安全/隐私保护
只需3步,无影云电脑一键部署Moltbot(Clawdbot)
本指南详解Moltbot(Clawdbot)部署全流程:一、购买无影云电脑Moltbot专属套餐(含2000核时);二、下载客户端并配置百炼API Key、钉钉APP KEY及QQ通道;三、验证钉钉/群聊交互。支持多端,7×24运行可关闭休眠。
2161 2
|
18天前
|
存储 人工智能 自然语言处理
OpenSpec技术规范+实例应用
OpenSpec 是面向 AI 智能体的轻量级规范驱动开发框架,通过“提案-审查-实施-归档”工作流,解决 AI 编程中的需求偏移与不可预测性问题。它以机器可读的规范为“单一真相源”,将模糊提示转化为可落地的工程实践,助力开发者高效构建稳定、可审计的生产级系统,实现从“凭感觉聊天”到“按规范开发”的跃迁。
2621 18
|
10天前
|
人工智能 前端开发 Docker
Huobao Drama 开源短剧生成平台:从剧本到视频
Huobao Drama 是一个基于 Go + Vue3 的开源 AI 短剧自动化生成平台,支持剧本解析、角色与分镜生成、图生视频及剪辑合成,覆盖短剧生产全链路。内置角色管理、分镜设计、视频合成、任务追踪等功能,支持本地部署与多模型接入(如 OpenAI、Ollama、火山等),搭配 FFmpeg 实现高效视频处理,适用于短剧工作流验证与自建 AI 创作后台。
1409 6