昨天我們提到 -greedy 方法,讓我們的模型有一定的機會,嘗試執行沒有用過的動作,增加模型找到真正最佳動作的機會。
依照昨天所說明的內容,以及書籍中的流程圖,我們可以實作蒙地卡羅控制法,於之前的 GridWorld 中。定義了以下的函數幫助我們作業
我們定義一個函數,模擬在環境中運行的情況,並將模擬中經過的狀態、決策、回饋... 等資訊存下來,供之後更新使用。這邊我們一樣有設定最高執行 50 次的設定,目的在於避免沒有抵達終點的情況。
def SimProc(init_state, init_action, policy, steps, gamma, reward, trans_mat):
record = []
state = init_state
action = init_action
for step in range(steps):
# get next information
next_state = np.argmax(trans_mat[:, state, action])
next_action = policy[next_state]
record.append([state, action, reward[next_state]])
# update information
state = next_state
action = next_action
if state == 0 or state == 15:
break
if step > 50:
print('Over 50 steps')
return record
進行玩模擬後,我們要計算這次模擬(之前有說過這個稱為一個 episode)中,所執行的動作其價值為多少。這裡有兩個函數, GetValue 函數是負責計算某個位置使用某個動作的價值, EpisodeValue 函數用於控制計算進行 (ex: 不重複計算已經計算過的動作價值)。
我們這邊不重複計算已經計算過的動作價值,是因為採用 first-visit 原則,與之相對的是 every-visit 原則。在同一個 episode 中,我們可能會重複經過某些狀態,在後續計算動作價值時,有兩種方案:
- first-visit 指的是,只計算第一次到這個作態、採取這個動作,所得到的動作價值。
- every-visit 指的是,計算每次到這個狀態、採去這個動作,所得到的動作價值。
因此,在一個 episode 中,使用 first-visit 原則時,相同的狀態、動作只會有一個動作價值。而 every-visit 原則則可能會有多個動作價值。
實作上, fisit-visit 要記錄是否有經過某個狀態、採取某個動作,而 every-visit 則要紀錄某個狀態、採取某個動作的次數。
def GetValue(records, gamma):
counter = 0
value = 0
for record in records:
reward = record[-1]
value += reward*pow(gamma, counter)
counter += 1
return value
def EpisodeValue(records, gamma):
episode_visited = np.zeros([16, 4])
episode_value = np.zeros([16, 4])
for counter in range(len(records)):
state = records[counter][0]
action = records[counter][1]
if episode_visited[state, action] == 1:
continue
episode_value[state, action] = GetValue(records[counter:], gamma)
episode_visited[state, action] = 1
return episode_value, episode_visited
透過不同模擬,我們可以累積許多 episode 的資料,用來更新動作價值,並使用新的動作價值決定新的策略。
def UpdateActionValue(episode_value, episode_visited, action_value, total_visited):
total_value = (action_value*total_visited) + episode_value
total_visited += episode_visited
rst = total_value / total_visited
action_value = np.nan_to_num(rst)
return action_value, total_visited
def NextPolicy(action_value, epsilon):
if np.random.rand(1) < epsilon:
policy = np.random.randint(0,4,16)
explore = True
else:
policy = np.argmax(action_value, axis = 1)
explore = False
return policy, explore
在 main 函數中設定一些環境與模擬的參數
def main(Episodes, InitState, InitAction, InitEpsilon):
# environment setting
Policy = np.random.randint(0, 4, 16)
ActionValue = np.zeros([16, 4])
TotalVisited = np.zeros([16, 4])
Reward = np.full(16, -1)
Reward[0] = 0
Reward[-1] = 0
TransMat = np.load('./gridworld/T.npy')
# parameters setting
GAMMA = 0.99
Steps = 50
for episode in range(Episodes):
EPSILON = InitEpsilon - InitEpsilon*(episode/Episodes)
Records = SimProc(InitState, InitAction, Policy, Steps, GAMMA, Reward, TransMat)
NowEpisodeValue, NowEpisodeVisited = EpisodeValue(Records, GAMMA)
ActionValue, TotalVisited = UpdateActionValue(NowEpisodeValue, NowEpisodeVisited,
ActionValue, TotalVisited)
Policy, Explore = NextPolicy(ActionValue, EPSILON)
PrintPolicy(episode, Policy, Explore)
time.sleep(1)
# next simulating InitState and InitAction
InitState = np.random.randint(1,16)
InitAction = Policy[InitState]
關於蒙地卡羅控制的完整程式碼,請見 GitHub。
這邊設定 episodes = 1000, 起始狀態 = 3, 起始動作 = 左, 起始 EPSILON = 0.5,在這個參數組下,進行了幾次模擬,比較好的結果像這個樣子
============================================================
[Policy]
Policy Type: Greedy
Episode: 1000
[['*' '<' '<' 'v']
['^' '<' '<' 'v']
['^' '>' 'v' 'v']
['>' '>' '>' '*']]
============================================================
比較差一點的像這個樣子:
============================================================
[Policy]
Policy Type: Greedy
Episode: 1000
[['*' '<' '<' '<']
['^' '<' 'v' 'v']
['^' '^' '>' 'v']
['^' '<' '>' '*']]
============================================================
在狀態 13 的地方繞了遠路。
有興趣的話可以自己跑幾次,或調整一些參數,說不定會看到其他有趣的結果。明天來討論為什麼執行結果會有差異,以及蒙地卡羅方法的特色。
請問這是[起始狀態 = 3, 起始動作 = 左, 起始 EPSILON = 0.5],是說如果手動設定嗎?程式看起來初始狀態跟動作是隨機的。
InitState = np.random.randint(1,16)
InitAction = Policy[InitState]
不好意思沒有表達清楚,我指的是第一個 episode 的設定,可以參考 GitHub 最後兩行。
而你說的沒錯,在第二次以後確實都是使用隨機決定。這部分是考量到,我使用的是蒙地卡羅算法,因此盡量減少人為決定的事情。