10.3 數學演算法的實戰案例#
概念說明#
演算法學到最後,考驗的不是會不會寫程式,而是把現實問題翻譯成數學模型的能力。本節用三個遞進的完整案例,示範全書技術的總集成:
- 案例一:最小平方回歸(統計建模)——7.2 節的理論 + 5.1 節的線性代數 + 9.3 節的信賴區間,組成完整資料分析管線。
- 案例二:投資組合最佳化(二次規劃)——Markowitz 模型:在「期望報酬 ≥ 目標」的線性限制下最小化組合變異數(二次目標、二次規劃)。這是「均值—變異數」(9.2 節概念)直接變成優化問題的典範。
- 案例三:logistic 分類(機器學習訓練)——非線性可微目標 + 梯度法(10.2 節),是深度學習訓練的迷你原型。
三個案例共用同一套工作流:辨認問題類型 → 選擇模型與演算法 → 實作 → 用獨立方式驗證 → 解讀誤差與限制。驗證的習慣尤其重要:每個案例我們都會用「另一條獨立的計算路徑」核對答案(如蒙地卡羅核對解析解、scipy 核對自寫實作)。
Python 程式碼範例#
import numpy as np
from scipy.optimize import minimize
import random
rng = np.random.default_rng(7)
# ========== 案例一:房價回歸完整管線 ==========
n = 100
area = rng.uniform(15, 60, n) # 坪數
age = rng.uniform(0, 40, n) # 屋齡
price = 3.5*area - 0.8*age + 50 + rng.normal(0, 8, n)
A = np.column_stack([np.ones(n), area, age])
Q, R = np.linalg.qr(A)
coef = np.linalg.solve(R, Q.T @ price)
resid = price - A @ coef
se = np.sqrt(resid @ resid / (n - 3)) * np.sqrt(np.diag(np.linalg.inv(A.T @ A)))
print("=== 案例一:回歸 ===")
for name, c, s in zip(["截距", "坪數係數", "屋齡係數"], coef, se):
lo, hi = c - 1.96*s, c + 1.96*s
print(f"{name}: {c:.3f} ± 1.96·{s:.3f} → 95% CI [{lo:.3f}, {hi:.3f}]")
# ========== 案例二:Markowitz 投資組合 ==========
# 3 檔資產的期望報酬與共變異矩陣
mu = np.array([0.08, 0.12, 0.15]) # 期望年報酬
Sigma = np.array([[0.010, 0.002, 0.001],
[0.002, 0.020, 0.004],
[0.001, 0.004, 0.035]])
target = 0.11 # 目標報酬 11%
def port_var(w):
return w @ Sigma @ w
cons = [{"type": "eq", "fun": lambda w: w.sum() - 1}, # 滿倉
{"type": "ineq", "fun": lambda w: w @ mu - target}] # 報酬達標
res = minimize(port_var, np.ones(3)/3, constraints=cons,
bounds=[(0, 1)]*3, method="SLSQP")
w = res.x
print("\n=== 案例二:投資組合 ===")
print(f"權重 = {np.round(w, 4)}, 年化波動 = {np.sqrt(port_var(w)):.4f}")
print(f"實際報酬 = {w @ mu:.4f}(目標 {target})")
# 蒙地卡羅驗證:隨機權重找出的最小波動不應更好
best_var, best_w = np.inf, None
for _ in range(200_000):
p = rng.dirichlet(np.ones(3))
if p @ mu >= target and p @ Sigma @ p < best_var:
best_var, best_w = p @ Sigma @ p, p
print(f"蒙地卡羅最佳隨機組合波動² = {best_var:.6f}(應接近且不顯著低於 SLSQP 的 {port_var(w):.6f})")
# ========== 案例三:logistic 分類 ==========
m = 300
x1 = rng.normal(0, 1, m)
x2 = rng.normal(0, 1, m)
y = np.where(2*x1 - x2 + 0.5 + rng.normal(0, 1, m) > 0, 1, -1)
X = np.column_stack([np.ones(m), x1, x2])
def logistic_loss(w):
z = y * (X @ w)
return np.logaddexp(0, -z).mean()
def logistic_grad(w):
z = y * (X @ w)
sig = 1 / (1 + np.exp(-z)) # σ(z)
return -((1 - sig) * y) @ X / m
# 梯度檢查(10.2 節的必修儀式)
w0 = rng.normal(size=3)
eps = 1e-6
num = np.array([(logistic_loss(w0 + eps*np.eye(3)[i])
- logistic_loss(w0 - eps*np.eye(3)[i])) / (2*eps)
for i in range(3)])
print("\n=== 案例三:logistic ===")
print("梯度檢查誤差 =", np.abs(num - logistic_grad(w0)).max())
w, eta = np.zeros(3), 0.5
for it in range(2000):
g = logistic_grad(w)
w -= eta * g
if np.linalg.norm(g) < 1e-10:
break
acc = np.mean(np.sign(X @ w) == y)
print(f"訓練 {it+1} 次迭代收斂,損失 {logistic_loss(w):.4f},準確率 {acc:.2%}")
try:
ref = minimize(logistic_loss, np.zeros(3), jac=logistic_grad,
method="BFGS")
print(f"BFGS 對照損失 = {ref.fun:.4f}(應一致)")
except Exception as e:
print("(BFGS 對照失敗:", e, ")")
誤差分析#
三個案例的誤差來源各自不同,正是全書誤差觀念的總複習:回歸的誤差分「估計誤差」(係數標準誤,由 9.3 節 t 分支給信賴區間)與「模型誤差」(線性假設不真),殘差診斷能抓後者;投資組合的誤差主要來自輸入估計——μ 與 Σ 是用歷史資料估的,期望報酬的估計誤差會被優化器放大(最優化對輸入誤差極敏感,「誤差最大化」效應),解藥是約束權重、收縮估計(shrinkage)或貝葉斯先驗;logistic 的誤差是優化未收斂(梯度範數沒壓下去)與過擬合(訓練準確率高、測試準確率低),需保留獨立測試集。設計層面的收穫是一張決策表:目標線性 → LP(10.1);二次目標線性限制 → 二次規劃;光滑非線性 → 梯度/牛頓/BFGS(10.2);黑箱或超多維 → 蒙地卡羅/演化法(6.3);要求整數解 → MILP,接受組合爆炸的代價(第 8 章)。先分類問題,再選演算法——這張表就是本書最想送給你的東西。
練習#
- 把案例一的資料量從 100 減到 10,觀察信賴區間變寬多少倍;再用 9.3 節的置換檢定檢驗「屋齡係數是否顯著異於 0」。
- 在案例二加上「每檔資產權重 ≤ 0.5」的限制,重新求解並比較波動變化;用蒙地卡羅驗證新解仍是限制下最優。
- 把案例三改成 Ridge 正則化:損失加上 λ‖w‖²(λ = 0.1),推導新梯度,重訓並在「生成參數不同的新測試集」上比較有無正則化的測試準確率。