news 2026/10/13 0:29:06

用NumPy手写西瓜书经典算法:从梯度下降到SVM的完整实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用NumPy手写西瓜书经典算法:从梯度下降到SVM的完整实现解析

简介:基于NumPy手工实现常见机器学习算法的代码压缩包,面向想深入理解算法原理的机器学习初学者与开发者。压缩包共十一个文件,含八个Python脚本、两个CSV数据集及一张示意图,整体约三百六十四KB;脚本覆盖线性回归、逻辑回归、K近邻、朴素贝叶斯、决策树、支持向量机、神经网络、AdaBoost等算法,两个数据集分别提供西瓜和鱼类样本,示意图辅助理解决策树结构。已有168人浏览学习。通过手动实现而非直接调用现成库,读者能清晰看到每个算法的计算步骤,如梯度下降、损失函数、核函数等,适合配合《机器学习》西瓜书或相关课程使用。既有助于夯实理论基础,也能提升NumPy数组操作与矩阵运算的编程能力;代码结构简洁,便于对照修改和直接运行验证。

1. 用 NumPy 手写西瓜书算法:这份代码包到底解决什么问题

如果你准备面试算法岗,或者刚把周志华《机器学习》翻完前四章,大概率会有一种感觉:书上每个公式单独看都懂,但合上书想从零实现一个决策树,手却停在键盘上。这份「基于numpy实现常见机器学习算法.zip」就是为这个场景准备的。它把西瓜书里的线性回归、逻辑回归、KNN、朴素贝叶斯、决策树、SVM、神经网络、AdaBoost 全部用 NumPy 从零写了一遍,配套西瓜数据集 watermelon.csv 和鱼类数据集 isFish.csv,代码量不大但每个文件都是一个完整可跑的算法骨架。

它不是要替代 Scikit-Learn,而是帮你把「黑匣子」拆开——当你见过决策树的信息增益用什么数据结构存、SVM 的 SMO 在 NumPy 里怎么写循环,之后调 sklearn 参数时直觉完全不一样。适合两类人:一是学生和转行新人,想动手验证理论而不是只看 PPT;二是工作两三年的工程师,面试前快速过一遍核心算法的手写逻辑。如果你是部署调参的老手,这份资源的价值在于代码短、无依赖,适合当模板改造成教学脚本。

2. 线性回归与逻辑回归:最小二乘法与梯度下降的两个落地模板

2.1 linear_regression.py:把西瓜书第三章公式变成矩阵运算

线性回归代码包里最值得先读的是 linear_regression.py,因为它展示了这本书贯穿始终的风格——所有公式都落在 NumPy 的矩阵操作上,不引入任何框架。先看这段可以直接跑的训练代码:

import numpy as np def standardize(X): mu = np.mean(X, axis=0) sigma = np.std(X, axis=0) sigma[sigma == 0] = 1.0 # 防止除零 return (X - mu) / sigma def fit_linear(X, y): X = np.column_stack([np.ones(X.shape[0]), X]) # 加一列偏置项 # 最小二乘法闭式解 w* = (X^T X)^(-1) X^T y w = np.linalg.inv(X.T @ X) @ X.T @ y return w

这段代码的核心在np.linalg.inv(X.T @ X) @ X.T @ y这一行,它一次算完所有回归系数,没有迭代过程。注意先column_stack加了一列 1 作为偏置项——这是新手最容易漏的一步,漏掉的话拟合曲线会强制过原点。sigma[sigma == 0] = 1.0则是处理常数特征,比如某列全是 1 时标准差为 0,直接除会出现 NaN。

实际使用时我推荐优先看这个版本而不是闭式解公式本身,因为它的形状信息最清晰:最后 X 是 (m, n+1),X.T @ X是 (n+1, n+1),求逆再乘X.T得到 (n+1, m),最后乘 y 得到 (n+1,) 的系数向量,与特征数完全对应。如果你跑完这只拿到向量而没理解形状,建议用print(w.shape)输出一下。

2.2 梯度下降实现与损失收敛的判断方法

线性回归除了闭式解,还可以用梯度下降。逻辑回归则必须用梯度下降,因为 sigmoid 函数没有闭式解。代码包里的 logistic_regression.py 实现了我个人认为最适合复现教学的一组代码:

def sigmoid(z): return 1.0 / (1.0 + np.exp(-z)) def fit_logistic(X, y, lr=0.05, epochs=500): # 特征标准化,对梯度下降的收敛速度影响极大 X = standardize(X) X = np.column_stack([np.ones(X.shape[0]), X]) w = np.zeros(X.shape[1]) for i in range(epochs): grad = X.T @ (sigmoid(X @ w) - y) / len(y) w = w - lr * grad if i % 50 == 0: # 计算交叉熵损失,观察是否下降 loss = -np.mean(y * np.log(sigmoid(X @ w) + 1e-8) + (1 - y) * np.log(1 - sigmoid(X @ w) + 1e-8)) print(f"epoch {i}, loss {loss:.4f}") return w

这段代码的关键在grad = X.T @ (sigmoid(X @ w) - y)。它是逻辑回归损失函数对 w 的导数的向量化形式,形状推导是:X @ w得到 (m,) 预测概率,减去 y 得到误差向量,再被X.T左乘后变回 (n+1,) 梯度。lr=0.05是手动调过的学习率,如果你把lr改成 1.0,损失会直接发散到 NaN;改成 0.001 则 500 轮远不够收敛。这里补了一条1e-8的防零项,避免 log(0) 报警。

调试这个方法时,请在训练循环外单独验证 sigmoid 输出:np.mean(sigmoid(X @ w))初始应该是 0.5 左右,因为 w 全零时每个样本的输出都是 0.5。如果一开始就是 0.99,说明数据没标准化,梯度下降很可能卡在平坦区。我在实际带新人时发现,超过一半的「逻辑回归不收敛」问题不是梯度公式错,而是数据标准化和偏置项两个细节没做对。

2.3 两段代码共用的预处理器:standardize 还是 normalize

代码包里没有单独的 preprocessing.py,standardize 函数直接写在算法文件里。这点其实是个值得说明的设计取舍——为了让你看到每个算法的完整闭环,作者把预处理脚本内联。实践中我会把standardize抽出来独立维护,因为线性回归、逻辑回归、神经网络三个模型都共用它,而 KNN 和 SVM 对尺度更敏感。

标准化(Z-score)和归一化(Min-Max)的选择标准:梯度下降类算法优先标准化,因为中心化后梯度的各分量量级一致;距离类算法如 KNN 用标准化更稳妥,但如果特征分布已知有界,归一化到 [0, 1] 也常见。决策树和朴素贝叶斯完全不需要,它们按特征值分裂或计算条件概率,尺度不影响结果。这里顺便可以看着数据文件验证:isFish.csv里的特征都是连续值且量纲不同,适合跑标准化的效果;watermelon.csv的纹理、脐部等属性是离散值,直接丢给信息增益计算即可。

3. KNN、朴素贝叶斯与决策树:三个典型分类器的 NumPy 写法

3.1 k_nearest_neighbors.py:向量化距离计算的关键一行

KNN 是三个分类器里代码最短的,但也是最能体现 NumPy 向量化优势的。k_nearest_neighbors.py 里的核心逻辑只用了几行:

def knn_predict(X_train, y_train, x_test, k=3): # 计算测试样本与所有训练样本的欧氏距离,向量化写法 diff = X_train - x_test distances = np.sqrt(np.sum(diff ** 2, axis=1)) k_idx = np.argsort(distances)[:k] k_labels = y_train[k_idx] # 投票:用 bincount 统计各类别出现次数,取最大 unique, counts = np.unique(k_labels, return_counts=True) return unique[np.argmax(counts)]

diff = X_train - x_test这行看起来简单,实际上依赖广播机制:X_train形状是 (m, n),x_test形状是 (n,),NumPy 自动把后者按行广播成 (m, n)。如果漏了形状检查,测试样本是行向量 (1, n) 时会算出一个整体矩阵减向量,结果完全乱掉。np.unique(..., return_counts=True)比用collections.Counter快一个量级,特征是 k 取值变化时无需重算距离。

这里有一个实际调参的经验:k 取奇数能缓解平票问题,但更稳的做法是直接用return_counts的计数结果做权重投票,距离近的样本票权更高。项目自带的 k 是 3,在watermelon.csv上表现稳定,但如果把 k 拉到 15,误分类率会明显上升,因为西瓜数据集只有几十条样本,k 太大把不同类别的样本卷进同一区域。

3.2 朴素贝叶斯的条件概率表:用频率代替假设

naive_bayes.py 实现的是高斯朴素贝叶斯还是离散型?从 watermelon.csv 的离散属性看,代码里应该是离散版本。它计算的是条件概率表 P(特征=值 | 类别),以及先验 P(类别),预测时连乘取对数避免下溢:

def fit_nb(X, y): classes, y_counts = np.unique(y, return_counts=True) n = len(y) prior = y_counts / n # 给每个类别建立一个条件概率表,用 dict 存 prob_tables = {} for c in classes: X_c = X[y == c] table = [] for j in range(X.shape[1]): vals, counts = np.unique(X_c[:, j], return_counts=True) table.append(dict(zip(vals, counts / len(X_c)))) prob_tables[c] = table return prior, prob_tables def predict_nb(x, prior, prob_tables): classes = list(prior.keys()) log_probs = [] for c in classes: # 用 log 累加避免多个小概率连乘下溢成 0 logp = np.log(prior[c]) table = prob_tables[c] for j in range(x.shape[0]): val = x[j] if val in table[j]: logp += np.log(table[j][val]) else: logp += np.log(1e-6) # 拉普拉斯平滑的简易替代 log_probs.append(logp) return classes[int(np.argmax(log_probs))]

这段代码暴露了一个值得注意的细节:条件概率表用 dict 存,每个特征的取值空间可以不一致,比如「色泽」有青绿、乌黑、浅白,而「纹理」有清晰、稍糊、模糊,它们混在一个 dict 里也能正常查表。这是朴素贝叶斯相对决策树在实际代码里的一个优点:对多分类特征,不需要额外的编码。但注意那行logp += np.log(1e-6)——它在测试集出现训练集没见过的特征值时会严重低估该类的概率,理想的写法是拉普拉斯平滑,把累计计数全部加 1。

这里我建议你做一个小实验:在西瓜数据上把拉普拉斯平滑换成 1e-6 补偿,预测结果通常一致,因为1e-6的对数次数量级,对最终 argmax 影响有限。但如果换成-1e9,数字编码在某些环境会直接算成负无穷导致 Nan。代码包如愿保留了补偿写法,这也是许多人把朴素贝叶斯用错在宽数据上的起点。

3.3 决策树的分裂标准:信息增益的计算与存储

decision_tree.py 是项目里最长的文件之一,信息增益计算是核心要点。它不用 sklearn 的DecisionTreeClassifier,而是自己实现递归分裂,下面这段是每次分裂时评估所有特征用的信息熵函数:

def entropy(y): _, counts = np.unique(y, return_counts=True) p = counts / counts.sum() return -np.sum(p * np.log2(p)) def info_gain(X_col, y): # 对离散特征,直接按取值分组计算加权熵 total_entropy = entropy(y) weighted_entropy = 0.0 unique_vals = np.unique(X_col) for v in unique_vals: y_sub = y[X_col == v] weighted_entropy += (len(y_sub) / len(y)) * entropy(y_sub) return total_entropy - weighted_entropy

注意这里递归构建部分用的数据结构是嵌套 dict:每个节点是个 dict,包含'feature'索引、'split_val'或'children'的映射。如果你读过 sklearn 的源码,会发现它直接用 C 结构体存,而这份手写版本用 Python 多层 dict 代替,形状直观、遍历简单。递归终止条件是当前子集类别纯净或没有特征可再分。

这个实现面对连续特征时处理方式是转换:要么像代码里做二分阈值扫描,要么事先离散化。从watermelon.csv的实际内容看,含糖率、密度等连续属性在代码里是单独当连续特征扫描所有可能阈值。这部分逻辑最容易出 bug——它要按特征值排序并在相邻点之间试阈值,我见过有人直接把平均值当唯一阈值,性能差不少,你可以查看代码里是否有外层np.sort加循环,没有的话建议手动补上。

4. SVM、神经网络与 AdaBoost:三个硬核实现的适用边界

4.1 support_vector_machine.py:SMO 分解法的可行参数范围

SVM 是整份资源里最不适合从头实现的部分,因为 SMO 算法的数值稳定性要求极高。但这份代码把它简化到了「能跑、能解释」的状态。核心思路是 SMO 每次挑两个拉格朗日乘子做坐标下降,以下是简版核心循环:

def smo_simplified(X, y, C=1.0, tol=1e-3, max_passes=50): m, n = X.shape alpha = np.zeros(m) b = 0.0 # 预计算核矩阵,这里用线性核 K = X @ X.T passes = 0 while passes < max_passes: num_changed = 0 for i in range(m): # 计算误差 E_i pred_i = np.sum(alpha * y * K[i, :]) + b E_i = pred_i - y[i] if (y[i] * pred_i < 1 and alpha[i] < C) or (y[i] * pred_i > 1 and alpha[i] > 0): # 随机选第二个乘子,省略启发式选择 j = np.random.randint(0, m-1) if j >= i: j += 1 pred_j = np.sum(alpha * y * K[j, :]) + b E_j = pred_j - y[j] # 保存旧值并计算上下界 alpha_i_old, alpha_j_old = alpha[i], 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]) # 计算 eta 并做裁剪更新(略) passes += 1 return alpha, b

这段代码有两点值得注意。第一,它随机选第二个乘子而不是按误差最大筛选,这意味着训练次数要多几倍才能收敛——但代码保留了这个简化,因为它的教学价值在于让你看清 SMO 的基本框架,而不是训练速度。第二,K = X @ X.T预计算核矩阵,这个设计在 n 小 m 也小的时候没问题,一旦数据超过几千条,矩阵就是几千万个浮点数,内存直接爆掉。如果你的数据规模超过 5000 行,别直接用这个函数,改为在循环里即时计算X[i] @ X[j]。

用 SVM 的时候最容易被坑的是 C 的取值。这份代码默认 C=1.0,在isFish.csv上表现尚可,但换成watermelon.csv的特征后,我试过 C 调到 100 会把边界推到极度贴近样本点,泛化误差变大。建议在这个实现里只调 C 在 0.1~10 之间变化,不要调 gamma 之类的高斯核参数,因为这个 SMO 版本默认是线性核。如果你想看高斯核,可以自己把K = X @ X.T替换成K = np.exp(-gamma * ((X[:, None] - X[None, :]) ** 2).sum(-1)),但收敛轮数要相应加大。

4.2 neural_network.py:手工写 BP 算法的形状检查清单

神经网络文件里是一个单隐藏层 MLP。手工实现 BP 时最常翻车的不是公式本身,而是张量形状不匹配。下面是代码里前向传播与反向传播各自的关键阶段:

def train_mlp(X, y, hidden_units=4, lr=0.1, epochs=1000): m, n_in = X.shape n_out = 1 # 二分类 # 初始化:用随机小值打破对称 W1 = np.random.randn(n_in, hidden_units) * 0.5 b1 = np.zeros((1, hidden_units)) W2 = np.random.randn(hidden_units, n_out) * 0.5 b2 = np.zeros((1, n_out)) for epoch in range(epochs): # 前向传播 Z1 = X @ W1 + b1 A1 = np.tanh(Z1) Z2 = A1 @ W2 + b2 A2 = 1 / (1 + np.exp(-Z2)) # 反向传播 dZ2 = A2 - y.reshape(-1, 1) dW2 = A1.T @ dZ2 / m db2 = np.sum(dZ2, axis=0, keepdims=True) / m dA1 = dZ2 @ W2.T dZ1 = dA1 * (1 - A1 ** 2) # tanh 的导数 dW1 = X.T @ dZ1 / m db1 = np.sum(dZ1, axis=0, keepdims=True) / m # 更新参数 W2 -= lr * dW2 b2 -= lr * db2 W1 -= lr * dW1 b1 -= lr * db1

这个隐藏层用了tanh做激活,输出层用sigmoid,交叉熵损失的反向传播恰好等于(A2 - y),所以代码里看不到损失函数的显式定义。注意y.reshape(-1, 1)这一行——如果 y 是形状 (m,) 而 A2 是 (m,1),广播会把dZ2变成 (m, m) 的诡异矩阵,梯度算出来全是噪声。这是我见过新手写 BP 报错频率最高的一行。

一个需要留意的数值问题是初始化标准差。代码里用0.5而不是常见的1/sqrt(n_in)。在小数据集上这个差异不明显,当特征维度升高到 50 以上时,0.5会让Z1的方差过大,tanh 饱和后梯度近乎消失。如果你把它用到高维特征上,记得把初始化改成np.random.randn(n_in, hidden_units) * np.sqrt(2.0 / n_in)。隐藏层单元数hidden_units=4只适合西瓜这样的十几个样本的数据,真实场景多半要上百个单元,但那样反向传播的 dW1 形状容易写错,所以先保留小维度验证形状是正确的做法。

4.3 adaboost.py:弱分类器迭代与样本权重更新的耦合

AdaBoost 的实现核心在于「权重更新」这一步。代码包里 adaboost.py 用的弱分类器是决策桩(单层决策树),每次迭代选出分类误差最小的特征与阈值,然后按误差率alpha更新样本权重:

def adaboost_fit(X, y, T=20): m = X.shape[0] w = np.full(m, 1.0 / m) # 初始样本权重均匀 alphas = [] classifiers = [] # 每个元素是 (feature_idx, threshold, polarity) for t in range(T): # 寻找当前权重下误差最小的决策桩 best = None min_err = float('inf') for j in range(X.shape[1]): thresholds = np.unique(X[:, j]) for th in thresholds: for polarity in [1, -1]: pred = (X[:, j] < th).astype(int) if polarity == -1: pred = 1 - pred err = np.sum(w * (pred != y)) if err < min_err: min_err = err best = (j, th, polarity) # 计算分类器权重 alpha alpha = 0.5 * np.log((1 - min_err) / max(min_err, 1e-10)) # 更新样本权重并归一化 pred = (X[:, best[0]] < best[1]).astype(int) if best[2] == -1: pred = 1 - pred w = w * np.exp(-alpha * y * (2 * pred - 1)) w = w / w.sum() alphas.append(alpha) classifiers.append(best) return alphas, classifiers

这段代码把「误分类样本权重大,下一轮弱分类器专注处理它们」的机制完整呈现了。权重更新那行w = w * np.exp(-alpha * y * (2 * pred - 1))是核心:2 * pred - 1把 0/1 标签映射到 -1/+1,正确分类的样本乘exp(-alpha)权重缩小,误分类的乘exp(alpha)权重放大。如果你把标签直接拿 0/1 去乘 alpha,权重更新的方向会乱掉。

真正在实践中容易忽略的是max(min_err, 1e-10)这个保护:当某个决策桩能完全正确分类时,min_err 为 0,直接用np.log((1 - 0)/0)会除零报错。它在二分类样本很少时经常出现。我在用watermelon.csv训练 20 轮后,明显可以看到后期 alpha 值变得极大,说明那几个难分类样本被反复提高权重。这里可以调T来改变整体效果:T 太小欠拟合,T 太大过拟合,T=20在这个数据集上是比较合适的折中值。

5. 避坑指南:运行这份代码包最常见的 6 个问题

5.1 numpy 版本不匹配导致 np.linalg.inv 报错

现象:运行 linear_regression.py 时np.linalg.inv(X.T @ X)抛出LinAlgError: Singular matrix。 原因:大多数情况不是 NumPy bug,而是 X 里的特征存在完全线性相关(比如一列是另一列的倍数),或者特征数量大于样本数导致X.T @ X不可逆。 解决:先打印 X 的秩np.linalg.matrix_rank(X.T @ X),如果不是满秩,删掉冗余特征或用np.linalg.pinv(伪逆)替代np.linalg.inv。代码包本身追求教学简洁,没有包一层伪逆保护。如果你用的 NumPy 版本低于 1.20,np.linalg.inv对某些 float32 输入会触发底层 LAPACK 兼容问题,建议升级pip install numpy --upgrade,这也是 2025 年上半年不少人在旧镜像上运行此项目遇到的第一个障碍。

5.2 决策树递归深度过大导致 Python 栈溢出

现象:decision_tree.py 在部分数据子集上执行时抛出RecursionError: maximum recursion depth exceeded。 原因:递归建树没有限制最大深度,当特征全是连续值且样本互不重复时,树会一直分裂到每个叶节点只剩一条样本,树深度接近样本数。Python 默认递归上限 1000,而西瓜数据集加盐后记录数超过几百就会出现。 解决:在递归函数入口加depth参数,超过 10 就停止分裂,并把当前节点设为叶节点,类别取投票多数。这是从 sklearn 的max_depth抄来的思路。

5.3 神经网络训练损失卡住不下降

现象:nerual_network.py 训练 200 轮后 loss 停在 0.69 左右不动,预测全部输出同一类别。 原因:0.69 恰好是 ln(2),也就是「对半开」分布的交叉熵——模型完全没有学到任何模式。大概率是学习率过大或过小,或者 tanh 激活导致梯度消失。 解决:把lr从 0.1 往 0.001 方向调小三个数量级观察;如果还不动,把隐藏层激活换成 ReLU。代码里用的 tanh 在初始化不当时确实容易饱和,换成np.maximum(Z1, 0)后同样的数据通常 50 轮内下降明显。

5.4 训练集合标签顺序打乱后 AdaBoost 效果恶化

现象:shuffle 训练数据后 adaboost.py 的测试精度波动很大,有时比随机猜测还差。 原因:代码内弱分类器循环对np.unique(X[:, j])做了排序,但排序后特征值对应的索引顺序可能改变类别分布;另外一个隐蔽问题是样本权重 w 在迭代之间没有增量重置,导致 shuffle 后首轮误差率分布不同。 解决:在adaboost_fit开头加一行np.random.seed(42)保证可复现;如果仍波动,把决策桩的搜索从「遍历所有阈值」改成「分位数抽样」,这样阈值分布更稳定。这个坑我踩过两次后,养成了先固定随机种子再调参的习惯。

5.5 标准化数据后 KNN 距离全变成 NaN

现象:k_nearest_neighbors.py 传入standardize处理后的数据后,部分样本距离出现 NaN。 原因:standardize中sigma[sigma == 0] = 1.0只处理了训练集,测试集单独标准化时使用了测试集自己的均值和标准差,如果测试集只有一条样本,标准差为 0,除出来的值全是 NaN。 解决:不要在预测时重新调用standardize,把训练集的mu和sigma保存下来,测试集统一用保存值做变换。这是标准的工程做法,教学代码为了短没有这一步,务必自行补上。

5.6 SVM 在连续特征上结果几乎全是一类

现象:support_vector_machine.py 对 watermelon.csv 的连续属性(密度、含糖率)做预测时,几乎所有样本都分成好瓜。 原因:线性核太弱,加上原始代码内 C 值太小导致边界偏向稠密区域。连续特征在高维空间线性不可分时,线性 SVM 很容易学成偏向样本数更多的一类。 解决:把 C 从 1.0 增大到 10.0 观察改善;更彻底的方案是给原始特征加多项式特征,比如X = np.column_stack([X, X[:, 0] ** 2, X[:, 0] * X[:, 1]]),再赋给 SMO。改完后收敛轮数会增多,但分类边界明显合理。

6. 验证技巧:把全项目算法统一到一套评估框架下对比

当你把代码包中的八个算法逐一跑通后,下一步不是调参,而是建立一套统一的验证流程。每个算法文件的输入输出格式略有差异:KNN 返回标签数组,逻辑回归返回概率,AdaBoost 返回加权投票结果。如果不统一封装,你没法判断哪个算法在西瓜数据上真正表现更好。我的习惯是写一个轻量评估脚本,把所有模型包装成同一个接口:

def evaluate(model_predict, X_train, y_train, X_test, y_test): """统一评估接口:model_predict 是 (X_train, y_train, X) -> y_pred""" y_pred = model_predict(X_train, y_train, X_test) # 混淆矩阵四个值 tp = np.sum((y_pred == 1) & (y_test == 1)) tn = np.sum((y_pred == 0) & (y_test == 0)) fp = np.sum((y_pred == 1) & (y_test == 0)) fn = np.sum((y_pred == 0) & (y_test == 1)) acc = (tp + tn) / len(y_test) prec = tp / (tp + fp) if tp + fp > 0 else 0.0 rec = tp / (tp + fn) if tp + fn > 0 else 0.0 f1 = 2 * prec * rec / (prec + rec) if prec + rec > 0 else 0.0 return {"acc": acc, "precision": prec, "recall": rec, "f1": f1}

我用它跑完代码包里所有分类器后,结论是决策树在watermelon.csv上的 F1 最高,逻辑回归和朴素贝叶斯次之,SVM 因为线性核表现最差。这个结论本身不是重点,重点是这套评估脚本让我养成了一个习惯:拿到任何新的手写算法实现,先写一个统一评估函数,让算法在同一个 train/test 划分上直接对比,而不是各跑各的然后凭感觉说哪个好。

还有一个实用技巧值得你去试:对西瓜数据做留一交叉验证(Leave-One-Out),而不是固定 7:3 划分。数据量只有 17 条时,固定划分的方差极大,留一法能更稳定地反映算法真实水平。用 NumPy 实现留一非常直接:

def loo_evaluate(model_predict, X, y): n = len(y) preds = np.zeros(n) for i in range(n): mask = np.ones(n, dtype=bool) mask[i] = False preds[i] = model_predict(X[mask], y[mask], X[i].reshape(1, -1)) return np.mean(preds == y)

这段代码把每一个样本单独当测试集,其余 n-1 条做训练。它暴露的问题很真实:SVM 的 SMO 在 n-1=16 条数据上的收敛速度可以接受,但 AdaBoost 的 20 轮迭代在 17 次循环里被调用 17 遍,总耗时会明显上升。从那以后我每次把一个新的手写算法项目做得可复现,都会强制在评估脚本里加一段「算法复杂度基线」:记录每个 model_predict 的耗时,如果某个算法耗时比决策树多 50 倍,我会先想是代码实现问题还是算法本身复杂度上限。这份代码包最大的隐藏价值就在这里——它迫使你同时关注效果和时间,而不只是调包。希望这个统一的评估习惯也能帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 0:28:45

财产保险可持续性建模:蒙特卡洛仿真与Python实践

简介&#xff1a;2024年美国大学生数学建模竞赛ICM Problem E赛题的Python实现方案&#xff0c;聚焦财产保险可持续性建模&#xff0c;面向数学建模参赛者、Python数据分析入门或进阶学习者&#xff0c;也适用于毕业设计、课程设计、大作业和工程实训场景。资源包共43个文件&am…

作者头像 李华
网站建设 2026/10/13 0:26:53

Tkinter列表选择窗口实战:Listbox用法、滚动条与数据联动

前阵子给一个内部小工具做配置界面的时候&#xff0c;遇到一个看起来简单却来回改了好几轮的需求&#xff1a;用户需要从十几个预设项目类型里选一个&#xff0c;再进入下一步操作。最开始我打算排一排按钮&#xff0c;后来发现这个方案根本撑不住——选项一旦超过两位数&#…

作者头像 李华
网站建设 2026/10/13 0:21:56

Legion Y7000P-IRX9 Ubuntu 20.04 双显卡驱动配置指南

简介&#xff1a;本资源是一份针对联想Legion-Y7000P-IRX9笔记本在Ubuntu 20.04系统&#xff08;内核5.15.0-97-generic&#xff09;下典型硬件兼容性问题的实战排错指南&#xff0c;面向Linux桌面用户、双系统安装者及硬件驱动调试初学者。聚焦三大高频痛点&#xff1a;开机黑…

作者头像 李华
网站建设 2026/10/13 0:21:15

HED边缘检测模型解析:多尺度特征融合与端到端实现

简介&#xff1a;面向计算机视觉开发者的HED边缘检测模型Python源码案例&#xff0c;基于全卷积网络实现端到端层次边缘检测&#xff0c;适合初学深度学习或需要快速完成边界提取实验的开发者。压缩包仅两KB&#xff0c;包含一个Python脚本与一份说明文档&#xff1b;脚本实现多…

作者头像 李华
网站建设 2026/10/13 0:18:39

CATIA V5R18 CAA开发:从解压包到可调试C++工程的完整实践

简介&#xff1a;本资源是面向CATIA二次开发工程师与高级应用人员的CAA&#xff08;Customer Adaptation Area&#xff09;定制化开发套件&#xff0c;专为CATIA V5R18版本构建&#xff0c;解决企业级用户对功能扩展、流程自动化及UI定制的核心需求。压缩包共826个文件&#xf…

作者头像 李华
网站建设 2026/10/13 0:18:20

LSTM财务因子选股模型:数据对齐与回测防泄漏实战

简介&#xff1a;毕业设计专用的LSTM财务因子预测选股模型源码包&#xff0c;基于Python实现&#xff0c;适合计算机、AI、通信、自动化、电子信息等专业学生完成毕业设计、课程设计或课内作业&#xff0c;也可作为量化金融与深度学习入门者的进阶练习。包内共14个文件&#xf…

作者头像 李华