恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
纯NumPy机器学习算法骨架:可调试、可插拔、可回溯
首页
资讯中心
/
纯NumPy机器学习算法骨架:可调试、可插拔、可回溯
纯NumPy机器学习算法骨架:可调试、可插拔、可回溯
发布时间:2026/10/12 21:40:22
简介这是一份面向Python初学者与机器学习实践者的算法实现源码合集聚焦逻辑回归、SVM、KMeans、岭回归、BP神经网络、DBSCAN、矩阵分解等16类主流算法的完整可运行代码兼顾原理理解与工程实践。资源共54个文件含31个核心Python源码如lr_train.py、svm_train.py、dbscan.py、19个文本类数据与说明文件含heart_scale、svm_test_data等典型数据集以及test_data、data等结构化测试资源整体压缩包仅323KB轻量易解压、即开即用。已有361人下载学习适合课程实验、算法复现与面试准备。项目按算法分章组织Chapter_1至Chapter_16目录清晰、命名规范每个算法均配套训练/测试脚本及对应数据辅以readme.txt等说明文档大幅降低上手门槛帮助读者从代码细节中掌握模型构建、参数调优与评估全流程。1. 这不是“抄个sklearn就能跑通”的玩具项目它是一套可调试、可插拔、可回溯的机器学习算法实现骨架你手头正缺一个能真正看懂梯度怎么算、损失怎么传、权重怎么更新的机器学习源码框架——不是调fit()就完事的黑匣子而是每一步都能打断点、改公式、换初始化、替激活函数、加自定义正则项的底层实现。这个标题里的“基于Python的机器学习算法设计源码”指的不是Jupyter里跑通iris分类的5行代码而是一套覆盖监督学习核心范式线性模型、树模型、神经网络前向/反向传播、优化器调度的模块化Python实现它用原生NumPy构建不依赖PyTorch/TensorFlow自动微分所有梯度都手动推导、显式编码所有数据流都可打印、可替换、可注入噪声。适合三类人想吃透西瓜书公式推导却卡在代码落地的新手需要在嵌入式/边缘设备部署轻量模型、必须控制内存与计算路径的工程师或是教学场景中需向学生展示“算法如何从数学符号变成可执行逻辑”的讲师。它解决的不是“能不能跑”而是“为什么这么跑”“哪里可以动”“动了之后哪一步崩了”。下面我们就从最基础的线性回归开始一层层把这套骨架搭出来。2. 用纯NumPy从零实现线性回归不只是公式搬运而是构建可调试的数据流管道2.1 为什么不用scikit-learn——理解“可调试性”对算法设计的硬约束很多初学者一上来就用sklearn.linear_model.LinearRegression训练快、接口干净但一旦模型效果差你根本不知道是数据预处理出错、特征缩放没做、还是损失函数本身对异常值过于敏感。而本方案强制你写predict(X)明确矩阵乘法维度X w b而非黑盒输出loss(y_true, y_pred)显式写出MSE公式np.mean((y_true - y_pred) ** 2)并留出接口支持MAE、Huber等替换gradient(X, y_true, y_pred)手动计算dL/dw 2/n * X.T (y_pred - y_true)这步直接暴露数值稳定性问题比如X未中心化时X.T X条件数爆炸。这种写法牺牲了开发速度换来的是每一行代码都对应一个可验证的数学操作。当你在gradient()里插入print(fgrad_w norm: {np.linalg.norm(grad_w)})就能实时监控梯度爆炸/消失当你把X换成X np.random.normal(0, 1e-8, X.shape)就能测试数值鲁棒性。这才是“算法设计”的起点——不是调参而是掌控数据在模型内部的每一步变形。2.2 构建最小可运行骨架LinearModel基类与LinearRegression具体实现我们定义一个基类强制所有算法实现fit()、predict()、score()三个方法并预留_init_params()和_update_params()钩子import numpy as np class LinearModel: def __init__(self): self.w None self.b None def _init_params(self, n_features): # 使用He初始化变体适配线性模型的简单性 self.w np.random.normal(0, 0.01, n_features) self.b 0.0 def predict(self, X): if self.w is None: raise ValueError(Model not fitted yet.) return X self.w self.b def score(self, X, y): y_pred self.predict(X) ss_res np.sum((y - y_pred) ** 2) ss_tot np.sum((y - np.mean(y)) ** 2) return 1 - ss_res / ss_tot if ss_tot ! 0 else 0 class LinearRegression(LinearModel): def __init__(self, learning_rate0.01, max_iter1000, tol1e-6): super().__init__() self.learning_rate learning_rate self.max_iter max_iter self.tol tol self.loss_history [] def fit(self, X, y): # 输入校验确保X是二维y是一维 if X.ndim ! 2: raise ValueError(X must be 2D array) if y.ndim ! 1 or len(y) ! X.shape[0]: raise ValueError(y must be 1D and match Xs rows) n_samples, n_features X.shape self._init_params(n_features) # 梯度下降主循环 for i in range(self.max_iter): y_pred self.predict(X) loss np.mean((y - y_pred) ** 2) self.loss_history.append(loss) # 计算梯度dL/dw 2/n * X.T (y_pred - y), dL/db 2/n * sum(y_pred - y) grad_w (2 / n_samples) * X.T (y_pred - y) grad_b (2 / n_samples) * np.sum(y_pred - y) # 参数更新 w_old, b_old self.w.copy(), self.b self.w - self.learning_rate * grad_w self.b - self.learning_rate * grad_b # 收敛判断参数变化小于阈值 if (np.linalg.norm(self.w - w_old) self.tol and np.abs(self.b - b_old) self.tol): break return self关键参数说明learning_rate0.01初始学习率对线性回归而言0.01~0.1较安全若X未标准化需降至1e-3以下max_iter1000最大迭代轮数避免死循环实际项目中建议配合early_stopping后文会补tol1e-6收敛阈值注意这里检测的是参数变化量而非损失下降量——后者易受学习率影响产生假收敛。这段代码的价值不在“能跑”而在所有中间变量y_pred,grad_w,loss都暴露在外。你可以随时在fit()里加print(fIter {i}: loss{loss:.6f}, grad_norm{np.linalg.norm(grad_w):.4f})观察训练动态。这是任何封装库都无法提供的调试粒度。3. 扩展为多算法统一框架用策略模式解耦模型、优化器与损失函数3.1 为什么单写LinearRegression不够——算法设计的本质是组合与替换真实项目中你不会只用一种算法。今天要对比岭回归L2正则、LassoL1正则、弹性网络L1L2明天要换用SGD优化器替代批量梯度下降后天要尝试Huber损失应对离群点。如果每个算法都重写一遍fit()代码将迅速失控。解决方案是分层解耦模型层Model定义预测逻辑如X w b和参数空间损失层Loss定义目标函数及梯度如MSE、Huber、Logistic Loss优化层Optimizer定义参数更新规则如GD、SGD、Adam变体。我们先定义损失函数接口class LossFunction: def __call__(self, y_true, y_pred): raise NotImplementedError def gradient(self, y_true, y_pred): 返回 dL/dy_pred供链式法则使用 raise NotImplementedError class MSELoss(LossFunction): def __call__(self, y_true, y_pred): return np.mean((y_true - y_pred) ** 2) def gradient(self, y_true, y_pred): return -2 * (y_true - y_pred) / len(y_true) class HuberLoss(LossFunction): def __init__(self, delta1.0): self.delta delta def __call__(self, y_true, y_pred): error y_true - y_pred abs_error np.abs(error) quadratic 0.5 * (error ** 2) linear self.delta * abs_error - 0.5 * (self.delta ** 2) return np.mean(np.where(abs_error self.delta, quadratic, linear)) def gradient(self, y_true, y_pred): error y_true - y_pred abs_error np.abs(error) grad np.where(abs_error self.delta, -error / len(y_true), -np.sign(error) * self.delta / len(y_true)) return grad再定义优化器基类以SGD为例class Optimizer: def step(self, params, grads): raise NotImplementedError class SGD(Optimizer): def __init__(self, lr0.01): self.lr lr def step(self, params, grads): for p, g in zip(params, grads): p - self.lr * g return params class Adam(Optimizer): def __init__(self, lr0.001, beta10.9, beta20.999, eps1e-8): self.lr lr self.beta1 beta1 self.beta2 beta2 self.eps eps self.m {} # 一阶矩估计 self.v {} # 二阶矩估计 self.t 0 def step(self, params, grads): self.t 1 for i, (p, g) in enumerate(zip(params, grads)): if i not in self.m: self.m[i] np.zeros_like(p) self.v[i] np.zeros_like(p) self.m[i] self.beta1 * self.m[i] (1 - self.beta1) * g self.v[i] self.beta2 * self.v[i] (1 - self.beta2) * (g ** 2) m_hat self.m[i] / (1 - self.beta1 ** self.t) v_hat self.v[i] / (1 - self.beta2 ** self.t) p - self.lr * m_hat / (np.sqrt(v_hat) self.eps) return params最后重构LinearModel使其接受外部Loss和Optimizerclass LinearModelV2: def __init__(self, loss_fnNone, optimizerNone): self.w None self.b None self.loss_fn loss_fn or MSELoss() self.optimizer optimizer or SGD(lr0.01) self.loss_history [] def _init_params(self, n_features): self.w np.random.normal(0, 0.01, n_features) self.b 0.0 def predict(self, X): return X self.w self.b def fit(self, X, y, epochs1000, verboseFalse): if X.ndim ! 2 or y.ndim ! 1 or len(y) ! X.shape[0]: raise ValueError(Input shape mismatch) n_samples, n_features X.shape self._init_params(n_features) for epoch in range(epochs): y_pred self.predict(X) loss self.loss_fn(y, y_pred) self.loss_history.append(loss) # 链式法则dL/dw dL/dy_pred * dy_pred/dw loss_grad * X.T loss_grad self.loss_fn.gradient(y, y_pred) # shape: (n_samples,) grad_w (1 / n_samples) * X.T loss_grad # shape: (n_features,) grad_b np.mean(loss_grad) # shape: scalar # 优化器更新传入[w, b]和[grad_w, grad_b] self.w, self.b self.optimizer.step( [self.w, self.b], [grad_w, grad_b] ) if verbose and epoch % 100 0: print(fEpoch {epoch}: loss{loss:.6f}) return self现在你可以自由组合# 岭回归在损失函数中加入L2项后续扩展 # Huber损失 Adam优化器 model LinearModelV2( loss_fnHuberLoss(delta1.5), optimizerAdam(lr0.001) ) model.fit(X_train, y_train, epochs500, verboseTrue)这种设计让算法不再是“一个整体”而是可插拔的组件。你想测试不同损失对异常值的鲁棒性换loss_fn想对比优化器收敛速度换optimizer想加正则项重写loss_fn.__call__()——所有改动都在单一职责模块内不影响其他部分。4. 避坑线性模型训练中5个血泪经验总结——别让玄学毁掉你的第一个源码项目4.1 现象训练loss震荡剧烈甚至发散原因学习率过大或输入特征未标准化。线性回归对特征尺度极度敏感——当X某列是[1, 2, 3]另一列是[1000, 2000, 3000]时梯度X.T (y_pred - y)中大尺度列主导更新方向小尺度列几乎不更新。解决对X做Z-score标准化X_std (X - np.mean(X, axis0)) / (np.std(X, axis0) 1e-8)学习率从0.001起步观察loss曲线若前10步loss下降缓慢可增至0.01若loss跳变10%立即降为0.0005。4.2 现象predict()输出全为nan或inf原因梯度计算中出现除零或溢出。常见于X.T X矩阵求逆虽然本方案未用正规方程但若你扩展为np.linalg.inv(X.T X) X.T y当X列相关时X.T X奇异或y_pred计算中X w结果过大触发浮点溢出。解决在predict()开头加检查if np.any(np.isnan(X)) or np.any(np.isinf(X)):报错权重初始化改用np.random.normal(0, 0.01, n_features)而非np.ones避免初始预测值过大在fit()循环中加防溢出if np.any(np.isnan(grad_w)) or np.any(np.isinf(grad_w)):则break并提示“梯度爆炸请检查数据标准化”。4.3 现象score()返回负值且绝对值很大原因R²公式中ss_tot np.sum((y - np.mean(y)) ** 2)为0即y全相同导致除零或模型预测比均值预测更差ss_res ss_tot。解决score()中增加保护if ss_tot 0: return 0.0 if np.allclose(y, y_pred) else -np.inf更稳健的评估用mean_absolute_error或median_absolute_error替代R²尤其当y有离群点时。4.4 现象多线程/多进程调用fit()时结果不一致原因np.random全局状态未隔离。np.random.normal()依赖全局随机种子多进程共享同一seed导致权重初始化相同训练路径完全一致。解决在_init_params()中创建局部随机生成器rng np.random.default_rng(seed42i)i为模型ID或在fit()开头设np.random.seed(int(time.time()) % 1000000)确保每次训练种子不同。4.5 现象保存模型后加载predict()报AttributeError: NoneType object has no attribute shape原因模型序列化时未保存w和b或保存了但加载后未正确赋值。pickle虽能存但跨Python版本易出错joblib对NumPy数组更友好但仍需显式定义__getstate__和__setstate__。解决实现模型持久化方法def save(self, path): import joblib state { w: self.w, b: self.b, loss_history: self.loss_history, config: {learning_rate: getattr(self.optimizer, lr, 0.01)} } joblib.dump(state, path) def load(self, path): import joblib state joblib.load(path) self.w state[w] self.b state[b] self.loss_history state[loss_history] return self注意不要直接pickle.dump(model)因model.optimizer可能含不可序列化对象如lambda函数。5. 进阶实战给线性模型加上L2正则岭回归并用交叉验证自动选超参5.1 正则化的本质不是“加惩罚项”而是约束解空间的几何操作岭回归的损失函数是$$ L(w,b) \frac{1}{n}\sum_{i1}^n (y_i - (x_i^Tw b))^2 \alpha |w|_2^2 $$其中$\alpha$控制正则强度。数学上这等价于在参数空间中将解限制在以原点为中心的球体内——$\alpha$越大球越小$w$越接近0。这不是“防止过拟合”的玄学解释而是通过改变优化目标使病态矩阵$X^TX$变为$X^TX \alpha I$从而保证可逆。我们在源码中实现它不是为了调包而是为了看清正则如何改变梯度class RidgeLoss(LossFunction): def __init__(self, alpha1.0): self.alpha alpha def __call__(self, y_true, y_pred, w): mse np.mean((y_true - y_pred) ** 2) l2_penalty self.alpha * np.sum(w ** 2) return mse l2_penalty def gradient(self, y_true, y_pred, w): # dL/dy_pred dMSE/dy_pred -2*(y_true-y_pred)/n mse_grad -2 * (y_true - y_pred) / len(y_true) # dL/dw dMSE/dw d(l2_penalty)/dw (2/n)*X.T(y_pred-y_true) 2*alpha*w # 注意此处返回的是 dL/dy_pred供链式法则使用所以只返回mse_grad # L2项对y_pred无影响故梯度不变 return mse_grad # 修改LinearModelV2.fit()使其支持传入w用于正则计算 def fit_ridge(self, X, y, alpha1.0, epochs1000, verboseFalse): # ... 初始化同前 ... for epoch in range(epochs): y_pred self.predict(X) loss self.loss_fn(y, y_pred, self.w) # 传入当前w self.loss_history.append(loss) loss_grad self.loss_fn.gradient(y, y_pred, self.w) # 仍只返回MSE梯度 grad_w (1 / len(y)) * X.T loss_grad 2 * alpha * self.w # 显式加L2梯度 grad_b np.mean(loss_grad) self.w, self.b self.optimizer.step([self.w, self.b], [grad_w, grad_b]) # ... 其余同前 ...关键点L2正则的梯度2*alpha*w直接加在grad_w上它不依赖数据只与当前权重有关——这就是为什么正则能让权重自动收缩。5.2 用K折交叉验证自动搜索最优alpha避免手动调参的盲目性手动试alpha0.1, 1.0, 10.0效率低且易过拟合验证集。我们实现一个轻量级CV工具def cross_val_score(model_class, X, y, alphas, cv5, scoringr2): from sklearn.model_selection import KFold kf KFold(n_splitscv, shuffleTrue, random_state42) scores {alpha: [] for alpha in alphas} for train_idx, val_idx in kf.split(X): X_train, X_val X[train_idx], X[val_idx] y_train, y_val y[train_idx], y[val_idx] for alpha in alphas: # 实例化模型并训练 model model_class(loss_fnRidgeLoss(alphaalpha)) model.fit_ridge(X_train, y_train, alphaalpha, epochs500, verboseFalse) # 评估 y_pred model.predict(X_val) if scoring r2: ss_res np.sum((y_val - y_pred) ** 2) ss_tot np.sum((y_val - np.mean(y_val)) ** 2) score 1 - ss_res / ss_tot if ss_tot ! 0 else 0 elif scoring mae: score np.mean(np.abs(y_val - y_pred)) scores[alpha].append(score) # 返回各alpha的平均分 mean_scores {alpha: np.mean(scores[alpha]) for alpha in alphas} best_alpha max(mean_scores, keymean_scores.get) return best_alpha, mean_scores # 使用示例 alphas [0.01, 0.1, 1.0, 10.0, 100.0] best_alpha, cv_scores cross_val_score( LinearModelV2, X_scaled, y, alphas, cv3, scoringr2 ) print(fBest alpha: {best_alpha}, CV R²: {cv_scores[best_alpha]:.4f})为什么用3折而非5折小数据集1000样本用3折更稳定——5折每折样本过少评估方差大大数据集10000可用5折或10折。这是经验法则不是教条。5.3 一个被低估的技巧用np.linalg.cond()监控矩阵病态程度提前预警正则需求在训练前快速检查X.T X的条件数condition numberdef check_condition_number(X): XtX X.T X cond_num np.linalg.cond(XtX) print(fX.T X condition number: {cond_num:.2e}) if cond_num 1e6: print(⚠️ Warning: Matrix is ill-conditioned. Consider standardization or ridge regression.) return cond_num # 调用 check_condition_number(X_train) # 若输出 1.23e08则必须加正则或标准化条件数1e6意味着矩阵接近奇异正规方程求解将严重失真。这个检查比等训练失败后再排查快10倍——它是你决定是否启用岭回归的第一道哨兵。我带过的实习生里80%的“模型不收敛”问题根源都在没做这一步检查。它不炫技但省下你三天调试时间。希望帮到你。本文还有配套的精品资源点击获取