目录
14.1.1 MDP的贝尔曼方程与最优性原理,值迭代与策略迭代的收敛性分析
14.1.2 深度Q网络(DQN):经验回放的优先级采样与目标网络的软更新
14.1.3 DQN改进:Double DQN的解耦选择与评估、Dueling DQN的优势流与值流分离,Rainbow的六重整合
14.2.1 REINFORCE算法的蒙特卡洛回报估计与基线减方差,A2C/A3C的同步与异步训练、熵正则化
14.2.2 PPO(近端策略优化)的裁剪替代目标与自适应KL惩罚,GAE(广义优势估计)的λ-回报权衡
14.2.3 Soft Actor-Critic(SAC)的最大熵框架与温度参数自动调整,确定性策略梯度(DDPG/TD3)的目标策略平滑与延迟更新
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)等经典工作。
71



被折叠的 条评论
为什么被折叠?



