☰
从零实现机器学习算法:决策树、XGBoost、SVM等源码拆解与实战
2026/10/13 7:41:47 网站建设 项目流程

简介:这份资源面向希望深入理解机器学习底层原理的学习者与开发者,用纯 Python 从零实现决策树、随机森林、梯度提升树、XGBoost、PCA、ICA、支持向量机、线性回归、逻辑回归、朴素贝叶斯与 KNN 等经典算法,帮助读者摆脱对现成库的黑盒依赖,通过阅读精简代码掌握模型训练与预测的完整流程。压缩包共 47 个文件,以 26 个 py 源码为主体,辅以 10 个 md 说明文档、10 个 pyc 缓存与 1 个 txt 数据文件,整体仅 58KB,轻量易读。每个算法模块均配有独立示例脚本与 README,并共享 utils 下的损失函数、数据操作与核函数等通用组件,目录按算法分门别类,便于按需查阅与二次修改。目前已有 185 人学习下载,适合作为课程作业、面试复习或自研框架的参考底稿。

1. 从一份“手写版”机器学习算法包说起:为什么我建议你拆开看

如果你已经用惯了 scikit-learn 的fit/predict,第一次看到这份《常用机器学习的算法简洁实现》压缩包时,大概率会愣一下:决策树、随机森林、XGBoost、PCA、SVM、线性回归、逻辑回归、朴素贝叶斯、GBDT,每个算法一个独立文件夹,里面是*_model.py加*_example.py,还有一个被反复 import 的utils目录。它不是一个能直接 pip 安装的库,而是一套把常见机器学习算法用 NumPy 从零写出来的教学级源码集合。适合谁?适合那些调包调得心里发虚、想搞清楚“树是怎么分裂的”“SVM 的核函数到底在哪一步起作用”“PCA 的特征值分解落在哪一行”的人。它解决的不是生产部署问题,而是把算法从黑匣子拆成可读代码的问题。下面我按“这是什么 → 怎么跑起来 → 坑在哪 → 怎么改”的顺序,把这份包拆一遍。

2. 目录结构与运行环境:先让代码跑起来再谈原理

2.1 这份包到底包含哪些算法模块

从文件清单看,根目录下按算法分文件夹,每个文件夹基本遵循“模型文件 + 示例文件 + README”的三件套结构。utils是公共依赖,data_operation.py负责数据加载与划分,loss_functions.py放损失函数,kernels.py放核函数,misc.py放一些辅助工具。这种拆分方式很朴素,但好处是每个算法只依赖自己需要的那几个工具函数,读代码时不会被无关逻辑干扰。

模块目录核心文件用途
decision_treedecision_tree_model.py分类树与回归树实现
random_forestrandom_forest_model.py基于决策树的 Bagging 集成
gradient_boosting_decision_treegbdt_model.py梯度提升树
xgboostxgboost_model.py简化版 XGBoost
pcapca.py主成分分析
support_vector_machinesvmModel.py、kernels.pySVM 与核函数
linear_regressionlinear_regression.py最小二乘线性回归
logistic_regressionlogistic_regression.py逻辑回归
naive_bayesnaive_bayes.py朴素贝叶斯
k_nearest_neighborsknnModel.pyKNN

注意,utils下的kernels.py和support_vector_machine下的kernels.py不是同一个文件,前者是公共核函数,后者是 SVM 专用。读代码时如果发现 import 路径对不上,先确认这一点。

2.2 环境准备与依赖安装

这份代码的__pycache__里出现了cpython-35,说明作者当年用的是 Python 3.5。现在直接跑大概率会遇到语法或依赖问题,但核心逻辑不依赖新特性,用 Python 3.8 以上也能跑。我一般会先建一个干净虚拟环境,避免和现有包冲突。

python -m venv ml_scratch_env source ml_scratch_env/bin/activate # Windows 用 ml_scratch_env\Scripts\activate pip install numpy matplotlib scikit-learn

这里装 scikit-learn 不是为了调包,而是示例文件里常用它来加载数据集(比如 iris、digits)和做结果对比。numpy 是核心依赖,matplotlib 用于部分示例的可视化。装完后先别急着跑所有示例,从最简单的线性回归开始验证环境。

cd linear_regression python linear_regression.py

如果这一步报ModuleNotFoundError: No module named 'utils',说明当前工作目录不对。这份代码的 import 是按“从项目根目录运行”设计的,常见做法是在根目录下用python -m linear_regression.linear_regression的方式跑,或者手动把根目录加到sys.path。我一般会在根目录建一个run_example.py,统一处理路径问题。

import sys import os sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from linear_regression.linear_regression import LinearRegression from utils.data_operation import mean_squared_error import numpy as np # 构造一个简单线性数据验证 np.random.seed(42) X = np.random.rand(100, 1) y = 2 * X.squeeze() + 1 + np.random.normal(0, 0.1, 100) model = LinearRegression() model.fit(X, y) predictions = model.predict(X) print("MSE:", mean_squared_error(y, predictions))

这段代码做了三件事:把项目根目录加入模块搜索路径、导入线性回归模型和均方误差函数、用带噪声的线性数据验证拟合效果。参数上,np.random.seed(42)保证每次运行结果一致,方便对比不同算法的输出。如果 MSE 在 0.01 量级,说明环境和代码都正常。

3. 核心算法拆解:从决策树分裂到 XGBoost 的加法模型

3.1 决策树:分裂准则与递归建树

decision_tree_model.py里最核心的是分裂点的选择。分类树用信息增益或基尼指数,回归树用方差 reduction。代码里通常会把候选特征和候选阈值遍历一遍,选让不纯度下降最多的那个组合。这个过程是贪心的,不保证全局最优,但计算上可行。

# 摘自 decision_tree_model.py 的分裂逻辑(简化示意) def _best_split(self, X, y): best_gain = -1 best_feature, best_threshold = None, None for feature in range(X.shape[1]): thresholds = np.unique(X[:, feature]) for threshold in thresholds: left_mask = X[:, feature] <= threshold right_mask = ~left_mask if left_mask.sum() == 0 or right_mask.sum() == 0: continue gain = self._information_gain(y, y[left_mask], y[right_mask]) if gain > best_gain: best_gain = gain best_feature = feature best_threshold = threshold return best_feature, best_threshold

这段逻辑说明:对每个特征,取所有唯一值作为候选阈值,把数据切成左右两部分,计算信息增益。参数上,np.unique会排序并去重,所以阈值是离散的;如果特征是连续值且取值很多,这一步会比较慢。常见优化是先对特征排序,再在相邻值中点处切分,但这份教学代码为了可读性没做这层优化。跑decision_tree_classifier_example.py时如果发现训练时间过长,先检查数据集大小和特征唯一值数量。

3.2 随机森林与 GBDT:两种集成思路的代码差异

随机森林在random_forest_model.py里体现为“对样本有放回采样 + 对特征随机子集建树 + 投票/平均”。GBDT 在gbdt_model.py里则是“串行建树,每棵树拟合上一轮的残差”。这两个文件夹的 README 都写了基本用法,但代码风格差异明显:随机森林的并行性体现在树与树之间独立,GBDT 的串行性决定了它没法简单并行。

# 随机森林的核心:多棵树独立训练后聚合 class RandomForest: def fit(self, X, y): self.trees = [] for _ in range(self.n_estimators): tree = DecisionTree(max_depth=self.max_depth) X_sample, y_sample = bootstrap_sample(X, y) feature_subset = np.random.choice(X.shape[1], self.max_features, replace=False) tree.fit(X_sample[:, feature_subset], y_sample) self.trees.append((tree, feature_subset)) def predict(self, X): predictions = [tree.predict(X[:, features]) for tree, features in self.trees] return np.mean(predictions, axis=0) # 回归取平均,分类取众数

参数上,n_estimators控制树的数量,max_features控制每棵树看到的特征子集大小。常见做法是分类任务取sqrt(n_features),回归任务取n_features/3。这份代码把这两个参数暴露在示例文件里,改起来直观。GBDT 那边则要注意学习率learning_rate和树的数量要配合调,学习率小就需要更多树,训练时间线性增长。

3.3 XGBoost 的简化实现:二阶泰勒与正则项

xgboost_model.py是这份包里相对复杂的部分。它和 GBDT 的关键区别在于:GBDT 用一阶导数(残差),XGBoost 用二阶泰勒展开近似损失函数,并在目标函数里加了正则项控制模型复杂度。代码里通常能看到gradient和hessian两个数组的计算。

# xgboost_model.py 中计算一阶和二阶导数的片段 def _compute_gradients(self, y_true, y_pred): # 以均方误差为例:grad = y_pred - y_true, hess = 1 gradient = y_pred - y_true hessian = np.ones_like(y_pred) return gradient, hessian # 分裂增益计算时同时用到 gradient 和 hessian def _split_gain(self, G_left, H_left, G_right, H_right, gamma): return 0.5 * (G_left**2 / (H_left + self.reg_lambda) + G_right**2 / (H_right + self.reg_lambda) - (G_left + G_right)**2 / (H_left + H_right + self.reg_lambda)) - gamma

这里的reg_lambda是 L2 正则项系数,gamma是分裂所需的最小增益阈值。参数调大reg_lambda会让模型更保守,调大gamma会减少分裂次数。示例文件xgboost_example.py里通常会用 sklearn 的load_boston或load_iris做对比,但注意新版 sklearn 已经移除了load_boston,需要换成fetch_california_housing或手动构造数据。

3.4 SVM 与核函数:从线性到非线性的分界点

support_vector_machine目录下的kernels.py定义了线性核、多项式核、RBF 核。SVM 的训练过程在svmModel.py里,核心是序列最小优化(SMO)或简化的梯度下降。这份代码大概率用的是简化版 SMO,因为完整 SMO 实现起来代码量不小。

# kernels.py 中的 RBF 核 def rbf_kernel(x1, x2, gamma=1.0): distance = np.sum((x1 - x2) ** 2) return np.exp(-gamma * distance) # svmModel.py 中核矩阵的构建 def _compute_kernel_matrix(self, X): n_samples = X.shape[0] K = np.zeros((n_samples, n_samples)) for i in range(n_samples): for j in range(n_samples): K[i, j] = self.kernel(X[i], X[j]) return K

参数上,gamma控制 RBF 核的“宽度”,越大越容易过拟合,越小越接近线性。C是惩罚系数,越大对误分类容忍度越低。示例文件svm_example.py里用TempLinkoping2016.txt做数据,这个文件是瑞典林雪平某年的温度数据,适合做回归或二分类。如果跑出来准确率很低,先检查gamma和C的量级,常见做法是在[0.001, 0.01, 0.1, 1, 10]里网格搜索。

4. 避坑与排查:跑这份代码时最容易翻车的五个地方

4.1 现象:import 报错No module named 'utils'

原因:这份代码的 import 路径是按项目根目录为工作目录设计的,直接在子文件夹里运行python xxx_example.py会导致utils找不到。解决:在根目录下运行,或者用python -m 子目录.示例文件的方式。我一般会在根目录建一个run_all.py,统一用sys.path处理。

4.2 现象:load_boston报错或返回 404

原因:scikit-learn 1.2 之后移除了load_boston,而这份代码写于更早时期。解决:把示例文件里的load_boston替换成fetch_california_housing,或者手动用np.loadtxt读本地数据。如果只是验证算法逻辑,用make_regression生成模拟数据更快。

4.3 现象:决策树训练极慢,卡在分裂点遍历

原因:_best_split对每个特征的所有唯一值做遍历,如果特征是连续值且样本量大,复杂度是O(n_features * n_unique * n_samples)。解决:对连续特征先排序,只在相邻值中点处尝试分裂;或者限制max_depth和min_samples_split。教学代码为了可读性通常不做这层优化,实际用的时候要心里有数。

4.4 现象:XGBoost 示例的损失不下降或震荡

原因:学习率learning_rate设得太大,或者reg_lambda太小导致数值不稳定。解决:先把学习率降到 0.1 以下,增大reg_lambda到 1.0 左右,观察损失曲线。如果还是震荡,检查hessian是否出现了接近零的值,加一个极小量1e-6防止除零。

4.5 现象:SVM 在TempLinkoping2016.txt上准确率异常低

原因:这个数据集的标签可能不是二分类的 0/1,而是连续温度值。直接拿来做分类会出问题。解决:先确认数据格式,如果是回归任务,把 SVM 换成 SVR 或对温度做分箱。另外,特征没做归一化时 RBF 核表现会很差,常见做法是先做StandardScaler。

5. 进阶用法:把这份包当成算法实验台

5.1 用统一接口封装各算法,做横向对比

这份包的每个算法都有自己的fit和predict,但参数命名不统一。我一般会写一个适配层,把它们的接口对齐到 sklearn 风格,然后在一个脚本里跑同一份数据,对比准确率、训练时间和内存占用。

# 统一适配层示例 class SklearnLikeWrapper: def __init__(self, model_class, **kwargs): self.model = model_class(**kwargs) def fit(self, X, y): self.model.fit(X, y) return self def predict(self, X): return self.model.predict(X) # 横向对比 from decision_tree.decision_tree_model import DecisionTreeClassifier from random_forest.random_forest_model import RandomForest from xgboost.xgboost_model import XGBoost models = { "DecisionTree": SklearnLikeWrapper(DecisionTreeClassifier, max_depth=5), "RandomForest": SklearnLikeWrapper(RandomForest, n_estimators=10), "XGBoost": SklearnLikeWrapper(XGBoost, n_estimators=10, learning_rate=0.1), } for name, model in models.items(): model.fit(X_train, y_train) acc = np.mean(model.predict(X_test) == y_test) print(f"{name}: {acc:.4f}")

这段代码的价值在于:同一份数据、同一个评估指标,能直观看出不同算法的表现差异。参数上,max_depth、n_estimators、learning_rate先给保守值,跑通后再调。

5.2 用 PCA 做可视化,验证降维效果

pca.py里的实现通常是基于特征值分解或 SVD。拿 iris 数据降到二维,画散点图,能直观看到三类是否可分。

from pca.pca import PCA from sklearn.datasets import load_iris import matplotlib.pyplot as plt iris = load_iris() X, y = iris.data, iris.target pca = PCA(n_components=2) X_reduced = pca.fit_transform(X) plt.scatter(X_reduced[:, 0], X_reduced[:, 1], c=y, cmap='viridis') plt.xlabel('PC1') plt.ylabel('PC2') plt.title('PCA on Iris') plt.show()

如果三类在二维平面上分得开,说明 PCA 保留了主要判别信息。如果混在一起,可能是n_components太小,或者数据本身线性不可分。常见做法是看解释方差比,累计到 95% 再决定保留几个主成分。

5.3 一个具体技巧:用__pycache__判断代码年代

这份包的__pycache__里是cpython-35,说明作者当年用 Python 3.5 编译过。如果你在 Python 3.8 以上环境跑,这些.pyc文件不会被加载,但也不会干扰。我一般会直接删掉所有__pycache__,避免混淆。另外,代码说明.py这个文件值得先读一遍,它通常写了作者对各算法的理解和参数建议,比 README 更接地气。

从那以后我每次拿到这种“从零实现”的算法包,都会先跑通一个最简单的示例,确认环境没问题,再逐个模块读代码、改参数、看输出变化。这份包的价值不在于性能,而在于让你能对着代码问出“这一行到底在算什么”。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询