简介基于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 里建了新项目但用的解释器是系统 pythonpip 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 需要 numpy1.26.0老版本会报“Could not build wheels”。版本不匹配的经典报错是numpy.dtype size changed或module numpy has no attribute XXX——后者在较新版本里出现了不少“改名”的情况比如trapz在新版中推荐使用np.trapezoid旧代码直接升级后就会踩到不存在属性的坑这不是 numpy 出 bug而是 API 迁移。参数说明如果只是学习这套算法numpy 1.241.26 都够用别追最新。matplotlib 不是必需但训练过程中把 loss 曲线画出来能让你“看见”收敛过程比盯着数字变化直观得多。2.2 把机器学习算法翻译成“矩阵的形状”用 numpy 手写算法核心不是背公式而是建立“矩阵形状”的直觉。绝大多数监督学习算法可以抽象为输入矩阵 X样本数 m特征数 n与参数向量 wn1 维含偏置做矩阵乘法得到预测值 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.010.1这个模拟数据里 0.05 左右比较稳。特征数量少、数据量只有 100 条时无需归一化而特征量级差异大比如一列是房价一列是面积时必须做标准化否则梯度更新会在量级大的维度上暴走量级小的维度上挪不动。3. 用 numpy 实现常见机器学习算法从梯度下降到 KNN3.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, lr0.05, epochs300): 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(fepoch {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 函数把输出压缩到 01 之间用于分类。它的损失函数不能再用 MSE而要换成交叉熵理由是 MSE 与 sigmoid 组合产生的梯度在饱和区域几乎为 0训练会停滞这在吴恩达课程里被反复强调过。def sigmoid(z): # 数值稳定写法z很大时返回1z很小时返回0 return np.where(z 0, 1 / (1 np.exp(-z)), np.exp(z) / (1 np.exp(z))) def logistic_regression(X, y, lr0.1, epochs500): 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(fepoch {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, lr0.5, epochs300) 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, k3): 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, axis1, keepdimsTrue) # (n_test, 1) X_train2 np.sum(self.X_train ** 2, axis1) # (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, axis1)[:, :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(k5) 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(axis0) self.stds[c] X_c.std(axis0) 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, keylog_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 上是防除零的保险丝尤其当某类别的某个特征值是常数时比如全是 0std 是 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, fX应为二维实际是{X.ndim}维 assert y.shape (X.shape[0], 1), fy应为({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_testNone): mean X_train.mean(axis0, keepdimsTrue) std X_train.std(axis0, keepdimsTrue) 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参数说明keepdimsTrue保留了维度信息让 mean 和 std 能直接与二维数组广播。如果不用 keepdims则 mean.shape 是 (n,)广播有时也能工作但容易出警告。另外如果某个特征的标准差为 0常量特征除出来是 inf这也是为什么前面朴素贝叶斯里要加 1e-6 的保险丝。4.3 sigmoid 函数溢出与 log 里出现 0数值稳定性的第一批坑现象逻辑回归训练几轮后 loss 显示inf或nan打印 sigmoid 输出有时是 1 有时是 0。原因np.exp(-z)当 z 为很大负数时溢出为 inf导致 sigmoid 返回 nan交叉熵里的np.log(0)就直接是负无穷。这两个问题在数据量大、特征数值大时几乎必然出现只是出现时间早晚的问题。解决sigmoid 用之前写过的np.where(z 0, 1/(1exp(-z)), exp(z)/(1exp(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_ratio0.2, seed42): 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)返回一个打乱后的 0m-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, axis1, keepdimsTrue) shifted logits - max_logit exp_sum np.sum(np.exp(shifted), axis1, keepdimsTrue) 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, lr0.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, lr0.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, dpi150)5.4 向更多算法扩展决策树、感知机与聚类掌握了线性回归、逻辑回归、KNN、朴素贝叶斯之后这个“numpy 实现机器学习算法”的方向就可以向外扩展了。常见做法是继续手写感知机Perceptron——它是最简单的神经网络单元更新规则只有一行w w lr * (y_true - y_pred) * X再进阶就是决策树的基尼指数计算和递归划分聚类方向则可以写 K-Means核心是重复“分配样本到最近质心 → 更新质心”两步同样只用 numpy 就能完成。我个人把 K-Means 的 numpy 实现当作一个很好的中期练习——它不涉及梯度但涉及“向量化距离计算”和“循环直至收敛”两个概念正好把前面学到的广播技巧复用一遍。等你把这些都写完再看 sklearn 源码会发现它的接口设计不过就是把这套东西封装成 fit / predict 两个方法而已。回头看用 numpy 手写机器学习算法这条路最大的收获不是能写出某个具体模型而是建立了“矩阵形状即契约”“数值稳定性优先”“结果必须可视化验证”这三个职业习惯。我后来做工程项目遇到模型 loss 异常、特征量级失控、评估指标虚高第一反应永远是回到这三条去找原因。希望你也能从这套练习里拿到同样的底子手撕完一个算法再聊调参和调优会从容得多。希望帮到你。本文还有配套的精品资源点击获取
