opencode×AI-Math
Lab 3·現場 25 分·agent 當學習夥伴

分類與筆記:讓 agent 成為學習夥伴

前兩關你在檢查 agent,這一關反過來,讓它當你的推導夥伴: 寫程式、驗證公式、然後把整個推導過程整理成一份你之後真的會回去看的筆記。

現場怎麼讀這一章

先理解分類、損失與梯度的意思,再完成程式與梯度檢查;LaTeX 筆記若現場時間不夠,可以課後補完。概似推導、誤差階數與線性可分的完整證明放在深入補充,課後可以慢慢讀;不必先會全部推導才開始操作。

任務

從 digits 裡挑出數字 3 和 8(357 筆),手刻一個 logistic 迴歸做二元分類。

二元分類是從兩個答案中選一個。這裡把像素值當輸入、數字 3 或 8 當已知答案;模型先給出機率,再依機率判斷類別。損失函數用一個數字表示預測和答案差多少,訓練就是尋找讓它變小的參數。

梯度記錄損失對各個參數的變化率,幫我們決定參數往哪邊調。本關會用小幅改動參數的方法,檢查程式算出的梯度是否合理。

「二元分類」在做什麼?logistic 迴歸為什麼長那樣?

跟 Lab 1 的分群不同,這次我們有答案:每張圖都知道它是 3 還是 8。 目標是學一個規則,看到新的圖能判斷它是哪一個。這叫監督式學習。

最直覺的想法,以及它的問題

最簡單的規則是「算一個加權總和 $z=\mathbf{w}^\top\mathbf{x}+b$,$z>0$ 說是 8,否則說是 3」。 這就是一個超平面把空間切兩半,完全是線代的東西。

問題是:這個規則只吐 0 或 1,沒有「有多確定」的資訊, 而且它不連續,沒辦法微分,也就沒辦法用微積分找最佳的 $\mathbf{w}$。

sigmoid:把實數壓成機率

所以改成先算 $z$,再用一個函數把它壓到 $(0,1)$ 當成「是 8 的機率」:

$$\sigma(z)=\frac{1}{1+e^{-z}}$$

這個函數有幾個剛好合用的性質:

  • $z\to+\infty$ 時趨近 1,$z\to-\infty$ 時趨近 0,$z=0$ 時剛好 0.5
  • 處處可微,而且導數漂亮得不像話:$\sigma'(z)=\sigma(z)\bigl(1-\sigma(z)\bigr)$ (值得自己動手驗一次,就是商法則)
  • 單調遞增,所以「$\sigma(z)>0.5$」跟「$z>0$」是同一件事, 決策邊界仍然是那個超平面,沒有變複雜

名字裡有「迴歸」卻在做分類,是歷史遺留的命名,別被騙了。

什麼是「概似」?為什麼要取 log?

模型說「第 $i$ 張圖是 8 的機率是 $p_i=\sigma(z_i)$」。 如果它的真實答案 $y_i=1$(是 8),模型給的機率就是 $p_i$; 如果 $y_i=0$(是 3),模型給的機率是 $1-p_i$。兩種情況可以合寫成

$$P(y_i\mid\mathbf{x}_i)=p_i^{\,y_i}(1-p_i)^{1-y_i}$$

(把 $y_i=0$ 或 $1$ 代進去檢查一下,就是上面那兩句話。) 假設各筆資料獨立,看到整批資料的機率就是連乘:

$$L(\mathbf{w})=\prod_{i=1}^{n}p_i^{\,y_i}(1-p_i)^{1-y_i}$$

這個 $L$ 叫概似(likelihood)。 「最大概似估計」就是:挑一組參數,讓「模型認為這批資料會發生」的機率最大。 這是統計學裡最基本的參數估計原則。

為什麼取 log

  • 連乘變連加:$\log\prod=\sum\log$,微分立刻好做很多
  • 數值不會爆炸:許多介於 0 和 1 的數連乘,可能小到浮點數無法表示而變成 0
  • 不改變答案:$\log$ 是嚴格遞增的,最大化 $L$ 跟最大化 $\log L$ 的解完全相同

習慣上會再取負號、除以 $n$,把「最大化概似」變成「最小化平均損失」, 這樣就跟其他最佳化問題長得一樣了。得到的就是本關要驗證的那個 $L(\mathbf{w})$。

接下來找最小值要用的梯度、偏微分、鏈鎖律,全部是你微積分課上的東西, 只是變數多了一點。

梯度下降在做什麼?

你在微積分學過:梯度 $\nabla L(\mathbf{w})$ 指向函數上升最快的方向。 那想下降就往反方向走:

$$\mathbf{w}\leftarrow\mathbf{w}-\eta\,\nabla L(\mathbf{w})$$

$\eta$ 是步伐大小(learning rate),本關用 0.5。反覆走,直到走不動為止。

為什麼不像 k-means 那樣「令導數為零、直接解」?因為 $\nabla L=\mathbf{0}$ 在這裡是一組沒有閉式解的非線性方程式($\sigma$ 卡在裡面), 只能用迭代的方式逼近。

這也是為什麼「驗證梯度公式對不對」這件事在本關這麼重要: 梯度是整個過程唯一的方向感,公式寫錯,後面跑再多次都是往錯的方向走。

過擬合與正則化是什麼?(本關最後會用到)

過擬合:模型把訓練資料背下來了,包括裡面的雜訊, 所以在訓練資料上表現完美,換一批新資料就崩。 症狀之一是權重變得很大:模型用極端的係數去遷就個別樣本。

正則化的做法是在要最小化的目標裡,加一項懲罰大權重的東西。 最常見的 L2 正則化:

$$L_{\text{reg}}(\beta,b)=L(\beta,b)+\frac{\lambda}{2}\|\beta\|^2$$

這裡 $\beta$ 是特徵的權重,$b$ 是截距;平方範數只計算權重,不包含截距。$\lambda$ 越大,懲罰越重。 效果是逼模型「除非證據夠強,否則不要把係數調大」。

本關會看到正則化的另一個作用,比防過擬合更基本: 在本例兩個類別都存在時,正的 L2 懲罰能讓原本沒有有限最小值點的問題得到有限且唯一的解。
稍後 sklearn 的 L2 範例,在未加樣本或類別權重時,對應 $\lambda=1/(nC)$,其中 $n$ 是訓練樣本數;$C$ 越小,懲罰越重。

sklearn 的 fit / predict 慣例

scikit-learn 裡幾乎每個模型都長同一個樣子,學會一個就會全部:

寫法意思
m = LogisticRegression(...)建立模型,括號裡放超參數(例如 C、max_iter)
m.fit(X, y)訓練。把資料餵進去,模型把學到的參數存在自己身上
m.coef_ m.intercept_學到的權重與截距。結尾有底線是 sklearn 的慣例,代表「訓練完才有的東西」
m.predict(X)吐出預測的標籤(0 或 1)
m.predict_proba(X)吐出機率,兩欄分別是「是 0 的機率」「是 1 的機率」
m.score(X, y)準確率,也就是猜對的比例

PCA 那類轉換器則是 fit 之後用 transform(做轉換) 與 inverse_transform(轉回去)。Lab 2 用的就是這組。

你要驗證的公式

把像素矩陣加上一欄 1 得到 $X_b$,並把截距放在參數向量 $w$ 的最後一項。令 $z=X_bw$、$\sigma(z)=\dfrac{1}{1+e^{-z}}$,平均對數損失與其梯度為

$$L(w)=\frac1n\sum_i\Bigl[\log\bigl(1+e^{z_i}\bigr)-y_iz_i\Bigr], \qquad \nabla_wL=\frac1nX_b^\top\bigl[\sigma(X_bw)-y\bigr]$$

agent 會很快寫出來。問題是:你怎麼知道它沒寫錯?

動手

先說明公式,再要求程式

我要手刻一個二元 logistic 迴歸,資料是 digits 裡的 3 和 8。

先用兩三句話說明:損失函數衡量什麼,梯度告訴我們什麼。
依本頁公式 L(w) = (1/n) Σ [log(1+e^z) - y z],
以及 ∇L = (1/n) Xb^T (σ(Xb w) - y) 實作。

說明後寫成 lab3_logistic.py。本輪只實作資料讀取、loss(w) 與 grad(w),
先不要訓練、切分資料、增加其他演算法或產生新資料。
使用全部 digits 的 3 與 8:像素除以 16,8 設為 1、3 設為 0;
最後加一欄 1 作為截距,所以 Xb 有 65 欄。loss 使用平均值。
用 uv run python lab3_logistic.py 執行,印出資料 shape 後停止,等我給下一步。

先對照本頁公式,確認損失與梯度各自的意義。待會的數值檢查只能檢查「程式內部是否一致」,不能單獨確認「公式本身正確」。完整的概似推導可在課後慢慢閱讀。

有限差分梯度檢查

這是數值方法裡最乾淨的自我驗證:用損失函數本身去逼近梯度,跟解析梯度比。

中央差分
$$\frac{\partial L}{\partial w_i}\;\approx\;\frac{L(w+h\mathbf{e}_i)-L(w-h\mathbf{e}_i)}{2h}$$
請加一段有限差分梯度檢查:

- 用中央差分,檢查**全部** 65 個維度(不要只抽樣幾個)
- 步長 h 掃過 1e-2 到 1e-9,每個 h 印出最大絕對誤差與最大相對誤差
- 使用 np.random.RandomState(0).normal(size=65)*0.01 作為檢查點 w0
- 最大絕對誤差小於 1e-8 記為 PASS,否則記為需調查;相對誤差分母至少取 1e-12
- 做成表格,用 uv run python lab3_logistic.py 執行
- 本輪只加梯度檢查,不訓練、不新增資料或演算法;完成後停止
opencode build · big-pickle
uv run python lab3_logistic.py
步長 h 最大絕對誤差 最大相對誤差 判定
1e-02 7.351e-08 6.888e-06 FAIL
1e-03 7.351e-10 6.889e-08 PASS
1e-04 7.171e-12 9.332e-08 PASS
1e-05 1.030e-11 1.858e-07 PASS
1e-06 6.535e-11 5.733e-06 PASS
1e-07 7.568e-10 7.971e-05 PASS
1e-09 8.026e-08 7.274e-04 FAIL
深入:為什麼步長太大或太小都可能 FAIL

為什麼兩頭都會 FAIL

中央差分的總誤差有兩個來源,方向相反:

誤差分解
$$E(h)\;\approx\;\underbrace{Ah^2}_{\text{截斷誤差}}\;+\;\underbrace{\frac{Bu}{h}}_{\text{捨入誤差}}$$

$u$ 是浮點精度(float64 約 $2.2\times10^{-16}$)。 $h$ 太大時泰勒展開的高階項來不及被忽略; $h$ 太小時 $L(w+h)$ 與 $L(w-h)$ 幾乎相等,相減會損失有效位數。 令導數為零可得最佳量級 $h\sim u^{1/3}\approx 6\times10^{-6}$。 這份輸出的最佳位置約在 $10^{-4}$ 附近;實際位置還受常數、資料與誤差判準影響。

FAIL 不等於公式錯

上表兩頭的 FAIL,程式完全沒問題。 所以看到梯度檢查 FAIL 時,正確的說法是「有不一致,要查原因」: 可能是公式錯、步長不當、捨入誤差,或該維梯度本身接近 0。 反過來說,PASS 也只提供局部數值證據:如果 loss() 和 grad() 一起照著同一個錯誤的目標函數寫,這關照樣全過。因此仍要對照目標公式,不能只看數值 PASS。

訓練,然後撞上一個陷阱

現在用梯度下降訓練(lr=0.5,20000 次迭代),
並在第 100、1000、5000、10000、20000 次時印出 ||w|| 與損失。

訓練完跟 sklearn 的 LogisticRegression 比對。
注意 sklearn 1.8 起 penalty 參數已棄用,無正則化要用 C=np.inf。
訓練過程
迭代 ||w|| 損失
100 4.1422 0.09344736
1000 8.8816 0.02217583
5000 13.8902 0.00625092
10000 16.4297 0.00343643
20000 19.1436 0.00184668
手刻 GD 準確率 = 1.0000 sklearn 準確率 = 1.0000
手刻 ||w|| = 19.1436  sklearn ||w|| = 97.8955
看出問題了嗎

損失一直往 0 掉,但 $\|w\|$ 一直長,沒有要停的跡象。 而 sklearn 算出來的 $\|w\|$ 是 97.9,是手刻版的五倍。 兩邊準確率都是 1.0000。誰對?

這份資料能被一個線性邊界完全分開;下面的補充用程式檢查這件事。現場先觀察係數持續增大,再比較預測結果。

深入:用線性規劃確認可分性與推導其影響

證明它真的線性可分

「$\|w\|$ 一直變大」只是個徵兆,不是證明。要真的確認,解一個線性可行性問題: 以下 $\mathbf{x}_i$ 表示已加上常數 1 的第 $i$ 筆資料,$v$ 的最後一項是截距。令 $t_i=2y_i-1\in\{-1,+1\}$,找 $v$ 使得

可行性問題
$$t_i\,(\mathbf{x}_i^\top v)\;\ge\;1\qquad\text{對所有 }i$$

如果這組不等式有解,就代表存在一個嚴格分隔超平面。

請用 scipy.optimize.linprog 解這個線性可行性問題:
找 v 使得對所有 i 都有 t_i * (x_i · v) >= 1,其中 t_i = 2*y_i - 1。

如果有解,v 的最後一項是截距;印出幾何間隔 min(t*(Xb@v)) / ||v[:-1]||,分母不含截距。
然後對找到的 v,計算 c = 1, 5, 20, 100, 500 時的 L(c*v)。
可分性證明
可行解存在 -> PASS
幾何間隔 = margin / ||v[:-1]|| = 1.174682e-02 -> PASS
c = 1 L(cv) = 0.1022643878
c = 5 L(cv) = 0.0011832838
c = 20 L(cv) = 0.0000000003
c = 100 L(cv) = 0.0000000000
c = 500 L(cv) = 0.0000000000

這就是完整的論證:存在嚴格分隔向量 $v$,沿著它放大 $c$,損失 $L(cv)=\frac1n\sum_i\log\bigl(1+e^{-c\,t_i\mathbf{x}_i^\top v}\bigr)\to 0$, 而任何有限的 $w$ 都給出 $L(w)>0$。

結論

對線性可分的資料,無正則化 logistic 迴歸的最大概似估計不存在有限解。 所以「手刻係數 vs sklearn 係數」根本沒有標準答案可以比。兩邊都只是在往無窮遠的路上停在不同位置。

一個常被講錯的地方

「把 $w$ 放大損失一定更小」這句話只在 $w$ 已經分對全部樣本時才成立。 如果還有分錯的點,放大只會讓那些點的損失爆掉。 另外,這個結論只適用於這 357 筆樣本,不代表所有手寫的 3 和 8 都線性可分。

那準確率都 1.0,是不是代表兩邊一樣?

請比較手刻與 sklearn 的:
1. 預測標籤是否完全一致
2. 預測機率的最大差
3. 兩個權重向量的夾角餘弦
決策邊界比對
預測標籤完全一致? PASS
預測機率最大差  = 0.0341  <- 標籤一樣,機率差很多
權重向量 cos   = 0.994942 <- 方向相近但不相同

所以「兩邊準確率都是 1.0」不代表決策邊界相同。 不同的超平面可以把同一批訓練資料分對,但對新資料的機率預測會不一樣。 準確率相同只能算是「表現檢查」,不能當作實作正確的證明。

正則化把問題變回有解

加上 L2 正則化(sklearn 用 l1_ratio=0, C=1.0)再跑一次,
比較 ||w|| 跟準確率。
加上 L2
無正則化 ||w|| = 97.8955 準確率 = 1.0000
L2 (C=1.0) ||w|| = 6.0144 準確率 = 1.0000

$\|w\|$ 從 97.9 掉到 6.0,準確率完全沒變。 這裡 sklearn 的 L2 懲罰作用於不含截距的特徵權重 $\beta$,以本章平均損失的記法,懲罰項為 $\|\beta\|^2/(2nC)$。上方顯示的 $\|w\|$ 則仍包含截距,方便與前面的輸出對照。
在本例兩類樣本都存在的條件下,加入正的 L2 懲罰後有有限且唯一的解。它除了限制權重大小,也解決了這個例子原本沒有有限最小值點的問題。

最後:把它變成一份筆記

把今天已完成的程式、檢查與發現整理成筆記。 這些東西如果只留在終端機的捲動紀錄裡,下週就沒了。

請把今天已完成的步驟整理成 notes/logistic.md,寫給之後複習的我看。

請說明:我們用什麼資料、損失與梯度各代表什麼、梯度檢查看到了什麼,
以及為什麼準確率相同還不能判斷兩個模型完全相同。
引用這次實際執行的指令、輸出與數字,數學式用 $...$ 與 $$...$$。
沒有完成的推導或檢查列在「接下來想弄懂的事」,不要寫成已完成。
最後給我一個可以不用看筆記就回答的問題,答案分開寫。
課後:補成包含完整推導的筆記

完成前面的深入補充後,再用這份需求擴充筆記。範例中的數值要核對自己實際的執行結果。

請把今天這一輪的推導與發現整理成 notes/logistic.md,內容包含:

1. 從負對數概似到 L(w) 的完整推導(用 $...$ 與 $$...$$ 寫數學式)
2. 梯度的推導
3. 有限差分梯度檢查:原理、誤差分解 E(h) ≈ A h² + B u/h、為什麼兩頭都會 FAIL
4. 線性可分導致 MLE 不存在的完整論證(含那個「只在已分對時成立」的但書)
5. 為什麼準確率相同不代表決策邊界相同(附上我們實測的機率差 0.0341)
6. 正則化怎麼讓問題重新有解

寫給三個月後的我看,那時我已經忘記細節了。
為什麼要寫成檔案,而不是在對話裡問

因為檔案可以進 git。下次你叫 agent 補充或修改這份筆記, git diff 會告訴你它到底改了哪幾行。 你不必重讀整份文件去找差異,也不會不知不覺讓它把你原本寫對的東西改掉。 筆記因此變成會累積的資產,而不是散落在各個對話視窗裡的碎片。 詳見第 07 章。

課後若想保留筆記的修改歷程,再依Git 安全網建立版本紀錄。現場先確認 notes/logistic.md 存在,並讀過內容。

檢查清單

如果還有時間

課後練習:從概似推導損失與梯度

理解前面的操作後,可以請 agent 陪你逐步推導,遇到不熟的符號就停下來問:

先定義 Bernoulli 模型的成功機率,解釋為什麼把獨立樣本的機率相乘。
從負對數概似逐步推導本頁的平均損失,再推導梯度。
每個新符號先定義,每一步說明用了哪個微分規則;先不要寫程式。
  • 把梯度檢查的表格畫成 log-log 圖($h$ 對最大誤差),觀察是否有步長過大或過小時誤差增加的趨勢。截斷誤差常呈 $h^2$、捨入項常呈 $1/h$ 的量級,但有限的取樣點與具體函數不保證畫出理想斜率。
  • 換成 3 vs 5(比較難分的一對),重跑可分性檢查。若未找到嚴格分隔,先核對求解器狀態;這仍不足以保證無正則化的 MLE 有有限且唯一的解。還可能有準完全分離或特徵相依,因此不要直接要求兩套方法的係數一致。
  • 掃過 $C$ 從 $10^{-3}$ 到 $10^{3}$,畫 $\|w\|$ 與訓練/測試準確率的曲線,看正則化路徑長什麼樣子。