Day 8 的 DQN 已經能把一個 Breakout state 轉成每個 action 的 Q-values,Day 9 也把遊戲互動產生的 transition 存進 Replay Buffer。
現在還差一個很重要的問題:拿到 Q-values 之後,Agent 到底要選哪個 action?
直覺上,好像直接選 Q-value 最大的 action 就行了。但訓練剛開始時,這些 Q-values 還只是很不成熟的估計。如果 Agent 太早相信目前最大的值,它可能從此只重複同一類行為,根本沒有機會發現其他 action 其實更好。
反過來,如果每一步都隨機選 action,雖然什麼都會試到,卻又完全沒有利用模型已經學到的資訊。
這就是強化學習裡很重要的 Exploration vs. Exploitation(探索與利用):
Day 10 要做的事情,就是用 epsilon-greedy 在兩者之間取得一個簡單、可控制的平衡。
argmax,Agent 可能永遠看不到其他選擇利用的做法很直接:從所有 Q-values 中選最大值。這個操作叫 argmax。
假設目前四個 action 的 Q-values 是:
[1.0, 2.0, 5.0, 3.0]
那麼 action index 2 的 Q-value 最大,所以 greedy action 就是 2。
問題是:Q-value 是目前模型的估計,不是真正答案。
如果訓練剛開始時,模型碰巧高估某個 action,而 Agent 又永遠只執行 argmax,那麼其他 action 就很少有機會被執行。沒有新的互動,就沒有新的 transition;沒有那些 transition,模型也很難知道自己原本的判斷可能是錯的。
所以 argmax 沒有算錯,它只是只回答:
以目前的估計來看,哪個 action 最大?
它不會主動去回答:
我是不是還漏掉了更好的選擇?
另一個極端是每一步都 random。這樣確實能探索不同 action,但當 Agent 已經慢慢學到一些有用資訊後,仍然完全不使用它,互動效率會很差。
Epsilon-greedy 的想法很簡單:每次要選 action 時,先做一次隨機判斷。
epsilon 的機率走 random branch,隨機選一個 action;1 - epsilon 的機率走 greedy branch,選 Q-value 最大的 action。epsilon 是一個 0 到 1 之間的數字。
例如:
| epsilon | 行為 |
|---|---|
0 |
完全不探索,每次都 greedy |
0.1 |
約 10% 的決策走 random branch |
0.5 |
約一半 random、一半 greedy |
1 |
每次都走 random branch |
所以 epsilon 不是拿來和 Q-value 比大小的門檻。它只是在控制:這一次到底要走 random 還是 greedy。
一次 action selection 的資料流如下:
圖中的重點不是某一局 Breakout 發生了什麼,而是每一次決策都會先經過同一個分岔:要嘛探索,要嘛利用,最後才把選出的 action 交給 environment。
如果兩個 action 剛好有一樣大的最高 Q-value,這個版本固定選索引比較前面的 action。這樣同樣的 Q-values 每次都會得到同一個 greedy action,不會因為 tie 而出現額外的隨機性。
現在再回頭看實際程式跑出來的結果會比較有意義。
固定 Q-values:
[1.0, 2.0, 5.0, 3.0]
其中 action 2 是 greedy action。使用 seed 42 後:
epsilon = 0, samples = 20
greedy action index : 2
random decisions : 0
greedy decisions : 20
action 2 : 20
epsilon = 1, samples = 20
greedy action index : 2
random decisions : 20
greedy decisions : 0
action counts : [6, 4, 6, 4]
epsilon = 0.1, samples = 1000
random decisions : 96
greedy decisions : 904
action counts : [29, 31, 925, 15]
前兩組很好理解:
epsilon = 0 時,20 次全部走 greedy;epsilon = 1 時,20 次全部走 random。比較有意思的是 epsilon = 0.1。
理論上每一次有 10% 機率走 random branch,所以 1,000 次裡大約會有 100 次 random。實際得到 96 次,這只是有限次隨機抽樣下很正常的波動。
但還有一個更重要的細節:random branch 不代表一定會選到「非 greedy action」。
這裡一共有四個 action。即使這次決定要 random,仍然有 1/4 的機率剛好又抽到 action 2。
所以當 epsilon = 0.1 時,action 2 最後被執行的理論機率不是 90%,而是:
greedy branch 選到 action 2
= 90%
random branch 又剛好抽到 action 2
= 10% × 1/4
= 2.5%
合計
= 92.5%
而這次實際結果剛好是:
action 2 = 925 / 1000 = 92.5%
這能幫我們分清楚兩件常被混在一起的事:
epsilon 控制的是 random branch 的機率;固定 epsilon = 0.1 當然可以運作,但訓練早期和訓練後期其實不需要同樣程度的探索。
訓練剛開始時,Agent 幾乎還不知道哪些 action 比較好,所以需要多試不同選擇。隨著經驗增加,才逐漸把更多決策交給目前學到的 Q-values。
因此 Day 10 使用 linear epsilon schedule(線性 epsilon 排程),讓 epsilon 隨著 environment step 逐漸下降。
這裡的 environment step 指的是 Agent 執行一次 action,環境回傳一次 transition。使用 step 而不是 episode,是因為每一局 Breakout 的長度可能不同;用互動次數來衡量探索進度會比較一致。
這次圖表使用:
start = 0.9
end = 0.05
decay_steps = 1000
也就是從 90% 的 random branch 機率,逐步下降到 5%。
幾個時間點可以直接看出變化:
| environment step | epsilon | random branch | greedy branch |
|---|---|---|---|
| 0 | 0.900 | 90% | 10% |
| 500 | 0.475 | 47.5% | 52.5% |
| 1,000 | 0.050 | 5% | 95% |
| 1,200 | 0.050 | 5% | 95% |
在 step 0 時,Agent 幾乎都在探索;到了 step 1,000,epsilon 已經降到 0.05,之後就維持在這個最低值,不再繼續下降。
這張圖只能告訴我們「探索比例怎麼變」,不能證明模型已經學會 Breakout。0.9 → 0.05 也是這次示範使用的設定,不是唯一正確答案;真正適合多快下降,還要等完整訓練後再用 evaluation 比較。
Day 9 的 Replay Buffer 負責保存 transition,但它不會自己創造新的經驗。資料仍然來自 Agent 和 Breakout 的互動:Agent 先選 action,environment 執行後產生 transition,最後才把這筆 transition 放進 Replay Buffer。
因此 epsilon-greedy 決定的不只是「Agent 下一步按哪個按鍵」,它也會間接決定 Replay Buffer 最後收集到什麼資料。
如果 Agent 從來沒有探索某些 action,那些 action 對應的遊戲結果就很少出現在 Replay Buffer 裡。後面的 DQN 即使能反覆抽樣 Replay Buffer,也無法憑空學到一段從來沒有被收集過的互動。
所以 Day 9 和 Day 10 解決的是兩個不同但連在一起的問題:
還有一個很容易混淆的設定叫 replay warm-up,也常寫成 learning_starts。
它和 epsilon schedule 控制的是完全不同的事情:
| 機制 | 它回答的問題 |
|---|---|
| epsilon schedule | 這一步要用 random 還是 greedy 方式選 action? |
| replay warm-up | Replay Buffer 累積多少資料後,才開始更新模型? |
換句話說,epsilon schedule 影響的是資料怎麼收集;replay warm-up 影響的是什麼時候開始拿資料訓練。
因此「前 10,000 步先不要訓練」不等於「前 10,000 步一定完全 random」。兩個條件可以分開設定,真正把它們串進完整 training loop 會留到 Day 12。
DQN 產生 Q-values 之後,epsilon-greedy 只是根據這些值決定下一個 action。這個過程是在使用目前的模型做決策,不是在更新模型。
PyTorch 訓練時會保存一些中間計算,之後才能根據 loss 反向調整模型參數;這個用來追蹤計算關係的機制,就是 gradient graph。
但 action selection 不需要之後回頭算梯度,所以這裡會使用 torch.no_grad(),避免保留不必要的訓練資訊。
真正需要 gradient 的時候,是之後從 Replay Buffer 抽出一批 transition,計算模型的預測誤差,再用這個誤差更新 network。那會在後面的 training loop 才正式串起來。
Epsilon-greedy 本身有隨機性。如果每次實驗都使用不同亂數序列,就很難判斷結果差異到底來自:
因此這個專案會明確傳入隨機數產生器(random number generator),並使用固定 seed 做可重現的 inspection。
相同的 Q-values、epsilon、action 數量與 seed,會產生相同的 random / greedy 決策序列。這讓我們比較容易驗證程式行為。
但 seed 只負責讓隨機過程可重現,不會讓錯誤的 Q-value 自動變正確。
走到這裡,Agent 已經有一條比較完整的互動資料路徑:DQN 先根據目前 state 產生 Q-values,epsilon-greedy 再決定這一步要探索還是利用,選出的 action 交給 Breakout,產生的新 transition 最後再進入 Replay Buffer。
Day 10 解決的是 action selection,還沒有真正開始訓練 DQN。
真正開始更新 network 後,還會遇到另一個問題:模型一邊學,拿來當作「學習答案」的估計值也跟著一直變,這會讓訓練變得不穩定。
Day 11 會用 Target Network 把這個問題拆開。