学习强化学习,码代码的能力必须要出众,要快速入门强化学习 搞清楚其中真正的原理,读源码是一个最简单的最直接的方式。最近创建了一系列该类型文章,希望对大家有多帮助。
传送门
另外,我会将所有的文章及所做的一些简单项目,放在我的个人网页上。
水平有限,可能有理解不到位的地方,希望大家主动沟通交流。
邮箱:[email protected]
Thanks for reading, and enjoy yourself。
DDPG 深度确定性策略梯度下降算法。论文链接。采用了Actor-Critic 架构,可以有效的处理连续域的问题。
同时,其actor的确定性动作输出,提高了采样的有效性。
强化学习算法的主要目标是去学习一个策略,来指导agent与环境交互从而得到更好的收益。策略 π θ ( a ∣ s ) \pi_{\theta}(a|s) πθ(a∣s)是以 θ \theta θ为参数的概率分布,代表不同状态下所采用的动作的概率分布。在学习的过程中不断的改变该函数的参数 θ \theta θ,从而改变应对环境的策略,以得到更好的奖励。当策略固定时,其所遍历的状态动作概率可以表示为
p θ ( s 1 , a 1 , … , s T , a T ) ⎵ p θ ( τ ) = p ( s 1 ) ∏ t = 1 T π θ ( a t ∣ s t ) p ( s t + 1 ∣ s t , a t ) \underbrace {{p_\theta }\left( {{{\bf{s}}_1},{{\bf{a}}_1}, \ldots ,{{\bf{s}}_T},{{\bf{a}}_T}} \right)}_{{p_\theta }(\tau )} = p\left( {{{\bf{s}}_1}} \right)\prod\limits_{t = 1}^T {{\pi _\theta }} \left( {{{\bf{a}}_t}|{{\bf{s}}_t}} \right)p\left( {{{\bf{s}}_{t + 1}}|{{\bf{s}}_t},{{\bf{a}}_t}} \right) pθ(τ) pθ(s1,a1,…,sT,aT)=p(s1)t=1∏Tπθ(at∣st)p(st+1∣st,at)
对单个状态而言,其到达概率为:
ρ π ( s ′ ) = ∫ S ∑ t = 1 ∞ γ t − 1 p 1 ( s ) p ( s → s ′ , t , π ) d s \rho^{\pi}(s')=\int_{\mathcal{S}} \sum_{t=1}^{\infty} \gamma^{t-1} p_{1}(s) p\left(s \rightarrow s^{\prime}, t, \pi\right) \mathrm{d} s ρπ(s′)=∫St=1∑∞γt−1p1(s)p(s→s′,t,π)ds
那么在策略 π θ ( a ∣ s ) \pi_{\theta}(a|s) πθ(a∣s)下得到的期望收益可以表示为:
J ( π θ ) = ∫ S ρ π ( s ) ∫ A π θ ( s , a ) r ( s , a ) d a d s = E s ∼ ρ π , a ∼ π θ [ r ( s , a ) ] \begin{aligned} J\left(\pi_{\theta}\right) &=\int_{\mathcal{S}} \rho^{\pi}(s) \int_{\mathcal{A}} \pi_{\theta}(s, a) r(s, a) \mathrm{d} a \mathrm{d} s \\ &=\mathbb{E}_{s \sim \rho^{\pi}, a \sim \pi_{\theta}}[r(s, a)] \end{aligned} J(πθ)=∫Sρπ(s)∫Aπθ(s,a)r(s,a)dads=Es∼ρπ,a∼πθ[r(s,a)]
实际上 DDPG是DPG算法利用深度神经网络去逼进 策略 π θ ( a ∣ s ) \pi_{\theta}(a|s) πθ(a∣s)和期望 Q Q Q。 Q Q Q函数的更新 需要与DQN类似:
Q ∗ ( s , a ) = Q ( s , a ) + α ( r + γ max a ′ Q ( s ′ , a ′ ) − Q ( s , a ) ) Q^{*}(s, a)=Q(s, a)+\alpha\left(r+\gamma \max _{a^{\prime}} Q\left(s^{\prime}, a^{\prime}\right)-Q(s, a)\right) Q∗(s,a)=Q(s,a)+α(r+γa′maxQ(s′,a′)−Q(s,a))
所以 Q Q Q函数更新的loss可以表示为:
L ( θ ) = E [ ( r + γ max a ′ Q ( s ′ , a ′ ; θ ) − Q ( s , a ; θ ) ) 2 ] L(\theta)=E\left[\left(r+\gamma \max _{a^{\prime}} Q\left(s^{\prime}, a^{\prime} ; \theta\right)-Q(s, a ; \theta)\right)^{2}\right] L(θ)=E[(r+γa′maxQ(s′,a′;θ)−Q(s,a;θ))2]
这样我们需要2组神经网络,其中一组 用来生成现在的状态S和动作A 另一组 用于生成 未来 Q Q Q函数估值 Q ( s ′ , a ′ ; θ ) Q\left(s^{\prime}, a^{\prime} ; \theta\right) Q(s′,a′;θ) 一组用于更新当前 Q ( s , a ; θ ) Q(s, a ; \theta) Q(s,a;θ)网络。
根据OPENAI-Baeslines-详解(一)中,需要在learning中传入的DDPG的参数。
在DDPG进行学习的时候,分为多个epoch。
每个epoch 中 有进行 多个cycles ,每个cycles ,进行rollout次采样 、train_steps次训练和eval_steps次评估。
total_step = epochs * epoch_cycles* rollout
总步数= 总回合数 * 每个回合的循环运行次数 * 每个回合与环境交互的次数。
一个回合 不等于 一个episode 。
由于可以使用多个环境并行采样,所以 在一个cycle中 多个环境同时采样,每个环境都采样rollout次,无论这个环境是否done。
有可能这个环境已经提前done了 ,他也要继续采样,到rollout次结束。
训练步数和 评估步数是不算在其中的
network, env,
seed=None,
# 总步数 和总回合数 只能存在一个
# 若两个都不存在,那么epoch为500
total_timesteps=None, # 总步数
nb_epochs=None, # 总回合数
nb_epoch_cycles=20, # 每个回合的循环运行次数
nb_rollout_steps=100, # 每个回合与环境交互的次数
nb_train_steps=50, # 每个回合训练次数
nb_eval_steps=100, # 每个回合的评估次数
eval_env=None, # 在每个回合训练完成之后,开始测试环境的步数。
render=False, # 是否显示交互
render_eval=False,
noise_type='adaptive-param_0.2',
gamma=0.99,
critic_l2_reg=1e-2, # critic正则化约束
actor_lr=1e-4, # actor 学习率
critic_lr=1e-3, # critic 学习率
tau=0.01, # 软切换 的参数
**network_kwarg # 网络参数
reward_scale=1.0, # 奖励的剪裁
normalize_returns=False, #
normalize_observations=True, # 是否对噪声归一化
popart=False, # 自适应Q值剪裁
clip_norm=None, # 将输出的模裁剪到一定范围内
#如果输出的为t 那么操作为t * clip_norm / l2norm(t)
batch_size=64, # per MPI worker
param_noise_adaption_interval=50,3
再运行程序的最后会得到progress.csv 输出的结果分为三个方面:
样本的输出
本次epoch 的样本的输出
总共的
‘train/loss_actor’ # 本epoch 的actor的loss
‘train/loss_critic’ # 本epoch 的critic的loss
‘train/param_noise_distance’ # 本epoch 的actor的loss
‘total/duration’ # 总共持续的时间
‘total/steps_per_second’ # 每一步所花的时间
‘total/episodes’ # 总共完成的回合数
‘rollout/episodes’
‘rollout/actions_std’ # 动作平均
DDPG文件夹下包含以下5个文件:
建立网络 63~65行
memory = Memory(limit=int(1e6), action_shape=env.action_space.shape, observation_shape=env.observation_space.shape) # 创建记忆库
critic = Critic(network=network, **network_kwargs) # critic 网络
actor = Actor(nb_actions, network=network, **network_kwargs) # actor 网络
67~84行 创建noise模型 ,noise 主要作用是用于增大探索
89行 调用ddpg—learner 创建agent 并开始循环与环境交互。
这里可以同时对多个环境 进行探索。
每个循环 有 epoch 、cycle、
每个epoch 需要有多个cycle 每个 cycle 中 rollout_step 次与环境交互 train_step 次进行训练。
for epoch in range(nb_epochs):
for cycle in range(nb_epoch_cycles):
与环境交换阶段
# reset环境
if nenvs > 1:
agent.reset()
for t_rollout in range(nb_rollout_steps):
# 输出动作
action, q, _, _ = agent.step(obs, apply_noise=True, compute_Q=True)
# 动作都是归一化在-1到1之间
new_obs, r, done, info = env.step(max_action * action)
t += 1
if rank == 0 and render:
env.render()
episode_reward += r
episode_step += 1
# 存进memory
epoch_actions.append(action)
epoch_qs.append(q)
agent.store_transition(obs, action, r, new_obs, done)
# 新旧 状态更新
obs = new_obs
for d in range(len(done)): # 对每一个agent进行reset
if done[d]:
if nenvs == 1:
agent.reset()
训练阶段
for t_train in range(nb_train_steps):
# 噪声更新
if memory.nb_entries >= batch_size and t_train % param_noise_adaption_interval == 0:
distance = agent.adapt_param_noise()
epoch_adaptive_distances.append(distance)
# agent 训练
cl, al = agent.train()
##### ddpg—learner
该类下,主要包含了各种DDPG中所需要包含的操作,包括利用状态值的actor 和critic 的 前向传播
、保存数据到经验池、从经验池提取数据 进行 后向传播训练、噪声的增加以及初始化等工作。
创建target—network 120-126行
target_actor = copy(actor)
target_actor.name = 'target_actor'
self.target_actor = target_actor
target_critic = copy(critic)
target_critic.name = 'target_critic'
self.target_critic = target_critic
创建 target—net的更新
# 先创建单个网络函数 36行定义的函数
def get_target_updates(vars, target_vars, tau)
# 返回的是两组操作op,一组是硬更新 一组是软更新。
# 每组更新都是一个对每一个参数 进行 更新。
return tf.group(*init_updates), tf.group(*soft_updates)
# 2个网络的更新函数 149行 class 中定义的函数
def setup_target_network_updates(self)
可以得到self.target_init_updates self.target_soft_updates
128行 首先需要创建loss 以及 创建actor 与 critic之间的链接
# actor
self.actor_tf = actor(normalized_obs0)
# critic 输入中的动作位置 为placeholder
self.normalized_critic_tf = critic(normalized_obs0, self.actions)
self.critic_tf = denormalize(tf.clip_by_value(self.normalized_critic_tf,self.return_range[0], self.return_range[1]), self.ret_rms)
# critic 输入中的动作位置 为actor的输出
self.normalized_critic_with_actor_tf = critic(normalized_obs0, self.actor_tf, reuse=True)
self.critic_with_actor_tf =denormalize(tf.clip_by_value(self.normalized_critic_with_actor_tf, self.return_range[0], self.return_range[1]), self.ret_rms)
# target Q值计算
Q_obs1 = denormalize(target_critic(normalized_obs1, target_actor(normalized_obs1)), self.ret_rms)
self.target_Q = self.rewards + (1. - self.terminals1) * gamma * Q_obs1
259行 step 函数 是在每次交互过程中 ,根据当前状态 前向传输。根据当前状态 求取动作和Q值
def step(self, obs, apply_noise=True, compute_Q=True):
feed_dict = {self.obs0: U.adjust_shape(self.obs0, [obs])}# 送入数据
# 利用网络计算动作和Q值
action, q = self.sess.run([actor_tf, self.critic_with_actor_tf], feed_dict=feed_dict)
# 之后是为了增加噪声
noise = self.action_noise()
action += noise
action = np.clip(action, self.action_range[0], self.action_range[1])
172 行 创建actor 网络训练
∇ θ J ( π θ ) \nabla_{\theta} J\left(\pi_{\theta}\right) ∇θJ(πθ)是 Q Q Q对actor的参数求导数。
采用的是 利用action 的输出作为输入的critic的输出
因为经验回放 更新actor的时候是对当前actor的参数求导,所以必须对当前actor输入state 然后求得action 再将此时的action和state 送入critic ,并最后得到Q值 来更新 actor 参数。
def setup_actor_optimizer(self):
self.actor_loss = -tf.reduce_mean(self.critic_with_actor_tf)# Q值
self.actor_grads = U.flatgrad(self.actor_loss, self.actor.trainable_vars, clip_norm=self.clip_norm) # 计算梯度
self.actor_optimizer = MpiAdam(var_list=self.actor.trainable_vars,beta1=0.9, beta2=0.999, epsilon=1e-08)
183 行 创建critic 网络训练
更新critic的时候,从经验库中取得的数据,其reward 是当时state-action所得到的,而此时critic网络参数经由多次训练之后,发生了非常大的变化, 所以必须用当前的网络在计算一遍Q值然后,利用当前target 网络Q值和 当前 main 网络Q值 加上当时的reward 重新计算。
def setup_critic_optimizer(self):
normalized_critic_target_tf = tf.clip_by_value(normalize(self.critic_target, self.ret_rms), self.return_range[0], self.return_range[1])
self.critic_loss = tf.reduce_mean(tf.square(self.normalized_critic_tf - normalized_critic_target_tf))
# 187-196 在这里会对critic的loss 增加 l2 约束。
self.critic_grads = U.flatgrad(self.critic_loss, self.critic.trainable_vars, clip_norm=self.clip_norm) # 计算梯度
self.critic_optimizer = MpiAdam(var_list=self.critic.trainable_vars,
beta1=0.9, beta2=0.999, epsilon=1e-08) # 反向训练
289 行 train
def train(self):
# 经验池随机采样
batch = self.memory.sample(batch_size=self.batch_size)
ops = [self.actor_grads, self.actor_loss, self.critic_grads, self.critic_loss]
# 根据采样数据重新计算Q值等。
actor_grads, actor_loss, critic_grads, critic_loss = self.sess.run(ops, feed_dict={
self.obs0: batch['obs0'],
self.actions: batch['actions'],
self.critic_target: target_Q,
})
# 训练328 行
self.actor_optimizer.update(actor_grads, stepsize=self.actor_lr)
self.critic_optimizer.update(critic_grads, stepsize=self.critic_lr)
噪声主要是为了增加action的探索作用。噪声主要有两种 一种是 静态参数的 一种是 动态参数(未使用)
噪声的生成主要是通过首先对actor 进行 copy (155行函数)
def setup_param_noise(self, normalized_obs0):
param_noise_actor = copy(self.actor)
self.perturbed_actor_tf = param_noise_actor(normalized_obs0)
然后对copy后的actor的输出增加噪声
#50 行
def get_perturbed_actor_updates(actor, perturbed_actor, param_noise_stddev):
# 增加均值为零 方差为param_noise_stddev的 高斯噪声
updates.append(tf.assign(perturbed_var, var + tf.random_normal(tf.shape(var), mean=0., stddev=param_noise_stddev)))
执行增加噪声, 在step函数中直接选择
#259行
if self.param_noise is not None and apply_noise:
actor_tf = self.perturbed_actor_tf # 注意 这里只选择了参数固定的噪声。
else:
actor_tf = self.actor_tf
其他函数 def reset(self):# 初始化噪声
# 初始化 将所有网络初始化、优化器初始化、硬更新一次target网络
def initialize(self, sess):
# 软更新target_net
def update_target_net(self):
# 通过从 数据库中采样数据并得到所有结果的函数
def setup_stats(self):
def get_stats(self):