本教程講解如何使用深度強化學習訓練一個可以在 CartPole 遊戲中獲勝的模型。研究人員使用 tf.keras、OpenAI 訓練了一個使用「非同步優勢動作評價」(Asynchronous Advantage Actor Critic,A3C)演算法的智慧體,透過 A3C 的實現解決了 CartPole 遊戲問題,過程中使用了貪婪執行、模型子類和自定義訓練迴圈。
該過程圍繞以下概念執行:
貪婪執行——貪婪執行是一個必要的、由執行定義的介面,此處的運算一旦從 Python 呼叫,就要立刻執行。這使得以 TensorFLow 開始變得更加容易,還可以使研究和開發變得更加直觀。
模型子類——模型子類允許透過編寫 tf.keras.Model 子類以及定義自己的正向傳導通路自定義模型。由於可以強制寫入前向傳導,模型子類在貪婪執行啟用時尤其有用。
自定義訓練迴圈。
本教程遵循的基本工作流程如下:
建立主要的智慧體監管
建立工作智慧體
實現 A3C 演算法
訓練智慧體
將模型表現視覺化
本教程面向所有對強化學習感興趣的人,不會涉及太深的機器學習基礎,但主題中涵蓋了高階策略網路和價值網路的相關知識。此外,我建議閱讀 Voldymyr Mnih 的《Asynchronous Methods for Deep Reinforcement Learning》(https://arxiv.org/abs/1602.01783),這篇文章很值得一讀,而且文中涉及到本教程採用的演算法的很多細節。
什麼是 Cartpole?
Cartpole 是一個遊戲。在該遊戲中,一根杆透過非驅動關節連線到小車上,小車沿無摩擦的軌道滑動。初始狀態(推車位置、推車速度、杆的角度和杆子頂端的速度)隨機初始化為 +/-0.05。透過對車施加 +1 或 -1(車向左或向右移動)的力對該系統進行控制。杆開始的時候是直立的,遊戲目標是防止杆倒下。杆保持直立過程中的每個時間步都會得到 +1 的獎勵。當杆傾斜 15 度以上或小車與中間位置相隔 2.4 個單位時遊戲結束。
程式碼
完整程式碼:https://github.com/tensorflow/models/blob/master/research/a3c_blogpost/a3c_cartpole.py
安裝指南:https://github.com/tensorflow/models/tree/master/research/a3c_blogpost
建立基線
為了正確判斷模型的實際效能以及評估模型的度量標準,建立一個基線通常非常有用。舉個例子,如果返回的分數很高,你就會覺得模型表現不錯,但事實上,我們很難確定高分是由好的演算法還是隨機行為帶來的。在分類問題的樣例中,可以透過簡單分析類別分佈以及預測最常見的類別來建立基線。但我們該如何針對強化學習建立基線呢?可以建立隨機的智慧體,該智慧體可以在我們的環境中做出一些隨機行為。
class RandomAgent:
"""Random Agent that will play the specified game
Arguments:
env_name: Name of the environment to be played
max_eps: Maximum number of episodes to run agent for.
"""
def __init__(self, env_name, max_eps):
self.env = gym.make(env_name)
self.max_episodes = max_eps
self.global_moving_average_reward = 0
self.res_queue = Queue()
def run(self):
reward_avg = 0
for episode in range(self.max_episodes):
done = False
self.env.reset()
reward_sum = 0.0
steps = 0
while not done:
# Sample randomly from the action space and step
_, reward, done, _ = self.env.step(self.env.action_space.sample())
steps += 1
reward_sum += reward
# Record statistics
self.global_moving_average_reward = record(episode,
reward_sum,
0,
self.global_moving_average_reward,
self.res_queue, 0, steps)
reward_avg += reward_sum
final_avg = reward_avg / float(self.max_episodes)
print("Average score across {} episodes: {}".format(self.max_episodes, final_avg))
return final_avg
就 CartPole 這個遊戲而言,我們在 4000 個迴圈中得到了 ~20 的平均值。為了執行隨機的智慧體,要先執行 python 檔案: python a3c_cartpole.py—algorithm=random—max-eps=4000。
什麼是非同步優勢動作評價演算法
非同步優勢動作評價演算法是一個非常拗口的名字。我們將這個名字拆開,演算法的機制就自然而然地顯露出來了:
非同步:該演算法是一種非同步演算法,其中並行訓練多個工作智慧體,每一個智慧體都有自己的模型和環境副本。由於有更多的工作智慧體並行訓練,我們的演算法不僅訓練得更快,而且可以獲得更多樣的訓練經驗,因為每一個工作體的經驗都是獨立的。
優勢:優勢是一個評價行為好壞和行為輸出結果如何的指標,允許演算法關注網路預測值缺乏什麼。直觀地講,這使得我們可以衡量在給定時間步時遵循策略 π 採取行為 a 的優勢。
動作-評價:演算法的動作-評價用了在策略函式和價值函式間共享層的架構。
它是如何起作用的?
在更高階別上,A3C 演算法可以採用非同步更新策略,該策略可以在固定的經驗時間步上進行操作。它將使用這些片段計算獎勵和優勢函式的估計值。每一個工作智慧體都會遵循下述工作流程:
獲取全域性網路引數
透過遵循最小化(t_max:到終極狀態的步長)步長數的區域性策略與環境進行互動
計算價值損失和策略損失
從損失中得到梯度
用梯度更新全域性網路
重複
在這樣的訓練配置下,我們期望看到智慧體的數量以線性速度增長。但你的機器可以支援的智慧體數量受可用 CPU 核的限制。此外,A3C 可以擴充套件到多個機器上,有一些較新的研究(像是 IMPALA(https://deepmind.com/blog/impala-scalable-distributed-deeprl-dmlab-30/))甚至支援它更進一步擴充套件。但新增太多機器可能會對速度和效能產生一些不利影響。參閱這篇文章(https://arxiv.org/abs/1602.01783)以獲取更深入的資訊。
重新審視策略函式和價值函式
如果你已經對策略梯度有所瞭解,那麼就可以跳過這一節。如果你不知道什麼是策略或價值,或是想要快速複習一些策略或價值,請繼續閱讀。
策略的思想是在給定輸入狀態的情況下引數化行為機率分佈。我們透過建立一個網路來了解遊戲的狀態並決定我們應該做什麼,以此來實現這個想法。因此,當智慧體進行遊戲時,每當它看到某些狀態(或是相似的狀態),它就可以在給定輸入狀態下計算出每一個可能的行為的機率,然後再根據機率分佈對行為進行取樣。從更深入的數學角度進行分析,策略梯度是更為通用的分數函式梯度估計的特例。一般情況下將期望表示為 p(x | ) [f(x)];但在我們的例子中,獎勵(優勢)函式的期望,f,在某些策略網路中,p。然後再用對數導數方法,算出如何更新我們的網路引數,使得行為樣本能獲得更高的獎勵並以 ∇ Ex[f(x)] =Ex[f(x) ∇ log p(x)] 結束。這個等式解釋瞭如何根據獎勵函式 f 在梯度方向上轉換 θ 使得分最大化。
價值函式基本上就可以判斷某種狀態的好壞程度。從形式上講,價值函式定義了當以狀態 s 開始,遵循策略 p 時得到獎勵的期望總和。這是模型中「評價」部分相關之處。智慧體使用價值估計(評價)來更新策略(動作)。
實現
我們首先來定義一下要使用的模型。主智慧體有全域性網路,每個區域性的工作體在它們自己的程式中擁有該網路的的副本。我們用模型子類例項化該模型。模型子類為我們提供了更高的靈活度,而代價是冗餘度也更高。
public class MyActivity extends AppCompatActivity {
@Override //override the function
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
OkhttpManager.getInstance().setTrustrCertificates(getAssets().open("mycer.cer");
OkHttpClient mOkhttpClient= OkhttpManager.getInstance().build();
} catch (IOException e) {
e.printStackTrace();
}
}
從前向傳遞中可以看出,模型得到輸入後會返回策略機率 logits 和 values。
主智慧體——主執行緒
我們來了解一下該操作的主體部分。主智慧體有可以更新全域性網路的共享最佳化器。該智慧體例項化了每個工作智慧體將要更新的全域性網路以及用來更新它的最佳化器。這樣每個工作智慧體和我們將使用的最佳化器就可以對其進行更新。A3C 對學習率的傳遞是很有彈性的,但針對 Cart Pole 我們還是要用學習率為 5e-4 的 AdamOptimizer(https://www.tensorflow.org/api_docs/python/tf/train/AdamOptimizer)。
class MasterAgent():
def __init__(self):
self.game_name = 'CartPole-v0'
save_dir = args.save_dir
self.save_dir = save_dir
if not os.path.exists(save_dir):
os.makedirs(save_dir)
env = gym.make(self.game_name)
self.state_size = env.observation_space.shape[0]
self.action_size = env.action_space.n
self.opt = tf.train.AdamOptimizer(args.lr, use_locking=True)
print(self.state_size, self.action_size)
self.global_model = ActorCriticModel(self.state_size, self.action_size) # global network
self.global_model(tf.convert_to_tensor(np.random.random((1, self.state_size)), dtype=tf.float32))
主智慧體將執行訓練函式以例項化並啟動每一個智慧體。主智慧體負責協調和監管每一個智慧體。每一個智慧體都將非同步執行。(因為這是在 Python 中執行的,從技術上講這不能稱為真正的非同步,由於 GIL(全域性直譯器鎖)的原因,一個單獨的 Python 過程不能並行多個執行緒(利用多核)。但可以同時執行它們(在 I/O 密集型操作過程中轉換上下文)。我們用執行緒簡單而清晰地實現了樣例。
def train(self):
if args.algorithm == 'random':
random_agent = RandomAgent(self.game_name, args.max_eps)
random_agent.run()
return
res_queue = Queue()
workers = [Worker(self.state_size,
self.action_size,
self.global_model,
self.opt, res_queue,
i, game_name=self.game_name,
save_dir=self.save_dir) for i in range(multiprocessing.cpu_count())]
for i, worker in enumerate(workers):
print("Starting worker {}".format(i))
worker.start()
moving_average_rewards = [] # record episode reward to plot
while True:
reward = res_queue.get()
if reward is not None:
moving_average_rewards.append(reward)
else:
break
[w.join() for w in workers]
plt.plot(moving_average_rewards)
plt.ylabel('Moving average ep reward')
plt.xlabel('Step')
plt.savefig(os.path.join(self.save_dir,
'{} Moving Average.png'.format(self.game_name)))
plt.show()
Memory 類——儲存我們的經驗
此外,為了更簡單地追蹤模型,我們用了 Memory 類。該類的功能是追蹤每一步的行為、獎勵和狀態。
class Memory:
def __init__(self):
self.states = []
self.actions = []
self.rewards = []
def store(self, state, action, reward):
self.states.append(state)
self.actions.append(action)
self.rewards.append(reward)
def clear(self):
self.states = []
self.actions = []
self.rewards = []
現在我們已經知道了演算法的關鍵:工作智慧體。工作智慧體繼承自 threading 類,我們重寫了來自 Thread 的 run 方法。這使我們得以實現 A3C 中的第一個 A——非同步。我們先透過例項化區域性模型和設定特定的訓練引數開始。
class Worker(threading.Thread): # Set up global variables across different threads global_episode = 0 # Moving average reward global_moving_average_reward = 0 best_score = 0 save_lock = threading.Lock() def __init__(self, state_size, action_size, global_model, opt, result_queue, idx, game_name='CartPole-v0', save_dir='/tmp'): super(Worker, self).__init__() self.state_size = state_size self.action_size = action_size self.result_queue = result_queue self.global_model = global_model self.opt = opt self.local_model = ActorCriticModel(self.state_size, self.action_size) self.worker_idx = idx self.game_name = game_name self.env = gym.make(self.game_name).unwrapped self.save_dir = save_dir self.ep_loss = 0.0
執行演算法
下一步是要實現 run 函式。這是要真正執行我們的演算法了。我們將針對給定的全域性最大執行次數執行所有執行緒。這是 A3C 中的「動作」所起的作用。我們的智慧體會在「評價」判斷行為時根據策略函式採取「行動」,這是我們的價值函式。儘管這一節的程式碼看起來很多,但實際上沒有進行太多操作。在每一個 episode 中,程式碼只簡單地做了這些:
1. 基於現有框架得到策略(行為機率分佈)。
2. 根據策略選擇行動。
3. 如果智慧體已經做了一些操作(args.update_freq)或者說智慧體已經達到了終端狀態(結束),那麼:
a. 用從區域性模型計算得到的梯度更新全域性模型。
4. 重複
def run(self):
total_step = 1
mem = Memory()
while Worker.global_episode < args.max_eps:
current_state = self.env.reset()
mem.clear()
ep_reward = 0.
ep_steps = 0
self.ep_loss = 0
time_count = 0
done = False
while not done:
logits, _ = self.local_model(
tf.convert_to_tensor(current_state[None, :],
dtype=tf.float32))
probs = tf.nn.softmax(logits)
action = np.random.choice(self.action_size, p=probs.numpy()[0])
new_state, reward, done, _ = self.env.step(action)
if done:
reward = -1
ep_reward += reward
mem.store(current_state, action, reward)
if time_count == args.update_freq or done:
# Calculate gradient wrt to local model. We do so by tracking the
# variables involved in computing the loss by using tf.GradientTape
with tf.GradientTape() as tape:
total_loss = self.compute_loss(done,
new_state,
mem,
args.gamma)
self.ep_loss += total_loss
# Calculate local gradients
grads = tape.gradient(total_loss, self.local_model.trainable_weights)
# Push local gradients to global model
self.opt.apply_gradients(zip(grads,
self.global_model.trainable_weights))
# Update local model with new weights
self.local_model.set_weights(self.global_model.get_weights())
mem.clear()
time_count = 0
if done: # done and print information
Worker.global_moving_average_reward = \
record(Worker.global_episode, ep_reward, self.worker_idx,
Worker.global_moving_average_reward, self.result_queue,
self.ep_loss, ep_steps)
# We must use a lock to save our model and to print to prevent data races.
if ep_reward > Worker.best_score:
with Worker.save_lock:
print("Saving best model to {}, "
"episode score: {}".format(self.save_dir, ep_reward))
self.global_model.save_weights(
os.path.join(self.save_dir,
'model_{}.h5'.format(self.game_name))
)
Worker.best_score = ep_reward
Worker.global_episode += 1
ep_steps += 1
time_count += 1
current_state = new_state
total_step += 1
self.result_queue.put(None)
如何計算損失?
工作智慧體透過計算損失得到所有相關網路引數的梯度。這是 A3C 中最後一個 A——advantage(優勢)所起的作用。將這些應用於全域性網路。損失計算如下:
價值損失:L=∑(R—V(s))²
策略損失:L=-log(?(s)) * A(s)
式中 R 是折扣獎勵,V 是價值函式(輸入狀態),? 是策略函式(輸入狀態),A 是優勢函式。我們用折扣獎勵估計 Q 值,因為我們不能直接用 A3C 決定 Q 值。
def compute_loss(self,
done,
new_state,
memory,
gamma=0.99):
if done:
reward_sum = 0. # terminal
else:
reward_sum = self.local_model(
tf.convert_to_tensor(new_state[None, :],
dtype=tf.float32))[-1].numpy()[0]
# Get discounted rewards
discounted_rewards = []
for reward in memory.rewards[::-1]: # reverse buffer r
reward_sum = reward + gamma * reward_sum
discounted_rewards.append(reward_sum)
discounted_rewards.reverse()
logits, values = self.local_model(
tf.convert_to_tensor(np.vstack(memory.states),
dtype=tf.float32))
# Get our advantages
advantage = tf.convert_to_tensor(np.array(discounted_rewards)[:, None],
dtype=tf.float32) - values
# Value loss
value_loss = advantage ** 2
# Calculate our policy loss
actions_one_hot = tf.one_hot(memory.actions, self.action_size, dtype=tf.float32)
policy = tf.nn.softmax(logits)
entropy = tf.reduce_sum(policy * tf.log(policy + 1e-20), axis=1)
policy_loss = tf.nn.softmax_cross_entropy_with_logits_v2(labels=actions_one_hot,
logits=logits)
policy_loss *= tf.stop_gradient(advantage)
policy_loss -= 0.01 * entropy
total_loss = tf.reduce_mean((0.5 * value_loss + policy_loss))
return total_loss
工作智慧體將重複在全域性網路中重置網路引數和與環境進行互動、計算損失再將梯度應用於全域性網路的過程。透過執行下列命令訓練演算法:python a3c_cartpole.py—train。
測試演算法
透過啟用新環境和簡單遵循訓練出來的模型得到的策略輸出測試演算法。這將呈現出我們的環境和模型產生的策略分佈中的樣本。
def play(self):
env = gym.make(self.game_name).unwrapped
state = env.reset()
model = self.global_model
model_path = os.path.join(self.save_dir, 'model_{}.h5'.format(self.game_name))
print('Loading model from: {}'.format(model_path))
model.load_weights(model_path)
done = False
step_counter = 0
reward_sum = 0
try:
while not done:
env.render(mode='rgb_array')
policy, value = model(tf.convert_to_tensor(state[None, :], dtype=tf.float32))
policy = tf.nn.softmax(policy)
action = np.argmax(policy)
state, reward, done, _ = env.step(action)
reward_sum += reward
print("{}. Reward: {}, action: {}".format(step_counter, reward_sum, action))
step_counter += 1
except KeyboardInterrupt:
print("Received Keyboard Interrupt. Shutting down.")
finally:
env.close()
你可以在模型訓練好後執行下列命令:python a3c_cartpole.py。
檢查模型所得分數的滑動平均:
我們應該看到得分 >200 後收斂了。該遊戲連續試驗 100 次平均獲得了 195.0 的獎勵,至此稱得上「解決」了該遊戲。
在新環境中的表現:
關鍵點
該教程涵蓋的內容:
透過 A3C 的實現解決了 CartPole。
使用了貪婪執行、模型子類和自定義訓練迴圈。
Eager 使開發訓練迴圈變得簡單,因為可以直接列印和除錯張量,這使編碼變得更容易也更清晰。
透過策略網路和價值網路對強化學習的基礎進行了學習,並將其結合在一起以實現 A3C
透過應用 tf.gradient 得到的最佳化器更新規則迭代更新了全域性網路。