從《西部世界》到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,θ)=Es,aπθk[πθ(as)πθk(as)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的基礎上加上了關於熵的正則項:
π=argmaxπEτπ[t=0γt(R(st,at,st+1)+αH(π(st)))] \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中有詳細介紹。公式可參考這裏的:
Hx=θ((θDˉKL(θθk))Tx) 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^tGAE(γ,λ):=l=0(γλ)lδt+lV \hat{A}^{GAE(\gamma, \lambda)}_t := \sum^{\infty}_{l=0} (\gamma\lambda)^l \delta^V_{t+l}
其中的參數lambda可以調節估計的variance和bias,默認爲0.97。

接下來通過解最優化問題來優化參數。這裏的優化問題是原問題的近似(目標用一階近似,約束用二階近似):
θk+1=argmaxθgT(θθk)s.t.12(θθk)TH(θθ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給出更新方向爲ss的話,考慮約束的話就變成:
α2δsTHss \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改進,去除了演示數據中的動作要求,並拓展到演示者和學習體的結構和物理參數不一致的情況。相信以後這個方向上還會有很多相關的研究成果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章