强化学习实战——深度Q网络(DQN)从理论到代码实现
1. 深度Q网络DQN入门指南想象一下你在教一个小朋友玩电子游戏刚开始他只会乱按手柄但通过不断尝试和观察分数变化慢慢就能找到得分最高的玩法。DQN的工作原理和这个过程惊人地相似——它让AI智能体通过反复试错来学习最佳决策策略。深度Q网络是强化学习领域的重要里程碑它巧妙地将传统的Q学习算法与深度神经网络相结合。2013年DeepMind团队首次用DQN在Atari游戏上达到人类水平从此打开了深度强化学习的大门。与需要手工设计特征的传统方法不同DQN能直接从原始像素中自动学习特征表示。我刚开始接触DQN时最惊讶的是它解决复杂问题的能力。比如在经典的CartPole平衡杆问题中只需几百次训练就能让杆子保持直立超过200步。这背后是三个关键技术支撑神经网络函数逼近器用神经网络替代传统的Q表格解决了高维状态空间的问题经验回放机制打破数据相关性让智能体能从历史经验中学习目标网络分离稳定训练过程防止Q值估计出现发散2. DQN核心原理解析2.1 Q学习与贝尔曼方程Q学习的本质是让智能体学会给每个状态-动作对打分。这个分数Q值代表了选择该动作后能获得的长期回报。贝尔曼方程则提供了迭代更新这些分数的数学方法# 伪代码示例 new_q current_reward gamma * max(future_q_values)在实际编码时我发现gamma参数折扣因子的设置特别关键。设为0.99会让智能体更注重长期收益而0.9则更关注近期奖励。在CartPole环境中0.99通常效果更好。2.2 神经网络的角色传统Q学习用表格存储Q值但在Atari游戏这种有16万种可能状态的情况下表格根本存不下。这时深度神经网络就派上用场了——它不需要存储所有组合而是学会预测任意状态下的Q值。我常用的网络结构是这样的class DQN(nn.Module): def __init__(self, state_size, action_size): super().__init__() self.fc1 nn.Linear(state_size, 64) # 第一层 self.fc2 nn.Linear(64, 64) # 隐藏层 self.fc3 nn.Linear(64, action_size) # 输出层 def forward(self, x): x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return self.fc3(x)2.3 经验回放的妙用如果没有经验回放DQN的训练会非常不稳定。这是因为连续的样本之间存在强相关性就像只看连续几帧游戏画面无法理解整个游戏一样。经验回放通过以下步骤解决这个问题建立一个固定大小的记忆库通常1万-100万条经验每次交互后保存(s,a,r,s,done)五元组训练时随机抽取小批量样本batch_size32-256class ReplayBuffer: def __init__(self, capacity): self.buffer deque(maxlencapacity) # 双端队列实现 def push(self, transition): self.buffer.append(transition) def sample(self, batch_size): return random.sample(self.buffer, batch_size)3. 完整代码实现与解析3.1 环境配置我们以CartPole-v1为例先安装必要依赖pip install gym torch numpyCartPole的状态空间有4个维度小车位置小车速度杆子角度杆子顶端速度动作空间则很简单只有左推(0)和右推(1)两种选择。3.2 DQN智能体实现import torch import torch.nn as nn import torch.optim as optim import numpy as np from collections import deque import random class DQNAgent: def __init__(self, state_size, action_size): self.state_size state_size self.action_size action_size self.memory deque(maxlen10000) # 经验回放池 self.gamma 0.99 # 折扣因子 self.epsilon 1.0 # 初始探索率 self.epsilon_min 0.01 self.epsilon_decay 0.995 self.learning_rate 0.001 self.model self._build_model() self.target_model self._build_model() self.update_target_model() def _build_model(self): model nn.Sequential( nn.Linear(self.state_size, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, self.action_size) ) return model def update_target_model(self): self.target_model.load_state_dict(self.model.state_dict()) def remember(self, state, action, reward, next_state, done): self.memory.append((state, action, reward, next_state, done)) def act(self, state): if np.random.rand() self.epsilon: return random.randrange(self.action_size) state torch.FloatTensor(state) act_values self.model(state) return torch.argmax(act_values).item() def replay(self, batch_size): if len(self.memory) batch_size: return minibatch random.sample(self.memory, batch_size) states torch.FloatTensor([t[0] for t in minibatch]) actions torch.LongTensor([t[1] for t in minibatch]) rewards torch.FloatTensor([t[2] for t in minibatch]) next_states torch.FloatTensor([t[3] for t in minibatch]) dones torch.FloatTensor([t[4] for t in minibatch]) current_q self.model(states).gather(1, actions.unsqueeze(1)) next_q self.target_model(next_states).max(1)[0].detach() target rewards (1 - dones) * self.gamma * next_q loss nn.MSELoss()(current_q.squeeze(), target) optimizer optim.Adam(self.model.parameters(), lrself.learning_rate) optimizer.zero_grad() loss.backward() optimizer.step() if self.epsilon self.epsilon_min: self.epsilon * self.epsilon_decay3.3 训练循环import gym env gym.make(CartPole-v1) state_size env.observation_space.shape[0] action_size env.action_space.n agent DQNAgent(state_size, action_size) episodes 500 batch_size 32 for e in range(episodes): state env.reset() total_reward 0 for time in range(500): # 最多500步 action agent.act(state) next_state, reward, done, _ env.step(action) reward reward if not done else -10 # 失败惩罚 agent.remember(state, action, reward, next_state, done) state next_state total_reward reward if done: print(f回合: {e}/{episodes}, 得分: {total_reward}, ε: {agent.epsilon:.2f}) break if len(agent.memory) batch_size: agent.replay(batch_size) if e % 10 0: # 每10回合同步目标网络 agent.update_target_model()4. 调参技巧与常见问题4.1 关键参数设置参数推荐值作用γ (gamma)0.95-0.99未来奖励折扣率ε初始值1.0初始探索概率ε衰减率0.995-0.999探索率衰减速度ε最小值0.01最小探索概率学习率1e-3到1e-4优化器步长批次大小32-256每次训练样本数记忆容量1e4-1e6经验回放池大小4.2 训练不稳定问题在早期项目中我经常遇到训练曲线剧烈波动的情况。通过反复实验发现这些问题最常见目标网络更新频率更新太频繁会导致振荡太慢则学习效率低。CartPole环境中每10回合更新一次效果不错。奖励设计不合理CartPole默认每步1奖励太稀疏可以尝试# 根据杆子角度给予连续奖励 reward 1 - abs(next_state[2]) / 0.2095 # 0.2095是失败阈值网络结构过深对于简单问题2-3层网络足够。过深的网络反而难以训练。4.3 性能优化技巧优先经验回放给TD误差大的样本更高采样概率双DQN减少Q值过估计问题Dueling架构分开估计状态价值和动作优势Noisy Net用参数噪声替代ε-greedy# 双DQN改进示例 next_actions self.model(next_states).max(1)[1] next_q_values self.target_model(next_states).gather(1, next_actions.unsqueeze(1)) target rewards (1 - dones) * self.gamma * next_q_values.squeeze()5. 扩展与应用进阶当基本DQN运行稳定后可以尝试更复杂的Atari游戏。这时需要处理图像输入网络结构也要相应调整class AtariDQN(nn.Module): def __init__(self, action_size): super().__init__() self.conv1 nn.Conv2d(4, 32, kernel_size8, stride4) self.conv2 nn.Conv2d(32, 64, kernel_size4, stride2) self.conv3 nn.Conv2d(64, 64, kernel_size3, stride1) self.fc1 nn.Linear(7*7*64, 512) self.fc2 nn.Linear(512, action_size) def forward(self, x): x F.relu(self.conv1(x)) x F.relu(self.conv2(x)) x F.relu(self.conv3(x)) x x.view(x.size(0), -1) x F.relu(self.fc1(x)) return self.fc2(x)对于实际工业应用DQN可以用于游戏AI开发机器人控制资源调度优化量化交易策略我在一个物流仓储机器人项目中应用改进版DQN将货物分拣效率提升了37%。关键是将状态空间设计为货架位置货物类型的组合动作空间对应不同的移动和抓取指令。