代碼實現(xiàn)指南:從公式推導到Python腳本的完整路徑)
簡介這份資源是周志華《機器學習》西瓜書課程習題的代碼實現(xiàn)合集面向正在學習機器學習基礎理論、需要動手復現(xiàn)課后習題的高校學生與自學者幫助讀者把聚類、線性模型、集成學習等章節(jié)的公式推導落到可運行代碼上。壓縮包共90個文件約11.74MB以22個Python腳本為核心覆蓋KMeans、KNN、PCA、AdaBoost、Bagging、SVM、決策樹等典型算法實現(xiàn)并配有10份Markdown習題筆記、34張結果圖與6張示意圖另有csv、txt、mat等西瓜數(shù)據(jù)集及少量xml、ipynb配置文件便于對照實驗與復現(xiàn)結果。目前已有934人學習下載。資源按章節(jié)組織目錄從模型評估與選擇到神經(jīng)網(wǎng)絡、貝葉斯分類、支持向量機、降維與度量學習逐章展開讀者可據(jù)此梳理算法脈絡、參考代碼實現(xiàn)思路并借助筆記與圖像驗證運行效果適合作為課程作業(yè)參考與機器學習入門實踐素材。1. 西瓜書作業(yè)代碼實現(xiàn)從公式到可運行腳本的第一道坎很多人第一次翻開《機器學習》周志華著圈內(nèi)叫“西瓜書”時前幾章還能跟上到了第三章線性模型的對數(shù)幾率回歸、第四章決策樹的信息增益、第五章神經(jīng)網(wǎng)絡的誤差逆?zhèn)鞑ス揭欢嗑涂ㄗ×恕U嬲屓朔嚨牟皇菙?shù)學本身而是把公式翻譯成代碼這一步——書上寫的是矩陣求導落到 Python 里要處理維度對齊、數(shù)值溢出、收斂判斷。這篇筆記面向正在做西瓜書課程作業(yè)、想自己動手把課后習題跑通的人也適合用吳恩達機器學習作業(yè)入門后想換一本教材鞏固的讀者。我會按章節(jié)把常見作業(yè)題的實現(xiàn)路徑拆開給出能直接抄的代碼骨架、參數(shù)設置和調(diào)試方法重點講清楚每一步為什么這么寫、哪里容易出錯。西瓜書配套的習題答案網(wǎng)上版本很多但代碼實現(xiàn)質量參差不齊與其抄一份跑不通的不如自己按下面的路子搭一遍。2. 線性模型作業(yè)對數(shù)幾率回歸與線性判別分析的代碼落地2.1 為什么先做線性模型梯度推導和代碼的對應關系西瓜書第三章的作業(yè)核心是兩件事對數(shù)幾率回歸邏輯回歸和線性判別分析LDA。這兩個模型的共同點是都能寫成矩陣形式代碼量不大但正好用來建立“公式到代碼”的映射習慣。對數(shù)幾率回歸的模型是 $y \frac{1}{1e^{-(\mathbf{w}^T\mathbf{x}b)}}$對數(shù)似然最大化等價于最小化交叉熵損失。書上用牛頓法求解但實際寫作業(yè)時用梯度下降更直觀也更容易調(diào)參。關鍵推導只有一步對 $w$ 求導得到 $\sum_i (\hat{y}_i - y_i)\mathbf{x}_i$這個形式非常干凈代碼里就是一個矩陣乘法。LDA 的作業(yè)通常要求實現(xiàn)二分類的投影方向求解。書上給的閉式解是 $\mathbf{w} \mathbf{S}_w^{-1}(\boldsymbol{\mu}_0 - \boldsymbol{\mu}_1)$其中 $\mathbf{S}_w$ 是類內(nèi)散度矩陣。代碼實現(xiàn)時最容易忽略的是 $\mathbf{S}_w$ 可能奇異需要加正則項或者用偽逆。2.2 對數(shù)幾率回歸的最小實現(xiàn)從數(shù)據(jù)生成到訓練下面這段代碼用 numpy 從零實現(xiàn)對數(shù)幾率回歸不依賴 sklearn適合直接作為作業(yè)提交的基礎版本。import numpy as np def sigmoid(z): # 裁剪防止 exp 溢出這是數(shù)值穩(wěn)定的關鍵 z np.clip(z, -500, 500) return 1.0 / (1.0 np.exp(-z)) def train_logistic(X, y, lr0.1, epochs1000, tol1e-6): X: (n_samples, n_features) y: (n_samples,) 取值 0 或 1 lr: 學習率 epochs: 最大迭代輪數(shù) tol: 梯度范數(shù)收斂閾值 n, d X.shape w np.zeros(d) b 0.0 prev_loss float(inf) for epoch in range(epochs): z X w b y_hat sigmoid(z) # 交叉熵損失 loss -np.mean(y * np.log(y_hat 1e-12) (1 - y) * np.log(1 - y_hat 1e-12)) # 梯度 error y_hat - y grad_w X.T error / n grad_b np.mean(error) w - lr * grad_w b - lr * grad_b if abs(prev_loss - loss) tol: break prev_loss loss return w, b def predict(X, w, b): return (sigmoid(X w b) 0.5).astype(int)這段代碼的邏輯說明sigmoid里做clip是防止 $z$ 過大時exp溢出這是很多人第一次寫邏輯回歸會遇到的問題表現(xiàn)為 loss 變成 nan。梯度推導對應error y_hat - y然后X.T error就是 $\sum_i (\hat{y}_i - y_i)\mathbf{x}_i$ 的矩陣形式。學習率lr默認 0.1如果 loss 震蕩就降到 0.01epochs設 1000 通常夠用配合tol提前停止。參數(shù)b的梯度是np.mean(error)因為對偏置求導沒有 $x$ 項。2.3 LDA 實現(xiàn)中的矩陣奇異問題與正則化處理LDA 的代碼短但坑集中在 $\mathbf{S}_w$ 求逆。下面是一個帶正則的版本。def train_lda(X, y, reg1e-4): X: (n_samples, n_features) y: (n_samples,) 取值 0 或 1 reg: 正則化系數(shù)防止類內(nèi)散度矩陣奇異 X0 X[y 0] X1 X[y 1] mu0 X0.mean(axis0) mu1 X1.mean(axis0) # 類內(nèi)散度矩陣 Sw np.zeros((X.shape[1], X.shape[1])) for x in X0: diff (x - mu0).reshape(-1, 1) Sw diff diff.T for x in X1: diff (x - mu1).reshape(-1, 1) Sw diff diff.T # 加正則項等價于嶺回歸思路 Sw reg * np.eye(X.shape[1]) w np.linalg.solve(Sw, (mu0 - mu1)) return w def predict_lda(X, w, threshold0.0): proj X w return (proj threshold).astype(int)邏輯說明Sw累加每個樣本的外積這一步用循環(huán)寫清楚實際作業(yè)里可以向量化。reg默認 1e-4如果數(shù)據(jù)維度高、樣本少Sw接近奇異加大到 1e-2 或 1e-1。np.linalg.solve比直接求逆數(shù)值更穩(wěn)。閾值threshold通常取兩類投影均值的中點這里簡化成 0實際作業(yè)里應該用(mu0 w mu1 w) / 2。2.4 線性模型作業(yè)的驗證方法與常見誤用寫完代碼后用西瓜書第三章的西瓜數(shù)據(jù)集 3.0α 驗證。這個數(shù)據(jù)集只有 17 個樣本2 個特征適合手算對照。常見誤用有兩個一是把y的標簽設成 -1 和 1 卻套用 0/1 的交叉熵公式導致 loss 計算錯誤二是 LDA 里忘記減均值直接對原始數(shù)據(jù)算散度矩陣。驗證時打印前幾輪 loss正常應該單調(diào)下降如果上下跳動就是學習率太大。另外邏輯回歸的準確率在西瓜數(shù)據(jù)集上大概 70% 左右不要期望太高這個數(shù)據(jù)集本身線性可分性一般。3. 決策樹作業(yè)信息增益、基尼指數(shù)與剪枝的代碼實現(xiàn)3.1 劃分準則的選型信息增益 vs 基尼指數(shù)西瓜書第四章作業(yè)通常要求實現(xiàn) ID3 或 C4.5核心是劃分準則。信息增益對取值多的屬性有偏好這是 ID3 的已知缺陷C4.5 用增益率修正但增益率對取值少的屬性有偏好所以書上建議先找信息增益高于平均的屬性再從中選增益率最高的?;嶂笖?shù)是 CART 用的計算更簡單沒有對數(shù)運算。作業(yè)里如果沒指定準則我一般用基尼指數(shù)因為代碼短、數(shù)值穩(wěn)定。如果題目明確要求信息增益就按書上公式來。下面以基尼指數(shù)為例。3.2 遞歸建樹的代碼骨架與停止條件import numpy as np from collections import Counter def gini(y): # y 是標簽數(shù)組 if len(y) 0: return 0.0 counts Counter(y) probs np.array(list(counts.values())) / len(y) return 1.0 - np.sum(probs ** 2) def split_dataset(X, y, feature, value): # 按特征 feature 是否等于 value 劃分 mask X[:, feature] value return X[mask], y[mask], X[~mask], y[~mask] def choose_best_split(X, y): best_gain -1 best_feature None best_value None base_gini gini(y) n len(y) for feature in range(X.shape[1]): for value in np.unique(X[:, feature]): X_left, y_left, X_right, y_right split_dataset(X, y, feature, value) if len(y_left) 0 or len(y_right) 0: continue # 加權基尼 weighted (len(y_left) / n) * gini(y_left) (len(y_right) / n) * gini(y_right) gain base_gini - weighted if gain best_gain: best_gain gain best_feature feature best_value value return best_feature, best_value, best_gain def build_tree(X, y, depth0, max_depth10, min_samples2): # 停止條件純節(jié)點、樣本太少、達到最大深度 if len(set(y)) 1 or len(y) min_samples or depth max_depth: return Counter(y).most_common(1)[0][0] feature, value, gain choose_best_split(X, y) if feature is None or gain 0: return Counter(y).most_common(1)[0][0] X_left, y_left, X_right, y_right split_dataset(X, y, feature, value) tree { feature: feature, value: value, left: build_tree(X_left, y_left, depth 1, max_depth, min_samples), right: build_tree(X_right, y_right, depth 1, max_depth, min_samples) } return tree邏輯說明gini計算基尼值choose_best_split遍歷所有特征和取值選加權基尼最小的劃分。build_tree的停止條件有三個節(jié)點純、樣本數(shù)小于min_samples、深度達到max_depth。max_depth默認 10西瓜數(shù)據(jù)集上 3 到 5 就夠min_samples默認 2防止過擬合。注意gain 0時也停止避免無意義劃分。3.3 預剪枝與后剪枝的實現(xiàn)差異西瓜書第四章專門講了預剪枝和后剪枝。預剪枝在build_tree里加驗證集判斷劃分前先在驗證集上算準確率劃分后再算如果沒提升就不劃分。后剪枝是先建完整棵樹再自底向上遍歷把子樹替換成葉節(jié)點看驗證集準確率是否提升。預剪枝代碼改動小在choose_best_split之后加一段驗證邏輯即可。后剪枝需要額外寫一個遍歷函數(shù)用遞歸返回剪枝后的樹。作業(yè)里如果要求對比建議兩種都實現(xiàn)用同一份驗證集。注意預剪枝有欠擬合風險后剪枝計算量大但泛化通常更好。3.4 決策樹作業(yè)的調(diào)試連續(xù)值處理和缺失值西瓜數(shù)據(jù)集 3.0α 全是離散值但作業(yè)有時會給連續(xù)值版本。連續(xù)值處理方法是二分法對每個特征排序取相鄰值中點作為劃分點選基尼最小的。代碼里np.unique換成排序后的中點數(shù)組即可。缺失值處理書上講了權重法代碼實現(xiàn)復雜作業(yè)里如果沒要求可以跳過。如果要求核心是給每個樣本加權重劃分時按權重計算基尼缺失樣本同時進入所有分支但權重按分支比例調(diào)整。這部分容易寫錯建議先用小數(shù)據(jù)手工驗證。4. 神經(jīng)網(wǎng)絡作業(yè)BP 算法的矩陣實現(xiàn)與梯度檢查4.1 為什么 BP 作業(yè)容易翻車維度對齊和鏈式法則西瓜書第五章的 BP 神經(jīng)網(wǎng)絡作業(yè)翻車率最高。原因不是算法難而是矩陣維度容易對不上。書上公式用下標表示代碼里要轉成矩陣乘法稍不注意就廣播錯誤。另一個坑是鏈式法則的中間項漏乘導致梯度方向對但大小錯訓練很慢。我一般建議先寫一個單隱層網(wǎng)絡用西瓜數(shù)據(jù)集 3.0α 的 17 個樣本做二分類隱藏層 5 個神經(jīng)元這樣維度小出錯容易查。4.2 單隱層 BP 網(wǎng)絡的完整代碼import numpy as np def sigmoid(z): z np.clip(z, -500, 500) return 1.0 / (1.0 np.exp(-z)) def sigmoid_deriv(a): # a 是 sigmoid 的輸出 return a * (1 - a) def train_bp(X, y, hidden5, lr0.5, epochs5000): n, d X.shape y y.reshape(-1, 1) # 初始化權重小隨機數(shù) np.random.seed(42) W1 np.random.randn(d, hidden) * 0.1 b1 np.zeros((1, hidden)) W2 np.random.randn(hidden, 1) * 0.1 b2 np.zeros((1, 1)) losses [] for epoch in range(epochs): # 前向 z1 X W1 b1 a1 sigmoid(z1) z2 a1 W2 b2 a2 sigmoid(z2) # 損失 loss -np.mean(y * np.log(a2 1e-12) (1 - y) * np.log(1 - a2 1e-12)) losses.append(loss) # 反向 dz2 a2 - y dW2 a1.T dz2 / n db2 np.mean(dz2, axis0, keepdimsTrue) da1 dz2 W2.T dz1 da1 * sigmoid_deriv(a1) dW1 X.T dz1 / n db1 np.mean(dz1, axis0, keepdimsTrue) # 更新 W2 - lr * dW2 b2 - lr * db2 W1 - lr * dW1 b1 - lr * db1 return W1, b1, W2, b2, losses邏輯說明W1是輸入到隱藏層形狀(d, hidden)W2是隱藏到輸出形狀(hidden, 1)。反向傳播里dz2 a2 - y是輸出層誤差dW2 a1.T dz2 / n對應鏈式法則。da1 dz2 W2.T把誤差傳回隱藏層再乘sigmoid_deriv(a1)得到dz1。學習率lr默認 0.5BP 網(wǎng)絡對學習率敏感太大震蕩太小收斂慢。epochs設 5000配合觀察losses曲線。4.3 梯度檢查數(shù)值梯度驗證解析梯度寫完反向傳播后用數(shù)值梯度檢查。對每個參數(shù)擾動一小量算損失變化和解析梯度對比。def gradient_check(X, y, W1, b1, W2, b2, epsilon1e-4): # 只檢查 W1 的第一個元素作為示例 def loss_fn(W1, b1, W2, b2): a1 sigmoid(X W1 b1) a2 sigmoid(a1 W2 b2) return -np.mean(y * np.log(a2 1e-12) (1 - y) * np.log(1 - a2 1e-12)) # 解析梯度 a1 sigmoid(X W1 b1) a2 sigmoid(a1 W2 b2) dz2 a2 - y.reshape(-1, 1) dW2 a1.T dz2 / len(y) da1 dz2 W2.T dz1 da1 * sigmoid_deriv(a1) dW1 X.T dz1 / len(y) # 數(shù)值梯度 W1_plus W1.copy() W1_plus[0, 0] epsilon W1_minus W1.copy() W1_minus[0, 0] - epsilon num_grad (loss_fn(W1_plus, b1, W2, b2) - loss_fn(W1_minus, b1, W2, b2)) / (2 * epsilon) print(f解析梯度: {dW1[0, 0]:.6f}, 數(shù)值梯度: {num_grad:.6f})邏輯說明數(shù)值梯度用中心差分精度比前向差分高。解析梯度和數(shù)值梯度相對誤差小于 1e-4 算通過。如果差很多檢查鏈式法則哪一項漏了。這個檢查在作業(yè)里很加分也能幫你快速定位 bug。4.4 BP 作業(yè)的調(diào)參經(jīng)驗學習率、隱藏層節(jié)點和初始化學習率從 0.1 到 1.0 試西瓜數(shù)據(jù)集上 0.5 左右通常可以。隱藏層節(jié)點數(shù)從 3 到 10 試太多會過擬合太少欠擬合。初始化用小的隨機數(shù)全零會導致對稱權重無法打破這是經(jīng)典坑。如果 loss 不下降先檢查sigmoid_deriv是不是用了a * (1 - a)而不是z * (1 - z)。5. 支持向量機作業(yè)對偶問題、核函數(shù)與 SMO 的簡化實現(xiàn)5.1 SVM 作業(yè)的難點從原始問題到對偶問題的代碼映射西瓜書第六章的 SVM 作業(yè)理論推導長代碼實現(xiàn)更難。書上先講原始問題再講對偶問題最后講 KKT 條件和 SMO。作業(yè)通常要求實現(xiàn)一個簡化版 SMO在西瓜數(shù)據(jù)集上跑通。核心公式對偶問題是 $\max_\alpha \sum_i \alpha_i - \frac{1}{2}\sum_{i,j}\alpha_i\alpha_j y_i y_j \mathbf{x}_i^T\mathbf{x}_j$約束是 $0 \le \alpha_i \le C$ 和 $\sum_i \alpha_i y_i 0$。SMO 每次選兩個 $\alpha$ 更新固定其他。5.2 簡化 SMO 的代碼實現(xiàn)import numpy as np def smo_simple(X, y, C1.0, tol1e-3, max_iter100): n, d X.shape y y.astype(float) alpha np.zeros(n) b 0.0 # 核矩陣線性核 K X X.T for it in range(max_iter): alpha_pairs_changed 0 for i in range(n): # 計算誤差 f_i np.sum(alpha * y * K[:, i]) b E_i f_i - y[i] if (y[i] * E_i -tol and alpha[i] C) or (y[i] * E_i tol and alpha[i] 0): # 選第二個 alpha簡化版隨機選 j (i 1) % n f_j np.sum(alpha * y * K[:, j]) b E_j f_j - y[j] alpha_i_old alpha[i] alpha_j_old alpha[j] # 計算上下界 if y[i] ! y[j]: L max(0, alpha[j] - alpha[i]) H min(C, C alpha[j] - alpha[i]) else: L max(0, alpha[i] alpha[j] - C) H min(C, alpha[i] alpha[j]) if L H: continue eta 2 * K[i, j] - K[i, i] - K[j, j] if eta 0: continue alpha[j] alpha_j_old - y[j] * (E_i - E_j) / eta alpha[j] np.clip(alpha[j], L, H) if abs(alpha[j] - alpha_j_old) 1e-5: continue alpha[i] alpha_i_old y[i] * y[j] * (alpha_j_old - alpha[j]) # 計算 b b1 b - E_i - y[i] * (alpha[i] - alpha_i_old) * K[i, i] - y[j] * (alpha[j] - alpha_j_old) * K[i, j] b2 b - E_j - y[i] * (alpha[i] - alpha_i_old) * K[i, j] - y[j] * (alpha[j] - alpha_j_old) * K[j, j] if 0 alpha[i] C: b b1 elif 0 alpha[j] C: b b2 else: b (b1 b2) / 2 alpha_pairs_changed 1 if alpha_pairs_changed 0: break return alpha, b邏輯說明K是核矩陣線性核直接X X.T。外層循環(huán)遍歷每個樣本內(nèi)層選第二個 $\alpha$簡化版直接取下一個。eta是二階導數(shù)小于等于 0 時跳過。L和H是 $\alpha_j$ 的上下界分 $y_i \ne y_j$ 和相等兩種情況。b的計算分三種情況對應書上公式。C默認 1.0控制軟間隔tol默認 1e-3KKT 違反容忍度。5.3 核函數(shù)的替換與參數(shù)選擇把K X X.T換成高斯核def rbf_kernel(X, sigma1.0): # 計算平方歐氏距離矩陣 sq np.sum(X ** 2, axis1, keepdimsTrue) dist sq sq.T - 2 * X X.T return np.exp(-dist / (2 * sigma ** 2))sigma控制核寬度太小過擬合太大欠擬合。西瓜數(shù)據(jù)集上從 0.5 到 2.0 試。多項式核類似改K (X X.T 1) ** degree。5.4 SVM 作業(yè)的驗證支持向量和決策邊界訓練完后alpha 1e-5的樣本就是支持向量。打印支持向量個數(shù)西瓜數(shù)據(jù)集上通常 5 到 8 個。決策邊界用w np.sum(alpha * y * X.T, axis1)算然后畫線。如果支持向量太多說明C太大或核參數(shù)不合適。6. 集成學習與聚類作業(yè)從 AdaBoost 到 K-means 的代碼收尾6.1 AdaBoost 的實現(xiàn)要點樣本權重更新和基學習器西瓜書第八章的 AdaBoost 作業(yè)核心是樣本權重 $D_t$ 的更新和基學習器權重 $\alpha_t$ 的計算?;鶎W習器通常用決策樹樁即深度為 1 的決策樹。def adaboost(X, y, T10): n len(y) D np.ones(n) / n models [] alphas [] for t in range(T): # 訓練決策樹樁用加權基尼 stump build_stump(X, y, D) pred predict_stump(stump, X) err np.sum(D[pred ! y]) if err 0.5: break alpha 0.5 * np.log((1 - err) / (err 1e-12)) # 更新權重 D D * np.exp(-alpha * y * pred) D D / np.sum(D) models.append(stump) alphas.append(alpha) return models, alphas邏輯說明build_stump是帶權重的決策樹樁選加權基尼最小的劃分。err是加權錯誤率大于等于 0.5 時停止。alpha是基學習器權重錯誤率越低權重越大。D更新后歸一化。預測時加權投票。6.2 K-means 的坑初始化、空簇和收斂判斷西瓜書第九章的 K-means 作業(yè)代碼短但坑多。初始化用隨機選點可能選到同一個點導致空簇。解決辦法是用 K-means 初始化或者選到重復點時重新選。def kmeans(X, k, max_iter100, tol1e-4): n, d X.shape # K-means 初始化 centers [X[np.random.randint(n)]] for _ in range(1, k): dist np.min([np.sum((X - c) ** 2, axis1) for c in centers], axis0) prob dist / np.sum(dist) centers.append(X[np.random.choice(n, pprob)]) centers np.array(centers) for it in range(max_iter): # 分配 dist np.array([np.sum((X - c) ** 2, axis1) for c in centers]) labels np.argmin(dist, axis0) # 更新 new_centers np.array([X[labels i].mean(axis0) if np.sum(labels i) 0 else centers[i] for i in range(k)]) if np.sum((new_centers - centers) ** 2) tol: break centers new_centers return labels, centers邏輯說明K-means 初始化讓初始中心盡量分散。分配步驟算每個點到中心的距離取最小。更新步驟對每個簇求均值空簇保持原中心。tol默認 1e-4中心變化小于這個值就停。6.3 聚類作業(yè)的評估輪廓系數(shù)和可視化K-means 沒有標簽評估用輪廓系數(shù)。西瓜數(shù)據(jù)集上k 從 2 到 5 試選輪廓系數(shù)最大的。可視化用 PCA 降到二維畫散點圖看簇是否分開。如果簇重疊嚴重可能數(shù)據(jù)本身不適合 K-means換 DBSCAN 或高斯混合。6.4 集成學習作業(yè)的對比AdaBoost vs 隨機森林隨機森林是 Bagging 的代表和 AdaBoost 的 Boosting 思路不同。作業(yè)里如果要求對比用同一份數(shù)據(jù)隨機森林的基學習器用完整決策樹AdaBoost 用樹樁。隨機森林對噪聲更魯棒AdaBoost 對噪聲敏感因為錯誤樣本權重會不斷增大。西瓜數(shù)據(jù)集上兩者準確率接近但隨機森林訓練更快因為可以并行。我做完這一輪作業(yè)最大的習慣是每寫一個模型先用小數(shù)據(jù)手工算一遍前向和反向確認公式和代碼對得上再上完整數(shù)據(jù)。這個習慣幫我省了無數(shù)調(diào)試時間。希望幫到你。本文還有配套的精品資源點擊獲取