资讯详情

DQN跑通迷宫:从Q-table到神经网络的强化学习实践

📅 2026/10/5 2:36:53 | 华诺云谱 👁 阅读
DQN跑通迷宫:从Q-table到神经网络的强化学习实践
简介面向深度学习与强化学习初学者这份PDF系统讲解深度强化学习DQN的核心原理并以“迷宫寻路”为例演示如何用神经网络近似Q函数突破传统Q-Learning在高维状态动作空间下的存贮与计算瓶颈。内容涵盖Q-Learning基础、DQN损失函数设计、Experience Replay经验回放、epsilon-greedy动作选择等关键知识点并基于TensorFlow实现一个可运行的迷宫求解示例源码直接附在文档中便于读者对照理解与二次实践。资源为单个PDF文件体积约205KB轻量易读适合快速学习入门也适合作为课程设计或科研项目中引入DQN的参考资料。目前已有1774人学习下载口碑与实用性得到一定验证。如需快速上手深度强化学习或希望用一个具体迷宫案例打通“原理—网络构建—训练流程—结果分析”这份文档能提供清晰的路径与可直接参考的TensorFlow代码。1. DQN跑通迷宫之前先想清楚它凭什么接替Q-table如果你翻过强化学习入门资料大概率见过这张6×6的迷宫奖励矩阵分别在状态2、4、5设置正奖励墙壁位置一律给负分agent要从任意房间出发找到去5号房间的路。传统Q-Learning的做法是维护一张Q-table每个state-action对占一格迷宫小的时候没问题一旦状态变多、动作变多表格大小指数膨胀内存根本装不下。这正是DQN出现的理由用神经网络去近似Q函数输入当前状态输出每个动作的Q值把查表问题变成回归问题。这篇笔记围绕一个可跑的DQN迷宫源码展开讲清楚网络结构、epsilon探索、经验回放、损失计算这几个核心模块再把我实际运行时踩过的坑和排查思路一并列出。适合刚看完Q-Learning、想弄明白DQN到底改了什么的人也适合拿到代码后不知道怎么调参数的新手。2. 项目结构与神经网络搭建6×6迷宫如何塞进3层网络2.1 先看懂这份源码的骨架整份源码只包含一个DeepQNetwork类没有拆成多个文件这在教学Demo里很常见。类内部按职责划分成几个方法create_network()负责建网络select_action()负责按策略选动作save_store()负责存记忆step()负责执行动作并返回奖励和下一个状态experience_replay()负责从记忆库中抽样训练train()是主循环pay()是训练完之后的测试展示。类的顶部定义了一组关键参数这些参数值得逐个看清楚因为它们直接决定训练行为参数值作用OBSERVE1000前1000步只探索不训练累积足够记忆BATCH20每次从记忆库抽20条样本训练INITIAL_EPSILON0.1epsilon初始值探索概率上限FINAL_EPSILON0.0001epsilon衰减下限EXPLORE3000000epsilon从初始值衰减到最小值所需总步数learning_rate0.001梯度下降学习率gamma0.9未来奖励折损率memory_size5000经验回放池容量上限state_num6状态数action_num6动作数state_list和action_list都用np.identity()生成单位矩阵每一行代表一个状态或动作的one-hot编码。比如状态1编码成[[0,1,0,0,0,0]]动作3编码成[[0,0,0,1,0,0]]。这种编码方式是理解后面损失函数计算的关键因为网络输出的是一个6维向量只有通过one-hot点乘才能把当前执行动作对应的Q值单独取出来。r矩阵是这份源码的核心先验知识它提前定义好了迷宫的地图结构r np.array([[-1, -1, -1, -1, 0, -1], [-1, -1, -1, 0, -1, 100.0], [-1, -1, -1, 0, -1, -1], [-1, 0, 0, -1, 0, -1], [0, -1, -1, 1, -1, 100], [-1, 0, -1, -1, 0, 100], ])这里r[state][action]表示在状态state下执行动作action得到的即时奖励next_state直接等于action。也就是说这个迷宫的规则是执行动作a就走到房间a奖励由r矩阵决定。动作5是目标房间从状态1、4、5出发到达动作5都能拿到100分。2.2 create_network三层网络如何输出Q值create_network()是理解DQN替代Q-table的关键入口。先看代码def create_network(self): self.q_eval_input tf.placeholder(shape[None, self.state_num], dtypetf.float32) self.action_input tf.placeholder(shape[None, self.action_num], dtypetf.float32) self.q_target tf.placeholder(shape[None], dtypetf.float32) neuro_layer_1 3 w1 tf.Variable(tf.random_normal([self.state_num, neuro_layer_1])) b1 tf.Variable(tf.zeros([1, neuro_layer_1]) 0.1) l1 tf.nn.relu(tf.matmul(self.q_eval_input, w1) b1) w2 tf.Variable(tf.random_normal([neuro_layer_1, self.action_num])) b2 tf.Variable(tf.zeros([1, self.action_num]) 0.1) self.q_eval tf.matmul(l1, w2) b2 self.reward_action tf.reduce_sum(tf.multiply(self.q_eval, self.action_input), reduction_indices1) self.loss tf.reduce_mean(tf.square((self.q_target - self.reward_action))) self.train_op tf.train.GradientDescentOptimizer(self.learning_rate).minimize(self.loss) self.predict tf.argmax(self.q_eval, 1)逻辑说明网络结构是6→3→6输入层6个神经元对应6个状态的one-hot编码隐藏层3个神经元配ReLU激活输出层6个神经元对应6个动作的Q值。q_eval_input是状态输入action_input是当前执行动作的one-hot编码q_target是计算出的目标Q值这三个都是placeholder训练时通过feed_dict填充。tf.multiply(self.q_eval, self.action_input)做的是逐元素相乘不是矩阵乘法。假设网络对状态1输出的Q值是[[0.81, 0.5, 0.24, 0.513, 0.9, 0.71]]agent执行了动作3action_input是[[0,0,0,1,0,0]]逐元素相乘后得到[[0,0,0,0.513,0,0]]reduce_sum把非零元素累加最终reward_action就是0.513——当前状态下执行动作3的Q值。这套做法本质上是把离散动作的Q值提取转化成回归标签避免了像分类任务那样计算softmax交叉熵。2.3 损失函数的直觉理解源码里loss用的是tf.square((self.q_target - self.reward_action))也就是均方误差。为什么不用交叉熵因为DQN的输出本质是连续数值回归网络要拟合的目标不是概率分布而是每个动作的价值估计。q_target是目标Q值reward_action是当前网络对已执行动作的Q值估计训练就是让后者去逼近前者。这里有一个容易混淆的点q_target并不是固定不变的它本身由即时奖励和下一状态的最大Q值计算而来随着网络参数更新q_target也在漂移。这是DQN和普通监督学习的本质区别——监督学习标签固定DQN的标签依赖网络自身的预测。所以源码中每次experience_replay()都在重新计算q_target而不是用预先算好的静态标签。我之前单独跑过一个最小网络验证这个思路确认3层网络加梯度下降能收敛再回到完整代码里调参。新手建议也按这个节奏来先把网络跑通再让agent去探索否则网络结构错了后面全是白费功夫。3. epsilon贪心与经验回放两个参数直接决定能不能收敛3.1 select_action探索与利用的拉锯战select_action()是DQN里最容易翻车的函数之一它决定了agent是去尝试没走过的路探索还是走当前认知中最优的路利用。源码里的实现是标准epsilon-greedydef select_action(self, state_index): current_state self.state_list[state_index:state_index 1] if np.random.uniform() self.epsilon: current_action_index np.random.randint(0, self.action_num) else: actions_value self.session.run(self.q_eval, feed_dict{self.q_eval_input: current_state}) action np.argmax(actions_value) current_action_index action if self.step_index self.OBSERVE and self.epsilon self.FINAL_EPSILON: self.epsilon - (self.INITIAL_EPSILON - self.FINAL_EPSILON) / self.EXPLORE return current_action_index逻辑说明每次选动作时生成一个[0,1)区间的随机数如果小于epsilon就走随机策略在6个动作里随便选一个否则把当前状态输入网络取输出Q值最大的动作。epsilon初始为0.1意味着训练早期有10%的概率随机探索随着步数增加、epsilon逐步衰减到0.0001agent越来越依赖网络判断。参数说明INITIAL_EPSILON设成0.1其实偏低很多DQN实现会用0.9甚至1.0起步让agent前期大量随机探索。这个迷宫只有6个状态0.1已经够用但换到更大的环境就得调高。EXPLORE设为3000000意味着衰减速度非常慢300万步后才基本停止随机探索配合OBSERVE1000前1000步只探索不训练记忆池有足够多样本后网络才开始学习。3.2 save_store记忆池的写入与淘汰经验回放的核心意义在于打破时间序列样本之间的相关性。如果每次采样后立刻训练相邻两个样本高度相关网络参数会朝一个方向持续偏置导致收敛困难甚至发散。源码用deque实现记忆池def save_store(self, current_state_index, current_action_index, current_reward, next_state_index, done): current_state self.state_list[current_state_index:current_state_index 1] current_action self.action_list[current_action_index:current_action_index 1] next_state self.state_list[next_state_index:next_state_index 1] self.replay_memory_store.append(( current_state, current_action, current_reward, next_state, done)) if len(self.replay_memory_store) self.memory_size: self.replay_memory_store.popleft() self.memory_counter 1逻辑说明每次agent执行一步动作就把(当前状态, 当前动作, 奖励, 下一个状态, 是否结束)这个五元组追加到deque尾部。deque自带popleft()当记忆数超过memory_size5000时自动淘汰最旧的记忆。这样记忆池始终保留最近5000条经验既避免内存无限增长也确保训练样本相对接近当前策略。参数说明memory_size设5000对6×6迷宫绰绰有余但要注意一个问题如果环境状态空间很大5000条记忆可能覆盖不到足够多样的状态网络会反复在少数状态上过拟合。大场景一般设到100000以上。done这个变量在save_store里虽然存了但后面计算q_target时源码并没有真正使用它这是个隐患后面避坑章节细说。3.3 step函数奖励矩阵的读取规则step()是环境交互层代码很短def step(self, state, action): reward self.r[state][action] next_state action done False if action 5: done True return next_state, reward, done逻辑说明next_state直接等于action意味着这是一个「搬到哪个房间」的决策问题而不是「往哪个方向走一步」的连续空间问题。6个动作对应6个目标房间r[state][action]给出执行该动作的即时奖励。动作5是走出迷宫返回doneTrue。这里我最初踩过一个直觉错误我以为迷宫问题应该用上下左右四方向动作看到6个动作还以为写错了。实际上这份源码把「移动」抽象成「直接跳转到目标房间」奖励矩阵决定了哪些跳转被惩罚、哪些被奖励。理解了这点再看r矩阵就清晰了——负奖励代表撞墙或非法移动0代表普通移动正奖励代表到达目标。4. 训练主循环与经验回放target_q是怎么一步步逼近的4.1 train函数先攒记忆再训练train()是agent和环境交互的总控制器def train(self): current_state np.random.randint(0, self.action_num - 1) self.epsilon self.INITIAL_EPSILON while True: action self.select_action(current_state) next_state, reward, done self.step(current_state, action) self.save_store(current_state, action, reward, next_state, done) if self.step_index self.OBSERVE: self.experience_replay() if self.step_index 10000: break if done: current_state np.random.randint(0, self.action_num - 1) else: current_state next_state self.step_index 1逻辑说明主循环的流程是「选动作→执行→存记忆→达到观察步数后训练→判断终止」。两个细节值得注意第一current_state初始化和done之后都用np.random.randint(0, self.action_num - 1)重新随机上限是action_num - 1也就是5所以初始状态永远不是目标房间5避免agent一开始就站在终点第二训练不是每一步都做step_index超过OBSERVE后才调用experience_replay()这是为了让记忆池先攒够样本否则抽样质量太差。参数说明10000是硬编码的最大训练步数到达就退出。这个数字对6×6迷宫足够了实际训练中大概几千步就能收敛但如果你增大迷宫规模这里也要跟着调大。np.random.seed没有设置所以每次运行结果可能不同这是随机训练的常态。4.2 experience_replay样本组装与q_target计算experience_replay()的代码最长但因为它是DQN的数据流核心值得仔细拆解def experience_replay(self): batch self.BATCH if self.memory_counter self.BATCH else self.memory_counter minibatch random.sample(self.replay_memory_store, batch) batch_state None batch_action None batch_reward None batch_next_state None batch_done None for index in range(len(minibatch)): if batch_state is None: batch_state minibatch[index][0] elif batch_state is not None: batch_state np.vstack((batch_state, minibatch[index][0])) # batch_action、batch_reward、batch_next_state、batch_done同样处理 q_next self.session.run([self.q_eval], feed_dict{self.q_eval_input: batch_next_state}) q_target [] for i in range(len(minibatch)): current_reward batch_reward[i][0] q_value current_reward self.gamma * np.max(q_next[0][i]) if current_reward 0: q_target.append(current_reward) else: q_target.append(q_value) _, cost, reward self.session.run( [self.train_op, self.loss, self.reward_action], feed_dict{self.q_eval_input: batch_state, self.action_input: batch_action, self.q_target: q_target}) self.cost_his.append(cost) self.learn_step_counter 1逻辑说明先从记忆池随机抽BATCH条样本把五元组拆成五个独立的batch数组。然后用batch_next_state喂给网络得到所有下一状态的动作Q值每个样本取最大值np.max(q_next[0][i])乘以gamma后加上即时奖励得到目标Q值q_target。最后用这个q_target和网络当前预测的reward_action计算loss执行一次梯度下降。这里有一个特殊处理if current_reward 0: q_target.append(current_reward)。翻译成人话就是——如果这一步拿到了负奖励撞墙或非法移动目标Q值直接等于这个负奖励不做未来奖励折损。为什么这样处理因为负奖励的下一步状态可能也是死路如果继续用reward gamma * max(q_next)会把负值通过折损传导到前面的状态导致所有状态都学成负值。直接截断让agent明确记住「这一步不该走」。参数说明gamma0.9表示未来10步的奖励折损到当前约0.35折损越快agent越短视折损越慢agent越倾向于考虑远期收益。迷宫只有6个状态0.9合适。BATCH20对这个小场景够用大场景一般至少32或64。GradientDescentOptimizer的learning_rate0.001偏保守换来稳定。4.3 网络输出与损失逼近的过程理解q_target这段代码需要厘清一个容易混淆的地方代码里用self.q_eval这个网络同时计算了reward_action和q_next。也就是说目标Q值和当前Q值用的是同一个网络、同一组权重。这在原始DQN的2013版本里是合法的做法但训练过程中网络权重一直在变化导致q_target也在不断变化相当于用移动的目标训练移动的模型容易震荡。2015年Nature版DQN引入了Target Network来缓解这个问题——用另一组延迟更新的参数计算q_target让目标在一段时间内保持稳定。如果这份源码训练不收敛优先考虑改成双网络结构。不过对这个6×6迷宫单网络的实现已经够用我在本地跑通的过程里loss从几千降到几十的量级大概只需要几百次experience_replay调用。5. 避坑清单DQN在6×6迷宫上的五个翻车现场5.1 训练结束后测试路径有问题agent仍然随机跑现象pay()里打印测试路径时agent从状态0出发走到了3又从3跳回1来回绕圈完全看不出学到了最优策略。原因select_action()在测试阶段仍然有epsilon的随机探索分支。如果epsilon衰减不到位或者训练步数不够随机动作概率仍然偏高。这个坑的根源在于训练主循环和测试共用同一个动作选择函数没有区分训练模式和推理模式。解决在pay()里直接用self.session.run(self.predict, feed_dict{...})取argmax结果绕过epsilon判断。或者设置一个self._is_training标志位测试时强制走贪心分支。我当时的做法是复制一份纯贪心的动作选择逻辑到pay()里确保测试路径完全确定。5.2 负奖励处的q_target直接截断导致正负样本训练比例失真现象训练过程中loss下降到一定值后就不再变化打印路径发现agent只学会了避开明显的负奖励动作但对多个正奖励动作没有偏好路径不唯一。原因experience_replay()里if current_reward 0直接截断q_target这个逻辑的初衷是好的——避免负值传导到前序状态。但它会造成学习信号的不平衡大量负奖励样本的优化目标是「等于负值」少量正奖励样本的优化目标是「reward gamma * max(q_next)」后者被前者淹没。解决如果追求最优最短路我的建议是把所有非负奖励统一走current_reward gamma * np.max(q_next)的计算路径只在doneTrue时才强制q_target current_reward。也就是让负奖励也能通过折损向后续状态传导。改完之后agent会明显更倾向于选择能连到正奖励的路径而不是孤立地避开负奖励。5.3 done标志物存了却不用到达终点后的状态没有正确处理现象训练收敛后从状态1出发的路径是1→3→5但从状态0出发偶尔会走出0→4→3→1→3的环然后才到5路径明显不是最短。原因save_store()存了done标志但experience_replay()计算q_target时完全没看它。当agent到达状态5后next_state还是5q_next会输出状态5下所有动作的Q值其中动作5的Q值已经被学成100左右折损后依然很大。这导致agent在非目标状态也能学到「跳到5就能拿高分」的迂回路线绕远路变成可接受行为。解决在q_target计算处对done做分支如果doneTrueq_target current_reward不再累加未来奖励否则才计算current_reward gamma * np.max(q_next)。同时把batch_done正确转成与batch_reward相同的维度格式否则batch_done[i][0]的取值方式会报错。5.4 奖励矩阵里用了100.0和1这样差距悬殊的数值路径稳定但不够短现象训练后agent能找到一条到达目标5的路但路径长度偏长不是最短路。原因奖励矩阵中r[1][5]100.0和r[3][5]1差距太大。Q-Learning的收敛目标是最优路径但当正奖励数值悬殊时agent会优先选择数值更大的奖励路径而不是步数更少的路径。比如从状态2出发可能倾向于走2→3→1→5拿100而不是2→3→5拿1再折损也可能大于1但数值上100更有吸引力。解决把正奖励统一改成相同数值比如都设1让agent通过gamma的折损自己去比较路径长短。或者把reward从稀疏大数值改成稠密小数值比如每一步给一个微小惩罚让agent自然倾向走短路径。我的经验是迷宫类问题尽量用稠密奖励稀疏大奖励容易让agent学出「贪大」而非「求近」的策略。5.5 训练样本是时间序列记忆池没攒够就开训loss震荡剧烈现象把OBSERVE改成100甚至0训练刚开始loss就在几千到几万之间来回跳完全无法下降。原因经验回放的意义在于打破样本相关性。如果记忆池里只有几十条样本random.sample抽出来的20条很可能仍然高度相关——它们都来自同一条探索轨迹。网络对这20条样本做梯度下降时会被同一方向的偏置带跑下一步又抽到另一段轨迹损失又往反方向调整形成震荡。解决OBSERVE1000不是玄学它保证记忆池里至少有1000条来自不同探索阶段的样本抽样才能覆盖足够多样的状态转移。如果环境状态空间更大OBSERVE还要继续加大。一个通用判断标准是OBSERVE至少是BATCH的10倍且记忆池里应该能看到每个状态至少出现几十次。避坑部分的这些现象源头几乎都指向同一个设计问题——训练和推理共用逻辑、奖励设计不够精细、q_target计算没有区分终止态。如果你改完代码还是收敛慢优先检查这三处。6. 验证与进阶把打印路径变成判断收敛的真正标准pay()方法在训练结束后会打印从每个状态出发的移动路径这是最直观的验证手段def pay(self): self.train() print(self.r) for index in range(5): start_room index current_state start_room step 0 target_state 5 while current_state ! target_state: out_result self.session.run( self.q_eval, feed_dict{self.q_eval_input: self.state_list[current_state:current_state 1]}) next_state np.argmax(out_result[0]) current_state next_state step 1 print(Agent 从, start_room, 出发走了, step, 步到达房间5)跑通这份代码之后如果只验证「能打印路径」就结束其实漏掉了两个更重要的检查。第一把训练参数learning_rate调大到0.01观察loss曲线是否震荡这能帮你直观理解学习率对DQN稳定性的影响第二把INITIAL_EPSILON从0.1改成0.9你会看到前期探索变多、收敛变慢但路径多样性更好这能帮你理解探索与利用的权衡。更进阶的做法是画loss曲线。cost_his列表在每次experience_replay()后追加当前loss训练结束后用matplotlib画出来你会发现一个典型特征——loss不是单调下降而是先快速下降、再缓慢波动。这很正常因为q_target本身在变。如果loss完全不平滑、一直剧烈震荡且幅度不缩小那大概率是学习率太高或BATCH太小。我的习惯是把pay()里的打印逻辑抽出来写成独立函数传入任意起点状态和最多步数这样方便批量验证也方便以后把迷宫换成更大的地图时调试——只需要改r矩阵和state_num、action_num其他代码可以复用。从那以后我每次跑强化学习Demo都会强制走一遍「先验证网络结构→再调epsilon衰减→确认记忆池存量→最后看路径合理性」这个流程避免基于一条偶然跑通的路径就相信模型真的学会了。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑