从《西部世界》到GAIL(Generative Adversarial Imitation Learning)算法

原文链接: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 , θ ) = 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)}]
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)))]
这是基于论文《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)
接下来作一些初始化,然后调用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}
其中的参数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
然后分两步走:第一步用共轭梯度(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 的话,考虑约束的话就变成:
α 2 δ s T H s s \alpha \sqrt{\frac{2\delta}{s^T H s}} 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改进,去除了演示数据中的动作要求,并拓展到演示者和学习体的结构和物理参数不一致的情况。相信以后这个方向上还会有很多相关的研究成果。

发布了211 篇原创文章 · 获赞 438 · 访问量 148万+

猜你喜欢

转载自blog.csdn.net/ariesjzj/article/details/85220327