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

CHAPTER 04 / 12 · PART 1 · 橫斷面資料的監督式學習

前饋神經網路

Feedforward Neural Networks

拆解前饋網路的架構與幾何直覺,親手實作反向傳播,建立金融時序資料的訓練與正則化紀律。

§01學習重點

§02課程內容

一、解剖一台前饋機器:深度、寬度、激活函數

第 1 章已經給了前饋神經網路一個素描:它是「線性變換、彎一下、再線性變換、再彎一下」的疊加,而且我們證明過,拿掉彎折(激活函數)之後,不管疊幾層都會塌縮回一條線性迴歸。這一章要把那張素描放大成工程藍圖:彎折到底怎麼選?層數與每層神經元數怎麼定?裝好之後怎麼訓練、怎麼驗證、怎麼防它學壞?

先把零件攤開。一個神經元做兩件事:把上一層的輸出加權求和再加偏移(這是線性部分),然後把結果丟進激活函數彎一下(這是非線性部分)。同一層的神經元各自擁有一組權重、平行運算,這一層的神經元個數叫寬度;層層堆疊的層數叫深度。「前饋」的意思是訊號只往前流——輸入進、預測出,中間不回頭、不循環(會回頭的循環網路是第 8 章的主角)。所以設計一個前饋網路,本質上只有三個自由度:多深、多寬、用什麼彎折

激活函數是三者中最容易被初學者忽略、卻最影響訓練成敗的一個。常用的三種:

$$ \mathrm{ReLU}(z) = \max(z, 0), \qquad \sigma(z) = \frac{1}{1 + e^{-z}}, \qquad \tanh(z) = \frac{e^{z} - e^{-z}}{e^{z} + e^{-z}} $$

逐項拆解:ReLU(修正線性單元)把負數斬成零、正數原樣放行,計算成本近乎零,導數不是 0 就是 1;sigmoid 把任何實數壓進 0 與 1 之間,天生適合當「機率」用,但它在兩端趨於平坦——輸入很大或很小時導數幾乎是零,而且導數最大也只有 0.25;tanh 形狀跟 sigmoid 像,但輸出範圍是 −1 到 1、以零為中心,導數最大可到 1。這些導數上限不是數學花絮:第五節會看到,訓練訊號要靠「逐層相乘的導數」往回傳,每過一層 sigmoid 就至少縮成四分之一,深一點的網路訊號傳到前排已經接近零——這叫梯度消失,是 sigmoid 退出隱藏層舞台、ReLU 上位的主因。實務起手式:隱藏層用 ReLU(或其變體),輸出層看任務——迴歸用線性輸出,二元分類用 sigmoid,多類別用 softmax。

零件講完,直接解剖一台最小可運轉的機器——兩個特徵、三個隱藏神經元、一個輸出:

PYTHON
import numpy as np

# 一台 2-3-1 的迷你前饋機器:權重先手動給定(訓練是第五節的事)
W1 = np.array([[ 0.8, -0.5],
               [-0.6,  0.9],
               [ 0.3,  0.7]])          # 隱藏層權重:3 個神經元 × 2 個特徵
b1 = np.array([0.1, -0.2, 0.0])        # 隱藏層偏移
W2 = np.array([[0.9, -0.4, 0.6]])      # 輸出層權重
b2 = np.array([0.02])

x = np.array([0.6, -0.4])              # 某檔股票今日:動能因子 +0.6、估值因子 -0.4
z1 = W1 @ x + b1                       # 線性部分:加權求和
h1 = np.maximum(z1, 0.0)               # 非線性部分:ReLU 彎折
y_hat = W2 @ h1 + b2                   # 輸出層(迴歸任務:線性輸出)
print("z1 =", np.round(z1, 3))         # 輸出:z1 = [ 0.78 -0.92 -0.1 ]
print("h1 =", np.round(h1, 3))         # 輸出:h1 = [0.78 0.   0.  ]
print("ŷ  =", np.round(y_hat, 4))      # 輸出:ŷ  = [0.722]

注意中間發生的事:三個神經元的線性部分算出 \(z_1 = (0.78, -0.92, -0.10)\),經過 ReLU 之後只有第一個活著,另外兩個被斬成零——對這筆輸入,網路實際上只動用了三分之一的零件。換一筆輸入,活著的組合就換一批。這個「不同輸入啟動不同子網路」的性質不是缺陷,正是下一節的主角。

二、幾何直覺:每個神經元都在摺空間

上一節的觀察值得放大:一個 ReLU 神經元對輸入空間做的事,是畫一條直線(高維時是一張超平面),把空間分成兩半——一半照常放行,另一半歸零。輸入空間裡每個點會落在「哪些神經元活著、哪些死了」的某個組合裡,而每個組合對應空間中的一塊凸區域;在同一塊區域內,活著的神經元固定,整台網路就是一條普通的線性函數。所以 ReLU 網路的全貌,是一張由許多平面片拼起來的分段線性曲面——它的「彎」,全部發生在區域的交界摺線上。

比喻: 摺紙。一張白紙是線性模型——再怎麼平移旋轉,它還是平的。每個 ReLU 神經元等於沿一條線把紙摺一次;第一層的 n 個神經元摺出 n 條摺痕,把紙面分成一塊塊平坦的小面。深度網路更狠:第二層是在已經摺過的紙上再摺,一刀下去,先前的每塊小面同時被切——摺痕數不再一條條累加,而是成倍翻升。紙還是那張紙,摺法決定了它能貼合多複雜的形狀。

區域數可以數。平面上 n 條直線最多把空間切成

$$ R(n) = 1 + n + \binom{n}{2} $$

塊。逐項拆解:1 是還沒切之前的整張平面;n 是每條線至少新增一塊;\(\binom{n}{2}\) 是兩兩相交帶來的額外分割——三個神經元最多 \(1+3+3=7\) 塊區域,模型就有 7 段不同的線性行為。關鍵在深度的效果:淺網路的區域數隨寬度多項式成長,深網路卻能讓區域數隨層數指數成長——摺過再摺,每一層的摺痕都作用在前面所有層摺出的每一塊小面上。這是「深度學習」的「深」第一個扎實的幾何理由。

對金融讀者,這個幾何觀點還有一層實用價值:它把「黑盒子」變成可檢查的物件。網路對某筆輸入的預測,等於「這筆輸入落在哪塊區域、那塊區域的線性係數是多少」——第 5 章講可解釋性時,我們會順著這條路把網路在單一樣本附近的行為攤開來看。

三、夠寬就能逼近,那為什麼要深?

第 1 章提過通用逼近定理:只要一個隱藏層加上非線性激活,神經網路就能以任意精度逼近任何連續函數。這句話聽起來像萬能保證書,這裡要把它的三個「但書」攤開,因為每一個都直接關係到你敢不敢把錢押在模型上。

但書一:它不告訴你要多寬。定理只說「存在一個夠寬的網路」,沒說夠寬是多寬。對某些函數,需要的神經元數會隨輸入維度指數爆炸——保證存在,不保證付得起。但書二:它不告訴你學不學得到。存在一組完美權重,跟梯度下降能從隨機起點走到那組權重,是兩回事;第六節會看到損失面崎嶇不平,訓練完全可能停在半山腰。但書三:它對樣本外隻字未提。逼近講的是「把訓練資料上的函數貼合好」,而第 1 章已經演示過,貼合得越好越可能是背雜訊——泛化要靠本章後半的驗證紀律與正則化來把守,定理幫不上忙。

那深度呢?既然一層夠寬就萬能,何必疊層?上一節的摺紙已經給了幾何答案,這裡補上資源帳:對一類「自我重複」結構的函數(鋸齒波是教科書例子),理論上可以證明——深網路用隨深度多項式成長的神經元就能表達,淺網路要達到同樣精度卻需要指數多的神經元。直覺是:深度做的是「函數的複合」——摺完再摺,每層重複利用前一層造出的結構;寬度做的是「函數的相加」——每個神經元各自為政,拼不出重複利用。當目標函數本身有層次結構(特徵的特徵、模式中的模式),深度是壓倒性省料的表達方式。

但金融讀者請把下面這句話跟定理一起收好:深度的表達力優勢,要在訊號夠強的問題上才兌現。影像與語音的訊噪比極高,堆深有回報;金融預測的訊噪比低得多——可預測成分常常只佔報酬變異的百分之幾——表達力早就不是瓶頸,過擬合才是。實務上金融表格資料的網路多半只有二到四層隱藏層,配上重手的正則化;把 ImageNet 的百層架構搬進日報酬預測,是把武器對錯了戰場。

四、訓練的紀律:三分法,與金融時序的特別規矩

裝好機器,下一步是餵資料。但在按下訓練鍵之前,先把資料紀律立好——這一節是全章最「不數學」的一節,卻可能是最能幫你省錢的一節。

標準做法是把資料切成三份,各司其職:訓練集用來學權重;驗證集用來選超參數——深度、寬度、學習率、正則化強度,這些不是梯度下降學出來的,是你拿驗證表現一輪輪挑出來的;測試集只做一件事:在所有決定都定案之後,給模型打最後一次分數。測試集的紀律是「只能用一次」——一旦你看著測試分數回頭改模型,它就降格成第二個驗證集,你的「樣本外表現」就開始灌水。

麻煩在金融資料的切法。教科書的預設是隨機切:把樣本洗勻、隨機抽三份。這在樣本彼此獨立時沒問題,但金融時序資料的相鄰樣本高度糾纏:今天的估值因子跟昨天的幾乎一樣(因子有持續性),「未來一週報酬」這種標籤在相鄰兩天共享四天的報酬(標籤重疊)。隨機切會把這些糾纏的樣本拆散到訓練集與驗證集兩邊——驗證集裡的每道「考題」,訓練集裡都躺著一個近乎相同的雙胞胎。

比喻: 把一本日記整本撕碎、紙片混勻,抽一部分當「考卷」考自己還原內容。你會考得驚人地好——因為每張考卷紙片的前一頁後一頁都在你手上,拼一下就知道答案。這不是記憶力好,是考卷洩了題。金融資料的隨機切就是撕日記:相鄰交易日互為前後頁,考題和答案本來就是連著的。

空口無憑,做實驗。造一個確定沒有訊號的世界:三個持續性因子(像估值、動能這種水位緩慢移動的變數)當特徵,未來五天累積報酬當標籤,而報酬是純雜訊——任何模型的真實預測力都應該是零。訓練同一個網路,只換切法:

PYTHON
import numpy as np

rng = np.random.default_rng(11)
n, rho = 600, 0.97
# 三個持續性因子:AR(1),今天的水位 ≈ 昨天的水位(像估值、動能因子)
eps = rng.normal(0, np.sqrt(1 - rho**2), (n + 1, 3))
F = np.zeros((n + 1, 3))
for t in range(1, n + 1):
    F[t] = rho * F[t - 1] + eps[t]
r = rng.normal(0.0, 1.0, n + 6)        # 日報酬(%):純雜訊,世界裡沒有訊號
X = F[1:]                              # 特徵:三個因子今日水位
y = np.array([r[t + 1:t + 6].sum() for t in range(n)])   # 標籤:未來 5 天累積報酬

def train_mlp(Xtr, ytr, hidden=64, lr=0.05, epochs=3000, seed=3):
    rg = np.random.default_rng(seed)
    W1 = rg.normal(0, 0.5, (hidden, Xtr.shape[1])); b1 = np.zeros(hidden)
    W2 = rg.normal(0, 0.5, (1, hidden)); b2 = np.zeros(1)
    m = len(Xtr)
    for ep in range(epochs):                     # 全批次梯度下降(第五節詳解)
        H1 = np.tanh(Xtr @ W1.T + b1)
        pred = (H1 @ W2.T + b2).ravel()
        err = (pred - ytr) * 2 / m
        gW2 = err[None, :] @ H1; gb2 = err.sum(keepdims=True)
        dZ = (err[:, None] @ W2) * (1 - H1**2)
        gW1 = dZ.T @ Xtr; gb1 = dZ.sum(0)
        W1 -= lr * gW1; b1 -= lr * gb1
        W2 -= lr * gW2; b2 -= lr * gb2
    return lambda Xq: (np.tanh(Xq @ W1.T + b1) @ W2.T + b2).ravel()

perm = np.random.default_rng(0).permutation(n)
va_r, tr_r = np.sort(perm[:120]), np.sort(perm[120:])   # 切法一:隨機切
f = train_mlp(X[tr_r], y[tr_r])
print(f"隨機切|驗證 MSE {np.mean((f(X[va_r]) - y[va_r])**2):.3f}"
      f"|猜零基準 {np.mean(y[va_r]**2):.3f}")
# 輸出:隨機切|驗證 MSE 4.516|猜零基準 5.459

f2 = train_mlp(X[:480], y[:480])                        # 切法二:沿時間切
print(f"時間切|驗證 MSE {np.mean((f2(X[480:]) - y[480:])**2):.3f}"
      f"|猜零基準 {np.mean(y[480:]**2):.3f}")
# 輸出:時間切|驗證 MSE 6.575|猜零基準 4.786

判讀方式:跟「什麼都不學、一律猜零」的基準比。隨機切的驗證 MSE 是 4.516,比基準 5.459 低了 17%——模型看起來找到了訊號,但我們知道這個世界裡根本沒有訊號,這 17% 全部是洩漏出來的假象:網路把訓練樣本的雜訊背下來,驗證時它的「雙胞胎」就在旁邊對答案。沿時間切的驗證 MSE 是 6.575,高於基準 4.786——誠實地宣告「這裡沒有可學的東西」。同一筆資料、同一台模型,切法決定你看到真相還是幻覺。

規矩因此只有一條硬的:金融時序資料一律沿時間切——訓練集在前、驗證集在後、測試集在最後,模擬「用過去學、對未來下注」的真實處境;標籤若有前瞻窗(例如未來五天報酬),訓練與驗證的交界處還要再挖掉一段等長的間隔(gap),把跨界的重疊標籤清掉。這是第 1 章前視偏差的操作版:那裡說的是「未來的資訊不准進特徵」,這裡再加一條「未來的資訊也不准經由切法滲進訓練集」。

五、學習的引擎:SGD 與反向傳播

紀律立好,可以點火了。訓練的目標是找一組權重讓損失函數最小——迴歸用均方誤差,分類用交叉熵(第 1 章講過:最小化交叉熵就是最大化概似,第 2 章又從貝葉斯角度把概似講透了,這裡直接沿用)。問題是:一個中型網路動輒數萬個參數,怎麼找?

答案是最不浪漫的方法:梯度下降。算出損失對每個參數的偏導數(梯度指向損失上升最快的方向),然後往反方向挪一小步,重複到收斂:

$$ \theta_{k+1} = \theta_k - \eta \, g_k, \qquad g_k = \frac{1}{|B_k|} \sum_{i \in B_k} \nabla_\theta \, \mathcal{L}\big(y_i, \hat{y}_i(\theta_k)\big) $$

逐項拆解:\(\theta_k\) 是第 k 步的全部參數打包成的向量;\(\eta\) 是學習率——每步跨多大,本章最重要的超參數;\(g_k\) 是梯度估計:不用全部樣本,只抽一小批 \(B_k\)(mini-batch,通常 32 到 256 筆)算平均梯度。抽批次有兩個理由:一是快——梯度品質隨批次大小的報酬遞減,全批次算得貴又沒好多少;二是妙——抽樣帶來的雜訊讓每步方向微微抖動,反而幫優化器晃出淺坑與鞍點。這個「故意帶雜訊的梯度下降」就是 SGD(隨機梯度下降)。

比喻: 濃霧夜裡下山。你看不到全景,只感覺得到腳下這塊地往哪邊傾斜(mini-batch 梯度:局部、帶噪,但夠用),於是朝最陡的下坡跨一步,重複。步幅就是學習率:太小,天亮了還在山頂附近;太大,一步跨過谷底撞上對面山壁,來回彈跳越彈越高。起霧其實有好處——偶爾踩偏的一步,反而讓你不會困死在半山腰的小凹地裡。

剩下的問題是:幾萬個參數的梯度怎麼算得動?這就是反向傳播——名字神祕,本體是「鏈式法則+不重算的記帳法」。網路是函數的複合,複合函數求導靠鏈式法則逐層相乘;反向傳播的聰明處在記帳順序:先做一趟前向傳播,把每層的中間輸出存起來;再從輸出層開始倒著走,把「損失對這一層輸入的敏感度」(記作 \(\delta\))一層層往回傳:

$$ \delta^{(\ell)} = \Big( \delta^{(\ell+1)} \, W^{(\ell+1)} \Big) \odot \sigma'\big(z^{(\ell)}\big), \qquad \nabla_{W^{(\ell)}} \mathcal{L} = \delta^{(\ell) \top} h^{(\ell-1)} $$

逐項拆解:\(\delta^{(\ell+1)}\) 是下一層已經算好的敏感度——乘上權重 \(W^{(\ell+1)}\) 等於問「這層的輸出經過下一層,對損失有多大影響」;\(\odot\) 是逐元素相乘,乘上激活函數的導數 \(\sigma'\)——彎折越平坦的地方,訊號被折損越多(sigmoid 導數上限 0.25,深層網路逐層乘 0.25 就是梯度消失的數學現場;ReLU 導數是 1,訊號原封不動通過,這是它統治隱藏層的第二個理由);右式說:這一層權重的梯度,就是「本層敏感度」與「上一層輸出」的外積——兩樣東西都是現成的,一趟回程全部算完,成本跟前向傳播同一個量級。

白話講完,親手做一次。手寫一台 1-8-1 網路,學一條非線性的因子—報酬關係:

PYTHON
import numpy as np

rng = np.random.default_rng(21)
n = 256
X = rng.uniform(-2.0, 2.0, size=(n, 1))                 # 某因子的水位
y = 0.03 * np.tanh(1.5 * X[:, 0]) + rng.normal(0, 0.005, n)   # 真實關係:飽和曲線+雜訊

W1 = rng.normal(0, 0.5, (8, 1)); b1 = np.zeros(8)       # 1-8-1 網路
W2 = rng.normal(0, 0.5, (1, 8)); b2 = np.zeros(1)
lr, batch = 0.1, 32

for epoch in range(401):
    if epoch in (0, 50, 400):
        pred = (np.tanh(X @ W1.T + b1) @ W2.T + b2).ravel()
        print(f"epoch {epoch:>3d}  train MSE {np.mean((pred - y)**2):.6f}")
    perm = rng.permutation(n)
    for s in range(0, n, batch):                        # mini-batch SGD
        idx = perm[s:s + batch]
        xi, yi = X[idx], y[idx]
        H1 = np.tanh(xi @ W1.T + b1)                    # —— 前向傳播(存中間值)
        pred = (H1 @ W2.T + b2).ravel()
        err = (pred - yi) * 2 / len(idx)                # dL/dpred
        gW2 = err[None, :] @ H1; gb2 = err.sum(keepdims=True)   # —— 反向傳播
        dZ = (err[:, None] @ W2) * (1 - H1**2)          # δ = (δ·W) ⊙ tanh'
        gW1 = dZ.T @ xi; gb1 = dZ.sum(0)
        W1 -= lr * gW1; b1 -= lr * gb1                  # —— 更新
        W2 -= lr * gW2; b2 -= lr * gb2
# 輸出:
# epoch   0  train MSE 4.501210
# epoch  50  train MSE 0.000046
# epoch 400  train MSE 0.000023

四百輪之後 MSE 收到 \(2.3\times 10^{-5}\)——正好是雜訊的變異 \(0.005^2 = 2.5\times 10^{-5}\):網路把可學的曲線全學走了,剩下的正是不可學的雜訊底線(第 1 章的不可約誤差,在這裡跟你打了照面)。整台引擎——前向、反向、更新——不過十幾行 numpy。深度學習框架做的事跟這十幾行本質相同,只是把「手推 \(\delta\) 遞迴」換成自動微分。

實務上還有一個常用的加裝件:動量。把每步更新改成「這步梯度」與「上步方向」的加權和,像推著一台有慣性的推車下山——連續同向的梯度越推越快,方向亂跳的雜訊被慣性抹平。Adam 這類自適應優化器再進一步,替每個參數各配一支獨立的學習率。原書對這一族優化器有完整的族譜,本課點到為止。

六、損失面是山脈,不是碗:非凸、初始化與學習率

線性迴歸的損失面是一只碗——凸函數,從任何起點滾下去都到同一個底。神經網路不是:權重之間存在大量對稱與交互作用,損失面是一整片山脈——多個谷底(局部極小)、大片鞍點、狹長峽谷。這帶來兩個訓練現場天天上演的現象,用一個一維玩具就能重現:

PYTHON
import numpy as np

def L(w):  return (w**2 - 1.0)**2 + 0.3 * w + 0.4     # 一維雙谷損失面(非凸)
def dL(w): return 4.0 * w * (w**2 - 1.0) + 0.3

for w0, lr in [(-1.6, 0.05), (1.6, 0.05), (-1.6, 0.35)]:
    w = w0
    for _ in range(60):
        w -= lr * dL(w)                                # 純梯度下降
        if abs(w) > 50:
            break
    tag = "發散" if abs(w) > 50 else f"停在 w={w:+.3f},L={L(w):.3f}"
    print(f"起點 {w0:+.1f}|學習率 {lr:.2f} -> {tag}")
# 輸出:
# 起點 -1.6|學習率 0.05 -> 停在 w=-1.036,L=0.095
# 起點 +1.6|學習率 0.05 -> 停在 w=+0.960,L=0.694
# 起點 -1.6|學習率 0.35 -> 發散

現象一:終點由起點決定。同一片損失面、同一個學習率,從 −1.6 出發滾進全域谷底(L=0.095),從 +1.6 出發被困在局部谷底(L=0.694)——中間隔著一道 L≈1.41 的山脊,純梯度下降翻不過去。現象二:學習率過大直接毀滅。0.35 的步幅讓每一步都跨過谷底撞上更陡的對面坡,幾步之內彈出可用範圍。真實網路的損失面是這個玩具的百萬維版本,兩個現象照樣上演,只是看不見。

對應的工程對策各有一套。初始化:不能全零——同層神經元會完全對稱,梯度一模一樣,永遠學成同一個神經元(所謂對稱破缺失敗);也不能太大——激活函數直接飽和,梯度歸零學不動。標準做法是小尺度隨機初始化,且尺度隨神經元數縮放(讓每層輸出的變異大致守恆);更務實的保險是多組隨機起點各訓練一次,比較驗證表現——山脈地形裡,多買幾張入場券是便宜的。學習率:從保守值起步,觀察損失曲線——平滑下降是健康,鋸齒亂跳是偏大,直接爆炸是太大;訓練後期把學習率逐步調小(衰減排程),讓優化器在谷底細部收斂。早停(early stopping):每輪訓練順手算一次驗證損失,一旦驗證損失連續數輪回升就停手、退回最佳那一輪的權重。早停既是優化決策也是免費的正則化——它阻止網路把訓練集的雜訊繼續刻進權重,是實務上性價比最高的一道護欄。

順帶拆一個迷思:「局部極小是不是深度學習的致命傷?」經驗上,高維損失面的多數局部極小品質都不差,真正拖慢訓練的更多是大片平坦的鞍點區——而 SGD 的抽樣雜訊(第五節的「霧」)恰好是離開鞍點的好幫手。非凸性在理論上礙眼,在工程上可控。

七、給網路上韁繩:L2、dropout 與貝葉斯視角

網路的表達力越強,防過擬合的韁繩就要越硬。第 1 章立了「正則化=在損失函數裡對複雜度收費」的總綱,這裡放進網路的語境。

第一條韁繩:L2 權重衰減。把目標函數改成

$$ \min_{W, b} \; \frac{1}{N} \sum_{i=1}^{N} \mathcal{L}\big(y_i, \hat{y}_i\big) \; + \; \lambda \sum_{\ell} \big\lVert W^{(\ell)} \big\rVert_2^2 $$

逐項拆解:第一項是原本的平均損失(貼資料);第二項把所有層的權重平方和加總(複雜度罰金)——權重越大、曲面彎得越劇烈,罰得越重;\(\lambda\) 是罰金費率,也是你用驗證集挑的超參數:\(\lambda = 0\) 回到裸網路,\(\lambda\) 太大則把權重全壓向零、模型退化成猜平均。梯度下降的實作只差一行:每步更新多扣 \(2\lambda W\)——權重不用就會「衰減」,因此得名。第 3 章的貝葉斯眼鏡在這裡直接戴上:L2 罰項恰好等於給每個權重一個零均值高斯先驗再取最大後驗——「沒有強證據之前,先相信權重是小的」;同理 L1 罰項(權重絕對值和)對應 Laplace 先驗,它會把不重要的權重壓到精確為零,兼職做了特徵選擇。正則化不是工程補丁,是先驗信念的頻率派化名——這條橋在第 3 章搭好,本章直接通行。

第二條韁繩:dropout。訓練時每一步隨機讓每個隱藏神經元以機率 p(常用 0.2 到 0.5)「暫時消失」——本步的前向與反向都當它不存在;下一步重新抽一批消失名單。預測時全員上場,但把相應輸出縮放以對齊訓練時的期望值。

比喻: 球隊練習賽每場隨機抽幾個主力坐板凳。教練的盤算:逼所有戰術都不能依賴特定球星,每個位置都要有人能補——練出來的是一支去掉單點依賴的球隊。dropout 對網路做同一件事:任何特徵的作用都不能繫在單一神經元身上,因為它隨時可能缺席;知識被逼著冗餘分散到全網。正式比賽(預測)當然全員上場,而全隊實力等於訓練中千百種殘缺陣容的平均——dropout 同時也是一種廉價的模型平均。

三條韁繩——L2、dropout、早停——機制不同、可以並用;用多緊,一律由驗證集(沿時間切的那種)說了算。作業三會讓你親手掃一遍 \(\lambda\),看驗證誤差走出第 1 章那條 U 形曲線。最後留一個路標:如果把「給權重一個先驗」貫徹到底——不只取最大後驗,而是維護權重的整個後驗分布——就得到貝葉斯神經網路:預測不再是單一數字,而是自帶不確定性刻度的分布。這對「錯一次代價極高」的金融決策特別有吸引力,原書第四章末節有完整的變分推斷處理,屬於進階選讀。

八、上戰場:非線性因子模型與信用評分

零件、幾何、紀律、引擎、韁繩都齊了,收尾看兩個前饋網路在金融的主戰場,順便把本章的裝備清單各就各位。

戰場一:橫斷面報酬預測(非線性因子模型)。傳統因子模型是線性的:下期報酬 ≈ 各因子曝險的加權和。但越來越多實證證據顯示因子之間有交互作用——動能效應在小型股上明顯更強、估值訊號在高波動環境下另有一套行為——線性模型天生寫不出「A 的效果取決於 B」這種句子,而這正是第二節那張分段線性曲面的主場:不同的因子組合落在不同區域,各配一套局部係數。台股研究者拿本益比、市值、過去報酬、周轉率等特徵餵進兩三層的網路做橫斷面排序,是完全標準的做法。但裝備清單必須全程掛著:訊噪比極低(第三節:淺架構+重正則化)、資料必須沿時間切(第四節)、多起點訓練取穩健者(第六節)、L2 與早停常開(第七節)。實證文獻的共識也誠實:非線性帶來的改善是真實但溫和的幾個百分點,且大半來自交互作用項——網路賺的是「寫得出線性模型寫不出的句子」的錢,不是魔法的錢。

戰場二:信用評分。台灣讀者最熟的版本是聯徵中心的信用報告:收入、負債比、繳款紀錄、查詢次數等特徵,預測未來一段時間的違約機率——教科書級的二元分類,輸出層 sigmoid、損失用交叉熵,第一節與第五節的裝備直接套用。網路的增益同樣來自交互作用(例如「高負債比」的風險含義隨「收入穩定度」劇烈變化)。但這個戰場有兩條線性模型時代留下的硬約束:類別極度不平衡——違約者可能只佔百分之幾,天真訓練會學成「全部猜不違約」的殭屍模型,需要對少數類加權或重抽樣;單調性與可解釋性——監理機關與內部風控會要求「負債比上升、核准機率不得上升」這類常識約束,而裸網路不保證單調,需要架構上的約束設計,且拒貸決策必須說得出理由。這條「說出理由」的線索,正好把我們送進第 5 章:神經網路的可解釋性。

兩個戰場的共同教訓,也是本章的總結:前饋網路在金融的成敗,架構只佔小半,紀律佔大半。切錯資料,再深的網路都在考自己背過的題;紀律到位,三層小網路就足以把非線性的錢賺走。

§03原書對照

本課以白話重組了原書第四章的工程主線,以下內容原書有、但本課未展開,按頁碼供進階讀者深入。其一,幾何與機率的補充論證:以高維隨機向量的期望距離上界(隨維度的平方根成長)說明資料在高維空間的稀疏化,並推導隱藏層寬度如何影響輸出向量的分離程度(pp.117–119,式 4.6–4.11)。其二,通用逼近定理的正式版本:以 Borel 可測函數與緊集上一致收斂的語言陳述,並討論定理的非建構性(pp.119–120)。其三,統計學習理論一脈:VC 維度、打散(shattering)、經驗風險最小化,以及期望風險以 VC 維度與樣本數表達的機率上界,含 ReLU 深網路 VC 維度與參數量的關係(pp.120–123,式 4.13–4.15)。其四,神經網路作為樣條的觀點:用 Heaviside 激活構造分段常數逼近並給出逐點誤差界,延伸到 Voronoi 分割的高維推廣(pp.124–127,式 4.16–4.22)。其五,「為什麼要深」的三條理論線:線性區域數隨深度的成長階、以二進位展開多項式的深網路構造(定理 4.2)、鋸齒函數在淺網路上的分類誤差下界(定理 4.3),本課第三節的資源帳即濃縮自此(pp.127–132)。其六,凸性與不等式約束:權重非負可保網路輸出對輸入凸,據此以受約束網路逼近歐式選擇權價格曲面與隱含波動率微笑(例 4.3–4.4),並延伸到 Dupire 局部波動率校準的反問題(pp.132–138)。其七,MLP 與其他監督學習器的功能等價與對照表:投影追蹤迴歸、核方法、樹模型的正則化參數整理(pp.138–139,表 4.1)。其八,softmax 的導數推導與反向傳播的完整矩陣式推導,含三層網路的手算範例(pp.141–146、158–162)。其九,優化器族譜的更新方程:動量法、Nesterov 加速、AdaGrad、RMSProp、Adam,以及二階方法在深度學習不實用的原因(pp.146–148)。其十,dropout 的伯努利遮罩形式化,及其與 ridge 迴歸和 g-先驗的理論關聯(p.148)。其十一,貝葉斯神經網路全節:變分推斷、KL 散度與證據下界(ELBO)、重參數化技巧、MCMC 取樣的計算瓶頸,與 dropout 作為不確定性估計的詮釋(pp.149–152)。其十二,章末習題含 Anscombe 四重奏的網路迴歸、半月資料集的深淺分類器對照與單輪反向傳播的手算(pp.153–157)。原書第四章對應印刷頁 pp.111–166。

§04作業和解答

作業一:梯度數值檢查——證明你的反向傳播沒寫錯

手寫反向傳播最大的風險是「梯度算錯但訓練照樣會動」——錯的梯度往往仍指向大致下坡的方向,損失照降,bug 藏到上線才爆。職業做法是梯度數值檢查:對每個參數 \(\theta_j\),用中央差分 \((\mathcal{L}(\theta_j + h) - \mathcal{L}(\theta_j - h)) / 2h\) 近似偏導數,跟反向傳播的解析梯度逐一比對相對誤差。請對一台 3-6-1 的 tanh 網路實作檢查,報告最大相對誤差。

解答 SOLUTION
PYTHON
import numpy as np

rng = np.random.default_rng(5)
Xh = rng.normal(size=(20, 3)); yh = rng.normal(size=20)
W1 = rng.normal(0, 0.5, (6, 3)); b1 = rng.normal(0, 0.1, 6)
W2 = rng.normal(0, 0.5, (1, 6)); b2 = np.zeros(1)

def loss_and_grads(W1, b1, W2, b2):
    H1 = np.tanh(Xh @ W1.T + b1)
    pred = (H1 @ W2.T + b2).ravel()
    err = (pred - yh) * 2 / len(yh)
    gW2 = err[None, :] @ H1; gb2 = err.sum(keepdims=True)
    dZ = (err[:, None] @ W2) * (1 - H1**2)
    gW1 = dZ.T @ Xh; gb1 = dZ.sum(0)
    return np.mean((pred - yh)**2), (gW1, gb1, gW2, gb2)

_, grads = loss_and_grads(W1, b1, W2, b2)
h, worst = 1e-6, 0.0
for p, g in zip([W1, b1, W2, b2], grads):
    it = np.nditer(p, flags=['multi_index'])
    for _ in it:
        i = it.multi_index
        old = p[i]
        p[i] = old + h; Lp, _ = loss_and_grads(W1, b1, W2, b2)
        p[i] = old - h; Lm, _ = loss_and_grads(W1, b1, W2, b2)
        p[i] = old
        num = (Lp - Lm) / (2 * h)                      # 中央差分近似
        ana = g.reshape(p.shape)[i]                    # 反向傳播解析梯度
        worst = max(worst, abs(num - ana) / max(1e-12, abs(num) + abs(ana)))
print(f"最大相對誤差: {worst:.2e}")
# 輸出:最大相對誤差: 5.95e-09

判讀:相對誤差 \(10^{-9}\) 量級,正是「解析梯度正確、殘差只剩浮點捨入」的特徵值。若你的實作出現 \(10^{-2}\) 這種量級,幾乎可以斷定某層的 \(\delta\) 遞迴寫錯了。兩個實務要點:檢查用小網路小樣本即可(數值差分要對每個參數跑兩次前向,貴);檢查一次通過後就可以關掉,正式訓練不跑。

作業二:洩漏的解剖——什麼條件下隨機切會說謊?

第四節的實驗裡,隨機切的假象來自「特徵有持續性+標籤有重疊」。請固定標籤重疊(未來 5 天累積報酬),把因子持續性 \(\rho\) 分別設為 0、0.8、0.97,每檔用「最會背答案」的 1-最近鄰模型(拿特徵空間中最近的訓練樣本的標籤當預測)對比隨機切與沿時間切的驗證 MSE。解釋:(1) 為什麼洩漏假象只在高持續性時出現?(2) 為什麼沿時間切對三種 \(\rho\) 都免疫?(3) 為什麼這裡刻意選 1-NN 而不是網路?

解答 SOLUTION
PYTHON
import numpy as np

n = 600
for rho in (0.0, 0.8, 0.97):
    rg = np.random.default_rng(11)
    scale = 1.0 if rho == 0 else np.sqrt(1 - rho**2)
    eps = rg.normal(0, scale, (n + 1, 3))
    F = np.zeros((n + 1, 3))
    for t in range(1, n + 1):
        F[t] = rho * F[t - 1] + eps[t]
    r = rg.normal(0, 1.0, n + 6)
    X = F[1:]
    y = np.array([r[t + 1:t + 6].sum() for t in range(n)])

    def onenn_mse(tr, va):                     # 1-最近鄰:純背答案模型
        out = []
        for j in va:
            d = np.sum((X[tr] - X[j])**2, axis=1)
            out.append((y[tr[np.argmin(d)]] - y[j])**2)
        return np.mean(out)

    p = np.random.default_rng(0).permutation(n)
    mse_r = onenn_mse(p[120:], p[:120])                       # 隨機切
    mse_t = onenn_mse(np.arange(480), np.arange(480, 600))    # 沿時間切
    print(f"rho={rho:.2f}  隨機切 {mse_r:.3f}  時間切 {mse_t:.3f}")
# 輸出:
# rho=0.00  隨機切 9.908  時間切 9.468
# rho=0.80  隨機切 10.271  時間切 10.017
# rho=0.97  隨機切 6.936  時間切 9.295

(1) 洩漏需要一條完整的通道:驗證樣本必須能在訓練集裡找到「特徵位置幾乎相同、標籤又幾乎相同」的雙胞胎。標籤重疊保證了時間上相鄰的樣本標籤相近,但只有當特徵也有持續性(\(\rho\) 高)時,時間上的鄰居才會同時是特徵空間裡的最近鄰——模型才找得到它。\(\rho = 0\) 或 0.8 時特徵每天大幅換位,最近鄰多半是不相干的樣本,其標籤與驗證標籤獨立,MSE 落在 \(2\times\) 標籤變異附近(兩個獨立變數之差的變異);\(\rho = 0.97\) 時最近鄰幾乎必是隔壁交易日,標籤差只剩一天雜訊,MSE 應聲跌到 6.9——低於誠實水準,假象成立。

(2) 沿時間切從結構上拆掉通道:驗證樣本的時間鄰居跟它一起被劃進驗證集,訓練集裡根本沒有雙胞胎可找——不管 \(\rho\) 多高,三行輸出的時間切 MSE 都停在誠實的 9 到 10 之間。

(3) 1-NN 是「記憶型模型」的極端純化:它除了背答案什麼都不會,洩漏通道存在與否直接反映在分數上,不被模型自身的擬合能力混淆。網路也會利用同一條通道(第四節已示範),但它的分數同時混雜了平滑與擬合的效果,做機制解剖時不如 1-NN 乾淨。

作業三:親手掃出正則化的 U 形

用 40 筆訓練資料(刻意少)擬合第五節的飽和曲線(雜訊放大到 0.02),網路加寬到 1-128-1(刻意過參數化),對 \(\lambda \in \{0, 10^{-4}, 10^{-3}, 10^{-2}\}\) 各訓練一次 L2 正則化網路,回報訓練與驗證 MSE,並用偏差–方差的語言解讀。

解答 SOLUTION
PYTHON
import numpy as np

rng = np.random.default_rng(9)
xtr = rng.uniform(-2, 2, (40, 1))
ytr = 0.03 * np.tanh(1.5 * xtr[:, 0]) + rng.normal(0, 0.02, 40)
xva = rng.uniform(-2, 2, (300, 1))
yva = 0.03 * np.tanh(1.5 * xva[:, 0]) + rng.normal(0, 0.02, 300)

def train_l2(lam, hidden=128, lr=0.01, epochs=60000, seed=2):
    rg = np.random.default_rng(seed)
    W1 = rg.normal(0, 1.5, (hidden, 1)); b1 = rg.normal(0, 1.5, hidden)
    W2 = rg.normal(0, 0.09, (1, hidden)); b2 = np.zeros(1)
    m = len(xtr)
    for ep in range(epochs):
        H1 = np.tanh(xtr @ W1.T + b1)
        pred = (H1 @ W2.T + b2).ravel()
        err = (pred - ytr) * 2 / m
        gW2 = err[None, :] @ H1 + 2 * lam * W2          # L2:梯度多扣 2λW
        gb2 = err.sum(keepdims=True)
        dZ = (err[:, None] @ W2) * (1 - H1**2)
        gW1 = dZ.T @ xtr + 2 * lam * W1
        gb1 = dZ.sum(0)
        W1 -= lr * gW1; b1 -= lr * gb1
        W2 -= lr * gW2; b2 -= lr * gb2
    ptr = (np.tanh(xtr @ W1.T + b1) @ W2.T + b2).ravel()
    pva = (np.tanh(xva @ W1.T + b1) @ W2.T + b2).ravel()
    return np.mean((ptr - ytr)**2), np.mean((pva - yva)**2)

for lam in (0.0, 1e-4, 1e-3, 1e-2):
    tr, va = train_l2(lam)
    print(f"lambda {lam:.0e}  train {tr:.6f}  val {va:.6f}")
# 輸出:
# lambda 0e+00  train 0.000219  val 0.000548
# lambda 1e-04  train 0.000217  val 0.000541
# lambda 1e-03  train 0.000264  val 0.000477
# lambda 1e-02  train 0.000342  val 0.000589

解讀:訓練 MSE 隨 \(\lambda\) 單調上升(罰金越重越不准貼資料),驗證 MSE 走出 U 形——\(\lambda = 0\) 時 128 個神經元對 40 筆資料,方差主導:曲面在資料點之間長出多餘的波浪,訓練誤差 2.19e-4 低於雜訊變異 4e-4,是背雜訊的直接證據,驗證誤差付出 5.48e-4 的代價;\(\lambda = 10^{-3}\) 時罰金把多餘的彎折燙平,驗證誤差降到谷底 4.77e-4;\(\lambda = 10^{-2}\) 時罰過頭、偏差接手:曲線被壓得太平、連真實的飽和形狀都跟不上,訓練與驗證誤差雙雙上升。這正是第 1 章 U 形曲線的正則化版本:橫軸從「模型複雜度」換成「罰金費率的倒數」,故事一字不改。

作業四:架構決策的口試題

不寫程式,回答三題:(1) 一個信用違約預測模型(二元分類)與一個報酬預測模型(迴歸),輸出層的激活函數與損失函數各該怎麼配?為什麼不能混用?(2) 同事主張「隱藏層全部用 sigmoid,因為輸出在 0 到 1 之間比較好控制」,請用本章的內容反駁。(3) 你有 3000 筆月頻資料、40 個特徵,同事提議上一個 6 層、每層 512 神經元的網路「反正表達力不嫌多」,給出至少三條反對理由與一個對案。

解答 SOLUTION

(1) 違約預測:輸出層 sigmoid(把實數壓成機率)配交叉熵損失——第 1 章講過交叉熵等於最大化概似,且它對「自信但錯誤」的預測施以重罰,是機率輸出的正確計分規則。報酬預測:輸出層線性(報酬可正可負、不該被壓縮範圍)配均方誤差。混用的後果具體而嚴重:迴歸接 sigmoid 會把預測硬夾在 (0,1),負報酬永遠預測不出來;分類接均方誤差則在輸出飽和區梯度趨零(誤差乘上 sigmoid 的近零導數),模型錯得很自信時反而幾乎收不到修正訊號,訓練極慢又不穩。

(2) 三記反駁:一是梯度消失——sigmoid 導數上限 0.25,反向傳播逐層相乘,四層之後訓練訊號最多剩 \(0.25^4 \approx 0.4\%\),前排幾乎學不到東西;ReLU 導數為 1,訊號無損通過。二是「輸出好控制」是誤解——隱藏層的輸出是內部表徵,不是給人看的機率,範圍受控毫無業務價值;真正需要控制範圍的只有輸出層。三是非零中心的 sigmoid 輸出恆正,會讓下一層權重的梯度全同號,更新路徑鋸齒化拖慢收斂。隱藏層的預設答案是 ReLU 家族。

(3) 反對理由:一、參數帳——這台網路約一百三十萬個參數(\(40 \times 512 + 5 \times 512^2 + 512\) 再加偏移),資料只有 3000 筆,參數是樣本的四百多倍,方差災難幾乎注定;二、訊噪比——月頻金融資料的可預測成分極小,第三節說過表達力不是瓶頸、過擬合才是,深度優勢兌現不了;三、驗證成本——沿時間切之後可用的驗證樣本本來就少,大網路的超參數搜索空間又大,選模型的統計功效根本不夠;四(加碼)、這種規模的網路在無訊號資料上也能把訓練誤差壓到很低,會系統性製造第四節那種假訊號的溫床。對案:一到兩層隱藏層、每層 8 到 32 個神經元起步,L2+早停常開,多隨機起點取穩健者,並以線性模型(ridge)作為必須擊敗的基準——贏不過基準的網路沒有上場資格。

§05參考資料