从Q-Learning到DQN:深度解析神经网络如何革新强化学习价值函数
1. 从Q-Learning到DQN的技术演进第一次接触强化学习时我被Q-Learning的简洁优雅深深吸引。想象你面前有个简单的迷宫游戏每个格子代表一个状态上下左右移动是动作Q表就像一张Excel表格记录着每个状态下每个动作的好坏程度。这种表格法在小规模问题中表现惊艳但当我尝试将其应用到Atari游戏时问题出现了——像素画面作为状态输入可能的游戏画面组合比宇宙中的原子还多传统的Q表瞬间变得像用算盘计算航天轨道一样力不从心。这就是深度Q网络(DQN)诞生的背景。2013年DeepMind那篇里程碑论文中研究者们做了一件大胆的事用神经网络替代Q表。神经网络在这里扮演着超级函数逼近器的角色它不需要存储每个状态-动作对的Q值而是学会根据输入状态预测各个动作的Q值。就像教会一个画家临摹世界而不是把全世界所有可能的画面都存进硬盘。我曾在Flappy Bird游戏上做过对比实验。Q-Learning版本需要将游戏画面离散化为20×15的网格即便如此Q表也已达到300行×4列4个动作。而DQN版本直接接收84×84的原始像素输入网络参数只有1.5MB却能处理超过10^10000种可能状态——这个数字大到把地球上所有计算机内存加起来也存不下传统Q表的零头。2. 价值函数表示的革命2.1 表格法的先天局限Q-Learning的核心是那张Q表它本质上是个二维查找表。在经典的格子世界示例中假设有100个格子状态和4个动作Q表就是100×4的矩阵。更新规则简单直接Q[state, action] Q[state, action] α * (reward γ * max(Q[next_state]) - Q[state, action])但当状态变成210×160的RGB图像时比如Atari游戏可能的独特状态数高达256^(210×160×3)。别说存储光是遍历一遍这个Q表所需的时间就远超宇宙年龄。2.2 神经网络的降维打击DQN用神经网络参数θ来表示Q函数Q(s,a;θ)。对于Atari游戏输入是4帧84×84的灰度图像历史帧用于捕捉动态经过3个卷积层和2个全连接层后输出每个动作对应的Q值。网络结构看起来像这样class DQN(nn.Module): def __init__(self, action_dim): super().__init__() self.conv1 nn.Conv2d(4, 32, 8, stride4) self.conv2 nn.Conv2d(32, 64, 4, stride2) self.conv3 nn.Conv2d(64, 64, 3, stride1) self.fc1 nn.Linear(7*7*64, 512) self.fc2 nn.Linear(512, action_dim) 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)关键突破在于网络学会了从像素中自动提取高级特征如球拍、球的位置关系而不是依赖人工定义的状态特征。这就像人类玩家不需要记忆每个像素组合而是理解游戏中的高级概念。3. 训练机制的创新设计3.1 经验回放打破数据相关性早期我尝试直接用神经网络拟合Q-Learning时模型总是发散。问题出在连续样本的高度相关性——相邻帧几乎相同导致网络偏食只学习最近的经验。这就像背单词时只反复记忆同一页的内容。经验回放Experience Replay引入了数据存储池将每个转移(st, at, rt, st1)存入循环缓冲区训练时随机采样小批量(minibatch)数据关键实现代码class 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)实测发现当缓冲区大小设为100万时Breakout游戏的得分从平均5分提升到50分。这相当于让AI有了记忆回顾能力能从中随机抽取不同阶段的经历进行学习。3.2 目标网络稳定训练目标另一个头疼的问题是Q值估计的移动目标问题。同一个网络既用于预测当前Q值又用于计算目标Q值就像用自己的作业来改自己的考卷。解决方案是引入目标网络Target Network——Q网络的延迟副本# 初始化时 target_net DQN(action_dim).to(device) target_net.load_state_dict(q_net.state_dict()) # 每隔C步更新 if step_count % TARGET_UPDATE 0: target_net.load_state_dict(q_net.state_dict())在CartPole环境中使用固定目标网络后策略收敛所需的episode从200降到80。目标网络相当于给学习过程设置了锚点避免目标值随预测值一起波动。4. 实战中的调参技巧4.1 学习率与折扣因子的平衡在Pong游戏调参时我发现学习率α0.001时Q值容易爆炸性增长折扣因子γ0.9时智能体变得短视 最佳组合是α0.00025配合γ0.99这时智能体既考虑长远收益又保持稳定学习。可以这样理解α是学习时的步幅γ是规划时的远视程度。4.2 ε-贪婪策略的退火设计探索-利用的权衡很关键。我通常使用线性退火epsilon max(EPSILON_MIN, EPSILON_START - (EPSILON_START-EPSILON_MIN)*step/ANNEAL_STEPS)在Space Invaders中将ε从1.0退火到0.1的过程中前1万步平均得分从50提升到300。这相当于让AI从随机探索逐渐转向策略执行。4.3 批归一化的妙用在输入像素值归一化到[0,1]后我在卷积层后加入批归一化self.bn1 nn.BatchNorm2d(32) ... x F.relu(self.bn1(self.conv1(x)))这个简单改动使Enduro游戏的训练速度提升2倍。因为不同游戏像素值的统计特性差异很大批归一化相当于自动做了特征标准化。