> 文章列表 > Python-DQN代码阅读(7)

Python-DQN代码阅读(7)

Python-DQN代码阅读(7)

目录

1.代码

1.1设置ε值

代码总括

代码分解

1.2 设置时间步长总数

1.3主循环贯穿整个回合

1.4跟踪时间步长

1.5更新目标网络


1.代码

1.1设置ε值

代码总括

# epsilon start
if (train_or_test == 'train'):# 计算训练初期和训练后期的 epsilon 值的差值delta_epsilon1 = (epsilon_start - epsilon_end[0]) / float(epsilon_decay_steps[0])delta_epsilon2 = (epsilon_end[0] - epsilon_end[1]) / float(epsilon_decay_steps[1])if (train_from_scratch == True):# 若从头开始训练,则 epsilon 初始值为 epsilon_startepsilon = epsilon_startelse:if (start_iter <= epsilon_decay_steps[0]):# 在 epsilon_decay_steps[0] 步之前,epsilon 按照 delta_epsilon1 进行线性衰减epsilon = max(epsilon_start - float(start_iter) * delta_epsilon1, epsilon_end[0])elif (start_iter > epsilon_decay_steps[0] and start_iter < epsilon_decay_steps[0] + epsilon_decay_steps[1]):# 在 epsilon_decay_steps[0] 和 epsilon_decay_steps[0] + epsilon_decay_steps[1] 之间,epsilon 按照 delta_epsilon2 进行线性衰减epsilon = max(epsilon_end[0] - float(start_iter) * delta_epsilon2, epsilon_end[1])else:# 在 epsilon_decay_steps[0] + epsilon_decay_steps[1] 步之后,epsilon 固定为 epsilon_end[1]epsilon = epsilon_end[1]
elif (train_or_test == 'test'):# 在测试阶段,epsilon 固定为 epsilon_end[1]epsilon = epsilon_end[1]

这段代码主要用于根据训练阶段和训练步数来计算当前的 epsilon 值,用于 epsilon-greedy 策略中的探索和利用权衡。具体逻辑如下:

  • 如果处于训练阶段 (train_or_test == 'train'),则根据 train_from_scratch 参数的值来确定 epsilon 初始值。若 train_from_scratch == True,则将 epsilon 初始值设为 epsilon_start,否则根据当前训练步数 start_iterepsilon_decay_steps[0] 计算 epsilon 初始值,使其在训练初期按照 delta_epsilon1 进行线性衰减,并且不低于 epsilon_end[0]。在训练过程中,当 start_iter 大于 epsilon_decay_steps[0] 且小于 epsilon_decay_steps[0] + epsilon_decay_steps[1] 时,epsilon 按照 delta_epsilon2 进行线性衰减,并且不低于 epsilon_end[1]。当 start_iter 大于 epsilon_decay_steps[0] + epsilon_decay_steps[1] 时,epsilon 固定为 epsilon_end[1]

  • 如果处于测试阶段 (train_or_test == 'test'),则 epsilon 固定为 epsilon_end[1],即不再进行探索,始终选择当前 Q 值最大的动作。

代码分解

(1)

delta_epsilon1 = (epsilon_start - epsilon_end[0]) / float(epsilon_decay_steps[0])这段代码计算了训练初期和训练过程中 epsilon 的衰减步长,具体解释如下:epsilon_start: epsilon 的初始值,表示在训练开始时的探索概率。
epsilon_end[0]: epsilon 的最终值,表示在训练初期的最低探索概率。
epsilon_decay_steps[0]: epsilon 衰减的步数,表示在训练初期的衰减步数。
通过以上三个参数的计算,可以得到训练初期 epsilon 按照步数进行线性衰减的步长 delta_epsilon1,具体计算方式为 (epsilon_start - epsilon_end[0]) / float(epsilon_decay_steps[0]),即 epsilon 初始值与最低探索概率之差除以衰减步数,得到每步 epsilon 的衰减值,用于控制 epsilon 的变化速度。在训练初期,epsilon 将按照这个步长进行线性衰减,直到不低于最低探索概率 epsilon_end[0]。各个参数的值在代码中是固定的,但是可以根据实际需求进行调整。epsilon_start: epsilon 的初始值,表示在训练开始时的探索概率。可以根据问题的难度和需要进行设置,一般情况下设置为较大的值,例如1.0,以便在训练初期进行较多的探索。
epsilon_end: epsilon 的最终值,表示在训练过程中的最低探索概率。通常设置为一个较小的值,例如0.1或0.01,以便在训练后期保持一定的探索能力。
epsilon_decay_steps: epsilon 衰减的步数,表示在训练初期和训练过程中的衰减步数。可以根据训练数据量和训练时长进行设置,一般情况下设置为一个较大的值,例如1e6或1e7,以确保 epsilon 在足够长的训练过程中能够充分衰减。
delta_epsilon1: 训练初期 epsilon 按照步数进行线性衰减的步长,计算方式为 (epsilon_start - epsilon_end[0]) / float(epsilon_decay_steps[0]),表示每步 epsilon 的衰减值,用于控制 epsilon 的变化速度。
这些参数的具体值需要根据具体问题和训练需求进行调整,以达到最优的训练效果。在实际使用中,可以通过多次试验和调整来找到合适的参数值。

(2)

delta_epsilon2 = (epsilon_end[0] - epsilon_end[1]) / float(epsilon_decay_steps[1])delta_epsilon2 和 delta_epsilon1 的计算方式相似,都是根据不同的 epsilon 值和对应的衰减步数计算得出的。delta_epsilon2 表示训练过程中 epsilon 在第二阶段衰减的步长,计算方式为 (epsilon_end[0] - epsilon_end[1]) / float(epsilon_decay_steps[1]),表示每步 epsilon 的衰减值,用于控制 epsilon 的变化速度。
这两个参数的差别在于计算时使用的 epsilon 和衰减步数不同,delta_epsilon1 是在训练初期衰减阶段使用的步长,而 delta_epsilon2 是在训练过程中第二阶段衰减时使用的步长。根据你之前提供的参数值:epsilon_start = 1.0
epsilon_end = [0.1, 0.01]
epsilon_decay_steps = [1e6, 1e6]
可以计算得出:delta_epsilon1 = (1.0 - 0.1) / 1e6 = 9e-7
delta_epsilon2 = (0.1 - 0.01) / 1e6 = 9e-8
这两个值即为在代码中用于控制 epsilon 衰减速度的步长,具体的值可能会因为浮点数运算的精度而略有不同。

(3)

# epsilon start
if (train_or_test == 'train'):# 计算训练初期和训练后期的 epsilon 值的差值delta_epsilon1 = (epsilon_start - epsilon_end[0]) / float(epsilon_decay_steps[0])delta_epsilon2 = (epsilon_end[0] - epsilon_end[1]) / float(epsilon_decay_steps[1])if (train_from_scratch == True):# 若从头开始训练,则 epsilon 初始值为 epsilon_startepsilon = epsilon_startelse:if (start_iter <= epsilon_decay_steps[0]):# 在 epsilon_decay_steps[0] 步之前,epsilon 按照 delta_epsilon1 进行线性衰减epsilon = max(epsilon_start - float(start_iter) * delta_epsilon1, epsilon_end[0])elif (start_iter > epsilon_decay_steps[0] and start_iter < epsilon_decay_steps[0] + epsilon_decay_steps[1]):# 在 epsilon_decay_steps[0] 和 epsilon_decay_steps[0] + epsilon_decay_steps[1] 之间,epsilon 按照 delta_epsilon2 进行线性衰减epsilon = max(epsilon_end[0] - float(start_iter) * delta_epsilon2, epsilon_end[1])else:# 在 epsilon_decay_steps[0] + epsilon_decay_steps[1] 步之后,epsilon 固定为 epsilon_end[1]epsilon = epsilon_end[1]
elif (train_or_test == 'test'):# 在测试阶段,epsilon 固定为 epsilon_end[1]epsilon = epsilon_end[1]

对于给定的参数值和代码逻辑,因为本程序设置的train_or_test == 'train'为真,所以执行if语句计算

delta_epsilon1和delta_epsilon2的值,又因为本程序设置的(train_from_scratch == True)为真,所以执行epsilon = epsilon_start,后续程序不再执行,所以本程序中epsilon恒为epsilon_start=1.0。

这意味着在整个训练过程中,epsilon 的值将保持不变,始终为 epsilon_start,不会发生衰减。这可能不符合一些需要动态衰减 epsilon 值的训练策略,因此需要根据实际需求和训练策略的目标来确定是否需要对程序逻辑进行调整。例如,如果希望在训练的不同阶段使用不同的 epsilon 值来控制探索和利用的权衡,那么可以考虑修改程序逻辑以实现所需的行为。

1.2 设置时间步长总数

 # total number of time stepstotal_t = start_iter

这段代码是将变量 start_iter 的值赋给变量 total_t,用于表示总的时间步数。在训练过程中,每执行一次动作都会增加一个时间步,因此可以通过 total_t 来记录训练的总时间步数。这个值在训练过程中会不断增加,用于记录整个训练过程的进展。

1.3主循环贯穿整个回合

# 进行 num_episodes 轮训练
for ep in range(start_episode, num_episodes):# 保存模型的检查点saver.save(tf.get_default_session(), checkpoint_path)# 重置游戏环境state = env.reset()state = state_processor.process(sess, state)  # 对状态进行预处理state = np.stack([state] * 4, axis=2)  # 堆叠成包含连续 4 个状态的张量# 初始化统计变量loss = 0.0  # 当前回合的损失值time_steps = 0  # 时间步数episode_rewards = 0.0  # 累计回报# 初始化关于游戏生命的变量ale_lives = 5  # 当前游戏生命状态info_ale_lives = ale_lives  # 游戏生命信息steps_in_this_life = 1000000  # 当前生命内已经进行的时间步数num_no_ops_this_life = 0  # 不执行操作 (no-op) 的时间步数

这部分代码是训练的主要循环。每一轮循环代表一个新的游戏回合 (episode)。具体的逻辑如下:

  1. 保存模型的检查点 (ckpt):在每一轮循环开始时,会调用 saver.save() 函数保存当前的 TensorFlow 会话 (session) 到指定的检查点文件 (checkpoint file)。这样在训练过程中出现意外中断时,可以从上一个检查点恢复训练。

  2. 环境重置:每个回合开始时,会调用 env.reset() 函数重置游戏环境,并获取初始状态。然后通过 state_processor(是前面定义的变量作用域) 对状态进行预处理,例如进行图像缩放、灰度化等操作,并将其堆叠成包含连续 4 个状态的张量。

  3. 初始化统计变量:losstime_stepsepisode_rewards 分别用于记录当前回合的损失值、时间步数和累计回报 (reward)。

  4. 初始化关于游戏生命 (lives) 的变量:ale_livesinfo_ale_livessteps_in_this_lifenum_no_ops_this_life 分别用于记录当前游戏生命的状态、游戏生命信息、当前生命内已经进行的时间步数和不执行操作 (no-op) 的时间步数。

接下来的代码中,会在每一轮循环中执行具体的训练逻辑,包括选择动作、执行动作、更新网络参数、记录统计信息等。在循环结束后,会继续进行下一轮回合的训练,直到达到指定的训练轮数 num_episodes

1.4跟踪时间步长

while True:  # 进入无限循环,用于持续训练或测试if (train_or_test == 'train'):  # 判断当前是否处于训练模式# 根据总时间步数更新 epsilon 值,实现 epsilon-greedy 策略if (total_t <= epsilon_decay_steps[0]):  # 如果总时间步数小于等于第一个 epsilon 衰减阶段epsilon = max(epsilon - delta_epsilon1, epsilon_end[0])  # 使用 delta_epsilon1 更新 epsilon,保证不小于 epsilon_end[0]elif (total_t >= epsilon_decay_steps[0] and total_t <= epsilon_decay_steps[0] + epsilon_decay_steps[1]):# 如果总时间步数介于第一个 epsilon 衰减阶段和第一个阶段与第二个阶段之和之间epsilon = epsilon_end[0] - (epsilon_end[0] - epsilon_end[1]) / float(epsilon_decay_steps[1]) * float(total_t - epsilon_decay_steps[0])# 根据线性插值公式更新 epsilon,使其在 epsilon_end[0] 到 epsilon_end[1] 之间线性变化epsilon = max(epsilon, epsilon_end[1])  # 确保 epsilon 不小于 epsilon_end[1]else:epsilon = epsilon_end[1]  # 如果总时间步数大于第一个阶段与第二个阶段之和,将 epsilon 设置为 epsilon_end[1],即固定值

在给定的代码中,total_t 是通过变量 start_iter 进行更新的,但是在代码片段中并没有展示如何更新 start_iter 的值。如果在实际的代码实现中,start_iter 的值一直保持为 0,那么确实会导致 total_t <= epsilon_decay_steps[0] 恒成立。这意味着在训练的初始阶段,epsilon 的更新只会使用 delta_epsilon1,而不会进行线性插值或使用固定值。

如果希望 total_t <= epsilon_decay_steps[0] 不恒成立,可以在代码中添加相应的逻辑来更新 start_iter 的值,使其在训练过程中逐步增加,从而使 epsilon 在训练的初始阶段也能够经过线性插值或使用固定值进行更新。具体的更新逻辑会根据实际的训练需求和策略进行设计。

1.5更新目标网络

(1)

            if total_t % update_target_net_every == 0:# 复制 Q 网络的参数到目标网络copy_model_parameters(sess, q_net, target_net)print("\\n copied params from Q net to target net ")

(2) 

copy_model_parameters(sess, q_net, target_net)copy_model_parameters(sess, q_net, target_net) 是一个用于复制模型参数的函数。在这里,它用于将 Q 网络的参数复制到目标网络。函数的参数解释如下:sess: TensorFlow 会话(Session)对象,用于执行计算图中的操作。
q_net: Q 网络的源模型,包含待复制的参数。
target_net: 目标网络的目标模型,用于接收复制后的参数。
通过调用这个函数,可以实现将 Q 网络的参数复制到目标网络,从而实现目标网络的更新。这是 DDPG 算法中的一种常见策略,用于稳定训练过程。

(3) 

time_to_fire = False  # 是否到达发射子弹的时刻标志if (time_steps == 0 or ale_lives != info_ale_lives):# 如果是新的游戏或者新的生命steps_in_this_life = 0  # 重置当前生命中的步数num_no_ops_this_life = np.random.randint(low=0, high=7)  # 生成一个随机整数,表示当前生命中的无操作步数action_probs = [0.0, 1.0, 0.0, 0.0]  # 将发射子弹的动作设为最大概率time_to_fire = True  # 标记为到达发射子弹的时刻if (ale_lives != info_ale_lives):ale_lives = info_ale_lives  # 更新当前生命剩余次数
else:action_probs = policy(sess, state, epsilon)  # 使用策略函数生成动作概率,考虑探索率 epsilon

以上代码段中的注释解释了以下几个关键点:

time_to_fire = False:用于标记是否到达发射子弹的时刻。

if (time_steps == 0 or ale_lives != info_ale_lives)::判断是否为新的游戏或者新的生命。当 time_steps(总的时间步数)为0或者当前生命的剩余次数 ale_lives 与游戏状态中的生命剩余次数 info_ale_lives 不一致时,表示新的游戏或者新的生命,需要重置相应的状态。

steps_in_this_life = 0:记录当前生命中的步数。用于控制无操作步数。

num_no_ops_this_life = np.random.randint(low=0, high=7):生成一个随机整数,用于表示当前生命中的无操作步数。用于增加环境中的随机性。

action_probs = [0.0, 1.0, 0.0, 0.0]:设置动作概率,将发射子弹的动作设为最大概率,用于在新的生命中立即发射子弹。action_probs 为动作概率列表,根据游戏状态和探索率 epsilon 生成不同动作的概率值。

time_to_fire = True:将发射子弹的标志设为 True。

if (ale_lives != info_ale_lives)::判断是否为新的生命。

ale_lives = info_ale_lives:更新当前生命剩余次数。

else::如果不是新的游戏或者新的生命,执行以下操作。

action_probs = policy(sess, state, epsilon):使用策略函数 policy 根据当前状态和探索率 epsilon 生成动作概率。这里的 action_probs 是一个包含四个动作概率值的列表,分别对应于四个可能的动作:不动、发射子弹、向左移动、向右移动。

num_no_ops_this_life = np.random.randint(low=0, high=7)这段代码使用 np.random.randint 函数生成一个随机整数,范围从 low 到 high(不包括 high)之间。
具体来说,它生成一个介于0到6之间的随机整数,用于表示当前生命中的无操作步数。
这样可以在每个生命中引入一定的随机性,增加训练的多样性和探索性。

在强化学习中,生命中的无操作步数是指在游戏中玩家没有执行任何动作的步数。在某些游戏中,当玩家的游戏角色处于某些状态下(例如,角色被固定、处于保护状态等),玩家可能无法执行有效的游戏动作,因此需要等待一定的时间。在这段代码中,num_no_ops_this_life 表示在当前生命中的无操作步数,通过生成一个随机整数来引入一定的随机性,增加训练的多样性。

action_probs = policy(sess, state, epsilon)action_probs 是通过调用 policy 函数得到的,其作用是根据当前的状态 state 和当前的 epsilon 值 epsilon 来计算一个动作概率分布。
在强化学习中,策略(policy)用于确定在给定状态下应该采取哪个动作。
这段代码中,根据当前的状态 state 和 epsilon 值 epsilon,通过调用 policy 函数来计算动作概率分布,并将结果存储在 action_probs 变量中,用于后续的动作选择过程。
具体的策略选择方法和计算方式可能依赖于代码中定义的 policy 函数的实现。
steps_in_this_life += 1if (steps_in_this_life < num_no_ops_this_life and not time_to_fire):# no-opaction_probs = [1.0, 0.0, 0.0, 0.0]  # no-opsteps_in_this_life 是记录当前生命中已经经过的步数的变量,num_no_ops_this_life 是表示当前生命中要进行的无操作步数的变量。
在这段代码中,steps_in_this_life 的值会在每个时间步递增一次,即 steps_in_this_life += 1。然后,通过判断 steps_in_this_life 是否小于 num_no_ops_this_life,并且 time_to_fire 不为真(即还没有到进行射击的时候),来判断是否进行无操作(no-op)。
如果满足这两个条件,将 action_probs 设置为 [1.0, 0.0, 0.0, 0.0],表示选择无操作(no-op)的动作概率为1,而其他动作的概率为0。
这样,在这段代码中,num_no_ops_this_life 可以用来控制每个生命中进行的无操作(no-op)的次数。