1. 程式人生 > >深度增強學習PPO(Proximal Policy Optimization)演算法原始碼走讀

深度增強學習PPO(Proximal Policy Optimization)演算法原始碼走讀

OpenAI出品的baselines專案提供了一系列deep reinforcement learning(DRL,深度強化學習或深度增強學習)演算法的實現。現在已經有包括DQN,DDPG,TRPO,A2C,ACER,PPO在內的近十種經典演算法實現,同時它也在不斷擴充中。它為對DRL演算法的復現驗證和修改實驗提供了很大的便利。本文主要走讀其中的PPO(Proximal Policy Optimization)演算法的原始碼實現。PPO是2017年由OpenAI提出的一種DRL演算法,它不僅有很好的performance(尤其是對於連續控制問題),同時相較於之前的TRPO方法更加易於實現。之前寫過一篇雜文

《深度增強學習(DRL)漫談 - 信賴域(Trust Region)系方法》對其歷史、原理及相關方法做了簡單介紹,因此本文主要focus在程式碼實現的學習瞭解上。

OpenAI baselines專案中對於PPO演算法有兩個實現,分別位於ppo1和ppo2目錄下。其中ppo2是利用GPU加速的,官方號稱會快三倍左右,所以下面主要是看ppo2。對應論文為《Proximal Policy Optimization Algorithms》,以下簡稱PPO論文。本文我們就以atari這個經典的DRL實驗場景為例看一下大體流程。啟動訓練的命令在readme中有:

$ python3 -m baselines.ppo2.run_atari
  • 1

這樣就開始訓練了,每輪引數更新後會打印出相關資訊。如:

------------------------------------
| approxkl           | 0.003101161 |
| clipfrac           | 0.17260742  |
| eplenmean          | 941         |
| eprewmean          | 34.9        |
| explained_variance | 0.704       |
| fps                | 981         |
| nupdates           | 1653
| | policy_entropy | 0.85041255 | | policy_loss | -0.01297911 | | serial_timesteps | 211584 | | time_elapsed | 1.63e+03 | | total_timesteps | 1692672 | | value_loss | 0.036234017 | ------------------------------------
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

可以看到,入口為run_atari.py中的main():

def main():
# 實現位於common/cmd_util.py。它主要為parser新增幾個引數:
#   1) env:代表要執行atari中的哪個遊戲環境。預設為BreakoutNoFrameskip-v4,即“打磚塊”。
#   2) seed:隨機種子。預設為0。
#   3) num-timesteps:訓練的頻數。預設為10M次。
     parser = atari_arg_parser()
# 通過引數選擇policy network的形式,實現在policies.py。預設為CNN。這裡有三種選擇:
#   1) CNN:相應函式為CnnPolicy()。發表於《Nature》上的經典DRL奠基論文《Human-level control through
#       deep reinforcement learning》中使用的神經網路結構:conv->relu->conv->relu->conv->relu->
#       fc->relu。注意它是雙頭網路,一頭輸出policy,一頭輸出value。
#   2) LSTM:相應函式為LstmPolicy()。它將CNN的輸出之上再加上LSTM層,這樣就結合了時間域的資訊。
#   3) LnLSTM:相應函式為LnLstmPolicy()。其它的和上面一樣,只是在構造LSTM層時添加了Layer normalization
#       (詳見論文《Layer Normalization》)
     parser.add_argument('--policy', help='Policy architecture', choices=['cnn', 'lstm', 'lnlstm'], default='cnn')
# 用剛才的構建的parser解析命令列傳入的引數。
     args = parser.parse_args()
# 這個專案中實現了簡單的日誌系統。其中日誌所在目錄和格式可以用過OPENAI_LOGDIR和OPENAI_LOG_FORMAT兩個環境
# 變數控制。實現類Logger中主要有兩個字典:name2val和name2cnt。它們分別是名稱到值和計數的對映。
     logger.configure()
# 這裡是開始正式訓練了。
     train(args.env, num_timesteps=args.num_timesteps, seed=args.seed,
         policy=args.policy)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

主函式中最後呼叫了train()函式進行訓練。

 def train(env_id, num_timesteps, seed, policy):
     # 首先是一坨和TensorFlow相關的環境設定,比如根據cpu核數設定並行執行緒數等。
     ...
     # 構建執行環境。流程還比較長,下面會再詳細地理下。
     env = VecFrameStack(make_atari_env(env_id, 8, seed), 4)
     # 對應前面說的三種策略網路。用於根據引數選取相應的實現函式。
     policy = {'cnn' : CnnPolicy, 'lstm' : LstmPolicy, 'lnlstm' : LnLstmPolicy}[policy]
     # 使用PPO演算法進行學習。其中傳入的引數不少是模型的超引數。詳細可參見PPO論文中的Table 5。
     ppo2.learn(policy=policy, env=env, nsteps=128, nminibatches=4,
         lam=0.95, gamma=0.99, noptepochs=4, log_interval=1,
         ent_coef=.01,
         lr=lambda f : f * 2.5e-4,
         cliprange=lambda f : f * 0.1,
         total_timesteps=int(num_timesteps * 1.1))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

可以看到,train()函式中比較重要的就是兩大塊:環境構建和模型引數學習。首先看看環境構建流程:

# make_atari_env()函式實現位於common/cmd_util.py。看函式名就知道主要就是建立atari環境。通過OpenAI gym
# 建立基本的atari環境後,還需要層層封裝。gym中提供了Wrapper介面,讓開發者通過decorator設計模式來改變環境中的設# 定。
def make_atari_env(env_id, num_env, ...):  # 這裡的num_env為8,意味著會建立8個獨立的並行執行環境。
    def make_env(rank):
        def _thunk():
            # 建立由gym構建的atari環境的封裝類。
            env = make_atari(env_id):       
                # 通過OpenAI的gym介面建立gym環境。
                env = gym.make(env_id)  
                # NoopResetEnv為gym.Wrapper的繼承類。每次環境重置(呼叫reset())時執行指定步隨機動作。
                env = NoopResetEnv(env) 
                # MaxAndSkipEnd也是gym.Wrapper的繼承類。每隔4幀返回一次。返回中的reward為這4幀reward
                # 之和,observation為最近兩幀中最大值。
                env = MaxAndSkipEnd(env)    
                return env
            # 每個環境選取不同的隨機種子,避免不同環境跑得都一樣。
            env.seed(seed + rank)       
            # 實現在monitor.py中。Monitor為gym中Wrapper的繼承類,對環境Env進行封裝,主要添加了對
            # episode結束時資訊的記錄。
            env = Monitor(env, ...)     
            return wrap_deepmind(env, ...):
                # 標準情況下,對於atari中的很多遊戲(比如這兒的打磚塊),命掉光了(如該遊戲有5條命)算episode
                # 結束,環境重置。這個Wrapper的作用是隻要掉命就讓step()返回done,但保持環境重置的時機不變
                #(仍然是命掉完時)。原註釋中說這個trick在DeepMind的DQN中用來幫助value的估計。
                env = EpisodeicLifeEnv(env)     
                # 通過OpenCV將原始輸入轉成灰度圖,且轉成84 x 84的解析度。
                env = WarpFrame(env)        
                # 將reward按正負值轉為+1, -1和0。
                env = ClipRewardEnv(env)    
                ...
                return env
        return _thunk   
    ...
    # 返回SubprocVecEnv物件。
    return SubprocVecEnv([make_env(i + start_index) for i in range(num_env)]) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

建立num_env個元素(這裡為8)的陣列,每一個元素為一個函式閉包_thunk()。VecEnv實現在baselines/common/vec_env/__init__.py,它是一個抽象類,代表異步向量化環境。其中包括幾個重要的抽象函式: reset()用於重置所有環境,step_async()用於通知所有環境開始根據給定動作執行一步,step_wait()得到執行完的結果。step_wait()等待step_async()的結果。step()就是step_async() 加上step_wait()。而VecEnvWrapper也為VecEnv的繼承類,和gym中提供的Wrapper功能類似,如果要對VecEnv實現的預設行為做修改的話就可以利用它。

上面函式最後返回的SubprocVecEnv類為VecEnv的繼承類,它主要將上面建立好的函式放到各個子程序中去執行。在SubprocVecEnv實現類中,構造時傳入在子程序中執行的函式。通過Process建立子程序,並通過Pipe進行程序間通訊。make_atari_env()中建立SubprocVecEnv後,又立馬被VecFrameStack封裝了一把。VecFrameStack為VecEnvWrapper的實現類,實現在vec_frame_stack.py。在VecFrameStack的建構函式中,wos為gym環境中的原始狀態空間,維度為[84,84,1]。low和high分別為這些維度的最低和最高值。stackedobs就是把幾個環境的狀態空間疊加起來,即維度變為(8, 84, 84, 4)。8為環境個數,(84,84)為單幀狀態維度,也就是遊戲的螢幕輸出,4代表最近4幀(因為會用最近4的幀的遊戲畫面來作為網路模型的輸入)。

可以看到,除了正常的封裝外,還需要做一些比較tricky,比較靠經驗的處理。理論上我們希望這部分越少越好,因為越少演算法就越通用。然而現狀是這一塊tuning對結果的好壞可能產生比較大的影響。。。

好了,接下去就可以看看PPO演算法主體了。入口為ppo2.py的learn()函式。

# 首先一坨引數設定,仍然以run_atari.py為例。
nenvs = env.num_envs                # 8 
ob_space = env.observation_space    # Box(84,84,4)
ac_space = env.action_space         # Discrete(4)
nbatch = nenvs * nsteps             # 1024 = 8 * 128。共8個並行環境,每個環境執行128步。即nbatch為單個batch中所有環境中執行的總步數。
nbatch_train = nbatch // nminibatches   # 256 = 1024 / 4。nbatch_train為訓練時batch的大小。即1024步分4次訓練。

# make_model()函式是一個用於構造Model物件的函式。
make_model = lambda : Model(policy=policy, ob_space=ob_space, ...)  
# 建立Model。
model = make_model()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

模型的構建也是最核心的部分。這塊要和PPO論文配合起來看,否則容易暈。

class Model(object):
    def __init__(self, *, policy, ob_space, ac_space, nbatch_act, nbatch_train,
                 nsteps, ent_coef, vf_coef, max_grad_norm):
        # 用前面指定的網路型別構造兩個策略網路。act_model用於執行策略網路根據當前observation返回
        # action和value等,即只做inference。train_model顧名思義主要用於引數的更新(模型的學習)。
        # 注意這兩個網路的引數是共享的,因此train_model更新的引數可以體現在act_model上。假設使用默
        # 認的CnnPolicy,其中的step()函式計算action, value function和action提供的資訊量;
        # value()函式計算value。

        # nbatch_act = 8,就等於環境個數nenvs。因為每一次都分別對8個環境執行,得到每個環境中actor的動作。
        # 1為nsteps。其實在CNN中沒啥用,在LSTM才會用到(因為LSTM會考慮前nsteps步作為輸入)。
        act_model = policy(sess, ob_space, ac_space, nbatch_act, 1, reuse=False)    
            h = nature_cnn(X)       # 如前面所說,《Nature》上的網路結構打底。然後輸出policy和value。
            pi = fc(h, 'pi', ...)   # for policy
            vf = fc(h, 'v')         # for value function
            # 根據action space建立相應的引數化分佈。如這裡action space是Discrete(4),那分佈
            # 就是CategoricalPdType()。然後根據該分佈型別,結合網路輸出(pi),得到動作概率分
            # 布CategoricalPd,最後在該分佈上取樣,得到動作a0。neglogp0即為該動作的自資訊量。
            pdtype = make_pdtype() 
            pd = self.pdtype.pdfromflat(pi) 
            a0 = self.pd.sample()
            neglogp0 = self.pd.neglogp(a0)
        # 和構建action model類似,構建用於訓練的網路train_model。nbatch_train為256,因為是用於模型的學習,
        # 因此和act_model不同,這兒網路輸入的batch size為256。
        train_model = policy(sess, ob_space, ac_space, nbatch_train, nsteps, reuse=True)    
        # 建立一坨placeholder,這些是後面要傳入的。
        A = train_model.pdtype.sample_placeholder([None])  # action
        ADV = tf.placeholder(tf.float32, [None])           # advantage
        R = tf.placeholder(tf.float32, [None])             # return
        OLDNEGLOGPAC = tf.placeholder(tf.float32, [None])  # old -log(action)
        OLDVPRED = tf.placeholder(tf.float32, [None])      # old value prediction
        LR = tf.placeholder(tf.float32, [])                # learning rate
        CLIPRANGE = tf.placeholder(tf.float32, [])         # clip range,就是論文中的epsilon。

        neglogpac = train_model.pd.neglogp(A)              # -log(action)
        entropy = tf.reduce_mean(train_model.pd.entropy())

        # 訓練模型提供的value預測。
        vpred = train_model.vf  
        # 和vpred類似,只是與上次的vpred相比變動被clip在由CLIPRANGE指定的區間中。
        vpredclipped = OLDVPRED + tf.clip_by_value(train_model.vf - OLDVPRED, - CLIPRANGE, CLIPRANGE)    
        vf_losses1 = tf.square(vpred - R)
        vf_losses2 = tf.square(vpredclipped - R)
        # V loss為兩部分取大值:第一部分是網路預測value值和R的差平方;第二部分是被clip過的預測value值
        # 和return的差平方。這部分和論文中似乎不太一樣。主要目的應該是懲罰value值的過大更新。
        vf_loss = .5 * tf.reduce_mean(tf.maximum(vf_losses1, vf_losses2))
        # 論文中的probability ratio。把這裡的exp和log展開就是論文中的形式。
        ratio = tf.exp(OLDNEGLOGPAC - neglogpac)    
        pg_losses = -ADV * ratio
        pg_losses2 = -ADV * tf.clip_by_value(ratio, 1.0 - CLIPRANGE, 1.0 + CLIPRANGE)
        # 論文公式(7),由於前面都有負號,這裡是取maximum.
        pg_loss = tf.reduce_mean(tf.maximum(pg_losses, pg_losses2)) 
        approxkl = .5 * tf.reduce_mean(tf.square(neglogpac - OLDNEGLOGPAC))
        clipfrac = tf.reduce_mean(tf.to_float(tf.greater(tf.abs(ratio - 1.0), CLIPRANGE)))
        # 論文公式(9),ent_coef, vf_coef分別為PPO論文中的c1, c2,這裡分別設為0.01和0.5。entropy為文中的S;pg_loss為文中的L^{CLIP}
        loss = pg_loss - entropy * ent_coef + vf_loss * vf_coef     

        # 構建trainer,用於引數優化。
        grads = tf.gradients(loss, params)
        trainer = tf.train.AdamOptimizer(learning_rate=LR, max_grad_norm)
        _train = trainer.apply_gradients()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

上面模型構造完了,接下來就是模型學習過程的skeleton了。Runner類主要作為學習過程的組織協調者。

# Runnder是整個訓練過程的協調者。
runner = Runner(env=env, model=model, nsteps=nsteps,...)
# total_timesteps = 11000000, nbatch = 1024,因此模型引數更新nupdates = 10742次。
nupdates = total_timesteps // nbatch
for update in range(1, nupdates+1) # 對應論文中Algorithm的外迴圈。
    obs, returns, masks, actions, values, ... = runner.run()
        # 模型(上面的act_model)執行nsteps步。有8個環境,即共1024步。該迴圈對應論文中Algorithm的第2,3行。
        for _ in range(self.nsteps): 
            # 執行模型,通過策略網路返回動作。
            actions, values, self.states, ... = self.model.step(self.obs, self.status, ...)
            # 通過之前建立的環境執行動作,得到observation和reward等資訊。
            self.obs[:], rewards, self.dones, infos = self.env.step(actions)
        # 上面環境執行返回的observation, action, values等資訊都加入mb_xxx中存起來,後面要拿來學習引數用。
        mb_obs = np.asarray(mb_obs, dtype=self.obs.dtype) 
        mb_rewards = np.asarray(mb_rewards, dtype=np.float32) 
        mb_actions = np.asarray(mb_actions) 
        ...
        # 估計Advantage。對應化文中Algorithm的第4行。
        for t in reversed(range(self.nsteps)):
            # 論文中公式(12)。
            delta = mb_rewards[t] + self.gamma * nextvalues * nextnonterminal - mb_values[t]  
            # 論文中公式(11)。
            mb_advs[t] = lastgaelam = delta + self.gamma * self.lam * nextnonterminal * lastgaelam 
        mb_returns = mb_advs + mb_values # Return = Advantage + Value
        return (*map(sf01, (mb_obs, mb_returns, mb_dones, mb_actions, mb_values, mb_neglogpacs)), mb_states, epinfos)
    epinfobuf.extend(epinfos) # Gym中返回的info。
    # 論文中Algorithm 1第6行。
    if states is None: # nonrecurrent version
        inds = np.arange(nbatch)   
        for _ in range(noptepochs): # epoch為4
            np.random.shuffle(inds)
            # 8個actor,每個執行128步,因此單個batch為1024步。1024步又分為4個minibatch,
            # 因此單次訓練的batch size為256(nbatch_train)。
            for start in range(0, nbatch, nbatch_train): # [0, 256, 512, 768]
                end = start + nbatch_train
                mbinds = inds[start:end]
                slices = (arr[mbinds] for arr in (obs, ...))
                # 將前面得到的batch訓練資料作為引數,呼叫模型的train()函式進行引數學習。
                mblossvals.append(model.train(lrnow, cliprangenow, *slices)) 
                    # Advantage = Return - Value
                    advs = returns - values 
                    # Normalization
                    advs = (advs - advs.mean()) / (advs.std() + 1e-8) 
                    # cliprange是隨著更新的步數遞減的。因為一般來說訓練越到後面越收斂,每一步的差異也會越來越小。 
                    # neglogpacs和values都是nbatch_train維向量,即shape為(256, )。
                    td_map = {train_mode.X:obs, A:actions, ADV:advs, R:returns, LR:lr, 
                        CLIPRANGE:cliprange, OLDNEGLOGPAC:neglogpacs, OLDVPRED:values}
                    return sess.run([pg_loss, vf_loss, entropy, approxkl, clipfrac, _train], td_map)
    else:
        ...
    # 每過指定間隔列印以下引數。
    if update % log_interval == 0 or update == 1:
        ev = explained_variance(values, returns)
        logger.logkv("serial_timesteps", update*nsteps)
        logger.logkv("nupdates", update)
        ...
    # 滿足條件時儲存模型。
    if save_interval and (update % save_interval == 0 or update == 1) and logger.get_dir():
        ...
        model.save(savepath)
env.close()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

訓練結束,我們可以通過下面命令將訓練過程中的主要指標-Episode Rewards圖形化。可以用–dirs引數指定前面訓練時log所在目錄。

python3 -m baselines.results_plotter
  • 1

可以看到,如期望地,隨著訓練的進行,學習到的策略使得agent能在一輪遊戲中玩得越來越久,一輪中的累積回報也就越來越大。 這裡寫圖片描述

最後,是騾子是馬還是要出來溜溜才知道。下面指令碼用於將訓練產生的ckpt load起來,然後執行atari環境,執行策略網路產生的動作,並將過程渲染出來。引數為ckpt檔案路徑。

import gym
from gym import spaces
import multiprocessing
import joblib
import sys
import os
import numpy as np
import tensorflow as tf

from baselines.ppo2 import ppo2
from baselines.common.cmd_util import make_atari_env, atari_arg_parser
from baselines.common.atari_wrappers import make_atari, wrap_deepmind
from baselines.ppo2.policies import CnnPolicy
from baselines.common.vec_env.vec_frame_stack import VecFrameStack

def main(argv):
    ncpu = multiprocessing.cpu_count()
    config = tf.ConfigProto(allow_soft_placement=True,
                            intra_op_parallelism_threads=ncpu,
                            inter_op_parallelism_threads=ncpu)
    config.gpu_options.allow_growth = True
    sess = tf.Session(config=config)

    env_id = "BreakoutNoFrameskip-v4"
    seed = 0
    nenvs = 1
    nstack = 4

    env = wrap_deepmind(make_atari(env_id))
    ob_space = env.observation_space
    ac_space = env.action_space

    wos = env.observation_space
    low = np.repeat(wos.low, nstack, axis=-1)
    high = np.repeat(wos.high, nstack, axis=-1)
    stackedobs = np.zeros((nenvs,)+low.shape, low.dtype)
    observation_space = spaces.Box(low=low, high=high, dtype=env.observation_space.dtype)
    vec_ob_space = observation_space

    act_model = CnnPolicy(sess, vec_ob_space, ac_space, nenvs, 1, reuse=False)

    with tf.variable_scope('model'):
        params = tf.trainable_variables()

    #load_path = '/tmp/openai-2018-05-27-15-06-16-102537/checkpoints/00030'
    load_path = argv[0]
    loaded_params = joblib.load(load_path)
    restores = []
    for p, loaded_p in zip(params, loaded_params):
        restores.append(p.assign(loaded_p))
    sess.run(restores)
    print("model " + load_path + " loaded")

    obs = env.reset()
    done = False
    for _ in range(1000):
        env.render()
        obs = np.expand_dims(obs, axis=0)

        stackedobs = np.roll(stackedobs, shift=-1, axis=-1)
        stackedobs[..., -obs.shape[-1]:] = obs

        actions, values, states, neglogpacs = act_model.step(stackedobs)
        print("%d, action=%d" % (_, actions[0]))
        obs, reward, done, info = env.step(actions[0])
        if done:
            print("done")
            obs = env.reset()
            stackedobs.fill(0)

    sess.close()

if __name__ == '__main__':
    if (len(sys.argv)) != 2:
        sys.exit("Usage: %s ckpt_path" % sys.argv[0])
    if not os.path.exists(sys.argv[1]):
        sys.exit("ckpt file %s not found" % sys.argv[1])
    main(sys.argv[1:])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78

可以看到當更新迭代500次時,演算法已經能學習到一些遊戲的基本策略了,但仍不是很嫻熟。5條命基本在1000步內就會被幹光。 這裡寫圖片描述 當更新迭代5000次後,學到的策略比500次時已經成熟很多了,5條命在1000步內基本夠用。 這裡寫圖片描述 當更新迭代10000次後,基本已經玩得很溜了。試驗中1000步只損了一條命。 這裡寫圖片描述