跟著做
共 2 支 · 在本機 venv 跑 · 還沒裝環境?
09_eval_basics.py
python套件:
在 GitHub 看完整原始碼 numpy原始碼
"""
09_eval_basics.py
================================
評估(evaluation / evals)入門——業界最看重、初學者最常忽略的一塊。
「能不能說清楚你的模型有多好、在哪裡爛」往往比「會不會訓練」更值錢。
對照講義 docs/00(第 1、4 站)、docs/06(名詞速查表)。
這支程式示範兩組最核心的指標,全部手算(不准用現成函式庫):
A) 分類指標:accuracy / precision / recall / F1 + 混淆矩陣
—— 回答「準不準?是寧可錯殺還是寧可放過?」
B) 語言模型指標:交叉熵 loss 與困惑度(perplexity)
—— 回答「模型對下一個字有多不確定?」(GPT 類模型的標準評估)
執行:python3 09_eval_basics.py (只需要 numpy)
"""
import sys
import numpy as np
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
# ──────────────────────────────────────────────────────────────
# A) 分類指標
# ──────────────────────────────────────────────────────────────
def classification_metrics(y_true, y_pred):
"""二元分類:1 = 陽性(例如「是垃圾郵件」),0 = 陰性。"""
y_true = np.asarray(y_true)
y_pred = np.asarray(y_pred)
tp = int(np.sum((y_pred == 1) & (y_true == 1))) # 真陽:說是、真的是
fp = int(np.sum((y_pred == 1) & (y_true == 0))) # 偽陽:說是、其實不是(誤報)
fn = int(np.sum((y_pred == 0) & (y_true == 1))) # 偽陰:說不是、其實是(漏抓)
tn = int(np.sum((y_pred == 0) & (y_true == 0))) # 真陰:說不是、真的不是
accuracy = (tp + tn) / len(y_true) # 整體答對比例
precision = tp / (tp + fp) if (tp + fp) else 0.0 # 你說是的,有多少真的是(重視「別誤報」)
recall = tp / (tp + fn) if (tp + fn) else 0.0 # 真的是的,你抓到多少(重視「別漏抓」)
f1 = (2 * precision * recall / (precision + recall)
if (precision + recall) else 0.0) # precision 與 recall 的調和平均
return dict(tp=tp, fp=fp, fn=fn, tn=tn, accuracy=accuracy,
precision=precision, recall=recall, f1=f1)
def demo_classification():
print("=" * 64)
print("A) 分類指標:以『垃圾郵件偵測』為例(1=垃圾, 0=正常)")
print("=" * 64)
# 10 封信的真實標籤與模型預測
y_true = [1, 0, 1, 1, 0, 0, 1, 0, 1, 0]
y_pred = [1, 0, 1, 0, 0, 1, 1, 0, 1, 0]
m = classification_metrics(y_true, y_pred)
print(" 混淆矩陣:")
print(f" 預測=垃圾 預測=正常")
print(f" 實際=垃圾 {m['tp']:^6} {m['fn']:^6} ← 漏抓(FN)={m['fn']}")
print(f" 實際=正常 {m['fp']:^6} {m['tn']:^6} ← 誤報(FP)={m['fp']}")
print()
print(f" Accuracy 準確率 = {m['accuracy']:.2f} (整體答對比例)")
print(f" Precision 精確率 = {m['precision']:.2f} (標成垃圾的信裡,真的是垃圾的比例)")
print(f" Recall 召回率 = {m['recall']:.2f} (所有垃圾信裡,被抓到的比例)")
print(f" F1 分數 = {m['f1']:.2f} (precision 與 recall 的平衡)")
print()
print(" 為什麼不能只看 accuracy?若 100 封信只有 1 封垃圾,")
print(" 模型「全部猜正常」accuracy 高達 99%,卻一封垃圾都沒抓到(recall=0)。")
print(" 這就是『不平衡資料』陷阱 → 要看 precision/recall/F1。\n")
# ──────────────────────────────────────────────────────────────
# B) 語言模型指標:交叉熵與困惑度
# ──────────────────────────────────────────────────────────────
def cross_entropy(probs_for_true):
"""
給每一步「模型分配給『真正下一個字』的機率」,算平均交叉熵:
CE = 平均( −log P(真正的字) )
猜得越準(機率越接近 1)→ −log 越接近 0 → loss 越小。
"""
probs_for_true = np.asarray(probs_for_true)
return float(np.mean(-np.log(probs_for_true)))
def perplexity(ce_loss):
"""困惑度 = exp(交叉熵)。直覺:模型平均在『幾個字之間猶豫』。越小越好。"""
return float(np.exp(ce_loss))
def demo_language_model():
print("=" * 64)
print("B) 語言模型指標:交叉熵 loss 與困惑度(perplexity)")
print("=" * 64)
# 假設詞表有 50 個字。比較三個模型對同一句(5 個字)的表現:
vocab_size = 50
print(f" 假設詞表 {vocab_size} 個字。完全亂猜的模型,每字機率 = 1/{vocab_size}。\n")
scenarios = {
"亂猜的模型 ": [1 / vocab_size] * 5,
"學了一點的模型 ": [0.10, 0.05, 0.20, 0.08, 0.12],
"學得不錯的模型 ": [0.6, 0.4, 0.8, 0.5, 0.7],
}
for name, probs in scenarios.items():
ce = cross_entropy(probs)
ppl = perplexity(ce)
print(f" {name} | 交叉熵 loss = {ce:.3f} | 困惑度 = {ppl:6.2f}")
print()
print(f" 亂猜模型的困惑度會接近詞表大小 {vocab_size}(在 {vocab_size} 個字之間瞎猜)。")
print(" 困惑度越往 1 靠 → 模型越確定下一個字。")
print(" 👉 這正是 code/02_tiny_gpt.py 訓練時印出的 loss(取 exp 就是困惑度)。\n")
if __name__ == "__main__":
demo_classification()
demo_language_model()
print("一句話:會訓練不稀奇,能說清楚『好在哪、爛在哪、用哪個指標衡量』才是專業。")
13_timeseries_forecast.py
python套件:
在 GitHub 看完整原始碼 numpy原始碼
"""
13_timeseries_forecast.py
================================
時間序列預測入門——股價、用電、需求預測都屬這塊(對你的股票興趣直接有用)。
對照講義 docs/07(全景地圖:時間序列)。只用 numpy,全部手刻。
這支程式的重點觀念(業界做時間序列最該先懂的事):
1. 時間序列「不能隨機切」train/test:未來不能拿來訓練 → 一定照時間切(前面訓練、後面測試)。
2. 一定要先比「笨基準線」:最笨的「明天=今天」往往比想像中難打贏。
打不贏基準線的花俏模型,等於沒用。
3. AR(p) 自迴歸:用「過去 p 天」線性預測「下一天」,用最小平方法求解。
4. 用 MAE / RMSE 評估,並和基準線比較。
⚠️ 重要:這裡用「合成資料(趨勢+季節+雜訊)」示範方法。真實股價接近隨機漫步,
極難預測;任何聲稱能輕鬆預測股價的,請高度懷疑。這支教的是「方法與紀律」,不是明牌。
執行:python3 13_timeseries_forecast.py (只需要 numpy)
"""
import sys
import numpy as np
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
rng = np.random.default_rng(0)
def make_series(n=300):
"""造一條有『上升趨勢 + 週期季節 + 雜訊』的時間序列(像某些銷售/用電資料)。"""
t = np.arange(n)
trend = 0.05 * t # 緩慢上升
season = 3.0 * np.sin(2 * np.pi * t / 30) # 每 30 步一個週期
noise = rng.normal(0, 0.8, n)
return 10 + trend + season + noise
def mae(y_true, y_pred):
return float(np.mean(np.abs(y_true - y_pred)))
def rmse(y_true, y_pred):
return float(np.sqrt(np.mean((y_true - y_pred) ** 2)))
def make_lagged(series, p):
"""把序列攤成監督式學習資料:用前 p 個值當特徵 X,下一個值當目標 y。"""
X, y = [], []
for i in range(p, len(series)):
X.append(series[i - p:i])
y.append(series[i])
return np.array(X), np.array(y)
def fit_ar(X, y):
"""AR(p) 線性模型:y ≈ X·w + b,用最小平方法(normal equation)一次解出。"""
X1 = np.concatenate([X, np.ones((len(X), 1))], axis=1) # 加一欄常數項當 b
coef, *_ = np.linalg.lstsq(X1, y, rcond=None) # 解 min ||X1·coef − y||²
return coef # 最後一個是 b
def predict_ar(X, coef):
X1 = np.concatenate([X, np.ones((len(X), 1))], axis=1)
return X1 @ coef
def main():
series = make_series()
n = len(series)
# ── 照時間切:前 80% 訓練、後 20% 測試(絕不打亂!)──
split = int(n * 0.8)
train, test = series[:split], series[split:]
print("=" * 64)
print(f"序列長度 {n}(訓練 {len(train)} / 測試 {len(test)})。時間序列照時間切,不可隨機打亂。")
print("=" * 64)
# ── 基準線 1:naive(明天 = 今天)──
naive_pred = series[split - 1:n - 1] # 用前一天當預測
print("\n[基準線] naive『明天=今天』:")
print(f" MAE={mae(test, naive_pred):.3f} RMSE={rmse(test, naive_pred):.3f}")
# ── 基準線 2:移動平均(過去 k 天的平均)──
k = 5
ma_pred = np.array([series[split + i - k:split + i].mean() for i in range(len(test))])
print(f"\n[基準線] 移動平均(過去 {k} 天):")
print(f" MAE={mae(test, ma_pred):.3f} RMSE={rmse(test, ma_pred):.3f}")
# ── 模型:AR(p) 自迴歸 ──
p = 10
Xtr, ytr = make_lagged(train, p)
coef = fit_ar(Xtr, ytr)
# 在測試段逐步預測(每次用「真實的過去 p 天」預測下一天,walk-forward)
ar_pred = []
for i in range(len(test)):
window = series[split + i - p:split + i]
ar_pred.append(predict_ar(window[None, :], coef)[0])
ar_pred = np.array(ar_pred)
print(f"\n[模型] AR({p}) 自迴歸(最小平方法):")
print(f" MAE={mae(test, ar_pred):.3f} RMSE={rmse(test, ar_pred):.3f}")
# ── 結論:模型有沒有打贏基準線?──
best_baseline = min(mae(test, naive_pred), mae(test, ma_pred))
ar_mae = mae(test, ar_pred)
print("\n" + "=" * 64)
if ar_mae < best_baseline:
print(f"✅ AR 模型 MAE={ar_mae:.3f} 打贏最好的基準線 {best_baseline:.3f}——這條序列有可學的規律。")
else:
print(f"⚠️ AR 模型沒打贏基準線——表示要嘛資料太隨機,要嘛模型/特徵要再想。")
print("這套紀律(照時間切、先比基準線、用 MAE/RMSE)對任何時間序列都適用,股票也一樣。")
print("想進階:把 AR 換成神經網路(LSTM / Transformer)、加更多特徵(成交量、技術指標)。")
if __name__ == "__main__":
main()