在DQN(Deep Q-learning)入門教程(四)之Q-learning Play Flappy Bird中,我們使用q-learning演算法去對Flappy Bird進行強化學習,而在這篇部落格中我們將使用神經網路模型來代替Q-table,關於DQN的介紹,可以參考我前一篇部落格:DQN(Deep Q-learning)入門教程(五)之DQN介紹
在這篇部落格中將使用DQN做如下操作:
- Flappy Bird
- MountainCar-v0
再回顧一下DQN的演算法流程:
專案地址:Github
MountainCar-v0
MountainCar的訓練好的Gif示意圖如下所示,汽車起始位置位於山的底部,最終目標是駛向右邊山的插旗的地方,其中,汽車的引擎不能夠直接駛向終點,必須藉助左邊的山體的重力加速度才能夠駛向終點。
MountainCar-v0
由OpenAI提供,python包為gym,官網網站為https://gym.openai.com/envs/MountainCar-v0/。在Gym包中,提供了很多可以用於強化學習的環境(env):
在MountainCar-v0中,狀態有2個變數,car position(汽車的位置),car vel(汽車的速度),action一共有3種: Accelerate to the Left
, Don't accelerate
,Accelerate to the Right
,然後當車達到旗幟的地方(position = 0.5)會得到\(reward = 1\)的獎勵,如果沒有達到則為\(-1\)。但是如果當你執行步驟超過200次的時候,遊戲就會結束。詳情可以參考原始碼(ps:官方文件中沒有這些說明)。
下面介紹一下gym中幾個常用的函式:
-
env = gym.make("MountainCar-v0")
這個就是建立一個
MountainCar-v0
的遊戲環境。 -
state = env.reset()
重置環境,返回重置後的state
-
env.render()
將執行畫面展示在螢幕上面,當我們在訓練的時候可以不使用這個來提升速度。
-
next_state, reward, done, _ = env.step(action)
執行action動作,返回下一個狀態,獎勵,是否完成,info。
初始化Agent
初始化Agent直接使用程式碼說明吧,這個還是比較簡單的:
import keras
import random
from collections import deque
import gym
import numpy as np
from keras.layers import Dense
from keras.models import Sequential
class Agent():
def __init__(self, action_set, observation_space):
"""
初始化
:param action_set: 動作集合
:param observation_space: 環境屬性,我們需要使用它得到state的shape
"""
# 獎勵衰減
self.gamma = 1.0
# 從經驗池中取出資料的數量
self.batch_size = 50
# 經驗池
self.memory = deque(maxlen=2000000)
# 探索率
self.greedy = 1.0
# 動作集合
self.action_set = action_set
# 環境的屬性
self.observation_space = observation_space
# 神經網路模型
self.model = self.init_netWork()
def init_netWork(self):
"""
構建模型
:return: 模型
"""
model = Sequential()
# self.observation_space.shape[0],state的變數的數量
model.add(Dense(64 * 4, activation="tanh", input_dim=self.observation_space.shape[0]))
model.add(Dense(64 * 4, activation="tanh"))
# self.action_set.n 動作的數量
model.add(Dense(self.action_set.n, activation="linear"))
model.compile(loss=keras.losses.mean_squared_error,
optimizer=keras.optimizers.RMSprop(lr=0.001))
return model
我們使用佇列來儲存經驗,這樣的話新的資料就會覆蓋遠古的資料。此時我們定義一個函式,專門用來將資料儲存到經驗池中,然後定義一個函式用來更新\(\epsilon\)探索率。
def add_memory(self, sample):
self.memory.append(sample)
def update_greedy(self):
# 小於最小探索率的時候就不進行更新了。
if self.greedy > 0.01:
self.greedy *= 0.995
訓練模型
首先先看程式碼:
def train_model(self):
# 從經驗池中隨機選擇部分資料
train_sample = random.sample(self.memory, k=self.batch_size)
train_states = []
next_states = []
for sample in train_sample:
cur_state, action, r, next_state, done = sample
next_states.append(next_state)
train_states.append(cur_state)
# 轉成np陣列
next_states = np.array(next_states)
train_states = np.array(train_states)
# 得到next_state的q值
next_states_q = self.model.predict(next_states)
# 得到state的預測值
state_q = self.model.predict_on_batch(train_states)
# 計算Q現實
for index, sample in enumerate(train_sample):
cur_state, action, r, next_state, done = sample
if not done:
state_q[index][action] = r + self.gamma * np.max(next_states_q[index])
else:
state_q[index][action] = r
self.model.train_on_batch(train_states, state_q)
大家肯定從上面的程式碼發現一些問題,使用了兩個for迴圈,why?首先先說一下兩個for迴圈分別的作用:
- 第一個for迴圈:得到
train_states
和next_states
,其中next_states
是為了計算Q現實。 - 第二個for迴圈:計算Q現實
可能有人會有一個疑問,為什麼我不寫成一個for迴圈呢?實際上寫成一個for迴圈是完全沒有問題的,很?,但是寫成一個for迴圈意味著我們要多次呼叫model.predict_on_batch
,這樣會耗費一定的時間(親身試驗過,這樣會比較慢),因此,我們寫成了兩個for迴圈,然後只需要呼叫一次predict
執行動作與選擇最佳動作
執行動作的程式碼如下所示:
def act(self, env, action):
"""
執行動作
:param env: 執行環境
:param action: 執行的動作
:return: ext_state, reward, done
"""
next_state, reward, done, _ = env.step(action)
if done:
if reward < 0:
reward = -100
else:
reward = 10
else:
if next_state[0] >= 0.4:
reward += 1
return next_state, reward, done
其中,我們可以修改獎勵以加快網路收斂。
選擇最好的動作的動作如下所示,會以一定的探索率隨機選擇動作。
def get_best_action(self, state):
if random.random() < self.greedy:
return self.action_set.sample()
else:
return np.argmax(self.model.predict(state.reshape(-1, 2)))
開始訓練
關於具體的解釋,在註釋中已經詳細的說明了:
if __name__ == "__main__":
# 訓練次數
episodes = 10000
# 例項化遊戲環境
env = gym.make("MountainCar-v0")
# 例項化Agent
agent = Agent(env.action_space, env.observation_space)
# 遊戲中動作執行的次數(最大為200)
counts = deque(maxlen=10)
for episode in range(episodes):
count = 0
# 重置遊戲
state = env.reset()
# 剛開始不立即更新探索率
if episode >= 5:
agent.update_greedy()
while True:
count += 1
# 獲得最佳動作
action = agent.get_best_action(state)
next_state, reward, done = agent.act(env, action)
agent.add_memory((state, action, reward, next_state, done))
# 剛開始不立即訓練模型,先填充經驗池
if episode >= 5:
agent.train_model()
state = next_state
if done:
# 將執行的次數新增到counts中
counts.append(count)
print("在{}輪中,agent執行了{}次".format(episode + 1, count))
# 如果近10次,動作執行的平均次數少於160,則儲存模型並退出
if len(counts) == 10 and np.mean(counts) < 160:
agent.model.save("car_model.h5")
exit(0)
break
訓練一定的次數後,我們就可以得到模型了。然後進行測試。
模型測試
測試的程式碼沒什麼好說的,如下所示:
import gym
from keras.models import load_model
import numpy as np
model = load_model("car_model.h5")
env = gym.make("MountainCar-v0")
for i in range(100):
state = env.reset()
count = 0
while True:
env.render()
count += 1
action = np.argmax(model.predict(state.reshape(-1, 2)))
next_state, reward, done, _ = env.step(action)
state = next_state
if done:
print("遊戲的次數:", count)
break
部分的結果如下:
Flappy Bird
FlappyBird的程式碼我就不過多贅述了,裡面的一些函式介紹可以參照這個來看:DQN(Deep Q-learning)入門教程(四)之Q-learning Play Flappy Bird,程式碼思想與訓練Mountain-Car基本是一致的。
import random
from collections import deque
import keras
import numpy as np
from keras.layers import Dense
from keras.models import Sequential
from ple import PLE
from ple.games import FlappyBird
class Agent():
def __init__(self, action_set):
self.gamma = 1
self.model = self.init_netWork()
self.batch_size = 128
self.memory = deque(maxlen=2000000)
self.greedy = 1
self.action_set = action_set
def get_state(self, state):
"""
提取遊戲state中我們需要的資料
:param state: 遊戲state
:return: 返回提取好的資料
"""
return_state = np.zeros((3,))
dist_to_pipe_horz = state["next_pipe_dist_to_player"]
dist_to_pipe_bottom = state["player_y"] - state["next_pipe_top_y"]
velocity = state['player_vel']
return_state[0] = dist_to_pipe_horz
return_state[1] = dist_to_pipe_bottom
return_state[2] = velocity
return return_state
def init_netWork(self):
"""
構建模型
:return:
"""
model = Sequential()
model.add(Dense(64 * 4, activation="tanh", input_shape=(3,)))
model.add(Dense(64 * 4, activation="tanh"))
model.add(Dense(2, activation="linear"))
model.compile(loss=keras.losses.mean_squared_error,
optimizer=keras.optimizers.RMSprop(lr=0.001))
return model
def train_model(self):
if len(self.memory) < 2500:
return
train_sample = random.sample(self.memory, k=self.batch_size)
train_states = []
next_states = []
for sample in train_sample:
cur_state, action, r, next_state, done = sample
next_states.append(next_state)
train_states.append(cur_state)
# 轉成np陣列
next_states = np.array(next_states)
train_states = np.array(train_states)
# 得到下一個state的q值
next_states_q = self.model.predict(next_states)
# 得到預測值
state_q = self.model.predict_on_batch(train_states)
for index, sample in enumerate(train_sample):
cur_state, action, r, next_state, done = sample
# 計算Q現實
if not done:
state_q[index][action] = r + self.gamma * np.max(next_states_q[index])
else:
state_q[index][action] = r
self.model.train_on_batch(train_states, state_q)
def add_memory(self, sample):
self.memory.append(sample)
def update_greedy(self):
if self.greedy > 0.01:
self.greedy *= 0.995
def get_best_action(self, state):
if random.random() < self.greedy:
return random.randint(0, 1)
else:
return np.argmax(self.model.predict(state.reshape(-1, 3)))
def act(self, p, action):
"""
執行動作
:param p: 通過p來向遊戲發出動作命令
:param action: 動作
:return: 獎勵
"""
r = p.act(self.action_set[action])
if r == 0:
r = 1
if r == 1:
r = 100
else:
r = -1000
return r
if __name__ == "__main__":
# 訓練次數
episodes = 20000
# 例項化遊戲物件
game = FlappyBird()
# 類似遊戲的一個介面,可以為我們提供一些功能
p = PLE(game, fps=30, display_screen=False)
# 初始化
p.init()
# 例項化Agent,將動作集傳進去
agent = Agent(p.getActionSet())
max_score = 0
scores = deque(maxlen=10)
for episode in range(episodes):
# 重置遊戲
p.reset_game()
# 獲得狀態
state = agent.get_state(game.getGameState())
if episode > 150:
agent.update_greedy()
while True:
# 獲得最佳動作
action = agent.get_best_action(state)
# 然後執行動作獲得獎勵
reward = agent.act(p, action)
# 獲得執行動作之後的狀態
next_state = agent.get_state(game.getGameState())
agent.add_memory((state, action, reward, next_state, p.game_over()))
agent.train_model()
state = next_state
if p.game_over():
# 獲得當前分數
current_score = p.score()
max_score = max(max_score, current_score)
scores.append(current_score)
print('第%s次遊戲,得分為: %s,最大得分為: %s' % (episode, current_score, max_score))
if len(scores) == 10 and np.mean(scores) > 150:
agent.model.save("bird_model.h5")
exit(0)
break
該部分相比較於Mountain-Car需要更長的時間,目前的我還沒有訓練出比較好的效果,截至寫完這篇部落格,最新的資料如下所示:
emm,我又不想讓我的電腦一直開著,?。
總結
上面的兩個例子便是DQN最基本最基本的使用,我們還可以將上面的FlappyBird的問題稍微複雜化一點,比如說我們無法直接的知道環境的狀態,我們則可以使用CNN網路去從遊戲圖片入手(關於這種做法,網路上有很多人寫了相對應的部落格)。
專案地址:Github