§01學習重點
- 說出強化學習與監督式學習的三個結構性差異:回饋是評價不是正解、決策是序列而非單發、行動會改變環境
- 把一個金融決策問題(部位出清、最適下單)翻譯成 MDP 五要素:狀態、行動、轉移機率、報酬、折現因子
- 區分政策、回報、狀態價值函數 V 與行動價值函數 Q,並逐項拆解 Bellman 期望方程與最適方程
- 在模型已知時用動態規劃求解:政策評估、政策迭代、價值迭代,並解釋收斂背後的收縮映射直覺
- 在模型未知時用抽樣求解:說出蒙地卡羅與時序差分(TD)各自用什麼代替期望、各有什麼偏差與方差性質
- 寫出 SARSA 與 Q-learning 的更新式,用「實際的自己 vs 假想中貪婪的自己」解釋 on-policy 與 off-policy
- 解釋 ε-greedy 的探索–利用取捨,以及為什麼金融裡的探索特別昂貴、批次(離線)學習特別重要
§02課程內容
一、從預測到決策:監督式學習止步之處
第 1 章第八節埋過一條伏筆:目前為止的模型都是「旁觀者」——預測違約機率、預測下一秒的價格方向,預測本身不改變世界。前八章我們把旁觀者的本事練到了頂:橫斷面的監督式學習(第一部)、時間序列的動態建模(第二部)。從這一章起進入第三部,角色換了——模型要下場。
下場之後,監督式學習的三個隱含前提逐一失效。
第一,回饋從「正解」變成「評價」。 監督式學習像有老師改考卷:每筆訓練資料都附標準答案,模型錯在哪、該往哪修,梯度說得明明白白。決策問題沒有這種老師。你把一張大單拆成五筆下出去,市場只會告訴你這次的成交成本是多少——它不會告訴你「正確的拆法其實是八筆」。你拿到的是分數,不是答案;而且分數高不代表這是最好的做法,只代表它比某些做法好。從答案裡學習和從分數裡學習,是兩種根本不同的學習。
第二,決策是序列,帳要算總的。 今天少賣一點,庫存留到明天,明天的處境就不同——每個動作都改變後面所有動作的舞台。單筆決策的好壞不能只看當下報酬,要看它牽動的整條路徑。這帶來強化學習最棘手的難題之一:功勞分配。一整季的損益出來了,功勞(或黑鍋)該記在哪一步的頭上?
第三,行動改變環境。 這是金融場景最尖銳的一刀。你的市價大單會把限價簿吃穿,價格被你自己推走——市場衝擊;你的放貸模型影響了誰拿得到貸款,也就改變了未來違約資料的分布。監督式學習假設資料分布是固定的、與你無關的;一旦你的輸出會回頭改寫輸入的分布,這個假設就塌了。強化學習從第一天起就把「你在環境裡、環境會回應你」寫進框架。
比喻: 在夜市擺攤賣鹽酥雞,沒有任何人會告訴你「正確的定價是多少」——你只看得到今晚的營收。定價 70 元生意冷清,60 元大排長龍,但排隊排太長隔壁攤就降價搶客,你的定價改變了整條街的行情。營收是評價、不是正解;每晚的定價牽動下一晚的客流;而你的動作會引來環境的反應。攤販學定價的方式——試、看分數、修正、再試——就是強化學習的原型。
整個框架就是這個迴圈:代理人(agent,你的交易策略)觀察狀態,選一個行動丟給環境(市場),環境回你一個報酬和新狀態,週而復始。代理人的目標不是猜對什麼,而是讓長期累積的報酬最大。這一章把迴圈的每個零件講清楚,並在一個小到可以手算、卻五臟俱全的金融問題上把全部演算法實際跑一遍。
二、把決策問題寫成數學:MDP 五要素
強化學習的標準數學骨架叫馬可夫決策過程(Markov Decision Process, MDP),由五個零件組成,記作 \((\mathcal{S}, \mathcal{A}, P, R, \gamma)\)。逐個翻譯成交易室的語言:
| 零件 | 數學身分 | 金融對應(以下單執行為例) | |---|---|---| | 狀態 \(\mathcal{S}\) | 環境的完整描述 | 剩餘庫存、限價簿形狀、剩餘時間 | | 行動 \(\mathcal{A}\) | 代理人的選項集合 | 市價單、限價單、單量、等待 | | 轉移機率 \(P(s'\mid s,a)\) | 環境怎麼回應 | 成交與否、價格怎麼動——通常未知 | | 報酬 \(R\) | 單步評分 | 成交損益 − 衝擊成本 − 持有成本 | | 折現因子 \(\gamma\in[0,1)\) | 未來的匯率 | 資金成本、風險耐受、時間偏好 |
「馬可夫」三個字是對狀態的一個要求:狀態要裝得夠飽,飽到「知道現在的狀態,就不需要再翻歷史」——下一步的分布只由當下狀態與行動決定。這不是對世界的斷言,而是對你怎麼定義狀態的紀律:如果只拿「最新成交價」當狀態,昨天的量價形態顯然還藏著資訊,馬可夫性不成立;把該裝的資訊裝進狀態向量(庫存、簿況、波動率環境……),馬可夫性就近似成立。裝不飽的情形叫部分可觀測(POMDP)——金融裡其實是常態,第九節會回來談這個誠實的難題。
整章的實驗都用同一個玩具問題——四股庫存的出清 MDP。你手上有 4 股要出清(想像單位是「千張」也行),每期可以選三種行動:市價賣 1 股(M1,立刻成交,但吃掉價差後每股只拿 0.97);市價賣 2 股(M2,一次倒兩股衝擊更大,每股 0.935);掛限價賣 1 股(L1,價格漂亮拿 1.02,但只有六成機率成交)。沒賣掉的庫存每期每股付 0.02 的持有成本(庫存風險的代價),折現因子 \(\gamma=0.95\)。庫存歸零,遊戲結束。麻雀雖小:市價單與限價單的取捨、衝擊成本、庫存風險、隨機成交——最適下單問題的骨架全在裡面。
三、政策、回報與價值函數:V 與 Q
代理人的行為由政策(policy)\(\pi\) 描述:每個狀態該做什麼。它可以是確定的(狀態 s 一律做 a),也可以是隨機的(\(\pi(a\mid s)\) 給每個行動一個機率)。學習的目標是找到讓長期成績最好的政策 \(\pi^*\)。
「長期成績」要先定義清楚。從時點 t 起算的回報(return)是折現後的報酬總和:
逐項拆解:\(R_{t+1}\) 是做完這一步立刻拿到的報酬;\(\gamma\) 把一期之後的報酬打一次折,兩期之後打兩次折——\(\gamma=0.95\) 意味著大約 14 期之後的報酬價值折半,越遠的未來越不值錢;級數收斂靠 \(\gamma<1\) 保證,所以「無限期的總帳」是一個有限的數。折現在金融裡不是抽象假設而是本行語言:資金有成本、承諾有風險,明天的一塊錢本來就小於今天的一塊錢。
有了回報,就能給「處境」和「處境+動作」標價。狀態價值函數 \(V^\pi(s)\):從狀態 s 出發、之後一路照政策 \(\pi\) 走,回報的期望值。行動價值函數 \(Q^\pi(s,a)\):在狀態 s 先做行動 a、之後再照 \(\pi\) 走的期望回報。兩者的關係:\(V^\pi(s) = \sum_a \pi(a\mid s)\, Q^\pi(s,a)\)——V 是 Q 在政策下的加權平均。
比喻: 下棋的人都懂這兩個數的差別。V 是盤面的形勢分:「這個局面,讓高手接手下去,勝算七成」——只評局面,不指定下一手。Q 是這一手棋的分數:「這個局面走馬三進四,之後讓同一位高手接手,勝算七成三」——把下一手也算進去。棋士挑棋就是在比各步的 Q;而所謂形勢大好,就是存在某一手讓 Q 特別高。強化學習裡代理人真正需要的是 Q——知道每手棋的分數,最好的政策就是每次挑分數最高的那手。
為什麼 Q 比 V 更好用?有了 \(Q^*\)(最適政策的 Q),選行動不需要知道環境模型——直接挑 \(\arg\max_a Q^*(s,a)\) 就好;只有 \(V^*\) 的話,你還得知道轉移機率才能比較各行動的後果。這個差別在「模型未知」的金融世界裡是決定性的,它是第七節 Q-learning 的存在理由。
四、Bellman 方程:把長期問題摺成一步
價值函數的定義裡藏著一個無限長的總和,直接算不可行。Bellman 方程是整個領域的軸心觀察:回報的定義是遞迴的——\(G_t = R_{t+1} + \gamma G_{t+1}\),今天的總帳等於今天的進帳加上打折後的明天總帳。對它取期望,就得到Bellman 期望方程:
逐項拆解:外層 \(\sum_a \pi(a\mid s)\) 是「政策擲骰選行動」的平均;內層 \(\sum_{s'} P(s'\mid s,a)\) 是「環境擲骰決定去向」的平均;中括號裡是一步的完整帳——立即報酬 \(r\) 加上打折後、站在新狀態 \(s'\) 的身價 \(V^\pi(s')\)。整條式子把「無限長的未來」摺成「一步之後查表」:未來的一切都已經濃縮在 \(V^\pi(s')\) 這個數字裡。Q 版本同理,只是第一步的行動已指定:\(Q^\pi(s,a) = \sum_{s'} P(s'\mid s,a)[\,r + \gamma V^\pi(s')\,]\)。
以上描述「照著給定政策走」的帳。若問的是「最好能多好」,把政策的加權平均換成挑最大,就得到Bellman 最適方程:
逐項拆解:\(V^*\) 版在根部取 \(\max\)——當下就挑最好的行動;\(Q^*\) 版把 \(\max\) 推遲到下一狀態——先做指定的 a,之後每一步都挑最好的 \(a'\)。注意 \(\max\) 讓方程變非線性:期望方程是線性方程組、可以直接解,最適方程只能迭代逼近——這個差別決定了下一節兩條演算法路線的分工。
圖上這棵倒立的樹叫備份圖:值從樹葉「備份」回樹根。左邊是期望備份——各枝加權平均;右邊是最適備份——只挑最好的那枝。圖中的數字不是示意,是接下來兩節用出清 MDP 實跑的結果:庫存 2 股時,三個行動的 \(Q^*\) 分別是 1.8755(M1)、1.8520(L1)、1.8700(M2),取最大得 \(V^*(2)=1.8755\);而「一律掛限價慢慢磨」政策的 \(V(2)=1.8376\)——差額 0.0379 就是換成最適政策能多賺的錢。
五、模型已知時:動態規劃三兄弟
先考慮一個奢侈的情境:轉移機率 \(P\) 與報酬結構全部已知(出清 MDP 正是如此,因為環境是我們自己造的)。這時 Bellman 方程不只是性質描述,而是演算法配方,統稱動態規劃(DP)。三兄弟如下。
政策評估(policy evaluation):給定政策 \(\pi\),求 \(V^\pi\)。期望方程是線性方程組,狀態少可直接解;也可以把方程當更新規則反覆掃過所有狀態,迭代收斂到 \(V^\pi\)。
政策迭代(policy iteration):評估與改進輪流做——先評估目前政策得到 \(V^\pi\),再對每個狀態「貪婪化」:改選讓 \(r + \gamma V^\pi(s')\) 期望最大的行動。可以證明新政策不會更差、通常嚴格更好;反覆「評估→貪婪化」,有限步內收斂到 \(\pi^*\)。
價值迭代(value iteration):不等評估做完,直接拿最適方程當更新規則——每輪對每個狀態取 \(\max\) 更新一次 V。它能收斂的理由值得記住:最適方程右邊那個運算子是收縮映射——任兩個價值函數經它一刷,最大差距至少縮成 \(\gamma\) 倍(sup-norm 意義下),所以誤差以幾何速度歸零,且不動點唯一。白話:每迭代一次,你和正確答案的距離至少打 95 折,而且複利地打下去。
在出清 MDP 上實跑價值迭代:
import numpy as np
GAMMA = 0.95 # 折現因子
FILL = 0.6 # 限價單成交機率
HOLD = 0.02 # 每股每期持有成本
P_MKT1, P_MKT2, P_LMT = 0.97, 0.935, 1.02 # 每股成交價
def actions(q):
"""庫存 q 時的合法行動:M1 市價賣1、M2 市價賣2、L1 限價賣1"""
acts = []
if q >= 1: acts += ["M1", "L1"]
if q >= 2: acts += ["M2"]
return acts
def outcomes(q, a):
"""回傳 [(機率, 即時報酬, 下一庫存), ...]"""
if a == "M1": fills = [(1.0, 1, P_MKT1)]
elif a == "M2": fills = [(1.0, 2, P_MKT2)]
else: fills = [(FILL, 1, P_LMT), (1 - FILL, 0, 0.0)] # L1
return [(p, n * px - HOLD * (q - n), q - n) for p, n, px in fills]
V = np.zeros(5) # V[0..4],庫存 0 是吸收態、價值恆為 0
for it in range(1, 200):
V_new = np.zeros(5)
for q in range(1, 5):
V_new[q] = max(sum(p * (r + GAMMA * V[q2]) for p, r, q2 in outcomes(q, a))
for a in actions(q))
err = float(np.abs(V_new - V).max())
V = V_new
if it in (1, 2, 5, 10, 20, 30) or err < 1e-10:
print(f"迭代 {it:>3d}|V*(4)={V[4]:.4f}|sup-norm 誤差 = {err:.2e}")
if err < 1e-10:
break
policy = {q: max(actions(q), key=lambda a: sum(p * (r + GAMMA * V[q2])
for p, r, q2 in outcomes(q, a))) for q in range(1, 5)}
print("V* =", np.round(V, 4))
print("最適政策:", policy)
# 輸出:
# 迭代 1|V*(4)=1.8300|sup-norm 誤差 = 1.87e+00
# 迭代 2|V*(4)=3.6065|sup-norm 誤差 = 1.78e+00
# 迭代 5|V*(4)=3.6112|sup-norm 誤差 = 8.92e-04
# 迭代 10|V*(4)=3.6117|sup-norm 誤差 = 7.07e-06
# 迭代 20|V*(4)=3.6117|sup-norm 誤差 = 4.44e-10
# 迭代 22|V*(4)=3.6117|sup-norm 誤差 = 6.41e-11
# V* = [0. 0.9742 1.8755 2.7755 3.6117]
# 最適政策: {1: 'L1', 2: 'M1', 3: 'M2', 4: 'M2'}兩個看點。收斂速度:22 輪就把誤差壓到 \(10^{-10}\),且第 5 輪之後每輪誤差約乘 0.38——比理論上限 \(\gamma=0.95\) 快得多,因為只有限價單不成交那一枝(機率 0.4)會留在原狀態,有效收縮率約 \(0.4\times 0.95=0.38\)。政策本身:最適政策是一道漂亮的梯度——庫存 4、3 股時用市價單一次倒兩股(M2,衝擊成本痛,但庫存風險更痛),剩 2 股改市價單一股一股出(M1),最後 1 股才掛限價單慢慢磨價(L1,這時持有成本已小,值得賭那六成機率的好價格)。「大部位求快、小部位求好價」——實務交易員的直覺,從五要素和一條遞迴方程裡自己長了出來。這正是把問題寫成 MDP 的回報:你放進去的是成本結構,拿出來的是行為。
那為什麼不到此為止?因為 DP 的前提在真實金融裡幾乎永遠不成立:你不知道 \(P\)。限價單在真實限價簿裡的成交機率、大單下去價格怎麼移動——這些是市場微結構最難的問題,寫得出模型也未必敢信。模型未知時怎麼辦?兩條路:把模型學出來再做 DP(model-based),或者跳過模型、直接從互動經驗學價值與政策(model-free)。本章走第二條路,它是 RL 的主流。
六、模型未知時(上):MC 與 TD,用樣本代替期望
Bellman 方程裡到處是期望值,而期望值的萬用平價替代品是樣本平均。model-free 方法的全部秘密就這一句:拿不到機率分布,就拿抽樣結果來平均。差別只在「什麼時候平均」。
蒙地卡羅(MC)法:跑完一整集(episode),把實際發生的回報 \(G_t\) 從尾巴往前結算,拿它當 \(V(s)\) 的一筆樣本,用樣本平均更新。它是無偏的——\(G_t\) 的期望就是真的 \(V^\pi(s)\)——但方差大(一整集的隨機性全疊在一筆樣本裡),而且必須等集結束才能更新。
時序差分(TD)法:不等結局,每走一步就用 Bellman 遞迴「先斬後奏」地更新:
逐項拆解:\(R_{t+1} + \gamma V(S_{t+1})\) 是走了一步之後對回報的新估計——實拿的報酬加上下一站的現有身價(拿自己的估計更新自己的估計,術語叫自助,bootstrapping);減去舊估計 \(V(S_t)\) 得到 TD 誤差 \(\delta_t\)——「驚訝的量」:正的 \(\delta\) 表示這一步的遭遇比預期好,該上修;\(\alpha\) 是步長,決定每次修多少。TD 的代價是有偏——早期的 \(V(S_{t+1})\) 本身就不準,用它當靶子會把誤差傳染出去;換來的是方差小、能線上逐步更新、不需要等集結束(對接近連續營運的交易系統是剛需)。
比喻: 兩種記帳性格。MC 是月結派:整個月刷卡不看帳,月底帳單來了才總結算「這個月原來花了這麼多」,據此修正對自己開銷的估計——結算絕對真實,但一個月只學一次,而且單月的大額意外(婚禮紅包)會讓估計大幅跳動。TD 是日結派:每天睡前用「今天實際花的+對月底的最新預估」滾動修正——每天都在學、曲線平滑,但如果你對月底的預估本來就偏,每天的修正都會被這個偏差帶著走,要一段時間才會自我糾正。
在出清 MDP 上做政策評估實驗——被評估的是「不論庫存多少一律掛限價單」的慢慢磨政策(不是最適政策,評誰是我們指定的)。因為環境是自己造的,可以先解出精確答案當對照:
import numpy as np
rng = np.random.default_rng(9)
GAMMA, FILL, HOLD, P_LMT = 0.95, 0.6, 0.02, 1.02
# 被評估的政策:不論庫存多少,一律掛限價單賣 1 股(「慢慢磨」政策)
# 精確解:V(q) = 0.6[1.02 - 0.02(q-1) + γV(q-1)] + 0.4[-0.02q + γV(q)]
V_exact = np.zeros(5)
for q in range(1, 5):
V_exact[q] = (FILL * (P_LMT - HOLD * (q - 1)) - (1 - FILL) * HOLD * q
+ FILL * GAMMA * V_exact[q - 1]) / (1 - (1 - FILL) * GAMMA)
print("精確 V^pi =", np.round(V_exact, 4))
def episode():
"""跑一集:從庫存 4 出發掛限價單到出清,回傳 [(狀態, 報酬), ...]"""
q, traj = 4, []
while q > 0:
if rng.random() < FILL:
traj.append((q, P_LMT - HOLD * (q - 1))); q -= 1
else:
traj.append((q, -HOLD * q))
return traj
V_mc, n_mc = np.zeros(5), np.zeros(5) # MC:首訪、樣本平均
V_td, ALPHA = np.zeros(5), 0.05 # TD(0):常數步長
for ep in range(1, 10001):
traj = episode()
G, ret = 0.0, {} # 從後往前算各狀態的回報
for s, r in reversed(traj):
G = r + GAMMA * G
ret[s] = G # 迴圈結束時留下的是首訪回報
for s, G in ret.items():
n_mc[s] += 1
V_mc[s] += (G - V_mc[s]) / n_mc[s]
for i, (s, r) in enumerate(traj): # TD(0):逐步自助更新
s2 = traj[i + 1][0] if i + 1 < len(traj) else 0
V_td[s] += ALPHA * (r + GAMMA * V_td[s2] - V_td[s])
if ep in (100, 1000, 10000):
print(f"{ep:>5d} 集|MC V(4)={V_mc[4]:.4f}|TD V(4)={V_td[4]:.4f}"
f"|精確={V_exact[4]:.4f}")
# 輸出:
# 精確 V^pi = [0. 0.9742 1.8376 2.5991 3.2669]
# 100 集|MC V(4)=3.2645|TD V(4)=3.0250|精確=3.2669
# 1000 集|MC V(4)=3.2798|TD V(4)=3.3029|精確=3.2669
# 10000 集|MC V(4)=3.2721|TD V(4)=3.2684|精確=3.2669兩條路都通到精確值 3.2669:一萬集後 MC 收在 3.2721、TD 收在 3.2684。過程的性格差異也如理論所料:MC 一百集就落在 3.2645(無偏,樣本平均直奔靶心),TD 一百集還在 3.0250——它正拖著「下一站身價還沒估準」的自助偏差暖機,之後才慢慢貼上來。另外注意:慢慢磨政策的 \(V^\pi(4)=3.2669\),比上一節最適政策的 \(V^*(4)=3.6117\) 少了 0.34——一味求好價、無視庫存風險,四股要磨平均快七期,持有成本與折現把價格優勢吃掉還倒貼。
七、模型未知時(下):SARSA、Q-learning 與探索的代價
政策評估只是踏板,真正要的是控制:找 \(\pi^*\)。把 TD 的思想從 V 搬到 Q(第三節說過,模型未知時 Q 才能直接選行動),得到兩個只差一個字、性格迥異的演算法。
SARSA(on-policy):走一步,看自己實際接著做了哪個行動 \(A_{t+1}\),用它的 Q 值當靶子:
Q-learning(off-policy):不管自己實際做了什麼,一律用下一狀態最好的行動當靶子:
逐項拆解兩式的唯一差別——靶子裡那一項:SARSA 用 \(Q(S_{t+1}, A_{t+1})\),評的是「實際的自己」——包含探索時的隨機亂步;Q-learning 用 \(\max_{a'} Q(S_{t+1}, a')\),評的是「假想中從此刻起完全貪婪的自己」。所以 SARSA 學到的是「我這種會手滑的人,這麼走值多少」,Q-learning 學到的是「完美執行的話,這麼走值多少」——即使產生資料的行為政策根本不完美。這就是 on-policy/off-policy 之分:學習目標跟不跟著行為政策走。off-policy 是一種解耦:用誰的經驗都能學最適政策——別人的、過去的、探索時亂走的,這個性質等一下會變成金融應用的命脈。
但兩者都得面對同一個前置問題:探索與利用。一直挑當前 Q 最高的行動(利用),沒試過的行動的 Q 永遠停在初始值,錯的永遠不會被發現;一直亂試(探索),又把已知的好行動晾在一邊。最簡單的折衷是 ε-greedy:以機率 \(1-\varepsilon\) 挑最好的,以機率 \(\varepsilon\) 隨機試。
比喻: 中午吃什麼的兩難。全然利用=每天都吃巷口那家熟悉的老店,穩,但你永遠不會知道兩條街外上個月新開的那家其實好吃得多;全然探索=每天換一家沒吃過的,見多識廣,但平均伙食水準被大量地雷店拖垮。ε-greedy 就是「十次有九次吃老店、一次試新店」。在餐廳問題裡試錯的代價是一頓難吃的午飯;在金融裡,每一次探索都是用真錢下一張可能很蠢的單——這就是為什麼金融 RL 對探索特別小氣。
實跑:把 Q-learning 丟進出清 MDP,agent 看不到轉移機率,只能透過 step() 這個黑盒與環境互動。因為第五節已解出精確 \(Q^*\),可以全程監測它離答案多遠:
import numpy as np
rng = np.random.default_rng(21)
GAMMA, FILL, HOLD = 0.95, 0.6, 0.02
P_MKT1, P_MKT2, P_LMT = 0.97, 0.935, 1.02
def actions(q):
return (["M1", "L1"] if q >= 1 else []) + (["M2"] if q >= 2 else [])
def outcomes(q, a):
if a == "M1": fills = [(1.0, 1, P_MKT1)]
elif a == "M2": fills = [(1.0, 2, P_MKT2)]
else: fills = [(FILL, 1, P_LMT), (1 - FILL, 0, 0.0)]
return [(p, n * px - HOLD * (q - n), q - n) for p, n, px in fills]
def step(q, a):
"""環境黑盒:抽樣一次轉移。agent 只看得到這個函式的輸出"""
outs = outcomes(q, a)
i = rng.choice(len(outs), p=[p for p, _, _ in outs])
return outs[i][1], outs[i][2]
# 對照組:用第五節的方法迭代出精確 Q*(agent 不知道這個答案)
Q_star = {q: {a: 0.0 for a in actions(q)} for q in range(1, 5)}
for _ in range(300):
for q in range(1, 5):
for a in actions(q):
Q_star[q][a] = sum(p * (r + GAMMA * (max(Q_star[q2].values()) if q2 else 0.0))
for p, r, q2 in outcomes(q, a))
# 表格式 Q-learning:只靠抽樣互動學 Q,不碰轉移機率
Q = {q: {a: 0.0 for a in actions(q)} for q in range(1, 5)}
N = {q: {a: 0 for a in actions(q)} for q in range(1, 5)}
EPS = 0.2 # ε-greedy 探索率
for ep in range(1, 20001):
q = int(rng.integers(1, 5)) # 隨機初始庫存,保證覆蓋
while q > 0:
acts = actions(q)
if rng.random() < EPS: # 探索:隨機挑一個行動
a = acts[int(rng.integers(len(acts)))]
else: # 利用:挑目前 Q 值最高的
a = max(acts, key=lambda x: Q[q][x])
r, q2 = step(q, a)
N[q][a] += 1
alpha = 1.0 / N[q][a] ** 0.7 # 隨訪問次數衰減的步長
target = r + GAMMA * (max(Q[q2].values()) if q2 else 0.0)
Q[q][a] += alpha * (target - Q[q][a]) # off-policy:target 用 max
q = q2
if ep in (10, 100, 1000, 5000, 20000):
gap = max(abs(Q[q][a] - Q_star[q][a]) for q in range(1, 5) for a in actions(q))
print(f"{ep:>5d} 集|max|Q - Q*| = {gap:.4f}")
print("學到的政策:", {q: max(actions(q), key=lambda a: Q[q][a]) for q in range(1, 5)})
print("q=1 精確 vs 學到:", {a: (round(Q_star[1][a], 4), round(Q[1][a], 4))
for a in actions(1)})
# 輸出:
# 10 集|max|Q - Q*| = 3.6117
# 100 集|max|Q - Q*| = 0.7633
# 1000 集|max|Q - Q*| = 0.0931
# 5000 集|max|Q - Q*| = 0.0219
# 20000 集|max|Q - Q*| = 0.0101
# 學到的政策: {1: 'L1', 2: 'M1', 3: 'M2', 4: 'M2'}
# q=1 精確 vs 學到: {'M1': (0.97, 0.97), 'L1': (0.9742, 0.9749)}值得慢讀的三件事。第一,agent 從頭到尾沒看過機率——不知道限價單六成成交、不知道任何成交價——兩萬集互動後 Q 表離 \(Q^*\) 不到 0.01,學到的政策與第五節動態規劃解出的 \(\pi^*\) 一字不差。第二,看 q=1 那行:M1 與 L1 的真實 Q 值只差 0.0042(0.97 對 0.9742),純靠抽樣把這麼細的差距排對順序,靠的是步長衰減(早期學得快、後期學得穩)與足夠的探索(ε=0.2 保證 L1 被試了夠多次)。第三,錯開步長與探索任何一個,這個排序就會錯——作業二會讓你親手把它弄壞。
八、狀態空間爆炸之後:函數逼近、批次學習與深度 RL
出清 MDP 只有 4 個狀態、9 個合法狀態–行動對,一張表就裝下了。真實問題不是這樣:狀態一旦包含連續的價格、部位、簿況,組合是天文數字,表格式方法當場出局——不是算力問題,是你永遠不可能每格都訪問過。
出路是函數逼近:不再一格一格記 Q,改用參數化函數 \(Q(s,a;\theta)\)——線性模型、樹、或神經網路——讓相似的狀態共享估計,用第一部學過的監督式機器把 TD 靶子當「偽標籤」去擬合。參數化之後,Q-learning 的更新變成對 \(\theta\) 的梯度步;當 \(Q(s,a;\theta)\) 是深度網路,就是深度強化學習(deep RL),2015 年 DQN 在 Atari 遊戲上以像素輸入練到人類水準是其成名作。但要誠實:函數逼近、自助、off-policy 三件事疊在一起,收斂保證大多失效,訓練以不穩定聞名——工程補丁(經驗回放、目標網路)是在馴服這個本質問題,不是消滅它。
對金融讀者,比「更深的網路」重要得多的是另一個維度:批次(batch)/離線學習。前面的演算法都假設能與環境即時互動、邊試邊學——線上學習。金融裡這幾乎是奢望:沒有人會讓一個半生不熟的 agent 拿真錢在市場裡探索。現實是你手上有的是歷史資料:過去的下單紀錄、成交回報、簿況快照——一堆別人(或過去的自己)的 \((s, a, r, s')\) 四元組。而第七節講的 off-policy 性質恰好是為此而生:Q-learning 不要求資料是誰產生的,把歷史四元組反覆餵給它(batch-mode Q-learning、fitted Q iteration,線性版的經典是 LSPI),就能在完全不下一張新單的前提下,從死資料裡萃取出活政策。這是 RL 之於金融最務實的一條路,也是下一章選擇權定價與組合管理應用的方法底座。當然,離線學習有自己的暗礁:資料裡沒出現過的狀態–行動組合,你學到的 Q 純屬外插——資料覆蓋不到的地方,政策再自信都是幻覺。
九、誠實限界:把醜話說在前面
按本課慣例,工具講完,講它會在哪裡騙你。
樣本效率。出清 MDP 區區 9 個 Q 值花了兩萬集才收斂到 0.01——RL 是出了名的吃資料。遊戲裡可以無限重開,市場不行:日頻資料一年只給你 252 筆,你想學的環境卻高維又充滿雜訊。金融 RL 的實務幾乎都繞不開模擬器或離線資料,而模擬器本身就是一個模型——model-free 方法吃著 model-based 的飯,模型錯了照樣一起錯,這筆帳要記得。
非平穩。MDP 假設轉移機率恆定,但市場的 \(P\) 會換:波動制度切換、參與者結構改變、你的策略被同行學走。辛苦收斂的 \(Q^*\) 是對那個市場的最適解,市場換了臉,舊 Q 表就是刻舟求劍。第 7 章的制度切換模型與這裡是同一個警覺的兩種表達。
報酬設計。報酬函數是你告訴 agent「什麼叫好」的唯一管道,而 agent 只會忠於字面。只給損益、不扣風險,它會學成扛著尾部風險賺小錢的策略——直到爆掉那天;忘了寫進交易成本,它會學出瘋狂換手的政策。RL 不會讀你的心,只會鑽你的漏;報酬函數寫錯,演算法越強、錯得越快。
部分可觀測。你以為的狀態往往不是完整狀態:限價簿看得到五檔,看不到冰山單;看得到價格,看不到對手的庫存。狀態裝不飽,馬可夫性破功,所有依賴它的收斂論證都要打折。務實對策是把狀態向量餵飽(加入歷史特徵、微結構訊號),並對「還是不飽」保持敬畏。
即便如此,方向是清楚的:金融的核心問題——執行、對沖、配置——本質上都是序列決策,強化學習是第一個把這件事原生寫進數學的框架。下一章就把這套語言用上真問題:從 Black–Scholes 的 RL 重寫到動態投資組合。
§03原書對照
本章對應原書第九章。特別說明:本課可核對的原書素材僅章首導言兩頁(印刷頁 pp.279–280),以下其餘節名依原書草稿版目錄整理,無出版頁碼可核。導言將本章定位為 Part 3 的方法地基:先建立 MDP 與經典動態規劃,再過渡到 RL 與各種近似解法,行文刻意偏工程風格,並以資產管理與交易的迷你案例收尾;章目標五條——熟悉 MDP、理解 Bellman 方程與經典動態規劃、掌握 RL 與近似求解的思想、分清 on-policy 與 off-policy、建立 RL 在資管與交易應用的洞見——本課全數覆蓋,關鍵計算概念原書點名為「greediness, batch learning, and Q-learning」(p. 279)。導言另把 RL 與監督式/非監督式學習並列比較,強調老師角色的差異與「部分回饋」的性質(pp.279–280),與本課第一節同題但敘事線不同。依草稿目錄,原書該章結構為:第 2 節 RL 的元素(報酬、價值與政策函數、可觀測與部分可觀測環境);第 3 節 MDP(決策政策、價值函數與 Bellman 方程、最適政策與 Bellman 最適性);第 4 節動態規劃方法(政策評估、政策迭代、價值迭代);第 5 節 RL 方法(蒙地卡羅、policy-based 學習、時序差分、SARSA 與 Q-learning、隨機逼近與批次 Q-learning、連續空間的函數逼近、批次 Q-learning、LSPI、深度 RL)。對照之下,原書有、本課未展開或僅點到的內容包括:其一,部分可觀測環境(POMDP)的正式處理——本課只在第二、九節做概念性提醒;其二,policy-based 方法(直接對政策做最佳化而不繞道價值函數)——本課完全走 value-based 路線;其三,隨機逼近理論——Q-learning 收斂性背後的 Robbins–Monro 條件與步長理論,本課只以「步長衰減」帶過;其四,LSPI(最小平方政策迭代)與連續狀態空間函數逼近的完整推導;其五,批次 Q-learning 在原書目錄中出現兩次(5.5 隨機逼近脈絡與 5.7 獨立小節),顯示作者把「從固定歷史資料集學 Q」視為金融場景的重點方法,本課第八節僅述其思想、未展開演算法細節;其六,穿插於原書行文的資管與交易迷你案例及配套 Python notebooks(章目標頁提及 notebooks 收錄於附錄,p.280)。另提醒兩點:原書草稿目錄標記本章起於第 307 頁,與出版印刷頁(第 279 頁起)不一致,引用時應以出版頁碼為準;本課的出清 MDP、全部數字與實驗皆為自創教學設計,原書並無對應例子。進階讀者可循原書該章與 Sutton–Barto 教科書補齊上述縱深。
§04作業和解答
作業一:γ 是耐心的價格
第五節的出清 MDP 用 \(\gamma=0.95\)。保持其他參數不變,分別用 \(\gamma=0.80\) 與 \(\gamma=0.99\) 重跑價值迭代,觀察最適政策怎麼變,並解釋:折現因子在這個問題裡扮演什麼經濟角色?
解答 SOLUTION
import numpy as np
FILL, HOLD = 0.6, 0.02
P_MKT1, P_MKT2, P_LMT = 0.97, 0.935, 1.02
def actions(q):
return (["M1", "L1"] if q >= 1 else []) + (["M2"] if q >= 2 else [])
def outcomes(q, a):
if a == "M1": fills = [(1.0, 1, P_MKT1)]
elif a == "M2": fills = [(1.0, 2, P_MKT2)]
else: fills = [(FILL, 1, P_LMT), (1 - FILL, 0, 0.0)]
return [(p, n * px - HOLD * (q - n), q - n) for p, n, px in fills]
def solve(gamma):
V = np.zeros(5)
for _ in range(2000):
V_new = np.zeros(5)
for q in range(1, 5):
V_new[q] = max(sum(p * (r + gamma * V[q2]) for p, r, q2 in outcomes(q, a))
for a in actions(q))
if np.abs(V_new - V).max() < 1e-12:
V = V_new; break
V = V_new
pi = {q: max(actions(q), key=lambda a: sum(p * (r + gamma * V[q2])
for p, r, q2 in outcomes(q, a))) for q in range(1, 5)}
return V, pi
for gamma in (0.80, 0.95, 0.99):
V, pi = solve(gamma)
print(f"γ={gamma:.2f}|V*(4)={V[4]:.4f}|政策={pi}")
# 輸出:
# γ=0.80|V*(4)=3.3260|政策={1: 'M1', 2: 'M2', 3: 'M2', 4: 'M2'}
# γ=0.95|V*(4)=3.6117|政策={1: 'L1', 2: 'M1', 3: 'M2', 4: 'M2'}
# γ=0.99|V*(4)=3.7608|政策={1: 'L1', 2: 'L1', 3: 'M1', 4: 'M2'}解讀:γ 在這裡就是耐心的價格。\(\gamma=0.80\) 時未來大打折,等待限價單成交的那六成機率「以後才拿錢」變得不值——最適政策全面轉向市價單,連最後一股都直接倒掉(全 M 政策);\(\gamma=0.95\) 出現本章正文的梯度政策;\(\gamma=0.99\) 時未來幾乎不打折,耐心近乎免費,政策整體向限價單傾斜——剩 2 股就開始掛單磨價,只有庫存最重時才用一次倒兩股的市價單。同一個市場、同一套成本,只動「時間偏好」這一顆旋鈕,行為從「急性子清倉」連續變形到「慢工出細活」。這也是實務警訊:γ 常被當成隨手設的超參數,但它直接改寫最適行為——資金成本高、隔夜風險大的臺股當沖帳戶與可以擺三個月的法人帳戶,本來就不該用同一個 γ。
作業二:把探索調壞給自己看
第七節的 Q-learning 用 \(\varepsilon=0.2\)。固定訓練 5000 集,分別用 \(\varepsilon\in\{0.01, 0.1, 0.4\}\) 重跑,比較:(1) 與精確 \(Q^*\) 的最大差距;(2) 訓練期間的每集平均報酬(線上成績);(3) 學到的政策對不對。解釋觀察到的取捨。
解答 SOLUTION
import numpy as np
GAMMA, FILL, HOLD = 0.95, 0.6, 0.02
P_MKT1, P_MKT2, P_LMT = 0.97, 0.935, 1.02
def actions(q):
return (["M1", "L1"] if q >= 1 else []) + (["M2"] if q >= 2 else [])
def outcomes(q, a):
if a == "M1": fills = [(1.0, 1, P_MKT1)]
elif a == "M2": fills = [(1.0, 2, P_MKT2)]
else: fills = [(FILL, 1, P_LMT), (1 - FILL, 0, 0.0)]
return [(p, n * px - HOLD * (q - n), q - n) for p, n, px in fills]
Q_star = {q: {a: 0.0 for a in actions(q)} for q in range(1, 5)}
for _ in range(300):
for q in range(1, 5):
for a in actions(q):
Q_star[q][a] = sum(p * (r + GAMMA * (max(Q_star[q2].values()) if q2 else 0.0))
for p, r, q2 in outcomes(q, a))
def run(eps, episodes=5000, seed=7):
rng = np.random.default_rng(seed)
Q = {q: {a: 0.0 for a in actions(q)} for q in range(1, 5)}
N = {q: {a: 0 for a in actions(q)} for q in range(1, 5)}
total_reward = 0.0
for ep in range(episodes):
q = int(rng.integers(1, 5))
while q > 0:
acts = actions(q)
if rng.random() < eps:
a = acts[int(rng.integers(len(acts)))]
else:
a = max(acts, key=lambda x: Q[q][x])
outs = outcomes(q, a)
i = rng.choice(len(outs), p=[p for p, _, _ in outs])
_, r, q2 = outs[i]
total_reward += r
N[q][a] += 1
target = r + GAMMA * (max(Q[q2].values()) if q2 else 0.0)
Q[q][a] += (target - Q[q][a]) / N[q][a] ** 0.7
q = q2
gap = max(abs(Q[q][a] - Q_star[q][a]) for q in range(1, 5) for a in actions(q))
pi = {q: max(actions(q), key=lambda a: Q[q][a]) for q in range(1, 5)}
return gap, total_reward / episodes, pi
for eps in (0.01, 0.1, 0.4):
gap, avg_r, pi = run(eps)
print(f"ε={eps:.2f}|max|Q-Q*|={gap:.4f}|每集平均報酬={avg_r:.4f}|政策={pi}")
# 輸出:
# ε=0.01|max|Q-Q*|=0.1122|每集平均報酬=2.3694|政策={1: 'M1', 2: 'M1', 3: 'M2', 4: 'M2'}
# ε=0.10|max|Q-Q*|=0.0565|每集平均報酬=2.3758|政策={1: 'M1', 2: 'M1', 3: 'M2', 4: 'M2'}
# ε=0.40|max|Q-Q*|=0.0161|每集平均報酬=2.4034|政策={1: 'L1', 2: 'M1', 3: 'M2', 4: 'M2'}解讀:\(\varepsilon=0.01\) 與 0.1 都學錯了 q=1 的行動(挑了 M1 而非 L1)——M1 與 L1 的真值只差 0.0042,探索太少時 L1 的樣本不夠,估計的雜訊蓋過真實差距,排序翻車;\(\varepsilon=0.4\) 才把政策學全對,且 Q 表誤差最小(0.0161)。更值得玩味的是線上成績:這個環境裡 ε 越大連訓練期間的平均報酬都越高(2.3694 → 2.4034)——因為此處所有行動都不算災難(最差的行動也只是「次好」),探索的即期代價低,換來的估值品質紅利反而更大。這是重要的細部教訓:探索的代價取決於環境的懲罰結構,不是一句「探索必然犧牲短期」能概括。但別把它外推到真市場——作業三的環境裡就存在一步致命的行動,那時探索的每一次手滑都是真實的大額虧損,兩題對照著看才完整。
作業三:SARSA 與 Q-learning 在風控懸崖邊的分歧
設計一個「執行網格」:3 列 × 7 行,起點在左下、終點在右下,底列中段五格是強制平倉區——踩進去罰 40 分並拖回起點;其餘每走一步罰 1 分(時間成本),\(\gamma=1\)。用 \(\varepsilon=0.1\) 的 ε-greedy 分別訓練 SARSA 與 Q-learning 各 3000 集,比較:(1) 關掉探索後的貪婪路徑;(2) 訓練後期(最後 1000 集)的平均每集報酬。解釋為什麼兩者學出不同的路。
解答 SOLUTION
import numpy as np
# 3 列 × 7 行的執行網格:起點左下、終點右下,底列中段是「強制平倉區」
# 踩進去:-40 分並被拖回起點。每走一步 -1 分。γ=1(episodic)
ROWS, COLS = 3, 7
START, GOAL = (2, 0), (2, 6)
CLIFF = {(2, c) for c in range(1, 6)}
MOVES = {"U": (-1, 0), "D": (1, 0), "L": (0, -1), "R": (0, 1)}
def step(s, a):
dr, dc = MOVES[a]
r2 = min(max(s[0] + dr, 0), ROWS - 1)
c2 = min(max(s[1] + dc, 0), COLS - 1)
if (r2, c2) in CLIFF:
return -40.0, START # 觸發風控:重罰並拖回起點
return -1.0, (r2, c2)
def train(algo, episodes=3000, alpha=0.5, eps=0.1, seed=3):
rng = np.random.default_rng(seed)
Q = {(r, c): {a: 0.0 for a in MOVES}
for r in range(ROWS) for c in range(COLS)}
def choose(s):
if rng.random() < eps:
return "UDLR"[int(rng.integers(4))]
return max(MOVES, key=lambda a: Q[s][a])
returns = []
for _ in range(episodes):
s, a, G = START, None, 0.0
a = choose(s)
while s != GOAL:
r, s2 = step(s, a)
G += r
if s2 == GOAL:
Q[s][a] += alpha * (r - Q[s][a])
break
a2 = choose(s2)
if algo == "sarsa": # on-policy:用實際會走的 a2
target = r + Q[s2][a2]
else: # q-learning:用 max(假想貪婪)
target = r + max(Q[s2].values())
Q[s][a] += alpha * (target - Q[s][a])
s, a = s2, a2
returns.append(G)
# 貪婪路徑(關掉探索走一遍)
path, s, n = [START], START, 0
while s != GOAL and n < 30:
a = max(MOVES, key=lambda x: Q[s][x])
_, s = step(s, a)
path.append(s); n += 1
return np.mean(returns[-1000:]), path
for algo in ("q-learning", "sarsa"):
avg, path = train(algo)
rows = "".join(str(r) for r, _ in path)
print(f"{algo:>10s}|最後 1000 集平均報酬 = {avg:.2f}|貪婪路徑列號 = {rows}"
f"|路徑長 = {len(path) - 1}")
# 輸出:
# q-learning|最後 1000 集平均報酬 = -17.16|貪婪路徑列號 = 211111112|路徑長 = 8
# sarsa|最後 1000 集平均報酬 = -16.11|貪婪路徑列號 = 21100000012|路徑長 = 10解讀:看貪婪路徑的列號序列——Q-learning 走 211111112:上到中列(列 1)後貼著懸崖直線衝刺,8 步到終點,是全域最短路;SARSA 走 21100000012:多爬一層到頂列(列 0)繞遠路,10 步才到。原因就在靶子那一項:Q-learning 評估的是「假想中完全貪婪的自己」——那個自己永遠不手滑,貼崖路線在它的帳本上就是最優;SARSA 評估的是「實際帶著 ε 手滑的自己」——貼崖走時每步都有約 2.5% 機率隨機踩崖(\(\varepsilon/4\)),期望罰款把貼崖格的 Q 值壓低,於是它學會繞開。訓練期間的成績單印證了這一點:SARSA 的線上平均報酬 −16.11 優於 Q-learning 的 −17.16——只要你還在探索,貼崖衝刺就會定期付出 −40 的代價。金融翻譯:Q-learning 給你「理想執行下的最適策略」,SARSA 給你「考慮到自己會出錯時的穩健策略」;上線的系統帶著雜訊與滑價在跑,兩者的差距就是紙上最優與實盤穩健之差。該用哪個,取決於你上線後還探不探索、以及踩一次崖賠不賠得起。
§05參考資料
- Sutton, R. S., & Barto, A. G. (2018). Reinforcement Learning: An Introduction (2nd ed.)(作者免費全文) — 強化學習標準教科書,本章 MDP、DP、TD、SARSA/Q-learning 框架的權威參照。
- Watkins, C. J. C. H., & Dayan, P. (1992). Q-learning. Machine Learning 8, 279–292 — Q-learning 收斂性證明的原始論文。
- Lagoudakis, M. G., & Parr, R. (2003). Least-Squares Policy Iteration. JMLR 4 — LSPI 原始論文:線性函數逼近下的批次政策迭代,第八節離線學習路線的經典。
- Mnih, V., et al. (2013). Playing Atari with Deep Reinforcement Learning. arXiv:1312.5602 — DQN 首篇:深度網路+經驗回放讓 Q-learning 上像素輸入。
- Nevmyvaka, Y., Feng, Y., & Kearns, M. (2006). Reinforcement Learning for Optimized Trade Execution — 用 RL 做最適下單執行的開山實證,本章出清玩具問題的真實版。
- OpenAI Spinning Up in Deep RL — 深度強化學習的免費入門教材,術語、演算法家譜與可跑代碼。
- Silver, D. UCL Course on Reinforcement Learning(課程講義與影片) — DeepMind 首席科學家的經典 RL 課程,MDP 到函數逼近的完整講義。
- Dixon, M. F., Halperin, I., & Bilokon, P. (2020). Machine Learning in Finance: From Theory to Practice. Springer — 本課程對照之原書,第九章導言見印刷頁 pp.279–280。