多層神經網路(Multilayer Neural Network)是在單層網路的輸入層與輸出層之間,加入一個或多個「隱藏層」(hidden layer)。每一層的輸出經過非線性活化函數後,成為下一層的輸入。這種層層堆疊的架構讓網路能學到愈來愈抽象的特徵表示,最終在輸出層做出預測。 📚 James, Witten, Hastie, Tibshirani (2023) An Introduction to Statistical Learning with Python, §10.2, pp. 403–406
本節以 MNIST 手寫數字辨識為核心案例。MNIST 包含 60,000 張訓練影像與 10,000 張測試影像,每張是 28×28 像素的灰階圖(0–255)。784 個像素值組成輸入向量 X,輸出則用 one-hot 編碼:10 個 dummy 變數 Y₀,…,Y₉,對應數字 0–9。
本節展示的雙隱藏層網路架構:
加上截距項(bias),總參數為 785×256 + 257×128 + 129×10 = 235,146 個權重,是訓練樣本數(60,000)的近四倍。沒有正則化,必定過度擬合。
第一層隱藏層的活化值(式 10.10):
其中 g 為活化函數(通常用 ReLU),K₁ = 256。
第二層隱藏層把 L₁ 的輸出當作輸入,再次經過線性組合 + 活化(式 10.11):
注意:\(A_\ell^{(2)}\) 最終仍是原始輸入 X 的函數——因為 \(A_k^{(1)}\) 本身就是 X 的函數。這就是「層層變換」的威力。
到達輸出層時,先計算 10 個線性分數 Z₀,…,Z₉(式 10.12):
再經 Softmax 函數轉換為類別機率(式 10.13):
這確保 10 個輸出值皆 ≥0 且總和為 1。
因為輸出是類別,訓練目標是最小化 負多項 log-概似,等價於交叉熵(cross-entropy,式 10.14):
# 10.2 多層神經網路 — 手寫數字辨識示範
# 使用 sklearn 內建 digits 資料集(8×8 灰階,1797 筆,即時載入)
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import classification_report
from sklearn.preprocessing import StandardScaler
# 載入 digits 資料集(8×8 手寫數字,1797 筆)
digits = load_digits()
X, y = digits.data, digits.target
print(f"資料集: {X.shape[0]} 筆, {X.shape[1]} 個特徵(8×8 像素)")
print(f"類別: {np.unique(y)}")
# 標準化(提升神經網路收斂速度)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y, test_size=0.3, random_state=42, stratify=y
)
print(f"訓練集: {X_train.shape[0]} 筆, 測試集: {X_test.shape[0]} 筆")
# 建立多層感知器:64 → 128 → 64 → 10
mlp = MLPClassifier(
hidden_layer_sizes=(128, 64),
activation='relu',
solver='adam',
alpha=0.0005,
batch_size=32,
learning_rate_init=0.001,
max_iter=300,
random_state=42,
verbose=False
)
print("訓練中...")
mlp.fit(X_train, y_train)
train_acc = mlp.score(X_train, y_train)
test_acc = mlp.score(X_test, y_test)
print(f"訓練準確率: {train_acc:.4f}")
print(f"測試準確率: {test_acc:.4f}")
n_params = sum(w.size for w in mlp.coefs_) + sum(b.size for b in mlp.intercepts_)
print(f"網路架構: 64→{'→'.join(str(s) for s in mlp.hidden_layer_sizes)}→10")
print(f"總參數: {n_params:,}")
# 前 12 筆預測可視化
fig, axes = plt.subplots(3, 4, figsize=(10, 7))
y_pred = mlp.predict(X_test)
for i, ax in enumerate(axes.flat):
ax.imshow(X_test[i].reshape(8, 8), cmap='gray')
color = 'green' if y_pred[i] == y_test[i] else 'red'
ax.set_title(f'預測:{y_pred[i]} 真實:{y_test[i]}', color=color, fontsize=10)
ax.axis('off')
plt.suptitle('MLP 預測結果(綠=正確,紅=錯誤)', fontsize=13)
plt.tight_layout()
plt.show()
print("\n分類報告(測試集):")
print(classification_report(y_test, y_pred, digits=3))
# 正則化強度對比:不同 alpha 對過擬合的影響(自給自足版本)
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import StandardScaler
# 載入資料(與前一個區塊獨立,自給自足)
digits = load_digits()
X, y = StandardScaler().fit_transform(digits.data), digits.target
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
alphas = [0.00001, 0.0001, 0.001, 0.01]
results = []
for a in alphas:
mlp = MLPClassifier(hidden_layer_sizes=(128, 64),
activation='relu', solver='adam',
alpha=a, max_iter=300, random_state=42)
mlp.fit(X_tr, y_tr)
results.append({
'alpha': a,
'train_acc': mlp.score(X_tr, y_tr),
'test_acc': mlp.score(X_te, y_te),
'gap': mlp.score(X_tr, y_tr) - mlp.score(X_te, y_te)
})
print(f"alpha={a:.5f} 訓練={results[-1]['train_acc']:.4f} "
f"測試={results[-1]['test_acc']:.4f} "
f"差距={results[-1]['gap']:.4f}")
fig, ax = plt.subplots(figsize=(8, 5))
alphas_log = np.log10([r['alpha'] for r in results])
ax.plot(alphas_log, [r['train_acc'] for r in results],
'o-', label='訓練準確率', color='#58a6ff')
ax.plot(alphas_log, [r['test_acc'] for r in results],
's-', label='測試準確率', color='#3fb950')
ax.fill_between(alphas_log,
[r['test_acc'] for r in results],
[r['train_acc'] for r in results],
alpha=0.15, color='#f85149', label='過擬合差距')
ax.set_xlabel('log10(alpha) — L2 正則化強度', fontsize=11)
ax.set_ylabel('準確率', fontsize=11)
ax.set_title('多層感知器:正則化強度 vs 準確率 (digits)', fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.2)
plt.tight_layout()
plt.show()
print("\n觀察:alpha 太小 → 過擬合(訓練>>測試);alpha 太大 → 欠擬合(兩者皆低)")
銀行每日處理數百萬張手寫支票。多層神經網路將手寫數字辨識錯誤率從 ~5% 降到 <0.3%,成為 ATM 支票存款的核心技術。課本提到 AT&T Bell Labs 在 1980 年代末就是以支票數字辨識推動了神經網路革命。
在癌症篩檢中,病理醫師需檢查數百張顯微切片。雙隱藏層網路從標準化切片影像中學習低階邊緣特徵(L₁)與高階形態特徵(L₂),輔助標記可疑區域。正則化(dropout)尤為重要,因標註資料昂貴稀少。
iOS 和 Android 的手寫辨識背後就是多層神經網路。784 像素對應觸控軌跡的標準化網格,10 個輸出對應數字或筆畫類別。手機端算力有限,通常使用 128→64 的小隱藏層加上權重壓縮。
多層網路的「低層擷取簡單特徵 → 高層組合抽象概念」與 Agent 系統的階層式派工異曲同工:第一層 worker 處理原始資料(PDF 擷取、OCR),第二層整合成摘要,主 Agent 做最終決策。ISLP 的 layer-by-layer abstraction 對應 kanban 的分層協作。正則化(dropout)則對應子 Agent 的隨機抽樣覆核——避免過度依賴單一 worker 導致「過擬合」。
| 方法 | MNIST 測試錯誤率 | 線性邊界? | 參數量 | 訓練成本 |
|---|---|---|---|---|
| NN + Ridge 正則化 | 2.3% | 否 | 235,146 | 高(GPU) |
| NN + Dropout 正則化 | 1.8% | 否 | 235,146 | 高(GPU) |
| 多項邏輯回歸 | 7.2% | 是 | 7,065 | 低 |
| 線性判別分析(LDA) | 12.7% | 是 | ~7,850 | 低 |
📚 數據來自課本 Table 10.1(p. 406)。
| 技術 | 隱藏層數 | 輸出方式 | 適用場景 | 參數效率 |
|---|---|---|---|---|
| 單層神經網路(§10.1) | 1 | 單一(線性/sigmoid) | 迴歸、二元分類 | 高(參數少) |
| 多層神經網路(§10.2) | ≥2 | Softmax 多類別 | 多元分類、多任務 | 中(需正則化) |
| 卷積神經網路 CNN(§10.3) | ≥5 | Softmax(接 flatten) | 影像分類、物件偵測 | 高(權重共享) |
| 多項邏輯回歸(§4.3.5) | 0 | Softmax 直接接輸入 | 簡單線性分類 | 最高 |
比較對象選自 ISLP §4.3, §10.1, §10.3。
「一個擁有 235,146 個參數的網路,配上僅 60,000 筆訓練資料——乍看是過擬合的災難配方。但搭配得當的正則化,它反而能擊敗所有人類直覺認為『更簡潔』的線性模型。深度學習的力量不在於更多的自由參數,而是那層層堆疊的非線性變換,把『784 像素』逐步變成『256 個邊緣→128 個形狀→10 個數字』。」