在前幾節,我們學了線性迴歸——一種參數化方法。它假設 f(X) 有特定的函數形式(線性),然後用資料估計參數。但這不是唯一的選擇。K-最近鄰(KNN)是一種非參數化方法:不做任何函數形式假設,直接從資料本身做預測。
KNN 迴歸的核心思想很直覺:想知道一間房子的價格?看看附近最近賣掉的 K 間類似房子,取它們的平均價。不需要假設房間數和價格之間是線性還是曲線關係——直接問鄰居。
其中 \(\mathcal{N}_0\) 是離 \(x_0\) 最近的 K 個訓練觀測值的集合。K 是我們必須選擇的超參數——沒有公式能自動算出最佳 K。
想像你在畫一條穿過資料點的曲線:
這不是一場宗教戰爭。兩種方法在不同情境下各有勝場:
這是 KNN 最殘酷的限制。想像你在一個大賣場找朋友:
在統計上,這意味著:當維度增加時,你需要指數級增長的資料,才能維持相同密度的「鄰居」。在圖 3.20 中,當 p=1 時 KNN 表現優於線性迴歸(對非線性資料),但 p 增加到 20 時,即使真實關係是非線性的,線性迴歸也開始勝出。
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.neighbors import KNeighborsRegressor
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
# ── 建立模擬資料:非線性關係 ──
np.random.seed(42)
n = 200
X = np.random.uniform(-3, 3, n).reshape(-1, 1)
y = X.ravel()**3 - 3*X.ravel() + np.random.normal(0, 2, n) # 三次函數 + 噪音
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1)
# ── 線性迴歸 ──
lr = LinearRegression().fit(X_train, y_train)
y_pred_lr = lr.predict(X_test)
mse_lr = mean_squared_error(y_test, y_pred_lr)
# ── KNN 迴歸:測試不同 K 值 ──
results = []
for K in [1, 3, 9, 25, 50]:
knn = KNeighborsRegressor(n_neighbors=K).fit(X_train, y_train)
y_pred_knn = knn.predict(X_test)
mse_knn = mean_squared_error(y_test, y_pred_knn)
results.append({'K': K, 'MSE': mse_knn})
print("=== 測試集 MSE 比較 ===")
print(f"線性迴歸 MSE: {mse_lr:.4f}")
for r in results:
print(f"KNN (K={r['K']:2d}) MSE: {r['MSE']:.4f}")
# ── 視覺化:不同 K 值的擬合曲線 ──
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
X_plot = np.linspace(-3, 3, 300).reshape(-1, 1)
y_plot_lr = lr.predict(X_plot)
Ks = [1, 9, 50]
titles = ['K=1 (過度擬合)', 'K=9 (適中)', 'K=50 (過度平滑)']
for ax, K, title in zip(axes, Ks, titles):
knn = KNeighborsRegressor(n_neighbors=K).fit(X_train, y_train)
y_plot_knn = knn.predict(X_plot)
ax.scatter(X_train, y_train, alpha=0.3, s=10, label='訓練資料')
ax.plot(X_plot, y_plot_lr, 'r-', linewidth=2, label='線性迴歸')
ax.plot(X_plot, y_plot_knn, 'g-', linewidth=2, label=f'KNN(K={K})')
ax.set_title(title)
ax.set_xlabel('X')
ax.set_ylabel('y')
ax.legend(fontsize=8)
plt.tight_layout()
plt.savefig('/tmp/knn_vs_lr.png', dpi=100, bbox_inches='tight')
plt.show()
print("\n→ K=1 完美記住訓練資料但泛化差,K=50 太平滑漏掉結構")
# ── 維度詛咒實證 ──
print("\n=== 維度詛咒模擬 ===")
for p in [1, 2, 5, 10, 20]:
np.random.seed(42)
n_train = 100
Xp = np.random.normal(0, 1, (n_train, p))
# 真實關係:只有第一個變數有非線性效果
yp = Xp[:, 0]**3 - 3*Xp[:, 0] + np.random.normal(0, 2, n_train)
Xp_test = np.random.normal(0, 1, (500, p))
yp_test = Xp_test[:, 0]**3 - 3*Xp_test[:, 0] + np.random.normal(0, 2, 500)
knn_p = KNeighborsRegressor(n_neighbors=5).fit(Xp, yp)
lr_p = LinearRegression().fit(Xp, yp)
mse_knn = mean_squared_error(yp_test, knn_p.predict(Xp_test))
mse_lr = mean_squared_error(yp_test, lr_p.predict(Xp_test))
winner = "KNN" if mse_knn < mse_lr else "LR "
print(f"p={p:2d} | LR MSE: {mse_lr:.4f} | KNN MSE: {mse_knn:.4f} | → {winner}")
你想估一間房子的市場價。兩種做法:
根據病患的多項指標預測糖尿病風險:
線性迴歸的「固定參數+強假設」策略對應於基於規則的 agent(明確的策略腳本);KNN 的「無假設+資料驅動」對應於完全自主的 LLM agent(每次推理都是從零開始)。
課程啟發:最佳實踐是混合——像彈性網(Elastic Net)結合 L1/L2 正則化一樣,Hermes 也應該在結構化工作流(skill 系統、固定管線)和自主推理(LLM 自由發揮)之間找到平衡。這正是我們目前的架構:cron job 提供固定結構(=參數化),delegate_task 提供靈活應變(=非參數化)。