简介这份资源面向希望夯实机器学习底层原理的开发者与学习者用纯NumPy从零实现常见算法帮助摆脱对现成框架的依赖真正理解矩阵运算与优化过程。包内共11个文件以8个Python脚本为核心覆盖线性回归、逻辑回归、决策树、支持向量机、K近邻、朴素贝叶斯、AdaBoost与神经网络等模型实现另附2个CSV数据集与1张决策树示意图压缩包约364KB轻量却结构完整。已有168人学习下载。读者可借助西瓜数据集与鱼类数据集动手复现标准化、梯度下降、核函数、K均值聚类、交叉验证及准确率、精确率、召回率、F1分数等评估指标在编码中打通线性代数与模型训练的关联适合作为课程实验、算法入门练习或面试前的原理复习材料。1. 从一份西瓜数据集说起为什么值得用 NumPy 手撸一遍机器学习算法很多人第一次接触机器学习是从sklearn的fit和predict开始的两行代码跑出 0.95 的准确率然后就没有然后了。真到面试被问「逻辑回归的梯度是怎么推的」「决策树的信息增益怎么算」脑子里只剩一个黑匣子。这份基于numpy实现常见机器学习算法.zip就是冲着这个痛点来的——它把线性回归、逻辑回归、KNN、朴素贝叶斯、决策树、AdaBoost、SVM、神经网络全部用 NumPy 从零实现了一遍配套的datasets里还塞了周志华《机器学习》里那个经典的西瓜数据集watermelon.csv和isFish.csv。它解决的不是「我要上线一个模型」而是「我要真正看懂模型内部在算什么」。适合两类人一类是刚学完《机器学习》课程、想找一份能跑通的参考实现对照公式的在校生另一类是工作几年、调包调腻了、想回头补底层原理的工程师。整个包是纯 Python NumPy没有 PyTorch、没有 TensorFlow连sklearn都只在个别地方做对比装好 NumPy 就能跑这一点对新手极其友好。2. 环境与数据准备把 NumPy 装对把西瓜数据读进来2.1 NumPy 安装与版本匹配的坑这个项目对 NumPy 的依赖很纯粹但恰恰是「纯 NumPy」的项目最容易在环境上翻车。常见报错ModuleNotFoundError: No module named numpy基本就是没装或者装到了错误的解释器里。我一般会先确认当前 Python 环境再装# 先看当前用的是哪个 python避免装到系统 python 而不是虚拟环境 which python python --version # 推荐用虚拟环境隔离避免污染全局 python -m venv ml_numpy_env source ml_numpy_env/bin/activate # Windows 用 ml_numpy_env\Scripts\activate # 安装 numpy指定一个稳定版本区间 pip install numpy1.21,2.0这里有个血泪经验NumPy 2.0 之后改了不少底层 ABI一些老代码里用到的np.float_、np.int_之类的别名被移除如果你装的是 2.x跑老项目很容易报AttributeError。所以上面我特意把版本卡在 1.21 到 2.0 之间。装完验证一下import numpy as np print(np.__version__) # 顺手验证矩阵乘法这是后面所有算法的地基 a np.array([[1, 2], [3, 4]]) b np.array([[5, 6], [7, 8]]) print(a b)运算符就是矩阵乘法等价于np.dot(a, b)。后面线性回归的正规方程、神经网络的层间传播全靠它。如果你在pip install时卡在installing backend dependencies多半是网络源的问题换成国内镜像源重试即可这属于环境问题不是代码问题。2.2 西瓜数据集的结构与读取datasets/watermelon.csv是这份资源里最有价值的部分之一它对应《机器学习》书里的西瓜分类问题。读进来看看长什么样import numpy as np # 假设 csv 第一行是表头用 genfromtxt 读数值列 # 实际字段名以文件为准这里演示通用读法 data np.genfromtxt(datasets/watermelon.csv, delimiter,, dtypeNone, encodingutf-8, namesTrue) print(data.dtype.names) # 查看所有字段名 print(data.shape) # 样本数量genfromtxt的namesTrue会把首行当字段名dtypeNone让 NumPy 自动推断每列类型字符串列会变成U类型。这里要注意西瓜数据集里既有连续特征密度、含糖率又有离散特征色泽、根蒂、敲声直接喂给纯数值算法会出问题。常见做法是先把离散特征做独热编码或者映射成整数再转成纯数值矩阵# 把结构化数组转成纯数值矩阵方便后续矩阵运算 # 假设我们只取连续特征做演示 density data[密度].astype(float) sugar data[含糖率].astype(float) X np.column_stack([density, sugar]) # 拼成 (n, 2) 的特征矩阵 print(X.shape)np.column_stack把两个一维数组按列拼成二维矩阵这是构造特征矩阵最常用的手法。参数上没什么玄学就是保证每个样本一行、每个特征一列。如果你的数据里有缺失值NumPy 本身不处理得先用 Pandas 清洗或者手动用np.nan标记再填充这一点摘要里也提到了别指望 NumPy 帮你兜底。3. 从线性回归到逻辑回归梯度下降的两种写法与收敛判断3.1 线性回归正规方程 vs 梯度下降linear_regression.py里通常会给两种解法。第一种是正规方程直接一步到位import numpy as np def linear_regression_normal_eq(X, y): # 给 X 加一列全 1作为偏置项 b 的系数 X_b np.hstack([np.ones((X.shape[0], 1)), X]) # 正规方程: theta (X^T X)^(-1) X^T y theta np.linalg.inv(X_b.T X_b) X_b.T y return thetanp.hstack横向拼接给每个样本补一个常数 1这样偏置项就能和权重一起用矩阵乘法表示。np.linalg.inv求逆做矩阵乘法。这条公式的推导来自最小二乘把损失函数对参数求导令其为零。它的优点是快、不用调学习率缺点是当特征维度很高或者X^T X接近奇异时求逆会数值不稳定甚至直接报LinAlgError: Singular matrix。第二种是梯度下降这也是后面逻辑回归、神经网络共用的优化框架def linear_regression_gd(X, y, lr0.01, epochs1000): m, n X.shape X_b np.hstack([np.ones((m, 1)), X]) theta np.zeros(n 1) loss_history [] for i in range(epochs): y_pred X_b theta error y_pred - y # 均方误差对 theta 的梯度 grad (2 / m) * (X_b.T error) theta - lr * grad loss np.mean(error ** 2) loss_history.append(loss) return theta, loss_history关键参数就两个lr学习率和epochs迭代次数。学习率太大损失会震荡甚至发散成nan太小收敛慢到你以为代码卡死了。我一般会先把loss_history画出来看曲线如果前几十轮损失不降反升直接调小学习率。loss_history这个设计很实用它是你判断「到底有没有在学」的唯一依据别省。3.2 逻辑回归sigmoid 与交叉熵梯度logistic_regression.py的核心是把线性输出套一个 sigmoid 变成概率def sigmoid(z): # 防止 exp 溢出对 z 做截断 z np.clip(z, -500, 500) return 1 / (1 np.exp(-z)) def logistic_regression_gd(X, y, lr0.1, epochs2000): m, n X.shape X_b np.hstack([np.ones((m, 1)), X]) theta np.zeros(n 1) for i in range(epochs): z X_b theta y_prob sigmoid(z) # 交叉熵损失对 theta 的梯度形式上和线性回归一样简洁 grad (1 / m) * (X_b.T (y_prob - y)) theta - lr * grad return thetasigmoid里那句np.clip是防溢出的后悔药。当z是很大的负数时np.exp(-z)会溢出成inf整个训练直接崩。逻辑回归的梯度形式和线性回归长得几乎一样区别只在于y_prob是 sigmoid 之后的概率而不是原始预测值这是交叉熵损失配合 sigmoid 导数化简后的漂亮结果。分类阈值默认取 0.5预测时(y_prob 0.5).astype(int)即可。3.3 收敛判断与学习率调试纯 NumPy 实现没有sklearn的tol自动停止你得自己加。常见做法是记录相邻两轮损失差if i 0 and abs(loss_history[-1] - loss_history[-2]) 1e-6: print(f第 {i} 轮提前收敛) break这个1e-6是经验阈值太小会白跑很多轮太大可能没收敛就停了。调试学习率时我习惯按 10 倍量级试0.001、0.01、0.1、1.0看哪个损失曲线下降最顺滑。这一步没有理论最优纯靠试属于典型的玄学环节但试几次就有手感了。4. KNN、朴素贝叶斯与决策树三个「非梯度」算法的实现细节4.1 KNN向量化距离计算k_nearest_neighbors.py的朴素写法是双重循环算距离样本一多就慢得离谱。向量化版本利用广播机制def knn_predict(X_train, y_train, X_test, k3): # 利用广播一次性算出所有测试样本到所有训练样本的距离 # X_test: (m, n), X_train: (p, n) - diff: (m, p, n) diff X_test[:, np.newaxis, :] - X_train[np.newaxis, :, :] dist np.sqrt(np.sum(diff ** 2, axis2)) # (m, p) # 取每个测试样本最近的 k 个训练样本索引 idx np.argsort(dist, axis1)[:, :k] # 多数投票 preds [] for neighbors in idx: labels y_train[neighbors] preds.append(np.bincount(labels).argmax()) return np.array(preds)X_test[:, np.newaxis, :]把形状从(m, n)变成(m, 1, n)X_train[np.newaxis, :, :]变成(1, p, n)两者相减触发广播得到(m, p, n)的三维数组。这就是热搜里常问的「numpy 三维数组相乘」的典型场景。axis2表示在特征维度上求和得到每个测试样本到每个训练样本的欧氏距离。np.argsort排序取前 k 个np.bincount做多数投票。参数k一般取奇数避免平票太小容易过拟合噪声太大又把类别边界抹平3 到 5 是常见起点。4.2 朴素贝叶斯拉普拉斯平滑不能省naive_bayes.py实现的是离散型朴素贝叶斯核心是条件概率表加贝叶斯公式。最容易翻车的地方是某个特征值在训练集里没出现过导致概率为 0连乘之后整个后验概率归零。解决办法是拉普拉斯平滑def train_naive_bayes(X, y, n_classes, n_features, alpha1.0): priors np.zeros(n_classes) cond_probs np.zeros((n_classes, n_features, X.max() 1)) for c in range(n_classes): X_c X[y c] priors[c] len(X_c) / len(X) for f in range(n_features): counts np.bincount(X_c[:, f], minlengthX.max() 1) # 拉普拉斯平滑分子加 alpha分母加 alpha * 取值数 cond_probs[c, f] (counts alpha) / (len(X_c) alpha * (X.max() 1)) return priors, cond_probsalpha1.0就是标准拉普拉斯平滑alpha小于 1 叫 Lidstone 平滑。这个参数不是可选项是必选项不加的话只要测试集出现训练集没见过的特征取值预测结果就完全不可信。预测阶段把先验和各个特征的条件概率连乘实际实现里通常取对数相加防下溢取最大后验对应的类别。4.3 决策树信息增益与递归分裂decision_tree.py是这份资源里逻辑最绕的一个。核心是选最优划分特征西瓜书里用的是信息增益def calc_entropy(y): # 计算标签的信息熵 _, counts np.unique(y, return_countsTrue) probs counts / len(y) return -np.sum(probs * np.log2(probs)) def info_gain(X, y, feature_idx): # 按某特征划分后的信息增益 base_entropy calc_entropy(y) values, counts np.unique(X[:, feature_idx], return_countsTrue) cond_entropy 0.0 for v, cnt in zip(values, counts): subset_y y[X[:, feature_idx] v] cond_entropy (cnt / len(y)) * calc_entropy(subset_y) return base_entropy - cond_entropynp.unique的return_countsTrue一次性拿到取值和频次比手写字典统计干净。信息增益就是划分前的熵减去划分后的条件熵增益越大说明这个特征越能把类别分开。递归建树时对每个特征算一遍增益选最大的那个分裂直到子集全属同一类或者特征用完。这里有个坑连续特征不能直接按取值划分得先离散化或者找最优分割点二分西瓜数据集里的密度、含糖率就属于这种处理起来比离散特征麻烦一截。5. 避坑与排查纯 NumPy 实现里最容易翻车的五件事5.1 损失变成 nan现象训练几轮后loss_history里出现nan参数全废。原因学习率过大导致梯度爆炸或者 sigmoid、log 里出现exp溢出、log(0)。解决先把学习率降一个量级重试在 sigmoid 里加np.clip算交叉熵时给概率加一个极小值1e-12再取对数避免log(0)。5.2 矩阵维度对不上现象ValueError: operands could not be broadcast together。原因特征矩阵和参数向量的维度没对齐常见于忘了给X补偏置列或者theta初始化长度写错。解决在每次矩阵乘法前打印X.shape和theta.shape确认内维相等。养成X_b np.hstack([np.ones((m,1)), X])之后立刻assert X_b.shape[1] len(theta)的习惯。5.3 准确率一直卡在 0.5 附近现象二分类任务训练很久准确率就是上不去。原因要么标签没转成 0/1 而是 1/2要么特征没做标准化导致梯度下降被大尺度特征主导。解决先np.unique(y)看标签取值确认是 0/1再对特征做 Z-score 标准化(X - X.mean(axis0)) / X.std(axis0)。KNN 和 SVM 对特征尺度尤其敏感不标准化基本没法用。5.4 决策树递归爆栈现象RecursionError: maximum recursion depth exceeded。原因特征取值太多或者没有正确设置停止条件树无限往下长。解决加最大深度限制或者在子集样本数小于某个阈值比如 5时停止分裂。纯 NumPy 实现没有max_depth参数的话自己加一个传进去。5.5 训练集准确率 100% 测试集一塌糊涂现象模型在训练集上完美换测试集就崩。原因过拟合尤其是 KNN 的k1和决策树不剪枝。解决KNN 把k调大决策树加深度限制或后剪枝逻辑回归加 L2 正则在梯度里加lambda * theta。纯 NumPy 实现正则化就是梯度里多一项不难但很多人忘了加。6. 用交叉验证和评估指标验证你的手撸实现写到这一步模型能跑通了但你怎么知道它是对的sklearn有cross_val_score纯 NumPy 就得自己写 K 折。核心逻辑是把数据打乱后切成 K 份轮流留一份做验证def k_fold_indices(n_samples, k5, seed42): rng np.random.default_rng(seed) indices rng.permutation(n_samples) fold_sizes np.full(k, n_samples // k) fold_sizes[:n_samples % k] 1 folds [] start 0 for size in fold_sizes: folds.append(indices[start:start size]) start size return foldsnp.random.default_rng(seed)是 NumPy 新版推荐的随机数生成器比老的np.random.seed更可控固定seed保证每次划分一致方便复现。permutation打乱索引然后按折大小切分。拿到每折的训练/验证索引后循环调用你的模型函数把每折的评估指标存下来取平均。评估指标也自己算别依赖sklearndef accuracy(y_true, y_pred): return np.mean(y_true y_pred) def precision_recall_f1(y_true, y_pred): tp np.sum((y_true 1) (y_pred 1)) fp np.sum((y_true 0) (y_pred 1)) fn np.sum((y_true 1) (y_pred 0)) precision tp / (tp fp 1e-12) recall tp / (tp fn 1e-12) f1 2 * precision * recall / (precision recall 1e-12) return precision, recall, f1分母加1e-12是防除零这是我在实际项目里养成的肌肉记忆。验证自己的实现对不对最直接的办法是拿同一份数据跑一遍sklearn的对应算法对比准确率。如果差距在几个百分点以内说明你的实现基本正确如果差出十几个点八成是某个梯度符号写反了或者平滑没加。我自己的习惯是每写完一个算法先用西瓜数据集这种小数据跑通确认损失曲线正常下降再上稍大的数据。从那以后我每次手撸算法都强制走一遍「小数据验证 → 损失曲线检查 → 和 sklearn 对比」这三步能省掉大量调试时间。这份资源的价值不在于代码多优雅而在于它逼你把每个公式亲手敲一遍敲完你对机器学习的理解会上一个台阶。希望帮到你。本文还有配套的精品资源点击获取