ARK · 金融機器學習CHAPTER 11 / 12

CHAPTER 11 / 12 · PART 3 · 時序決策:強化學習

逆強化學習與模仿學習

Inverse RL & Imitation Learning

報酬不可觀察時,從專家行為反推目標——最大熵 IRL、行為複製到 GAIL 的金融應用。

§01學習重點

§02課程內容

一、問題反轉:看行為,猜報酬

第 9 章建立了強化學習的標準劇本:環境是一個馬可夫決策過程(MDP),有狀態 \(s\)、行動 \(a\)、轉移機率 \(P(s'\mid s,a)\),以及一個報酬函數 \(r(s,a)\);代理人的任務是找出讓長期累積報酬最大的策略 \(\pi^*\)。整套機器有一個沉默的前提:報酬函數是給定的。訓練交易執行代理人,你先寫好「損益減去風險懲罰」;訓練避險策略,你先定義避險誤差的成本。報酬是輸入,行為是輸出。

本章把這個劇本整個反過來。現實世界裡大量存在的是行為資料:成交紀錄、委託簿上的掛單與撤單、基金的持股變動、消費者的刷卡與轉帳。這些行為背後的決策者顯然在最佳化「某個東西」,但那個東西——他們心裡的報酬函數——從來不會出現在資料欄位裡。逆強化學習(Inverse Reinforcement Learning, IRL)問的正是反向問題:給定一個(近似)最適代理人的行為軌跡,反推他在最佳化的報酬函數。模仿學習(Imitation Learning, IL)則問一個更務實的姊妹題:不一定要把報酬還原出來,只要讓我的策略表現得跟專家一樣就好。

比喻: 強化學習像照著食譜做菜:報酬函數是食譜,演算法負責把食譜練成一桌菜。逆強化學習像吃到一道驚豔的菜之後反推食譜:你看不到主廚的配方,只看得到成品——但成品的每一個細節(鹹淡、火候、擺盤的取捨)都在洩漏主廚心中「什麼重要、什麼不重要」的權重。IRL 就是把這些洩漏出來的權重系統性地收集起來,重建那份沒人給你看的食譜。

為什麼金融特別需要這個反轉?至少三個理由。第一,報酬本來就寫不出來。你想建模「散戶在恐慌時怎麼調整部位」或「外資在台股的調倉邏輯」,這些主體的目標函數混合了損益、風險、流動性、稅務甚至情緒,沒有任何一方能替他們寫下顯式報酬;但他們的成交行為天天都在產生資料。第二,反推出來的報酬本身就是情報。若能從對手的成交足跡推出「他在最佳化什麼」,你就能預測他在新情境下會怎麼做——這比單純模仿他過去的動作更進一步。第三,報酬是比策略更可攜的知識。策略跟環境綁死:手續費制度一改、流動性一變,背下來的動作全部作廢;報酬函數描述的是目標,目標不變時可以在新環境下重新求解出新策略。這條「可攜性」的線索,會在本章後段的 AIRL 再次出現。

IRL 這個問題在學術上由 Ng 與 Russell 在 2000 年正式形式化;2008 年 Ziebart 等人的最大熵 IRL 讓它變得可實用——他們的原始應用是從十萬英里的 GPS 行車資料反推駕駛對路線的偏好。從駕駛的路線選擇,到交易者的下單選擇,中間只隔著一層符號替換。

二、不適定性:同一種行為,無限多種解釋

把 IRL 當成一個反問題,第一件要面對的事是:它是不適定的(ill-posed)。同一組觀察到的行為,可以被無限多組報酬函數解釋。

最極端的例子一句話就能講完:「所有狀態報酬都是零」可以解釋任何行為——如果代理人對一切無差別,那他做什麼都是最適的。Ng 與 Russell 在開山論文裡把這個退化解列為 IRL 的核心困難。再來一個溫和些的例子:把報酬函數整體加上一個常數 \(c\),每條長度 \(T\) 的軌跡的總報酬都恰好多了 \(cT\),任兩條軌跡的相對優劣完全不變,最適策略也就完全不變。你在作業一會親手驗證:報酬全體平移後,策略的最大變化是 \(10^{-15}\) 量級——浮點誤差而已。更一般地,還存在一整族「潛函數塑形」(potential-based shaping)變換,能改寫報酬卻不動最適策略。

比喻: 你注意到同事每天中午都買同一家便當。這個行為至少有五種解釋:那是他的最愛;那是離辦公室最近的一家;那是唯一不用排隊的;他懶得做選擇;或者他根本沒發現巷口開了新店。單看「天天買同一家」這個行為,五種偏好模型全部吻合資料——行為是偏好的投影,而投影必然丟失資訊。IRL 的全部技術含量,就在於決定「用什麼原則從無限多個吻合的解釋裡挑一個」。

所以每一種 IRL 方法,本質上都是「一個歸納偏好的選擇」。Ng 與 Russell 用線性規劃挑「讓觀察到的策略贏過其他策略最多」的報酬;後來的邊際方法(margin-based)挑「把專家與次佳行為分得最開」的報酬;貝葉斯 IRL 把報酬當隨機變數、用後驗分布表達不確定;而本章的主角——最大熵 IRL——選擇資訊理論的答案:在符合資料的前提下,選擇對未知承諾最少的那個分布

這裡有一個值得停下來想清楚的觀念分層。IRL 的成功判準到底是什麼?是「反推的報酬數值接近真實報酬」嗎?上面的平移不變性已經告訴你,這個判準太天真——報酬本來就只能識別到「差一個不影響策略的變換」。更務實的判準有兩層:弱判準是行為等價——用反推報酬重新求解出的策略,其行為與專家不可區分;強判準是可遷移——把反推報酬搬到新環境(新的手續費、新的流動性結構)重新求解,產生的行為仍然合理。金融應用幾乎總是衝著強判準去的,而強判準困難得多——這是貫穿本章的一條暗線。

三、最大熵 IRL:只承諾資料要求的,其餘保持最大開放

最大熵 IRL 的設定如下。假設報酬是狀態特徵的線性組合:\(r_\theta(s) = \theta^\top \phi(s)\),其中 \(\phi(s)\) 是狀態 \(s\) 的特徵向量(在最簡單的表格型環境裡就是 one-hot 向量),\(\theta\) 是待反推的權重。一條軌跡 \(\tau = (s_0, s_1, \dots, s_{T-1})\) 的總報酬是 \(R_\theta(\tau) = \sum_t \theta^\top \phi(s_t)\)。

觀察資料給我們的「硬約束」只有一條:模型預測的特徵期望要等於專家示範的特徵期望(例如:專家平均每條軌跡在各狀態停留幾步)。滿足這條約束的軌跡分布有無限多個——這正是上一節的不適定性。最大熵原理說:挑熵最大的那一個。第 1 章講過,熵衡量分布的「不確定程度」;最大熵分布就是「除了約束硬給的資訊之外,不多做任何假設」的分布。對這個問題做變分求解,答案有漂亮的閉式結構:

$$ P_\theta(\tau) \;=\; \frac{1}{Z(\theta)} \exp\big(R_\theta(\tau)\big), \qquad Z(\theta) \;=\; \sum_{\tau'} \exp\big(R_\theta(\tau')\big) $$

逐項拆解:\(P_\theta(\tau)\) 是「觀察到軌跡 \(\tau\)」的機率——總報酬越高的軌跡,出現機率呈指數放大,但差報酬的軌跡機率不是零,只是小;\(Z(\theta)\) 是配分函數(partition function),把所有可能軌跡的指數權重加總起來做歸一化,讓機率合計為 1。這個結構把「專家是理性的」軟化成「專家是大致理性的」:他偏好高報酬路徑,但允許犯錯與雜訊——對真實金融行為資料,這比「完美最適」的假設誠實得多。

學習的方式是最大化示範資料的對數概似 \(L(\theta) = \frac{1}{N}\sum_i \log P_\theta(\tau_i)\),它的梯度乾淨得出奇:

$$ \nabla_\theta L \;=\; \bar{\mu}_E \;-\; \mathbb{E}_{\tau \sim P_\theta}\!\left[\sum_t \phi(s_t)\right] $$

逐項拆解:\(\bar{\mu}_E\) 是專家示範的平均特徵計數(資料算出來的固定向量);第二項是目前模型認為的期望特徵計數(給定目前的 \(\theta\),把模型自己會走的軌跡分布攤開來算)。梯度就是兩者之差:模型在某狀態停留得比專家少,就調高該狀態的報酬,反之調低——像在做特徵層面的供需撮合,直到模型「想去的地方」與專家「實際去的地方」完全對齊,梯度歸零。

計算上的死穴在 \(Z(\theta)\):軌跡數量隨長度指數爆炸,直接加總不可行。表格型環境的標準解法是動態規劃——用軟性值迭代(soft value iteration)算出每一期的軟值函數:

$$ V_t(s) \;=\; \log \sum_{a} \exp\big(r_\theta(s) + V_{t+1}(s')\big) $$

逐項拆解:這是把第 9 章的貝爾曼方程裡的 \(\max\) 換成 log-sum-exp——「軟性最大值」。它不是只挑最好的行動,而是把所有行動依其好壞做指數加權的柔性彙總。第 10 章講 G-learning 時你已經見過同一個算子:熵正則化的 RL,其最適策略是波茲曼形式 \(\pi(a\mid s) \propto \exp Q(s,a)\)。這不是巧合,而是同一枚硬幣的兩面——最大熵 IRL 假設專家服從軟性最適策略,所以它的前向模型恰好就是熵正則化 RL。第 1 章的熵、第 10 章的軟 Q、本章的 MaxEnt IRL,在這裡接成了一條線。連續或大型狀態空間裡連動態規劃都跑不動,配分函數只能靠重要性抽樣等方法近似——這是 MaxEnt IRL 規模化的主要瓶頸,也是後面 GAIL 登場的伏筆。

四、動手做:從交易軌跡反推報酬

把上面的機器完整跑一遍。玩具環境:一條 7 格的「庫存線」,狀態 0 到 6 對應部位 −3 到 +3,行動是減碼、不動、加碼。真實報酬偏好小幅做多(部位 +1 最舒服,兩端重倉都痛苦)——這是我們扮演上帝才知道的答案,演算法只能看到軌跡:

PYTHON
import numpy as np

rng = np.random.default_rng(11)

# 玩具環境:7 格庫存線,狀態 0..6 對應部位 -3..+3
n_s, T = 7, 20
actions = (-1, 0, +1)                     # 減碼、不動、加碼
theta_true = np.array([-1.5, -0.8, -0.3, -0.1, 0.0, -0.4, -1.0])  # 真實報酬:偏好小幅做多

def step(s, a):
    return min(max(s + a, 0), n_s - 1)

def soft_policy(theta):
    """有限視野軟性值迭代(溫度=1),回傳各期策略 pi[t][s, a_idx]"""
    V = np.zeros(n_s)
    pis = []
    for _ in range(T):                    # 從最後一期往回推
        Q = np.array([[theta[s] + V[step(s, a)] for a in actions] for s in range(n_s)])
        Vn = np.log(np.exp(Q - Q.max(1, keepdims=True)).sum(1)) + Q.max(1)
        pis.append(np.exp(Q - Vn[:, None]))
        V = Vn
    return pis[::-1]                      # 轉回時間順序

def visit_counts(pis):
    """前向遞推:模型期望的狀態造訪次數(每條軌跡合計)"""
    d = np.zeros(n_s); d[3] = 1.0         # 起點:空手(部位 0)
    mu = np.zeros(n_s)
    for t in range(T):
        mu += d
        d_next = np.zeros(n_s)
        for s in range(n_s):
            for j, a in enumerate(actions):
                d_next[step(s, a)] += d[s] * pis[t][s, j]
        d = d_next
    return mu

# 1) 專家示範:真實報酬下的軟性最適策略,抽 300 條軌跡
pi_exp = soft_policy(theta_true)
mu_E = np.zeros(n_s)
for _ in range(300):
    s = 3
    for t in range(T):
        mu_E[s] += 1
        s = step(s, actions[rng.choice(3, p=pi_exp[t][s])])
mu_E /= 300
print("每條軌跡的平均狀態造訪次數:", np.round(mu_E, 2))

# 2) MaxEnt IRL:特徵匹配的梯度上升(特徵=狀態 one-hot)
theta = np.zeros(n_s)
for it in range(2000):
    grad = mu_E - visit_counts(soft_policy(theta))
    theta += 0.05 * grad

# 3) 報酬只識別到「差一個常數」:兩邊都居中後比較
tc = theta_true - theta_true.mean()
th = theta - theta.mean()
corr = np.corrcoef(tc, th)[0, 1]
print("真實報酬(居中):", np.round(tc, 2))
print("反推報酬(居中):", np.round(th, 2))
print(f"相關係數 {corr:.4f}|最大絕對誤差 {np.abs(tc - th).max():.3f}")
# 輸出:
# 每條軌跡的平均狀態造訪次數: [0.   0.24 2.64 8.16 6.95 1.84 0.17]
# 真實報酬(居中): [-0.91 -0.21  0.29  0.49  0.59  0.19 -0.41]
# 反推報酬(居中): [-1.31 -0.23  0.4   0.6   0.66  0.28 -0.39]
# 相關係數 0.9907|最大絕對誤差 0.400

實跑結果值得逐行讀。反推報酬與真實報酬的相關係數 0.9907——中段狀態(部位 −1 到 +2)的形狀幾乎完美復原:演算法從純行為資料裡「看穿」了這位交易員偏好小幅做多。但最大絕對誤差高達 0.400,而且不偏不倚落在狀態 0(滿手空單)——看一眼造訪次數表就懂了:專家在 300 條示範裡一次都沒去過狀態 0(平均造訪 0.00 次),狀態 6 也只有 0.17 次。專家不去的地方,資料裡就沒有關於那裡的資訊,那格的報酬本質上是外插猜的。

這個現象不是玩具環境的毛病,而是 IRL 的一般規律,對金融應用是第一等重要的警告:你只能在專家行為覆蓋到的狀態區域裡信任反推的報酬。從一位基金經理的持倉紀錄反推他的效用函數,你反推出來的是「他在承平時期造訪過的那片狀態空間」上的效用;他從沒經歷過的流動性枯竭區域,模型給出的數字只是外插。拿這種報酬去做壓力情境的反事實推演,等於拿沒有資料支撐的猜測當結論。

五、模仿學習與行為複製:單步很準,全局崩壞

現在切到模仿學習這一支。如果你的目標不是理解專家、只是複製專家,最直覺的做法是行為複製(Behavioral Cloning, BC):把示範資料拆成一筆一筆的 \((s, a)\) 配對,當成監督式學習來訓練——狀態是特徵,行動是標籤,第 1 章到第 4 章的全部武器直接搬過來用。BC 完全跳過報酬,也完全跳過「序列」這件事。

而「跳過序列」正是它的致命傷。監督式學習的成績單是在示範資料的分布上打的:專家造訪過的狀態附近,模仿的單步誤差可以壓到極小。但部署之後,你的策略要自己在環境裡連續決策——每一步小誤差都會把你帶到稍微偏離專家路線的狀態,那裡的示範資料更稀疏,下一步誤差更大,把你帶得更偏……誤差不是相加,是複利。Ross 與 Bagnell 給過正式的刻畫:單步模仿誤差為 \(\varepsilon\) 時,BC 的 \(T\) 步累積代價最壞可達 \(O(\varepsilon T^2)\) 的量級——比「誤差乖乖線性累積」的 \(O(\varepsilon T)\) 多整整一個 \(T\) 因子,這個多出來的 \(T\) 就是「偏離後回不來」的代價。

比喻: 只靠看教練的行車紀錄器畫面學開車。教練開得穩,畫面裡全是車道正中央的操作——你把這些畫面背得再熟,也只學會了「在正中央該做什麼」。第一次自己上路,一個小顛簸讓車偏了半個車位,你需要的是「偏了怎麼修正」,但畫面庫裡根本沒有這一幕,因為教練從來不讓車偏掉。你只好硬套最接近的畫面,越修越歪。示範資料越完美,覆蓋越窄——專家的優秀,反而是模仿者的資料貧困

用代碼把這件事釘死。環境是一個會自我放大的曝險過程:\(x_{t+1} = 1.15\,x_t + a_t + \varepsilon_t\)——放著不管,部位的偏離會以每步 15% 的速度自己長大(想成一個帶槓桿、有動能的未對沖部位);專家的修正規則是 \(a = -0.5x\),閉環係數 0.65,穩穩壓住:

PYTHON
import numpy as np

rng = np.random.default_rng(23)

# 環境:帶自我放大的曝險 x_{t+1} = 1.15 x_t + a_t + 雜訊(不管它,部位會自己長大)
AMP, NOISE = 1.15, 0.02

def expert_action(x):
    return -0.5 * x                       # 專家:修正力道與偏離成正比(閉環 0.65,穩定)

# 1) 蒐集專家示範:40 條軌跡、每條 50 步——全部發生在「正常狀態」附近
xs, acts = [], []
for _ in range(40):
    x = rng.normal(0.0, 0.02)
    for t in range(50):
        a = expert_action(x) + rng.normal(0.0, 0.005)   # 專家也有一點執行雜訊
        xs.append(x); acts.append(a)
        x = AMP * x + a + rng.normal(0.0, NOISE)
xs, acts = np.array(xs), np.array(acts)
print(f"示範資料範圍: [{xs.min():.3f}, {xs.max():.3f}]({len(xs)} 筆)")

# 2) 行為複製:1-NN——查表模仿,示範分布之內表現極好
def bc_action(x):
    return acts[np.abs(xs - x).argmin()]

hold = rng.normal(0.0, 0.03, 500)          # 專家分布內的抽樣點
mse = np.mean([(bc_action(x) - expert_action(x))**2 for x in hold])
print(f"分布內單步模仿 MSE: {mse:.2e}")

# 3) 無衝擊 rollout:BC 與專家幾乎無法區分
def rollout(policy, x0, T, seed):
    r = np.random.default_rng(seed)
    x, path = x0, []
    for t in range(T):
        x = AMP * x + policy(x) + r.normal(0.0, NOISE)
        path.append(x)
    return np.array(path)

for tag, pol in (("專家", expert_action), ("BC  ", bc_action)):
    fin = [abs(rollout(pol, 0.0, 50, 1000 + k)[-1]) for k in range(200)]
    print(f"無衝擊 50 步|{tag}平均終點偏離 {np.mean(fin):.3f}")

# 4) 一次市場衝擊把狀態推到 x0=0.4(示範資料沒見過的區域)
for tag, pol in (("專家", expert_action), ("BC  ", bc_action)):
    paths = np.array([rollout(pol, 0.4, 25, 2000 + k) for k in range(200)])
    m = np.abs(paths).mean(axis=0)
    print(f"衝擊後|{tag} t=5 平均|x|={m[4]:.3f}  t=15 {m[14]:.3f}  t=25 {m[24]:.3f}")
# 輸出:
# 示範資料範圍: [-0.093, 0.101](2000 筆)
# 分布內單步模仿 MSE: 2.44e-05
# 無衝擊 50 步|專家平均終點偏離 0.021
# 無衝擊 50 步|BC  平均終點偏離 0.022
# 衝擊後|專家 t=5 平均|x|=0.049  t=15 0.020  t=25 0.020
# 衝擊後|BC   t=5 平均|x|=0.467  t=15 0.870  t=25 2.511

三段數字講完了整個故事。分布內:單步模仿 MSE 只有 \(2.4\times 10^{-5}\),無衝擊時 BC 與專家的 50 步表現根本分不出來(0.022 對 0.021)——如果你只做到這裡就上線,會以為模型完美。分布外:一次衝擊把狀態推到 0.4(示範資料的邊界只到 0.101),專家五步內拉回(0.049),BC 卻一路發散到 2.511——因為查表查到的最強修正力道,是專家在 0.1 附近用過的力道,根本壓不住 0.4 處自我放大的曝險。金融裡的對應再直白不過:跟單平台上覆製冠軍操盤手的持倉、用歷史成交紀錄訓練的執行模型,在行情正常時與本尊難分軒輊;行情把狀態推出歷史分布的那一天,就是複合誤差開始滾雪球的那一天。對治之道有二:其一是 DAgger 一路的「讓專家對模仿者實際到達的狀態重新示範」(作業三親手做);其二,就是下一節的主角。

六、GAIL:讓判別器代替報酬說話

行為複製的病根,在於它逐筆比對「單步動作」,而不在乎整條軌跡走出來的分布長不長得像專家。生成對抗模仿學習(Generative Adversarial Imitation Learning, GAIL)直接把目標換掉:不再問「這一步抄得像不像」,改問「你走出來的(狀態, 行動)佔據分布,跟專家的佔據分布,能不能被分辨出來」。

機制是一場兩人賽局。生成器是模仿策略 \(\pi\),在環境裡走出自己的軌跡;判別器 \(D\) 是一個二元分類器,看到一筆 \((s,a)\) 就輸出「這是專家的機率」。兩者的目標針鋒相對:

$$ \min_{\pi} \max_{D} \; \mathbb{E}_{(s,a)\sim \pi_E}\big[\log D(s,a)\big] + \mathbb{E}_{(s,a)\sim \pi}\big[\log\big(1 - D(s,a)\big)\big] - \lambda H(\pi) $$

逐項拆解:第一項獎勵判別器把專家樣本認成專家(\(D\) 越接近 1 越好);第二項獎勵它把模仿者樣本認成假貨(\(D\) 越接近 0 越好)——判別器把兩項一起最大化,就是在練「分辨真偽」的本事。生成器則反向最小化:它想讓自己的樣本騙過判別器。最後的 \(H(\pi)\) 是策略熵的正則項(係數 \(\lambda\))——鼓勵模仿策略保持隨機性、別過早坍縮成單一動作,你會認出這正是第 10 章熵正則化的老朋友。理論上可以證明:固定生成器時,最優判別器是密度比 \(D^*(s,a) = \rho_E / (\rho_E + \rho_\pi)\)(\(\rho\) 代表兩者的佔據密度),此時生成器的目標恰好化為最小化兩個佔據分布之間的 Jensen–Shannon 散度——GAIL 在數學上就是「佔據分布匹配」,複合誤差的病根被連根拔掉,因為比對的單位從「單步」升級成了「分布」。

比喻: 偽鈔集團與銀行驗鈔線的軍備競賽。驗鈔線(判別器)每攔下一批偽鈔,等於告訴偽鈔集團(生成器)「你的紙質/油墨/浮水印哪裡露餡」;偽鈔集團據此改進製程,逼驗鈔線升級檢測。多輪攻防之後,如果驗鈔線再也分不出真偽——不是驗鈔線失職,而是偽鈔的統計特徵已經與真鈔無法區分。GAIL 的妙處在於:沒有人寫過「真鈔的定義」,這個定義是攻防過程中被隱式學出來的——正如沒有人寫過報酬函數,判別器的評分就是隱式的報酬。

注意 GAIL 對 IRL 的巧妙繞道:傳統 IRL 管線是「反推報酬 → 用 RL 解報酬 → 得到策略」,其中每一輪報酬更新都要完整跑一遍 RL 內迴圈,貴到難以規模化;MaxEnt IRL 的配分函數估計正是卡在這裡。GAIL 的推導從佔據分布的對偶形式出發,證明這個內外雙迴圈可以壓成單一的對抗賽局——跳過顯式報酬,直接從示範學到策略。代價是:賽局結束後你手上只有策略,沒有那份可攜的報酬函數(這筆帳 AIRL 會回來算)。

用第四節的環境做一個最小可運行的示意——判別器是邏輯迴歸,生成器從均勻亂走起步,做一步對抗更新:

PYTHON
import numpy as np

rng = np.random.default_rng(31)

# 沿用第四節的 7 格庫存線環境與專家(真實報酬下的軟性最適策略)
n_s, T = 7, 20
actions = (-1, 0, +1)
theta_true = np.array([-1.5, -0.8, -0.3, -0.1, 0.0, -0.4, -1.0])

def step(s, a):
    return min(max(s + a, 0), n_s - 1)

def soft_policy(reward_sa):
    """軟性值迭代;reward_sa 形狀 (n_s, 3),回傳各期策略"""
    V = np.zeros(n_s)
    pis = []
    for _ in range(T):
        Q = np.array([[reward_sa[s, j] + V[step(s, a)] for j, a in enumerate(actions)]
                      for s in range(n_s)])
        Vn = np.log(np.exp(Q - Q.max(1, keepdims=True)).sum(1)) + Q.max(1)
        pis.append(np.exp(Q - Vn[:, None]))
        V = Vn
    return pis[::-1]

def sample_sa(pis, n_traj, seed):
    """抽 (s, a_idx) 樣本對"""
    r = np.random.default_rng(seed)
    out = []
    for _ in range(n_traj):
        s = 3
        for t in range(T):
            j = r.choice(3, p=pis[t][s])
            out.append((s, j))
            s = step(s, actions[j])
    return np.array(out)

def onehot(sa):
    Z = np.zeros((len(sa), n_s * 3))
    Z[np.arange(len(sa)), sa[:, 0] * 3 + sa[:, 1]] = 1.0
    return Z

def train_disc(X_exp, X_gen, steps=400, lr=0.5):
    """邏輯迴歸判別器:D=1 猜「專家」"""
    w = np.zeros(n_s * 3)
    for _ in range(steps):
        pe = 1 / (1 + np.exp(-X_exp @ w))
        pg = 1 / (1 + np.exp(-X_gen @ w))
        w += lr * (X_exp.T @ (1 - pe) / len(X_exp) - X_gen.T @ pg / len(X_gen))
    return w

pi_exp = soft_policy(np.repeat(theta_true[:, None], 3, axis=1))
sa_exp = sample_sa(pi_exp, 300, seed=7)

# 第 0 代生成器:均勻亂走的「模仿交易員」
pi_gen = [np.full((n_s, 3), 1 / 3)] * T
sa_gen = sample_sa(pi_gen, 300, seed=8)

w = train_disc(onehot(sa_exp), onehot(sa_gen))
D = lambda sa: 1 / (1 + np.exp(-(onehot(sa) @ w)))
print(f"第 0 代|判別器平均輸出:專家 {D(sa_exp).mean():.3f}|生成器 {D(sa_gen).mean():.3f}")

# 生成器一步更新:把判別器輸出當替代報酬 r_hat = log D - log(1-D),重解軟性最適策略
r_hat = (np.log(np.clip(1/(1+np.exp(-w)), 1e-6, 1-1e-6))
         - np.log(np.clip(1 - 1/(1+np.exp(-w)), 1e-6, 1-1e-6))).reshape(n_s, 3)
pi_gen2 = soft_policy(r_hat)
sa_gen2 = sample_sa(pi_gen2, 300, seed=9)
print(f"更新後|同一個判別器對新生成器的平均輸出:{D(sa_gen2).mean():.3f}(越接近專家越難分辨)")

# 佔據分布距離:生成器的狀態造訪分布向專家靠攏了多少
def state_dist(sa):
    return np.bincount(sa[:, 0], minlength=n_s) / len(sa)
d0 = np.abs(state_dist(sa_gen) - state_dist(sa_exp)).sum()
d1 = np.abs(state_dist(sa_gen2) - state_dist(sa_exp)).sum()
print(f"狀態佔據分布 L1 距離:更新前 {d0:.3f} → 更新後 {d1:.3f}")
# 輸出:
# 第 0 代|判別器平均輸出:專家 0.621|生成器 0.418
# 更新後|同一個判別器對新生成器的平均輸出:0.689(越接近專家越難分辨)
# 狀態佔據分布 L1 距離:更新前 0.755 → 更新後 0.346

數字有兩層可讀。第一層是成功:一步更新就讓生成器的狀態佔據分布與專家的 L1 距離從 0.755 砍半到 0.346——判別器的評分確實把「往專家靠攏」的方向資訊傳給了生成器。第二層更有教育意義:更新後的生成器在同一個判別器眼中得分 0.689,竟然超過了專家本人的 0.621。這不是生成器青出於藍,而是它過度迎合了這個固定判別器的口味——把判別器目前認為「最像專家」的那幾格狀態塞好塞滿。真正的 GAIL 必須交替更新:判別器馬上針對新生成器重新訓練,戳破這種過擬合。這個小小的過衝現象,就是對抗訓練出了名不穩定的縮影——實務上要靠梯度懲罰、譜歸一化等正則化手段,以及小心的學習率配平,才能讓攻防雙方都不脫韁。

GAIL 之後。GAIL 丟掉了報酬,AIRL(Adversarial IRL)把它撿回來:給判別器一個特殊的結構 \(D = \exp f / (\exp f + \pi)\),其中 \(f(s,a)\) 在訓練收斂時會逼近報酬(加上一個塑形項)——於是你同時得到策略一份可以搬去新環境的報酬函數,正是第二節說的「強判準」路線;AIRL 論文展示了在動力學改變後,用學到的報酬重新訓練仍然有效。再往後,f-MAX 這一族工作把整個版圖統一起來:GAIL 最小化 JS 散度、AIRL 對應反向 KL、FAIRL 對應前向 KL——各種模仿學習方法,其實只是「用哪把尺量兩個佔據分布的距離」的不同選擇;反向 KL 傾向「守住專家最常做的事」(保守),前向 KL 傾向「專家做過的都要覆蓋」(激進),金融場景裡前者通常是更安全的預設。這一句話的視角,就足夠你讀懂這條線的大部分後續文獻。

七、IRL 在金融的五個戰場

方法講完,盤點戰場。以下五個場景是 IRL/IL 在量化金融裡最活躍的應用方向,各自的「專家是誰、報酬藏著什麼」都不一樣。

其一,交易策略辨識。觀察某個對手帳戶(或某類參與者聚合)的成交足跡,反推其目標函數:他在最佳化短期 alpha、還是最小化衝擊成本、還是在做流動性供給?反推出的報酬可以用來分類交易者類型(做市商、動能追逐者、被動指數調倉),也可以預測「同一目標在新行情下的行為」。對監理機關,這是從行為資料辨識操縱型策略的工具雛形;對造市商,這是理解訂單流毒性的另一條路。台股的環境對這件事其實很友善——三大法人買賣超、鉅額交易、分點進出這些公開行為資料,都是天然的「示範軌跡」。

其二,情緒驅動交易的建模。行為金融學告訴我們市場參與者並非最適化教科書效用;IRL 提供一個反向工程的框架——不假設投資人理性,而是從其實際買賣行為反推「他們事實上在最佳化的東西」,把恐慌拋售、追高、處分效應這些行為偏誤內生地編碼進報酬函數,再用這個報酬模擬市場在壓力下的群體行為。

其三,選擇權定價的 IRL 視角。選擇權的對沖與定價可以表述為序列決策問題——狀態是標的價格與避險部位,行動是調整對沖,報酬是風險調整後的損益。反轉之後:市場上觀察到的選擇權報價與對沖流量,本身就是「市場這位集體代理人」的行為軌跡,IRL 可以從中反推市場隱含的風險偏好與風險趨避係數——與其說是定價工具,不如說是從價格行為讀出市場在最佳化什麼的顯微鏡。

其四,投資組合投資人的偏好推斷。從一位投資人(或一檔基金)的持倉調整序列,反推其效用函數的參數:風險趨避多強、目標部位在哪、對交易成本多敏感。這正是原書用 G-learning 框架詳細展開的例子——在熵正則化的設定下,投資人的調倉行為服從波茲曼分布,於是偏好推斷化為一個可以寫出概似函數的統計估計問題。應用面從機器人理財的客戶畫像(不靠問卷、靠實際行為校準風險屬性),到主管機關檢視基金實際行為與公開說明書宣稱的風險水準是否一致。

其五,市場動態建模——「看不見的手」的推斷。最大膽的一步:把整個市場當成單一有界理性代理人,其「行為」是價格與成交量的演化軌跡,用 IRL 反推市場整體在最佳化的目標泛函。Halperin 與 Feldshteyn 的工作沿這條路展開,把市場的訊號結構、自我衝擊與最適交易統一在一個自由能框架下——市場動態成了「集體報酬函數」的梯度流。這條線在第 12 章講金融機器學習前沿時會再回來。

五個場景共用一組前提,值得白紙黑字寫下:行為資料要足夠覆蓋你關心的狀態區域(第四節的教訓);被觀察的主體要大致在最佳化某個穩定的目標(最大熵框架容忍雜訊,但不容忍目標本身亂跳);以及環境動態要麼已知、要麼可估。三個前提在金融裡都不是白給的——這就是最後一節的主題。

八、誠實限界:IRL 不是讀心術

本章收在三條誠實限界上。

限界一:專家資料的品質與覆蓋。 IRL 的輸入是行為,行為資料的坑比報價資料更深。你看到的「專家」可能根本不是專家——跟單平台上的高績效帳戶可能只是倖存者偏差的中獎者;你看到的行為可能被你看不到的約束扭曲——基金經理的調倉混著申贖壓力、風控指令與稅務考量,這些外生約束會被 IRL 誤讀成「偏好」。而覆蓋問題在第四節已經數字化地演示過:示範沒去過的狀態,反推的報酬只是外插。在金融裡,最要命的狀態(崩盤、擠兌、流動性真空)恰恰是歷史行為資料最稀薄的地方。

限界二:行為不等於最適。 最大熵框架把「完美理性」軟化成「大致理性」,這是誠實的進步,但它仍然假設行為圍繞著某個目標函數展開。真實市場參與者會犯系統性的錯(不是雜訊,是偏誤)、會學習、會改主意。把一段「其實在犯錯」的行為餵給 IRL,它會忠實地反推出一個「把犯錯合理化」的報酬函數——模型不會舉手說「這個人其實不知道自己在幹嘛」。反推結果的解讀,永遠需要領域知識當防腐劑。

限界三:非平穩性讓報酬漂移。 IRL 的隱含假設是報酬函數在觀察窗口內穩定。但市場參與者的目標會隨制度變遷而變:升息週期裡的壽險資金與零利率時代的壽險資金,最佳化的東西不一樣;同一個做市商在監管改革前後的報酬函數也不一樣。第 1 章講過分布移轉是交叉驗證救不了的問題——這裡是同一個幽靈的 IRL 版本:你反推的是過去那個制度下的報酬,而你要預測的行為發生在下一個制度裡。可攜報酬(AIRL 一路)處理的是「環境動態改變、目標不變」的遷移;「目標本身改變」的遷移,目前沒有任何方法能保證。務實的做法與第 1 章的結論同構:滾動窗口重估、監控反推報酬的漂移、把「報酬突變」本身當成制度轉換的警訊——後者甚至可能是 IRL 在金融裡最被低估的用法。

一句話收束本章:IRL 把「行為資料」升級成「目標情報」,是強大的顯微鏡;但顯微鏡照不出樣本裡沒有的東西——它反推的是行為所投影的報酬,不是決策者的內心。

§03原書對照

本課以白話重組了原書第十一章的主幹。原書 PDF 樣本不含本章正文,以下依原書草稿目錄的節結構整理「原書有、但本課未展開」的內容(依原書草稿目錄,無出版頁碼可核;標注草稿節號與對應論文錨點供深入)。其一,IRL 的成功判準與可攜報酬有專節討論(草稿 §2.2–2.3):何謂 IRL 做對了、真正可攜的報酬函數能否被學到——本課第二節僅以「弱判準/強判準」二分帶過。其二,最大熵一族的完整推導鏈(§3):最大熵原理、最大因果熵(區分「對整條軌跡取熵」與「對每步決策取條件熵」,後者才正確處理動態環境的因果結構)、G-learning 與軟 Q 學習作為前向模型的角色、以及配分函數估計的數值方法——本課只走了表格型動態規劃一條路。其三,一個完整的顧客偏好推斷案例(§4):以 IRL 框架推斷顧客效用函數,含資料需求量化(觀察雜訊下需要多少樣本)、反事實模擬與 MLE 估計量的有限樣本性質——這是把 IRL 當計量工具使用的完整示範,本課作業二僅做了資料需求的迷你版。其四,對抗式模仿學習的工程細節(§5):GAIL 的實務正則化、對抗訓練的穩定化技巧,以及 f-散度訓練、Wasserstein GAN、最小平方 GAN 等變體損失。其五,GAIL 之後的完整譜系(§6):AIRL(arXiv:1710.11248)、前向與後向 KL 的選擇、f-MAX(arXiv:1911.02256)、FAIRL、風險敏感的 RS-GAIL——本課僅以散度視角一段帶過。其六,貝葉斯 IRL 與高斯過程 IRL(§7):把報酬當隨機函數、以 GP 先驗表達不確定性,連回第 3 章的高斯過程機器。其七,「IRL 能否超越老師」(§8):從失敗示範學習(IRL from Failure)、偏好學習、T-REX/D-REX 以「排序過的軌跡」外推出比示範者更好的報酬——這對金融格外有意思,因為市場上的示範者本來就良莠不齊,能利用壞示範的方法等於把可用資料擴大了一個量級;本課完全未展開。其八,金融懸崖行走實驗(§9):在模擬環境中對照多種 IRL 方法的表現。其九,金融應用各有專節(§10):其中投資組合投資人的 G-learning IRL 附有對數概似梯度的完整推導(原書附錄 A),「看不見的手」推斷對應 arXiv:1805.06126——本課第七節僅做了地圖式的概述。

§04作業和解答

作業一:親手驗證報酬的不可識別性

延續第四節的環境:(1) 把真實報酬向量整體加上常數 2.0,重新求解軟性最適策略,量化策略的最大變化;(2) 只把狀態 0 的報酬加 2.0,再量化一次;(3) 用這兩個數字說明「IRL 反推的報酬為什麼至少差一個常數」,以及這對「比較兩位交易員反推報酬」的實務含義。

解答 SOLUTION
PYTHON
import numpy as np

n_s, T = 7, 20
actions = (-1, 0, +1)
theta_true = np.array([-1.5, -0.8, -0.3, -0.1, 0.0, -0.4, -1.0])

def step(s, a):
    return min(max(s + a, 0), n_s - 1)

def soft_policy(theta):
    V = np.zeros(n_s)
    pis = []
    for _ in range(T):
        Q = np.array([[theta[s] + V[step(s, a)] for a in actions] for s in range(n_s)])
        Vn = np.log(np.exp(Q - Q.max(1, keepdims=True)).sum(1)) + Q.max(1)
        pis.append(np.exp(Q - Vn[:, None]))
        V = Vn
    return pis[::-1]

pi_a = soft_policy(theta_true)
pi_b = soft_policy(theta_true + 2.0)       # (1) 全體平移
diff = max(np.abs(pa - pb).max() for pa, pb in zip(pi_a, pi_b))
print(f"報酬全體平移 +2 後,策略最大差異: {diff:.2e}")

theta_c = theta_true.copy(); theta_c[0] += 2.0   # (2) 只動狀態 0
pi_c = soft_policy(theta_c)
diff_c = max(np.abs(pa - pc).max() for pa, pc in zip(pi_a, pi_c))
print(f"只平移狀態 0 後,策略最大差異: {diff_c:.3f}")
# 輸出:
# 報酬全體平移 +2 後,策略最大差異: 4.33e-15
# 只平移狀態 0 後,策略最大差異: 0.814

(1) 全體平移後策略最大差異 \(4.3\times 10^{-15}\)——浮點精度的零。原因:每條長度 20 的軌跡總報酬都恰好增加 40,log-sum-exp 裡的公共常數在歸一化時完全消掉,任何兩條軌跡的相對機率不變。(2) 只動一個狀態則策略劇變(0.814):這改變了相對結構,「去不去狀態 0」的誘因整個翻轉。(3) 含義:資料只包含「相對偏好」的資訊,絕對水平不可識別——所以第四節比較真實與反推報酬前要先各自居中。實務上,兩位交易員的反推報酬數值不可直接互比(各差一個任意常數,若特徵不同還差塑形變換),能比的是各自報酬的形狀:峰值在哪個部位、對偏離的懲罰多陡。報告 IRL 結果時永遠呈現居中(或以某參考狀態為錨)後的報酬,是基本紀律。

作業二:IRL 的資料需求——多少示範才夠

把第四節的示範軌跡數從 300 改成 30 與 3000,各自重跑 MaxEnt IRL(梯度上升 2000 步不變),記錄反推報酬與真實報酬的相關係數與最大絕對誤差。解釋誤差來源,並回答:如果你只關心「中段部位的報酬形狀」,30 條夠不夠?

解答 SOLUTION
PYTHON
import numpy as np

n_s, T = 7, 20
actions = (-1, 0, +1)
theta_true = np.array([-1.5, -0.8, -0.3, -0.1, 0.0, -0.4, -1.0])

def step(s, a):
    return min(max(s + a, 0), n_s - 1)

def soft_policy(theta):
    V = np.zeros(n_s)
    pis = []
    for _ in range(T):
        Q = np.array([[theta[s] + V[step(s, a)] for a in actions] for s in range(n_s)])
        Vn = np.log(np.exp(Q - Q.max(1, keepdims=True)).sum(1)) + Q.max(1)
        pis.append(np.exp(Q - Vn[:, None]))
        V = Vn
    return pis[::-1]

def visit_counts(pis):
    d = np.zeros(n_s); d[3] = 1.0
    mu = np.zeros(n_s)
    for t in range(T):
        mu += d
        d_next = np.zeros(n_s)
        for s in range(n_s):
            for j, a in enumerate(actions):
                d_next[step(s, a)] += d[s] * pis[t][s, j]
        d = d_next
    return mu

pi_exp = soft_policy(theta_true)
for N in (30, 300, 3000):
    r2 = np.random.default_rng(100 + N)
    mu_E = np.zeros(n_s)
    for _ in range(N):
        s = 3
        for t in range(T):
            mu_E[s] += 1
            s = step(s, actions[r2.choice(3, p=pi_exp[t][s])])
    mu_E /= N
    theta = np.zeros(n_s)
    for it in range(2000):
        theta += 0.05 * (mu_E - visit_counts(soft_policy(theta)))
    tc = theta_true - theta_true.mean(); th = theta - theta.mean()
    print(f"N={N:>4d}|相關係數 {np.corrcoef(tc, th)[0,1]:.4f}|最大絕對誤差 {np.abs(tc-th).max():.3f}")
# 輸出:
# N=  30|相關係數 0.9469|最大絕對誤差 0.363
# N= 300|相關係數 0.9971|最大絕對誤差 0.075
# N=3000|相關係數 0.9995|最大絕對誤差 0.025

相關係數隨樣本數穩定爬升(0.947 → 0.997 → 0.9995),最大絕對誤差以大約 \(1/\sqrt{N}\) 的速度收縮(0.363 → 0.075 → 0.025)——這是標準的統計收斂行為,因為 MaxEnt IRL 本質上是 MLE,誤差由「經驗特徵期望 \(\bar\mu_E\) 對母體期望的估計誤差」主導。但注意分布是不均勻的:誤差集中在邊緣狀態,因為每條軌跡平均只造訪它們 0 到 0.2 次——狀態 0 的有效樣本數只有名目樣本數的百分之一不到。若只關心中段部位(造訪次數 2 到 8 次/軌跡),30 條示範已經能給出定性正確的形狀(相關 0.95 主要靠中段撐起);但要對邊緣狀態的數值下任何結論,3000 條都嫌少。實務讀法:IRL 的資料需求不是「幾條軌跡」一個數字,而是一張「各狀態有效樣本數」的表——這正是原書顧客偏好案例中「需要多少資料」一節用更精細的統計工具回答的問題。另外,與第四節(同為 N=300、不同亂數種子)對照可見邊緣狀態誤差在種子間波動很大(0.400 對 0.075)——單次實驗的邊緣估計連自己都不可信,這本身就是結論的一部分。

作業三:DAgger 精神——讓專家對「你走到的狀態」重新示範

第五節的 BC 在衝擊後崩壞,病根是「模仿者實際到達的狀態」不在示範分布裡。實作 DAgger 的最小版本:(1) 用目前的 BC 策略在衝擊情境(\(x_0 = 0.4\))下跑 40 條軌跡;(2) 對這些軌跡實際經過的所有狀態,向專家查詢正確動作(重標);(3) 把新資料併入訓練集、重建 1-NN,重複兩輪;(4) 比較原始 BC 與增強後 BC 在衝擊測試下的表現。

解答 SOLUTION
PYTHON
import numpy as np

rng = np.random.default_rng(23)
AMP, NOISE = 1.15, 0.02
def expert_action(x): return -0.5 * x

xs, acts = [], []
for _ in range(40):
    x = rng.normal(0.0, 0.02)
    for t in range(50):
        a = expert_action(x) + rng.normal(0.0, 0.005)
        xs.append(x); acts.append(a)
        x = AMP * x + a + rng.normal(0.0, NOISE)
xs, acts = np.array(xs), np.array(acts)

def make_bc(X, A):
    return lambda x: A[np.abs(X - x).argmin()]

def rollout(policy, x0, T, seed):
    r = np.random.default_rng(seed)
    x, path = x0, []
    for t in range(T):
        x = AMP * x + policy(x) + r.normal(0.0, NOISE)
        path.append(x)
    return np.array(path)

bc0 = make_bc(xs, acts)

# DAgger 迴圈(2 輪):拿目前策略在衝擊情境下跑,讓專家對「實際到達的狀態」重標
X_aug, A_aug = xs.copy(), acts.copy()
for k in range(2):
    bc_k = make_bc(X_aug, A_aug)
    new_states = np.concatenate([rollout(bc_k, 0.4, 25, 3000 + k * 40 + i) for i in range(40)])
    X_aug = np.concatenate([X_aug, new_states])
    A_aug = np.concatenate([A_aug, expert_action(new_states)])   # 專家重標:正確答案
bc_dagger = make_bc(X_aug, A_aug)

for tag, pol in (("原始 BC   ", bc0), ("DAgger 兩輪", bc_dagger)):
    paths = np.array([rollout(pol, 0.4, 25, 5000 + k) for k in range(200)])
    m = np.abs(paths).mean(axis=0)
    print(f"衝擊後|{tag} t=5 平均|x|={m[4]:.3f}  t=25 {m[24]:.3f}")
# 輸出:
# 衝擊後|原始 BC    t=5 平均|x|=0.471  t=25 2.593
# 衝擊後|DAgger 兩輪 t=5 平均|x|=0.048  t=25 0.023

兩輪重標之後,增強版 BC 在衝擊測試下的表現(t=5 偏離 0.048、t=25 偏離 0.023)已經與第五節的專家本人(0.049、0.020)無法區分——而原始 BC 在同一測試下發散到 2.593。機制:第一輪 rollout 時策略走進了 0.1 到 0.4 之間(以及發散出去更遠)的「災區」,專家重標把這些災區狀態的正確修正動作補進了訓練集;1-NN 查表從此在災區也查得到正確答案,複合誤差的鏈條被掐斷。理論上 DAgger 把 BC 的 \(O(\varepsilon T^2)\) 保證改進回 \(O(\varepsilon T)\)。但注意它的隱含成本:需要一位隨時可查詢的專家。在機器人領域這是請操作員多示範幾次;在金融裡,「讓明星操盤手對你的模型走出來的每個狀態標注正確動作」通常不可得——這正是 GAIL 一路「不查詢專家、改比對佔據分布」的價值所在。

作業四:可攜的報酬,不可攜的策略(概念題)

你用 GAIL 從某位專家的執行軌跡學到了一個漂亮的模仿策略;三個月後交易所把手續費調高了三倍。(1) 解釋為什麼 GAIL 學到的策略在新環境下大概率失效,且無法廉價修復;(2) 解釋若當初用 AIRL 同時反推出報酬函數,處境會好在哪裡;(3) 什麼情況下連 AIRL 的報酬也救不了你?

解答 SOLUTION

(1) GAIL 的產出是策略——「狀態到行動」的映射,它是專家報酬與舊環境動態共同求解的結果。手續費三倍意味著環境的成本結構變了:舊策略裡「頻繁小額調倉」之類的行為在舊環境下最適、在新環境下變成穩定虧損,但策略本身沒有任何機制知道這件事。修復只能重新蒐集新環境下的專家示範再訓練一次——而專家自己可能也還在摸索新環境,短期內根本沒有「新示範」可用。(2) AIRL 的判別器結構會額外交出一個 \(f(s,a)\),收斂時逼近(塑形等價類意義下的)專家報酬。報酬編碼的是目標(在風險與成本之間怎麼取捨),不是動作。手續費改變的是環境動態與成本項,只要專家的根本目標不變,就能拿同一份報酬在新環境下重新跑一次 RL 求解,得到適應新費率的新策略——這正是第二節「強判準」與第一節「報酬是更可攜的知識」的具體兌現。(3) 兩種情況會讓報酬也失效:其一,目標本身變了——費率暴漲若導致專家轉換商業模式(從高頻造市改為低頻擇時),舊報酬描述的目標已不存在,這是第八節限界三的「報酬漂移」;其二,報酬吸收了環境——如果訓練時的特徵設計讓成本結構滲進了報酬估計(例如報酬函數裡混進了「與舊費率相關的行為特徵」),那份報酬其實是「目標×舊環境」的混合物,可攜性只是幻覺。AIRL 論文用「解耦報酬」(disentangled reward)的條件刻畫何時能避免後者——實務上,特徵集越貼近「目標的語言」(損益、風險、庫存)而非「動作的語言」(下單頻率、單量),報酬的可攜性越可信。

§05參考資料