【机器学习基础算法:从零实现到精通】第14章 深度强化学习

一切皆是映射:值函数与策略函数:深度强化学习的理论基础 1. 背景介绍 1.1 强化学习概述 强化学习 (Reinforcement Learning, RL) 是一种机器学习范式,它关注的是智能体 (agent) 如何在环境中采取行动以最大化累积奖励。与监督学习和无监督学习不同,强化学习不需要标记数据或预定义的目标函数,而是通过与环境的交互来学习。 阅读详情

目录

第14章 深度强化学习

14.1 马尔可夫决策过程与值函数方法

14.1.1 MDP的贝尔曼方程与最优性原理,值迭代与策略迭代的收敛性分析

14.1.2 深度Q网络(DQN):经验回放的优先级采样与目标网络的软更新

14.1.3 DQN改进:Double DQN的解耦选择与评估、Dueling DQN的优势流与值流分离,Rainbow的六重整合

14.2 策略梯度与Actor-Critic

14.2.1 REINFORCE算法的蒙特卡洛回报估计与基线减方差,A2C/A3C的同步与异步训练、熵正则化

14.2.2 PPO(近端策略优化)的裁剪替代目标与自适应KL惩罚,GAE(广义优势估计)的λ-回报权衡

14.2.3 Soft Actor-Critic(SAC)的最大熵框架与温度参数自动调整,确定性策略梯度(DDPG/TD3)的目标策略平滑与延迟更新

14.3 离线强化学习与世界模型

14.3.1 分布偏移与外推误差:CQL(保守Q学习)的悲观值估计与正则项,IQL(隐式Q学习)的期望回归与AWR(优势加权回归)

14.3.2 基于模型的RL:MuZero的MCTS规划与潜在动力学模型,Dreamer的RSSM(循环状态空间模型)与想象轨迹训练

14.3.3 离线到在线(Offline-to-Online)微调的策略约束(AWAC/IQL)与不确定性加权



第14章 深度强化学习

14.1 马尔可夫决策过程与值函数方法

14.1.1 MDP的贝尔曼方程与最优性原理,值迭代与策略迭代的收敛性分析

马尔可夫决策过程为序列决策提供了数学框架,其中智能体通过观察状态、执行动作、接收奖励与环境交互。贝尔曼方程建立了状态值函数的自一致性关系,表明当前状态的长期回报期望等于即时奖励加上 discounted 的未来回报期望。这一递归结构将无限时域的优化问题转化为可通过迭代求解的局部一致性约束。贝尔曼最优性原理进一步指出,最优策略具有子结构最优性,即无论初始状态或先前决策如何,剩余决策必须构成最优策略。

值迭代通过反复应用贝尔曼最优算子更新值函数估计,每轮迭代中每个状态的值被更新为即时奖励加上后继状态值的最大期望。该过程在无限轮迭代后收敛至最优值函数,收敛速率由折扣因子决定,形成几何级数收缩。策略迭代交替执行策略评估(在固定策略下计算精确值函数)与策略改进(基于当前值函数贪婪地选择动作),由于策略改进步骤保证单调提升且有限策略空间的存在,算法在有限步内收敛至最优策略。相比值迭代的渐进收敛,策略迭代通常需要更少的外部循环但每个循环计算成本更高。

脚本:mdp_bellman_iteration.py

Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
脚本内容:MDP贝尔曼方程、值迭代与策略迭代实现与收敛性分析
使用方式:直接运行 python mdp_bellman_iteration.py
依赖包:numpy, matplotlib, scipy
功能说明:
1. 实现MDP环境(GridWorld)与贝尔曼方程求解
2. 实现值迭代(Value Iteration)与策略迭代(Policy Iteration)
3. 分析两种算法的收敛速率与计算效率
4. 可视化值函数演化与策略改进过程
"""

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Arrow, Rectangle
import matplotlib.patches as mpatches


class GridWorldMDP:
    """GridWorld MDP环境实现"""
    
    def __init__(self, grid_size=8, discount_factor=0.95):
        self.grid_size = grid_size
        self.n_states = grid_size * grid_size
        self.n_actions = 4  # 上、下、左、右
        self.gamma = discount_factor
        
        # 状态映射:(i,j) -> index
        self.states = [(i, j) for i in range(grid_size) for j in range(grid_size)]
        self.state_to_idx = {s: i for i, s in enumerate(self.states)}
        
        # 定义障碍物与目标
        self.obstacles = [(2, 2), (2, 3), (3, 2), (5, 5), (5, 6), (6, 5)]
        self.goal = (7, 7)
        self.trap = (7, 0)
        
        # 转移概率与奖励函数
        self.P = self._build_transition_matrix()
        self.R = self._build_reward_matrix()
        
    def _build_transition_matrix(self):
        """构建转移概率矩阵 P[state, action, next_state]"""
        P = np.zeros((self.n_states, self.n_actions, self.n_states))
        
        # 动作效果:[上, 下, 左, 右]
        actions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        
        for s_idx, (i, j) in enumerate(self.states):
            for a_idx, (di, dj) in enumerate(actions):
                # 计算下一个状态(考虑随机性:80%按意图,20%随机)
                next_i, next_j = i + di, j + dj
                
                # 边界检查与障碍物
                if (next_i, next_j) in self.obstacles or not (0 <= next_i < self.grid_size and 0 <= next_j < self.grid_size):
                    next_i, next_j = i, j  # 撞墙停留原地
                
                next_s_idx = self.state_to_idx[(next_i, next_j)]
                
                # 确定性转移(简化版)
                P[s_idx, a_idx, next_s_idx] = 1.0
        
        return P
    
    def _build_reward_matrix(self):
        """构建奖励函数 R[state, action, next_state]"""
        R = np.zeros((self.n_states, self.n_actions, self.n_states))
        
        for s_idx, (i, j) in enumerate(self.states):
            for a_idx in range(self.n_actions):
                for next_s_idx in range(self.n_states):
                    next_i, next_j = self.states[next_s_idx]
                    
                    # 目标状态奖励
                    if (next_i, next_j) == self.goal:
                        R[s_idx, a_idx, next_s_idx] = 10.0
                    elif (next_i, next_j) == self.trap:
                        R[s_idx, a_idx, next_s_idx] = -10.0
                    elif (next_i, next_j) in self.obstacles:
                        R[s_idx, a_idx, next_s_idx] = -1.0
                    else:
                        R[s_idx, a_idx, next_s_idx] = -0.04  # 每步小额负奖励鼓励快速到达
        
        return R
    
    def bellman_backup(self, V, policy=None):
        """
        贝尔曼备份操作
        如果policy为None,执行最优贝尔曼备份(max)
        否则执行策略评估备份(按policy加权)
        """
        if policy is None:
            # 值迭代的贝尔曼最优算子
            Q = np.sum(self.P * (self.R + self.gamma * V[np.newaxis, np.newaxis, :]), axis=2)
            return np.max(Q, axis=1), np.argmax(Q, axis=1)
        else:
            # 策略评估的贝尔曼算子
            Q = np.sum(self.P * (self.R + self.gamma * V[np.newaxis, np.newaxis, :]), axis=2)
            return np.sum(policy * Q, axis=1), None
    
    def value_iteration(self, max_iter=100, threshold=1e-6):
        """值迭代算法"""
        V = np.zeros(self.n_states)
        V_history = [V.copy()]
        policy_history = []
        errors = []
        
        for iteration in range(max_iter):
            V_new, policy = self.bellman_backup(V, policy=None)
            
            error = np.max(np.abs(V_new - V))
            errors.append(error)
            
            V = V_new
            V_history.append(V.copy())
            policy_history.append(policy.copy())
            
            if error < threshold:
                print(f"值迭代在{iteration+1}轮后收敛(误差{error:.2e})")
                break
        
        return V, policy, V_history, policy_history, errors
    
    def policy_iteration(self, max_iter=20, eval_iter=50, eval_threshold=1e-6):
        """策略迭代算法"""
        # 随机初始化策略
        policy = np.ones((self.n_states, self.n_actions)) / self.n_actions
        policy_history = [policy.copy()]
        V_history = []
        policy_changes = []
        
        for iteration in range(max_iter):
            # 策略评估
            V = np.zeros(self.n_states)
            for _ in range(eval_iter):
                V_new, _ = self.bellman_backup(V, policy=policy)
                if np.max(np.abs(V_new - V)) < eval_threshold:
                    break
                V = V_new
            
            V_history.append(V.copy())
            
            # 策略改进
            _, new_policy_det = self.bellman_backup(V, policy=None)
            new_policy = np.eye(self.n_actions)[new_policy_det]
            
            # 统计策略变化
            changes = np.sum(np.abs(new_policy - policy)) / 2
            policy_changes.append(changes)
            
            # 检查收敛
            if np.array_equal(new_policy_det, np.argmax(policy, axis=1)):
                print(f"策略迭代在{iteration+1}轮后收敛")
                break
            
            policy = new_policy
            policy_history.append(policy.copy())
        
        return V, new_policy_det, V_history, policy_history, policy_changes


def visualize_mdp_algorithms():
    """可视化MDP算法"""
    
    # 创建MDP
    mdp = GridWorldMDP(grid_size=8, discount_factor=0.95)
    
    fig, axes = plt.subplots(2, 3, figsize=(16, 10))
    
    # 1. GridWorld环境可视化
    ax1 = axes[0, 0]
    grid = np.zeros((8, 8))
    
    # 标记特殊状态
    for obs in mdp.obstacles:
        grid[obs] = -1
    grid[mdp.goal] = 2
    grid[mdp.trap] = -2
    
    im1 = ax1.imshow(grid, cmap='RdYlGn', vmin=-2, vmax=2, alpha=0.6)
    ax1.set_title('GridWorld Environment\nRed: Trap, Green: Goal, Gray: Obstacles',
                 fontsize=12, fontweight='bold')
    
    # 添加网格线
    for i in range(9):
        ax1.axhline(i-0.5, color='black', linewidth=0.5)
        ax1.axvline(i-0.5, color='black', linewidth=0.5)
    
    # 2. 值迭代收敛曲线
    ax2 = axes[0, 1]
    V_opt, policy_opt, V_hist, pol_hist, errors = mdp.value_iteration(max_iter=50)
    
    # 绘制误差收敛(对数坐标)
    ax2.semilogy(range(len(errors)), errors, 'o-', color='navy', linewidth=2, markersize=6)
    ax2.axhline(y=1e-6, color='red', linestyle='--', alpha=0.5, label='Convergence Threshold')
    ax2.set_xlabel('Iteration', fontsize=11)
    ax2.set_ylabel('Bellman Error (max|V-V\'|)', fontsize=11)
    ax2.set_title('Value Iteration Convergence\nGeometric Contraction Rate',
                 fontsize=12, fontweight='bold')
    ax2.legend()
    ax2.grid(True, alpha=0.3)
    
    # 添加收敛速度注释
    if len(errors) > 5:
        # 计算近似收缩率
        rate = errors[-1] / errors[-5] if errors[-5] > 0 else 0
        ax2.text(0.5, 0.9, f'Approx. Contraction Rate: {rate:.3f}',
                transform=ax2.transAxes, fontsize=10,
                bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.7))
    
    # 3. 策略迭代vs值迭代对比
    ax3 = axes[0, 2]
    
    # 策略迭代
    V_pi, policy_pi, V_hist_pi, pol_hist_pi, pol_changes = mdp.policy_iteration(max_iter=15)
    
    # 绘制策略稳定性(策略变化数)
    ax3.plot(range(len(pol_changes)), pol_changes, 's-', color='forestgreen', 
            linewidth=2, markersize=8, label='Policy Iteration (Policy Changes)')
    
    # 值迭代的策略变化(间接计算)
    pol_changes_vi = []
    for i in range(1, len(pol_hist)):
        changes = np.sum(pol_hist[i] != pol_hist[i-1])
        pol_changes_vi.append(changes)
    
    # 由于值迭代和策略迭代长度可能不同,取前min_len个
    min_len = min(len(pol_changes), len(pol_changes_vi))
    ax3.plot(range(min_len), pol_changes_vi[:min_len], 'o-', color='coral',
            linewidth=2, markersize=6, label='Value Iteration (Policy Changes)')
    
    ax3.set_xlabel('Iteration', fontsize=11)
    ax3.set_ylabel('Number of State Policy Changes', fontsize=11)
    ax3.set_title('Policy Stability: VI vs PI\nPI reaches stability faster',
                 fontsize=12, fontweight='bold')
    ax3.legend()
    ax3.grid(True, alpha=0.3)
    
    # 4. 最优值函数热力图
    ax4 = axes[1, 0]
    V_grid = V_opt.reshape(8, 8)
    
    im4 = ax4.imshow(V_grid, cmap='viridis', interpolation='nearest')
    ax4.set_title('Optimal Value Function V*(s)\nLearned by Value Iteration',
                 fontsize=12, fontweight='bold')
    plt.colorbar(im4, ax=ax4, label='Value')
    
    # 添加策略箭头
    for i in range(8):
        for j in range(8):
            if (i, j) not in mdp.obstacles:
                s_idx = mdp.state_to_idx[(i, j)]
                action = policy_opt[s_idx]
                arrows = [(-0.3, 0), (0.3, 0), (0, -0.3), (0, 0.3)]  # 上、下、左、右
                di, dj = arrows[action]
                ax4.annotate('', xy=(j+0.3*di*2, i+0.3*dj*2), xytext=(j, i),
                            arrowprops=dict(arrowstyle='->', color='red', lw=1.5))
    
    # 5. 贝尔曼算子收缩可视化
    ax5 = axes[1, 1]
    
    # 模拟不同折扣因子下的收缩
    gammas = [0.5, 0.7, 0.9, 0.95, 0.99]
    iterations = range(20)
    
    for gamma in gammas:
        # 几何收敛:error_t = gamma^t * error_0
        errors_sim = [gamma**t for t in iterations]
        ax5.semilogy(iterations, errors_sim, 'o-', label=f'γ={gamma}', 
                    markersize=4, alpha=0.8)
    
    ax5.set_xlabel('Iteration', fontsize=11)
    ax5.set_ylabel('Error Upper Bound', fontsize=11)
    ax5.set_title('Bellman Operator Contraction\nGeometric Convergence Rate ∝ γ^t',
                 fontsize=12, fontweight='bold')
    ax5.legend(loc='upper right')
    ax5.grid(True, alpha=0.3)
    
    # 6. 计算效率对比(迭代次数vs每次迭代时间)
    ax6 = axes[1, 2]
    
    # 模拟不同规模MDP下的计算效率
    sizes = [4, 6, 8, 10, 12]
    vi_iters = [15, 25, 35, 45, 55]  # 值迭代平均收敛轮数
    pi_iters = [4, 6, 8, 10, 12]     # 策略迭代外部循环轮数
    pi_eval_cost = [s*s*10 for s in sizes]  # 每次策略评估的迭代成本
    
    # 总计算量(假设单位)
    vi_total = [vi_iters[i] * sizes[i]**2 for i in range(len(sizes))]
    pi_total = [pi_iters[i] * pi_eval_cost[i] for i in range(len(sizes))]
    
    x = np.arange(len(sizes))
    width = 0.35
    
    ax6.bar(x - width/2, vi_total, width, label='Value Iteration', color='coral', alpha=0.8)
    ax6.bar(x + width/2, pi_total, width, label='Policy Iteration', color='forestgreen', alpha=0.8)
    
    ax6.set_xlabel('Grid Size', fontsize=11)
    ax6.set_ylabel('Total Computation (arb. units)', fontsize=11)
    ax6.set_title('Computational Efficiency: VI vs PI\nPI converges faster but each iteration costs more',
                 fontsize=12, fontweight='bold')
    ax6.set_xticks(x)
    ax6.set_xticklabels([f'{s}x{s}' for s in sizes])
    ax6.legend()
    ax6.set_yscale('log')
    
    plt.tight_layout()
    plt.savefig('mdp_bellman_analysis.png', dpi=150, bbox_inches='tight')
    print("可视化已保存至 mdp_bellman_analysis.png")
    plt.show()


if __name__ == "__main__":
    visualize_mdp_algorithms()

14.1.2 深度Q网络(DQN):经验回放的优先级采样与目标网络的软更新

深度Q网络通过参数化神经网络近似动作值函数,克服了传统表格方法在高维状态空间中的维度灾难。网络以环境状态为输入,输出所有离散动作的估计值,通过时序差分误差进行端到端训练。这种函数逼近使得 agents 能够直接从高维视觉输入学习控制策略,无需手工设计的特征工程。

经验回放机制存储交互历史中的转移样本于固定容量的回放缓冲区,训练时从中随机采样小批量数据打破时间相关性,提高样本利用效率并平滑数据分布变化。优先级采样扩展了均匀采样策略,根据时序差分误差的绝对值为每条转移分配采样概率,使网络更频繁地学习那些预测偏差较大的"惊喜"样本,加速收敛并提升最终策略性能。目标网络通过维护一份延迟更新的参数副本来计算TD目标值,避免自举过程中目标值与当前估计之间的追逐效应,软更新策略以混合系数逐步将在线网络参数滑动平均至目标网络,保证学习动态稳定性。

脚本:dqn_advanced_mechanisms.py

Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
脚本内容:DQN核心机制实现(经验回放、优先级采样、目标网络软更新)
使用方式:直接运行 python dqn_advanced_mechanisms.py
依赖包:torch, numpy, matplotlib, gymnasium(或gym)
功能说明:
1. 实现Prioritized Experience Replay(SumTree结构)
2. 实现DQN与Double DQN网络架构
3. 实现目标网络的硬更新与软更新(Polyak averaging)
4. 可视化TD误差分布、优先级演化与值函数稳定性
"""

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
from collections import deque, namedtuple
import random
import math


class SumTree:
    """Sum Tree数据结构用于高效优先级采样"""
    
    def __init__(self, capacity):
        self.capacity = capacity
        self.tree = np.zeros(2 * capacity - 1)  # 完全二叉树
        self.data = np.zeros(capacity, dtype=object)
        self.write_idx = 0
        self.size = 0
    
    def _propagate(self, idx, change):
        """向上传播优先级变化"""
        parent = (idx - 1) // 2
        self.tree[parent] += change
        if parent != 0:
            self._propagate(parent, change)
    
    def _retrieve(self, idx, s):
        """根据累积和检索样本"""
        left = 2 * idx + 1
        right = 2 * idx + 2
        
        if left >= len(self.tree):
            return idx
        
        if s <= self.tree[left]:
            return self._retrieve(left, s)
        else:
            return self._retrieve(right, s - self.tree[left])
    
    def total(self):
        """根节点存储总优先级"""
        return self.tree[0]
    
    def add(self, priority, data):
        """添加新样本"""
        idx = self.write_idx + self.capacity - 1
        
        self.data[self.write_idx] = data
        self.update(idx, priority)
        
        self.write_idx = (self.write_idx + 1) % self.capacity
        self.size = min(self.size + 1, self.capacity)
    
    def update(self, idx, priority):
        """更新叶子节点优先级"""
        change = priority - self.tree[idx]
        self.tree[idx] = priority
        self._propagate(idx, change)
    
    def get(self, s):
        """根据值s采样(s在[0,total]之间)"""
        idx = self._retrieve(0, s)
        data_idx = idx - self.capacity + 1
        
        return idx, self.tree[idx], self.data[data_idx]
    
    def sample(self, batch_size, beta=0.4):
        """
        优先级采样
        返回:样本、索引、重要性采样权重
        """
        batch = []
        indices = []
        priorities = []
        
        # 分段采样(stratified sampling)
        segment = self.total() / batch_size
        
        for i in range(batch_size):
            a = segment * i
            b = segment * (i + 1)
            s = random.uniform(a, b)
            idx, priority, data = self.get(s)
            
            indices.append(idx)
            priorities.append(priority)
            batch.append(data)
        
        # 计算重要性采样权重
        # w_i = (N * P_i)^(-beta) / max(w)
        sampling_probs = np.array(priorities) / self.total()
        is_weights = np.power(self.size * sampling_probs, -beta)
        is_weights /= is_weights.max()  # 归一化
        
        return batch, indices, is_weights


class PrioritizedReplayBuffer:
    """带优先级的经验回放缓冲区"""
    
    def __init__(self, capacity, alpha=0.6, beta_start=0.4, beta_frames=100000):
        self.tree = SumTree(capacity)
        self.alpha = alpha  # 优先级指数(0=均匀采样,1=完全优先)
        self.beta_start = beta_start
        self.beta_frames = beta_frames
        self.frame = 0
        
        self.epsilon = 1e-6  # 避免零优先级
    
    def beta_by_frame(self, frame_idx):
        """动态调整beta(重要性采样修正强度)"""
        return min(1.0, self.beta_start + frame_idx * (1.0 - self.beta_start) / self.beta_frames)
    
    def push(self, transition):
        """存储转移,使用最大优先级初始化"""
        max_priority = self.tree.tree[self.tree.capacity-1:self.tree.capacity-1+self.tree.size].max()
        if max_priority == 0:
            max_priority = 1.0
        
        self.tree.add(max_priority ** self.alpha, transition)
    
    def sample(self, batch_size):
        """采样并返回重要性采样权重"""
        beta = self.beta_by_frame(self.frame)
        self.frame += 1
        
        batch, indices, is_weights = self.tree.sample(batch_size, beta)
        return batch, indices, is_weights
    
    def update_priorities(self, indices, td_errors):
        """根据新的TD误差更新优先级"""
        for idx, td_error in zip(indices, td_errors):
            priority = (abs(td_error) + self.epsilon) ** self.alpha
            self.tree.update(idx, priority)
    
    def __len__(self):
        return self.tree.size


class DQNNetwork(nn.Module):
    """DQN网络架构(Dueling DQN可选)"""
    
    def __init__(self, state_dim, action_dim, dueling=False):
        super().__init__()
        self.dueling = dueling
        
        # 共享特征层
        self.feature = nn.Sequential(
            nn.Linear(state_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 128),
            nn.ReLU()
        )
        
        if dueling:
            # Dueling DQN:分离价值流与优势流
            self.value_stream = nn.Sequential(
                nn.Linear(128, 64),
                nn.ReLU(),
                nn.Linear(64, 1)  # 状态值V(s)
            )
            self.advantage_stream = nn.Sequential(
                nn.Linear(128, 64),
                nn.ReLU(),
                nn.Linear(64, action_dim)  # 优势值A(s,a)
            )
        else:
            self.q_values = nn.Linear(128, action_dim)
    
    def forward(self, x):
        features = self.feature(x)
        
        if self.dueling:
            value = self.value_stream(features)
            advantages = self.advantage_stream(features)
            
            # 聚合:Q(s,a) = V(s) + (A(s,a) - mean(A(s,a')))
            # 这是Dueling DQN的关键:减去均值保持可识别性
            q_values = value + (advantages - advantages.mean(dim=1, keepdim=True))
        else:
            q_values = self.q_values(features)
        
        return q_values


class DQNAgent:
    """DQN智能体:实现Double DQN与目标网络软更新"""
    
    def __init__(self, state_dim, action_dim, 
                 dueling=False, double_dqn=True,
                 soft_update=True, tau=0.005):
        self.state_dim = state_dim
        self.action_dim = action_dim
        self.double_dqn = double_dqn
        self.soft_update = soft_update
        self.tau = tau  # 软更新系数
        
        # 在线网络(用于选择动作和计算当前Q值)
        self.policy_net = DQNNetwork(state_dim, action_dim, dueling)
        # 目标网络(用于计算TD目标)
        self.target_net = DQNNetwork(state_dim, action_dim, dueling)
        self.target_net.load_state_dict(self.policy_net.state_dict())
        self.target_net.eval()
        
        self.optimizer = optim.Adam(self.policy_net.parameters(), lr=1e-3)
        self.memory = PrioritizedReplayBuffer(capacity=10000)
        
        self.gamma = 0.99
        self.batch_size = 64
    
    def select_action(self, state, epsilon=0.1):
        """epsilon-贪婪策略"""
        if random.random() < epsilon:
            return random.randrange(self.action_dim)
        
        with torch.no_grad():
            state = torch.FloatTensor(state).unsqueeze(0)
            q_values = self.policy_net(state)
            return q_values.argmax().item()
    
    def compute_td_errors(self, batch):
        """计算TD误差(支持Double DQN)"""
        states = torch.FloatTensor([t[0] for t in batch])
        actions = torch.LongTensor([t[1] for t in batch])
        rewards = torch.FloatTensor([t[2] for t in batch])
        next_states = torch.FloatTensor([t[3] for t in batch])
        dones = torch.FloatTensor([t[4] for t in batch])
        
        # 当前Q值:Q(s,a)
        current_q = self.policy_net(states).gather(1, actions.unsqueeze(1)).squeeze()
        
        # 目标Q值计算
        with torch.no_grad():
            if self.double_dqn:
                # Double DQN:使用policy_net选择动作,target_net评估
                next_actions = self.policy_net(next_states).argmax(1)
                next_q = self.target_net(next_states).gather(1, next_actions.unsqueeze(1)).squeeze()
            else:
                # 标准DQN:直接使用target_net的最大值
                next_q = self.target_net(next_states).max(1)[0]
            
            target_q = rewards + self.gamma * next_q * (1 - dones)
        
        # TD误差
        td_errors = target_q - current_q
        
        # 带重要性采样的损失
        return td_errors, current_q, target_q
    
    def update(self):
        """执行一次参数更新"""
        if len(self.memory) < self.batch_size:
            return None
        
        batch, indices, is_weights = self.memory.sample(self.batch_size)
        td_errors, current_q, target_q = self.compute_td_errors(batch)
        
        # 重要性采样权重
        is_weights = torch.FloatTensor(is_weights)
        
        # 加权MSE损失
        loss = (is_weights * td_errors.pow(2)).mean()
        
        self.optimizer.zero_grad()
        loss.backward()
        
        # 梯度裁剪(增强稳定性)
        torch.nn.utils.clip_grad_norm_(self.policy_net.parameters(), max_norm=10)
        
        self.optimizer.step()
        
        # 更新优先级
        td_errors_np = td_errors.detach().abs().numpy()
        self.memory.update_priorities(indices, td_errors_np)
        
        # 目标网络更新
        if self.soft_update:
            # 软更新(Polyak averaging):θ_target = τ*θ_policy + (1-τ)*θ_target
            for param, target_param in zip(self.policy_net.parameters(), 
                                          self.target_net.parameters()):
                target_param.data.copy_(self.tau * param.data + (1.0 - self.tau) * target_param.data)
        else:
            # 硬更新(每隔N步复制一次)
            pass  # 简化:这里只展示软更新
        
        return loss.item(), td_errors_np.mean()


def visualize_dqn_mechanisms():
    """可视化DQN核心机制"""
    
    # 创建模拟环境(CartPole简化版)
    state_dim = 4
    action_dim = 2
    
    agent = DQNAgent(state_dim, action_dim, dueling=True, double_dqn=True, soft_update=True)
    
    # 生成模拟经验
    for i in range(1000):
        state = np.random.randn(state_dim)
        action = random.randint(0, action_dim-1)
        reward = np.random.randn()  # 随机奖励
        next_state = np.random.randn(state_dim)
        done = random.random() < 0.1
        
        agent.memory.push((state, action, reward, next_state, done))
    
    fig, axes = plt.subplots(2, 3, figsize=(16, 10))
    
    # 1. SumTree结构可视化
    ax1 = axes[0, 0]
    
    # 模拟优先级分布
    priorities = agent.memory.tree.tree[agent.memory.tree.capacity-1:]
    priorities = priorities[priorities > 0]
    
    ax1.hist(priorities, bins=30, color='steelblue', alpha=0.7, edgecolor='black')
    ax1.set_xlabel('Priority Value (TD error^α)', fontsize=11)
    ax1.set_ylabel('Frequency', fontsize=11)
    ax1.set_title('Prioritized Replay: TD Error Distribution\n'
                 'High-error transitions sampled more frequently',
                 fontsize=12, fontweight='bold')
    ax1.axvline(x=priorities.mean(), color='red', linestyle='--', 
               label=f'Mean: {priorities.mean():.2f}')
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    
    # 2. 重要性采样权重演化
    ax2 = axes[0, 1]
    
    # 模拟beta参数随时间增长的重要性采样权重修正效果
    frames = np.arange(0, 100000, 1000)
    betas = [agent.memory.beta_by_frame(f) for f in frames]
    
    # 模拟权重分布变化(beta=0时差异大,beta=1时均匀)
    weight_variance = [(1 - b) * 5 + 0.5 for b in betas]  # beta越小,方差越大
    
    ax2.plot(frames, betas, 'b-', label='β (importance sampling exponent)', linewidth=2)
    ax2_twin = ax2.twinx()
    ax2_twin.plot(frames, weight_variance, 'r--', label='Weight variance', linewidth=2)
    
    ax2.set_xlabel('Training Frame', fontsize=11)
    ax2.set_ylabel('Beta Value', fontsize=11, color='blue')
    ax2_twin.set_ylabel('Weight Variance', fontsize=11, color='red')
    ax2.set_title('Annealing Importance Sampling\n'
                 'Early: High variance correction, Late: Uniform',
                 fontsize=12, fontweight='bold')
    ax2.legend(loc='center left')
    ax2_twin.legend(loc='center right')
    ax2.grid(True, alpha=0.3)
    
    # 3. 目标网络软更新可视化
    ax3 = axes[0, 2]
    
    # 模拟两个网络参数的收敛过程
    iterations = 100
    tau = agent.tau
    
    # 策略网络参数(快速变化)
    policy_params = [1.0] * iterations
    for i in range(1, iterations):
        policy_params[i] = policy_params[i-1] + np.random.randn() * 0.1  # 随机游走
    
    # 目标网络参数(平滑跟随)
    target_params = [1.0] * iterations
    for i in range(1, iterations):
        target_params[i] = (1 - tau) * target_params[i-1] + tau * policy_params[i]
    
    ax3.plot(range(iterations), policy_params, 'o-', label='Policy Network (Online)', 
            color='coral', alpha=0.7, markersize=4)
    ax3.plot(range(iterations), target_params, 's-', label='Target Network (Soft Update)',
            color='forestgreen', linewidth=2, markersize=4)
    
    ax3.set_xlabel('Update Step', fontsize=11)
    ax3.set_ylabel('Parameter Value', fontsize=11)
    ax3.set_title(f'Target Network Soft Update (τ={tau})\n'
                 f'Exponential Moving Average reduces chasing',
                 fontsize=12, fontweight='bold')
    ax3.legend()
    ax3.grid(True, alpha=0.3)
    
    # 4. Dueling DQN架构可视化
    ax4 = axes[1, 0]
    ax4.set_xlim(0, 10)
    ax4.set_ylim(0, 10)
    ax4.axis('off')
    ax4.set_title('Dueling DQN Architecture\nV(s) + A(s,a) - mean(A)', 
                 fontsize=12, fontweight='bold')
    
    # 输入层
    ax4.add_patch(plt.Rectangle((3, 8), 4, 1, facecolor='lightblue', edgecolor='black'))
    ax4.text(5, 8.5, 'State Features', ha='center', va='center', fontsize=10)
    
    # 共享卷积/特征层
    ax4.annotate('', xy=(5, 7), xytext=(5, 8))
    ax4.add_patch(plt.Rectangle((3, 6), 4, 1, facecolor='lightyellow', edgecolor='black'))
    ax4.text(5, 6.5, 'Shared Feature Layers', ha='center', va='center', fontsize=10)
    
    # 分流:V(s)和A(s,a)
    ax4.annotate('', xy=(2, 5), xytext=(4, 6))
    ax4.annotate('', xy=(8, 5), xytext=(6, 6))
    
    # V(s)流
    ax4.add_patch(plt.Rectangle((0.5, 3.5), 3, 1.5, facecolor='lightgreen', edgecolor='black'))
    ax4.text(2, 4.25, 'Value Stream\nV(s)', ha='center', va='center', fontsize=9)
    
    # A(s,a)流
    ax4.add_patch(plt.Rectangle((6.5, 3.5), 3, 1.5, facecolor='lightcoral', edgecolor='black'))
    ax4.text(8, 4.25, 'Advantage Stream\nA(s,a)', ha='center', va='center', fontsize=9)
    
    # 聚合层
    ax4.annotate('', xy=(5, 3), xytext=(2, 3.5))
    ax4.annotate('', xy=(5, 3), xytext=(8, 3.5))
    ax4.add_patch(plt.Circle((5, 2.5), 0.8, facecolor='plum', edgecolor='black'))
    ax4.text(5, 2.5, 'Aggregate', ha='center', va='center', fontsize=9)
    
    # 输出
    ax4.annotate('', xy=(5, 1.2), xytext=(5, 1.7))
    ax4.add_patch(plt.Rectangle((3, 0.2), 4, 1, facecolor='gold', edgecolor='black'))
    ax4.text(5, 0.7, 'Q-values: V(s) + A(s,a) - mean(A)', ha='center', va='center', fontsize=9)
    
    # 5. Double DQN vs DQN的过估计对比
    ax5 = axes[1, 1]
    
    # 模拟Q值估计过程
    true_values = np.linspace(0, 10, 20)
    noise = np.random.randn(20) * 0.5
    
    # 标准DQN:max操作导致正偏差(overestimation)
    dqn_estimates = true_values + np.abs(noise)  # max引入向上偏差
    
    # Double DQN:解耦选择与评估,减少偏差
    double_dqn_estimates = true_values + noise * 0.3  # 偏差减小
    
    ax5.plot(true_values, true_values, 'k--', label='True Values', linewidth=2)
    ax5.scatter(true_values, dqn_estimates, c='red', alpha=0.6, s=80, 
               label='Standard DQN (Overestimation)', marker='o', edgecolors='black')
    ax5.scatter(true_values, double_dqn_estimates, c='green', alpha=0.6, s=80,
               label='Double DQN (Bias Reduction)', marker='s', edgecolors='black')
    
    ax5.set_xlabel('True Q-Values', fontsize=11)
    ax5.set_ylabel('Estimated Q-Values', fontsize=11)
    ax5.set_title('Overestimation Bias: DQN vs Double DQN\n'
                 'Max operator in DQN causes systematic positive bias',
                 fontsize=12, fontweight='bold')
    ax5.legend()
    ax5.grid(True, alpha=0.3)
    
    # 添加偏差箭头
    sample_idx = 10
    ax5.annotate('', xy=(true_values[sample_idx], dqn_estimates[sample_idx]),
                xytext=(true_values[sample_idx], true_values[sample_idx]),
                arrowprops=dict(arrowstyle='->', color='red', alpha=0.7))
    ax5.text(true_values[sample_idx]+0.3, (true_values[sample_idx]+dqn_estimates[sample_idx])/2,
            'Bias', fontsize=9, color='red')
    
    # 6. 训练稳定性:目标网络vs无目标网络
    ax6 = axes[1, 2]
    
    # 模拟训练过程中的Q值波动
    episodes = 100
    # 无目标网络:高方差震荡
    no_target = np.cumsum(np.random.randn(episodes) * 0.5) + 10
    # 有目标网络:平滑收敛
    with_target = np.cumsum(np.random.randn(episodes) * 0.2) + 10
    
    # 添加趋势
    trend = np.linspace(10, 15, episodes)
    no_target = no_target * 0.3 + trend + np.random.randn(episodes) * 0.8
    with_target = with_target * 0.3 + trend + np.random.randn(episodes) * 0.3
    
    ax6.plot(range(episodes), no_target, 'o-', label='Without Target Network',
            color='coral', alpha=0.7, markersize=4)
    ax6.plot(range(episodes), with_target, 's-', label='With Target Network',
            color='forestgreen', alpha=0.9, markersize=4, linewidth=2)
    
    ax6.set_xlabel('Episode', fontsize=11)
    ax6.set_ylabel('Estimated Q-Value', fontsize=11)
    ax6.set_title('Training Stability: Effect of Target Network\n'
                 'Target network reduces chasing and divergence',
                 fontsize=12, fontweight='bold')
    ax6.legend()
    ax6.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('dqn_mechanisms.png', dpi=150, bbox_inches='tight')
    print("可视化已保存至 dqn_mechanisms.png")
    plt.show()


if __name__ == "__main__":
    visualize_dqn_mechanisms()

14.1.3 DQN改进:Double DQN的解耦选择与评估、Dueling DQN的优势流与值流分离,Rainbow的六重整合

Double DQN通过解耦动作选择与动作评估两个步骤,缓解了标准DQN中最大化操作导致的系统性过估计偏差。在线网络负责选择下一状态的最优动作,目标网络则专用于评估该动作的期望价值,这种职责分离打破了自举过程中的正向反馈循环,使值函数估计更接近真实期望。

Dueling DQN在网络架构层面进行分解,将单输出的Q值预测拆分为状态价值流与动作优势流。共享的特征提取层之后,网络分支出两个独立的子网络分别估计状态本身的固有价值与每个动作的相对优势,最终通过聚合模块输出Q值。这种结构在存在多个等价动作或某些动作选择对长期回报影响不大的状态下,能够更有效地学习值函数,因为优势流可以直接将接近零的优势值分配给相似动作,而无需精确估计每个动作的绝对Q值。

Rainbow框架整合了六种独立改进:Double DQN的偏差校正、Dueling网络的架构优化、优先级回放的高效采样、异步演员-学习者的并行架构、分布性强化学习的概率值预测以及多步自举的时序扩展。这种整合并非简单叠加,而是通过共享底层机制实现协同增强,多步回报为优先级计算提供更准确的TD误差,分布性表示与Double DQN的结合进一步稳定了学习过程。实验表明,Rainbow在Atari基准上的样本效率与最终性能均显著超越单一改进版本,验证了整合的有效性。

脚本:rainbow_integration.py

Python

复制

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
脚本内容:Rainbow DQN六重整合实现
使用方式:直接运行 python rainbow_integration.py
依赖包:torch, numpy, matplotlib
功能说明:
1. 实现Rainbow的六大组件(Double+Priority+Dueling+Multi-step+NoisyNet+Distributional)
2. 可视化各组件的协同效应与消融实验结果
3. 展示C51(分布性DQN)的分位数投影
4. 对比单一改进vs整合版本的性能曲线
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt


class NoisyLinear(nn.Module):
    """NoisyNet:噪声参数化的线性层(替代epsilon-贪婪)"""
    
    def __init__(self, in_features, out_features, sigma_init=0.017):
        super().__init__()
        self.in_features = in_features
        self.out_features = out_features
        
        # 可学习的权重
        self.weight_mu = nn.Parameter(torch.Tensor(out_features, in_features))
        self.weight_sigma = nn.Parameter(torch.Tensor(out_features, in_features))
        self.bias_mu = nn.Parameter(torch.Tensor(out_features))
        self.bias_sigma = nn.Parameter(torch.Tensor(out_features))
        
        # 噪声缓冲区
        self.register_buffer('weight_epsilon', torch.Tensor(out_features, in_features))
        self.register_buffer('bias_epsilon', torch.Tensor(out_features))
        
        self.sigma_init = sigma_init
        self.reset_parameters()
        self.reset_noise()
    
    def reset_parameters(self):
        mu_range = 1.0 / np.sqrt(self.in_features)
        self.weight_mu.data.uniform_(-mu_range, mu_range)
        self.weight_sigma.data.fill_(self.sigma_init / np.sqrt(self.in_features))
        self.bias_mu.data.uniform_(-mu_range, mu_range)
        self.bias_sigma.data.fill_(self.sigma_init / np.sqrt(self.out_features))
    
    def reset_noise(self):
        epsilon_in = self._scale_noise(self.in_features)
        epsilon_out = self._scale_noise(self.out_features)
        self.weight_epsilon.copy_(epsilon_out.outer(epsilon_in))
        self.bias_epsilon.copy_(epsilon_out)
    
    def _scale_noise(self, size):
        x = torch.randn(size)
        return x.sign().mul_(x.abs().sqrt_())
    
    def forward(self, x):
        if self.training:
            weight = self.weight_mu + self.weight_sigma * self.weight_epsilon
            bias = self.bias_mu + self.bias_sigma * self.bias_epsilon
        else:
            weight = self.weight_mu
            bias = self.bias_mu
        
        return F.linear(x, weight, bias)


class RainbowNetwork(nn.Module):
    """Rainbow网络:整合Dueling + NoisyNet + Distributional (C51)"""
    
    def __init__(self, state_dim, action_dim, num_atoms=51, v_min=-10, v_max=10):
        super().__init__()
        self.action_dim = action_dim
        self.num_atoms = num_atoms
        
        # 分布性参数
        self.v_min = v_min
        self.v_max = v_max
        self.delta_z = (v_max - v_min) / (num_atoms - 1)
        self.register_buffer('support', torch.linspace(v_min, v_max, num_atoms))
        
        # 特征提取(共享)
        self.feature = nn.Sequential(
            nn.Linear(state_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 128),
            nn.ReLU()
        )
        
        # Dueling分支(使用NoisyLinear)
        self.value_stream = nn.Sequential(
            NoisyLinear(128, 64),
            nn.ReLU(),
            NoisyLinear(64, num_atoms)  # 输出每个原子的概率
        )
        
        self.advantage_stream = nn.Sequential(
            NoisyLinear(128, 64),
            nn.ReLU(),
            NoisyLinear(64, action_dim * num_atoms)
        )
    
    def forward(self, x):
        batch_size = x.size(0)
        features = self.feature(x)
        
        # 价值流:输出V的分布 (batch, num_atoms)
        value_dist = self.value_stream(features).view(batch_size, 1, self.num_atoms)
        
        # 优势流:输出A的分布 (batch, action_dim, num_atoms)
        adv_dist = self.advantage_stream(features).view(batch_size, self.action_dim, self.num_atoms)
        
        # Dueling聚合:Q(s,a) = V(s) + A(s,a) - mean(A)
        # 在分布性设置下,对logits进行聚合
        q_logits = value_dist + adv_dist - adv_dist.mean(dim=1, keepdim=True)
        
        # Softmax得到概率分布
        q_dist = F.softmax(q_logits, dim=2)  # (batch, action_dim, num_atoms)
        
        # 计算Q值期望(用于动作选择)
        q_values = (q_dist * self.support.view(1, 1, -1)).sum(dim=2)
        
        return q_dist, q_values
    
    def reset_noise(self):
        """重置所有噪声层"""
        for module in self.modules():
            if isinstance(module, NoisyLinear):
                module.reset_noise()


class RainbowAgent:
    """Rainbow智能体:整合六大改进"""
    
    def __init__(self, state_dim, action_dim):
        self.action_dim = action_dim
        self.network = RainbowNetwork(state_dim, action_dim)
        self.target_network = RainbowNetwork(state_dim, action_dim)
        self.target_network.load_state_dict(self.network.state_dict())
        
        self.optimizer = torch.optim.Adam(self.network.parameters(), lr=1e-4)
        
        # 多步学习参数
        self.n_step = 3
        self.gamma = 0.99
        
        # 分布性投影参数
        self.v_min = -10
        self.v_max = 10
        self.num_atoms = 51
    
    def project_distribution(self, rewards, next_dists, dones):
        """
        C51分布投影算法:将T(z)投影到支持点
        rewards: (batch,)
        next_dists: (batch, action_dim, num_atoms)
        dones: (batch,)
        """
        batch_size = rewards.size(0)
        
        # 计算T(z) = r + gamma * z(对于非终止状态)
        rewards = rewards.unsqueeze(1).unsqueeze(2)  # (batch, 1, 1)
        gamma = self.gamma ** self.n_step
        
        # 展开支持点
        support = self.network.support  # (num_atoms,)
        
        # 计算目标分布的支撑点
        tz = rewards + (1 - dones.unsqueeze(1).unsqueeze(2)) * gamma * support.view(1, 1, -1)
        tz = tz.clamp(self.v_min, self.v_max)
        
        # 投影到离散支撑点(线性插值)
        b = (tz - self.v_min) / self.network.delta_z  # 连续索引
        l = b.floor().long()  # 左索引
        u = b.ceil().long()   # 右索引
        
        # 处理边界
        l[(u > 0) * (l == u)] -= 1
        u[(l < (self.num_atoms - 1)) * (l == u)] += 1
        
        # 分配概率质量
        target_dist = torch.zeros(batch_size, self.action_dim, self.num_atoms, 
                                 device=rewards.device)
        
        # 获取下一动作(Double DQN:使用在线网络选择,目标网络评估)
        with torch.no_grad():
            _, next_q = self.network(next_dists[:, 0, :].unsqueeze(1).repeat(1, self.action_dim, 1))
            next_actions = next_q.argmax(dim=1)
            next_dist = next_dists[range(batch_size), next_actions]  # (batch, num_atoms)
        
        # 分布投影(C51核心)
        offset = torch.linspace(0, (batch_size - 1) * self.action_dim * self.num_atoms, 
                             batch_size, device=rewards.device).long()
        offset = offset.unsqueeze(1).expand(batch_size, self.num_atoms)
        
        # 将概率质量分配给左右邻居
        # 这里简化处理,实际实现需要考虑action_dim维度
        # 为可视化目的,我们返回目标分布用于展示
        
        return target_dist


def visualize_rainbow_components():
    """可视化Rainbow的六重整合"""
    
    fig, axes = plt.subplots(2, 3, figsize=(16, 10))
    
    # 1. Rainbow六组件示意图
    ax1 = axes[0, 0]
    ax1.set_xlim(0, 10)
    ax1.set_ylim(0, 10)
    ax1.axis('off')
    ax1.set_title('Rainbow DQN: Six Improvements Integration', fontsize=12, fontweight='bold')
    
    components = [
        ('1. Double DQN', 2, 8, 'lightblue'),
        ('2. Dueling Architecture', 2, 6.5, 'lightgreen'),
        ('3. Prioritized Replay', 2, 5, 'lightyellow'),
        ('4. Multi-step Learning', 8, 8, 'lightcoral'),
        ('5. Noisy Networks', 8, 6.5, 'plum'),
        ('6. Distributional RL', 8, 5, 'wheat')
    ]
    
    for name, x, y, color in components:
        ax1.add_patch(plt.Circle((x, y), 0.8, facecolor=color, edgecolor='black', linewidth=2))
        ax1.text(x, y, name, ha='center', va='center', fontsize=9, fontweight='bold')
        
        # 连接到中心
        ax1.plot([x, 5], [y, 3.5], 'k--', alpha=0.3, linewidth=1)
    
    # 中心:Rainbow
    ax1.add_patch(plt.Circle((5, 3.5), 1.2, facecolor='gold', edgecolor='red', linewidth=3))
    ax1.text(5, 3.5, 'RAINBOW', ha='center', va='center', fontsize=12, fontweight='bold', color='red')
    
    # 2. 消融实验结果(模拟数据)
    ax2 = axes[0, 1]
    
    components_list = ['Baseline\nDQN', 'Double', 'Dueling', 'Prioritized', 
                      'Multi-step', 'NoisyNet', 'Distributional', 'Rainbow\n(All)']
    # 模拟性能提升(归一化到Rainbow=100)
    performance = [35, 50, 55, 65, 70, 75, 80, 100]
    colors = ['gray'] + ['lightblue']*6 + ['gold']
    
    bars = ax2.barh(components_list, performance, color=colors, edgecolor='black')
    ax2.set_xlabel('Relative Performance (%)', fontsize=11)
    ax2.set_title('Ablation Study: Component Contributions\n'
                 'Integrating all yields superlinear improvement',
                 fontsize=12, fontweight='bold')
    
    # 添加数值标签
    for i, (bar, perf) in enumerate(zip(bars, performance)):
        width = bar.get_width()
        ax2.text(width + 1, bar.get_y() + bar.get_height()/2,
                f'{perf}%', va='center', fontsize=10, fontweight='bold')
    
    # 3. 分布性RL(C51)投影可视化
    ax3 = axes[0, 2]
    
    # 模拟概率分布
    support = np.linspace(-10, 10, 51)
    
    # 下一状态分布(双峰)
    next_dist = np.exp(-((support - 3)**2) / 2) + 0.5 * np.exp(-((support + 2)**2) / 2)
    next_dist /= next_dist.sum()
    
    # 当前奖励r=1,折扣后分布应右移并收缩
    shifted_support = 1 + 0.99 * support
    projected_dist = np.interp(support, shifted_support, next_dist, left=0, right=0)
    projected_dist /= projected_dist.sum()
    
    # 离散化投影(C51算法)
    discrete_dist = np.zeros_like(support)
    for i, (p, z) in enumerate(zip(projected_dist, support)):
        if -10 <= z <= 10:
            idx = int((z - (-10)) / (20/50))
            idx = np.clip(idx, 0, 49)
            # 线性插值到相邻支撑点
            b = (z - (-10)) / (20/50)
            l = int(np.floor(b))
            u = int(np.ceil(b))
            prob = b - l
            if l < 50:
                discrete_dist[l] += p * (1 - prob)
            if u < 50:
                discrete_dist[u] += p * prob
    
    discrete_dist /= discrete_dist.sum()
    
    ax3.plot(support, next_dist, 'b-', linewidth=2, label='Next State Distribution', alpha=0.7)
    ax3.plot(support, discrete_dist, 'r-o', linewidth=2, markersize=4, 
            label='Projected Distribution (C51)', alpha=0.7)
    
    ax3.set_xlabel('Value (z)', fontsize=11)
    ax3.set_ylabel('Probability', fontsize=11)
    ax3.set_title('Distributional RL: Probability Projection\n'
                 'Bellman update in distributional space',
                 fontsize=12, fontweight='bold')
    ax3.legend()
    ax3.grid(True, alpha=0.3)
    
    # 4. 多步学习(n-step returns)对比
    ax4 = axes[1, 0]
    
    steps = [1, 3, 5, 10]
    # 模拟不同n-step的偏差-方差权衡
    bias = [5, 2, 1, 0.5]  # 多步减少偏差
    variance = [1, 2, 4, 8]  # 但增加方差
    
    ax4.plot(steps, bias, 'o-', label='Bias', color='navy', linewidth=2, markersize=8)
    ax4.plot(steps, variance, 's-', label='Variance', color='crimson', linewidth=2, markersize=8)
    
    ax4.set_xlabel('N-step (Number of Bootstrap Steps)', fontsize=11)
    ax4.set_ylabel('Error Magnitude', fontsize=11)
    ax4.set_title('Multi-step Learning Trade-off\n'
                 'Larger n: Less bias, More variance',
                 fontsize=12, fontweight='bold')
    ax4.legend()
    ax4.grid(True, alpha=0.3)
    
    # 5. NoisyNet噪声演化(替代epsilon-贪婪)
    ax5 = axes[1, 1]
    
    episodes = 200
    # 标准epsilon-贪婪:阶梯式下降
    epsilon_greedy = [max(0.01, 1.0 - ep * 0.005) for ep in range(episodes)]
    # NoisyNet:噪声幅度自动学习(模拟递减趋势)
    noisy_net = [0.5 * np.exp(-ep * 0.02) + 0.05 for ep in range(episodes)]
    
    ax5.plot(range(episodes), epsilon_greedy, 'o-', label='Epsilon-Greedy (Hand-tuned)',
            color='coral', alpha=0.7, markersize=4)
    ax5.plot(range(episodes), noisy_net, 's-', label='NoisyNet (Learned)',
            color='forestgreen', alpha=0.9, markersize=4, linewidth=2)
    
    ax5.set_xlabel('Episode', fontsize=11)
    ax5.set_ylabel('Exploration Magnitude', fontsize=11)
    ax5.set_title('NoisyNet: Adaptive Exploration\n'
                 'Parameter noise replaces epsilon decay',
                 fontsize=12, fontweight='bold')
    ax5.legend()
    ax5.grid(True, alpha=0.3)
    
    # 6. Rainbow vs Baseline学习曲线
    ax6 = axes[1, 2]
    
    frames = np.arange(0, 10000000, 100000)
    # Rainbow:快速收敛、高最终性能
    rainbow_score = [0] + list(np.minimum(50, np.array(frames[1:]) / 100000 + np.random.randn(len(frames)-1) * 2))
    rainbow_score = np.maximum.accumulate(rainbow_score)
    
    # DQN:慢速收敛、较低性能
    dqn_score = [0] + list(np.minimum(30, np.array(frames[1:]) / 200000 + np.random.randn(len(frames)-1) * 3))
    dqn_score = np.maximum.accumulate(dqn_score)
    
    ax6.plot(frames/1000000, rainbow_score, 'o-', label='Rainbow (Integrated)',
            color='gold', linewidth=2.5, markersize=5)
    ax6.plot(frames/1000000, dqn_score, 's-', label='Standard DQN',
            color='gray', linewidth=2, markersize=5, alpha=0.7)
    
    ax6.set_xlabel('Training Frames (Million)', fontsize=11)
    ax6.set_ylabel('Average Episode Return', fontsize=11)
    ax6.set_title('Sample Efficiency: Rainbow vs DQN\n'
                 'Rainbow achieves better performance with fewer samples',
                 fontsize=12, fontweight='bold')
    ax6.legend()
    ax6.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('rainbow_integration.png', dpi=150, bbox_inches='tight')
    print("可视化已保存至 rainbow_integration.png")
    plt.show()


if __name__ == "__main__":
    visualize_rainbow_components()

14.2 策略梯度与Actor-Critic

14.2.1 REINFORCE算法的蒙特卡洛回报估计与基线减方差,A2C/A3C的同步与异步训练、熵正则化

REINFORCE算法通过蒙特卡洛采样估计完整回合的累积回报,沿着策略梯度方向更新参数以最大化期望回报。由于轨迹回报的随机性,梯度估计具有高方差,引入状态依赖的基线函数估计当前状态的期望回报,从实际回报中减去该基线形成优势估计,能够在保持无偏性的同时显著降低方差。状态值函数作为自然的基线选择,构成了Actor-Critic架构的基础。

A2C与A3C采用并行架构训练多个智能体实例,A2C的同步更新等待所有并行工作者完成梯度计算后统一应用,保证参数一致性但受限于最慢工作者的速度;A3C的异步更新允许各工作者独立计算并立即提交梯度,虽然引入了参数陈旧性但显著提高了吞吐量与壁钟时间效率。熵正则化在策略优化目标中增加动作分布的熵奖励,鼓励策略保持探索性,防止过早收敛至次优的确定性策略,其系数动态平衡探索与利用的权衡。

脚本:policy_gradient_a2c_a3c.py

Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
脚本内容:策略梯度方法(REINFORCE、A2C、A3C)实现
使用方式:直接运行 python policy_gradient_a2c_a3c.py
依赖包:torch, numpy, matplotlib, gymnasium
功能说明:
1. 实现REINFORCE算法与基线减方差技术
2. 实现Actor-Critic架构(A2C同步更新)
3. 模拟A3C的异步更新机制(并行训练可视化)
4. 对比不同方差缩减技术的效果与熵正则化的影响
"""

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
from torch.distributions import Categorical
import random


class PolicyNetwork(nn.Module):
    """策略网络(Actor)"""
    
    def __init__(self, state_dim, action_dim, hidden_dim=128):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, action_dim)
        )
    
    def forward(self, x):
        logits = self.fc(x)
        probs = torch.softmax(logits, dim=-1)
        return probs, logits


class ValueNetwork(nn.Module):
    """价值网络(Critic)作为基线"""
    
    def __init__(self, state_dim, hidden_dim=128):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 1)
        )
    
    def forward(self, x):
        return self.fc(x)


class REINFORCEAgent:
    """REINFORCE算法:蒙特卡洛策略梯度"""
    
    def __init__(self, state_dim, action_dim, use_baseline=True, entropy_coef=0.01):
        self.policy = PolicyNetwork(state_dim, action_dim)
        self.value_net = ValueNetwork(state_dim) if use_baseline else None
        
        self.policy_optimizer = optim.Adam(self.policy.parameters(), lr=1e-3)
        self.value_optimizer = optim.Adam(self.value_net.parameters(), lr=1e-3) if use_baseline else None
        
        self.use_baseline = use_baseline
        self.entropy_coef = entropy_coef
        self.gamma = 0.99
    
    def select_action(self, state):
        state = torch.FloatTensor(state)
        probs, logits = self.policy(state)
        dist = Categorical(probs)
        action = dist.sample()
        log_prob = dist.log_prob(action)
        entropy = dist.entropy()
        return action.item(), log_prob, entropy
    
    def compute_returns(self, rewards, dones):
        """计算蒙特卡洛回报( discounted sum of future rewards)"""
        returns = []
        G = 0
        for r, done in zip(reversed(rewards), reversed(dones)):
            if done:
                G = 0
            G = r + self.gamma * G
            returns.insert(0, G)
        
        returns = torch.tensor(returns, dtype=torch.float32)
        return (returns - returns.mean()) / (returns.std() + 1e-8)  # 归一化
    
    def update(self, states, actions, log_probs, entropies, rewards, dones):
        """策略更新(带基线减方差)"""
        returns = self.compute_returns(rewards, dones)
        
        if self.use_baseline:
            # 使用Critic估计基线
            states_tensor = torch.FloatTensor(states)
            values = self.value_net(states_tensor).squeeze()
            advantages = returns - values.detach()
            
            # 更新Critic
            value_loss = F.mse_loss(values, returns)
            self.value_optimizer.zero_grad()
            value_loss.backward()
            self.value_optimizer.step()
        else:
            # 无基线:直接使用归一化回报
            advantages = returns
        
        # 策略损失(带熵正则化)
        log_probs_tensor = torch.stack(log_probs)
        entropies_tensor = torch.stack(entropies)
        
        policy_loss = -(log_probs_tensor * advantages).mean()
        entropy_loss = -entropies_tensor.mean() * self.entropy_coef
        
        total_loss = policy_loss + entropy_loss
        
        self.policy_optimizer.zero_grad()
        total_loss.backward()
        self.policy_optimizer.step()
        
        return policy_loss.item(), value_loss.item() if self.use_baseline else 0


class AsyncWorkerSimulator:
    """模拟A3C的异步工作者行为"""
    
    def __init__(self, global_agent, worker_id, update_delay=0):
        self.global_agent = global_agent
        self.worker_id = worker_id
        self.local_policy = PolicyNetwork(global_agent.policy.fc[0].in_features, 
                                        global_agent.policy.fc[-1].out_features)
        self.local_policy.load_state_dict(global_agent.policy.state_dict())
        
        # 模拟异步延迟(gradients stale by N steps)
        self.update_delay = update_delay  # 梯度提交延迟
        self.pending_gradients = []
    
    def compute_gradient(self, batch_data):
        """计算本地梯度但不立即应用(模拟异步)"""
        # 简化:模拟梯度计算
        gradient_magnitude = np.random.randn() * 0.1 + 0.5  # 随机梯度大小
        
        # 添加到待处理队列
        self.pending_gradients.append({
            'grad': gradient_magnitude,
            'step': len(self.pending_gradients),
            'worker': self.worker_id
        })
        
        return gradient_magnitude
    
    def apply_async_update(self, global_step):
        """应用异步更新(检查是否有延迟到达的梯度)"""
        updates_applied = 0
        stale_gradients = []
        
        for pending in self.pending_gradients[:]:
            if global_step - pending['step'] >= self.update_delay:
                # 这是一个"陈旧"的梯度
                stale_gradients.append(global_step - pending['step'])
                self.pending_gradients.remove(pending)
                updates_applied += 1
        
        return updates_applied, stale_gradients


def visualize_policy_gradient():
    """可视化策略梯度方法"""
    
    fig, axes = plt.subplots(2, 3, figsize=(16, 10))
    
    # 1. REINFORCE方差缩减效果对比
    ax1 = axes[0, 0]
    
    # 模拟有无基线的梯度方差
    episodes = 50
    # 无基线:高方差(回报范围大)
    returns_no_baseline = np.random.randn(episodes) * 5 + 10
    # 有基线:低方差(优势函数中心化)
    returns_with_baseline = np.random.randn(episodes) * 2 + 10
    
    ax1.scatter(range(episodes), returns_no_baseline, c='red', alpha=0.6, 
               s=50, label='No Baseline (High Variance)', edgecolors='black')
    ax1.scatter(range(episodes), returns_with_baseline, c='green', alpha=0.6,
               s=50, label='With Baseline (Low Variance)', edgecolors='black')
    
    ax1.axhline(y=10, color='blue', linestyle='--', linewidth=2, label='True Expected Return')
    
    # 绘制移动平均
    window = 5
    ma_no = np.convolve(returns_no_baseline, np.ones(window)/window, mode='valid')
    ma_with = np.convolve(returns_with_baseline, np.ones(window)/window, mode='valid')
    
    ax1.plot(range(window-1, episodes), ma_no, 'r-', linewidth=2, alpha=0.8)
    ax1.plot(range(window-1, episodes), ma_with, 'g-', linewidth=2, alpha=0.8)
    
    ax1.set_xlabel('Episode', fontsize=11)
    ax1.set_ylabel('Gradient Estimate Scale', fontsize=11)
    ax1.set_title('Variance Reduction: Effect of Baseline\n'
                 'Centering returns reduces gradient noise',
                 fontsize=12, fontweight='bold')
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    
    # 2. Actor-Critic架构示意图
    ax2 = axes[0, 1]
    ax2.set_xlim(0, 10)
    ax2.set_ylim(0, 10)
    ax2.axis('off')
    ax2.set_title('Actor-Critic Architecture', fontsize=12, fontweight='bold')
    
    # 环境交互
    ax2.add_patch(plt.Rectangle((0, 8), 2, 1.5, facecolor='lightblue', edgecolor='black'))
    ax2.text(1, 8.75, 'Environment', ha='center', va='center', fontsize=9)
    
    # Actor
    ax2.add_patch(plt.Rectangle((3.5, 8), 3, 1.5, facecolor='lightgreen', edgecolor='black'))
    ax2.text(5, 8.75, 'Actor (Policy)\nπ(a|s)', ha='center', va='center', fontsize=9, fontweight='bold')
    
    # Critic
    ax2.add_patch(plt.Rectangle((7, 8), 3, 1.5, facecolor='lightcoral', edgecolor='black'))
    ax2.text(8.5, 8.75, 'Critic (Value)\nV(s)', ha='center', va='center', fontsize=9, fontweight='bold')
    
    # 连接
    ax2.annotate('', xy=(3.5, 8.75), xytext=(2, 8.75),
                arrowprops=dict(arrowstyle='->', color='black', lw=2))
    ax2.annotate('', xy=(7, 8.75), xytext=(6.5, 8.75),
                arrowprops=dict(arrowstyle='->', color='black', lw=2))
    
    # 下方:详细流程
    ax2.text(5, 6.5, '1. Actor selects action a ~ π(s)', ha='center', fontsize=10, style='italic')
    ax2.text(5, 6, '2. Environment returns r, s\'', ha='center', fontsize=10, style='italic')
    ax2.text(5, 5.5, '3. Critic computes TD error: δ = r + γV(s\') - V(s)', 
            ha='center', fontsize=10, style='italic')
    ax2.text(5, 5, '4. Update Actor to maximize δ (policy gradient)', 
            ha='center', fontsize=10, style='italic')
    ax2.text(5, 4.5, '5. Update Critic to minimize δ² (value regression)', 
            ha='center', fontsize=10, style='italic')
    
    # 优势函数计算
    ax2.add_patch(plt.Rectangle((3, 2), 4, 2, facecolor='wheat', edgecolor='black', alpha=0.7))
    ax2.text(5, 3, 'Advantage Estimation\nA(s,a) = Q(s,a) - V(s)\n≈ r + γV(s\') - V(s)', 
            ha='center', va='center', fontsize=10, fontweight='bold')
    
    # 3. A2C同步vs A3C异步对比
    ax3 = axes[0, 2]
    
    # 模拟时间线
    time_steps = np.arange(0, 20, 0.5)
    
    # A2C同步:等待所有工作者(周期性暂停)
    a2c_throughput = []
    for t in time_steps:
        cycle = int(t) % 4
        if cycle == 3:  # 同步点,等待
            a2c_throughput.append(0.5)
        else:
            a2c_throughput.append(2.0)
    
    # A3C异步:持续处理,但有梯度陈旧
    a3c_throughput = [2.2] * len(time_steps)  # 更高吞吐量
    a3c_staleness = [0.5 + 0.5 * np.sin(t) for t in time_steps]  # 梯度陈旧度变化
    
    ax3.plot(time_steps, a2c_throughput, 'o-', label='A2C Synchronous', 
            color='steelblue', linewidth=2, markersize=4, alpha=0.8)
    ax3.plot(time_steps, a3c_throughput, 's-', label='A3C Asynchronous',
            color='forestgreen', linewidth=2, markersize=4, alpha=0.8)
    
    ax3.set_xlabel('Wall-clock Time', fontsize=11)
    ax3.set_ylabel('Gradient Updates / Unit Time', fontsize=11)
    ax3.set_title('A2C vs A3C: Throughput Comparison\n'
                 'A3C achieves higher throughput but with stale gradients',
                 fontsize=12, fontweight='bold')
    ax3.legend()
    ax3.grid(True, alpha=0.3)
    
    # 4. 熵正则化效果可视化
    ax4 = axes[1, 0]
    
    # 模拟策略熵随训练的变化(无熵正则化vs有熵正则化)
    episodes = 100
    # 无熵正则化:快速收敛到低熵(确定性策略)
    entropy_no_reg = [np.exp(-ep * 0.05) * 2 for ep in range(episodes)]
    # 有熵正则化:保持探索性(高熵)
    entropy_with_reg = [1.5 + 0.5 * np.exp(-ep * 0.02) for ep in range(episodes)]
    
    # 对应性能(无正则化可能陷入局部最优)
    reward_no_reg = [10 + 5 * (1 - np.exp(-ep * 0.03)) + np.random.randn() * 0.5 
                     for ep in range(episodes)]
    reward_with_reg = [10 + 8 * (1 - np.exp(-ep * 0.04)) + np.random.randn() * 0.5 
                       for ep in range(episodes)]
    
    ax4_twin = ax4.twinx()
    
    line1 = ax4.plot(range(episodes), entropy_no_reg, 'r--', label='Entropy (No Reg)', 
                    linewidth=2, alpha=0.7)
    line2 = ax4.plot(range(episodes), entropy_with_reg, 'g--', label='Entropy (With Reg)',
                    linewidth=2, alpha=0.7)
    line3 = ax4_twin.plot(range(episodes), np.maximum.accumulate(reward_no_reg), 
                         'r-', label='Reward (No Reg)', linewidth=2)
    line4 = ax4_twin.plot(range(episodes), np.maximum.accumulate(reward_with_reg),
                         'g-', label='Reward (With Reg)', linewidth=2)
    
    ax4.set_xlabel('Episode', fontsize=11)
    ax4.set_ylabel('Policy Entropy', fontsize=11, color='red')
    ax4_twin.set_ylabel('Cumulative Reward', fontsize=11, color='green')
    ax4.set_title('Entropy Regularization Effect\n'
                 'Prevents premature convergence to suboptimal policy',
                 fontsize=12, fontweight='bold')
    
    lines = line1 + line2 + line3 + line4
    labels = [l.get_label() for l in lines]
    ax4.legend(lines, labels, loc='center right')
    ax4.grid(True, alpha=0.3)
    
    # 5. 梯度估计方差对比(REINFORCE vs Actor-Critic)
    ax5 = axes[1, 1]
    
    # 不同批量大小下的梯度方差
    batch_sizes = [1, 10, 50, 100, 500]
    
    # REINFORCE(蒙特卡洛):高方差,O(1/N)衰减
    variance_reinforce = [10 / (1 + np.sqrt(n)) for n in batch_sizes]
    # Actor-Critic(TD):低方差,更快收敛
    variance_ac = [3 / (1 + np.sqrt(n)) for n in batch_sizes]
    
    ax5.plot(batch_sizes, variance_reinforce, 'o-', label='REINFORCE (Monte Carlo)',
            color='coral', linewidth=2, markersize=8)
    ax5.plot(batch_sizes, variance_ac, 's-', label='Actor-Critic (TD)',
            color='forestgreen', linewidth=2, markersize=8)
    
    ax5.set_xlabel('Batch Size (Number of Trajectories)', fontsize=11)
    ax5.set_ylabel('Gradient Variance', fontsize=11)
    ax5.set_title('Gradient Variance: Sample Efficiency\n'
                 'Actor-Critic reduces variance by ~70% at same batch size',
                 fontsize=12, fontweight='bold')
    ax5.legend()
    ax5.grid(True, alpha=0.3)
    ax5.set_xscale('log')
    ax5.set_yscale('log')
    
    # 6. 并行工作者架构(A3C可视化)
    ax6 = axes[1, 2]
    ax6.set_xlim(0, 10)
    ax6.set_ylim(0, 10)
    ax6.axis('off')
    ax6.set_title('A3C: Asynchronous Advantage Actor-Critic Architecture', 
                 fontsize=12, fontweight='bold')
    
    # 全局网络(中央)
    ax6.add_patch(plt.Rectangle((4, 4), 2, 2, facecolor='gold', edgecolor='red', linewidth=3))
    ax6.text(5, 5, 'Global\nNetwork\n(Shared)', ha='center', va='center', 
            fontsize=10, fontweight='bold', color='red')
    
    # 多个异步工作者
    worker_positions = [(1, 8), (8, 8), (1, 1), (8, 1), (0.5, 5)]
    colors = ['lightblue', 'lightgreen', 'lightyellow', 'lightcoral', 'plum']
    
    for i, ((wx, wy), color) in enumerate(zip(worker_positions, colors)):
        # 工作者
        ax6.add_patch(plt.Circle((wx, wy), 0.8, facecolor=color, edgecolor='black', linewidth=2))
        ax6.text(wx, wy, f'Worker\n{i+1}', ha='center', va='center', fontsize=8, fontweight='bold')
        
        # 异步连接到全局网络
        ax6.annotate('', xy=(4, 5), xytext=(wx+0.6 if wx < 4 else wx-0.6, wy),
                    arrowprops=dict(arrowstyle='->', color='blue', alpha=0.6, lw=1.5,
                                   connectionstyle="arc3,rad=0.1"))
    
    # 添加文字说明
    ax6.text(5, 8.5, 'Each worker: Copy params -> Rollout -> Compute grad -> Async apply', 
            ha='center', fontsize=9, style='italic',
            bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
    ax6.text(5, 0.5, 'No synchronization barriers - gradients may be stale (N-step delay)', 
            ha='center', fontsize=9, style='italic',
            bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
    
    plt.tight_layout()
    plt.savefig('policy_gradient_methods.png', dpi=150, bbox_inches='tight')
    print("可视化已保存至 policy_gradient_methods.png")
    plt.show()


if __name__ == "__main__":
    visualize_policy_gradient()

14.2.2 PPO(近端策略优化)的裁剪替代目标与自适应KL惩罚,GAE(广义优势估计)的λ-回报权衡

近端策略优化通过裁剪替代目标限制单步策略更新的幅度,防止传统策略梯度方法中更新过大导致的性能崩溃。替代目标计算当前策略与旧策略的动作概率比率,当比率偏离1超过预设阈值时进行裁剪,形成信任区域约束的轻量级实现。自适应KL惩罚动态调整正则系数以保持策略更新前后的KL散度接近目标值,当实际散度超过上限时增加惩罚强度,低于下限时减弱惩罚,实现约束的自动满足。

广义优势估计通过参数λ在蒙特卡洛回报与时序差分估计之间进行连续插值,平衡偏差与方差的权衡。当λ趋近于1时,估计依赖完整回合的长程回报,偏差低但方差高;当λ趋近于0时,估计简化为单步TD误差,方差低但偏差高。GAE通过指数加权的TD误差和形式,在保持计算效率的同时获得多步自举的优势,成为策略梯度方法中的标准优势估计技术。

脚本:ppo_gae_implementation.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
脚本内容:PPO(近端策略优化)与GAE(广义优势估计)实现
使用方式:直接运行 python ppo_gae_implementation.py
依赖包:torch, numpy, matplotlib, gymnasium
功能说明:
1. 实现PPO的裁剪替代目标(Clipped Surrogate Objective)
2. 实现GAE的λ-回报计算与TD(λ)权重分析
3. 实现自适应KL惩罚机制
4. 可视化PPO的信任区域约束与GAE的偏差-方差权衡
"""

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
from torch.distributions import Categorical
import torch.nn.functional as F


class PPONetwork(nn.Module):
    """PPO的Actor-Critic网络"""
    
    def __init__(self, state_dim, action_dim, hidden_dim=64):
        super().__init__()
        
        # 共享特征层
        self.feature = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.Tanh(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.Tanh()
        )
        
        # Actor头(策略)
        self.actor = nn.Linear(hidden_dim, action_dim)
        
        # Critic头(价值)
        self.critic = nn.Linear(hidden_dim, 1)
    
    def forward(self, x):
        features = self.feature(x)
        logits = self.actor(features)
        value = self.critic(features)
        return logits, value
    
    def get_action_and_value(self, state, action=None):
        logits, value = self.forward(state)
        probs = Categorical(logits=logits)
        
        if action is None:
            action = probs.sample()
        
        return action, probs.log_prob(action), probs.entropy(), value.squeeze()


class PPOAgent:
    """PPO智能体实现(包含GAE)"""
    
    def __init__(self, state_dim, action_dim, clip_epsilon=0.2, 
                 gae_lambda=0.95, entropy_coef=0.01, value_coef=0.5):
        self.network = PPONetwork(state_dim, action_dim)
        self.optimizer = optim.Adam(self.network.parameters(), lr=3e-4)
        
        self.clip_epsilon = clip_epsilon  # PPO裁剪阈值
        self.gae_lambda = gae_lambda      # GAE lambda参数
        self.gamma = 0.99                 # 折扣因子
        self.entropy_coef = entropy_coef
        self.value_coef = value_coef
        
        # KL自适应参数
        self.adaptive_kl = False
        self.kl_target = 0.01
        self.kl_coeff = 1.0
    
    def compute_gae(self, rewards, values, dones, next_value):
        """
        计算广义优势估计(GAE)
        
        公式:A_t = sum_{l=0}^{inf} (gamma * lambda)^l * delta_{t+l}
        其中 delta_t = r_t + gamma * V(s_{t+1}) - V(s_t)
        """
        advantages = []
        gae = 0
        
        # 倒序计算(从回合结束开始)
        values = values + [next_value]
        for t in reversed(range(len(rewards))):
            if dones[t]:
                # 回合结束,重置GAE
                delta = rewards[t] - values[t]
                gae = delta
            else:
                # TD误差
                delta = rewards[t] + self.gamma * values[t + 1] - values[t]
                # GAE累加(指数衰减)
                gae = delta + self.gamma * self.gae_lambda * gae
            
            advantages.insert(0, gae)
        
        advantages = torch.tensor(advantages, dtype=torch.float32)
        # 归一化优势(稳定训练)
        advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
        
        return advantages
    
    def compute_kl_divergence(self, old_logits, new_logits, actions):
        """计算策略间的KL散度(用于自适应惩罚)"""
        old_probs = F.softmax(old_logits, dim=-1).detach()
        new_probs = F.softmax(new_logits, dim=-1)
        
        # 采样动作的KL近似
        old_log_probs = F.log_softmax(old_logits, dim=-1).gather(1, actions.unsqueeze(1))
        new_log_probs = F.log_softmax(new_logits, dim=-1).gather(1, actions.unsqueeze(1))
        
        kl = (old_log_probs - new_log_probs).exp() - 1 - (old_log_probs - new_log_probs)
        return kl.mean()
    
    def update(self, states, actions, old_log_probs, advantages, returns, 
               num_epochs=4, batch_size=64):
        """
        PPO更新:多次epoch,每次随机小批量
        """
        states = torch.FloatTensor(states)
        actions = torch.LongTensor(actions)
        old_log_probs = torch.stack(old_log_probs) if isinstance(old_log_probs, list) else old_log_probs
        advantages = torch.FloatTensor(advantages)
        returns = torch.FloatTensor(returns)
        
        dataset_size = len(states)
        
        # 存储损失历史用于可视化
        policy_losses = []
        value_losses = []
        kl_divs = []
        clip_fractions = []
        
        for epoch in range(num_epochs):
            # 随机打乱
            indices = torch.randperm(dataset_size)
            
            for start in range(0, dataset_size, batch_size):
                end = start + batch_size
                batch_idx = indices[start:end]
                
                batch_states = states[batch_idx]
                batch_actions = actions[batch_idx]
                batch_old_log_probs = old_log_probs[batch_idx]
                batch_advantages = advantages[batch_idx]
                batch_returns = returns[batch_idx]
                
                # 前向传播
                logits, values = self.network(batch_states)
                probs = Categorical(logits=logits)
                new_log_probs = probs.log_prob(batch_actions)
                entropy = probs.entropy().mean()
                
                # 概率比率
                ratio = torch.exp(new_log_probs - batch_old_log_probs)
                
                # PPO裁剪目标
                surr1 = ratio * batch_advantages
                surr2 = torch.clamp(ratio, 1 - self.clip_epsilon, 1 + self.clip_epsilon) * batch_advantages
                policy_loss = -torch.min(surr1, surr2).mean()
                
                # 记录裁剪比例(用于监控)
                clipped = ratio.abs() > (1 + self.clip_epsilon)
                clip_fraction = clipped.float().mean().item()
                clip_fractions.append(clip_fraction)
                
                # 价值损失
                value_loss = F.mse_loss(values.squeeze(), batch_returns)
                
                # KL惩罚(如果使用自适应)
                if self.adaptive_kl:
                    kl = self.compute_kl_divergence(
                        self.network(batch_states)[0].detach(), logits, batch_actions
                    )
                    policy_loss += self.kl_coeff * kl
                    kl_divs.append(kl.item())
                
                # 总损失
                loss = policy_loss + self.value_coef * value_loss - self.entropy_coef * entropy
                
                self.optimizer.zero_grad()
                loss.backward()
                torch.nn.utils.clip_grad_norm_(self.network.parameters(), 0.5)
                self.optimizer.step()
                
                policy_losses.append(policy_loss.item())
                value_losses.append(value_loss.item())
        
        # 自适应KL系数调整
        if self.adaptive_kl and len(kl_divs) > 0:
            avg_kl = np.mean(kl_divs)
            if avg_kl > self.kl_target * 2:
                self.kl_coeff *= 2
            elif avg_kl < self.kl_target / 2:
                self.kl_coeff /= 2
        
        return {
            'policy_loss': np.mean(policy_losses),
            'value_loss': np.mean(value_losses),
            'clip_fraction': np.mean(clip_fractions),
            'kl_coeff': self.kl_coeff if self.adaptive_kl else 0
        }


def visualize_ppo_gae():
    """可视化PPO与GAE机制"""
    
    fig, axes = plt.subplots(2, 3, figsize=(16, 10))
    
    # 1. PPO裁剪目标可视化(核心机制)
    ax1 = axes[0, 0]
    
    # 生成概率比率范围
    r = np.linspace(0.5, 1.5, 100)
    epsilon = 0.2
    
    # 正优势(A>0):鼓励增加概率,但限制在1+ε
    A_pos = 1.0
    L_clip_pos = np.minimum(r * A_pos, np.where(r > 1 + epsilon, (1 + epsilon) * A_pos, r * A_pos))
    L_clip_pos = np.where(r < 1 - epsilon, r * A_pos, L_clip_pos)  # 实际min操作
    
    # 修正:正确的PPO裁剪逻辑
    # L_clip = min(r * A, clip(r, 1-ε, 1+ε) * A)
    r_A_pos = r * A_pos
    r_clipped = np.clip(r, 1 - epsilon, 1 + epsilon) * A_pos
    L_clip_pos_correct = np.minimum(r_A_pos, r_clipped)
    
    # 负优势(A<0):鼓励减少概率,但限制在1-ε
    A_neg = -1.0
    r_A_neg = r * A_neg
    r_clipped_neg = np.clip(r, 1 - epsilon, 1 + epsilon) * A_neg
    L_clip_neg_correct = np.maximum(r_A_neg, r_clipped_neg)  # 对于负A,取max(因为都是负的)
    
    ax1.plot(r, r_A_pos, 'b--', label='Unclipped (A>0)', alpha=0.5)
    ax1.plot(r, L_clip_pos_correct, 'b-', linewidth=2, label='PPO Clipped (A>0)')
    ax1.plot(r, r_A_neg, 'r--', label='Unclipped (A<0)', alpha=0.5)
    ax1.plot(r, L_clip_neg_correct, 'r-', linewidth=2, label='PPO Clipped (A<0)')
    
    ax1.axvline(1 + epsilon, color='green', linestyle=':', alpha=0.5, label=f'Clip threshold (1±{epsilon})')
    ax1.axvline(1 - epsilon, color='green', linestyle=':', alpha=0.5)
    ax1.axvline(1.0, color='black', linestyle='-', alpha=0.3)
    
    ax1.set_xlabel('Probability Ratio r(θ) = π_new / π_old', fontsize=11)
    ax1.set_ylabel('Objective L(θ)', fontsize=11)
    ax1.set_title('PPO Clipped Surrogate Objective\n'
                 'Prevents large policy updates outside [1-ε, 1+ε]',
                 fontsize=12, fontweight='bold')
    ax1.legend(loc='lower right')
    ax1.grid(True, alpha=0.3)
    ax1.set_xlim(0.5, 1.5)
    
    # 2. GAE的λ参数效果(偏差-方差权衡)
    ax2 = axes[0, 1]
    
    lambdas = [0.0, 0.5, 0.8, 0.95, 1.0]
    colors = plt.cm.viridis(np.linspace(0, 1, len(lambdas)))
    
    # 模拟不同lambda的权重分布(对过去TD误差的权重)
    max_steps = 50
    
    for lam, color in zip(lambdas, colors):
        # GAE权重:w_l = (γλ)^l
        weights = [(0.99 * lam) ** l for l in range(max_steps)]
        ax2.plot(range(max_steps), weights, 'o-', color=color, 
                label=f'λ={lam}', markersize=3, alpha=0.8)
    
    ax2.set_xlabel('Step into Future (l)', fontsize=11)
    ax2.set_ylabel('Weight (γλ)^l', fontsize=11)
    ax2.set_title('GAE Weight Decay: Bias-Variance Trade-off\n'
                 'λ=0: TD(0) high bias, low var | λ=1: MC low bias, high var',
                 fontsize=12, fontweight='bold')
    ax2.legend(loc='upper right')
    ax2.set_yscale('log')
    ax2.grid(True, alpha=0.3)
    
    # 3. KL散度自适应调整
    ax3 = axes[0, 2]
    
    iterations = 20
    kl_target = 0.01
    
    # 模拟KL散度变化
    kl_divs = [0.02, 0.015, 0.012, 0.01, 0.008, 0.009, 0.01, 0.011, 0.015, 0.02] + \
              [0.02, 0.018, 0.015, 0.012, 0.01, 0.009, 0.01, 0.011, 0.01, 0.01]
    
    # 自适应系数
    kl_coeffs = [1.0]
    for i in range(1, len(kl_divs)):
        if kl_divs[i] > kl_target * 2:
            kl_coeffs.append(kl_coeffs[-1] * 2)
        elif kl_divs[i] < kl_target / 2:
            kl_coeffs.append(kl_coeffs[-1] / 2)
        else:
            kl_coeffs.append(kl_coeffs[-1])
    
    ax3.plot(range(len(kl_divs)), kl_divs, 'o-', label='Actual KL Divergence', 
            color='navy', linewidth=2, markersize=6)
    ax3.axhline(y=kl_target, color='red', linestyle='--', label='Target KL')
    ax3.axhline(y=kl_target*2, color='orange', linestyle=':', alpha=0.5, label='Upper bound')
    ax3.axhline(y=kl_target/2, color='orange', linestyle=':', alpha=0.5, label='Lower bound')
    
    ax3_twin = ax3.twinx()
    ax3_twin.plot(range(len(kl_coeffs)), kl_coeffs, 's-', color='green', 
                 label='KL Coefficient', alpha=0.7, markersize=6)
    
    ax3.set_xlabel('Update Iteration', fontsize=11)
    ax3.set_ylabel('KL Divergence', fontsize=11, color='navy')
    ax3_twin.set_ylabel('Adaptive Coefficient', fontsize=11, color='green')
    ax3.set_title('Adaptive KL Penalty\n'
                 'Auto-adjusts to keep KL near target',
                 fontsize=12, fontweight='bold')
    ax3.legend(loc='upper right')
    ax3_twin.legend(loc='center right')
    ax3.grid(True, alpha=0.3)
    
    # 4. 多epoch训练的稳定性
    ax4 = axes[1, 0]
    
    epochs = range(1, 9)
    # 不同epoch数下的性能(模拟)
    # PPO:多epoch提高效率,但过多会导致过拟合
    performance_ppo = [100, 150, 180, 195, 205, 210, 208, 200]  # 先升后降
    performance_trpo = [120, 140, 155, 165, 172, 178, 183, 188]  # 缓慢上升(单epoch)
    
    ax4.plot(epochs, performance_ppo, 'o-', label='PPO (Multi-epoch)', 
            color='forestgreen', linewidth=2, markersize=8)
    ax4.plot(epochs, performance_trpo, 's--', label='TRPO-style (Single-epoch)',
            color='coral', linewidth=2, markersize=6, alpha=0.7)
    
    ax4.axvline(x=4, color='blue', linestyle=':', alpha=0.5)
    ax4.text(4.2, 180, 'Typical\nPPO setting', fontsize=9, color='blue')
    
    ax4.set_xlabel('Number of Epochs per Update', fontsize=11)
    ax4.set_ylabel('Policy Performance', fontsize=11)
    ax4.set_title('PPO Data Efficiency: Multi-epoch Updates\n'
                 'Reuses batch data for multiple gradient steps',
                 fontsize=12, fontweight='bold')
    ax4.legend()
    ax4.grid(True, alpha=0.3)
    
    # 5. 裁剪比例与训练稳定性关系
    ax5 = axes[1, 1]
    
    # 模拟不同epsilon下的训练动态
    epsilons = [0.1, 0.2, 0.3, 0.5]
    colors = ['blue', 'green', 'orange', 'red']
    
    for eps, color in zip(epsilons, colors):
        # 模拟训练曲线(epsilon越大,初期进展快但后期不稳定)
        episodes = 100
        if eps == 0.1:
            rewards = [10 + 8 * (1 - np.exp(-ep * 0.05)) + np.random.randn() * 0.3 
                      for ep in range(episodes)]
        elif eps == 0.2:
            rewards = [10 + 10 * (1 - np.exp(-ep * 0.06)) + np.random.randn() * 0.4
                      for ep in range(episodes)]
        else:
            # 大epsilon:后期震荡
            rewards = [10 + 12 * (1 - np.exp(-ep * 0.07)) + 
                      (np.random.randn() * 1.5 if ep > 50 else np.random.randn() * 0.3)
                      for ep in range(episodes)]
            rewards = [min(r, 25) if ep > 70 else r for ep, r in enumerate(rewards)]  # 性能崩溃
        
        ax5.plot(range(episodes), np.maximum.accumulate(rewards), 'o-', 
                color=color, label=f'ε={eps}', linewidth=2, markersize=3, alpha=0.7)
    
    ax5.set_xlabel('Episode', fontsize=11)
    ax5.set_ylabel('Cumulative Reward', fontsize=11)
    ax5.set_title('Clip Parameter Sensitivity\n'
                 'Small ε: stable but slow | Large ε: fast but unstable',
                 fontsize=12, fontweight='bold')
    ax5.legend()
    ax5.grid(True, alpha=0.3)
    
    # 6. PPO整体架构与数据流
    ax6 = axes[1, 2]
    ax6.set_xlim(0, 10)
    ax6.set_ylim(0, 10)
    ax6.axis('off')
    ax6.set_title('PPO: Proximal Policy Optimization Pipeline', 
                 fontsize=12, fontweight='bold')
    
    # 收集轨迹
    ax6.add_patch(plt.Rectangle((0.5, 7), 2.5, 1.5, facecolor='lightblue', edgecolor='black'))
    ax6.text(1.75, 7.75, 'Collect\nTrajectories\n(π_old)', ha='center', va='center', fontsize=9)
    
    # 计算GAE
    ax6.add_patch(plt.Rectangle((4, 7), 2.5, 1.5, facecolor='lightgreen', edgecolor='black'))
    ax6.text(5.25, 7.75, 'Compute GAE\nAdvantages', ha='center', va='center', fontsize=9)
    
    # 多epoch更新
    ax6.add_patch(plt.Rectangle((7.5, 7), 2.5, 1.5, facecolor='lightyellow', edgecolor='black'))
    ax6.text(8.75, 7.75, 'Multi-epoch\nUpdate', ha='center', va='center', fontsize=9)
    
    # 连接箭头
    ax6.annotate('', xy=(4, 7.75), xytext=(3, 7.75),
                arrowprops=dict(arrowstyle='->', color='black', lw=2))
    ax6.annotate('', xy=(7.5, 7.75), xytext=(6.5, 7.75),
                arrowprops=dict(arrowstyle='->', color='black', lw=2))
    
    # 下方详细:PPO核心
    ax6.add_patch(plt.Rectangle((2, 3.5), 6, 3, facecolor='wheat', edgecolor='red', linewidth=2))
    ax6.text(5, 6, 'PPO Core: Clipped Surrogate Objective', ha='center', fontsize=10, fontweight='bold')
    
    # 公式说明
    ax6.text(5, 5, r'L(θ) = E[ min( r(θ)A, clip(r(θ), 1-ε, 1+ε)A ) ]',
            ha='center', fontsize=11, style='italic',
            bbox=dict(boxstyle='round', facecolor='white', alpha=0.9))
    
    # 详细步骤
    details = [
        '1. r(θ) = π_new(a|s) / π_old(a|s)  [probability ratio]',
        '2. If r(θ) in [1-ε, 1+ε]: normal gradient',
        '3. If r(θ) > 1+ε and A>0: clip to (1+ε)A [no extra reward]',
        '4. If r(θ) < 1-ε and A<0: clip to (1-ε)A [no extra penalty]'
    ]
    
    for i, detail in enumerate(details):
        ax6.text(5, 4.2 - i*0.5, detail, ha='center', fontsize=9, style='italic')
    
    # 输出
    ax6.annotate('', xy=(5, 3.5), xytext=(5, 2.5),
                arrowprops=dict(arrowstyle='->', color='black', lw=2))
    ax6.add_patch(plt.Rectangle((3, 1), 4, 1.5, facecolor='lightcoral', edgecolor='black'))
    ax6.text(5, 1.75, 'Updated Policy π_new\n(Trust Region Constrained)', 
            ha='center', va='center', fontsize=10, fontweight='bold')
    
    plt.tight_layout()
    plt.savefig('ppo_gae_mechanisms.png', dpi=150, bbox_inches='tight')
    print("可视化已保存至 ppo_gae_mechanisms.png")
    plt.show()


if __name__ == "__main__":
    visualize_ppo_gae()

14.2.3 Soft Actor-Critic(SAC)的最大熵框架与温度参数自动调整,确定性策略梯度(DDPG/TD3)的目标策略平滑与延迟更新

Soft Actor-Critic在标准强化学习目标中引入熵增项,将策略优化目标扩展为同时最大化期望回报与策略随机性。这种最大熵框架鼓励探索,增强对多模态最优策略的表达能力,并改善对奖励函数误差的鲁棒性。温度参数自动调整机制动态调节熵增项的权重,使策略熵保持接近预设目标值,无需手动调参即可在探索与利用之间获得任务自适应的平衡。

确定性策略梯度方法(DDPG与TD3)针对连续控制任务,直接对确定性策略的期望回报进行梯度上升。DDPG使用演员-评论者架构,演员输出确定性动作,评论者估计Q值并指导演员梯度。TD3通过三重机制改进稳定性:双评论者网络抑制Q值过估计,延迟策略更新确保评论者充分收敛后再更新演员,目标策略平滑在目标计算中向动作添加噪声减少方差。这些技术共同解决了连续控制中的值函数逼近误差累积问题。

脚本:sac_td3_continuous_control.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
脚本内容:SAC(最大熵强化学习)与TD3(确定性策略梯度)实现
使用方式:直接运行 python sac_td3_continuous_control.py
依赖包:torch, numpy, matplotlib, gymnasium
功能说明:
1. 实现SAC的自动温度调整(Automating Entropy Adjustment)
2. 实现TD3的双评论者、延迟更新与目标策略平滑
3. 对比随机策略(SAC)与确定性策略(TD3)的探索特性
4. 可视化连续控制中的值函数 landscape 与策略分布
"""

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
from torch.distributions import Normal
import random


class GaussianPolicy(nn.Module):
    """SAC的高斯策略(随机策略)"""
    
    def __init__(self, state_dim, action_dim, hidden_dim=256, 
                 log_std_min=-20, log_std_max=2):
        super().__init__()
        self.log_std_min = log_std_min
        self.log_std_max = log_std_max
        
        self.fc = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU()
        )
        
        self.mean_linear = nn.Linear(hidden_dim, action_dim)
        self.log_std_linear = nn.Linear(hidden_dim, action_dim)
    
    def forward(self, state):
        x = self.fc(state)
        mean = self.mean_linear(x)
        log_std = self.log_std_linear(x)
        log_std = torch.clamp(log_std, self.log_std_min, self.log_std_max)
        return mean, log_std
    
    def sample(self, state):
        """重参数化采样"""
        mean, log_std = self.forward(state)
        std = log_std.exp()
        
        # 重参数化技巧
        normal = Normal(mean, std)
        x_t = normal.rsample()  # 可微采样
        action = torch.tanh(x_t)  # 限制在[-1, 1]
        
        # 计算对数概率(考虑tanh变换)
        log_prob = normal.log_prob(x_t)
        log_prob -= torch.log(1 - action.pow(2) + 1e-6)
        log_prob = log_prob.sum(1, keepdim=True)
        
        return action, log_prob, mean


class DeterministicPolicy(nn.Module):
    """DDPG/TD3的确定性策略"""
    
    def __init__(self, state_dim, action_dim, hidden_dim=256):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, action_dim),
            nn.Tanh()  # 输出限制在[-1, 1]
        )
    
    def forward(self, state):
        return self.net(state)


class QNetwork(nn.Module):
    """Q网络(用于SAC和TD3)"""
    
    def __init__(self, state_dim, action_dim, hidden_dim=256):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim + action_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 1)
        )
    
    def forward(self, state, action):
        x = torch.cat([state, action], dim=1)
        return self.net(x)


class SACAgent:
    """Soft Actor-Critic智能体(含自动温度调整)"""
    
    def __init__(self, state_dim, action_dim, hidden_dim=256,
                 target_entropy=None, alpha_lr=3e-4):
        self.policy = GaussianPolicy(state_dim, action_dim, hidden_dim)
        self.q1 = QNetwork(state_dim, action_dim, hidden_dim)
        self.q2 = QNetwork(state_dim, action_dim, hidden_dim)  # 双Q网络
        
        self.target_q1 = QNetwork(state_dim, action_dim, hidden_dim)
        self.target_q2 = QNetwork(state_dim, action_dim, hidden_dim)
        self.target_q1.load_state_dict(self.q1.state_dict())
        self.target_q2.load_state_dict(self.q2.state_dict())
        
        # 自动温度调整
        if target_entropy is None:
            self.target_entropy = -action_dim  # 默认:-dim(A)
        else:
            self.target_entropy = target_entropy
        
        # log_alpha是可学习参数
        self.log_alpha = torch.zeros(1, requires_grad=True)
        self.alpha = self.log_alpha.exp()
        self.alpha_optimizer = optim.Adam([self.log_alpha], lr=alpha_lr)
        
        # 优化器
        self.policy_optimizer = optim.Adam(self.policy.parameters(), lr=3e-4)
        self.q_optimizer = optim.Adam(list(self.q1.parameters()) + list(self.q2.parameters()), lr=3e-4)
        
        self.gamma = 0.99
        self.tau = 0.005  # 软更新系数
    
    def select_action(self, state, evaluate=False):
        state = torch.FloatTensor(state).unsqueeze(0)
        if evaluate:
            _, _, action = self.policy.sample(state)
        else:
            action, _, _ = self.policy.sample(state)
        return action.detach().numpy()[0]
    
    def update_temperature(self, states):
        """自动调整温度参数alpha"""
        with torch.no_grad():
            _, log_probs, _ = self.policy.sample(states)
        
        # 目标:alpha * (log_prob + target_entropy) = 0
        # 即策略熵保持在target_entropy附近
        alpha_loss = -(self.log_alpha * (log_probs + self.target_entropy)).mean()
        
        self.alpha_optimizer.zero_grad()
        alpha_loss.backward()
        self.alpha_optimizer.step()
        
        self.alpha = self.log_alpha.exp().detach()
        return alpha_loss.item()
    
    def update(self, batch):
        """SAC更新"""
        states, actions, rewards, next_states, dones = batch
        
        # Q函数更新
        with torch.no_grad():
            next_actions, next_log_probs, _ = self.policy.sample(next_states)
            
            # 目标Q值(带熵增项)
            target_q1 = self.target_q1(next_states, next_actions)
            target_q2 = self.target_q2(next_states, next_actions)
            target_q = torch.min(target_q1, target_q2) - self.alpha * next_log_probs
            
            q_target = rewards + (1 - dones) * self.gamma * target_q
        
        # 当前Q值
        current_q1 = self.q1(states, actions)
        current_q2 = self.q2(states, actions)
        
        q_loss = F.mse_loss(current_q1, q_target) + F.mse_loss(current_q2, q_target)
        
        self.q_optimizer.zero_grad()
        q_loss.backward()
        self.q_optimizer.step()
        
        # 策略更新(最大化期望回报+熵)
        new_actions, log_probs, _ = self.policy.sample(states)
        q1_new = self.q1(states, new_actions)
        q2_new = self.q2(states, new_actions)
        q_new = torch.min(q1_new, q2_new)
        
        policy_loss = (self.alpha * log_probs - q_new).mean()
        
        self.policy_optimizer.zero_grad()
        policy_loss.backward()
        self.policy_optimizer.step()
        
        # 更新温度
        alpha_loss = self.update_temperature(states)
        
        # 软更新目标网络
        for param, target_param in zip(self.q1.parameters(), self.target_q1.parameters()):
            target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
        for param, target_param in zip(self.q2.parameters(), self.target_q2.parameters()):
            target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
        
        return {
            'q_loss': q_loss.item(),
            'policy_loss': policy_loss.item(),
            'alpha': self.alpha.item(),
            'entropy': -log_probs.mean().item()
        }


class TD3Agent:
    """Twin Delayed Deep Deterministic Policy Gradient (TD3)"""
    
    def __init__(self, state_dim, action_dim, hidden_dim=256,
                 policy_noise=0.2, noise_clip=0.5, policy_freq=2):
        self.actor = DeterministicPolicy(state_dim, action_dim, hidden_dim)
        self.actor_target = DeterministicPolicy(state_dim, action_dim, hidden_dim)
        self.actor_target.load_state_dict(self.actor.state_dict())
        
        # 双评论者(Twin Q-networks)
        self.critic1 = QNetwork(state_dim, action_dim, hidden_dim)
        self.critic2 = QNetwork(state_dim, action_dim, hidden_dim)
        self.critic1_target = QNetwork(state_dim, action_dim, hidden_dim)
        self.critic2_target = QNetwork(state_dim, action_dim, hidden_dim)
        self.critic1_target.load_state_dict(self.critic1.state_dict())
        self.critic2_target.load_state_dict(self.critic2.state_dict())
        
        self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=3e-4)
        self.critic_optimizer = optim.Adam(
            list(self.critic1.parameters()) + list(self.critic2.parameters()), lr=3e-4)
        
        self.policy_noise = policy_noise      # 目标策略平滑噪声
        self.noise_clip = noise_clip          # 噪声裁剪
        self.policy_freq = policy_freq        # 延迟更新频率(每N次更新actor一次)
        self.tau = 0.005
        self.gamma = 0.99
        self.total_it = 0  # 总更新计数
    
    def select_action(self, state, noise=0.1):
        state = torch.FloatTensor(state).unsqueeze(0)
        action = self.actor(state).detach().numpy()[0]
        if noise > 0:
            action += np.random.normal(0, noise, size=action.shape)
            action = np.clip(action, -1, 1)
        return action
    
    def update(self, batch):
        """TD3更新(含延迟策略更新和目标策略平滑)"""
        self.total_it += 1
        states, actions, rewards, next_states, dones = batch
        
        with torch.no_grad():
            # 目标策略平滑:向目标动作添加噪声
            noise = (torch.randn_like(actions) * self.policy_noise).clamp(
                -self.noise_clip, self.noise_clip)
            next_actions = (self.actor_target(next_states) + noise).clamp(-1, 1)
            
            # 双评论者:取较小值(抑制过估计)
            target_q1 = self.critic1_target(next_states, next_actions)
            target_q2 = self.critic2_target(next_states, next_actions)
            target_q = torch.min(target_q1, target_q2)
            
            q_target = rewards + (1 - dones) * self.gamma * target_q
        
        # 更新双评论者
        current_q1 = self.critic1(states, actions)
        current_q2 = self.critic2(states, actions)
        
        critic_loss = F.mse_loss(current_q1, q_target) + F.mse_loss(current_q2, q_target)
        
        self.critic_optimizer.zero_grad()
        critic_loss.backward()
        self.critic_optimizer.step()
        
        actor_loss = None
        
        # 延迟策略更新:每policy_freq次才更新actor
        if self.total_it % self.policy_freq == 0:
            # 更新actor(最大化Q1)
            actions_pred = self.actor(states)
            actor_loss = -self.critic1(states, actions_pred).mean()
            
            self.actor_optimizer.zero_grad()
            actor_loss.backward()
            self.actor_optimizer.step()
            
            # 软更新目标网络
            for param, target_param in zip(self.actor.parameters(), self.actor_target.parameters()):
                target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
        
        # 总是更新评论者目标网络
        for param, target_param in zip(self.critic1.parameters(), self.critic1_target.parameters()):
            target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
        for param, target_param in zip(self.critic2.parameters(), self.critic2_target.parameters()):
            target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
        
        return {
            'critic_loss': critic_loss.item(),
            'actor_loss': actor_loss.item() if actor_loss is not None else 0,
            'min_q': target_q.min().item(),
            'max_q': target_q.max().item()
        }


def visualize_sac_td3():
    """可视化SAC与TD3机制"""
    
    fig, axes = plt.subplots(2, 3, figsize=(16, 10))
    
    # 创建模拟智能体用于可视化
    sac_agent = SACAgent(state_dim=2, action_dim=1)
    td3_agent = TD3Agent(state_dim=2, action_dim=1)
    
    # 1. SAC的随机策略分布 vs TD3的确定性策略
    ax1 = axes[0, 0]
    
    # 固定状态
    state = torch.zeros(1, 2)
    
    # SAC:采样多个动作展示分布
    sac_actions = []
    for _ in range(1000):
        with torch.no_grad():
            action, _, _ = sac_agent.policy.sample(state)
            sac_actions.append(action.item())
    
    # TD3:确定性输出
    with torch.no_grad():
        td3_action = td3_agent.actor(state).item()
    
    ax1.hist(sac_actions, bins=30, color='lightblue', alpha=0.7, 
            edgecolor='black', label='SAC Stochastic Policy')
    ax1.axvline(x=td3_action, color='red', linewidth=3, 
               label=f'TD3 Deterministic: {td3_action:.2f}')
    ax1.axvline(x=np.mean(sac_actions), color='green', linestyle='--', 
               linewidth=2, label=f'SAC Mean: {np.mean(sac_actions):.2f}')
    
    ax1.set_xlabel('Action Value', fontsize=11)
    ax1.set_ylabel('Frequency', fontsize=11)
    ax1.set_title('Policy Distribution: Stochastic vs Deterministic\n'
                 'SAC explores via noise, TD3 requires explicit exploration',
                 fontsize=12, fontweight='bold')
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    
    # 2. SAC自动温度调整过程
    ax2 = axes[0, 1]
    
    # 模拟训练过程中alpha的自动调整
    episodes = 200
    target_entropy = -1.0  # 目标熵
    
    # 模拟熵的变化(开始时高,逐渐降低但保持在目标附近)
    entropy_trajectory = [2.0 * np.exp(-ep * 0.02) + target_entropy + 0.3 
                         for ep in range(episodes)]
    entropy_trajectory = [max(e, target_entropy - 0.5) for e in entropy_trajectory]
    
    # alpha的自动调整(根据熵与目标的差距)
    alpha_trajectory = []
    alpha = 0.2
    for ep in range(episodes):
        entropy = entropy_trajectory[ep]
        # 如果熵低于目标,减少alpha(鼓励探索);反之亦然
        if entropy < target_entropy:
            alpha *= 0.98
        else:
            alpha *= 1.02
        alpha = np.clip(alpha, 0.01, 2.0)
        alpha_trajectory.append(alpha)
    
    ax2.plot(range(episodes), entropy_trajectory, 'b-', label='Policy Entropy', linewidth=2)
    ax2.axhline(y=target_entropy, color='r', linestyle='--', 
               label=f'Target Entropy ({target_entropy})', linewidth=2)
    ax2_twin = ax2.twinx()
    ax2_twin.plot(range(episodes), alpha_trajectory, 'g-', 
                 label='Temperature α', linewidth=2, alpha=0.7)
    
    ax2.set_xlabel('Episode', fontsize=11)
    ax2.set_ylabel('Entropy', fontsize=11, color='blue')
    ax2_twin.set_ylabel('Alpha (Temperature)', fontsize=11, color='green')
    ax2.set_title('SAC Automatic Temperature Adjustment\n'
                 'α adjusts to maintain target entropy',
                 fontsize=12, fontweight='bold')
    ax2.legend(loc='upper right')
    ax2_twin.legend(loc='center right')
    ax2.grid(True, alpha=0.3)
    
    # 3. TD3的目标策略平滑(Target Policy Smoothing)
    ax3 = axes[0, 2]
    
    # 原始确定性动作
    original_action = np.linspace(-1, 1, 100)
    # 添加裁剪的随机噪声(TD3机制)
    noise_std = 0.2
    noise_clip = 0.5
    
    # 多次采样展示平滑效果
    for i in range(5):
        noise = np.random.normal(0, noise_std, size=original_action.shape)
        noise = np.clip(noise, -noise_clip, noise_clip)
        smoothed_action = np.clip(original_action + noise, -1, 1)
        
        alpha_val = 0.3 + 0.1 * i
        ax3.plot(original_action, smoothed_action, 'o', alpha=alpha_val, 
                markersize=3, label=f'Sample {i+1}' if i < 3 else None)
    
    ax3.plot([-1, 1], [-1, 1], 'k--', linewidth=2, label='Original (no noise)')
    ax3.fill_between(original_action, original_action - noise_clip, 
                    original_action + noise_clip, alpha=0.2, color='gray',
                    label='Clipped noise region')
    
    ax3.set_xlabel('Original Action (Deterministic)', fontsize=11)
    ax3.set_ylabel('Smoothed Action (Target Network)', fontsize=11)
    ax3.set_title('TD3 Target Policy Smoothing\n'
                 'Reduces variance in target Q-value estimation',
                 fontsize=12, fontweight='bold')
    ax3.legend(loc='upper left')
    ax3.grid(True, alpha=0.3)
    
    # 4. 双评论者抑制过估计(Clipped Double Q-learning)
    ax4 = axes[1, 0]
    
    # 模拟Q值估计
    true_q = 10.0
    states = np.linspace(-2, 2, 50)
    
    # 单Q网络:容易过估计(正向偏差)
    single_q = true_q + 2 + np.random.randn(50) * 0.5
    
    # 双Q网络(TD3/SAC):取最小值,减少过估计
    q1 = true_q + 1.5 + np.random.randn(50) * 0.5
    q2 = true_q + 1.0 + np.random.randn(50) * 0.5  # 通常一个高估,一个低估
    double_q = np.minimum(q1, q2)
    
    ax4.plot(states, single_q, 'o-', label='Single Q-network (Overestimation)',
            color='coral', alpha=0.7, markersize=4)
    ax4.plot(states, q1, 'o-', label='Q1', color='lightblue', alpha=0.5, markersize=3)
    ax4.plot(states, q2, 'o-', label='Q2', color='lightgreen', alpha=0.5, markersize=3)
    ax4.plot(states, double_q, 's-', label='min(Q1, Q2) - TD3/SAC', 
            color='forestgreen', linewidth=2, markersize=5)
    ax4.axhline(y=true_q, color='red', linestyle='--', linewidth=2, label='True Q-value')
    
    ax4.set_xlabel('State', fontsize=11)
    ax4.set_ylabel('Estimated Q-value', fontsize=11)
    ax4.set_title('Twin Q-networks: Overestimation Bias Reduction\n'
                 'Taking minimum of two estimates reduces positive bias',
                 fontsize=12, fontweight='bold')
    ax4.legend(loc='upper right', fontsize=8)
    ax4.grid(True, alpha=0.3)
    
    # 5. 延迟策略更新效果(TD3特有)
    ax5 = axes[1, 1]
    
    # 模拟更新频率的影响
    update_steps = np.arange(0, 1000, 10)
    
    # 频繁更新(如每步都更新):不稳定
    frequent_reward = [10 + 5 * (1 - np.exp(-s * 0.01)) + np.random.randn() * 2 
                      for s in update_steps]
    
    # 延迟更新(每2步更新一次):更稳定(TD3)
    delayed_reward = [10 + 5 * (1 - np.exp(-s * 0.01)) + np.random.randn() * 0.8 
                     for s in update_steps]
    
    # 过度延迟(每10步):学习慢
    slow_reward = [10 + 5 * (1 - np.exp(-s * 0.002)) + np.random.randn() * 0.5 
                  for s in update_steps]
    
    # 平滑处理
    def smooth(data, window=10):
        return np.convolve(data, np.ones(window)/window, mode='same')
    
    ax5.plot(update_steps, smooth(frequent_reward), 'o-', label='Update every step (Unstable)',
            color='coral', alpha=0.7, markersize=3)
    ax5.plot(update_steps, smooth(delayed_reward), 's-', label='Delayed update (TD3, freq=2)',
            color='forestgreen', linewidth=2, markersize=3)
    ax5.plot(update_steps, smooth(slow_reward), '^-', label='Too slow (freq=10)',
            color='gray', alpha=0.7, markersize=3)
    
    ax5.set_xlabel('Training Step', fontsize=11)
    ax5.set_ylabel('Episode Return', fontsize=11)
    ax5.set_title('TD3 Delayed Policy Update\n'
                 'Update actor less frequently than critic for stability',
                 fontsize=12, fontweight='bold')
    ax5.legend(loc='lower right')
    ax5.grid(True, alpha=0.3)
    
    # 6. 最大熵RL(SAC)的探索优势
    ax6 = axes[1, 2]
    
    # 模拟多模态奖励函数(两个峰值)
    x = np.linspace(-3, 3, 100)
    reward_landscape = np.exp(-(x - 1.5)**2 / 0.3) + 0.8 * np.exp(-(x + 1.5)**2 / 0.3)
    
    # 确定性策略(如DDPG/TD3):容易陷入第一个遇到的峰值
    deterministic_traj = []
    current_x = -2.5
    lr = 0.1
    for _ in range(50):
        # 梯度上升
        grad = (reward_landscape[int(current_x * 10 + 30) + 1] - 
                reward_landscape[int(current_x * 10 + 30)]) * 10
        current_x += lr * grad
        current_x = np.clip(current_x, -3, 3)
        deterministic_traj.append(current_x)
        if abs(grad) < 0.01:
            break
    
    # 最大熵策略(SAC):保持探索,可能找到更高峰值
    # 模拟:有概率探索到右侧峰值
    entropy_traj = []
    current_x = -2.5
    for step in range(50):
        # 添加熵驱动的探索噪声
        noise = np.random.randn() * (0.5 * np.exp(-step * 0.05))  # 随时间衰减
        # 向高奖励方向移动,但保持随机性
        if step < 20:  # 早期探索
            move = np.random.choice([-0.2, 0.2])
        else:  # 后期利用
            grad = (reward_landscape[min(int(current_x * 10 + 30) + 1, 99)] - 
                   reward_landscape[max(int(current_x * 10 + 30), 0)]) * 10
            move = lr * grad + noise * 0.3
        
        current_x += move
        current_x = np.clip(current_x, -3, 3)
        entropy_traj.append(current_x)
    
    ax6.plot(x, reward_landscape, 'k-', linewidth=2, label='Reward Landscape')
    ax6.plot(deterministic_traj, 
            [reward_landscape[int(t * 10 + 30)] for t in deterministic_traj],
            'o-', color='red', markersize=6, label='Deterministic Policy (Local Optimum)',
            alpha=0.8)
    ax6.plot(entropy_traj, 
            [reward_landscape[min(max(int(t * 10 + 30), 0), 99)] for t in entropy_traj],
            's-', color='blue', markersize=5, label='Max-Entropy Policy (Better Exploration)',
            alpha=0.8)
    
    ax6.set_xlabel('State Space', fontsize=11)
    ax6.set_ylabel('Reward', fontsize=11)
    ax6.set_title('Maximum Entropy RL: Multi-modal Exploration\n'
                 'SAC explores multiple modes, avoids local optima',
                 fontsize=12, fontweight='bold')
    ax6.legend(loc='upper right')
    ax6.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('sac_td3_continuous_control.png', dpi=150, bbox_inches='tight')
    print("可视化已保存至 sac_td3_continuous_control.png")
    plt.show()


if __name__ == "__main__":
    visualize_sac_td3()

14.3 离线强化学习与世界模型

14.3.1 分布偏移与外推误差:CQL(保守Q学习)的悲观值估计与正则项,IQL(隐式Q学习)的期望回归与AWR(优势加权回归)

离线强化学习旨在从预先收集的静态数据集中学习策略,无需在线交互。与标准在线方法不同,离线设置面临分布偏移与外推误差:学习策略可能查询数据集支撑外的动作,导致值函数外推至未探索区域,产生严重的过估计偏差。CQL通过向标准贝尔曼备份添加保守正则项,在保持对数据集中动作的准确估计的同时,惩罚对未见过动作的高Q值预测。这种悲观主义原则确保策略不会偏好那些因外推误差而被错误高估的分布外动作。

IQL采用隐式Q学习框架避免直接查询分布外动作的Q值。通过将Q函数分解为状态值函数与优势函数,并利用期望回归仅基于数据集动作估计状态值,IQL完全规避了对未知动作的评估。AWR作为相关方法,通过优势加权的行为克隆调整数据分布,仅强化那些估计优势为正的数据点,自然约束策略保持在数据支撑内,同时逐步向高回报区域移动。

脚本:offline_rl_cql_iql.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
脚本内容:离线强化学习(CQL与IQL)实现
使用方式:直接运行 python offline_rl_cql_iql.py
依赖包:torch, numpy, matplotlib, gymnasium
功能说明:
1. 实现CQL的保守Q值正则项(降低分布外动作的Q值)
2. 实现IQL的隐式Q学习(期望回归与AWR策略提取)
3. 可视化分布偏移问题与外推误差
4. 对比离线RL与在线RL的学习曲线与策略保守性
"""

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
from torch.utils.data import Dataset, DataLoader
import random


class OfflineDataset(Dataset):
    """离线数据集(模拟收集的静态数据)"""
    
    def __init__(self, num_samples=10000, state_dim=10, action_dim=4, 
                 data_quality='mixed'):  # 'good', 'mixed', 'random'
        self.states = torch.randn(num_samples, state_dim)
        self.next_states = torch.randn(num_samples, state_dim)
        
        # 根据数据质量生成动作
        if data_quality == 'random':
            # 随机动作(均匀分布)
            self.actions = torch.randint(0, action_dim, (num_samples,))
            self.rewards = torch.randn(num_samples) * 0.5 - 0.5  # 负奖励为主
        elif data_quality == 'good':
            # 好的动作(偏向某些维度)
            self.actions = torch.randint(0, action_dim, (num_samples,))
            self.rewards = torch.randn(num_samples) * 0.5 + 3.0  # 高奖励
        else:  # mixed
            # 混合:部分好,部分坏(模拟真实离线数据)
            good_mask = torch.rand(num_samples) < 0.3
            self.actions = torch.randint(0, action_dim, (num_samples,))
            self.rewards = torch.randn(num_samples) * 0.5
            self.rewards[good_mask] += 5.0
            self.rewards[~good_mask] -= 1.0
        
        self.dones = torch.zeros(num_samples)
        self.dones[torch.rand(num_samples) < 0.01] = 1.0  # 1%终止
        
        self.size = num_samples
    
    def __len__(self):
        return self.size
    
    def __getitem__(self, idx):
        return (self.states[idx], self.actions[idx], self.rewards[idx],
                self.next_states[idx], self.dones[idx])


class QNetwork(nn.Module):
    """Q网络(用于CQL和IQL)"""
    
    def __init__(self, state_dim, action_dim, hidden_dim=256):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, action_dim)  # 输出每个动作的Q值
        )
    
    def forward(self, state):
        return self.net(state)


class ValueNetwork(nn.Module):
    """价值网络V(s)(用于IQL)"""
    
    def __init__(self, state_dim, hidden_dim=256):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 1)
        )
    
    def forward(self, state):
        return self.net(state)


class PolicyNetwork(nn.Module):
    """策略网络(用于AWR)"""
    
    def __init__(self, state_dim, action_dim, hidden_dim=256):
        super().__init__()
        self.logits_net = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, action_dim)
        )
    
    def forward(self, state):
        logits = self.logits_net(state)
        return F.softmax(logits, dim=-1), logits


class CQLAgent:
    """保守Q学习(Conservative Q-Learning)智能体"""
    
    def __init__(self, state_dim, action_dim, cql_alpha=1.0):
        self.q_network = QNetwork(state_dim, action_dim)
        self.q_target = QNetwork(state_dim, action_dim)
        self.q_target.load_state_dict(self.q_network.state_dict())
        
        self.optimizer = optim.Adam(self.q_network.parameters(), lr=3e-4)
        self.cql_alpha = cql_alpha  # CQL正则项系数
        
        self.gamma = 0.99
        self.tau = 0.005
    
    def cql_regularizer(self, q_values, actions):
        """
        CQL核心:保守正则项
        鼓励数据集中的动作有高Q值,未见过动作有低Q值
        """
        # 数据集中动作的Q值(取实际采取的动作)
        dataset_q = q_values.gather(1, actions.unsqueeze(1)).squeeze()
        
        # 所有动作的logsumexp(softmax的平滑版本)
        # logsumexp计算了隐含的对所有动作的求和,但数值更稳定
        logsumexp_q = torch.logsumexp(q_values, dim=1)
        
        # CQL损失:鼓励 logsumexp(Q) 远大于 dataset_Q
        # 即让未采取动作的Q值尽可能低
        cql_loss = (logsumexp_q - dataset_q).mean()
        
        return cql_loss
    
    def update(self, batch):
        """CQL更新(带保守正则项)"""
        states, actions, rewards, next_states, dones = batch
        
        # 标准Q学习目标
        with torch.no_grad():
            next_q = self.q_target(next_states)
            next_q_max = next_q.max(1)[0]
            q_target = rewards + (1 - dones) * self.gamma * next_q_max
        
        current_q = self.q_network(states)
        current_q_values = current_q.gather(1, actions.unsqueeze(1)).squeeze()
        
        # 标准贝尔曼误差
        bellman_loss = F.mse_loss(current_q_values, q_target)
        
        # CQL保守正则项
        cql_loss = self.cql_regularizer(current_q, actions)
        
        # 总损失:平衡贝尔曼一致性与保守性
        total_loss = bellman_loss + self.cql_alpha * cql_loss
        
        self.optimizer.zero_grad()
        total_loss.backward()
        self.optimizer.step()
        
        # 软更新目标网络
        for param, target_param in zip(self.q_network.parameters(), 
                                      self.q_target.parameters()):
            target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
        
        return {
            'bellman_loss': bellman_loss.item(),
            'cql_loss': cql_loss.item(),
            'total_loss': total_loss.item()
        }
    
    def get_policy(self, state):
        """基于保守Q值提取策略(epsilon-贪婪)"""
        with torch.no_grad():
            q_values = self.q_network(state)
            return q_values.argmax(dim=1)


class IQLAgent:
    """隐式Q学习(Implicit Q-Learning)智能体"""
    
    def __init__(self, state_dim, action_dim, expectile=0.7, temperature=3.0):
        # 期望回归用于V(s)学习
        self.v_network = ValueNetwork(state_dim)
        self.q_network = QNetwork(state_dim, action_dim)
        self.policy = PolicyNetwork(state_dim, action_dim)
        
        self.v_optimizer = optim.Adam(self.v_network.parameters(), lr=3e-4)
        self.q_optimizer = optim.Adam(self.q_network.parameters(), lr=3e-4)
        self.policy_optimizer = optim.Adam(self.policy.parameters(), lr=3e-4)
        
        self.expectile = expectile  # 期望分位数(不对称损失)
        self.temperature = temperature  # AWR温度参数
        self.gamma = 0.99
    
    def expectile_loss(self, diff, expectile):
        """不对称L2损失(期望回归)"""
        weight = torch.where(diff > 0, expectile, 1 - expectile)
        return weight * (diff ** 2)
    
    def update_v(self, states, actions, rewards, next_states, dones):
        """更新V(s)使用期望回归(避免查询分布外动作)"""
        with torch.no_grad():
            next_v = self.v_network(next_states).squeeze()
            target_q = rewards + (1 - dones) * self.gamma * next_v
            
            # 获取当前状态对实际采取动作的Q估计
            current_q = self.q_network(states).gather(1, actions.unsqueeze(1)).squeeze()
        
        # V的目标是介于期望回归位置
        v_pred = self.v_network(states).squeeze()
        
        # 期望损失:对高估和低估赋予不同权重
        # 高expectile(如0.7)意味着更关注高估(分位数回归)
        loss = self.expectile_loss(current_q - v_pred, self.expectile).mean()
        
        self.v_optimizer.zero_grad()
        loss.backward()
        self.v_optimizer.step()
        
        return loss.item()
    
    def update_q(self, states, actions, rewards, next_states, dones):
        """标准Q更新(但只用于支撑集中的动作)"""
        with torch.no_grad():
            next_v = self.v_network(next_states).squeeze()
            q_target = rewards + (1 - dones) * self.gamma * next_v
        
        current_q = self.q_network(states).gather(1, actions.unsqueeze(1)).squeeze()
        loss = F.mse_loss(current_q, q_target)
        
        self.q_optimizer.zero_grad()
        loss.backward()
        self.q_optimizer.step()
        
        return loss.item()
    
    def update_policy(self, states, actions):
        """AWR(优势加权回归):基于优势的行为克隆"""
        with torch.no_grad():
            v = self.v_network(states).squeeze()
            q = self.q_network(states).gather(1, actions.unsqueeze(1)).squeeze()
            advantage = q - v  # 优势估计
            
            # 计算权重:exp(advantage / temperature)
            weights = torch.exp(advantage / self.temperature)
            weights = torch.clamp(weights, max=100.0)  # 防止爆炸
        
        # 策略优化:加权的行为克隆
        probs, logits = self.policy(states)
        log_probs = F.log_softmax(logits, dim=1).gather(1, actions.unsqueeze(1)).squeeze()
        
        # 加权对数似然
        loss = -(weights * log_probs).mean()
        
        self.policy_optimizer.zero_grad()
        loss.backward()
        self.policy_optimizer.step()
        
        return loss.item()
    
    def update(self, batch):
        """完整IQL更新流程"""
        states, actions, rewards, next_states, dones = batch
        
        v_loss = self.update_v(states, actions, rewards, next_states, dones)
        q_loss = self.update_q(states, actions, rewards, next_states, dones)
        policy_loss = self.update_policy(states, actions)
        
        return {
            'v_loss': v_loss,
            'q_loss': q_loss,
            'policy_loss': policy_loss
        }


def visualize_offline_rl():
    """可视化离线强化学习机制"""
    
    # 创建模拟数据集
    dataset = OfflineDataset(num_samples=5000, data_quality='mixed')
    dataloader = DataLoader(dataset, batch_size=256, shuffle=True)
    
    # 初始化智能体
    state_dim = 10
    action_dim = 4
    cql_agent = CQLAgent(state_dim, action_dim, cql_alpha=1.0)
    iql_agent = IQLAgent(state_dim, action_dim, expectile=0.7, temperature=3.0)
    
    # 训练几轮以获取数据用于可视化
    cql_losses = []
    iql_losses = []
    
    for i, batch in enumerate(dataloader):
        if i > 50:  # 仅训练少量轮次用于演示
            break
        
        cql_info = cql_agent.update(batch)
        iql_info = iql_agent.update(batch)
        
        cql_losses.append(cql_info['total_loss'])
        iql_losses.append(iql_info['q_loss'])
    
    fig, axes = plt.subplots(2, 3, figsize=(16, 10))
    
    # 1. 分布偏移问题示意图(外推误差)
    ax1 = axes[0, 0]
    
    # 模拟Q值对分布内vs分布外动作的估计
    in_dist_q = np.random.randn(100) * 0.5 + 5  # 分布内:合理估计
    out_dist_q = np.random.randn(50) * 1.0 + 15  # 分布外:严重过估计
    
    ax1.hist(in_dist_q, bins=20, alpha=0.7, color='green', label='In-distribution actions (Dataset)')
    ax1.hist(out_dist_q, bins=20, alpha=0.7, color='red', label='Out-of-distribution actions (Extrapolation)')
    ax1.axvline(x=5, color='blue', linestyle='--', linewidth=2, label='True Value')
    
    ax1.set_xlabel('Q-Value Estimate', fontsize=11)
    ax1.set_ylabel('Frequency', fontsize=11)
    ax1.set_title('Distribution Shift: Extrapolation Error\n'
                 'Standard RL overestimates unseen actions',
                 fontsize=12, fontweight='bold')
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    
    # 2. CQL保守正则项效果
    ax2 = axes[0, 1]
    
    # 模拟CQL训练前后的Q值分布变化
    iterations = 50
    # 标准Q学习:分布外Q值持续上升(过估计)
    standard_ood = [10 + i * 0.5 + np.random.randn() * 2 for i in range(iterations)]
    # CQL:分布外Q值被抑制,分布内Q值保持稳定
    cql_indist = [5 + np.random.randn() * 0.5 for i in range(iterations)]
    cql_ood = [5 - i * 0.1 + np.random.randn() * 0.5 for i in range(iterations)]  # 逐渐降低
    
    ax2.plot(range(iterations), standard_ood, 'r--', label='Standard Q (OOD actions)', alpha=0.7)
    ax2.plot(range(iterations), cql_indist, 'g-', linewidth=2, label='CQL (In-distribution)')
    ax2.plot(range(iterations), cql_ood, 'b-', linewidth=2, label='CQL (OOD actions, suppressed)')
    
    ax2.axhline(y=5, color='gray', linestyle=':', alpha=0.5, label='True Value')
    ax2.set_xlabel('Training Iteration', fontsize=11)
    ax2.set_ylabel('Q-Value Estimate', fontsize=11)
    ax2.set_title('CQL Conservative Regularization\n'
                 'Pushes down OOD action values while keeping in-dist values',
                 fontsize=12, fontweight='bold')
    ax2.legend()
    ax2.grid(True, alpha=0.3)
    
    # 3. IQL期望回归(Expectile Regression)的不对称损失
    ax3 = axes[0, 2]
    
    # 展示不同expectile下的损失函数
    diff = np.linspace(-5, 5, 100)
    expectiles = [0.1, 0.5, 0.7, 0.9]
    colors = ['blue', 'green', 'orange', 'red']
    
    for exp, color in zip(expectiles, colors):
        if exp == 0.5:
            loss = diff ** 2  # 标准MSE
        else:
            # 不对称损失
            loss = np.where(diff > 0, exp * diff**2, (1-exp) * diff**2)
        ax3.plot(diff, loss, color=color, linewidth=2, label=f'Expectile τ={exp}')
    
    ax3.axvline(x=0, color='black', linestyle='--', alpha=0.3)
    ax3.set_xlabel('Error (Q - V)', fontsize=11)
    ax3.set_ylabel('Loss', fontsize=11)
    ax3.set_title('IQL Expectile Regression\n'
                 'τ>0.5 penalizes underestimation more (optimistic)',
                 fontsize=12, fontweight='bold')
    ax3.legend()
    ax3.grid(True, alpha=0.3)
    
    # 4. AWR优势加权权重分布
    ax4 = axes[1, 0]
    
    # 模拟优势值和对应的AWR权重
    advantages = np.linspace(-5, 5, 100)
    temperature = 3.0
    weights = np.exp(advantages / temperature)
    weights = np.clip(weights, 0, 100)  # 裁剪防止爆炸
    
    ax4.plot(advantages, weights, 'b-', linewidth=3, label=f'Weight = exp(A/τ), τ={temperature}')
    ax4.axvline(x=0, color='red', linestyle='--', alpha=0.5, label='Zero advantage threshold')
    ax4.fill_between(advantages, 0, weights, where=(advantages > 0), 
                    alpha=0.3, color='green', label='Up-weighted (A>0)')
    ax4.fill_between(advantages, 0, weights, where=(advantages < 0),
                    alpha=0.3, color='red', label='Down-weighted (A<0)')
    
    ax4.set_xlabel('Advantage A(s,a)', fontsize=11)
    ax4.set_ylabel('Sampling Weight', fontsize=11)
    ax4.set_title('AWR Advantage Weighted Regression\n'
                 'High advantage actions weighted more in BC loss',
                 fontsize=12, fontweight='bold')
    ax4.legend()
    ax4.set_yscale('log')
    ax4.grid(True, alpha=0.3)
    
    # 5. 离线RL方法对比(学习曲线)
    ax5 = axes[1, 1]
    
    # 模拟不同方法的性能(在固定数据集上)
    epochs = range(100)
    
    # 标准Q学习:过估计导致性能崩溃
    standard_q = [10 + 2 * (1 - np.exp(-ep * 0.05)) for ep in epochs[:30]]
    standard_q.extend([12 - (ep-30) * 0.3 for ep in epochs[30:60]])  # 崩溃
    standard_q.extend([2 for ep in epochs[60:]])  # 稳定在低性能
    
    # CQL:保守但稳定,避免崩溃
    cql_perf = [10 + 4 * (1 - np.exp(-ep * 0.03)) + np.random.randn() * 0.2 for ep in epochs]
    cql_perf = np.maximum.accumulate(cql_perf)
    
    # IQL:通过期望回归稳定学习
    iql_perf = [10 + 3.5 * (1 - np.exp(-ep * 0.04)) + np.random.randn() * 0.3 for ep in epochs]
    iql_perf = np.maximum.accumulate(iql_perf)
    
    # 行为克隆(BC):上限受限
    bc_perf = [10 + 1 * (1 - np.exp(-ep * 0.1)) for ep in epochs]
    
    ax5.plot(epochs, standard_q, 'o-', label='Standard Q-Learning (Fails)', 
            color='red', alpha=0.7, markersize=3)
    ax5.plot(epochs, cql_perf, 's-', label='CQL (Conservative)', 
            color='forestgreen', linewidth=2, markersize=3)
    ax5.plot(epochs, iql_perf, '^-', label='IQL (Implicit)', 
            color='steelblue', linewidth=2, markersize=3)
    ax5.plot(epochs, bc_perf, '--', label='Behavior Cloning (Baseline)', 
            color='gray', alpha=0.7)
    
    ax5.set_xlabel('Training Epoch', fontsize=11)
    ax5.set_ylabel('Policy Performance (Normalized)', fontsize=11)
    ax5.set_title('Offline RL Comparison on Fixed Dataset\n'
                 'CQL/IQL avoid extrapolation error collapse',
                 fontsize=12, fontweight='bold')
    ax5.legend()
    ax5.grid(True, alpha=0.3)
    
    # 6. 离线RL架构对比图
    ax6 = axes[1, 2]
    ax6.set_xlim(0, 10)
    ax6.set_ylim(0, 10)
    ax6.axis('off')
    ax6.set_title('Offline RL Methods Architecture', fontsize=12, fontweight='bold')
    
    # CQL架构
    ax6.add_patch(plt.Rectangle((0.5, 6), 4, 3.5, facecolor='lightcoral', 
                               edgecolor='red', linewidth=2, alpha=0.5))
    ax6.text(2.5, 9.2, 'CQL: Conservative Q-Learning', ha='center', 
            fontsize=11, fontweight='bold', color='red')
    cql_steps = [
        '1. Standard Bellman Backup',
        '2. + CQL Regularizer: logsumexp(Q) - Q(dataset)',
        '3. Minimize Q-values for unseen actions',
        '4. Result: Pessimistic value estimates'
    ]
    for i, step in enumerate(cql_steps):
        ax6.text(2.5, 8.5 - i*0.6, step, ha='center', fontsize=9, style='italic')
    
    # IQL架构
    ax6.add_patch(plt.Rectangle((5.5, 6), 4, 3.5, facecolor='lightblue',
                               edgecolor='blue', linewidth=2, alpha=0.5))
    ax6.text(7.5, 9.2, 'IQL: Implicit Q-Learning', ha='center',
            fontsize=11, fontweight='bold', color='blue')
    iql_steps = [
        '1. Expectile Regression for V(s)',
        '2. Avoid querying unseen actions',
        '3. AWR: Weighted behavior cloning',
        '4. Result: Policy stays in data support'
    ]
    for i, step in enumerate(iql_steps):
        ax6.text(7.5, 8.5 - i*0.6, step, ha='center', fontsize=9, style='italic')
    
    # 底部:核心问题
    ax6.add_patch(plt.Rectangle((2, 1), 6, 4.5, facecolor='wheat', 
                               edgecolor='black', linewidth=2))
    ax6.text(5, 5.2, 'Core Challenge: Distribution Shift', ha='center',
            fontsize=12, fontweight='bold')
    ax6.text(5, 4.5, 'Offline RL: Learn from fixed dataset without exploration', 
            ha='center', fontsize=10, style='italic')
    ax6.text(5, 3.8, 'Problem: Policy may query states/actions not in data', 
            ha='center', fontsize=10, style='italic')
    ax6.text(5, 3.1, 'Result: Q-network extrapolates to unrealistic high values', 
            ha='center', fontsize=10, style='italic')
    ax6.text(5, 2.4, 'Solution: CQL (pessimism) or IQL (avoid query)', 
            ha='center', fontsize=10, style='italic', color='darkgreen')
    
    plt.tight_layout()
    plt.savefig('offline_rl_cql_iql.png', dpi=150, bbox_inches='tight')
    print("可视化已保存至 offline_rl_cql_iql.png")
    plt.show()


if __name__ == "__main__":
    visualize_offline_rl()

14.3.2 基于模型的RL:MuZero的MCTS规划与潜在动力学模型,Dreamer的RSSM(循环状态空间模型)与想象轨迹训练

基于模型的强化学习通过学习环境动力学模型,允许智能体在内部模拟未来轨迹进行规划。MuZero在未知环境中同时学习值函数、策略与动力学模型,无需真实环境动力学知识。通过结合深度学习与蒙特卡洛树搜索,MuZero在潜在状态空间执行规划,预测未来奖励、价值与策略,在国际象棋、围棋与Atari游戏上达到超人类性能。其核心创新在于搜索与学习的深度整合:MCTS生成的搜索策略作为训练目标,同时网络预测支撑MCTS中的前向模拟。

Dreamer系列算法专注于高效的世界模型学习,通过循环状态空间模型紧凑表示环境动态。RSSM将确定性的循环状态与随机性的潜在变量分离,在保持预测能力的同时支持通过重参数化技巧进行可微分训练。Dreamer在想象的潜在轨迹上直接执行策略优化,无需真实环境交互即可更新策略,实现了极高的样本效率。通过端到端训练,Dreamer学习压缩观测至抽象状态,过滤无关细节,专注于预测未来任务相关的关键因素。

脚本:world_models_muzero_dreamer.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
脚本内容:基于模型的RL(MuZero与Dreamer)实现
使用方式:直接运行 python world_models_muzero_dreamer.py
依赖包:torch, numpy, matplotlib, gymnasium
功能说明:
1. 实现MuZero的MCTS规划(UCB选择、扩展、反向传播)
2. 实现Dreamer的RSSM(循环状态空间模型)组件
3. 可视化MCTS搜索树与潜在状态空间轨迹
4. 对比模型预测误差与真实环境动态
"""

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
from collections import deque
import random
import math


class MuZeroMCTS:
    """MuZero的MCTS实现(潜在空间规划)"""
    
    def __init__(self, num_actions, c_puct=1.25, num_simulations=50):
        self.num_actions = num_actions
        self.c_puct = c_puct  # UCB探索常数
        self.num_simulations = num_simulations
        
        # 节点统计
        self.N = {}  # 访问次数
        self.W = {}  # 累积价值
        self.Q = {}  # 平均价值
        self.P = {}  # 先验概率
    
    def search(self, root_state, prediction_fn, dynamics_fn, representation_fn=None):
        """
        MCTS规划
        prediction_fn: (state) -> (policy_logits, value)
        dynamics_fn: (state, action) -> (next_state, reward)
        """
        # 根节点初始化
        root_key = self._state_to_key(root_state)
        
        if root_key not in self.N:
            # 扩展根节点
            policy_logits, value = prediction_fn(root_state)
            policy = F.softmax(policy_logits, dim=-1).detach().cpu().numpy().flatten()
            
            self.N[root_key] = np.zeros(self.num_actions)
            self.W[root_key] = np.zeros(self.num_actions)
            self.Q[root_key] = np.zeros(self.num_actions)
            self.P[root_key] = policy
        
        for _ in range(self.num_simulations):
            # 1. 选择:从根节点沿UCB路径向下
            trajectory = []
            current_state = root_state
            current_key = root_key
            
            while current_key in self.N and np.sum(self.N[current_key]) > 0:
                # UCB公式:Q + c_puct * P * sqrt(N_parent) / (1 + N)
                ucb_scores = self.Q[current_key] + self.c_puct * self.P[current_key] * \
                           math.sqrt(np.sum(self.N[current_key])) / (1 + self.N[current_key])
                
                action = np.argmax(ucb_scores)
                trajectory.append((current_key, action))
                
                # 模拟状态转移(使用学到的动力学模型)
                next_state, _ = dynamics_fn(current_state, torch.tensor([action]))
                current_state = next_state
                current_key = self._state_to_key(current_state)
                
                # 检查是否到达叶节点
                if current_key not in self.N:
                    break
            
            # 2. 扩展与评估
            if current_key not in self.N:
                policy_logits, value = prediction_fn(current_state)
                policy = F.softmax(policy_logits, dim=-1).detach().cpu().numpy().flatten()
                
                self.N[current_key] = np.zeros(self.num_actions)
                self.W[current_key] = np.zeros(self.num_actions)
                self.Q[current_key] = np.zeros(self.num_actions)
                self.P[current_key] = policy
            else:
                _, value = prediction_fn(current_state)
            
            # 3. 反向传播
            for key, action in reversed(trajectory):
                self.N[key][action] += 1
                self.W[key][action] += value
                self.Q[key][action] = self.W[key][action] / self.N[key][action]
        
        # 返回搜索后的动作分布(基于访问次数)
        action_probs = self.N[root_key] / np.sum(self.N[root_key])
        return action_probs
    
    def _state_to_key(self, state):
        """将状态转换为可哈希的键"""
        if isinstance(state, torch.Tensor):
            state = state.detach().cpu().numpy()
        return tuple(state.flatten().round(decimals=4))


class RSSM(nn.Module):
    """循环状态空间模型(Dreamer核心)"""
    
    def __def__(self, state_dim=30, action_dim=4, hidden_dim=200, latent_dim=30):
        super().__init__()
        self.state_dim = state_dim
        self.latent_dim = latent_dim
        
        # 确定性路径(RNN)
        self.gru = nn.GRUCell(latent_dim + action_dim, hidden_dim)
        
        # 随机性路径(VAE风格)
        self.prior_mean = nn.Linear(hidden_dim, latent_dim)
        self.prior_std = nn.Linear(hidden_dim, latent_dim)
        
        # 后验编码器(观测到潜在状态)
        self.encoder = nn.Sequential(
            nn.Linear(state_dim + hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, latent_dim * 2)  # mean, std
        )
        
        # 观测解码器(重构)
        self.decoder = nn.Sequential(
            nn.Linear(hidden_dim + latent_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, state_dim)
        )
        
        # 奖励预测
        self.reward_predictor = nn.Sequential(
            nn.Linear(hidden_dim + latent_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 1)
        )
        
        self.hidden_dim = hidden_dim
    
    def imagine(self, prev_hidden, prev_state, action):
        """想象一步(使用先验,无观测)"""
        # 确定性更新
        gru_input = torch.cat([prev_state, action], dim=-1)
        hidden = self.gru(gru_input, prev_hidden)
        
        # 随机性先验
        prior_mean = self.prior_mean(hidden)
        prior_std = F.softplus(self.prior_std(hidden)) + 0.1
        
        # 采样(重参数化)
        noise = torch.randn_like(prior_mean)
        state = prior_mean + prior_std * noise
        
        return hidden, state, prior_mean, prior_std
    
    def observe(self, hidden, obs):
        """基于观测更新(后验)"""
        # 编码器:从观测推断潜在状态
        encoded = self.encoder(torch.cat([obs, hidden], dim=-1))
        post_mean, post_std_logit = torch.chunk(encoded, 2, dim=-1)
        post_std = F.softplus(post_std_logit) + 0.1
        
        # 采样
        noise = torch.randn_like(post_mean)
        state = post_mean + post_std * noise
        
        return state, post_mean, post_std
    
    def decode(self, hidden, state):
        """从潜在状态重构观测"""
        h = torch.cat([hidden, state], dim=-1)
        obs_recon = self.decoder(h)
        return obs_recon
    
    def predict_reward(self, hidden, state):
        """预测奖励"""
        h = torch.cat([hidden, state], dim=-1)
        return self.reward_predictor(h)


class DreamerAgent:
    """Dreamer风格智能体(基于RSSM)"""
    
    def __init__(self, state_dim, action_dim):
        self.rssm = RSSM(state_dim, action_dim)
        
        # 策略(actor)在想象轨迹上训练
        self.actor = nn.Sequential(
            nn.Linear(230, 400),  # hidden + latent
            nn.ReLU(),
            nn.Linear(400, action_dim),
            nn.Tanh()
        )
        
        # 评价器(critic)预测期望回报
        self.critic = nn.Sequential(
            nn.Linear(230, 400),
            nn.ReLU(),
            nn.Linear(400, 1)
        )
        
        self.optimizer = optim.Adam(
            list(self.rssm.parameters()) + 
            list(self.actor.parameters()) + 
            list(self.critic.parameters()), 
            lr=1e-4
        )
        
        self.gamma = 0.99
        self.lambda_ = 0.95  # GAE lambda
    
    def imagine_trajectory(self, start_hidden, start_state, horizon=15):
        """在潜在空间中想象未来轨迹"""
        states = [start_state]
        hiddens = [start_hidden]
        actions = []
        rewards = []
        
        hidden, state = start_hidden, start_state
        
        for t in range(horizon):
            # 使用当前策略选择动作
            actor_input = torch.cat([hidden, state], dim=-1)
            action = self.actor(actor_input)
            
            # 想象下一步(动力学模型)
            hidden, state, _, _ = self.rssm.imagine(hidden, state, action)
            
            # 预测奖励
            reward = self.rssm.predict_reward(hidden, state)
            
            hiddens.append(hidden)
            states.append(state)
            actions.append(action)
            rewards.append(reward)
        
        return hiddens, states, actions, rewards
    
    def update(self, obs_seq, action_seq, reward_seq):
        """
        Dreamer更新:
        1. 训练世界模型(RSSM)
        2. 想象轨迹上的策略优化
        """
        batch_size = obs_seq.size(0)
        seq_len = obs_seq.size(1)
        
        # 初始化
        hidden = torch.zeros(batch_size, 200)
        state = torch.zeros(batch_size, 30)
        
        # 观测编码(后验)
        rssm_loss = 0
        
        for t in range(seq_len):
            # 观测更新
            state, post_mean, post_std = self.rssm.observe(hidden, obs_seq[:, t])
            
            # 想象对比(先验vs后验)
            if t > 0:
                prior_mean = self.rssm.prior_mean(hidden)
                prior_std = F.softplus(self.rssm.prior_std(hidden)) + 0.1
                
                # KL散度(后验||先验)
                kl = torch.log(prior_std/post_std) + (post_std**2 + (post_mean-prior_mean)**2)/(2*prior_std**2) - 0.5
                rssm_loss += kl.mean()
                
                # 重构损失
                obs_recon = self.rssm.decode(hidden, state)
                recon_loss = F.mse_loss(obs_recon, obs_seq[:, t])
                rssm_loss += recon_loss
                
                # 奖励预测损失
                reward_pred = self.rssm.predict_reward(hidden, state)
                rssm_loss += F.mse_loss(reward_pred.squeeze(), reward_seq[:, t])
            
            # GRU更新(确定性路径)
            if t < seq_len - 1:
                gru_input = torch.cat([state, action_seq[:, t]], dim=-1)
                hidden = self.rssm.gru(gru_input, hidden)
        
        # 想象轨迹上的策略优化(Dreamer核心)
        with torch.no_grad():
            # 使用当前模型想象
            start_hidden = torch.zeros(batch_size, 200)
            start_state = torch.zeros(batch_size, 30)
        
        imagined_hiddens, imagined_states, imagined_actions, imagined_rewards = \
            self.imagine_trajectory(start_hidden[:1], start_state[:1], horizon=15)
        
        # 计算想象轨迹的回报(用于策略梯度)
        returns = []
        G = 0
        for r in reversed(imagined_rewards):
            G = r + self.gamma * G
            returns.insert(0, G)
        
        actor_loss = -sum(returns) / len(returns)  # 最大化期望回报
        
        # 总损失
        total_loss = rssm_loss + actor_loss * 0.1  # 平衡系数
        
        self.optimizer.zero_grad()
        total_loss.backward()
        torch.nn.utils.clip_grad_norm_(self.rssm.parameters(), 10)
        self.optimizer.step()
        
        return {
            'rssm_loss': rssm_loss.item(),
            'actor_loss': actor_loss.item()
        }


def visualize_world_models():
    """可视化世界模型方法"""
    
    fig, axes = plt.subplots(2, 3, figsize=(16, 10))
    
    # 1. MuZero MCTS搜索树可视化
    ax1 = axes[0, 0]
    ax1.set_xlim(0, 10)
    ax1.set_ylim(0, 10)
    
    # 绘制简化的MCTS树
    # 根节点
    ax1.add_patch(plt.Circle((5, 9), 0.4, facecolor='gold', edgecolor='black', linewidth=2))
    ax1.text(5, 9, 'Root\ns', ha='center', va='center', fontsize=9, fontweight='bold')
    
    # 第一层(动作)
    actions_x = [2, 5, 8]
    for i, x in enumerate(actions_x):
        ax1.add_patch(plt.Circle((x, 6.5), 0.3, facecolor='lightblue', edgecolor='black'))
        ax1.text(x, 6.5, f'a{i}', ha='center', va='center', fontsize=8)
        ax1.plot([5, x], [8.6, 6.8], 'k-', alpha=0.5)
        
        # 第二层(状态)
        next_x = x + np.random.randn() * 0.3
        ax1.add_patch(plt.Circle((next_x, 4), 0.3, facecolor='lightgreen', edgecolor='black'))
        ax1.text(next_x, 4, f's\'{i}', ha='center', va='center', fontsize=8)
        ax1.plot([x, next_x], [6.2, 4.3], 'k-', alpha=0.5)
        
        # 价值标注
        value = 0.5 + np.random.rand() * 0.5
        ax1.text(next_x, 3.2, f'V={value:.2f}', fontsize=7, ha='center')
    
    # UCB标注
    ax1.text(5, 1, 'UCB Selection: argmax(Q + c_puct * P * sqrt(N_parent)/(1+N))',
            ha='center', fontsize=9, style='italic',
            bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.7))
    
    ax1.set_title('MuZero MCTS Tree Search\nPlanning in Latent Space', 
                 fontsize=12, fontweight='bold')
    ax1.axis('off')
    
    # 2. Dreamer RSSM结构
    ax2 = axes[0, 1]
    ax2.set_xlim(0, 10)
    ax2.set_ylim(0, 10)
    ax2.axis('off')
    ax2.set_title('Dreamer RSSM: Recurrent State-Space Model', 
                 fontsize=12, fontweight='bold')
    
    # 时间步t
    y_t = 8
    ax2.add_patch(plt.Rectangle((1, y_t-0.5), 1.5, 1, facecolor='lightyellow', edgecolor='black'))
    ax2.text(1.75, y_t, 'h_t\n(deterministic)', ha='center', va='center', fontsize=9)
    
    ax2.add_patch(plt.Rectangle((4, y_t-0.5), 1.5, 1, facecolor='lightblue', edgecolor='black'))
    ax2.text(4.75, y_t, 's_t\n(stochastic)', ha='center', va='center', fontsize=9)
    
    ax2.add_patch(plt.Rectangle((7, y_t-0.5), 1.5, 1, facecolor='lightcoral', edgecolor='black'))
    ax2.text(7.75, y_t, 'a_t\n(action)', ha='center', va='center', fontsize=9)
    
    # GRU更新
    ax2.annotate('', xy=(1.75, y_t-0.7), xytext=(4.75, y_t-0.7),
                arrowprops=dict(arrowstyle='->', color='blue', lw=2))
    ax2.text(3.25, y_t-1, 'GRU(h_t, [s_t, a_t])', fontsize=8, ha='center', style='italic')
    
    # 时间步t+1
    y_t1 = 4
    ax2.add_patch(plt.Rectangle((1, y_t1-0.5), 1.5, 1, facecolor='lightyellow', edgecolor='black'))
    ax2.text(1.75, y_t1, 'h_{t+1}', ha='center', va='center', fontsize=9)
    
    ax2.add_patch(plt.Rectangle((4, y_t1-0.5), 1.5, 1, facecolor='lightblue', edgecolor='black'))
    ax2.text(4.75, y_t1, 's_{t+1}', ha='center', va='center', fontsize=9)
    
    # 先验采样
    ax2.annotate('', xy=(4.75, y_t1+0.6), xytext=(1.75, y_t-0.6),
                arrowprops=dict(arrowstyle='->', color='green', lw=1.5))
    ax2.text(3, 6, 'Prior: p(s_{t+1}|h_{t+1})', fontsize=8, ha='center', style='italic', color='green')
    
    # 观测更新(后验)
    ax2.add_patch(plt.Rectangle((7, y_t1-0.5), 1.5, 1, facecolor='lightgreen', edgecolor='black'))
    ax2.text(7.75, y_t1, 'o_{t+1}\n(obs)', ha='center', va='center', fontsize=9)
    
    ax2.annotate('', xy=(4.75, y_t1), xytext=(7, y_t1),
                arrowprops=dict(arrowstyle='->', color='red', lw=1.5))
    ax2.text(5.9, y_t1+0.5, 'Encoder', fontsize=8, ha='center', style='italic', color='red')
    ax2.text(5.9, y_t1-0.5, 'Posterior', fontsize=8, ha='center', style='italic', color='red')
    
    # 图例
    ax2.text(5, 1, 'Deterministic path (h): GRU carries info across time\n'
                  'Stochastic path (s): VAE handles uncertainty\n'
                  'Imagining: Use prior (no observation needed)',
            ha='center', fontsize=9, style='italic',
            bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
    
    # 3. 潜在空间轨迹可视化
    ax3 = axes[0, 2]
    
    # 模拟想象轨迹vs真实轨迹
    np.random.seed(42)
    
    # 真实轨迹(2D投影)
    real_traj = np.cumsum(np.random.randn(50, 2) * 0.1, axis=0)
    
    # 想象轨迹(基于模型)
    imagined_traj = real_traj[:10].tolist()
    for i in range(10, 50):
        next_point = imagined_traj[-1] + np.random.randn(2) * 0.15 + 0.01 * (real_traj[i] - imagined_traj[-1])
        imagined_traj.append(next_point)
    imagined_traj = np.array(imagined_traj)
    
    ax3.plot(real_traj[:, 0], real_traj[:, 1], 'o-', label='Real Trajectory', 
            color='blue', alpha=0.7, markersize=4)
    ax3.plot(imagined_traj[:, 0], imagined_traj[:, 1], 's--', label='Imagined Trajectory',
            color='red', alpha=0.7, markersize=4)
    
    # 标记分歧点
    ax3.plot(real_traj[10, 0], real_traj[10, 1], 'o', color='blue', markersize=12, 
            markeredgecolor='black', markeredgewidth=2, label='Divergence Start')
    
    ax3.set_xlabel('Latent Dimension 1', fontsize=11)
    ax3.set_ylabel('Latent Dimension 2', fontsize=11)
    ax3.set_title('Imagination in Latent Space\n'
                 'Model predicts future, compounding error over time',
                 fontsize=12, fontweight='bold')
    ax3.legend()
    ax3.grid(True, alpha=0.3)
    
    # 4. MCTS访问计数与策略改进
    ax4 = axes[1, 0]
    
    # 模拟MCTS迭代过程中的访问分布收敛
    visits_early = np.array([5, 3, 8, 2, 12])  # 早期:探索性
    visits_late = np.array([2, 1, 45, 1, 1])   # 后期:集中于最优
    
    x = np.arange(5)
    width = 0.35
    
    ax4.bar(x - width/2, visits_early/visits_early.sum(), width, label='Early Simulations',
           color='lightblue', alpha=0.8)
    ax4.bar(x + width/2, visits_late/visits_late.sum(), width, label='Late Simulations',
           color='darkblue', alpha=0.8)
    
    ax4.set_xlabel('Action Index', fontsize=11)
    ax4.set_ylabel('Visit Probability', fontsize=11)
    ax4.set_title('MCTS Policy Refinement\n'
                 'Concentrates search on promising actions',
                 fontsize=12, fontweight='bold')
    ax4.legend()
    ax4.grid(True, alpha=0.3, axis='y')
    
    # 5. 模型预测误差累积
    ax5 = axes[1, 1]
    
    horizon = np.arange(1, 51)
    # 单步预测误差小,多步累积误差大
    single_step_error = [0.1] * 50
    multi_step_error = [0.1 * (1.1 ** h) for h in horizon]
    
    ax5.semilogy(horizon, single_step_error, '--', label='Single-step Error (Bounded)',
                color='green', linewidth=2)
    ax5.semilogy(horizon, multi_step_error, '-', label='Multi-step Rollout Error (Compounding)',
                color='red', linewidth=2)
    
    ax5.axhline(y=1.0, color='orange', linestyle=':', alpha=0.7, label='Acceptable threshold')
    ax5.fill_between(horizon, 0.1, 1.0, alpha=0.2, color='green', label='Reliable region')
    ax5.fill_between(horizon, 1.0, 10, alpha=0.2, color='red', label='Unreliable region')
    
    ax5.set_xlabel('Imagination Horizon (steps)', fontsize=11)
    ax5.set_ylabel('Prediction Error (MSE)', fontsize=11)
    ax5.set_title('Model Error Accumulation\n'
                 'Short rollouts reliable, long rollouts diverge',
                 fontsize=12, fontweight='bold')
    ax5.legend()
    ax5.grid(True, alpha=0.3)
    
    # 6. 样本效率对比(模型化vs免模型)
    ax6 = axes[1, 2]
    
    methods = ['DQN\n(Model-free)', 'SAC\n(Model-free)', 'Dreamer\n(Model-based)', 'MuZero\n(Model+Search)']
    sample_efficiency = [1, 3, 20, 50]  # 相对样本效率(越高越好)
    final_performance = [85, 95, 90, 98]  # 最终性能
    
    x = np.arange(len(methods))
    width = 0.35
    
    ax6_twin = ax6.twinx()
    
    bars1 = ax6.bar(x - width/2, sample_efficiency, width, label='Sample Efficiency',
                   color='steelblue', alpha=0.8)
    bars2 = ax6_twin.bar(x + width/2, final_performance, width, label='Final Performance %',
                        color='coral', alpha=0.8)
    
    ax6.set_ylabel('Relative Sample Efficiency', fontsize=11, color='steelblue')
    ax6_twin.set_ylabel('Final Performance (%)', fontsize=11, color='coral')
    ax6.set_title('Model-based vs Model-free RL\n'
                 'Model-based: High efficiency, Comparable performance',
                 fontsize=12, fontweight='bold')
    ax6.set_xticks(x)
    ax6.set_xticklabels(methods, rotation=15, ha='right')
    
    # 添加数值标签
    for bar, val in zip(bars1, sample_efficiency):
        height = bar.get_height()
        ax6.text(bar.get_x() + bar.get_width()/2., height,
                f'{val}x', ha='center', va='bottom', fontsize=10, fontweight='bold')
    
    plt.tight_layout()
    plt.savefig('world_models_muzero_dreamer.png', dpi=150, bbox_inches='tight')
    print("可视化已保存至 world_models_muzero_dreamer.png")
    plt.show()


if __name__ == "__main__":
    visualize_world_models()

14.3.3 离线到在线(Offline-to-Online)微调的策略约束(AWAC/IQL)与不确定性加权

离线到在线强化学习结合了两阶段训练的优势:首先在大型静态数据集上进行离线预训练以获得良好的初始化,随后通过有限的在线交互进行微调以适应目标任务。直接微调面临分布偏移与灾难性遗忘风险,预训练策略可能在在线探索时快速偏离数据分布进入未知区域,导致性能崩溃。

AWAC与IQL等策略约束方法在微调阶段保持对预训练策略的保守调整,通过优势加权回归仅强化那些显著优于预训练策略的动作,自动限制策略更新幅度。不确定性加权机制估计值函数或动力学的置信度,对高不确定性状态-动作对降低学习率或增加探索,确保智能体在可靠数据支撑区域内快速学习,在不确定区域谨慎探索。这种渐进式适应策略有效平衡了样本效率与安全性,在机器人学习、推荐系统等实际场景中实现了从仿真到真实环境的可靠迁移。

脚本:offline_to_online_finetuning.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
脚本内容:Offline-to-Online微调(AWAC策略约束与不确定性加权)
使用方式:直接运行 python offline_to_online_finetuning.py
依赖包:torch, numpy, matplotlib, gymnasium
功能说明:
1. 实现AWAC(加速在线学习)的优势加权策略约束
2. 实现不确定性估计(不确定性加权Q学习)
3. 可视化离线预训练到在线微调的过渡过程
4. 对比直接微调vs约束微调的稳定性差异
"""

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
from torch.utils.data import Dataset
import random


class UncertaintyQNetwork(nn.Module):
    """带不确定性估计的Q网络(贝叶斯近似或集成方法)"""
    
    def __init__(self, state_dim, action_dim, num_ensembles=5):
        super().__init__()
        self.num_ensembles = num_ensembles
        
        # 创建Q网络集成
        self.ensembles = nn.ModuleList([
            nn.Sequential(
                nn.Linear(state_dim + action_dim, 256),
                nn.ReLU(),
                nn.Linear(256, 256),
                nn.ReLU(),
                nn.Linear(256, 1)
            ) for _ in range(num_ensembles)
        ])
    
    def forward(self, state, action, return_uncertainty=False):
        x = torch.cat([state, action], dim=-1)
        
        # 所有集成的预测
        predictions = torch.stack([net(x) for net in self.ensembles], dim=0)  # [ensemble, batch, 1]
        
        mean_q = predictions.mean(dim=0)
        std_q = predictions.std(dim=0)  # 不确定性估计
        
        if return_uncertainty:
            return mean_q, std_q
        return mean_q
    
    def get_uncertainty(self, state, action):
        """获取预测的不确定性"""
        _, std = self.forward(state, action, return_uncertainty=True)
        return std


class AWACAgent:
    """AWAC (Accelerated Weighted Actor-Critic) 智能体"""
    
    def __init__(self, state_dim, action_dim, behavior_clone_coef=0.01):
        # Actor(策略)
        self.actor = nn.Sequential(
            nn.Linear(state_dim, 256),
            nn.ReLU(),
            nn.Linear(256, 256),
            nn.ReLU(),
            nn.Linear(256, action_dim),
            nn.Tanh()
        )
        
        # Critic(Q函数,带不确定性)
        self.critic = UncertaintyQNetwork(state_dim, action_dim)
        self.critic_target = UncertaintyQNetwork(state_dim, action_dim)
        self.critic_target.load_state_dict(self.critic.state_dict())
        
        self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=3e-4)
        self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=3e-4)
        
        self.behavior_clone_coef = behavior_clone_coef  # 行为克隆系数
        self.gamma = 0.99
        self.tau = 0.005
        
        # 预训练模式
        self.pretrained = False
    
    def select_action(self, state, noise=0.1):
        """选择动作(带探索噪声)"""
        state = torch.FloatTensor(state)
        with torch.no_grad():
            action = self.actor(state)
            if noise > 0:
                action += torch.randn_like(action) * noise
                action = torch.clamp(action, -1, 1)
        return action.numpy()
    
    def update_offline(self, batch, behavior_policy=None):
        """离线阶段更新(AWAC)"""
        states, actions, rewards, next_states, dones = batch
        
        # Critic更新(标准贝尔曼)
        with torch.no_grad():
            next_actions = self.actor(next_states)
            next_q, _ = self.critic_target(next_states, next_actions, return_uncertainty=True)
            q_target = rewards + (1 - dones) * self.gamma * next_q
        
        current_q, _ = self.critic(states, actions, return_uncertainty=True)
        critic_loss = F.mse_loss(current_q, q_target)
        
        self.critic_optimizer.zero_grad()
        critic_loss.backward()
        self.critic_optimizer.step()
        
        # Actor更新(AWAC核心:优势加权回归)
        with torch.no_grad():
            # 计算优势:A(s,a) = Q(s,a) - V(s) ≈ Q(s,a) - Q(s, π(s))
            policy_actions = self.actor(states)
            policy_q, _ = self.critic(states, policy_actions, return_uncertainty=True)
            advantages = (current_q - policy_q).squeeze()
            
            # 指数权重:exp(advantage / β),β是温度参数
            weights = torch.exp(advantages / 1.0).clamp(max=20.0)
        
        # 加权行为克隆
        pred_actions = self.actor(states)
        actor_loss = (weights * (pred_actions - actions).pow(2).mean(dim=1)).mean()
        
        # 可选:添加行为克隆正则(保持接近数据分布)
        if behavior_policy is not None:
            with torch.no_grad():
                behavior_actions = behavior_policy(states)
            bc_loss = F.mse_loss(pred_actions, behavior_actions)
            actor_loss += self.behavior_clone_coef * bc_loss
        
        self.actor_optimizer.zero_grad()
        actor_loss.backward()
        self.actor_optimizer.step()
        
        # 软更新
        for param, target_param in zip(self.critic.parameters(), self.critic_target.parameters()):
            target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data)
        
        return {
            'critic_loss': critic_loss.item(),
            'actor_loss': actor_loss.item(),
            'mean_advantage': advantages.mean().item(),
            'mean_weight': weights.mean().item()
        }
    
    def update_online(self, batch, use_uncertainty=True):
        """在线微调阶段(带不确定性加权)"""
        states, actions, rewards, next_states, dones = batch
        
        # 不确定性估计
        if use_uncertainty:
            current_q, uncertainty = self.critic(states, actions, return_uncertainty=True)
            uncertainty_bonus = uncertainty.squeeze()
            
            # 高不确定性样本:降低学习率或增加探索奖励
            uncertainty_weights = torch.exp(-uncertainty_bonus * 2.0)  # 高不确定->低权重
        else:
            current_q = self.critic(states, actions)
            uncertainty_weights = torch.ones_like(current_q.squeeze())
        
        # Critic更新(加权)
        with torch.no_grad():
            next_actions = self.actor(next_states)
            next_q = self.critic_target(next_states, next_actions)
            q_target = rewards + (1 - dones) * self.gamma * next_q
        
        td_errors = (current_q.squeeze() - q_target.squeeze()).pow(2)
        critic_loss = (uncertainty_weights * td_errors).mean()
        
        self.critic_optimizer.zero_grad()
        critic_loss.backward()
        self.critic_optimizer.step()
        
        # Actor更新(更激进的探索,但仍保持约束)
        pred_actions = self.actor(states)
        advantages = (current_q.detach() - self.critic(states, pred_actions).detach()).squeeze()
        
        # 在线阶段:更小的权重温度,允许更大更新
        weights = torch.exp(advantages / 2.0).clamp(max=50.0)
        actor_loss = (weights * (pred_actions - actions).pow(2).mean(dim=1)).mean()
        
        self.actor_optimizer.zero_grad()
        actor_loss.backward()
        self.actor_optimizer.step()
        
        return {
            'critic_loss': critic_loss.item(),
            'actor_loss': actor_loss.item(),
            'mean_uncertainty': uncertainty.mean().item() if use_uncertainty else 0
        }


class NaiveFinetuneAgent:
    """直接微调基线(无约束,用于对比)"""
    
    def __init__(self, state_dim, action_dim):
        self.actor = nn.Sequential(
            nn.Linear(state_dim, 256),
            nn.ReLU(),
            nn.Linear(256, action_dim),
            nn.Tanh()
        )
        self.critic = nn.Sequential(
            nn.Linear(state_dim + action_dim, 256),
            nn.ReLU(),
            nn.Linear(256, 1)
        )
        self.optimizer = optim.Adam(list(self.actor.parameters()) + list(self.critic.parameters()), lr=3e-4)
    
    def update(self, batch):
        states, actions, rewards, next_states, dones = batch
        
        # 标准TD更新(无约束)
        current_q = self.critic(torch.cat([states, actions], dim=-1))
        next_actions = self.actor(next_states)
        next_q = self.critic(torch.cat([next_states, next_actions], dim=-1)).detach()
        q_target = rewards + 0.99 * (1 - dones) * next_q.squeeze()
        
        loss = F.mse_loss(current_q.squeeze(), q_target)
        
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()
        
        return loss.item()


def generate_synthetic_data(num_samples=1000, state_dim=10):
    """生成合成离线数据"""
    states = torch.randn(num_samples, state_dim)
    # 有偏数据分布(只覆盖部分状态空间)
    states[:, 0] = torch.abs(states[:, 0])  # 只在一侧
    
    actions = torch.randn(num_samples, 2) * 0.5
    actions = torch.clamp(actions, -1, 1)
    rewards = -(states[:, :2] - actions).pow(2).sum(dim=1) + torch.randn(num_samples) * 0.1
    next_states = states + torch.randn_like(states) * 0.1
    dones = torch.zeros(num_samples)
    
    return states, actions, rewards, next_states, dones


def visualize_offline_to_online():
    """可视化Offline-to-Online微调过程"""
    
    # 创建智能体
    state_dim = 10
    action_dim = 2
    
    awac_agent = AWACAgent(state_dim, action_dim)
    naive_agent = NaiveFinetuneAgent(state_dim, action_dim)
    
    # 生成离线数据
    offline_data = generate_synthetic_data(1000, state_dim)
    
    fig, axes = plt.subplots(2, 3, figsize=(16, 10))
    
    # 1. 离线预训练过程
    ax1 = axes[0, 0]
    
    offline_epochs = 50
    awac_losses = []
    
    for epoch in range(offline_epochs):
        # 批量训练
        batch_size = 256
        indices = torch.randperm(1000)[:batch_size]
        batch = tuple(t[indices] for t in offline_data)
        
        info = awac_agent.update_offline(batch)
        awac_losses.append(info['actor_loss'])
    
    ax1.plot(range(offline_epochs), awac_losses, 'o-', color='steelblue', 
            linewidth=2, markersize=4)
    ax1.set_xlabel('Offline Training Epoch', fontsize=11)
    ax1.set_ylabel('Actor Loss', fontsize=11)
    ax1.set_title('Offline Pre-training Phase\n'
                 'AWAC learns from static dataset',
                 fontsize=12, fontweight='bold')
    ax1.grid(True, alpha=0.3)
    
    # 2. 在线微调对比(AWAC vs 直接微调)
    ax2 = axes[0, 1]
    
    online_steps = 100
    awac_performance = []
    naive_performance = []
    
    # 模拟在线性能
    awac_perf = 5.0  # 离线预训练后的性能
    naive_perf = 5.0
    
    for step in range(online_steps):
        # AWAC:稳定增长(约束更新)
        awac_perf += np.random.randn() * 0.2 + 0.1
        awac_perf = np.clip(awac_perf, 0, 10)
        awac_performance.append(awac_perf)
        
        # 直接微调:初期可能崩溃(分布偏移)
        if step < 20:
            naive_perf += np.random.randn() * 0.8 - 0.3  # 负面漂移
        else:
            naive_perf += np.random.randn() * 0.3 + 0.05  # 缓慢恢复
        naive_perf = np.clip(naive_perf, 0, 10)
        naive_performance.append(naive_perf)
    
    ax2.plot(range(online_steps), awac_performance, 'o-', label='AWAC (Constrained)',
            color='forestgreen', linewidth=2, markersize=4)
    ax2.plot(range(online_steps), naive_performance, 's-', label='Naive Finetuning (Unconstrained)',
            color='crimson', linewidth=2, markersize=4)
    
    ax2.axvline(x=0, color='blue', linestyle='--', alpha=0.5)
    ax2.text(5, 9, 'Online Start', fontsize=9, color='blue')
    
    ax2.set_xlabel('Online Training Step', fontsize=11)
    ax2.set_ylabel('Policy Performance', fontsize=11)
    ax2.set_title('Online Fine-tuning Stability\n'
                 'Constraint prevents catastrophic forgetting',
                 fontsize=12, fontweight='bold')
    ax2.legend()
    ax2.grid(True, alpha=0.3)
    
    # 3. 优势加权可视化(AWAC核心)
    ax3 = axes[0, 2]
    
    # 模拟优势分布
    advantages = np.linspace(-3, 3, 100)
    
    # AWAC权重(离线阶段,低温)
    awac_weights_offline = np.exp(advantages / 1.0)
    awac_weights_offline = np.clip(awac_weights_offline, 0, 20)
    
    # AWAC权重(在线阶段,高温,允许更大更新)
    awac_weights_online = np.exp(advantages / 2.0)
    awac_weights_online = np.clip(awac_weights_online, 0, 50)
    
    ax3.plot(advantages, awac_weights_offline, 'b-', linewidth=2, label='Offline (Conservative, β=1.0)')
    ax3.plot(advantages, awac_weights_online, 'r-', linewidth=2, label='Online (Adaptive, β=2.0)')
    ax3.axvline(x=0, color='gray', linestyle='--', alpha=0.5)
    
    ax3.fill_between(advantages, 0, awac_weights_offline, where=(advantages > 0),
                    alpha=0.2, color='blue', label='Weighted region')
    
    ax3.set_xlabel('Advantage A(s,a)', fontsize=11)
    ax3.set_ylabel('Update Weight', fontsize=11)
    ax3.set_title('AWAC Advantage Weighting\n'
                 'Exponential weighting: large updates only when confident',
                 fontsize=12, fontweight='bold')
    ax3.legend()
    ax3.set_yscale('log')
    ax3.grid(True, alpha=0.3)
    
    # 4. 不确定性演化
    ax4 = axes[1, 0]
    
    # 模拟在线学习过程中的不确定性变化
    steps = range(50)
    # 初始:高不确定性(新环境)
    uncertainty_initial = [2.0 * np.exp(-s * 0.05) + 0.3 for s in steps]
    # 学习后:不确定性降低,但保持在安全水平
    uncertainty_awac = [2.0 * np.exp(-s * 0.08) + 0.5 for s in steps]
    # 直接微调:不确定性波动大(探索危险区域)
    uncertainty_naive = [2.0 * np.exp(-s * 0.03) + 0.5 + np.random.randn() * 0.3 for s in steps]
    
    ax4.plot(steps, uncertainty_initial, '--', label='Initial (Pretrain)', color='gray', alpha=0.7)
    ax4.plot(steps, uncertainty_awac, 'o-', label='AWAC (Stable reduction)',
            color='forestgreen', linewidth=2, markersize=4)
    ax4.plot(steps, uncertainty_naive, 's-', label='Naive (Volatile)',
            color='crimson', linewidth=2, markersize=4, alpha=0.7)
    
    ax4.axhline(y=0.5, color='blue', linestyle=':', alpha=0.5, label='Target uncertainty')
    ax4.fill_between(steps, 0, 0.5, alpha=0.1, color='green', label='Safe region')
    
    ax4.set_xlabel('Online Step', fontsize=11)
    ax4.set_ylabel('Prediction Uncertainty', fontsize=11)
    ax4.set_title('Uncertainty Estimation during Fine-tuning\n'
                 'Uncertainty weighting prevents overconfident exploration',
                 fontsize=12, fontweight='bold')
    ax4.legend(loc='upper right', fontsize=8)
    ax4.grid(True, alpha=0.3)
    
    # 5. 状态空间覆盖演化
    ax5 = axes[1, 1]
    
    # 2D状态空间可视化
    theta = np.linspace(0, 2*np.pi, 100)
    
    # 离线数据分布(有偏,只覆盖上半部分)
    offline_center = np.array([0, 2])
    offline_radius = 1.5
    
    # 在线探索后的覆盖
    online_awac_center = np.array([0, 0])
    online_awac_radius = 2.5  # 渐进扩展
    
    online_naive_center = np.array([1, 1])
    online_naive_radius = 3.5  # 过度探索(可能进入危险区域)
    
    # 绘制
    ax5.fill(offline_center[0] + offline_radius * np.cos(theta),
            offline_center[1] + offline_radius * np.sin(theta),
            alpha=0.3, color='blue', label='Offline data coverage')
    
    ax5.plot(online_awac_center[0] + online_awac_radius * np.cos(theta),
            online_awac_center[1] + online_awac_radius * np.sin(theta),
            'g-', linewidth=2, label='AWAC exploration (constrained)')
    
    ax5.plot(online_naive_center[0] + online_naive_radius * np.cos(theta),
            online_naive_center[1] + online_naive_radius * np.sin(theta),
            'r--', linewidth=2, label='Naive exploration (unconstrained)')
    
    # 标记危险区域
    danger_zone = plt.Circle((-2, -2), 1.0, color='red', alpha=0.2, label='Danger zone')
    ax5.add_patch(danger_zone)
    
    ax5.set_xlim(-4, 4)
    ax5.set_ylim(-4, 4)
    ax5.set_xlabel('State Dimension 1', fontsize=11)
    ax5.set_ylabel('State Dimension 2', fontsize=11)
    ax5.set_title('State Space Coverage\n'
                 'AWAC stays close to safe region while expanding',
                 fontsize=12, fontweight='bold')
    ax5.legend(loc='upper right', fontsize=8)
    ax5.grid(True, alpha=0.3)
    ax5.set_aspect('equal')
    
    # 6. Offline-to-Online流程架构
    ax6 = axes[1, 2]
    ax6.set_xlim(0, 10)
    ax6.set_ylim(0, 10)
    ax6.axis('off')
    ax6.set_title('Offline-to-Online Architecture', fontsize=12, fontweight='bold')
    
    # 阶段1:离线预训练
    ax6.add_patch(plt.Rectangle((0.5, 6), 4, 3.5, facecolor='lightblue', 
                               edgecolor='blue', linewidth=2))
    ax6.text(2.5, 9, 'Offline Pre-training', ha='center', fontsize=11, 
            fontweight='bold', color='blue')
    ax6.text(2.5, 8.2, 'Large Static Dataset', ha='center', fontsize=9, style='italic')
    ax6.text(2.5, 7.6, 'AWAC/IQL: Conservative Learning', ha='center', fontsize=9, style='italic')
    ax6.text(2.5, 7.0, 'Goal: Good Initialization', ha='center', fontsize=9, style='italic')
    
    # 箭头
    ax6.annotate('', xy=(5, 7.75), xytext=(4.5, 7.75),
                arrowprops=dict(arrowstyle='->', color='black', lw=3))
    
    # 阶段2:在线微调
    ax6.add_patch(plt.Rectangle((5.5, 6), 4, 3.5, facecolor='lightgreen',
                               edgecolor='green', linewidth=2))
    ax6.text(7.5, 9, 'Online Fine-tuning', ha='center', fontsize=11,
            fontweight='bold', color='green')
    ax6.text(7.5, 8.2, 'Limited Environment Interaction', ha='center', fontsize=9, style='italic')
    ax6.text(7.5, 7.6, 'Constraint: Policy close to pretrain', ha='center', fontsize=9, style='italic')
    ax6.text(7.5, 7.0, 'Goal: Adapt to target task', ha='center', fontsize=9, style='italic')
    
    # 关键技术
    tech_y = 4
    ax6.add_patch(plt.Rectangle((1, tech_y-0.5), 8, 3, facecolor='wheat',
                               edgecolor='black', linewidth=2))
    ax6.text(5, tech_y+2.2, 'Key Techniques', ha='center', fontsize=11, fontweight='bold')
    
    techniques = [
        '1. Advantage Weighted Regression: Only improve when confident',
        '2. Uncertainty Weighting: Downweight high-uncertainty transitions',
        '3. Policy Constraint: Keep π_close to π_pretrained (KL or behavior cloning)',
        '4. Progressive Expansion: Gradually leave data support'
    ]
    
    for i, tech in enumerate(techniques):
        ax6.text(5, tech_y+1.5-i*0.6, tech, ha='center', fontsize=9, style='italic')
    
    # 风险对比
    ax6.text(5, 0.8, 'Risk: Direct finetuning → Catastrophic forgetting & Distribution shift\n'
                     'Benefit: Constrained finetuning → Stable adaptation & Sample efficiency',
            ha='center', fontsize=10, style='italic', color='darkgreen',
            bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
    
    plt.tight_layout()
    plt.savefig('offline_to_online_finetuning.png', dpi=150, bbox_inches='tight')
    print("可视化已保存至 offline_to_online_finetuning.png")
    plt.show()


if __name__ == "__main__":
    visualize_offline_to_online()

以上交付物包含第14章深度强化学习的9个独立脚本,涵盖值函数方法(MDP、DQN、Rainbow)、策略梯度(REINFORCE、PPO、SAC/TD3)以及离线RL与世界模型(CQL/IQL、MuZero/Dreamer、Offline-to-Online)的完整技术栈。每个脚本均包含深度实现与专门可视化,可直接复制使用。技术内容参考了UC Berkeley CS285(Sergey Levine)与CMU 10-715(Zico Kolter)的课程体系,以及Mnih et al. (DQN)、Schulman et al. (PPO, GAE)、Haarnoja et al. (SAC)、Kumar et al. (CQL)、Kostrikov et al. (IQL, AWAC)、Schrittwieser et al. (MuZero)、Hafner et al. (Dreamer)等经典工作。

基于深度强化学习的动态装配算法 机器人控制技术的发展使机器人可以完成多种精密装配任务[在这些装配任务中,作业平台和机器人底座之间的相对位置固定,机器人在柔顺装配方法的基础上,根据接触力的不同取值,做出不同动作。但当作业平台位于柔索之上时,机器人与作业平台之间会形成紧密的动力学刚柔耦合关系。柔索的位置会因受到机器人的挤压而产生复杂的动态变化,位置的动态变化又会反过来造成接触力具有瞬时变化的动态噪声,进而大大影响装配动作的柔顺性。我们将这类装配平台不固定,且位置受机器人运动影响的装配任务定义为动态装配任务,柔索装配环境是其中的典型应用场景。 阅读详情

相关推荐

14、使用ML - Agents在坦克战斗中构建机器学习机器人

本博客探讨了使用Unity的ML-Agents工具包结合深度强化学习(DRL)训练坦克战斗中的智能机器人。研究设计了一个包含目标瞄准、避障和资源收集的环境,采用PPO算法进行训练,并通过课程学习逐步提升复杂度以优化学习效果。实验结果显示代理能有效学习基本战斗行为,但仍存在挑战。未来工作将引入多代理合作、视觉观察及跨领域应用,以拓展强化学习在游戏与现实场景中的潜力。

lemon的博客 71

多Agent 深度强化学习综述

多Agent 深度强化学习综述 人工智能技术与咨询 来源:《自动化学报》,作者梁星星等 摘 要近年来,深度强化学习(Deep reinforcement learning,DRL) 在诸多复杂序贯决策问题中取得巨大突破.由于融合了深度学习强大的表征能力和强化学习有效的策略搜索能力,深度强化学习已经成为实现人工智能颇有前景的学习范式.然而,深度强化学习在多Agent 系统的研究与应用中,仍存在诸多困难和挑战,以StarCraft II 为代表的部分观测环境下的多Agent学习仍然很难达到理想效果..

m0_62870606的博客 5996

14、量子控制中的机器学习应用

本文探讨了机器学习在量子控制领域的应用,重点介绍了强化学习和深度强化学习在优化量子计算速度与保真度、寻找量子门最优控制、量子状态控制以及快速精确量子控制中的具体实践。同时,对梯度下降、遗传算法、进化策略等传统机器学习算法进行了总结和比较。通过实验结果分析,深度强化学习在控制精度和效率方面表现突出。文章最后展望了未来量子控制中机器学习的发展方向,包括算法可扩展性、不确定性处理和跨领域融合等关键研究方向。

brown的博客 82

第2章 机器学习概述

本文对机器学习进行了了一个简要介绍。

qq_40940944的博客 1199

2、机器学习与TensorFlow:从基础到实践

本文详细介绍了机器学习的基础概念和TensorFlow框架的应用,内容涵盖机器学习的工作原理、TensorFlow的基本操作,以及核心学习算法和神经网络范式的实践案例。通过书籍的学习结构,读者可以从基础装备开始,逐步学习到复杂的模型构建和实际问题的解决方法。此外,还提供了代码资源、学习建议和未来发展方向,帮助读者系统掌握机器学习技能。

c6d7e8f9g的博客 55

深度强化学习模型训练效率优化:架构师必须掌握的7个技巧

深度强化学习(Deep Reinforcement Learning, DRL)已在多个领域展现出革命性潜力——从AlphaGo击败世界围棋冠军,到OpenAI Five在Dota 2中战胜职业选手,再到自动驾驶系统在复杂路况中的决策能力。。一个典型的DRL模型往往需要数百万甚至数十亿的环境交互样本,在数十台GPU上训练数周才能达到人类水平性能。例如,OpenAI的Dota 2 AI需要训练约10,000年的游戏时间(通过加速模拟);

AI Python 编程之道的博客 1113

【赠书】掌握人工智能重要主题,深度强化学习实践书籍推荐

‍‍今天要给大家介绍的书是深度强化学习实践的第二版,本书的主题是强化学习(Reinforcement Learning,RL),它是机器学习(Machine Learning,ML)的一个...

hacker_long的专栏 661

基于深度强化学习的卫星互联网路由优化研究

以深度强化学习为代表的机器学习算法在处理大规模复杂问题时具有良好的性能,能满足低轨卫星网络的高时变性和地面网络的接入随机性等特点,适合用于解决卫星互联网中的路由问题。由于基于深度强化学习的路由优化算法性能的优劣与卫星星座的规模相关性较低,当卫星互联网规模增大时,会占用大量的网络资源和深度强化学习算法的训练时间。利用机器学习进行网络的路由优化,能够适应大规模网络的动态性,实时捕获网络状态信息(时延、丢包率等),并预测实时路由对网络带来的影响。随着太空探测任务的逐步推进,深空网络也将在未来网络的发展规划中。

罗伯特技术屋 562

机器学习周刊第二期:300个机器学习应用案例集

大家好继续分享我最近看过并觉得非常硬核的资源,包括Python、机器学习、深度学习、大模型等等。

1477

机器学习算法PPT:核心算法的综合教育资源

机器学习是人工智能的一个分支,它通过让计算机系统利用数据来自我改进,而不是通过明确的程序指令来实现。从20世纪50年代艾伦·图灵提出“学习机器”的构想到如今AI技术的广泛应用,机器学习经历了从理论到实践的蓬勃发展。半监督学习是一种机器学习方法,它结合了监督学习和无监督学习的特性。在这种学习过程中,算法主要利用一小部分标记数据和大量未标记数据来学习模型。标记数据提供了关于数据分布的直接信息,而未标记数据则通过数据本身的结构和分布来增强学习过程,从而提升模型的泛化能力。

weixin_28931507的博客 1132

MATLAB实现基于深度强化学习(DRL)进行无人机三维路径规划的详细项目实例

本项目基于深度强化学习(DRL)实现无人机三维路径规划,通过MATLAB平台构建了完整的解决方案。项目采用DDPG算法结合3D卷积网络,在复杂三维环境中实现自主避障和路径优化。主要创新点包括:多维度环境建模、连续动作空间设计、多目标奖励机制和GPU加速训练。系统包含环境交互、状态表示、策略网络、价值网络和训练优化五大模块,支持军事侦察、物流配送等应用场景。项目解决了高维状态空间、训练稳定性等关键技术挑战,并通过GUI界面实现了数据导入、模型训练、结果可视化的全流程管理。实验表明,该方法在路径安全性和效率上显

优质创作者: 人工智能技术领域 对于有一定基础的 MATLAB和Python带具体需求都可以详谈 MATLAB和Python可以提供代码调试服务加油 谢谢 你的鼓励是我前行的动力 谢谢支持 加油 谢谢 1995

机器学习和深度学习相关书籍

深度学习及TensorFlow框架的相关背景;机器学习是人工智能研究领域中一个极其重要的研究方向,在现今的大数据时代背景下,捕获数据并从中萃取有价值的信息或模式,成为各行业求生存、谋发展的决定性手段,这使得这一过去为分析师和数学家所专属的研究领域越来越为人们所瞩目。《机器学习实战》分主要介绍机器学习基础,以及如何利用算法进行分类,并逐步介绍了多种经典的监督学习算法,如k近邻算法、朴素贝叶斯算法、Logistic回归算法、支持向量机、AdaBoost集成方法、基于树的回归算法和分类回归树(CART)算法等。

weixin_42098295的博客 2013

Dual-stream multiple instance learning networks for tumor detection in Whole Slide Image——论文笔记

关于多示例学习的资料网上比较少,但是在医学图像方面还是有人在应用的。主要是因为一般比较大,几个G都是十分常见的,这么大图片是无法直接训练,一般都会进行切图,所以天然契合多示例学习bag和instance的概念。最近看了一篇论文刚好关于这方面的,所以做下笔记。 github地址:https://github.com/binli123/dsmil-wsi paper:https://arxiv.org/abs/2011.08939 背景知识 在多示例学习(MIL)中,一组训练样本被视为包含多个instance的

weixin_41693877的博客 4170

分享!数百个金融类机器学习仓库:financial-ML

GitHub金融机器学习仓库概览 本文介绍了GitHub上获赞8000+的financial-machine-learning仓库,该仓库收集了近千个金融机器学习资源,涵盖算法交易、风险管理等领域。文章重点推荐了三个代表性项目:PyPortfolioOpt投资组合优化工具(支持多种风险模型)、FinRL强化学习交易框架(提供端到端深度强化学习方案)以及FinBERT文本分析模型(专为金融领域优化的BERT)。这些开源项目为研究者提供了可复现的工具链和文献参考,展示了机器学习在金融领域的创新应用,如PyPor

Stata连享会 464

Gamma变换算法之Matlab&FPGA实现

Gamma映射原理 什么是Gamma曲线 人眼的感光与光强并不是呈线性关系的,而是呈非线性关系(指数型关系,如下图左-曲线。在低照度下,人眼更容易分辨出亮度的变化,随着照度的增加,人眼不易分辨出亮度的变化。而图像传感器与输入光强呈线性关系(如下图左-直线)。从图中可见,当传感器感光在20%左右时,人眼感光响应达到了50%,人眼对低照度的变化更为敏感。从下图右侧可见,Reference Tone为人眼感光50%的亮度,而传感器在感光50%时则相对暗的多(见Select,近似中性灰)。这种将原始图像通过映射操

CrazyBingo的博客 7407

对比学习损失函数避坑指南:为什么你的自监督模型不收敛?

本文深入剖析对比学习损失函数中的关键陷阱,揭示自监督模型不收敛的根本原因。通过分析温度系数选择、负样本采样策略和特征归一化等核心因素,提供实用解决方案。实验数据表明,正确设置温度系数τ(建议0.07起)可提升30%性能,而MoCo风格的负样本队列比SimCLR方案在小批量时准确率高6-8%。

nokia的博客 923

【机器人】基于matlab深度强化学习的两足机器人行走【含Matlab源码 10030期】

深度强化学习的两足机器人行走 完整代码,包运行;可提供运行操作视频!适合小白!

订阅付费专栏Matlab(奶茶价版),可赠送奶茶价版付费专栏指定代码1份; 970

【毕业设计】_基于STM32单片机的按摩椅( 原理图+源码)

按摩器系统基于STM32单片机设计,作为核心控制单元。系统配备了OLED显示屏、蓝牙模块、喇叭模块、播报模块和光电感应模块。光电感应器用于检测是否有人接近,同时,系统可以通过蓝牙接收控制指令并执行相应的操作。该系统支持40℃的热敷功能以及气囊按摩功能,具备温度显示功能,并且可以通过按键切换不同的按摩模式。当光电感应器检测到有人时,按摩椅会自动启动。使用DHT11传感器的红色按钮可以调整温度。

2401_85381100的博客 724

【控制】基于深度强化学习网络DDPG实现SMC抖振抑制附Matlab代码

DDPG是一种无模型的、基于策略的深度强化学习算法,适用于连续动作空间。它结合了深度神经网络来表示策略和值函数,并使用Actor-Critic架构进行训练。基于DDPG的SMC抖振抑制是一种有效的方法,它可以通过学习来减少抖振,提高系统的性能。这种方法需要根据具体的应用场景和系统特性来设计状态空间、动作空间和网络结构。在实际应用中,可能需要多次实验和调整来达到最佳的性能。⛳️ 运行结果。

m0_57702748的博客 1023

把 ABAP ALV 做成可点、可改、可保存:Interactive Editable ALV Grid 的一套落地打法

文章摘要(148字): SAP ALV报表开发中,交互式可编辑网格的实用方案需解决三个核心问题:数据展示、交互下钻与编辑保存。本文以SCARR航空公司和SFLIGHT航班数据为例,演示如何实现双击主列表下钻明细、动态切换编辑模式、精确保存修改的技术路线。关键点包括:通过PF-STATUS添加业务按钮、使用CHECK_CHANGED_DATA同步前端修改、保护关键字段避免误操作,以及变更数据的审计跟踪方法。该方案适用于SAP GUI环境下的快速业务调整需求,相比Fiori开发具有更高的实施效率。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 232

RTCA DO-160G标准详解:航空电子设备的全面环境适应性测试指南

第二章的主要目的是提供标准中使用的术语和定义。了解这些术语对于准确理解标准的要求至关重要,特别是在技术实施过程中。RTCA DO-160G 涉及多个学科的知识,包括航空、电子、电磁学和机械工程,因此需要确保使用的术语在所有学科中具有一致的定义。为了确保测试人员和工程师在执行测试时对所有条件和要求的理解一致,RTCA DO-160G 标准中定义了大量术语。环境应力:指的是设备在操作过程中可能遇到的外部物理或化学条件,包括温度、湿度、振动等。电磁干扰(EMI)

qq_41854911的博客 5280

机器学习十大算法+各大深度学习名词(一段话讲清楚)

误差值为负就提升权重,为正就降低权重受比率即学习率的制约,用来调整学习率调整参数的高低由于强大的调整能力,BP控制下的神经网络容易过拟合,也就是在训练数据上表现得很好,在测试数据上不行,可以采用提前停止的策略,分一个验证集,来估算误差,如果训练集的误差减少但是验证集的误差增高,就证明网络过于适用于训练集,这时就可以结束训练。

qq_44717133的博客 2526

Rerender A Video 技术浅析(四):跨帧约束

时间一致性是指在视频处理过程中,保持相邻帧之间内容、风格和运动的平滑过渡,避免出现视觉上的不连续性或伪影。时间一致性跨帧约束模块的主要目标是在视频增强、风格迁移等任务中,确保处理后的视频在时间维度上保持一致性和稳定性。该模块主要通过以下几种方式实现时间一致性:1.时序损失(Temporal Loss):最小化相邻帧之间的差异,确保帧间内容的平滑过渡。2.循环一致性损失(Cycle Consistency Loss):确保帧间转换的一致性,避免出现风格漂移。3.

m0_75253143的博客 1481

价值连城 知名深度强化学习Pieter Abbeel的采访 给机器学习 深度学习 和机器人学研究者从业者的建议

非常感谢你 Pieter 今天来参加访谈 我想很多人觉得你是一个 知名的机器学习 深度学习 和机器人学研究者 我希望能让观众们听一听你的故事 你是如何最终走上了现在的道路的呢? 这是个好问题 其实如果你问14岁的我这个问题 我要立志做什么 问题的答案大概不会是这些 实际上 在当时 我认为成为一名专业篮球运动员应该不错 但我觉得没有能力实现那个目标 我感觉 对机器学习来说这是一件幸事 因为篮球的打算没能实现 对 篮球之梦没有实现 虽然打篮球本身非常有乐趣 但是 把它当成事业却行不通 我在学校时真正喜欢...

AI架构师易筋 598

水很深的深度学习(四): 卷积神经网络CNN

深度学习-CNN,卷积神经网络的概念 上世纪60年代,Hubel等人通过对猫视觉皮层细胞的研究,提出了感受野这个概念,到80年代,Fukushima在感受野概念的基础之上提出了神经认知机的概念,可以看作是卷积神经网络的第一个实现网络,神经认知机将一个视觉模式分解成许多子模式(特征),然后进入分层递阶式相连的特征平面进行处理

专注大数据与人工智能技术分享,欢迎私信加群互相学习! 3624

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

上一篇: 【机器学习基础算法:从零实现到精通】第15章 因果推断与鲁棒机器学习
下一篇: 【图神经网络:原理与实战】各章代码实现
VectorShift
博客等级 码龄1年 1839粉丝 1109原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VectorShift

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值