强化学习之DQN算法族(基于gymnasium开发)

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,渐进消除分布偏移,保证梯度更新无偏,训练后还会实时更新样本优先级,持续优化采样效率。

相关推荐
何以解忧,唯有..2 小时前
Go语言循环语句详解:for、range与循环控制
开发语言·算法·golang
想吃火锅10053 小时前
【leetcode】88.合并两个有序数组js
算法
生成论实验室4 小时前
机器人:一个自主运动的系统
人工智能·算法·语言模型·机器人·自动驾驶·agi·安全架构
Qres8214 小时前
算法复键——树状数组
数据结构·算法
H178535090964 小时前
SolidWorks第四部分_直接实体建模特征9_替换面原理
线性代数·算法·机器学习·3d建模·solidworks
不会就选b4 小时前
算法日常・每日刷题--<二分查找>3
算法
绿算技术5 小时前
Mooncake 与绿算ForinnBase GroundPool如何联手打破推理僵局?
科技·算法·架构
-森屿安年-5 小时前
63. 不同路径 II
c++·算法·动态规划
老余捞鱼5 小时前
线性回归实战:5步验证你的量化因子是否真有效
算法·金融·回归·线性回归·ai量化