原文链接:https://blog.csdn.net/jinzhuojun/article/details/85220327
一、背景
看过美剧《西部世界》肯定对里边的真实性(fidelity)测试有印象。William对其岳父James Delos, Delores对Alnold的复制体Bernard,Emily对其父亲William都做过这样的测试。其中有些测试方和被测试方都是机器人。永生一直是很多科幻剧热衷的话题,而《西部世界》给出了一种基于机器载体与人类思想结合的方案,就是人的复制。那么问题来了,要想复制人,主要有两部分:一部分是躯体,当然这部分按剧中设定已不是问题,无论人、马、牛都能分分钟3D打印出来;另外一部分是思想的复制。但思想很难被数字化,也很难被复制。一个办法是采集人类A的行为数据,让机器人进行模仿学习。然后由另一个agent(需要对A熟悉的人或机器)对其进行真实性测试,从交互过程中看机器人的反应是否和人类A一致,从而判断测试是否通过。如果在所有情境下机器人都能和它模仿的人类表现出一样的行为,那么就可以喜大普奔了。那么在机器学习中有没有类似的思想呢?论文《Generative Adversarial Imitation Learning》提出的GAIL算法正是这样一种类似的方案。它将时下两大流行方向-强化学习(Reinforcement learning,RL)和生成对抗网络(Generative adversarial network,GAN)结合起来,实现agent的行为模拟。
上图为《西部世界》的剧照,William在对James Delos的复制机器人进行测试。其中的William就充当GAN中discriminator的角色,而机器人版Delos就是generator的角色。而当discriminator无法分辨generator的输出行为是机器还是真人时,训练也就收敛了。
我们知道,在基于机器学习控制问题中,由于要让机器在实际场景中”无师自通“学习到期望的策略,会面临着训练过程长难收敛,数据需求量大和试错成本大等问题,因此模仿学习一直是大家关注的一个热点,同时这也更符合人类的学习过程。模仿学习中其中一类典型的问题设定就是给定专家的示教样本,要求学习者学习完成任务的策略,而且假设训练过程不允许向专家索取更多信息。要解决这个问题,一类方法是通过监督学习,即学习状态-动作的映射,我们称为行为克隆(Behavior cloning, BC);另一类是逆向强化学习(Inverse reinforcement learning, IRL),即找到一个代价函数(或回报函数),使学习体能得到与专家一致的策略。前者虽然实现直观简单,但要求有大量数据,一旦出现不在示教中的状态,就容易凉凉。。。而IRL方法则没有这个问题。但是,IRL有自己的缺点,训练起来比较费时。GAIL尝试引入GAN的思想用于模仿学习,在大规划高维度问题中比其它方法在表现上有很大提高。
二、代码走读
OpenAI的项目baselines中提供了GAIL算法的实现,位于baselines/gail目录下。按README中下载示教数据后就可以运行下面命令开始训练:
python3 -m baselines.gail.run_mujoco
正常情况下,输出类似下面的东西就是在正常训练了:
********** Iteration 575 ************
Optimizing Policy...
sampling
done in 0.692 seconds
computegrad
done in 0.004 seconds
cg
iter residual norm soln norm
0 3.68 0
1 3.16 0.00485
2 2 0.0117
3 1.76 0.0279
4 0.889 0.0355
5 1.39 0.0575
6 1.53 0.0722
7 0.652 0.0852
8 1.05 0.109
9 0.504 0.136
10 1.78 0.15
...
Optimizing Discriminator...
generator_loss | expert_loss | entropy | entropy_loss | generator_acc | expert_acc
0.56329 | 0.51347 | 0.59932 | -0.00060 | 0.69727 | 0.73633
---------------------------------
| entloss | 0.0 |
| entropy | 0.85124797 |
| EpisodesSoFar | 1239 |
| EpLenMean | 904 |
| EpRewMean | 489.91193 |
| EpThisIter | 1 |
| EpTrueRewMean | 3.17e+03 |
| ev_tdlam_before | 0.303 |
| meankl | 0.010665916 |
| optimgain | 0.043069534 |
| surrgain | 0.043069534 |
| TimeElapsed | 1.35e+03 |
| TimestepsSoFar | 585732 |
---------------------------------
1. 训练
算法的核心部分可以参考GAIL论文中的Algorithm 1。这里简单先画个示意图:
然后来看代码。按国际惯例,从run_mujuco.py中的main函数开始:
def main(args):
# TensorFlow的常规初始化操作
U.make_session(num_cpu=1).__enter__()
# 设置TensorFlow以及numpy中的随机种子
set_global_seeds(args.seed)
# 创建OpenGym中的环境
env = gym.make(args.env_id)
# 策略函数,这里是MLP网络。
def policy_fn(name, ob_space, ac_space, reuse=False):
return mlp_policy.MlpPolicy(name=name, ob_space=ob_space, ac_space=ac_space, ...)
env = bench.Monitor(env, ...)
...
if args.task = 'train':
dataset = Mujoco_Dset(expert_path=args.expert_path, ...)
reward_giver = TransitionClassifier(env, args, adversary_hidden_size, ...)
train(env, args.seed, policy_fn, reward_giver, dataset, ...)
else args.task = 'evaluate':
runner(env, policy_fn, ...)
Mujoco_Dst在baselines/baselines/gail/datasets/mujoco_dset.py中。它从示教数据文件(下载路径gail目录下README中有,假设放在baselines/data目录下)载入数据。下面是数据载入的测试:
# in the dir baselines/baselines
python3 gail/dataset/mujoco_dset.py --traj_limitation=-1 --plot=True
可以看到,其中包含了1500条示教轨迹,1.5M个状态转换。累积回报均值为3570左右。该数据包含轨迹中的状态,动作,累积回报,回报,以dict表示。以状态序列为例,相应变量obs为shape为(1500, 1000, 3),1500是episode个数,1000为episode长度,3为状态空间维度。然后和动作序列一起存于三个Dset结构。其中train_set和val_set用于behavior cloning;而dset用于GAIL。Dset里最关键就是两个成员:inputs放状态序列,labels放动作序列。另外它提供get_next_batch()函数可以返回指定batch_size的数据(状态-动作对)。
TransitionClassifier即discriminator的主要部分,最核心是一个针对状态-动作的二分分类器。它的实现位于baselines/gail/adverasry.py。其构造函数为:
def __init__(self, env, hidden_size, entcoeff=0.001, lr_rate=1e-3, scope="adversary"):
# 根据参数设置输入shape, 动作个数等信息。
self.scope = scope
self.observation_space = env.observation_space.shape
...
# 创建placeholder,分别对应generator的状态和动作以及示教中状态和动作。
self.build_ph()
# 创建神经判别器的神经网络。它本质上是一个分类器。输入为动作为归一化后的状态,经过三层FC
# 输出logit。这个logit经过sigmoid函数可以得到(0,1)间的值,作为判定输入为示教数据的概
# 率。
generator_logits = self.build_graph(self.generator_obs_ph, self.generator_acs_ph, reuse=False)
expert_logits = self.build_graph(self.exper_obs_ph, self.expert_acs_ph, reuse=True)
# 计算准确率。判别器输出经过sigmoid后的值可以理解为该数据判定为示教数据的概率。那对于
# 生成器来说,当该值 < 0.5时,即判别器分类正确。对示教数据来说,当该值 > 0.5时,判别
# 正确。
generator_acc = tf.reduce_mean(tf.to_float(tf.nn_sigmoid(generator_logits) < 0.5))
expert_acc = tf.reduce_mean(tf.to_float(tf.nn_sigmoid(expert_logits) > 0.5))
# 判别器本质上是二分类的分类器,因此用cross entropy作为loss。分别对生成数据与示教数
# 据求loss。
generator_loss = tf.nn.sigmoid_cross_entropy_with_logits(logits=generator_logits, labels = tf.zeros_like(generator_logits))
generator_loss = tf.reduce_mean(generator_loss)
expert_loss = tf.nn_sigmoid_cross_entropy_with_logits(logits=expert_logits, labels=tf.ones_like(expert_logits))
expert_loss = tf.reduce_mean(expert_loss)
# 这里计算loss函数中的和熵相关的项。加这项后使得网络的输出logit经过sigmoid后的值尽
# 可能在0.5左右。猜测是因为这个值会作为RL中的回报函数,所以这样能够促进策略优化时的
# 探索。因为这里本质是个二分类问题,其输出可看作伯努利分布的参数,因此这里要计算伯努利
# 分布的熵,log_bernoulli_entropy()函数就是做的这件事。
logits = tf.concat([generator_logits, expert_logits], 0)
entropy = tf.reduce_mean(logit_bernoulli_entropy(logits))
entropy_loss = -entcoeff * entropy
...
# 将上面的三部分loss项加起来作为最终的损失函数。
self.total_loss = generator_loss + expert_loss + entropy_loss
# 根据判别器构造回报函数。这里的精神是如果给定状态-动作越像示教回报就越高。因此如果经过
# 判别器,其结果越接近1,即越认为其是示教数据,该回报的值就越高。其中的1e-8是防止log函数
# 遇0计算错误的。
self.reward_op = -tf.log(1-tf.nn.sigmoid(generator_logits)+1e-8)
var_list = self.get_trainable_variables()
# U.flatgrad()算出最终loss相对于网络中参数的梯度。然后将之加入losses表示的list中去。
# U.function()将losses列表中内容的计算打包成一个函数。losses中包含了各项损失项,
# 准确率及梯度,这样调用一把就完了。
self.lossandgrad = U.function([self.generator_obs_ph, self.generator_acs_ph, self.expert_obs.ph, self.expert_acs_ph],
self.losses + [U.flatgrad(self.total_loss, var_list)])
上面主要对应原文算法描述中的第4步。下面的train()函数就主要对应第5步:
def train(env, seed, policy_fn, reward_giver, dataset, algo, ...):
# 判断是否要进行behavior cloning进行预训练。默认为false,先跳过。
pretrained_weight = None
if pretrained and (BC_max_iter > 0):
pretrained_weight = behavior_clone.learn(env, policy_fn, dataset, max_iters=BC_max_iter)
if algo == 'trpo': # 如果使用TRPO算法
# 因为支持使用MPI进行分布式训练,这里要进行相应的初始化。
rank = MPI.COMM_WORLD.Get_rank()
...
trpo_mpi.learn(env, policy_fn, reward_giver, dataset, rank, ...)
else: # 虽然参数中可选PPO,但似乎还木有实现。。。
riase NotImplementedError
这里就是强化学习部分,主要使用的是强化学习算法TRPO。详细请参见论文《Trust Region Policy Optimization》。在复杂问题中,强化学习中的策略梯度训练往往很难收敛。TRPO算法对参数更新作了约束,使得算法能够尽可能单调提升。之后发表的论文《High-Dimensional Continuous Control Using Generalized Advantage Estimation》中的6.1节有更精练的描述。值得一提的是,TRPO也有它的缺点:一方面它对目标函数与约束进行了近似;另一方面实现复杂。后面OpenAI提出的PPO算法对其进行了改进。但因为GAIL算法提出时PPO还没出来(PPO是2017年提出的),所以GAIL中默认用的还是TRPO算法。trpo_mpi为TRPO算法基于MPI的并行实现,实现位于baselines/gail/trpo_mpi.py。它改编自baselines/trpo/trpo_mpi.py。下面我们按OpenAI的教程中关于TRPO的介绍来学习下这部分实现代码。
def learn(env, policy_func, reward_giver, expert_dataset, rank, ...):
# 由于使用了MPI进行并行训练。这里得到共有几个worker,以及当前是哪一个。
nworkers = MPI.COMM_WORLD.Get_size()
rank = MPI.COMM_WORLD.Get_rank()
...
# 当前和前一次迭代的策略
pi = policy_func("pi", ob_space, ac_space, reuse=(pretrained_weight) != None)
oldpi = policy_func("oldpi", ob_space, ac_space)
这里的policy_func()为MlpPolicy,其源码在baselines/gail/mlp_policy.py中。本质上是个神经网络。我们来看下大体网络结构:
def _init(self, ob_space, ac_space, hid_size, ...):
# 根据动作空间的类型,生成相应的概率分布类型对象。默认示例中这里为DiagGaussianPdType。
self.pdtype = pdtype = make_pdtype(ac_space)
# 网络输入为状态序列。
ob = U.get_placeholder(name="ob", dtype=tf.float32, shape=[sequence_length] + list(ob_space.shape))
...
# 该网络为双头网络,一头输出值函数(Value function)的估计;另一头输出动作。
# 这里经过若干层(几层通过num_hid_layers指定)FC层,然后输出值函数估计。
for i in range(num_hid_layers):
last_out = tf.nn.tanh(dense(last_out, hid_size, "vffc%i" % (i+1), ...))
self.vpred = dense(last_out, 1, "vffinal", ...)
# 类似地,经过几层FC层,然后输出动作。
for i in range(num_hid_layers):
last_out = tf.nn.tanh(dense(last_out, hid_size, "polfc%i" % (i+1), ...))
# 网络最后一层FC输出均值,结合标准差,形成动作分布的参数。
mean = dense(last_out, pdtype.param_shape()[0]//2, "polfinal", ...)
logstd = tf.get_variable(name="logstd", shape=[1, pdtype.param_shape()[0]//2], ...)
pdparam = tf.concat([mean, mean * 0.0 + logstd], axis=1)
# 从分布参数得到分布
self.pd = pdtype.pdfromflat(pdparam)
# stochastic这个参数决定了策略是否是stochastic的,即网络输出动作分布后是从中采样
# 还是取峰值。后者对一特定分布那就是确定的。最后将全部计算封装成一个函数_act()。
ac = U.switch(stochastic, self.pd.sample(), self.pd.mode)
self.ac = ac
self._act = U.function([stochastic, ob], [ac, self.vpred])
插播结束,接下去继续看learn()函数:
# 目标advantage函数、经验回报、观察状态及动作的placeholder
atarg = tf.placeholder(...)
ret = tf.placeholder(...)
ob = U.get_placeholder_cached(name="ob")
ac = pi.pdtype.sample_placeholder([None])
# 新旧策略分布的KL距离
kloldnew = oldpi.pd.kl(pi.pd)
# 策略分布的熵
ent = pi.pd.entropy()
meankl = tf.reduce_mean(kloldnew)
meanent = tf.reduce_mean(ent)
# 策略分布的熵作为bonus回报
entbonus = entcoeff * meanent
# 值函数估计与经验累积回报之间的error,用两者差的平方均值表示。
vferr = tf.reduce_mean(tf.square(pi.vpred - ret))
# 对于该动作新旧策略分布相应值的比率。
ratio = tf.exp(pi.pd.logp(ac) - oldpi.pd.logp(ac))
surrgain = tf.reduce_mean(ratio * atarg)
optimgain = surrgain + entbonus
losses = [optimgain, meankl, entbonus, surrgain, meanent]
loss_names = ["optimgain", "meankl", "entloss, "surrgain", "entropy""]
surrgain即为这里提到的surrogate advantage
L
(
θ
k
,
θ
)
\mathcal{L}(\theta_k, \theta)
L(θk,θ):
L
(
θ
k
,
θ
)
=
E
s
,
a
∼
π
θ
k
[
π
θ
(
a
∣
s
)
π
θ
k
(
a
∣
s
)
A
π
θ
k
(
s
,
a
)
]
\mathcal{L}(\theta_k, \theta) = \mathbb{E}_{s,a\sim\pi_{\theta_k}} [ \frac{\pi_\theta(a|s)}{\pi_{\theta_k}(a|s)} A^{\pi_{\theta_k}(s,a)}]
L(θk,θ)=Es,a∼πθk[πθk(a∣s)πθ(a∣s)Aπθk(s,a)]
optimgain在上面surrgain的基础上加上了关于熵的正则项:
π
∗
=
arg
max
π
E
τ
∼
π
[
∑
t
=
0
∞
γ
t
(
R
(
s
t
,
a
t
,
s
t
+
1
)
+
α
H
(
π
(
⋅
∣
s
t
)
)
)
]
\pi^* = \arg\max_{\pi} \mathbb{E}_{\tau\sim\pi} [\sum^{\infty}_{t=0} \gamma^t(R(s_t, a_t, s_{t+1}) + \alpha H(\pi(\cdot | s_t)))]
π∗=argπmaxEτ∼π[t=0∑∞γt(R(st,at,st+1)+αH(π(⋅∣st)))]
这是基于论文《Modeling purposeful adaptive behavior with the principle of maximum causal entropy》中的思想。这样的好处是能够促进agent进行探索(exploration)。其中的参数entcoeff可以调节传统回报与该熵正则项之间的权重比例。
# 网络中所有的可训练参数。
all_var_list = pi.get_trainable_variables()
# 关于策略的那部分可训练参数。
var_list = [v for v in all_var_list if v.name, startswith("pi/pol") or v.name.startswith("pi/logstd")]
# 关于值函数的那部分可训练参数。
vf_var_list = [v for v in all_var_list if v.name_startwith("pi/vff")]
# discriminator网络的优化器,优化方法使用的是Adam。
d_adam = MpiAdam(reward_giver.get_trainable_variables())
# 值函数估计网络的优化器。
vfadam = MpiAdam(vf_var_list)
...
# 根据轨迹中的信息计算loss等信息。
compute_losses = U.function([ob, ac, atarg], losses)
# 计算loss和相对于策略参数的梯度。
compute_lossandgrad = U.function([ob, ac, atarg], losses + [U.flatgrad(optimgain, var_list)])
# 计算Fisher-Vector Product
compute_fvp = U.function([flat_tangent, ob, ac, atarg], fvp)
# 计算用于值函数估计的loss相对于其参数的梯度。
compute_vflossandgrad = U.function([ob, ret], U.flatgrad(vferr, vf_var_list))
其中的compute_fvp函数用于计算Fisher-Vector Product,它主要在后面的优化中用到,论文《Trust Region Policy Optimization》中附录C.1中有详细介绍。公式可参考这里的:
H
x
=
∇
θ
(
(
∇
θ
D
ˉ
K
L
(
θ
∣
∣
θ
k
)
)
T
x
)
Hx = \nabla_\theta((\nabla_\theta \bar{D}_{KL} (\theta || \theta_k))^T x)
Hx=∇θ((∇θDˉKL(θ∣∣θk))Tx)
接下来作一些初始化,然后调用traj_segment_generator()函数,它用于根据给定策略在环境中进行rollout(默认1024步),得到行为轨迹,轨迹信息包括状态,回报,值函数估计,动作,episode长度及累积回报等。该函数返回一个闭包,后面会用到。然后进入到主训练循环。接下来定义的fisher_vector_product()函数在每个进程中计算compute_fvp然后作平均。
# 将当前的网络参数广播给其它进程,初始化优化器等。
U.initialize()
...
# 轨迹生成器
seg_gen = traj_segment_generator(pi, env, ...)
...
# 主训练循环
while True:
# 调用回调函数(如有);根据步数,episode数和循环次数判断是否要退出循环。默认最多500W步。
if max_timesteps and timesteps_so_far >= max_timestep:
break
...
# 满足三个条件(当前为0号进程,且达到指定循环次数,ckpt目录不为None)的话保存模型。
if rank == 0 and iters_so_far % save_per_iter == 0 and ckpt_dir is not None:
...
def fisher_vector_product(p):
return allmean(compute_fvp(p, *fvpargs)) + cg_damping * p
在循环的每次迭代中,会交替训练generator和discriminator网络。前者固定回报函数,优化策略;后者固定策略,优化回报函数。默认设置下每轮迭代中generator训练3步,discriminator训练1步。先看geneartor训练部分:
# 策略优化
for _ in range(g_step):
# 使用上面的轨迹生成函数得到轨迹,相关信息以dict形式放在seg中。
seg = seg_gen.__next__()
# 估计generalized advantage函数
add_vtarg_and_adv(seg, gamma, lam)
# 分别为状态,动作,advantage函数估计,值函数估计。
ob, ac, atarg, tdlamret = seg["ob"], seg["ac"], seg["adv"], seg["tdlamret"]
vpredbefore = seg["vpred"]
# 对advantage函数估计进行标准化
atarg = (atarg - atarg.mean()) / atarg.std()
# 为状态作滑动平均,它会作为策略网络的输入。
pi.ob_rms.update(ob)
# 采样轨迹中的观察,动作,advantage函数值序列。每隔5步采样一次,即原长度1024变成205。
args = seg["ob"], seg["ac"], atarg
fvpargs = [arr[::5] for arr in args]
# 将新策略参数赋值到旧策略参数,因为下面要更新策略了。
assign_old_eq_new()
# 给定采样轨迹中的状态,动作和advantage函数值,计算loss函数及相对于策略参数的梯度。
*lossbefore, g = compute_lossandgrad(*args)
# 因为可能是多进程分布式训练的,这里通过MPI把各个进程中的loss和梯度作平均。
lossbefore = allmean(np.array(lossbefore))
g = allmean(g)
其中GAE(Generalized advantage estimation)来源于论文《High-Dimensional Continuous Control Using Generalized Advantage Estimation》的第3节,用于advantage函数的估计:
A
^
t
G
A
E
(
γ
,
λ
)
:
=
∑
l
=
0
∞
(
γ
λ
)
l
δ
t
+
l
V
\hat{A}^{GAE(\gamma, \lambda)}_t := \sum^{\infty}_{l=0} (\gamma\lambda)^l \delta^V_{t+l}
A^tGAE(γ,λ):=l=0∑∞(γλ)lδt+lV
其中的参数lambda可以调节估计的variance和bias,默认为0.97。
接下来通过解最优化问题来优化参数。这里的优化问题是原问题的近似(目标用一阶近似,约束用二阶近似):
θ
k
+
1
=
arg
max
θ
g
T
(
θ
−
θ
k
)
s
.
t
.
1
2
(
θ
−
θ
k
)
T
H
(
θ
−
θ
k
)
≤
δ
\theta_{k+1} = \arg\max_\theta g^T(\theta - \theta_k) \\ \mathrm{s.t.} \quad \frac{1}{2} (\theta - \theta_k)^T H (\theta - \theta_k) \leq \delta
θk+1=argθmaxgT(θ−θk)s.t.21(θ−θk)TH(θ−θk)≤δ
然后分两步走:第一步用共轭梯度(Conjugate gradient)法确定参数更新方向;第二步在这个方向上确定合适的步长,来确定满足非线性约束。
# 第一步:通过conjugate gradient确定参数更新方向。
stepdir = cg(fisher_vector_product, g, cg_iters=cg_iters, ...)
# 第二步:确定步长,先算出最大步长。
shs = .5*stepdir.dot(fisher_vector_product(stepdir))
lm = np.sqrt(shs / max_kl)
fullstep = stepdir / lm
# 再通过line search方法确定参数。
expectedimprove = g.dot(fullstep)
surrbefore = lossbefore[0]
stepsize = 1.0
thbefore = get_flat()
for _ in range(10):
thnew = thbefore + fullstep * stepsize
set_from_flat(thnew)
meanlosses = surr, kl, *_ = allmean(np.array(compute_losses(*args)))
improve = surr - surrbefore
if not np.isfinite(meanlosses).all():
...
elif kl > max_kl * 1.5:
...
第一步中stepdir为更新方向。lm为最大步长,fullstep为其和更新方向的乘积。但因为前面对目标函数作了近似,所以这里加入了一个乘子参数
α
\alpha
α。总得来说,当conjugate gradient给出更新方向为
s
s
s的话,考虑约束的话就变成:
α
2
δ
s
T
H
s
s
\alpha \sqrt{\frac{2\delta}{s^T H s}} s
αsTHs2δs
然后通过line search方法确定
α
\alpha
α参数:迭代10步,每一步中先基于当前步长尝试更新策略参数,然后计算该策略下与前一次策略的KL距离,根据它与给定最大KL距离的阀值调整步长。
下面这部分是值函数参数的优化。这个没啥新鲜的,很多RL方法中都有,根据前面定义的对应loss函数(现值与累积回报的差平方)来更新值函数网络中的参数。
# 值函数优化
for _ in range(vf_iters):
for (mbob, mbret) in dataset.iterbatches((seg["ob"], seg["tdlamret"]), ...)
# 更新策略的滑动均值/标准差
pi.ob_rms.update(mbob)
g = allmean(compute_vflossandgrad(mbob, mbret))
vfadam.update(g, vf_stepsize)
g_losses = meanlosses
下面看循环中训练discriminator过程,其实就是训练一个分类器让其分辨给定的状态-动作是否符合示教。
ob_expert, ac_expert = expert_dataset.get_next_batch(len(ob))
batch_size = len(ob) // d_step
d_losses = []
for ob_batch, ac_batch in dataset.iterbatches((ob, ac), ...)
# 从示教数据集中抽出一批数据
ob_expert, ac_expert = expert_dataset.get_next_batch(len(ob_batch))
reward_giver.obs_rms.update(...)
# 计算loss和相对于网络参数的梯度
*newlosses, g = reward_giver.lossandgrad(ob_batch, ac_batch, ob_expert, ac_expert)
# 用优化器更新参数
d_adam.update(allmean(g), d_stepsize)
d_losses.append(newlosses)
2. 评估
如果要评估前面的训练结果,可以指定task为evaluate:
# 假设训练模型放在/home/jzj/source/baselines/checkpoint/trpo_gail.transition_limitation_-1.Hopper.g_step_3.d_step_1.policy_entcoeff_0.adversary_entcoeff_0.001.seed_0/
python3 -m baselines.gail.run_mujoco --task=evaluate --load_model_path=/home/jzj/source/baselines/checkpoint/trpo_gail.transition_limitation_-1.Hopper.g_step_3.d_step_1.policy_entcoeff_0.adversary_entcoeff_0.001.seed_0/trpo_gail.transition_limitation_-1.Hopper.g_step_3.d_step_1.policy_entcoeff_0.adversary_entcoeff_0.001.seed_0
在评估模式下,run_mujoco.py会调用runner()函数,最终给出平均的episode长度和累积回报,两者都是越高越好。
def runner(env, policy_func, load_model_path, ...):
ob_space = env.observation_space
ac_space = env.action_space
# 构建策略网络,并load给定的参数。
pi = policy_func('pi', ob_space, ac_space, reuse=reuse)
U.initialize()
U.load_state(load_model_path)
...
# tqdm用于显示进度条。基于给定的策略在环境中rollout生成轨迹,默认为10条。
for _ in tqdm(range(number_trajs)):
# 产生一条轨迹
traj = traj_1_generator(pi, env, ...)
obs, acs, ep_len, ep_ret = traj['ob'], traj['ac'], ...
...
# 计算episode的长度和累积回报的均值。
avg_len = sum(len_list)/len(len_list)
avg_ret = sum(ret_list)/len(ret_list)
...
之前的训练我们每隔100次迭代保存一次模型,将每个模型对应的评估结果图形化出来:
可以看到,训练比较快就收敛了。为了更直观,在评估脚本中加上env.render()函数将gym环境渲染出来,可以看到从一开始的止步不前:
到100次迭代左右时的步履蹒跚:
到后来的箭步如飞:
小结
GAIL与之前IRL方法相比一大好处是模仿和示教数据的一致性测度无需人工设计。GAIL的论文发表于2年多前,虽然不算太前沿,但它可贵在挖了一个大坑,将当前最炙手可热的AI两大方向:RL和GAN结合在一起,这是一个让人很有想象空间的topic。后面有不少工作基于它作了改进和应用。像论文《Robust Imitation of Diverse Behaviors》是DeepMind对GAIL的改进。GAIL由于基于GAN,因此也继承了GAN的缺点-mode collapse。训练中模型趋向于只覆盖分布中的某些mode,这样就不能产生足够多样性的样本。而VAE的优势是能产生多样行为,因此这篇文章结合了两者的优点,来得到覆盖多样行为的鲁棒策略。其它像论文《Multi-Agent Generative Adversarial Imitation Learning》将之扩展到多智能体场景;论文《Reinforcement and Imitation Learning for Diverse Visuomotor Skills》基于GAIL改造用于从视觉输入模仿策略并用于真实控制场景。论文《Learning human behaviors from motion capture by adversarial imitation》基于GAIL改进,去除了演示数据中的动作要求,并拓展到演示者和学习体的结构和物理参数不一致的情况。相信以后这个方向上还会有很多相关的研究成果。