简介这份资源是周志华《机器学习》西瓜书课程习题的代码实现合集面向正在学习机器学习基础理论、需要动手复现课后习题的高校学生与自学者帮助解决从公式推导到代码落地之间的衔接问题。压缩包共90个文件约11.74MB以22个Python脚本为核心覆盖KMeans聚类、KNN、PCA降维、AdaBoost、StumpBagging、SVM、决策树等经典算法的习题实现并配有10份Markdown习题笔记、34张结果截图、若干CSV与TXT格式的西瓜数据集、MAT数据文件及XML工程配置便于对照运行与验证。目录按章节组织从模型评估与选择、线性模型、决策树、神经网络、支持向量机到贝叶斯分类、集成学习、聚类与降维结构清晰方便按知识点检索。目前已有934人学习下载适合希望系统完成西瓜书课后练习、积累算法实现经验并查漏补缺的读者参考。1. 西瓜书作业代码实现从公式到手写 NumPy 的落地路线很多人第一次翻开《机器学习》周志华著圈内叫西瓜书时会被满页的公式劝退但真正卡住人的往往不是数学而是合上书之后不知道代码该从哪一行写起。西瓜书机器学习课程作业代码实现这件事本质上是把书里每一章的算法从数学定义翻译成可运行的 Python再用数据集验证它到底学没学到东西。它解决的是「看得懂推导、写不出实现」这个断层适合正在上机器学习课程、准备期末或课程设计、以及想靠手写代码真正吃透算法的人。我的建议是别一上来就调 sklearn先用 NumPy 把线性回归、对数几率回归、决策树、K-Means 这几个核心算法各写一遍再回头对照库函数你会发现自己对梯度、损失、收敛的理解完全不一样。这一章先把整条路线讲清楚后面几章逐个算法拆开。2. 作业环境与数据准备把西瓜数据集 3.0 变成能喂给模型的矩阵2.1 为什么先搭环境再谈算法西瓜书里的例子大多围绕西瓜数据集 2.0 和 3.0 展开这两个数据集规模很小但字段类型混杂有离散的色泽、敲声也有连续的密度、含糖率。很多同学作业跑不通不是算法写错而是数据在第一步就变形了。常见做法是固定一套环境Python 3.9 以上、NumPy、pandas、Matplotlib不依赖任何自动机器学习框架。这样做的理由是作业的目的是理解算法一旦引入高层封装梯度怎么算、树怎么分裂都被藏进黑匣子出了问题你连排查方向都没有。我一般会建一个独立虚拟环境把依赖锁死避免不同作业之间版本打架。下面是最小可用的环境准备命令注意 NumPy 版本不要低于 1.21否则后面矩阵运算里的一些广播写法会报奇怪的错。python -m venv ml_hw source ml_hw/bin/activate # Windows 用 ml_hw\Scripts\activate pip install numpy pandas matplotlib pip freeze requirements.txt # 锁版本方便复现逻辑说明虚拟环境隔离依赖freeze 把当前版本写进文件下次换机器直接pip install -r requirements.txt就能还原。参数上唯一要留意的是 Python 版本3.9 到 3.11 都稳3.12 早期有些科学计算包轮子不全容易在安装阶段翻车。2.2 西瓜数据集 3.0 的字段与编码方式西瓜数据集 3.0 一共 17 个样本包含色泽、根蒂、敲声、纹理、脐部、触感 6 个离散特征密度、含糖率 2 个连续特征以及好瓜、坏瓜标签。作业里最常见的两种处理是离散特征做 one-hot 或序号编码连续特征做标准化。这里有个血泪经验——序号编码会给无序特征强加大小关系比如把色泽「青绿、乌黑、浅白」编成 0、1、2模型会误以为浅白比青绿「大」线性模型上尤其明显。所以离散无序特征优先 one-hot只有像「大小」这种本身有序的才用序号。下面这段代码把数据集手工录入并做 one-hot之所以手工录入是因为作业环境常常没有网络直接内嵌最稳。import numpy as np import pandas as pd data { 色泽: [青绿,乌黑,乌黑,青绿,浅白,青绿,乌黑,乌黑,乌黑,青绿,浅白,浅白,青绿,浅白,乌黑,浅白,青绿], 根蒂: [蜷缩,蜷缩,蜷缩,蜷缩,蜷缩,稍蜷,稍蜷,稍蜷,稍蜷,硬挺,硬挺,蜷缩,稍蜷,稍蜷,稍蜷,蜷缩,蜷缩], 敲声: [浊响,沉闷,浊响,浊响,浊响,浊响,浊响,沉闷,浊响,清脆,清脆,浊响,浊响,沉闷,浊响,浊响,沉闷], 纹理: [清晰,清晰,清晰,清晰,清晰,清晰,稍糊,清晰,稍糊,清晰,稍糊,稍糊,清晰,稍糊,稍糊,清晰,稍糊], 脐部: [凹陷,凹陷,凹陷,凹陷,凹陷,稍凹,稍凹,稍凹,稍凹,平坦,平坦,平坦,凹陷,稍凹,稍凹,凹陷,稍凹], 触感: [硬滑,硬滑,硬滑,硬滑,硬滑,软粘,软粘,硬滑,硬滑,硬滑,软粘,软粘,硬滑,软粘,硬滑,硬滑,硬滑], 密度: [0.697,0.774,0.634,0.608,0.556,0.403,0.481,0.437,0.666,0.243,0.245,0.343,0.639,0.657,0.360,0.593,0.719], 含糖率: [0.460,0.376,0.264,0.318,0.215,0.237,0.149,0.211,0.091,0.267,0.057,0.099,0.161,0.198,0.370,0.042,0.103], 好瓜: [是,是,是,是,是,是,是,是,否,否,否,否,否,否,否,否,否], } df pd.DataFrame(data) # 离散无序特征 one-hot连续特征保留原值 discrete_cols [色泽,根蒂,敲声,纹理,脐部,触感] df_encoded pd.get_dummies(df, columnsdiscrete_cols, prefixdiscrete_cols) X df_encoded.drop(columns[好瓜]).astype(float).values y (df[好瓜] 是).astype(int).values # 连续特征标准化避免量纲影响梯度下降 cont_idx [df_encoded.columns.get_loc(c) for c in [密度,含糖率]] X[:, cont_idx] (X[:, cont_idx] - X[:, cont_idx].mean(axis0)) / X[:, cont_idx].std(axis0) print(X.shape, y.shape) # (17, 24) (17,)逻辑说明pd.get_dummies把 6 个离散列展开成 0/1 列连续列不动标准化只对密度和含糖率做因为 one-hot 列本身已经是 0/1再标准化会破坏稀疏性。参数上axis0表示按列求均值和标准差这是特征维度的正确方向写成 axis1 就是按样本标准化属于典型误用。跑完打印形状17 个样本、24 维特征样本数远小于特征数这也提醒你后面线性模型必须考虑过拟合。3. 线性模型作业从最小二乘到对数几率回归的手写实现3.1 线性回归的闭式解与梯度下降两条路西瓜书第 3 章线性模型是作业重灾区因为它同时考数学推导和代码。线性回归有两种解法闭式解正规方程和梯度下降。闭式解一行就能写但要求矩阵可逆或加正则梯度下降要自己定学习率和迭代次数能让你真正看到损失下降的过程。作业里我建议两条都写然后对比结果这样你对「为什么实际工程更常用梯度下降」会有体感——特征维度一高矩阵求逆的代价就上去了。import numpy as np def linear_regression_closed(X, y): # 加一列偏置项对应 w0 Xb np.hstack([np.ones((X.shape[0], 1)), X]) # 正规方程 w (X^T X)^-1 X^T y加小正则防止奇异 A Xb.T Xb 1e-6 * np.eye(Xb.shape[1]) w np.linalg.solve(A, Xb.T y) return w def linear_regression_gd(X, y, lr0.01, epochs2000): Xb np.hstack([np.ones((X.shape[0], 1)), X]) w np.zeros(Xb.shape[1]) losses [] for _ in range(epochs): pred Xb w err pred - y grad Xb.T err / X.shape[0] # 均方误差对 w 的梯度 w - lr * grad losses.append(np.mean(err ** 2)) return w, losses逻辑说明闭式解里加1e-6 * np.eye是数值上的后悔药防止 X^T X 奇异导致求解失败梯度下降里grad Xb.T err / n是均方误差的梯度除以样本数是取平均学习率 lr 控制步长。参数上lr 取 0.01 到 0.1 之间比较稳太大损失会震荡甚至发散太小 2000 轮都收敛不了。判断收敛不要只看轮数把 losses 画出来看到曲线走平才算真收敛。3.2 对数几率回归sigmoid、交叉熵与牛顿法对数几率回归Logistic Regression名字带回归实际是分类。它的核心是把线性输出塞进 sigmoid 得到概率再用极大似然推出交叉熵损失。作业里常见要求是用牛顿法求解因为西瓜书 3.3 节给了牛顿法的更新公式。牛顿法收敛快但要算 Hessian 矩阵维度高时开销大这也是为什么工程上更常用梯度下降。def sigmoid(z): # 数值稳定写法避免 exp 溢出 return np.where(z 0, 1/(1np.exp(-z)), np.exp(z)/(1np.exp(z))) def logistic_gd(X, y, lr0.1, epochs3000): Xb np.hstack([np.ones((X.shape[0], 1)), X]) w np.zeros(Xb.shape[1]) for _ in range(epochs): p sigmoid(Xb w) grad Xb.T (p - y) / X.shape[0] # 交叉熵梯度形式与线性回归一致 w - lr * grad return w def logistic_newton(X, y, epochs20): Xb np.hstack([np.ones((X.shape[0], 1)), X]) w np.zeros(Xb.shape[1]) for _ in range(epochs): p sigmoid(Xb w) grad Xb.T (p - y) # Hessian: X^T diag(p(1-p)) X S np.diag(p * (1 - p)) H Xb.T S Xb 1e-6 * np.eye(Xb.shape[1]) w - np.linalg.solve(H, grad) return w逻辑说明sigmoid 用np.where分正负两支是为了防止 z 很负时np.exp(-z)溢出这是数值计算里必须养成的习惯。梯度下降的梯度Xb.T (p - y)和线性回归形式一样区别只在 p 是 sigmoid 输出。牛顿法里 Hessian 是X^T diag(p(1-p)) X加小正则同样防奇异。参数上牛顿法迭代 20 次通常足够梯度下降要几千次学习率 lr 在逻辑回归上可以比线性回归大一点0.1 起步比较合适。3.3 用准确率和混淆矩阵验证分类结果写完模型不算完作业要给出评估。西瓜数据集只有 17 个样本直接看准确率会很不稳定所以最好同时打印混淆矩阵看看是哪个类被错分。常见做法是留一法交叉验证每次留一个样本做测试这样能充分利用小数据集。def leave_one_out(X, y, train_fn): correct 0 for i in range(len(y)): mask np.arange(len(y)) ! i w train_fn(X[mask], y[mask]) Xb np.hstack([np.ones((1, 1)), X[i:i1]]) pred 1 if sigmoid(Xb w)[0] 0.5 else 0 correct (pred y[i]) return correct / len(y) acc leave_one_out(X, y, lambda Xt, yt: logistic_gd(Xt, yt, lr0.1, epochs3000)) print(留一法准确率:, acc)逻辑说明留一法每次只留一个样本测试训练集是其余全部适合样本极少的情况。参数上sigmoid(...) 0.5是默认阈值如果正负样本不平衡这个阈值要调不能死守 0.5。跑出来准确率大概在 0.7 到 0.9 之间波动别因为一次结果低就怀疑代码小数据集本身方差就大。4. 决策树与聚类作业信息增益、基尼指数和 K-Means 的代码骨架4.1 决策树分裂准则信息增益和基尼指数怎么选西瓜书第 4 章决策树作业通常要求实现 ID3信息增益或 CART基尼指数。信息增益偏向取值多的特征这是它的固有缺陷西瓜书里也专门讲了用增益率C4.5来纠正。基尼指数计算更简单没有对数运算工程上更常用。作业里两个都实现一遍对比选出来的根节点特征你会对「准则影响树结构」有直观认识。import numpy as np def entropy(y): _, counts np.unique(y, return_countsTrue) p counts / counts.sum() return -np.sum(p * np.log2(p 1e-12)) def gini(y): _, counts np.unique(y, return_countsTrue) p counts / counts.sum() return 1 - np.sum(p ** 2) def info_gain(X_col, y): base entropy(y) vals np.unique(X_col) rest 0.0 for v in vals: sub y[X_col v] rest len(sub) / len(y) * entropy(sub) return base - rest def gini_index(X_col, y): vals np.unique(X_col) rest 0.0 for v in vals: sub y[X_col v] rest len(sub) / len(y) * gini(sub) return rest逻辑说明entropy 和 gini 都是先统计标签分布再算纯度1e-12是防止 log2(0) 报错。info_gain 用父节点熵减去按特征划分后的加权熵gini_index 直接算划分后的加权基尼越小越好。参数上注意np.unique对连续特征会返回每个不同取值决策树处理连续特征时要先排序再找切分点不能直接当离散用这是作业里最容易忽略的一步。4.2 递归建树与剪枝的边界处理决策树的核心是递归选最优特征、划分、对子集继续建树直到全纯或没有特征可用。递归写不好容易栈溢出或死循环边界条件必须写全。预剪枝用最大深度、最小样本数控制后剪枝要先建完整树再自底向上评估作业里预剪枝更常见因为实现简单。def build_tree(X, y, feat_names, depth0, max_depth5): # 边界1全纯 if len(np.unique(y)) 1: return {leaf: y[0]} # 边界2达到深度上限或无特征 if depth max_depth or X.shape[1] 0: return {leaf: np.bincount(y).argmax()} best_feat, best_gain None, -1 for i in range(X.shape[1]): g info_gain(X[:, i], y) if g best_gain: best_gain, best_feat g, i if best_gain 1e-9: # 增益太小停止分裂 return {leaf: np.bincount(y).argmax()} node {feat: feat_names[best_feat], children: {}} for v in np.unique(X[:, best_feat]): mask X[:, best_feat] v node[children][v] build_tree( np.delete(X[mask], best_feat, axis1), y[mask], [n for j, n in enumerate(feat_names) if j ! best_feat], depth 1, max_depth) return node逻辑说明三个边界分别是全纯、深度到顶、增益过小缺一个都可能递归失控。np.bincount(y).argmax()是取多数类作为叶子标签。参数上 max_depth 控制过拟合西瓜数据集上 3 到 5 比较合适太深会把每个样本都记住训练准确率 100% 但没泛化。注意每次递归要删掉已用特征否则同一个特征会被反复选。4.3 K-Means 的初始化陷阱与迭代终止条件西瓜书第 9 章聚类K-Means 是必做作业。它的坑集中在初始中心点选择上随机选可能收敛到很差的局部最优。常见改进是 K-Means让初始中心尽量分散。迭代终止条件一般用中心点变化小于阈值或达到最大轮数。def kmeans(X, k, max_iter100, tol1e-6, seed42): rng np.random.default_rng(seed) # K-Means 初始化 centers [X[rng.integers(len(X))]] for _ in range(k - 1): d2 np.min([np.sum((X - c) ** 2, axis1) for c in centers], axis0) prob d2 / d2.sum() centers.append(X[rng.choice(len(X), pprob)]) centers np.array(centers) for _ in range(max_iter): labels np.argmin(np.linalg.norm(X[:, None] - centers[None], axis2), axis1) new_centers np.array([X[labels j].mean(axis0) if np.any(labels j) else centers[j] for j in range(k)]) if np.linalg.norm(new_centers - centers) tol: break centers new_centers return labels, centers逻辑说明K-Means 第一个中心随机选后续每个中心按距离平方成正比的概率选距离越远被选中概率越大从而分散。np.linalg.norm(X[:, None] - centers[None], axis2)算出每个样本到每个中心的距离矩阵。参数上 seed 固定保证可复现tol 控制收敛精度max_iter 兜底防止不收敛时死循环。注意空簇的处理某个中心没有样本归属时要保留原中心否则会出 NaN。5. 避坑与排查西瓜书作业代码最常见的 5 个翻车点5.1 矩阵维度对不上广播悄悄帮你算错现象代码不报错但结果完全不对损失不下降。原因NumPy 广播机制在维度不匹配时会自动扩展比如(17,1)和(17,)相加得到(17,17)你以为在算误差其实在算外积。解决每个矩阵运算前打印 shape养成assert习惯比如assert Xb.shape[0] y.shape[0]。这是最隐蔽的坑因为不报错只能靠结果异常反推。5.2 学习率设太大损失震荡甚至变成 NaN现象训练几轮后损失突然变成 nan 或无穷大。原因学习率过大梯度更新步子太猛参数飞到发散区sigmoid 或 exp 溢出。解决先把 lr 调小一个数量级试比如从 0.1 降到 0.01同时给 sigmoid 加数值稳定写法。判断标准是看损失曲线正常应该单调下降后走平如果上下跳就是 lr 大了。5.3 忘记加偏置项模型永远欠拟合现象线性回归和逻辑回归结果总是差一截怎么调都上不去。原因设计矩阵没有加全 1 列模型只能过原点表达能力受限。解决统一在X前面hstack一列 1对应权重 w0。这个坑在作业里极常见因为西瓜书公式里 w 和 b 分开写代码里合并成增广矩阵时容易漏。5.4 离散特征序号编码线性模型被误导现象线性模型准确率异常低换 one-hot 后明显提升。原因无序离散特征用 0、1、2 编码模型误以为存在大小和等距关系。解决无序特征一律 one-hot有序特征才用序号。判断方法很简单问自己「这个特征的取值之间有没有天然顺序」没有就 one-hot。5.5 小数据集不做交叉验证结论不可信现象换一次随机种子准确率从 0.9 掉到 0.6。原因西瓜数据集只有 17 个样本单次划分测试集方差极大。解决用留一法或 k 折交叉验证报告平均准确率和标准差而不是单次结果。作业里如果只报一个数字基本没有说服力老师一看就知道你没做验证。6. 进阶技巧用梯度检验和损失曲线把作业从「能跑」做到「可信」作业能跑通只是及格线真正拉开差距的是你能不能证明自己的实现是对的。这里分享两个我一直在用的技巧。第一个是梯度检验手写梯度最容易出错用数值梯度对照解析梯度误差在 1e-6 量级就说明推导和代码一致。第二个是损失曲线它是最直观的黑匣子探测器收敛、震荡、发散一眼就能看出来。梯度检验的核心是中心差分对每个参数 w_i分别加和减一个极小量 eps算损失差除以 2eps得到数值梯度再和解析梯度比。def grad_check(f, grad_f, w, eps1e-5): num_grad np.zeros_like(w) for i in range(len(w)): w1, w2 w.copy(), w.copy() w1[i] eps w2[i] - eps num_grad[i] (f(w1) - f(w2)) / (2 * eps) ana_grad grad_f(w) rel_err np.linalg.norm(num_grad - ana_grad) / (np.linalg.norm(num_grad) np.linalg.norm(ana_grad) 1e-12) return rel_err逻辑说明中心差分比前向差分精度高一个量级eps 取 1e-5 是精度和舍入误差的折中太大截断误差明显太小浮点舍入误差占主导。相对误差公式里分母加 1e-12 防止除零。判断标准rel_err 小于 1e-6 说明梯度实现正确大于 1e-3 基本可以确定有 bug。参数上 eps 不要小于 1e-7否则数值误差会淹没真实梯度。损失曲线的用法更简单把每轮损失存进列表画出来。正常曲线是快速下降后平缓如果出现周期性震荡多半是学习率偏大如果一直不降检查梯度和数据标准化如果先降后升可能是过拟合或学习率调度有问题。我一般会把训练损失和验证损失画在同一张图上两条线的间距就是过拟合程度的直观体现。现象可能原因排查动作损失变 NaN学习率过大、exp 溢出降 lr、改数值稳定 sigmoid损失不下降梯度算错、未标准化梯度检验、检查标准化损失震荡学习率偏大lr 降一个数量级训练好验证差过拟合加正则、减深度、增数据结果随机波动大未交叉验证留一法或 k 折最后说个习惯。我早期做西瓜书作业时总想一口气把整章代码写完再跑结果一报错就不知道从哪查起。后来改成每写一个函数就用两三个样本手算验证比如 sigmoid(0) 必须等于 0.5entropy 全纯标签必须等于 0这些小断言帮我省了无数排查时间。作业代码的价值不在于跑出多高的准确率而在于你能对着每一行说清楚它对应书里哪个公式、哪个参数在控制什么。把梯度检验和损失曲线变成肌肉记忆你后面做课程设计选题、复现更复杂的模型都会轻松很多。希望帮到你。本文还有配套的精品资源点击获取