
简介这份资源汇集了常用机器学习算法的简洁实现涵盖决策树、随机森林、梯度提升、XGBoost、主成分分析、支持向量机、线性回归、逻辑回归、K近邻、朴素贝叶斯与独立成分分析等面向希望从代码层面理解算法原理的数据科学初学者和开发者。压缩包共47个文件以Python源码为主附带编译后的pyc缓存、各算法模块的README说明文档及一个示例数据文件整体大小约58KB。目录按算法分模块组织每个算法均配有模型实现代码与可直接运行的示例脚本便于对照学习。工具模块中还提供了数据操作、损失函数、核函数等底层组件有助于理解算法间的公共部分和实现细节。资源目前已有185人学习适合作为算法入门、面试准备或课程作业中的快速参考无论是刚接触机器学习还是希望快速回顾常用算法都能从中获得直观的代码参考。1. 常用机器学习算法简洁实现这份代码包把“调包”变成了“懂原理”如果你和我一样简历上写着“熟悉决策树、随机森林、XGBoost”但面试官一问“XGBoost 相比 GBDT 到底改了什么”就心里发虚那这份资源就是给你准备的。它不是 sklearn 的封装调用而是把 10 个常用算法——决策树、随机森林、GBDT、XGBoost、PCA、SVM、线性回归、逻辑回归、朴素贝叶斯、KNN——全部用原生 Python NumPy 从零写了一遍。每个算法目录下都有模型实现代码、可运行的示例脚本和 README直接python xxx_example.py就能看到结果。适合三类人准备算法面试的、期末复习机器学习原理的、以及想脱离黑匣子自己动手复现一遍的。2. 决策树与随机森林从单棵树的 CART 分裂到 Bagging 集成的完整链路2.1 先看源码结构决策树目录里到底有什么打开decision_tree目录你会看到三个核心文件decision_tree_model.py是模型本体decision_tree_classifier_example.py和decision_tree_regressor_example.py分别是分类和回归的示例脚本。这种“模型 示例”的拆法非常良心——你可以先跑示例脚本热个身再钻进模型文件里看每一行实现。decision_tree_model.py里没有用任何第三方库树节点的分裂逻辑全靠数组运算完成。核心数据结构是一个字典类型的节点包含特征索引、阈值、左子树、右子树、当前节点的预测值这几个键。分类树用基尼系数Gini选分裂点回归树用均方误差MSE减少量来选。这和 sklearn 里criteriongini和criterionmse的底层逻辑是一一对应的。# decision_tree_model.py 中的核心分裂逻辑简化示意 def _best_split(self, X, y): best_gain 0 best_idx, best_thr None, None n_samples, n_features X.shape # 当前节点的基尼系数或 MSE作为分裂前的基准 if self.criterion gini: base_impurity self._gini(y) else: base_impurity self._mse(y) # 遍历每个特征的每个取值贪心找最优分裂点 for idx in range(n_features): thresholds np.unique(X[:, idx]) for thr in thresholds: left_mask X[:, idx] thr right_mask ~left_mask if np.sum(left_mask) 0 or np.sum(right_mask) 0: continue left_y, right_y y[left_mask], y[right_mask] # 加权杂质 左子树样本占比 * 左子树杂质 右子树样本占比 * 右子树杂质 weighted_impurity (len(left_y) / n_samples) * self._impurity(left_y) \ (len(right_y) / n_samples) * self._impurity(right_y) gain base_impurity - weighted_impurity if gain best_gain: best_gain, best_idx, best_thr gain, idx, thr return best_idx, best_thr这段代码的逻辑很直白先算分裂前的杂质再遍历所有特征的所有取值计算分裂后的加权杂质两者之差就是信息增益分类或方差减少量回归选最大的那个作为当前节点的分裂方案。注意thresholds np.unique(X[:, idx])这一步——它天然去重了但也会让连续特征的分裂点搜索变成纯暴力枚举特征多、数据量大时非常慢。跑示例脚本没问题跑到真实数据集上就要小心了。2.2 随机森林的 Bagging 与随机特征选择在代码里怎么体现random_forest目录里的random_forest_model.py很好地演示了“森林”的构建方式。和我见过的一些教学代码不同这份实现没有直接调用自己写的决策树类而是把树构建逻辑又写了一遍然后在两个地方注入了随机性。第一处是样本采样。每棵树训练前用np.random.choice对训练集做有放回抽样抽样数量和原始样本数一致——这就是 Bagging 的经典做法。第二处是特征选择。每个节点分裂时不是遍历全部特征而是先从总特征里随机抽一个子集代码里max_features默认取sqrt(n_features)再在这个子集里找最优分裂。这对应随机森林和袋装决策树的本质区别Bagging 只在样本上做随机随机森林在样本和特征两方面都做了随机。# random_forest_model.py 中构建单棵树的抽样逻辑 n_samples X.shape[0] for tree in range(self.n_estimators): # 有放回抽样得到本棵树的训练索引 sample_idx np.random.choice(n_samples, n_samples, replaceTrue) X_sample X[sample_idx] y_sample y[sample_idx] # 注意这里可以加一个样本权重的调整但当前版本没有实现 tree_model self._build_tree(X_sample, y_sample) self.trees.append(tree_model)这里的replaceTrue是关键。如果不做有放回抽样每棵树的训练集都一样森林就没有多样性集成的方差削减效果直接归零。另一个细节是参数n_estimators在示例脚本里设为了 10这个数偏小实际用的时候至少要 50~100 棵才有稳定效果。你可以改random_forest_example.py里的参数重新跑对比一下和默认结果的差异。2.3 示例脚本跑通的三个前置要求跑random_forest_example.py之前先确认 Python 版本。项目里多处保留了cpython-35.pyc的缓存文件说明作者开发环境是 Python 3.5 左右代码里有些语法比如print不带括号的兼容写法在 Python 3.8 也能跑但保险起见建议直接用 Python 3.8 或 3.9 建个干净环境跑。依赖只有 NumPy。如果你本机没装直接pip install numpy就行。数据是代码里内置的不需要额外下载。示例脚本最后会把预测准确率打印出来还会画一张简单的分类结果图如果没报错的话。如果报错大概率是matplotlib没装这个包并不在项目依赖里需要手动补上。3. GBDT 与 XGBoost梯度提升家族的实现差异与调参边界3.1 GBDT 的“负梯度 残差”在代码里是怎么落地的gradient_boosting_decision_tree目录里有gbdt_model.py、gbdt_classifier_example.py和gbd_regressor_example.py三个文件。看完实现你会发现GBDT 的核心就是“每一棵新树去拟合前面所有树的负梯度”。回归问题里负梯度就是残差真实值减预测值分类问题里负梯度经过 sigmoid 转换后变成概率残差。gbdt_model.py里维护了一个self.trees列表每轮迭代都新建一棵决策树回归器注意这里内部用的还是 CART 回归树所以它依赖decision_tree_model.py用当前模型的负梯度作为树的训练目标。# gbdt_model.py 中每轮迭代的核心逻辑简化 for _ in range(self.n_estimators): # 当前模型的预测值累计所有树的结果 current_pred self._predict_raw(X) # 回归任务负梯度 真实值 - 当前预测值 if self.task regression: residual y - current_pred else: # 分类任务先算 sigmoid 概率再算概率残差 prob self._sigmoid(current_pred) residual y - prob # 用残差训练一棵新的回归树 tree DecisionTreeRegressor(max_depthself.max_depth) tree.fit(X, residual) self.trees.append(tree) # 每棵树只贡献 learning_rate 比例的修正 self.lr * self.learning_rate注意最后一行self.lr * self.learning_rate。这是一个很容易被忽略的实现细节——每一轮迭代后学习率都会再乘以初始学习率作用是让后面的树贡献越来越小。这种衰减策略在经典 GBDT 论文里并没有强制要求属于工程上的常见做法目的是用更多树逼近目标时避免过拟合。3.2 XGBoost 的“二阶近似 正则项”在源码里怎么体现xgboost目录下的xgboost_model.py是目前网上能找到的教学实现里比较规矩的一版。和 GBDT 只对损失函数做一阶泰勒展开不同XGBoost 用二阶泰勒展开来近似损失函数同时在目标函数里显式加入了叶子节点数的惩罚项和叶子权重的 L2 正则项。# xgboost_model.py 中目标函数近似计算的示意 def _calc_gain(self, g_left, h_left, g_right, h_right, reg_lambda): # 分裂后的增益 左子树增益 右子树增益 - 分裂前增益 gain_left (g_left ** 2) / (h_left reg_lambda) gain_right (g_right ** 2) / (h_right reg_lambda) gain_parent ((g_left g_right) ** 2) / (h_left h_right reg_lambda) return gain_left gain_right - gain_parent这里的g是损失函数的一阶导gradienth是二阶导hessianreg_lambda是 L2 正则系数。窗口期你自己试试就知道reg_lambda设成 0 的话XGBoost 就退化成了和 GBDT 差不多的东西。源码里还有一个细节分裂点的搜索不是暴力遍历而是对特征值做分位点预排序这直接减少了搜索次数是 XGBoost 比 GBDT 快的一个重要原因。3.3 模型对比实验一份数据跑出性能差异xgboost_example.py里自带了一份二分类数据直接跑完会输出准确率。建议你顺手做一个对照实验在同一个脚本里调用gradient_boosting_decision_tree里的 GBDT 模型设置相同的n_estimators、max_depth、learning_rate对比两边的验证集准确率。我的经验是小数据上两者差距不大但 XGBoost 收敛更快——同样 50 棵树XGBoost 的验证集误差已经平稳GBDT 可能还在下降。原因就在二阶导数信息量更大每一步迈得更准。学习率这个超参数在两边的影响方式不太一样。GBDT 的学习率衰减写在代码里了调参时要留意初始学习率设 0.1跑 50 轮实际最后一轮的贡献权重已经衰减到0.1^50几乎可以忽略。XGBoost 的默认实现不做这种衰减更依赖正则项来控制复杂度所以调 XGBoost 时优先动reg_lambda和max_depth而不是盲目调低学习率。4. PCA 与 SVM降维、分类边界与核函数的选择逻辑4.1 PCA 的实现是特征分解还是 SVD两种路线差别在哪pca目录下只有pca.py和 README例子很克制。打开pca.py实现的路线是“中心化 协方差矩阵特征分解”不是 SVD奇异值分解。这两种路线在结果上等价但数值稳定性差了很远。协方差矩阵是d x dd 为特征维度当特征维度很高时协方差矩阵的求解会非常慢而且可能出现数值病态。SVD 直接对原始数据矩阵做分解不需要构造协方差矩阵对大维度数据更稳健。# pca.py 中的核心降维逻辑 def transform(self, X, k): # 1. 中心化每个特征减去均值 X_mean np.mean(X, axis0) X_centered X - X_mean # 2. 计算协方差矩阵 cov_matrix np.cov(X_centered.T) # 3. 特征值分解 eigenvalues, eigenvectors np.linalg.eig(cov_matrix) # 4. 按特征值降序排列取前 k 个特征向量 idx np.argsort(eigenvalues)[::-1][:k] projection_matrix eigenvectors[:, idx] return X_centered.dot(projection_matrix)这里有个典型的坑np.linalg.eig返回的特征向量列向量不是按特征值大小排列的必须手动排序取前 k 个源码里这个操作做了。如果你改代码改成 SVD 路线记得np.linalg.svd返回的Vt行向量才是特征向量方向不用再转置。4.2 SVM 的软间隔实现TempLinkoping2016.txt 数据是拿来干嘛的support_vector_machine目录里有一个TempLinkoping2016.txt文件这是瑞典林雪平市 2016 年的逐日温度记录被用作 SVM 回归SVR的示例数据——预测当天的平均气温。选这个数据集挺聪明的温度序列有明显的趋势和周期性线性核搞不定必须上 RBF 核这正好演示了核函数的必要性。svmModel.py实现的是软间隔 SVM目标函数里带松弛变量惩罚系数C。kernels.py里提供了线性核、多项式核和 RBF 核三种。最有意思的是这个实现没有用经典的 SMO 序列最小优化算法而是用梯度下降直接优化原始问题。# svmModel.py 中梯度下降更新权重的示意 def _optimize(self, X, y): n_samples, n_features X.shape w np.zeros(n_features) b 0 lr 0.001 for epoch in range(self.n_epochs): for i in range(n_samples): # 计算当前样本的决策值 decision np.dot(w, X[i]) b # 距离边界较远的样本梯度只来自正则项 if y[i] * decision 1: w_grad w b_grad 0 # 违反间隔约束的样本梯度同时来自正则项和合页损失 else: w_grad w - self.C * y[i] * X[i] b_grad -self.C * y[i] w - lr * w_grad b - lr * b_grad这版实现是给教学用的收敛速度比 SMO 慢如果你直接在svm_example.py上改数据集要做好训练耗时十倍以上增长的准备。路径上的TempLinkoping2016.txt是原始数据示例脚本会读它你不需要手动打开这个文件。4.3 核函数的选择对分类边界的影响kernels.py里三个核函数的写法非常简洁。线性核就是标准内积多项式核多了一个degree参数RBF 核的核心是gamma参数控制单个样本的影响半径。拿温度回归这个任务来说gamma太小所有样本的高斯影响范围叠在一起预测曲线被拉平gamma太大预测曲线跟着每一个训练样本剧烈震荡直接过拟合。实践中我会建议跑项目自带的示例先看一遍gamma默认值的分类效果然后把gamma调大 10 倍和调小 10 倍各跑一次观察边界变化。这个动作的成本很低但对理解 RBF 核的行为帮助明显。5. 线性回归到贝叶斯回归损失函数、条件与常见排查5.1 从线性回归的解析解到逻辑回归的梯度下降linear_regression目录下只有linear_regression.py和 README而logistic_regression目录下有模型和示例。线性回归走的是正规方程解析解逻辑回归走的是梯度下降两者的代码风格完全不同。# linear_regression.py 中正规方程求解的示意 def fit(self, X, y): # 给 X 加一列全 1 的特征作为偏置项 X_b np.c_[np.ones((X.shape[0], 1)), X] # 正规方程theta (X^T X)^-1 X^T y theta np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y) self.theta theta正规方程的优势是无需调学习率、不用迭代一步算出全局最优解。但它有个致命前置条件X^T X必须可逆。如果你的数据特征之间存在高度相关性比如同时放了摄氏温度和华氏温度或者特征数量大于样本数量np.linalg.inv就会直接报错或返回数值上极不稳定的结果。看到这种报错不要怀疑是代码写错了先去查特征是否冗余。逻辑回归的logistic_regression.py用的是批量梯度下降。损失函数是交叉熵每一轮迭代都遍历所有样本计算梯度。这里有一个工程上的关键细节如果你直接拿原始特征去跑特征尺度差异大的话比如一个特征范围 0~1另一个是 0~10000梯度下降会非常慢甚至不收敛。我拿到所有数据集的第一件事就是先做标准化。# 注意跑逻辑回归示例前先做特征标准化这是我踩坑后的习惯 from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)5.2 朴素贝叶斯的独立假设与概率连乘的下溢问题naive_bayes目录下只有naive_bayes.py和 README实现的是高斯朴素贝叶斯。它假设每个特征在给定类别下服从高斯分布且特征之间相互独立。这个“独立假设”在真实数据上几乎从不成立但对很多文本分类、垃圾邮件识别场景即便假设不成立它的效果也够用——这就是朴素贝叶斯最反直觉的地方。代码里有一个极容易翻车的点概率连乘。当特征维度很高时多个小概率连乘的结果会极度接近 0Python 浮点数直接下溢。教学中常见的补救方式是在概率连乘的循环里对每个概率取对数把连乘变成连加。这份代码没有做这个处理所以如果你拿高维数据直接跑大概率会看到预测结果全部指向同一个类别——不是算法不行是数值计算崩了。# naive_bayes.py 中需要手动修补的下溢问题现象所有样本都被分到同一类 # 把概率连乘改为对数概率连加 log_prob 0.0 for i in range(n_features): # 原来prob * self._gaussian_pdf(x[i], mean[i], var[i]) log_prob np.log(self._gaussian_pdf(x[i], mean[i], var[i]) 1e-9)5.3 KNN 的实用经验距离度量与特征尺度k_nearest_neighbors目录里有knnModel.py和knnExample.py。KNN 本身没有训练过程但它的“训练”其实发生在预测时——每次预测都要计算新样本与所有训练样本的距离。knnExample.py里默认使用欧氏距离k值设成 5。KNN 最大的坑不在代码里而在数据准备上。欧氏距离对特征尺度极其敏感。假设你有两个特征一个是年龄范围 20~60一个是年收入范围 5 万~50 万那么年龄的差异在距离计算中被完全淹没模型约等于只看年收入。这个项目没有自带 KNN 的数据预处理示例但使用前一定先手动加一步标准化否则你的 KNN 基本等于没调参。6. 避坑清单这份代码包的五个常见翻车点与排查方案6.1 Python 版本兼容问题现象运行示例脚本时出现SyntaxError或ModuleNotFoundError最常见的是print语法报错和__pycache__的版本不匹配。原因作者开发环境是 Python 3.5部分语法与高版本 Python 不兼容。解决用 Python 3.8 或 3.9 新建虚拟环境先手动跑一个最简单的脚本比如linear_regression.py确认环境没问题后再跑其他依赖较多文件的算法。不要试图在最新版 Python3.12上硬跑老代码大概率会碰上 NumPy API 变更导致的兼容错误。6.2 NumPy 版本导致np.float报错现象导入xgboost_model.py时提示module numpy has no attribute float。原因NumPy 1.24 及以上移除了np.float、np.int等旧别名而这份早期代码里有类似的旧写法。解决安装 NumPy 1.23.x 版本比如pip install numpy1.23.5。如果你不想降级 NumPy也可以手动把所有np.float替换成float、np.int替换成int但这个工程量大降级省事得多。6.3 多分类任务直接跑崩现象用决策树或随机森林跑一个三分类数据集模型训练时报错或预测结果全变成同一个类别。原因部分模型实现只为二分类设计比如svmModel.py的合页损失函数假设标签为{-1, 1}而你的数据标签是{0, 1, 2}。解决在使用 SVM、逻辑回归相关实现前先检查数据集的类别数。KNN、决策树、随机森林可以支持多分类但 SVM 和逻辑回归的实现需要先做 One-vs-Rest 包装或者干脆只用于二分类任务。6.4 特征不标准化导致梯度下降不收敛现象跑logistic_regression.py时迭代了几百轮损失函数不降反升或者准确率始终在 50% 附近徘徊。原因特征尺度差异太大梯度下降的更新步长对某些维度过大或过小导致震荡。解决所有跑梯度下降的模型逻辑回归、SVM训练前都做 StandardScaler 标准化。做完之后再跑一次你会发现损失函数下降曲线立刻变得顺滑。这个问题在此类手写实现里几乎必现不是特例。6.5 数据文件路径写死导致换机器跑不了现象换一台电脑或者换目录后svm_example.py报FileNotFoundError。原因脚本里用了相对路径TempLinkoping2016.txt而当前工作目录不是support_vector_machine。解决运行时先cd到对应算法目录或者用os.path.join(os.path.dirname(__file__), xxx.txt)方式读取文件。这是早期教学代码的通病不算 bug但足以让你浪费十几分钟。7. 进阶验证如何用自己的数据系统验证这批手写实现跑完示例脚本只是热身。真正让这份代码产生价值的是用它做“手写实现 vs sklearn 基线”的对比实验。我自己的做法是固定一个数据集比如加州房价或鸢尾花把 hand-written 模型和 sklearn 的同名模型放在同一个脚本里设置完全相同的超参数然后对比训练时间、预测精度和推理速度。差距大的地方就是你对算法理解不够深的地方。拿决策树举例。手写实现里没有对特征预排序也没做任何缓存优化在大数据集上训练时间可能是 sklearn 的 10 倍以上——这并不代表实现差而是说明 sklearn 里做了大量的工程优化。知道差距在哪个环节比单纯会用.fit()有价值得多。一个更实用的验证方案用utils目录里的data_operation.py和data_manipulation.py现有工具自己造一份带噪声的回归数据分别跑线性回归、决策树回归、随机森林回归和 GBDT 回归画出四条预测曲线。你会直观看到线性回归在非线性数据上彻底失效决策树能拟合但极度毛糙随机森林平滑了一个档次GBDT 甚至能逼近复杂曲线——整个过程不依赖任何“调包”全是你自己控制代码的每一步。从那以后我每次上手机器学习项目都会强制走一遍这个流程选一份干净数据 → 手写实现跑通 → sklearn 跑同参数 → 对比训练时间和效果差异。这个习惯帮我筛掉了无数“看起来懂了其实没懂”的知识盲区。这份资源最大的价值不在于代码质量而在于它给了你一个可以肆意改动、随时打断、逐行打印中间结果的实验台——希望帮到你。本文还有配套的精品资源点击获取