一、价值表达:从表格到函数近似

1、为什么需要使用函数?

  • 状态空间连续:使用表格 Q(s,a) 只能存离散状态,而很多环境状态是连续的,无法列出所有状态。
  • 高维状态表示:在存储高维状态数据时,表格存储 Q值会爆炸式增长。而函数可以使用一个低维向量(函数参数向量)来描述一个高维向量(状态值),极大提高存储效率。
  • 泛化能力:当使用表格时,只能针对已知状态进行对应修改。而函数是通过修改参数,从而以学习状态之间的规律,把经验推广到未见过的状态。

2、函数近似的方法

  • 线性函数近似(Linear Function Approximation)
  • 神经网络(Deep Neural Networks)

二、DQN(Deep Q-Network)

1、DQN算法的核心思想

  • 用神经网络近似 Q 函数 Q(s,a;\theta)

  • 利用 TD 目标 更新 Q 主网络(Policy Network)

  • 通过 经验回放(Experience Replay) 目标网络(Target Network) 提高训练稳定性

2、核心更新公式

在数学公式上,DQN旨在最小化如下的损失函数:

\mathrm{loss}=\left(Q_{\mathrm{policy}}(s,a;\theta)-(r+\gamma\max_{a^{\prime}}Q_{\mathrm{target}}(s^{\prime},a^{\prime};\theta^-))\right)^2

其实际上就是贝尔曼最优误差,当Q_{target}(s,a;\theta)等于最优动作值的时候,上述损失函数在期望意义上等于0。

其中:

  • Q_{policy}主网络(Policy Network)预测的 Q 值

  • Q_{target}目标网络(Target Network)生成的 TD 目标

  • TD 目标:r+\gamma\max_{a^{\prime}}Q_{\mathrm{target}}(s^{\prime},a^{\prime};\theta^-)

训练步骤:

  1. 经验回放中采样一批 (s,a,r,s',done)

  2. 计算主网络预测的 Q 值

  3. 计算 TD 目标

  4. MSE 损失 → 反向传播 → 更新主网络参数

3、为什么需要引入两个网络?

        在最小化损失函数的过程中,需要使用梯度下降来更新神经网络的参数\theta。如果只使用一个网络,则\theta在损失函数的公式中出现两次,导致生成的TD目标随着参数更新变化,训练过程不稳定。

        所以在引入目标网络Q_{target}用于生成TD目标,而该网络的参数不随着训练更改参数,仅依靠一段时间步后将主网络Q_{policy}的参数赋值给他。这样可以保持短期内TD目标的稳定,便于主网络逼近目标。

4经验回放(Experience Replay)

什么是经验回放?

在强化学习中,智能体和环境交互产生数据:

\left ( s,a,r,s',done \right )

这些数据称为 经验样本(experience / transition)
经验回放的做法就是:

  • 用一个 缓冲区(replay buffer) 存储大量过去的经验

  • 在训练时 随机采样一小批经验 来更新网络

在标准经验回放中,抽取经验样本的时候应该服从均匀分布

经验回放的作用

  1. 打破数据相关性
    神经网络训练通常假设数据是 独立同分布(i.i.d.) 的,而RL 原始数据是 马尔可夫链。如果不使用经验回放进行随机采样,实际上相邻的样本(s0,a0,r0,s1),(s1,a1,r1,s2)具有强相关性,并不符合独立同分布的要求,会导致网络梯度方向受偏序列影响。
  2. 提高样本使用效率
    每个样本可以被采样使用多次。

5、DQN与Table Q-learning更新方式的区别

特性 表格 Q-Learning DQN
更新对象 每个 Q(s,a) 独立值 网络参数 θ
依据 TD 误差的随机样本 loss 对参数的梯度
收敛理论 Robbins-Monro随机逼近保证收敛 梯度下降保证 loss 减小
是否显式梯度 不需要计算梯度 需要梯度

三、DQN代码

DQN类

import random
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import gymnasium as gym
from collections import deque

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

class QNetwork(nn.Module):
    def __init__(self, state_dim, action_dim):
        super(QNetwork, self).__init__()
        self.fc = nn.Sequential(
            nn.Linear(state_dim, 128), nn.ReLU(),
            nn.Linear(128, 128), nn.ReLU(),
            nn.Linear(128, action_dim)
        )
    def forward(self, x):
        return self.fc(x)

class DQNAgent:
    def __init__(self, state_dim, action_dim, gamma=0.99, lr=1e-4,
                 epsilon=1.0, epsilon_min=0.01, epsilon_decay=0.995,
                 buffer_size=20000, batch_size=64, target_update=500,
                 device=device):

        self.device = device
        self.state_dim = state_dim
        self.action_dim = action_dim
        self.gamma = gamma
        self.epsilon = epsilon
        self.epsilon_min = epsilon_min
        self.epsilon_decay = epsilon_decay
        self.batch_size = batch_size
        self.target_update = target_update
        self.learn_step = 0

        self.policy_net = QNetwork(state_dim, action_dim).to(self.device)
        self.target_net = QNetwork(state_dim, action_dim).to(self.device)
        self.target_net.load_state_dict(self.policy_net.state_dict()) #目标网络与主网络初始参数相同
        self.target_net.eval() #目标网络不随训练更新

        self.optimizer = optim.Adam(self.policy_net.parameters(), lr=lr) #优化器
        self.memory = deque(maxlen=buffer_size)

    def choose_action(self, state):
        if np.random.rand() < self.epsilon:
            return np.random.randint(self.action_dim)
        state = torch.FloatTensor(state).unsqueeze(0).to(self.device)
        with torch.no_grad():
            q_values = self.policy_net(state)
        return int(torch.argmax(q_values).item())

    def store_transition(self, state, action, reward, next_state, done):
        self.memory.append((state, action, reward, next_state, done))

    def update(self):
        if len(self.memory) < self.batch_size:
            return

        batch = random.sample(self.memory, self.batch_size)
        states, actions, rewards, next_states, dones = zip(*batch)

        states = torch.FloatTensor(states).to(self.device)
        actions = torch.LongTensor(actions).unsqueeze(1).to(self.device)
        rewards = torch.FloatTensor(rewards).unsqueeze(1).to(self.device)
        next_states = torch.FloatTensor(next_states).to(self.device)
        dones = torch.FloatTensor(dones).unsqueeze(1).to(self.device)

        q_values = self.policy_net(states).gather(1, actions) #根据actions索引获取对应动作价值

        # y = r + gamma * max_a' Q_target(s',a')
        with torch.no_grad():
            next_q = self.target_net(next_states).max(1, keepdim=True)[0]
            target = rewards + (1 - dones) * self.gamma * next_q

        # 计算损失
        loss = nn.MSELoss()(q_values, target)

        # 反向传播更新主网络参数
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

        # epsilon 衰减
        self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)

        # 更新目标网络
        self.learn_step += 1
        if self.learn_step % self.target_update == 0:
            self.target_net.load_state_dict(self.policy_net.state_dict())

Openai Gym测试结果

import matplotlib.pyplot as plt

env = gym.make("CartPole-v1")

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

agent = DQNAgent(state_dim, action_dim, device=device)

episodes = 1000
reward_list = []

for ep in range(episodes):
    state, _ = env.reset()
    total_reward = 0
    done = False

    while not done:
        action = agent.choose_action(state)
        next_state, reward, terminated, truncated, _ = env.step(action)
        done = terminated or truncated

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

        state = next_state
        total_reward += reward

    reward_list.append(total_reward)

    if (ep + 1) % 50 == 0:
        avg_reward = np.mean(reward_list[-50:])
        print(f"Episode {ep+1}, 平均奖励: {avg_reward:.2f}, epsilon: {agent.epsilon:.3f}")

env.close()

奖励函数曲线:

因为ε-greedy 策略存在探索性,所以奖励最后容易产生波动。

使用greedy策略的进行评估的结果:

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐