news 2026/9/24 21:03:33

用NumPy手写机器学习算法:从线性回归到KNN的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用NumPy手写机器学习算法:从线性回归到KNN的实战指南

简介:基于NumPy从零手写机器学习经典算法,旨在弥补直接调用Sklearn时对底层原理的模糊理解,非常适合理工科学生、AI初学者以及准备算法面试的开发者。代码覆盖线性回归、逻辑回归、决策树、SVM、k近邻、朴素贝叶斯、AdaBoost、BP神经网络等主流模型,结合《西瓜书》项目结构组织,并附两个CSV小数据集和一张示意图,方便对照教材边读边跑。资源包共11个文件,包含8个Python脚本、2个CSV数据文件和1张PNG图片,压缩包仅364KB,轻量便携。当前已有168人学习/下载。通过运行这些脚本,能直观观察梯度下降、核函数、数据划分等关键步骤的具体实现,掌握数据标准化、缺失值处理、损失函数和评估指标的计算方法;脚本内部保留关键注释,便于二次修改和拓展实验,是一份贴近底层原理的动手型学习资料。

1. 用 numpy 手写机器学习算法:为什么我劝你先别急着调 sklearn

如果你搜过“numpy 机器学习算法”,大概率是在准备面试手撕代码、应付《机器学习》周志华那本书的课后题,或者被吴恩达课程里那些绕不开的矩阵公式折磨过。这个“基于numpy实现常见机器学习算法.zip”标题背后,其实是一条很老但一直有效的路:不调 sklearn,不调 pytorch,只用 numpy 的矩阵运算把线性回归、逻辑回归、KNN、朴素贝叶斯这些常见算法从零搭出来。它解决的是“黑匣子恐惧症”——当你只调用 model.fit 和 model.predict 时,损失函数怎么更新、梯度怎么回传、特征怎么归一化,对你全是玄学;而用 numpy 裸写一遍,这些全变成可打印的中间变量。

这套东西适合三类人:一是面试前需要手推公式并现场编码的求职者;二是正在上机器学习课、对着公式推导“看懂了但不会写代码”的学生;三是想给 sklearn 结果做交叉验证、确认自己没调错参数的工程师。哪怕你不做算法岗,把梯度下降和矩阵求导手写一遍再跑出结果,也会发现“调参”这门手艺突然有了手感。

我也把这个方向当成回顾基础课的章法用了很多次——每次跳回 numpy 重写,都会有新发现。这篇就按“准备环境 → 拆解核心套路 → 照着实现 → 踩坑清单 → 验证进阶”的顺序,把这套方案的落地路径完整拆开。

2. 先搭好 numpy 动手环境:版本、矩阵思维与第一个可运行的结果

2.1 环境里最重要的不是 numpy 最新版,而是“能用”

标题里带 numpy,第一步自然是把 numpy 装好。这里有个常见的反直觉点:numpy 并不总是越新越好。你在搜索引擎里看到“numpy安装”“python安装numpy库的方法”这类高频问题,八成是从 pycharm 导入失败开始的。常见的翻车现场是——pycharm 里建了新项目,但用的解释器是系统 python,pip install numpy 装到了另一个环境;或者 conda 基础环境是 py3.8,强行装 py3.12 的 numpy 包导致版本不匹配。

先给一个稳妥的安装路径:

# 创建虚拟环境,避免污染系统 python python -m venv ml_env # 激活环境(windows 用 ml_env\Scripts\activate) source ml_env/bin/activate # 安装 numpy 与 matplotlib(画损失曲线用) pip install numpy matplotlib

逻辑说明:虚拟环境是“后悔药”,装坏了直接删目录重来,不用和系统环境纠缠。numpy 安装失败时优先级最高的排查是 python 版本与 numpy 版本的 wheel 是否匹配,比如 py3.12 需要 numpy>=1.26.0,老版本会报“Could not build wheels”。版本不匹配的经典报错是numpy.dtype size changedmodule 'numpy' has no attribute 'XXX'——后者在较新版本里出现了不少“改名”的情况,比如trapz在新版中推荐使用np.trapezoid,旧代码直接升级后就会踩到不存在属性的坑,这不是 numpy 出 bug,而是 API 迁移。

参数说明:如果只是学习这套算法,numpy 1.24~1.26 都够用,别追最新。matplotlib 不是必需,但训练过程中把 loss 曲线画出来,能让你“看见”收敛过程,比盯着数字变化直观得多。

2.2 把机器学习算法翻译成“矩阵的形状”

用 numpy 手写算法,核心不是背公式,而是建立“矩阵形状”的直觉。绝大多数监督学习算法可以抽象为:输入矩阵 X(样本数 m,特征数 n)与参数向量 w(n+1 维,含偏置)做矩阵乘法,得到预测值 y_hat,再与真实标签 y 计算损失,最后对 w 求梯度并更新。

这套流程里有三个必须时刻心里有数的形状:

  • X.shape = (m, n),每一行是一个样本,每一列是一个特征;
  • w.shape = (n, 1),列向量便于矩阵乘法;
  • y.shape = (m, 1),真实标签也要是列向量,而不是 (m,) 的形状。

新手最容易翻车的地方是把 y 做成一维数组 (m,),然后广播机制帮你“自动对齐”,结果梯度算出来形状全乱。我的习惯是在读数据后立刻打印.shape确认,宁多勿少。

import numpy as np # 生成模拟数据:y ≈ 3*x1 - 2*x2 + 5 m = 100 X_raw = np.random.randn(m, 2) true_w = np.array([[3.0], [-2.0]]) true_b = 5.0 y = X_raw @ true_w + true_b + 0.1 * np.random.randn(m, 1) # 构造带偏置的X:在X左侧拼一列1 X = np.hstack([np.ones((m, 1)), X_raw]) print("X shape:", X.shape) # (100, 3) # 初始化参数:与X列数一致 w = np.zeros((3, 1)) print("w shape:", w.shape)

逻辑说明:左边拼一列 1 是把偏置 b 折叠进 w 的常用手法,这样X @ w就同时完成了线性加权与加偏置两个操作,写梯度时不需要单独处理 b。代码里true_w是真实参数,模拟数据里加了一点噪声,便于后面验证算法是否能恢复出接近 3 和 -2 的参数。

参数说明:学习步长(学习率)初始值一般取 0.01~0.1,这个模拟数据里 0.05 左右比较稳。特征数量少、数据量只有 100 条时无需归一化;而特征量级差异大(比如一列是房价,一列是面积)时,必须做标准化,否则梯度更新会在量级大的维度上暴走,量级小的维度上挪不动。

3. 用 numpy 实现常见机器学习算法:从梯度下降到 KNN

3.1 线性回归手写:梯度下降的最小闭环

线性回归是整个机器学习算法列表里最该先手写的一个。它公式简单、损失函数是凸函数、梯度有解析式,很适合用来验证“前向传播 → 算损失 → 反向求梯度 → 更新参数”的闭环是否通畅。常见做法是假设 h(X) = X·w,损失函数用均方误差(MSE),梯度公式可以直接对损失函数求导得到:

def mse_loss(y_true, y_pred): m = y_true.shape[0] return np.mean((y_pred - y_true) ** 2) def linear_regression_gd(X, y, lr=0.05, epochs=300): m, n = X.shape w = np.zeros((n, 1)) loss_history = [] for epoch in range(epochs): y_pred = X @ w loss = mse_loss(y, y_pred) loss_history.append(loss) # 梯度:2/m * X.T @ (X @ w - y) grad = (2 / m) * (X.T @ (y_pred - y)) w = w - lr * grad if epoch % 50 == 0: print(f"epoch {epoch}, loss {loss:.4f}") return w, loss_history w_trained, losses = linear_regression_gd(X, y) print("训练后的参数:", w_trained.ravel())

逻辑说明:梯度公式里的X.T @ (y_pred - y)是向量化写法,展开写是sum(x_i * (y_pred_i - y_i)),numpy 里用矩阵乘法一次算完所有维度的梯度。除以 m 是为了把梯度归一化到“每个样本平均贡献”的量级,否则 m 越大梯度越大,学习率就得跟着缩。这里 epoch 取 300 是因为模拟数据线性可分且低维,收敛得快;如果损失曲线还在明显下降,说明没跑到平台期,要加 epochs 或调大学习率。

参数说明:epochs(迭代轮数)和 lr(学习率)是三个最核心的超参数中的两个。lr 太小收敛慢,训练过程就像原地踏步;lr 太大 loss 会出现锯齿状跳动甚至 NaN。一个实用技巧是打印每轮的 loss,看它是否单调下降;如果前几轮 loss 剧烈震荡,先调小 lr 两个数量级再重新跑。

3.2 逻辑回归:把线性回归的y换成概率

线性回归解决回归问题,逻辑回归则是在线性模型上套一个 sigmoid 函数,把输出压缩到 0~1 之间,用于分类。它的损失函数不能再用 MSE,而要换成交叉熵,理由是 MSE 与 sigmoid 组合产生的梯度在饱和区域几乎为 0,训练会停滞,这在吴恩达课程里被反复强调过。

def sigmoid(z): # 数值稳定写法:z很大时返回1,z很小时返回0 return np.where(z >= 0, 1 / (1 + np.exp(-z)), np.exp(z) / (1 + np.exp(z))) def logistic_regression(X, y, lr=0.1, epochs=500): m, n = X.shape w = np.zeros((n, 1)) for epoch in range(epochs): z = X @ w y_prob = sigmoid(z) # 交叉熵损失 eps = 1e-12 loss = -np.mean(y * np.log(y_prob + eps) + (1 - y) * np.log(1 - y_prob + eps)) # 梯度:X.T @ (sigmoid(Xw) - y) / m grad = (1 / m) * (X.T @ (y_prob - y)) w = w - lr * grad if epoch % 100 == 0: print(f"epoch {epoch}, loss {loss:.4f}") return w # 构造二分类数据 np.random.seed(42) X_cls = np.random.randn(150, 2) y_cls = (X_cls[:, 0] - X_cls[:, 1] + 0.5 > 0).astype(int).reshape(-1, 1) w_log = logistic_regression(X_cls, y_cls, lr=0.5, epochs=300) print("逻辑回归参数:", w_log.ravel())

逻辑说明:sigmoid 函数里用np.where分情况计算,是为了防止np.exp(-z)溢出——z 为非常大的负数时,exp(-z) 会变成 inf。交叉熵里加了eps防止 y_prob 为 0 或 1 时取对数出现负无穷。这里的模拟数据用了线性可分的“x1 - x2 + 0.5 > 0”作为真实分类边界,逻辑回归学到的权重应该接近 [0.5, -0.5] 这个方向,数值大小与学习率、迭代轮数有关,但决策边界方向应当一致。

参数说明:逻辑回归的收敛速度明显慢于线性回归,且对学习率更敏感。用 0.5 的学习率在这个数据上能较快收敛;如果 loss 出现 inf,第一时间检查 y 是否被正确 reshape 成 (m, 1)。另外注意预测时要用sigmoid(X @ w) > 0.5来判定类别,“0.5 阈值”就是分类边界,阈值调高或调低会影响精确率与召回率的取舍。

3.3 KNN 的 numpy 实现:不训练也能分类的算法

KNN 是机器学习算法里少数没有显式训练过程的——它只是把训练数据存起来,预测时计算新样本与所有训练样本的距离,取最近的 K 个点投票。用 numpy 实现它的核心在于距离矩阵的向量化计算,这正好是理解“矩阵化思维”的绝佳训练:

class KNN: def __init__(self, k=3): self.k = k def fit(self, X_train, y_train): self.X_train = X_train self.y_train = y_train.reshape(-1) def predict(self, X_test): # 向量化计算欧氏距离:||a-b||^2 = ||a||^2 + ||b||^2 - 2ab^T X2 = np.sum(X_test ** 2, axis=1, keepdims=True) # (n_test, 1) X_train2 = np.sum(self.X_train ** 2, axis=1) # (n_train,) cross = X_test @ self.X_train.T # (n_test, n_train) dist_sq = X2 - 2 * cross + X_train2 # 广播 dist_sq = np.maximum(dist_sq, 0) # 避免负数的浮点误差 # 按距离排序,取前k个样本的标签,多数投票 k_idx = np.argsort(dist_sq, axis=1)[:, :self.k] k_labels = self.y_train[k_idx] # 逐行统计众数 preds = np.array([np.bincount(row).argmax() for row in k_labels]) return preds # 示例:用上面的二分类数据演示(前100为训练,后50为测试) knn = KNN(k=5) knn.fit(X_cls[:100], y_cls[:100]) preds = knn.predict(X_cls[100:]) acc = np.mean(preds == y_cls[100:].ravel()) print("KNN准确率:", acc)

逻辑说明:距离计算没有用循环,而是展开成平方和公式||a - b||² = ||a||² + ||b||² - 2ab^T,其中广播机制自动把 (n_test, 1) 的 X2 与 (n_train,) 的 X_train2 对齐成 (n_test, n_train),再减去 2 倍交叉项,一次算出全部样本对的距离。这是我个人认为 numpy手写算法最值得学的一步——把双层 for 循环拆成矩阵运算,速度提升上百倍。np.bincount对每个测试样本的 K 个邻居标签做直方图统计,取最大值下标作为预测类别。

参数说明:K 是最关键的参数,太小容易受单个噪声点影响,太大则会把远处的类别也拉进来。实践中常用交叉验证确定 K,经验值大约是训练样本数的平方根左右。另一个被忽视的参数是距离度量:欧氏距离对特征的量级很敏感,如果特征范围差异大,必须先做标准化,否则“量级大的特征主导距离”这个问题无从避免。

3.4 朴素贝叶斯的 numpy 实现:用条件概率做分类

朴素贝叶斯的核心假设是“特征之间相互独立”,基于这个简化假设,联合概率可以拆成每个特征条件概率的连乘。在 numpy 里实现高斯朴素贝叶斯(适用于连续特征)是最常见的做法,因为每个特征在每个类别下的条件概率用高斯分布拟合,公式小而清晰:

class GaussianNB: def fit(self, X, y): self.classes = np.unique(y) self.means = {} self.stds = {} self.priors = {} for c in self.classes: X_c = X[y == c] self.means[c] = X_c.mean(axis=0) self.stds[c] = X_c.std(axis=0) + 1e-6 # 防止除0 self.priors[c] = X_c.shape[0] / X.shape[0] def _gaussian_pdf(self, x, mean, std): # 高斯概率密度函数 exponent = -0.5 * ((x - mean) ** 2) / (std ** 2) return np.exp(exponent) / (np.sqrt(2 * np.pi) * std) def predict(self, X): preds = [] for x in X: log_probs = {} for c in self.classes: # 用log概率连乘,避免数值下溢 log_probs[c] = np.log(self.priors[c]) + np.sum( np.log(self._gaussian_pdf(x, self.means[c], self.stds[c])) ) preds.append(max(log_probs, key=log_probs.get)) return np.array(preds) # 使用说明:X为连续特征矩阵,y为分类标签 nb = GaussianNB() nb.fit(X_cls[:100], y_cls[:100].ravel()) nb_preds = nb.predict(X_cls[100:]) print("朴素贝叶斯准确率:", np.mean(nb_preds == y_cls[100:].ravel()))

逻辑说明:对每个类别,分别计算该类别下每个特征的均值与标准差,然后对新样本计算它在每个类别下的对数概率,取最大者作为分类结果。这里特意用np.log把连乘变成连加,是为了防止上百个概率相乘后数值下溢成 0——这是高斯朴素贝叶斯 numpy 实现里最该注意的坑,也是朴素贝叶斯被诟病“数值不稳”的真正原因。

参数说明:1e-6加到 std 上是防除零的保险丝,尤其当某类别的某个特征值是常数时(比如全是 0),std 是 0 会导致概率计算崩溃。实际项目中如果特征数量很大,还可以考虑对每个特征做独立性检验,这里不做展开——那个属于特征选择的范畴,不是 numpy 实现的重点。

4. numpy 实现机器学习算法的避坑与排查手册

4.1 维度不匹配:广播机制把你坑了,你还以为是自己代码写错

现象:代码能跑,但结果明显不对——loss 不降反升,或者模型预测全是一个类别。

原因:这是 numpy 新手最常见的翻车现场。比如y_pred形状是 (100, 1),y形状是 (100,),相减时广播机制会自动把 (100,) 扩展成 (100, 100),得到一个 100x100 的差值矩阵,np.mean不会报错,但算出来的 loss 是错误的平均值。更隐蔽的是,(100, 1)(1, 100)两个矩阵相减,广播出来是(100, 100),你的损失值看起来“正常”,但训练方向完全错误。

解决:在所有数据进入训练循环前,统一加上形状断言:

assert X.ndim == 2, f"X应为二维,实际是{X.ndim}维" assert y.shape == (X.shape[0], 1), f"y应为({X.shape[0]}, 1),实际是{y.shape}"

根本思路是让程序在入口处就“报错”,而不是等到结果不对了再回头猜。这也是我多条血泪经验中排第一的一条——宁可在入口处多写三个 assert,也不要在训练完才发现 loss 曲线是锯齿形的。

4.2 梯度爆炸与数据归一化:为什么同一个模型换个数据集就翻车

现象:训练一小段时间后 loss 变成 nan,或者 w 的值变成 1e8 量级。

原因:最常见的原因是特征量级差异过大。比如房价预测中面积是几十的数量级,房龄是几年的数量级,x1 变化一个单位对输出影响远大于 x2,梯度更新时 x2 对应的 w 更新慢,x1 对应的 w 更新快。极端情况下学习率稍大,某个特征的大梯度直接把 w 推到发散区域。

解决:对特征做标准化处理,常用 z-score:每个特征减去均值、除以标准差。注意测试集用的均值与标准差要用训练集的,不能重新计算。

def zscore_normalize(X_train, X_test=None): mean = X_train.mean(axis=0, keepdims=True) std = X_train.std(axis=0, keepdims=True) X_train_norm = (X_train - mean) / std if X_test is not None: X_test_norm = (X_test - mean) / std return X_train_norm, X_test_norm, mean, std return X_train_norm, mean, std

参数说明:keepdims=True保留了维度信息,让 mean 和 std 能直接与二维数组广播。如果不用 keepdims,则 mean.shape 是 (n,),广播有时也能工作但容易出警告。另外如果某个特征的标准差为 0(常量特征),除出来是 inf,这也是为什么前面朴素贝叶斯里要加 1e-6 的保险丝。

4.3 sigmoid 函数溢出与 log 里出现 0:数值稳定性的第一批坑

现象:逻辑回归训练几轮后 loss 显示infnan,打印 sigmoid 输出有时是 1 有时是 0。

原因:np.exp(-z)当 z 为很大负数时溢出为 inf,导致 sigmoid 返回 nan;交叉熵里的np.log(0)就直接是负无穷。这两个问题在数据量大、特征数值大时几乎必然出现,只是出现时间早晚的问题。

解决:sigmoid 用之前写过的np.where(z >= 0, 1/(1+exp(-z)), exp(z)/(1+exp(z)))分段形式;交叉熵里加 eps 并限制预测概率范围:

def stable_cross_entropy(y_true, y_prob): eps = 1e-12 y_prob = np.clip(y_prob, eps, 1 - eps) # 裁剪概率范围 return -np.mean(y_true * np.log(y_prob) + (1 - y_true) * np.log(1 - y_prob))

np.clip与加 eps 二选一即可,同时用更保险。这条不仅适用于逻辑回归,凡是模型最后输出概率的(KNN 不做概率输出但在算距离时也会碰到浮点误差),都应该做一次裁剪。

4.4 矩阵乘法顺序与转置错误:X @ w 还是 X.T @ w,千万不能凭感觉

现象:loss 在某个 epoch 突变,或者训练出来的 w 反向(符号反了)。

原因:梯度计算时,X.T @ (y_pred - y)(y_pred - y).T @ X形状虽然不同,但有时凑巧能广播出一个“看起来不错”的结果。一个具体的隐蔽场景是:你更新参数时用了w = w - lr * grad,但 grad 形状是 (n, 1),而 w 是 (n,),两者相减后 w 变成 (n, n) 对称矩阵。

解决:初始化时就确定 w 的 shape,强制所有中间变量也保持这个形状:

n = X.shape[1] w = np.zeros((n, 1)) # 列向量 y = y.reshape(-1, 1) # 强制成列向量 # 在训练循环内打印一次形状,确认无误 assert w.shape == (n, 1) assert y.shape == (m, 1) assert (X @ w).shape == (m, 1)

我的习惯是做到“三个一致”:X 的行数等于 y 的行数,w 的行数等于 X 的列数,预测值形状等于 y 的形状。其中任何一个不一致,别犹豫,停下来打印 shape。

4.5 训练集测试集划分不当:用手写的模型看评估指标,结果虚高

现象:模型在“验证”时准确率 95%,一旦上线或者拿到新数据就崩到 60%。

原因:没有打乱数据就直接按前 80% / 后 20% 划分。真实数据往往有顺序性——前 80% 可能全是某一类别的样本,后 20% 是另一类别,模型只是“背住了”训练集里的分布,遇到新数据立刻现出原形。

解决:手动实现随机打乱划分,最好固定随机种子,保证可复现:

def train_test_split_by_numpy(X, y, test_ratio=0.2, seed=42): np.random.seed(seed) m = X.shape[0] idx = np.random.permutation(m) n_test = int(m * test_ratio) test_idx, train_idx = idx[:n_test], idx[n_test:] return X[train_idx], X[test_idx], y[train_idx], y[test_idx]

逻辑说明:np.random.permutation(m)返回一个打乱后的 0~m-1 排列,用它做索引就能同时保持 X 与 y 的对应关系。固定 seed 是让实验结果可复现的必备操作,否则每次跑结果都不一样,调参就成了玄学。

5. 向量化与数值稳定性进阶:让 numpy 手写算法跑得更快、更稳

5.1 用 log-sum-exp 替换朴素贝叶斯里的连乘

前面在朴素贝叶斯里用np.log把连乘变成连加,已经是一个数值稳定处理。更进一步的做法是用 log-sum-exp 技巧,它在处理多分类概率输出时更通用:假设你算了三个类别的对数概率 [log_p1, log_p2, log_p3],想归一化成概率,直接做log_p / np.sum(log_p)是错误的,因为 log 空间不能直接相除。

def log_softmax(logits): # logits: (n_samples, n_classes) 各样本的对数概率 max_logit = np.max(logits, axis=1, keepdims=True) shifted = logits - max_logit exp_sum = np.sum(np.exp(shifted), axis=1, keepdims=True) return shifted - np.log(exp_sum) # 示例:两个样本,三个类别的对数概率 logits = np.array([[1000, 1001, 999], [0.2, 0.3, 0.1]]) probs = np.exp(log_softmax(logits)) print("归一化概率:", probs)

逻辑说明:先减去每行的最大值,再取 exp 就不会溢出。比如第一行 [1000, 1001, 999],直接算 exp 会超出 float64 可表示范围,减去 1001 后变成 [-1, 0, -2],exp 后是 [0.37, 1, 0.14],安全又精确。这在手写多分类逻辑回归或 softmax 回归时是必用的技巧。

5.2 用矩阵运算替代 for 循环:一个可量化的性能对比

手写算法时最容易写出三重 for 循环——外层遍历 epoch,中层遍历样本,内层遍历特征。同样的算法,向量化写法在 10000 条数据上可能快 50 倍以上。这里给出一个直观的对比实验思路:

import time # 慢速写法:逐样本计算梯度 def gradient_descent_slow(X, y, w, lr=0.01): m = X.shape[0] grad = np.zeros_like(w) for i in range(m): pred = np.dot(X[i], w) for j in range(X.shape[1]): grad[j] += (pred - y[i]) * X[i, j] return grad / m # 快速写法:矩阵一次算完 def gradient_descent_fast(X, y, w, lr=0.01): m = X.shape[0] pred = X @ w # (m, 1) grad = X.T @ (pred - y) # (n, 1) return grad / m

逻辑说明:慢速写法里两层 for 循环的累加,本质是矩阵乘法的逐项展开。X.T @ (pred - y)一次完成所有样本、所有特征维度的梯度累积,完全等价但性能天差地别。建议在自己的机器上跑一个 5000 样本、20 特征的数据,分别计时,你会对“numpy向量化”这件事有肌肉记忆。

5.3 验证手写模型的三个标准动作:残差检查、与 sklearn 对拍、可视化

花大力气手写算法之后,最大的疑问是“我写对了没有”。我的习惯是三个动作挨个过:

第一,构造一个已知参数的数据集(比如 y = 3x1 - 2x2 + 5),训练后检查参数能否恢复到 [3, -2, 5] 附近。这能验证梯度公式是否写对——如果梯度符号反了,恢复出来的参数是 [-3, 2, -5],一眼就能发现。

第二,对拍 sklearn 的相同模型。线性回归与 sklearn 的 LinearRegression 对比,逻辑回归与 LogisticRegression 对比,数值不必完全一致(因为迭代次数、学习率不同),但决策边界和损失量级应接近。差异过大说明实现里有 bug。

第三,画 loss 曲线与决策边界。这条最直观:loss 曲线应该平滑下降并趋于平缓;决策边界应该大致把两类数据分开。画图用 matplotlib,几行代码就能看到手写算法到底学到了什么:

import matplotlib.pyplot as plt plt.plot(losses) plt.xlabel("epoch") plt.ylabel("mse loss") plt.title("梯度下降收敛曲线") plt.savefig("loss_curve.png", dpi=150)

5.4 向更多算法扩展:决策树、感知机与聚类

掌握了线性回归、逻辑回归、KNN、朴素贝叶斯之后,这个“numpy 实现机器学习算法”的方向就可以向外扩展了。常见做法是继续手写感知机(Perceptron)——它是最简单的神经网络单元,更新规则只有一行w = w + lr * (y_true - y_pred) * X;再进阶就是决策树的基尼指数计算和递归划分;聚类方向则可以写 K-Means,核心是重复“分配样本到最近质心 → 更新质心”两步,同样只用 numpy 就能完成。

我个人把 K-Means 的 numpy 实现当作一个很好的中期练习——它不涉及梯度,但涉及“向量化距离计算”和“循环直至收敛”两个概念,正好把前面学到的广播技巧复用一遍。等你把这些都写完,再看 sklearn 源码,会发现它的接口设计不过就是把这套东西封装成 fit / predict 两个方法而已。

回头看,用 numpy 手写机器学习算法这条路,最大的收获不是能写出某个具体模型,而是建立了“矩阵形状即契约”“数值稳定性优先”“结果必须可视化验证”这三个职业习惯。我后来做工程项目,遇到模型 loss 异常、特征量级失控、评估指标虚高,第一反应永远是回到这三条去找原因。希望你也能从这套练习里拿到同样的底子;手撕完一个算法,再聊调参和调优,会从容得多。希望帮到你。

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

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

Node.js+PHP+Vue搭建校园寝室小卖部系统全解析

记得我是在大学里搞了一个代购跑腿的创业项目,后来慢慢变成帮零食品牌跑校园市场,就是你们看到的那种“扫码进小程序,寝室零食10分钟送到”的模式。2023年那会儿接了学校附近一个连锁便利店的需求,要把整个寝室小卖部的业务线上化…

作者头像 李华
网站建设 2026/9/24 21:03:25

企业云、公有云、私有云、混合云怎么选?附私有云搭建实战

别急着上来就讨论买哪家的服务器、选哪个云厂商。过去几年我给不少企业做过基础设施的规划,发现一个特别普遍的现象:很多团队连“企业云、公有云、私有云、混合云”这四个词到底指什么、边界在哪里都没完全厘清,就急着做技术选型,…

作者头像 李华
网站建设 2026/9/24 21:02:34

自托管埋点分析平台选型:ClickHouse与Superset工程实践指南

1. 自托管埋点分析平台不是“搭个ClickHouseSuperset”就完事了“自托管埋点分析平台应该怎么选?”——这个问题最近在技术群、架构师论坛和数据团队内部会议里高频出现。不是因为大家突然对埋点产生了兴趣,而是被SaaS型分析工具的账单、数据出境合规压力…

作者头像 李华
网站建设 2026/9/24 21:01:32

电脑连上WiFi却无法上网?从IP到DNS的五种排查方法搞定网络故障

上周一位同事抱着一台“才买不到一年”的笔记本找我,表情很绝望:“昨晚还好好的,今天一开机,WiFi连上了,右下角却冒出一个黄色感叹号,网页全打不开。手机连同一个路由,上网却飞快。” 这种场景我…

作者头像 李华
网站建设 2026/9/24 21:01:04

DeepSeek Harness长会话不崩盘:上下文压缩与目标管理实战

1. 为什么“长会话不崩盘”是Agent落地的第一道生死线 你有没有遇到过这样的场景:一个用户在智能体对话中连续追问了17轮,从查天气、订咖啡、比价买耳机,再到让AI帮你写一封辞职信的初稿——每一轮都带着上下文依赖,前一轮说“这个…

作者头像 李华
网站建设 2026/9/24 20:57:23

Unity异步文件拷贝工具类:分块读写与进度上报实战

从实际项目角度出发,这种工具类几乎是 Unity 工程里绕不开的“基建”。无论是做热更新资源准备、存档导出、编辑器批处理,还是运行时把大型文件从托管目录搬到持久化目录,一个稳定、不卡主线程、能反馈进度、还能处理重命名和错误回调的拷贝工…

作者头像 李华