1、DQN概述
DQN(Deep Q-Network)是一种结合深度学习和强化学习算法的算法,它使用神经网络来近似Q值函数(即动作-价值函数),并通过Q-Learning)更新策略来训练智能体。
DQN算法在标准Q-Learning上进行了改进,主要包括以下2个关键技巧:
1.经验回放(Experience Replay):为了打破数据的相关性,DQN使用一个经验回放池(Replay Buffer)来存储智能体的历史经验。每次从回放池中随机采样一个批次(batch)来进行训练,而不是每次都用最新的经验更新网络。
2.目标网络(Target Network):为了避免Q网络在训练过程中剧烈波动,DQN引入了一个目标网络Q_target,这个目标网络的结构和Q网络相同,但其参数θ_target是Q网络参数θ的延迟副本。目标网络用于计算目标Q值,定期从Q网络复制参数。
2、DQN核心原理
(1)经验元组格式
分别表示:当前状态,当前动作,即时奖励,下一状态,回合结束标记(True=游戏终止,无后续状态)
(2)整体流程概览
1.智能体与环境交互,每走一步生成一条经验存入回放池
2.回放池存满最小样本量后,才开始训练
3.每次训练:随机抽样一批经验,批量计算损失更新Q网络
4.循环交互存经验+抽样训练,直到回合结束
(3)训练流程
1.拿采样样本里的进主网络Q_current,得到当前状态所有动作Q值,取出真实执行的动作
对应的Q值,记为
2.拿样本里的进目标网络Q_target,算出下一状态所有动作Q,取最大值
3.用奖励+折扣 * 下一状态最大Q值算出目标值,
表示回合结束,后面项直接清零,只有单步奖励。
4.用和当前Q做均方误差损失,反向传播只更新主网络
MSE损失
(4)经验回放的作用
1.打破时序相关性:连续状态高度相似,直接串行训练会让梯度高度相关,收敛困难
2.数据复用:一条经验可以被多次采样训练,充分利用少量交互数据,提升样本效率
3.平滑数据分布:避免近期少数几步奖励波动导致训练不稳定
(5)两个网络的作用
Q_current(主网络):负责选动作、输出预测Q,每次训练都更新权重
Q_target(目标网络):只用来算下一状态的目标Q,一段时间才复制一次主网络权重,平时完全冻结。
(6)损失函数的本质
损失函数核心就是TD误差(TD error)。
DQN的损失本质就是让TD误差尽可能小。
(7)延迟副本设计
延迟副本本质是切断预测值与目标标签的耦合关系,小幅牺牲瞬时精度,换取训练全程稳定收敛,是DQN两大核心改进之一。
1)无目标网络的致命缺陷
仅单Q网络时,TD目标公式:
更新权重会同时改变预测值与目标标签
,出现循环依赖:每轮训练标签持续漂移,梯度剧烈震荡,模型难以收敛。
2)目标网络核心方案
1.搭建结构完全相同的双网络:主网络Q_current、目标网络Q_target
2.主网络:实时梯度更新,输出当前状态Q预测值
3.目标网络:参数延迟复制,间隔N步同步一次,间隔内参数冻结,专门计算固定TD目标
3)拟合旧估值是否存在问题
1.稳定优先:固定标签消除目标偏移,避免训练发散,稳定性比瞬时精准更重要
2.之后是暂时的:定期同步主网络最新参数,刷新目标估值,不会永久使用旧参数
4)之后偏差的抵消手段
1.折扣因子<1,削弱未来旧值带来的误差
2.经验回放批量采样,平均平滑单样本偏差
3.同步间隔步长较短,误差不会持续累积
(8)DQN解决了传统Q-Learning的什么痛点
1.普通Q-Learning是表格形式,状态空间、动作空间一大就无法存储;DQN用神经网络拟合Q值,适配高维连续状态
2.普通Q-Learning单步在线更新,样本时序强相关,不能直接用SGD训练神经网络(SGD要求样本独立同分布,否则训练会梯度震荡、不收敛);DQN用经验回放解决
3.单网络训练目标持续漂移,梯度震荡不收敛;DQN用目标网络稳定TD目标
(6)DQN输入预处理(Atari原版细节)
输入是连续4帧堆叠,把时序信息融入状态。
作用:单帧画面缺少运动信息,堆叠多帧让网络看懂物体移动。
(7)动作选择策略
-贪心平衡探索&利用。
DQN 和 Q-learning 共用 ε- 贪心平衡探索 & 利用:
探索(随机选动作):概率 ε,尝试未知动作,避免局部最优;
利用(选最大 Q 动作):概率 1-ε,选当前最优动作获取高奖励;
训练常用衰减 ε:初始 ε=1(全随机探索),逐步降到 0.01 左右,后期基本只利用。
不做探索的后果:智能体固化一套动作,永远发现不了更高回报策略。
(8)DQN的缺陷
1.过估计偏差
TD目标取,最大值天然高估真是好动作价值,长期测录额偏向乐观高估,影响最终性能;后续Double DQN专门解决。
2.经验回放均匀采样,无优先级
所有样本同等概率抽取,高TD误差、高学习价值的稀有样本反复训练概率低;改进:Prioritized DQN(优先经验回放)。
3.只输出状态所有动作Q,无法区分状态价值、优势值
无法评估动作相对好坏;改进 Dueling DQN,拆分 V (s)+A (s,a)。
4.离散动作专用,不能直接处理连续动作
DQN仅适用于有限离散动作空间,连续动作要用DDPG、PPO等。
3、DQN伪代码(*)
python
# 初始化
Q_network = initialize_network() # 初始化 Q 网络
target_network = copy.deepcopy(Q_network) # 初始化目标网络
replay_buffer = deque(maxlen=10000) # 经验回放池
epsilon = 1.0 # 初始探索率
epsilon_min = 0.01 # 最小探索率
epsilon_decay = 0.995 # 探索率衰减因子
learning_rate = 0.0001 # 学习率
gamma = 0.99 # 折扣因子
batch_size = 64 # 批次大小
# 训练过程
for episode in range(total_episodes):
state = environment.reset() # 重置环境,获取初始状态
done = False
while not done:
if random.random() < epsilon: # 以 epsilon 的概率选择随机动作
action = random.choice(actions)
else: # 否则选择当前状态下 Q 值最大的动作
action = argmax(Q_network(state))
# 执行动作,获得奖励和下一个状态
next_state, reward, done, _ = environment.step(action)
# 存储经验到回放池
replay_buffer.append((state, action, reward, next_state, done))
# 从回放池中随机采样一个小批次
if len(replay_buffer) >= batch_size:
batch = random.sample(replay_buffer, batch_size)
for s, a, r, s_next, done in batch:
# 计算目标 Q 值
if done:
target = r
else:
target = r + gamma * np.max(target_network(s_next))
# 更新 Q 网络
loss = (Q_network(s, a) - target) ** 2
Q_network.update(loss)
state = next_state # 更新当前状态
# 更新 epsilon
if epsilon > epsilon_min:
epsilon *= epsilon_decay
# 每隔一定步骤更新目标网络
if episode % target_update_frequency == 0:
target_network = copy.deepcopy(Q_network)
4、DQN训练冰湖环境代码
python
import gymnasium as gym
import numpy as np
import matplotlib.pyplot as plt
from collections import deque
import random
import torch
import torch.nn as nn
import torch.nn.functional as F
# Define model
class DQN(nn.Module):
def __init__(self, in_states, h1_nodes, out_actions):
super().__init__()
self.fc1 = nn.Linear(in_states, h1_nodes)
self.out = nn.Linear(h1_nodes, out_actions)
def forward(self, x):
x = F.relu(self.fc1((x)))
x = self.out(x)
return x
# Define memory for Experience Replay
class ReplayMemory():
def __init__(self, maxlen):
self.memory = deque([], maxlen=maxlen)
def append(self, transition):
self.memory.append(transition)
def sample(self, sample_size):
return random.sample(self.memory, sample_size)
def __len__(self):
return len(self.memory)
# FrozeLake Deep Q-Learning
class FrozenLakeDQN():
learning_rate_a = 0.001
discount_factor_g = 0.9
network_sync_rate = 10
replay_memory_size = 1000
mini_batch_size = 32
loss_fn = nn.MSELoss()
optimizer = None
ACTIONS = ['L', 'D', 'R', 'U']
def state_to_dqn_input(self, state, num_states):
input_tensor = torch.zeros(num_states)
input_tensor[state] = 1
return input_tensor
def print_dqn(self, dqn):
# Get number of input nodes
num_states = dqn.fc1.in_features
# Loop each state and print policy to console
for s in range(num_states):
# Format q values for printing
q_values = ''
for q in dqn(self.state_to_dqn_input(s, num_states)).tolist():
q_values += "{:+.2f}".format(q) + ' ' # Concatenate q values, format to 2 decimals
q_values = q_values.rstrip() # Remove space at the end
# Map the best action to L D R U
best_action = self.ACTIONS[dqn(self.state_to_dqn_input(s, num_states)).argmax()]
# Print policy in the format of: state, action, q values
# The printed layout matches the FrozenLake map.
print(f'{s:02},{best_action},[{q_values}]', end=' ')
if (s + 1) % 4 == 0:
print() # Print a newline every 4 states
def train(self, episodes, render=False, is_slippery=False):
env = gym.make('FrozenLake-v1', map_name="8x8", is_slippery=is_slippery,
render_mode='human' if render else None)
num_states = env.observation_space.n
num_actions = env.action_space.n
epsilon = 1
memory = ReplayMemory(self.replay_memory_size)
policy_dqn = DQN(in_states=num_states, h1_nodes=num_states, out_actions=num_actions)
target_dqn = DQN(in_states=num_states, h1_nodes=num_states, out_actions=num_actions)
target_dqn.load_state_dict(policy_dqn.state_dict())
print('Policy (random, before training):')
self.print_dqn(policy_dqn)
self.optimizer = torch.optim.Adam(policy_dqn.parameters(), lr=self.learning_rate_a)
rewards_per_episode = np.zeros(episodes)
epsilon_history = []
step_count = 0
for i in range(episodes):
state = env.reset()[0]
terminated = False
truncated = False
while(not terminated and not truncated):
if random.random() < epsilon:
action = env.action_space.sample()
else:
with torch.no_grad():
action = policy_dqn(self.state_to_dqn_input(state, num_states)).argmax().item()
new_state, reward, terminated, truncated, _ = env.step(action)
memory.append((state, action, new_state, reward, terminated))
state = new_state
step_count += 1
if reward == 1:
rewards_per_episode[i] = 1
if len(memory) > self.mini_batch_size and np.sum(rewards_per_episode) > 0:
mini_batch = memory.sample(self.mini_batch_size)
self.optimize(mini_batch, policy_dqn, target_dqn)
epsilon = max(epsilon - 1 / episodes, 0)
epsilon_history.append(epsilon)
if step_count > self.network_sync_rate:
target_dqn.load_state_dict(policy_dqn.state_dict())
step_count = 0
env.close()
torch.save(policy_dqn.state_dict(), '1_frozen_lake_dqn.pt')
plt.figure(1)
sum_rewards = np.zeros(episodes)
for x in range(episodes):
sum_rewards[x] = np.sum(rewards_per_episode[max(0, x - 100):(x + 1)]) # 计算过去 100 回合的平均奖励
plt.subplot(121) # 在一个 1 行 2 列的子图中,选择第一个位置绘制图像
plt.plot(sum_rewards)
# 绘制 epsilon 衰减(Y 轴)与回合数(X 轴)的关系
plt.subplot(122) # 在一个 1 行 2 列的子图中,选择第二个位置绘制图像
plt.plot(epsilon_history)
# 保存图表
plt.savefig('1_frozen_lake_dql.png')
def optimize(self, mini_batch, policy_dqn, target_dqn):
num_states = policy_dqn.fc1.in_features
current_q_list = []
target_q_list = []
for state, action, new_state, reward, terminated in mini_batch:
if terminated:
target = torch.FloatTensor([reward])
else:
with torch.no_grad():
target = torch.FloatTensor(
reward + self.discount_factor_g * target_dqn(self.state_to_dqn_input(new_state, num_states)).max()
)
current_q = policy_dqn(self.state_to_dqn_input(state, num_states))
current_q_list.append(current_q)
target_q = target_dqn(self.state_to_dqn_input(state, num_states))
target_q[action] = target
target_q_list.append(target_q)
loss = self.loss_fn(torch.stack(current_q_list), torch.stack(target_q_list))
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
def test(self, episodes, is_slippery=False):
# 创建 FrozenLake 环境实例
# 'FrozenLake-v1' 是环境名称,'map_name="8x8"' 表示使用 8x8 的地图
# 'is_slippery' 控制是否让地面滑,设置为 False 时,地面不滑
# 'render_mode' 设置为 'human' 表示渲染并显示可视化界面
env = gym.make('FrozenLake-v1', map_name="8x8", is_slippery=is_slippery, render_mode='human')
num_states = env.observation_space.n # 获取状态空间的数量(即状态的总数,FrozenLake 环境中为 64)
num_actions = env.action_space.n # 获取动作空间的数量(即动作的总数,FrozenLake 环境中为 4)
# 加载已训练的策略
policy_dqn = DQN(in_states=num_states, h1_nodes=num_states, out_actions=num_actions) # 创建策略网络实例
policy_dqn.load_state_dict(torch.load("1_frozen_lake_dqn.pt")) # 加载保存的训练好的策略网络权重
policy_dqn.eval() # 将模型切换到评估模式(不需要计算梯度,节省内存)
print('Policy (trained):') # 打印策略网络的信息
self.print_dqn(policy_dqn) # 打印训练后的策略网络结构
# 在环境中进行多个回合的测试
for i in range(episodes):
state = env.reset()[0] # 重置环境,返回初始状态,通常是状态 0
terminated = False # 初始化终止状态(当智能体掉进坑里或到达目标时为 True)
truncated = False # 初始化截断状态(当智能体执行超过 200 步时回合被截断,设置为 True)
# 智能体在环境中进行导航,直到它掉进坑里或到达目标,或者执行超过 200 步(回合被截断)
while (not terminated and not truncated):
# 选择最佳动作
with torch.no_grad(): # 在测试时,不需要计算梯度
# 使用已训练的策略网络预测当前状态下每个动作的 Q 值,并选择 Q 值最大的动作
action = policy_dqn(self.state_to_dqn_input(state, num_states)).argmax().item()
# 执行动作并获取反馈
state, reward, terminated, truncated, _ = env.step(action)
# 关闭环境
env.close()
if __name__ == '__main__':
frozen_lake = FrozenLakeDQN()
is_slippery = False
# frozen_lake.train(2000, is_slippery=is_slippery)
frozen_lake.test(10, is_slippery=is_slippery)
5、Double DQN算法原理
(1)解决核心问题:DQN过估计偏差
Double DQN是为了解决过估计问题而设计的。
原始DQN TD目标:
对下一状态直接取最大值有问题,神经网络Q_target值本身存在预测噪声,最大值会天然放大正向误差,高估真实动作回报,导致过估计偏差。长期高估会让策略变得过度乐观,最终收敛到次优策略。
(2)核心改进:选动作和评估动作拆分成两个网络
原始DQN:同一套target网络的max操作,既选出了最优动作,又得到了该动作的评估价值,两个操作导致噪声叠加放大高估。
Double DQN将max操作拆分成两步:
1.用Q_current网络选出最优动作
2.用Q_target网络评估该动作的真实Q值
把上面选出的动作送入冻结的目标网络,只取这个特定动作的Q值,不再全局取max:
Double DQN 完整 TD 目标公式:
(3)为什么这样能消除过估计
1.选动作,估值分开在两套参数不同的网络,两者的预测噪声互不相关
2.不会重复放大同一网络的正向预测误差,抑制最大值带来的乐观高估
(4)代码
仅修改optimze函数的的计算公式即可。
python
def optimize_ddqn(self, mini_batch, policy_dqn, target_dqn):
num_states = policy_dqn.fc1.in_features
current_q_list = []
target_q_list = []
for state, action, new_state, reward, terminated in mini_batch:
if terminated:
target = torch.FloatTensor([reward])
else:
with torch.no_grad():
next_state_tensor = self.state_to_dqn_input(new_state, num_states)
# 步骤1:主网络选下一状态最优动作 a*
best_next_action = policy_dqn(next_state_tensor).argmax()
# 步骤2:目标网络取该动作的Q值,而非全局max
target_next_q = target_dqn(next_state_tensor)[best_next_action]
target = torch.FloatTensor(reward + self.discount_factor_g * target_next_q)
current_q = policy_dqn(self.state_to_dqn_input(state, num_states))
current_q_list.append(current_q)
target_q = target_dqn(self.state_to_dqn_input(state, num_states))
target_q[action] = target
target_q_list.append(target_q)
loss = self.loss_fn(torch.stack(current_q_list), torch.stack(target_q_list))
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
(5)总结Double DQN解决什么问题?原理是什么?
解决 DQN 取 max Q 带来的过估计偏差。将下一状态最优动作选择与价值评估解耦:用实时主网络选出最优动作,再用冻结的目标网络计算该动作的 Q 值,避免同一网络噪声叠加放大高估,缓解策略过度乐观问题,其余训练逻辑和 DQN 完全一致。
6、Prioritized DQN原理
(1)解决原始DQN回放池的缺陷
原始经验回放均匀随机采样,所有样本被抽到概率完全相同。但不同样本学习价值天差地别:
-TD误差大的样本:网络预测和真实目标差距大,蕴含大量学习信息,急需多训练
-TD误差接近0的样本:网络已经学懂,反复训练收益极低。
均匀采样浪费算力、收敛速度慢,因此剔除优先经验回放DQN。
(2)核心设计
Prioritized DQN涉及采样调整,而此采样改变了数据分布,需要修改Loss修正分布偏移。
1)优先级定义
用TD误差衡量样本重要性:样本优先级p和绝对TD误差正相关。
,
是为了防止TD误差为0时样本优先级变成0,永远采样不到。
为TD误差
2)比例优先采样
,
属于0,1
=0时,退化成原始均匀DQN;
越大,越偏向高TD误差样本。
3)重要权重
优先采样会改变原始数据分布,引入分布偏移,梯度估计有偏,需要IS(Importance Sampling Weight)权重修正损失。

对于基础项:,
若均匀采样:,和原始DQN一致
若样本i优先级高,优先级远大于
,分母变大,整体小于1,权重变小
若样本i优先级低,则权重变大。
(3)Prioritized DQN改进点和核心原理是什么?
针对原生DQN经验回放均匀随机采样 、样本利用率低的问题做优化。它根据样本TD误差大小 分配优先级,TD误差越大、学习价值越高,被采样概率就越高,重点训练难学样本,加速收敛。但优先采样会破坏原始数据分布、导致梯度有偏,因此引入重要性采样IS权重做修正:高频抽到的高优先级样本压低权重,低频样本抬高权重,同时β从0逐步升到1,渐进消除分布偏移,保证梯度更新无偏,训练后还会实时更新样本优先级,持续优化采样效率。