§01學習重點
- 說出金融業要求模型可解釋的三個理由:模型風險監管、受託責任、除錯與信任
- 定義模型敏感度=輸出對輸入的偏導數,說明它如何把「線性迴歸係數」的解讀方式推廣到神經網路
- 寫出單隱藏層 tanh 網路的 Jacobian 公式,並解釋為什麼這條路線要求激活函數平滑
- 在線性資料上完成「降落傘測試」:驗證神經網路的平均敏感度收回 OLS 係數
- 用平均絕對敏感度做特徵重要性排序,並辨認它的三個陷阱:對稱抵銷、飽和、局部性
- 用 Hessian 的非對角元素偵測交互作用效應,說明 ReLU 為何在這項任務上失能
- 描述深度基本面因子模型的解讀流程:從逐點敏感度、橫斷面彙總,到追蹤曝險隨時間漂移
§02課程內容
一、為什麼金融特別需要可解釋性
第 4 章我們訓練出了會預測的前饋神經網路。在多數產業,故事到這裡就可以收尾——模型準,上線。金融不行。金融裡有三股力量,硬性要求你在「準」之外還要能「說清楚」。
第一股是監管。銀行體系對模型出錯的容忍度極低,因為 2008 年已經示範過一次模型集體失靈的代價。美國聯準會與貨幣監理署在 2011 年發布的 SR 11-7 模型風險管理準則,把「有效挑戰」(effective challenge)定為核心:任何用於決策的模型,都必須能被開發者以外的人檢驗、質疑、複核,而一個說不出自己在做什麼的模型,根本無從被挑戰起。台灣的方向一致:金管會 2024 年發布的「金融業運用人工智慧(AI)指引」,把「落實透明性與可解釋性」列為六大核心原則之一。對金融機構來說,可解釋性不是加分項,是模型能不能過模型驗證部門(model validation)這一關的門票。
第二股是受託責任。資產管理業拿的是客戶的錢。投資委員會不會接受「因為神經網路這麼說」作為建倉理由;出了虧損,你更需要能夠回答「模型當時押的是什麼、為什麼」。線性因子模型之所以在業界活了半世紀,不是因為它最準,而是因為它的每個係數都有名字、有單位、可以寫進報告。
第三股最實際:除錯與信任。第 1 章講過,資料洩漏、前視偏差、標籤錯置是金融機器學習的日常地雷——而這些地雷通常不是靠測試誤差抓到的,是靠「解讀模型後發現它押了一個不該這麼重要的特徵」抓到的。一個你無法解讀的模型,壞掉的時候不會通知你。
好消息是:對神經網路而言,「黑盒子」在很大程度上是個選擇,不是宿命。本章走一條「白盒」路線——不把模型當成只能從外面戳的黑箱,而是直接打開它的微分結構,用梯度回答兩個問題:每個輸入對輸出的影響多大(敏感度)?哪些輸入要一起動才有效果(交互作用)?這條路線的最大優點,是它與金融從業者已經在用的語言——迴歸係數、因子曝險——無縫接軌。
二、敏感度:把「因子曝險」推廣到彎曲的世界
先回到你最熟悉的模型。線性迴歸
逐項拆解:\(\hat{Y}\) 是預測值;每個 \(\beta_i\) 告訴你「其他條件不變,\(X_i\) 增加一單位,預測增加 \(\beta_i\)」。用微積分的話說,\(\beta_i\) 就是輸出對輸入的偏導數:
這個量有個素樸的名字:模型敏感度(model sensitivity)。金融人對它再熟悉不過——如果 \(X_i\) 是市場因子的報酬,\(\beta_i\) 就是因子曝險(beta);如果 \(X_i\) 是利率,這就是存續期間的邏輯。整套線性因子語言,本質上都是在讀敏感度。
關鍵觀察來了:「輸入動一點、輸出動多少」這個問題,並不依賴模型是線性的。任何可微分的模型都答得出來,只是答案從「一個常數」變成「一個隨地點改變的函數」。對神經網路 \(\hat{Y} = F(X)\),敏感度就是在每個輸入點 \(x\) 上求偏導數——把全部偏導數排成一列,就是 Jacobian 向量。以第 4 章的單隱藏層網路為例,設激活函數為 \(\tanh\):
逐項拆解:\(I^{(1)}\) 是隱藏層的淨輸入(線性組合的結果,還沒經過彎折);\(1-\tanh^2\) 是 \(\tanh\) 的導數——每個神經元「此刻彎折的斜率」,介於 0 與 1 之間;\(\mathrm{diag}(\cdot)\) 把這些斜率排成對角矩陣;整條式子就是鏈鎖律:輸入先經過 \(W^{(1)}\) 放大、再被各神經元依當下位置打折、最後由 \(W^{(2)}\) 加權彙總。層數更多時同理,就是把「權重矩陣、斜率對角陣」交替連乘下去。注意一件優雅的事:如果所有神經元都不彎(斜率恆為 1),對角陣變成單位矩陣,敏感度退回 \(W^{(2)}W^{(1)}\)——正是第 1 章「軟尺摺回直尺」時那條線性迴歸的係數。敏感度分析是舊語言的推廣,不是新語言。
比喻: 線性模型像均一費率的計程車:不管開在哪一段路,跳表速率都一樣,一個數字(每公里單價)就說完了整趟車資結構。神經網路像分段費率的計程車:市區一種速率、高速公路一種速率、深夜再加成——「每公里多少錢」依然是個有意義的問題,只是答案取決於你此刻人在哪裡。敏感度就是當下這一刻的跳表速率:它把「係數」從全域常數變成局部讀數,而不是把這個概念作廢。
代價是有的:這條路線要求模型處處可微、而且導數有界——數學上叫 Lipschitz 連續,白話說就是「輸入挪一小步,輸出不能瞬移」。\(\tanh\) 完美符合:平滑、導數介於 0 到 1。ReLU(\(\max(z,0)\))在折點不可微,導數只有 0 與 1 兩種值,會給敏感度分析製造麻煩——麻煩多大,第五節見分曉。這也是為什麼本章的實驗一律用 \(\tanh\)。
三、降落傘測試:網路能學回迴歸係數嗎
一個聲稱更強的工具,要先在已知答案的考題上交卷。做法是金融模型驗證的標準動作:用線性模型生成資料——這樣真實係數是我們自己寫的,網路學完之後算敏感度,答案對不對一翻兩瞪眼。這個實驗同時檢驗兩件事:非線性模型在線性世界裡會不會「自作聰明」;以及敏感度這把尺量出來的數字,能不能與 OLS 對得上。我把它叫做降落傘測試——不管飛得多花俏,你得證明自己能安全落回地面。
import numpy as np
rng = np.random.default_rng(5)
# 模擬 400 期橫斷面資料:4 個風格因子曝險 -> 個股超額報酬
n, p = 400, 4
X = rng.normal(0.0, 1.0, size=(n, p))
beta_true = np.array([0.8, -0.5, 0.3, 0.0]) # 第 4 個因子其實無關
y = X @ beta_true + rng.normal(0.0, 0.1, size=n)
# 對照組:OLS 閉式解
beta_ols = np.linalg.lstsq(np.hstack([X, np.ones((n, 1))]), y, rcond=None)[0][:p]
# 單隱藏層 tanh 網路(8 個神經元),手寫 Adam 訓練
H = 8
W1 = rng.normal(0, 0.5, size=(H, p)); b1 = np.zeros(H)
W2 = rng.normal(0, 0.5, size=(1, H)); b2 = np.zeros(1)
params = [W1, b1, W2, b2]
mom = [np.zeros_like(q) for q in params]
vel = [np.zeros_like(q) for q in params]
for t in range(1, 3001):
Hh = np.tanh(X @ W1.T + b1) # 隱藏層輸出
err = Hh @ W2.ravel() + b2[0] - y # 預測誤差
dH = np.outer(err, W2.ravel()) * (1 - Hh**2) # 誤差反傳到隱藏層
grads = [dH.T @ X / n, dH.mean(axis=0),
(err @ Hh)[None, :] / n, np.array([err.mean()])]
for k in range(4): # Adam 更新
mom[k] = 0.9 * mom[k] + 0.1 * grads[k]
vel[k] = 0.999 * vel[k] + 0.001 * grads[k] ** 2
step = (mom[k] / (1 - 0.9 ** t)) / (np.sqrt(vel[k] / (1 - 0.999 ** t)) + 1e-8)
params[k] -= 0.01 * step
def sensitivities(Xq):
"""逐點敏感度:dY/dX = W2 diag(1 - tanh^2) W1,每列一個樣本"""
Hh = np.tanh(Xq @ W1.T + b1)
return ((1 - Hh ** 2) * W2.ravel()) @ W1 # 形狀 (樣本數, p)
J = sensitivities(X)
print("真實係數 :", beta_true)
print("OLS 估計 :", np.round(beta_ols, 3))
print("網路平均敏感度:", np.round(J.mean(axis=0), 3))
print("敏感度跨樣本標準差:", np.round(J.std(axis=0), 3))
# 輸出:
# 真實係數 : [ 0.8 -0.5 0.3 0. ]
# OLS 估計 : [ 0.794 -0.506 0.308 0.008]
# 網路平均敏感度: [ 0.799 -0.505 0.3 0.006]
# 敏感度跨樣本標準差: [0.041 0.032 0.062 0.03 ]三行數字並排讀:真實係數 0.8/−0.5/0.3/0,OLS 估出 0.794/−0.506/0.308/0.008,網路的平均敏感度是 0.799/−0.505/0.300/0.006——與 OLS 同一個精度等級,連「第 4 個因子無關」都判對了。這就是敏感度方法的第一張成績單:神經網路面對線性資料時,透過這把尺讀出來的東西與線性迴歸一致;係數解讀這條血脈沒有斷。最後一行也別放過:敏感度的跨樣本標準差不是零(0.03 到 0.06),因為網路畢竟是彎的,各點斜率有些微差異——這個「彎度殘量」在線性資料上是雜訊,在非線性資料上就是訊號了。
對模型驗證部門,這個實驗有實戰意義:把「新模型必須在線性資料上收回線性答案」寫成一條自動化檢核,是深度模型上線流程裡便宜又有效的守門員。它保證了新舊模型之間存在一條連續的橋——出問題時,你永遠可以退回橋的另一端對帳。
四、特徵重要性排序:直覺、實驗與三個陷阱
敏感度是逐點的,一個 600 筆樣本、5 個特徵的資料集會給你 3000 個偏導數。要回答「哪個特徵重要」,得把它們摘要成每個特徵一個分數。最自然的兩個候選:平均敏感度(保留正負號)與平均絕對敏感度(先取絕對值再平均)。兩者的差異不是數學潔癖——用錯一個,重要的特徵會從你的報表上憑空消失。跑個實驗看清楚。這次資料是非線性的,真實生成函數我們自己寫,所以每個特徵「該多重要」有標準答案:
import numpy as np
rng = np.random.default_rng(11)
# 真實生成函數:Y = 1.2 sin(X1) + 0.9 X2 X3 + 0.6 X4(X5 完全無關)
n, p = 600, 5
X = rng.normal(0.0, 1.0, size=(n, p))
y = (1.2 * np.sin(X[:, 0]) + 0.9 * X[:, 1] * X[:, 2]
+ 0.6 * X[:, 3] + rng.normal(0.0, 0.1, size=n))
def train_tanh_net(X, y, H, seed, iters):
"""單隱藏層 tanh 網路 + Adam;回傳訓練後參數"""
r = np.random.default_rng(seed)
n, p = X.shape
W1 = r.normal(0, 0.4, size=(H, p)); b1 = np.zeros(H)
W2 = r.normal(0, 0.4, size=(1, H)); b2 = np.zeros(1)
params = [W1, b1, W2, b2]
mom = [np.zeros_like(q) for q in params]
vel = [np.zeros_like(q) for q in params]
for t in range(1, iters + 1):
Hh = np.tanh(X @ W1.T + b1)
err = Hh @ W2.ravel() + b2[0] - y
dH = np.outer(err, W2.ravel()) * (1 - Hh ** 2)
grads = [dH.T @ X / n, dH.mean(axis=0),
(err @ Hh)[None, :] / n, np.array([err.mean()])]
for k in range(4):
mom[k] = 0.9 * mom[k] + 0.1 * grads[k]
vel[k] = 0.999 * vel[k] + 0.001 * grads[k] ** 2
step = (mom[k] / (1 - 0.9 ** t)) / (np.sqrt(vel[k] / (1 - 0.999 ** t)) + 1e-8)
params[k] -= 0.01 * step
return params
W1, b1, W2, b2 = train_tanh_net(X, y, H=16, seed=7, iters=6000)
Hh = np.tanh(X @ W1.T + b1)
print("訓練後 MSE:", round(float(np.mean((Hh @ W2.ravel() + b2[0] - y) ** 2)), 4))
J = ((1 - Hh ** 2) * W2.ravel()) @ W1 # 逐點敏感度 (600, 5)
print("平均敏感度 :", np.round(J.mean(axis=0), 3))
print("平均絕對敏感度:", np.round(np.abs(J).mean(axis=0), 3))
# 輸出:
# 訓練後 MSE: 0.0079
# 平均敏感度 : [0.724 0.015 0.049 0.6 0.003]
# 平均絕對敏感度: [0.834 0.739 0.721 0.6 0.022]先對答案。理論上:\(\partial Y/\partial X_1 = 1.2\cos(X_1)\),在標準常態下平均約 0.728;\(\partial Y/\partial X_4 = 0.6\) 恆定;\(X_5\) 應為 0。網路的平均敏感度給出 0.724、0.600、0.003——三個都命中。但看 \(X_2\) 與 \(X_3\):它們透過乘積項 \(0.9\,X_2 X_3\) 實實在在地驅動 \(Y\),平均敏感度卻只有 0.015 與 0.049,幾乎隱形。原因藏在導數裡:\(\partial Y/\partial X_2 = 0.9\,X_3\),而 \(X_3\) 平均為零——正的樣本點與負的樣本點兩兩抵銷,加總歸零。換到平均絕對敏感度那一行,\(X_2\) 與 \(X_3\) 立刻現形(0.739、0.721,理論值 \(0.9\sqrt{2/\pi}\approx 0.718\)),五個特徵的排序這才與真相一致。
這個實驗直接給出陷阱一:對稱抵銷——影響方向會翻轉的特徵(交互作用、U 形效果都是),在帶正負號的平均下互相抵消。所以重要性排序一律用絕對值(或平方)彙總;帶號平均另有用途——它告訴你影響的淨方向,兩個都要看,但別拿淨方向排名。
陷阱二:飽和。 \(\tanh\) 的導數在淨輸入很大時趨近零。如果某個特徵沒做標準化、動輒把神經元推進飽和區,它的梯度會被壓扁——模型明明重度依賴它,敏感度卻讀出接近零。這正是深度因子模型實務把所有輸入標準化的原因之一:不只是幫助訓練收斂,更是讓「敏感度」這把尺在各特徵之間可比。
比喻: 飽和就像調光旋鈕轉到底:旋鈕(輸入)再怎麼轉,燈(輸出)也不會更亮,此刻的「靈敏度」是零。但你不能因此說這顆旋鈕不重要——燈那麼亮,就是它轉出來的。梯度量的是「再動一點會怎樣」,不是「一路走來貢獻多少」;在旋鈕轉到底的地方,這兩個問題的答案天差地遠。
陷阱三:局部性。 敏感度是切線斜率,只在你評估的那個點附近有效。單看一個點的 Jacobian 就下全域結論,等於拿台北的計程車費率推算全國車資。務實作法是在整個訓練分布上評估、看分布而不只看平均——以及,對關心的子區域(例如「高波動樣本」對「低波動樣本」)分開彙總,這在第六節會變成正式的工具。
五、交互作用:兩個特徵一起動才有的效果
上一節的 \(X_2\) 與 \(X_3\) 還留了一個問題:絕對敏感度只告訴我們「這兩個特徵重要」,沒告訴我們它們是綁在一起重要的。「\(X_2\) 的影響取決於 \(X_3\) 在哪」——這句話用微積分寫,就是混合二階偏導數不為零:
逐項拆解:先對 \(X_j\) 求導得到「\(X_j\) 的邊際效果」,再問這個效果隨 \(X_i\) 變不變——不變(二階導為零)就是兩特徵各管各的,會變就是交互作用。把所有配對排成矩陣,就是 Hessian。這裡藏著本章對線性模型最深的一刀:線性模型的 Hessian 恆等於零矩陣——它不是「不擅長」抓交互作用,是在結構上就把這個問題的答案寫死為零。你想在線性框架裡看交互作用,得自己手動造 \(X_2 \times X_3\) 這種乘積特徵;而第 1 章算過,特徵一多,乘積項的數量會組合爆炸,你不可能全部猜到。神經網路反過來:交互作用自動內建在彎折的複合結構裡,Hessian 只是把它讀出來。對單隱藏層 tanh 網路,二階導同樣有閉式解:
逐項拆解:每個神經元 \(k\) 貢獻一項,大小由三個因素相乘——它連向輸出的權重 \(w^{(2)}_k\)、它此刻的曲率 \(\sigma''(I_k)\)(彎得越厲害、值越大),以及它同時接收 \(X_i\) 與 \(X_j\) 的權重乘積 \(w^{(1)}_{ki} w^{(1)}_{kj}\)。白話版:一個神經元要製造 \(X_i\)–\(X_j\) 交互作用,必須「同時聽這兩個特徵的話,而且正好站在彎的地方」。延續上一節訓練好的網路,把每一對特徵的交互強度算出來:
def interaction_matrix(Xq):
"""平均絕對交互強度:|d2Y/dXi dXj| 對樣本取平均
tanh 的二階導 sigma'' = -2 tanh (1 - tanh^2)"""
T = np.tanh(Xq @ W1.T + b1)
s2 = -2 * T * (1 - T ** 2) # (樣本數, H)
M = np.zeros((p, p))
for i in range(p):
for j in range(p):
hij = (s2 * W2.ravel() * W1[:, i] * W1[:, j]).sum(axis=1)
M[i, j] = np.abs(hij).mean()
return M
M = interaction_matrix(X)
print("非對角(交互作用):")
for i in range(p):
for j in range(i + 1, p):
print(f" X{i+1}-X{j+1}: {M[i, j]:.3f}")
print("對角(自身曲率) :", np.round(np.diag(M), 3))
# 輸出:
# 非對角(交互作用):
# X1-X2: 0.034
# X1-X3: 0.037
# X1-X4: 0.038
# X1-X5: 0.021
# X2-X3: 0.887
# X2-X4: 0.043
# X2-X5: 0.029
# X3-X4: 0.035
# X3-X5: 0.021
# X4-X5: 0.012
# 對角(自身曲率) : [0.662 0.119 0.091 0.044 0.014]十個特徵配對裡,\(X_2\)–\(X_3\) 的交互強度 0.887 一枝獨秀(真實乘積項係數是 0.9),其餘全部壓在 0.05 以下——網路不但學到了「有交互作用」,還把是哪一對、多強都指認出來了。對角線是每個特徵「自己跟自己」的二階導,也就是自身彎曲程度:\(X_1\) 的 0.662 反映 \(\sin\) 的曲率(理論平均 \(1.2\,\mathbb{E}|\sin X_1| \approx 0.658\)),線性進場的 \(X_4\) 曲率貼近零。一張 Hessian 摘要表,把「誰重要、誰是彎的、誰跟誰綁在一起」三個問題一次答完。
比喻: 中藥師傅都知道,複方的力量不等於各味藥單獨藥效的加總——有些藥對「相須」,合用才起效;有些「相惡」,同碗互相牽制。西藥的仿單也一定有「藥物交互作用」欄位。敏感度是單味藥的藥效表,Hessian 就是那張交互作用表:它專門記錄「A 的效果,因為 B 在場而改變」的條目。金融資料裡這種配對比比皆是——動能訊號的效果隨波動率狀態翻轉、信用利差的意義取決於利率水位——只看單味藥效表的人,永遠讀不懂這帖方子。
現在可以把 ReLU 的帳算清楚了。ReLU 是兩段直線拼的:折點以外曲率處處為零,於是 \(\sigma''\equiv 0\),上面那條公式整個歸零——ReLU 網路的 Hessian 讀數永遠是零矩陣,交互作用在這套方法下直接失明。這不是實作瑕疵,是函數形狀的數學事實。加上敏感度本身在 ReLU 下只有「開/關」兩檔(導數非 0 即 1),逐點 Jacobian 跳動劇烈、隨網路加寬越發不穩定(作業二會實測)。結論很直接:要做梯度基解讀,用平滑激活函數。追求訓練速度用 ReLU、追求可解釋性用 tanh,是深度因子模型實務裡真實存在的取捨。
六、深度因子模型跑出來之後:這個模型在押什麼
工具備齊,回到本章的主戰場。先補一塊業界地圖。橫斷面股票報酬建模有兩大傳統:統計因子路線讓因子從資料裡自己浮現(主成分分析那一族),因子解釋力強但沒有名字,投資委員會不愛;基本面因子路線(業界慣稱 Barra 式)反過來,用看得見的公司特徵當因子曝險——估值(如股價淨值比)、規模(市值對數)、動能、品質——然後估計每期的因子報酬。寫成式子:
逐項拆解:\(\mathbf{r}_t\) 是第 \(t\) 期全體股票的報酬向量;\(B_t\) 是「每支股票在每個因子上的曝險」矩陣——這是看得到的資料(從財報與市場數據算出來);\(\mathbf{f}_t\) 是該期的因子報酬——這是要估的東西,每期用橫斷面迴歸解出;\(\boldsymbol{\varepsilon}_t\) 是個股殘差。它的可解釋性是機構等級的:本期虧損可以逐項歸因到「價值因子貢獻多少、動能貢獻多少」。但它有一條寫死的假設:特徵與報酬的關係是線性的、因子之間沒有交互作用——小型股的價值效應與大型股一樣強?動能在高波動期與低波動期同一個係數?線性模型連問都不問。
神經網路版的升級方式現在看起來理所當然:把線性映射換成一個可微的非線性函數,
其中 \(F\) 用前饋網路近似——特徵之間的非線性與交互作用,交給網路自己從資料裡長出來。這就是深度基本面因子模型。而它之所以敢在機構環境上線,靠的正是本章前五節鋪好的三塊地板:第一,它是線性因子模型的嚴格推廣——網路退化(零隱藏層)時整個框架塌回 Barra 式模型,降落傘測試保證新舊模型可對帳;第二,敏感度讓它繼續說因子語言;第三,Hessian 讓它說出線性模型說不出的話。
這裡有個值得停一秒的細節。在第二、三節的例子裡,輸入是因子報酬、敏感度讀出因子曝險;在 Barra 式設定裡,輸入換成了曝險 \(B_t\),於是敏感度 \(\partial F/\partial B_k\) 讀出的是隱含因子報酬——「本期每多一單位價值曝險,模型多給多少預期報酬」。同一把微分的尺,量的對象由「誰當輸入」決定。讀任何模型的敏感度之前,先確認輸入是什麼,這個習慣能省掉很多張冠李戴。
於是「這個模型在押什麼」有了標準作業流程,三步:
第一步,逐股敏感度。 對每支股票、每個因子算 \(\partial F/\partial B_k\),得到一張與持股清單同長的表:模型認為台積電的報酬此刻主要由哪個特徵驅動、對某檔金融股又換成哪個。線性模型裡這張表每一列都相同;網路版裡它逐股不同——這不是缺點,這正是非線性的內容。
第二步,橫斷面彙總。 把逐股敏感度沿股票維度取中位數與分位距,得到「本期模型的因子檢視表」:哪些因子的隱含報酬大、方向為何、股票之間分歧多大。分歧本身就是資訊——某因子的敏感度在全體股票上同號,說明它的作用接近線性;正負參半,說明它與別的特徵有強交互(第五節的 Hessian 可以進一步指認是跟誰)。
第三步,沿時間追蹤。 每期重算、疊成時間序列,觀察模型押注的漂移。這一步在實務上最重要,因為市場是有狀態的:多頭裡動能因子的敏感度可能高居榜首,壓力期防禦性因子接管;訓練資料橫跨不同狀態的模型,其敏感度排序本來就應該隨狀態輪動。把因子敏感度的時間序列畫在儀表板上,模型解讀就從「上線前寫一次的報告」升級成「每天在看的監控訊號」——排序突然洗牌,往往比績效回撤更早告訴你市場狀態變了,或者模型壞了。
誠實供述實證結果的溫度:在因子數少的小型設定裡,深度因子模型對線性模型的樣本外優勢通常很薄——非線性沒有素材可施展;因子數放大到幾十個、股票池放大到幾千支時,差距才明顯拉開。這符合直覺:交互作用的組合空間隨因子數爆炸,正是人工造項造不完、而網路自動搜尋開始值回票價的地方。另一個反覆出現的觀察是:網路的敏感度分布通常比 OLS 係數窄——正則化與彎折讓它對單一因子的押注更節制,重要性排序也與 OLS 不同。兩個模型並排跑、對照各自的因子檢視表,至今仍是深度因子模型上線時最有說服力的驗收儀式。
七、界線:這套方法不回答的問題
收尾前,把界線畫清楚——會用工具的下一級,是知道工具的失效模式。
梯度基方法不是唯一的路。 模型無關(model-agnostic)陣營用「擾動」代替「微分」:部分依賴圖(PDP)把單一特徵掃一遍、其他特徵平均掉;LIME 在單一預測點附近用可解釋的小模型局部貼合;SHAP 用合作賽局的 Shapley 值把預測公平分帳到每個特徵。它們的優點是不要求模型可微、樹模型也能用;代價是計算昂貴、且解讀結果對擾動方式的選擇敏感。本課主推梯度路線,理由前面已經演示過:當模型可微時,梯度給的是精確的閉式解,而且與迴歸係數、因子曝險這套金融母語直接對接。
可解釋性不等於因果。 敏感度回答的是「模型認為輸入動一點、預測會怎麼動」,這是模型的信念,不是世界的定律。模型重押股價淨值比,不代表股價淨值比「造成」報酬——可能只是它與某個真正的驅動因素相關。把敏感度報表當成因果結論拿去做業務決策,是這套工具最危險的誤用方式。它的正確用途是稽核模型:檢查模型的信念合不合理、有沒有偷用不該用的資訊、押注集不集中。
解讀有保存期限。 敏感度是在訓練分布上算的;市場狀態一換、模型一重訓,整張重要性排序都可能洗牌。第六節把解讀做成時間序列監控,正是對這件事的制度化回應——單次解讀是快照,不是承諾。
平均會騙人。 本章反覆出現的教訓:帶號平均抵銷交互作用(第四節)、局部讀數不能外推全域(陷阱三)、飽和讓重要特徵裝死(陷阱二)。解讀永遠要看分布、看多個彙總方式的交叉印證,不要只看一個數字。
到這裡,第一部「橫斷面資料的監督式學習」收官:我們有了機率語言(第 2、3 章)、非線性模型(第 4 章)、以及打開它的方法(本章)。下一章換一個維度——資料不再是一期一期獨立的橫斷面,而是有記憶、有順序的時間序列,金融資料最深的水從那裡開始。
§03原書對照
本課以敏感度與交互作用為主線重組了原書第五章,以下內容原書有、本課未展開,按頁碼供深入。其一,黑盒法譜系的完整回顧:Garson 演算法以連接權重絕對值分配各輸入的貢獻、Olden 演算法以輸入—輸出權重乘積計算相對重要性,並以「階梯係數」測試把兩者與敏感度法並排比較,指出唯有敏感度法與線性迴歸的係數解讀一致(pp.168, 170–171, 186,式 5.22,圖 5.1)。其二,方法成立的正式條件:以 Lipschitz 連續性保證導數有界,tanh 的複合仍為 Lipschitz、ReLU 因折點不可微而被排除的完整論證,以及敏感度的權重乘積上下界(pp.168–170,式 5.6)。其三,Friedman 基準資料實驗:十個均勻分布輸入、僅五個進入真實函數,以交叉驗證選定的單隱藏層網路重現重要性與交互作用排名(pp.171–172,圖 5.2–5.3)。其四,本課只給結論的 ReLU 失能論,原書給了定理與證明:ReLU 網路的 Jacobian 是 Heaviside 指示函數的線性組合,其變異數有下界隨隱藏單元數上升——寬度換不來敏感度的穩定(pp.172–174 定理與註記,附錄 pp.186–187 完整證明,式 5.10–5.12、5.23–5.29)。其五,對任意激活函數的 Chernoff 型機率界,刻畫 Jacobian 偏離其期望的尾部機率(p.174,式 5.13–5.15,圖 5.4)。其六,線性資料模擬的收斂證據:隱藏單元從 2 增至 200,敏感度經驗分布的標準差由約 0.109 單調縮至約 0.027,含 99% 信賴區間表;並以「零隱藏層網路」「單隱藏層網路」「OLS」三種模型的敏感度函數形式對照表,展示三者在線性資料上估出同一組係數——本課第三節的降落傘測試是這組實驗的精簡重演(pp.174–176,表 5.1–5.3,圖 5.5,式 5.16)。其七,兩組真實市場實證的完整設定:標普 500 前 250 大市值股(去缺值後 218 檔)配六個估值與規模因子跑一百個月,以及羅素 3000 指數 3290 檔配約五十個因子的大型版本,含 L1 正則化對樣本內外誤差的影響曲線、資訊比率對照與五十因子的逐項定義表(pp.177–183,表 5.4,圖 5.6–5.11)。其八,附錄對部分依賴圖(PDP)的定義式與侷限討論(pp.185–186,式 5.20–5.21)。其九,習題六題,含把敏感度函式推廣到 L 層與固定總單元數下層數對可解釋性影響的實驗(pp.184–185)。原書第五章對應印刷頁 pp.167–188。
§04作業和解答
作業一:解析 Jacobian 對 數值梯度
延續第四節訓練好的網路(變數 W1, b1, W2, b2, X, p):在樣本點 X[0] 上,(1) 用中央差分數值估計每個輸入的偏導數;(2) 用第二節的閉式 Jacobian 公式算解析值;(3) 比較兩者。說明這個核對在實務流程裡的位置。
解答 SOLUTION
def net(x):
return float(np.tanh(x @ W1.T + b1) @ W2.ravel() + b2[0])
x0 = X[0]
h = 1e-5
num = np.zeros(p)
for i in range(p):
e = np.zeros(p); e[i] = h
num[i] = (net(x0 + e) - net(x0 - e)) / (2 * h) # 中央差分
Hh0 = np.tanh(x0 @ W1.T + b1)
ana = ((1 - Hh0 ** 2) * W2.ravel()) @ W1 # 閉式 Jacobian
print("解析 Jacobian:", np.round(ana, 6))
print("數值中央差分 :", np.round(num, 6))
print("最大差異:", np.abs(ana - num).max())
# 輸出:
# 解析 Jacobian: [1.079164 1.057783 1.186666 0.633519 0.02688 ]
# 數值中央差分 : [1.079164 1.057783 1.186666 0.633519 0.02688 ]
# 最大差異: 1.0000567041146269e-10兩組數字到小數第六位完全一致,最大差異約 \(10^{-10}\)——正是中央差分(截斷誤差階數為步長平方)疊上浮點捨入後該有的量級,證明解析式推導與實作都對。實務位置:敏感度報表一旦要進模型驗證文件,「解析梯度對數值梯度」就是標配的單元測試——解析式算得快、可大規模跑,數值差分慢但幾乎不會寫錯,用後者替前者背書。任何一次重構或換激活函數後,這個核對都應該自動重跑。順帶注意 \(X_5\) 的敏感度在這個點只有 0.027——無關特徵在逐點層面也乾淨地貼近零。
作業二:寬度、激活函數與敏感度的穩定性
用第三節的線性資料(真實係數 0.8/−0.5/0.3/0),分別以 tanh 與 ReLU 訓練寬度 4、16、64 的單隱藏層網路,觀察 \(X_1\) 敏感度的「平均」與「跨樣本標準差」如何隨寬度變化。資料是線性的,真實敏感度是常數 0.8——所以跨樣本標準差全部是多出來的雜訊。你預期看到什麼?
解答 SOLUTION
import numpy as np
rng = np.random.default_rng(5)
n, p = 400, 4
X = rng.normal(0.0, 1.0, size=(n, p))
y = X @ np.array([0.8, -0.5, 0.3, 0.0]) + rng.normal(0.0, 0.1, size=n)
def train(act, H, seed, iters=3000):
r = np.random.default_rng(seed)
W1 = r.normal(0, 0.5, size=(H, p)); b1 = np.zeros(H)
W2 = r.normal(0, 0.5, size=(1, H)); b2 = np.zeros(1)
params = [W1, b1, W2, b2]
mom = [np.zeros_like(q) for q in params]
vel = [np.zeros_like(q) for q in params]
for t in range(1, iters + 1):
I = X @ W1.T + b1
if act == "tanh":
Hh = np.tanh(I); dact = 1 - Hh ** 2
else: # ReLU
Hh = np.maximum(I, 0); dact = (I > 0).astype(float)
err = Hh @ W2.ravel() + b2[0] - y
dH = np.outer(err, W2.ravel()) * dact
grads = [dH.T @ X / n, dH.mean(axis=0),
(err @ Hh)[None, :] / n, np.array([err.mean()])]
for k in range(4):
mom[k] = 0.9 * mom[k] + 0.1 * grads[k]
vel[k] = 0.999 * vel[k] + 0.001 * grads[k] ** 2
step = (mom[k] / (1 - 0.9 ** t)) / (np.sqrt(vel[k] / (1 - 0.999 ** t)) + 1e-8)
params[k] -= 0.01 * step
I = X @ W1.T + b1
dact = 1 - np.tanh(I) ** 2 if act == "tanh" else (I > 0).astype(float)
return (dact * W2.ravel()) @ W1 # 逐點敏感度
for act in ("tanh", "relu"):
for H in (4, 16, 64):
J = train(act, H, seed=7)
print(f"{act:>4s} H={H:>2d} X1 平均敏感度 {J[:,0].mean():.3f} 跨樣本 std {J[:,0].std():.3f}")
# 輸出:
# tanh H= 4 X1 平均敏感度 0.795 跨樣本 std 0.035
# tanh H=16 X1 平均敏感度 0.799 跨樣本 std 0.091
# tanh H=64 X1 平均敏感度 0.793 跨樣本 std 0.193
# relu H= 4 X1 平均敏感度 0.792 跨樣本 std 0.033
# relu H=16 X1 平均敏感度 0.795 跨樣本 std 0.093
# relu H=64 X1 平均敏感度 0.803 跨樣本 std 0.290三個讀點。第一,平均值全程穩:六種設定的平均敏感度都落在 0.79–0.80,離真值 0.8 一步之內——彙總後的敏感度對架構選擇相當強韌,這是它能當報表指標的本錢。第二,逐點讀數隨寬度變毛:跨樣本標準差從窄網路的 0.03 一路放大到寬網路的 0.19–0.29。資料明明是線性的,真實斜率處處相同,多出來的離散全是「網路用多餘容量把雜訊彎進模型」的痕跡——寬度是表達力,也是解讀雜訊的放大器。第三,同寬度下 ReLU 比 tanh 更毛(0.290 對 0.193):ReLU 的逐點導數是「開/關」的階梯讀數,樣本落在不同的線性分段上,Jacobian 就跳一階。原書把這件事做成了定理:ReLU 網路 Jacobian 的變異數界隨隱藏單元數上升。給實務的訊息:要讀逐點敏感度(而不只是平均)時,控制寬度、用平滑激活、必要時上正則化——可解釋性是要在架構選擇裡付出代價去買的。
作業三:被平均抹掉的特徵
構造資料 \(Y = X_1^2 + 0.5X_2 + \varepsilon\)(\(X_1, X_2\) 標準常態):\(X_1\) 是主要驅動力但只有二次效果,\(X_2\) 是次要的線性項。訓練 tanh 網路後,分別用「平均敏感度」與「平均絕對敏感度」給兩個特徵排序。哪個彙總方式會把 \(X_1\) 排錯?先用理論算出兩種彙總各自的期望值,再實跑驗證。
解答 SOLUTION
理論先行:\(\partial Y/\partial X_1 = 2X_1\)。帶號平均 \(\mathbb{E}[2X_1] = 0\)——拋物線左坡的負斜率與右坡的正斜率完美抵銷;絕對平均 \(\mathbb{E}|2X_1| = 2\sqrt{2/\pi} \approx 1.596\)。\(X_2\) 兩種彙總都是 0.5。所以帶號平均會把主驅動力 \(X_1\) 排在 \(X_2\) 之後、甚至判成無關。
import numpy as np
rng = np.random.default_rng(21)
n = 500
X = rng.normal(0.0, 1.0, size=(n, 2))
y = X[:, 0] ** 2 + 0.5 * X[:, 1] + rng.normal(0.0, 0.1, size=n)
r = np.random.default_rng(33)
H = 12
W1 = r.normal(0, 0.4, size=(H, 2)); b1 = np.zeros(H)
W2 = r.normal(0, 0.4, size=(1, H)); b2 = np.zeros(1)
params = [W1, b1, W2, b2]
mom = [np.zeros_like(q) for q in params]
vel = [np.zeros_like(q) for q in params]
for t in range(1, 6001):
Hh = np.tanh(X @ W1.T + b1)
err = Hh @ W2.ravel() + b2[0] - y
dH = np.outer(err, W2.ravel()) * (1 - Hh ** 2)
grads = [dH.T @ X / n, dH.mean(axis=0),
(err @ Hh)[None, :] / n, np.array([err.mean()])]
for k in range(4):
mom[k] = 0.9 * mom[k] + 0.1 * grads[k]
vel[k] = 0.999 * vel[k] + 0.001 * grads[k] ** 2
step = (mom[k] / (1 - 0.9 ** t)) / (np.sqrt(vel[k] / (1 - 0.999 ** t)) + 1e-8)
params[k] -= 0.01 * step
Hh = np.tanh(X @ W1.T + b1)
J = ((1 - Hh ** 2) * W2.ravel()) @ W1
print("平均敏感度 :", np.round(J.mean(axis=0), 3))
print("平均絕對敏感度:", np.round(np.abs(J).mean(axis=0), 3))
# 輸出:
# 平均敏感度 : [0.083 0.496]
# 平均絕對敏感度: [1.57 0.496]實跑與理論吻合:帶號平均給 \(X_1\) 的分數是 0.083(理論 0)——比 \(X_2\) 的 0.496 低一大截,排序錯誤;絕對平均給 1.57(理論 1.596),排序正確。這是第四節「對稱抵銷」陷阱的極端版:連交互作用都不需要,一個 U 形效果就足以讓帶號平均失明。金融裡 U 形關係並不稀奇——換手率過低(乏人問津)與過高(過熱)都對後續報酬不利、中間最好,就是典型例子。補救原則同第四節:排序用絕對值或平方彙總;帶號平均留著讀淨方向;更完整的做法是直接看敏感度的分布(例如按 \(X_1\) 正負分組各自平均,會讀出「左坡負、右坡正」的完整形狀)。一個數字說不完的故事,就不要硬用一個數字說。
§05參考資料
- Board of Governors of the Federal Reserve System (2011). SR 11-7: Guidance on Model Risk Management — 美國模型風險管理的監管基準文件,「有效挑戰」原則的出處。
- Dixon, M. F., & Polson, N. G. (2019). Deep Fundamental Factor Models. arXiv:1903.07677 — 本章第六節深度基本面因子模型的學術原型,含敏感度框架與大型股票池實證。
- Ribeiro, M. T., Singh, S., & Guestrin, C. (2016). "Why Should I Trust You?": Explaining the Predictions of Any Classifier. arXiv:1602.04938 — LIME 原始論文:以局部代理模型解釋任意分類器的模型無關路線。
- Lundberg, S. M., & Lee, S.-I. (2017). A Unified Approach to Interpreting Model Predictions. arXiv:1705.07874 — SHAP 原始論文:用 Shapley 值統一多種特徵歸因方法。
- Sundararajan, M., Taly, A., & Yan, Q. (2017). Axiomatic Attribution for Deep Networks. arXiv:1703.01365 — 積分梯度法:以公理化方式處理梯度飽和問題的深度網路歸因方法,與本章第四節的飽和陷阱直接相關。
- Molnar, C. Interpretable Machine Learning(免費線上書) — 可解釋機器學習的系統性教科書,PDP、LIME、SHAP 各方法的完整處理。
- Dixon, M. F., Halperin, I., & Bilokon, P. (2020). Machine Learning in Finance: From Theory to Practice. Springer — 本課程對照之原書,第五章 pp.167–188。