拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深度强化学习DQN实战:用神经网络替换Q表的五个关键细节

深度强化学习DQN实战:用神经网络替换Q表的五个关键细节

简介:一份系统讲解深度强化学习DQN(Deep Q-Network)原理与实战应用的PDF文档,面向从传统Q-Learning跨向深度Q网络学习的算法学习者。内容以经典迷宫问题为案例,从强化学习的基本状态(state)与动作(action)概念讲起,说明Q-Learning维护Q表在状态与动作增多时的存储瓶颈,进而引出用神经网络拟合Q函数的核心思路。文档详细拆解了经验回放(Experience Replay)的设计动机、训练样本抽取方式、损失函数(target_q与q的均方误差)以及epsilon-greedy探索策略,并提供了基于TensorFlow实现的完整示例代码,包括6×6迷宫的状态矩阵、动作矩阵、记忆库存储与网络更新流程。资源为单文件PDF,整体大小约205KB,适合随时翻阅;已有1774人下载学习,可作为DQN入门讲解、课程参考或实现智能体避障小项目的底层参考。

1. 深度强化学习DQN:用神经网络替换Q表之前,先想清楚这五件事

深度强化学习DQN(Deep Q-Network)解决迷宫问题,核心思路是用神经网络替代Q-Learning里的Q表。Q-Learning在状态少时很好用,但状态一多、动作一多,Q表的内存占用和计算量就会爆炸,深度强化学习DQN的提出就是为了把这张表换成网络。我拆这份代码时最直观的感受是:它把DQN最关键的三件事都讲透了——经验回放怎么存样本、epsilon-greedy怎么平衡探索与利用、loss怎么从目标Q值和当前Q值的差值中构造。适合刚学完Q-Learning、想知道神经网络怎么接进来的读者,也适合做路径规划想快速跑通一个最小示例的从业者。资源里自带源码和PDF讲解,如果手边恰好有Python和TensorFlow环境,半小时内就能看到一个小agent在6个房间之间学会找目标。整个过程不算复杂,但里面有几个参数和设计细节,不拆开讲清楚,照着敲完你也不知道它为什么能跑通。

2. DQN的网络搭建与loss设计:三层网络怎么替掉6×6的Q表

2.1 状态与动作的编码方式:one-hot是DQN里最容易被忽略的细节

状态数和动作数在代码里直接定义为6,这6对应的是迷宫里6个房间。状态矩阵用np.identity(6)生成,意思是每个状态用一个6维的one-hot向量表示:状态0是[1,0,0,0,0,0],状态1是[0,1,0,0,0,0]。动作矩阵也一样,每个动作也是一个one-hot向量。这个设计不是随便来的——DQN的神经网络输入层必须接收固定维度的向量,one-hot编码把离散的状态映射成了网络能消化的数值形式。

self.state_list = np.identity(self.state_num) self.action_list = np.identity(self.action_num)

state_num和action_num都是6,所以这两个矩阵都是6×6。代码里后面取状态、取动作时都是切片操作,比如self.state_list[state_index:state_index + 1],拿到的是一个1×6的行向量,正好作为网络输入。这里要注意:等训练结束测试时,也要用同样的one-hot方式喂给网络,否则网络没见过这种输入格式,输出值会完全没有意义。我自己第一次跑通后,想直接喂一个整数索引进去测试,结果Q值全乱了,就是这个原因。

2.2 网络结构各层的shape与参数:为什么要选3个神经元的隐藏层

网络结构是最简单的三层全连接:输入层6个节点(状态维度),隐藏层3个节点,输出层6个节点(动作维度)。隐藏层之所以选3,并没有经过严格调参,属于这个规模问题下的经验值——6维输入映射到3维隐藏特征再映射回6维输出,参数总量小,训练快,不容易过拟合。

self.q_eval_input = tf.placeholder(shape=[None, self.state_num], dtype=tf.float32) self.action_input = tf.placeholder(shape=[None, self.action_num], dtype=tf.float32) self.q_target = tf.placeholder(shape=[None], dtype=tf.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

三个占位符的含义要分清:q_eval_input是当前状态,action_input是当前执行的动作(one-hot),q_target是目标Q值——它不在网络前向计算里,而是在训练时由外部算好喂进来的。tf.matmul(q_eval_input, w1)是输入层到隐藏层的线性变换,经过relu激活后进入第二层线性变换,最终得到6个动作各自的Q值估计。偏置b1设为0.1是为了防止relu把负输入全部杀死导致神经元死亡,这在网络层数浅时影响不大,但养成习惯没坏处。学习率0.001配梯度下降,参数更新步长小,loss曲线会平稳一些——如果换成AdamOptimizer,收敛会快不少,但初始阶段容易震荡,我在后面踩坑部分会细说。

2.3 loss构造的完整拆解:目标Q值与当前Q值的逼近逻辑,以及reward_action的数学含义

loss这部分是DQN和监督学习最像的地方。DQN把目标Q值看成监督学习里的“标签”,把当前网络输出的Q值看成“预测值”,loss就是两者差值的平方。这里有个细节很关键:网络的输出是6个动作的Q值,但当前只执行了一个动作,所以在计算loss之前,要先从6个Q值里把当前动作对应的那个分量抽出来。

self.reward_action = tf.reduce_sum(tf.multiply(self.q_eval, self.action_input), reduction_indices=1) self.loss = tf.reduce_mean(tf.square((self.q_target - self.reward_action)))

tf.multiply是元素级相乘,q_eval是6个Q值,action_input是one-hot的当前动作向量,两者相乘后,只有当前动作对应的分量被保留下来,其余全是0。reduce_sum按行求和,得到的就是“当前状态下执行当前动作的Q值”。举个例子:假设网络输出[0.81, 0.5, 0.24, 0.513, 0.9, 0.71],当前动作是动作3(索引从0开始),one-hot向量是[0,0,0,1,0,0],相乘后得到[0,0,0,0.513,0,0],求和得到0.513。如果目标Q值算出来是1.03,loss就是(1.03 - 0.513)² = 0.267。减法的方向很明确:目标Q值是用当前奖励加上折扣后的未来最优Q值算出来的,比网络当前的估计值更接近真实值,所以训练就是让reward_action一点点向q_target靠拢。

2.4 Q表失效的边界条件:招架不住的状态爆炸问题

Q-Learning在状态数少时完全够用,但一旦状态变成连续量(比如坐标、速度、角度),或者离散状态数量达到百万级,Q表就彻底失效了。DQN用神经网络替换Q表的本质,是把“存储所有状态动作对的查表问题”转换成了“学习一个从状态到动作价值向量的函数逼近问题”。网络只需要保存权重参数,就能泛化到没见过的状态——当然泛化质量取决于网络容量和训练数据质量。这份代码里状态数只有6,神经网络的优势体现得不明显,但你可以把state_num改成60甚至600,改动网络的输入维度和隐藏层大小后,仍然能训练,而Q表在600个状态下就得有600×动作数条目,存储和更新成本已经完全不是一个量级。这也是为什么把这份代码跑通之后,理解DQN的参数泛化能力要比记住代码本身更重要。

3. epsilon-greedy动作选择:探索与利用的平衡,直接决定能不能收敛

3.1 epsilon初始值0.1的代价:为什么说探索步数决定了训练效果的上限

epsilon-greedy策略是DQN里控制“探索”与“利用”的天平。epsilon表示随机探索的概率,初始值设为0.1,意味着有10%的概率随机选动作,90%的概率选当前网络认为的最优动作。这个比例在训练初期会影响收敛速度——10%的探索率在6个动作的规模下其实是够的,因为状态只有6个,随机试几次就能覆盖大部分转移关系。但如果状态空间变大,0.1的初始值会显得探索不足,agent很容易困在局部最优路径里。代码里还设置了FINAL_EPSILON = 0.0001,意思是训练后期几乎不再随机探索,完全靠网络决策。这个衰减过程不是按步数线性递推的,而是在select_action里每次调用时判断是否衰减。

if self.step_index > self.OBSERVE and self.epsilon > self.FINAL_EPSILON: self.epsilon -= (self.INITIAL_EPSILON - self.FINAL_EPSILON) / self.EXPLORE

衰减公式的含义:在EXPLORE = 3000000步内,把epsilon从0.1线性降到0.0001。每一步减少(0.1 - 0.0001) / 3000000 ≈ 3.33e-8,是个非常缓慢的过程。注意衰减必须发生在step_index > OBSERVE之后,也就是前1000步只采集经验不训练,epsilon保持不变。这个设计很像生产线上的预热环节——先让机器空转一段时间,再正式投料。

3.2 动作选择的双通道逻辑:随机探索与网络决策在代码里的分岔口

动作选择函数的核心逻辑就是一行判断:随机数小于epsilon就随机选,否则在网络输出里取最大值。代码里np.random.uniform()的取值范围是[0,1),所以epsilon=0.1时正好是10%的概率走随机分支。随机分支是让agent在训练初期多踩“错路”的关键——因为迷宫里的负奖励(-1)会把agent从不可走路径上推回去,而正奖励(100)会吸引agent靠近目标,必须让agent有机会走到那些“看似绕路”的位置,才能发现更优路径。网络分支则负责把当前状态喂进网络,取6个Q值中最大的那个动作作为输出。

def 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

有个细节值得停下来看一下:epsilon衰减放在动作选择函数里,意味着衰减频率和决策频率一致,决策一次就衰减一次,而不是每训练一步衰减一次。在step_index小于1000时,因为不衰减,探索率保持0.1,有利于前期的经验积累。我跑代码时观察过epsilon的实时值,在训练到几万步后它依然在0.09左右,说明这个衰减确实很慢。如果你希望探索更激进,可以把初始值调到0.3或0.5,但要注意训练前期的随机动作比例增高会导致路径更长、记忆库里的负奖励样本更多,网络需要更长时间才能学到稳定策略。

3.3 探索率衰减的时机检查:为什么衰减太快会让agent变成“路痴”

衰减速度本身是个需要反复试的参数。如果EXPLORE设得太小(比如3万步),epsilon会快速跌到0.0001,agent会在没充分探索的情况下过早进入纯利用模式。一旦随机探索停止,agent对某些状态的Q值估计可能一直是错的,但网络又没机会再通过随机动作去修正——这就相当于一个人只走过一条路就认定这是唯一的路,再也不看地图。我在调参时把EXPLORE改成过30万步,收敛速度明显变慢,但最终策略更稳;改回300万步后,前几万步基本看不到loss下降的趋势,这是正常的,因为在探索阶段网络就是在一堆互相矛盾的经验里来回震荡。有个判断收敛是否到位的土办法:打印每个起点出发后的最终路径。如果agent总是走同一条路且路径最短,说明网络已经收敛;如果路径常变或绕远路,说明探索还不充分或者epsilon衰减太早。

3.4 状态转移的确定性设计:R矩阵的负奖励与终止条件解读

R矩阵是这份代码里“迷宫”的真正定义。6×6矩阵,r[state][action]表示在state状态下执行action动作能拿到的即时奖励。从矩阵内容看,不是所有状态都能动作自由移动——奖励为-1的位置代表不可走(或者说不推荐走),奖励为0的位置代表可走但无收益,奖励为100的位置代表到达目标。值得注意的是,可走的位置也不是对称的:比如状态0执行动作4的奖励是0,但状态4执行动作0的奖励也是0,这说明状态转移是双向可逆的;而状态1执行动作5的奖励是100,动作5对应的就是到达目标。

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

step函数里有个隐含假设:执行动作a后,agent一定转移到状态a。也就是说,这个“迷宫”的拓扑结构是——从当前房间直接跳到目标房间,只要动作编号等于目标房间编号就成功。如果你把动作理解为“移动方向”,那这个设计只适用于房间之间一跳可达的图结构。换到更复杂的迷宫(比如需要多步转弯的网格迷宫),next_state的计算需要换成基于地图的转移逻辑,R矩阵也要重新定义。理解这个边界,扫一眼就能判断这份代码能不能直接用到自己的场景里。我在复现时最快踩到的坑就是这里,改地图时只改了R,忘了改step,结果agent学会的策略和地图完全对不上。

4. 经验回放与训练循环:样本怎么存、怎么抽、怎么更新Q值

4.1 为什么不 Q表还要经验回放:样本连续性会拖垮梯度下降

DQN最核心的机制之一就是经验回放(Experience Replay)。强化学习中agent连续探索环境得到的样本是一串时间序列,相邻样本间高度相关:状态s(t)和s(t+1)只差一个动作。如果每得到一个样本就立即用它更新网络参数,梯度方向会被这批相关性极强的样本带偏,导致loss剧烈震荡、网络难以收敛。经验回放的做法是:先把样本存进一个固定大小的存储区,训练时再从里面随机抽一小批。随机抽样打乱了时间相关性,相当于把一串互相依赖的事件变成了“独立同分布”的训练数据,梯度更新才稳定。

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()

这段代码里的popleft()保证存储区只保留最近5000条经验,超出后最老的记忆被丢弃。“为什么不用全部经验训练”也是个常见疑问——全部经验里包含大量早期探索期的随机动作记录,这些记录对收敛后期的策略修正几乎没有帮助,反而会拖慢训练速度。用deque的另一个好处是popleft是O(1)操作,比list的pop(0)快得多。样本容量5000对这个6状态问题来说是相当充裕的,跑一遍训练可能根本填不满这个池子——因为终止状态会频繁触发重置,实际生成的样本量有限。但在更大规模的问题上,memory_size通常要设到几万甚至几十万,否则经验多样性不够,网络学到的策略会很“偏科”。

4.2 经验池的存储格式与内存上限:deque、popleft和memory_size的配合

经验池里的每条记忆是一个五元组:当前状态、当前动作、即时奖励、下一个状态、是否结束。状态和动作在存储时就是one-hot向量,奖励是一个浮点数。设计上有个小门道——最后一个done字段在代码里存了但没用上,注释里还留着“游戏是否结束”的判断代码。这说明原作者的意图是区分终止状态和非终止状态的目标Q值计算方式,但最终实现里没走到那一步。实际计算Q值时只用了一个if current_reward < 0的判断,奖励为负时直接把目标Q值设为当前奖励,奖励非负时才加上后续的折扣Q值。这种简化在这个迷宫场景里是可接受的,但放到一般DQN场景(比如Atari游戏)就明显不够了——终止状态的Q值不应该加上未来的折扣奖励,因为游戏已经结束了。这就牵扯到一个问题:原代码在这里的处理是否值得沿用?我的建议是如果复用到其他场景,把done的语义完整用起来,按终止状态和非终止状态分别计算目标Q值。

4.3 小批量抽样的实现细节:边界条件下batch_size动态收缩的逻辑

experience_replay函数里有一个容易看漏但很重要的细节:当记忆池里的样本总数不足BATCH(20)时,实际抽样的数量不是硬凑20条,而是用当前记忆数做批大小。代码里是batch = self.BATCH if self.memory_counter > self.BATCH else self.memory_counter,接着用random.sample从deque中抽样。这么做的原因很直接:random.sample不能从不足20条的记忆池里抽出20条不重复的样本,不处理这个边界条件,程序会直接抛异常。这个边界条件出现的时机是训练前半段——OBSERVE是1000步,训练从第1001步才开始,经验池里此时已有1000多条记忆,20条样本是够的;但如果把OBSERVE调小,或者把BATCH调大,边界情况就会显现出来。

batch = self.BATCH if self.memory_counter > self.BATCH else self.memory_counter minibatch = random.sample(self.replay_memory_store, batch)

抽样完成后,代码把minibatch里的状态、动作、奖励、下一状态分别拼成矩阵。拼接逻辑是逐条用np.vstack堆叠,第一次遇到的样本作为初始矩阵,后续的样本垂直拼接。这个写法功能上没错,但有个性能隐患——在不断vstack的过程中,数组会反复复制,数据量大时效率堪忧。我的习惯是先把minibatch按字段拆成list,用np.array一次性转换,效率和可读性都更好。

4.4 目标Q值的计算方式:奖励为负直接赋值,奖励非负才做折扣累加

目标Q值计算是整个训练过程数据流的核心交汇点。计算逻辑分两段:先读出当前奖励,再用贝尔曼方程计算当前奖励 + gamma × max(Q(下一个状态的所有动作))。代码里对负奖励做了单独处理——如果当前奖励是-1,目标Q值直接等于-1;否则才使用折扣累加公式。这个处理的本意是把不可走路径(奖励为-1)直接标记为低价值,让网络学到“这些动作不要选”。但这么做有个副作用:目标Q值里-1是恒定值,没有考虑下一个状态的情况,相当于把“执行这个动作后进入的下一状态”的信息完全丢掉了。比如从状态0跳到状态3(奖励为-1),即使状态3本身距离目标很近,网络也只学到“动作3在状态0处是危险的”,学不到“跳到3其实离目标近了”。这在简单的6房间迷宫问题里影响不大,但如果在网格迷宫里用这个简化逻辑,agent会学到一些奇怪的绕路策略。

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)

这里的折扣因子gamma=0.9,意思是未来一步的Q值只按90%折算到当前。0.9在短路径问题里算合理——如果最长路径不超过3步,0.9³≈0.729,远期收益的打折还不算严重;但路径一旦变长,折扣因子太小会导致远期目标几乎不被重视,agent会变得“短视”。在迷宫规模更大、路径更长时,我一般会把gamma调到0.95到0.99之间,否则目标房间的100分奖励传不到起点位置。

4.5 训练主循环的流程拆解:观察期、学习期和终止重置的协作方式

训练主循环是DQN完整生命周期的主轴。初始状态随机从0到4号房间选一个(注意不是5,因为5是目标),epsilon设为0.1,然后开始无限循环。每轮循环里做四件事:选动作、执行动作拿奖励、存入记忆池、判断是否进入训练阶段。观察期和学习期的分界线就是OBSERVE=1000——前1000步只存经验不训练,第1001步开始抽样训练。

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

终止重置的细节藏在最后三行:done为真时,当前状态重新随机初始化;done为假时,agent进入动作指向的下一个状态。总步数上限10000,这个上限直接决定了训练时长和最终效果——跑完10000步,epsilon大约在0.1左右,远没衰减到FINAL_EPSILON,也就是说训练结束时机和探索衰减节奏是脱节的。原作者应该是在这个规模下发现10000步已经足够学到好策略,就把上限定在这了。如果你增大状态空间或迷宫复杂度,这个10000的硬上限需要同步调大,否则训练会提前终止在策略还没收敛的时候。我在复现时把步数上限提到50000,发现loss能降到更低、路径更稳定,但训练时间也线性拉长了。怎么平衡,就得看你的实际场景了。

5. DQN避坑记录:房间导航场景下五个必踩的坑

5.1 现象:loss不降反升,甚至直接发散成nan

loss在训练初期不但不降,反而一路飙升到几万甚至变成nan。原因通常是学习率过高导致梯度爆炸,或者网络初始权重过大。代码里w1用的是tf.random_normal,标准差为1的随机值。在6维输入映射到3维隐藏层时有18个权重,它们的乘积和可能很大,叠加relu激活后输出Q值可能到达几十甚至上百。这时配上0.001的学习率还好,但如果把学习率调到0.01以上,梯度一步跨出太远,loss直接起飞。解决方法是把权重初始化改成标准差更小的分布,比如tf.random_normal(..., stddev=0.1),或者用tf.truncated_normal——这样初始化的网络输出会集中在0附近,loss起点更低。我自己遇到过loss在几千步内从1左右涨到5000多的情况,排查后发现是权重初始化范围太大,把stddev改成0.1后训练恢复正常。

5.2 现象:epsilon衰减过快,agent永远在绕远路

训练完成后,从起点出发的路径长度明显大于最优路径,且路径不稳定,每次测试走的路线都不一样。原因是无探索能力后,网络对那些从未被随机探索过的状态动作对完全没有正确估计,纯利用模式只会重复输出早期学到的次优策略。解决方法是把EXPLORE从3000000改大到更大的值,或者在训练中期人为把epsilon重新拉高一段时间,给agent一次“再探索”的机会。我调试时把EXPLORE改成6000000后,路径长度明显缩短,但训练时间也翻倍了。一个折中的做法是分阶段训练:先用较高的epsilon跑一段时间,等loss曲线开始下降后再切换到较低的epsilon继续微调——这本质上就是手动控制探索节奏。

5.3 现象:reward为负的样本导致策略卡死在局部最优

agent学会了从起点走到一个“中间站”就停住,不再往目标走;或者总是在同一个位置来回跳。原因有二:一是负奖励的目标Q值直接赋值current_reward(即-1),没有考虑下一状态的潜在价值,导致网络高估了某些“看起来危险但实际通向目标”的路径的代价;二是经验池里的负奖励样本没有按权重筛选,大量随机探索期的“碰壁”样本淹没了少量到达目标的正样本。解决方法是先把reward的赋值逻辑改得更接近标准DQN:负奖励也计算current_reward + gamma * max(q_next),而不是截断赋值;再把经验池改成带优先级的采样(比如正奖励样本的抽样权重更高)。本地验证的代码改动只需要把if current_reward < 0这个分支删掉,统一走折扣累加逻辑,就能看到策略跳出了局部最优。

5.4 现象:训练到后期loss小幅回升,然后永久停滞

训练曲线先下降后回升,之后无论怎么调学习率都回不到最低点。原因是经验池容量5000,但训练步数超过观察期后一直在累积新经验,老经验被popleft挤掉——后期池子里全是探索期的“陈旧样本”,抽样训练时新旧样本比例失衡,网络在刚学好的策略和旧经验之间反复横跳。解决方法是扩大memory_size,比如改成50000,让更多经验被保留;或者把训练步数上限和memory_size联动调整,保证整个训练周期里经验池的更新是平滑的。我还试过一个做法:在loss进入平台期后,把学习率从0.001降到0.0001继续训练几百步,loss经常能再降一小截——这个操作在深度学习里叫“学习率退火”,在DQN里同样有效。

5.5 现象:测试时从不同起点出发,agent的步数差异很大

训练完成后,分别从0、1、2、3、4号房间测试,有的起点一步直达目标,有的起点绕很远的弯路。原因和5.2里说的探索不充分是同一个根源,但这里更可能是状态空间的访问频率不均衡——在随机探索阶段,某些状态被访问的次数远多于其他状态,网络拟合的天平倒向了高频状态。比如状态0和状态1被随机初始化命中的概率高,Q值估计准;状态4很少被选为初始状态,网络对它的估计偏差就大。解决方法是初始化时均匀覆盖所有非目标状态,或在训练时对低频访问的状态做额外采样,把它们的转移样本多存一些进经验池。跑测试时我习惯用脚本把5个起点的路径全部打出来,一眼就能看出哪个状态的策略还不可信。

6. 用测试循环验证训练结果:五条路径与Q值可信度检查

训练完成不等于策略可信,这一步是把训练结果“验明正身”的关键环节。pay()函数里有一段测试逻辑:分别从0到4号房间出发,循环执行网络决策,直到到达5号目标房间,同时打印每一步的移动轨迹。这个循环的机制是:把当前状态输入网络,取Q值最大的动作作为下一步,然后更新当前状态,再重复这个过程。因为网络输出是6个动作的Q值,所以next_state = np.argmax(out_result[0])取的就是最大值对应的动作索引。

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")

测试结果我是这么解读的:起点0到目标5的最优路径是0→4→5或0→3→5,2步;起点1到目标5的最优路径是1→5,1步;起点3到目标5的最优路径是3→5,1步。如果测试输出中某个起点的步数明显多于最优值,说明这个状态附近的Q值估计还不够准,需要回到训练阶段调整探索策略。

Q值可信度检查的另一个方法,是把某一状态下6个动作的Q值全部打印出来,观察数值排序是否合理。比如在起点0时,走向3和走向4的动作Q值应该明显高于走向1或走向2的Q值;走向不可走位置的Q值应该非常低(接近-1)。如果打印出来的Q值排序和R矩阵的奖励结构对不上,说明网络还没完全收敛,需要继续训练。这个检查方法在多步路径迷宫里更有价值——它能直观展示网络内部对不同动作的“偏好强度”,而不仅仅看最终路径结果。

我习惯把测试输出的路径长度和loss曲线的最终值一起记录,每次调参后都跑一遍对比,积累几组数据之后,就能看出哪些参数对路径结果有决定性的影响。从供给侧看,memory_size、EXPLORE和learning_rate这三个参数出问题的概率最高——它们分别控制经验广度、探索时长和更新步幅,任何一个失衡都会反映在测试路径上。从那以后我每次跑完训练,都强制走一遍“五路径+Q值打印”的验证流程,确认无异常才认为这次训练真正合格。希望这份拆解能帮你少踩几个我已经替你踩过的坑。

本文还有配套的精品资源,点击获取

返回列表