【强化学习】从Q-learning到DQN的直观理解与代码
本文介绍了使用函数近似方法解决强化学习中的状态空间问题,重点分析了DQN算法。主要内容包括:1)函数近似的必要性,即解决连续状态空间、高维状态存储和泛化能力问题;2)DQN算法核心思想,通过神经网络近似Q函数,结合经验回放和目标网络提高稳定性;3)DQN与表格Q-learning的区别;4)DQN算法的PyTorch实现,包括Q网络结构、经验回放缓冲区和训练过程。实验结果表明,DQN能有效处理连续
一、价值表达:从表格到函数近似
1、为什么需要使用函数?
- 状态空间连续:使用表格 Q(s,a) 只能存离散状态,而很多环境状态是连续的,无法列出所有状态。
- 高维状态表示:在存储高维状态数据时,表格存储 Q值会爆炸式增长。而函数可以使用一个低维向量(函数参数向量)来描述一个高维向量(状态值),极大提高存储效率。
- 泛化能力:当使用表格时,只能针对已知状态进行对应修改。而函数是通过修改参数,从而以学习状态之间的规律,把经验推广到未见过的状态。
2、函数近似的方法
- 线性函数近似(Linear Function Approximation)
- 神经网络(Deep Neural Networks)
二、DQN(Deep Q-Network)
1、DQN算法的核心思想
-
用神经网络近似 Q 函数
-
利用 TD 目标 更新 Q 主网络(Policy Network)
-
通过 经验回放(Experience Replay) 和 目标网络(Target Network) 提高训练稳定性
2、核心更新公式
在数学公式上,DQN旨在最小化如下的损失函数:
其实际上就是贝尔曼最优误差,当等于最优动作值的时候,上述损失函数在期望意义上等于0。
其中:
-
:主网络(Policy Network)预测的 Q 值
-
:目标网络(Target Network)生成的 TD 目标
-
TD 目标:
训练步骤:
-
从经验回放中采样一批
(s,a,r,s',done) -
计算主网络预测的 Q 值
-
计算 TD 目标
-
MSE 损失 → 反向传播 → 更新主网络参数
3、为什么需要引入两个网络?
在最小化损失函数的过程中,需要使用梯度下降来更新神经网络的参数。如果只使用一个网络,则
在损失函数的公式中出现两次,导致生成的TD目标随着参数更新变化,训练过程不稳定。
所以在引入目标网络用于生成TD目标,而该网络的参数不随着训练更改参数,仅依靠一段时间步后将主网络
的参数赋值给他。这样可以保持短期内TD目标的稳定,便于主网络逼近目标。

4、经验回放(Experience Replay)
什么是经验回放?
在强化学习中,智能体和环境交互产生数据:
这些数据称为 经验样本(experience / transition)。
经验回放的做法就是:
-
用一个 缓冲区(replay buffer) 存储大量过去的经验
-
在训练时 随机采样一小批经验 来更新网络
在标准经验回放中,抽取经验样本的时候应该服从均匀分布。
经验回放的作用
- 打破数据相关性
神经网络训练通常假设数据是 独立同分布(i.i.d.) 的,而RL 原始数据是 马尔可夫链。如果不使用经验回放进行随机采样,实际上相邻的样本(s0,a0,r0,s1),(s1,a1,r1,s2)具有强相关性,并不符合独立同分布的要求,会导致网络梯度方向受偏序列影响。 - 提高样本使用效率
每个样本可以被采样使用多次。
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策略的进行评估的结果:

更多推荐
所有评论(0)