从零实现优化器:用Python函数模拟SGD、Momentum与Adam核心原理

📅 2026/8/8 11:00:52
从零实现优化器:用Python函数模拟SGD、Momentum与Adam核心原理
1. 项目概述从“无法识别”到“自己动手”最近在社区里看到不少朋友被各种“无法将‘xxx’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”这类错误困扰。从npm、git到pip甚至是opencode、claude这类工具错误提示如出一辙但背后的原因却可能千差万别。与此同时另一个高频词“优化器”也频繁出现无论是机器学习里的 Adam 优化器还是数据库升级时的优化器调整都指向了一个核心诉求如何让某个过程执行命令、训练模型、查询数据变得更高效、更顺畅。这让我想到我们每天都在和“函数”打交道。Python 的raw_input现在是input、C 语言的sprintf、Excel 里的SUMPRODUCT甚至是 STM32 里计算正弦的sin函数它们都是封装好的、用于完成特定任务的代码块。但当现成的工具链断裂命令找不到或者现成的算法如优化器不符合我们的特定需求时我们该怎么办“简单函数模拟优化器”这个项目就是一次有趣的思维和实践演练。它不要求你立刻去重写一个媲美 PyTorch 的 SGD 或 Adam而是从一个最根本的问题出发如果我们没有现成的优化器库该如何用最基础的 Python 函数去模拟优化器最核心的“参数更新”过程这个过程不仅能帮你彻底理解“梯度下降”究竟在做什么更能让你掌握一种“自给自足”的解决问题能力。下次再遇到“无法识别”的窘境时你或许就能淡定地思考“它的核心逻辑是什么我能不能用几行简单的代码先模拟出核心功能让流程跑起来”这篇文章我将带你从零开始用纯 Python 函数构建一个简易的优化器。我们会涵盖从损失函数定义、梯度计算到模拟 SGD、Momentum 乃至 Adam 的核心更新逻辑。无论你是被环境配置搞得焦头烂额的新手还是想深入理解算法本质的开发者相信都能从中获得启发和可以直接复用的代码。2. 核心思路化繁为简理解本质在开始写代码之前我们得先想清楚要模拟什么以及为什么这么模拟。市面上成熟的优化器如 TensorFlow 或 PyTorch 中的考虑了大量工程细节分布式计算、自动微分、稀疏梯度处理、数值稳定性等等。我们的“简单函数模拟”目标不是复现这些工业级特性而是剥离外壳直视其最本质的数学原理和更新逻辑。2.1 优化器到底在优化什么优化器的任务非常明确调整模型参数以最小化损失函数。 我们可以用一个简单的类比来理解你蒙着眼站在山坡上损失函数曲面要找到最低的山谷最小损失。你每走一步一次参数更新都需要根据脚下的坡度梯度来决定下一步往哪个方向、迈多大的步子。优化器就是那个告诉你“下一步怎么走”的策略。这个过程抽象成数学公式对于任何一个参数w其更新规则可以概括为w_new w_old - learning_rate * update_direction其中update_direction是优化器算法的核心差异所在。learning_rate学习率决定了步长。2.2 模拟策略与边界划定我们的模拟将遵循以下原则以确保简单性和教育性手动梯度计算我们将不依赖任何深度学习框架的autograd自动微分功能。对于要优化的函数我们将手动推导出其梯度导数的数学表达式并用 Python 函数实现。这能强迫我们理解“梯度”究竟是什么。聚焦参数更新逻辑我们将实现优化器“类”的核心方法step()。这个方法接收当前参数和计算出的梯度按照特定算法如 SGD, Momentum计算出更新量并修改参数。使用简单、可视化的例子我们将选择一个二维的凸函数如f(x) x^2 2*y^2作为我们的“损失函数”。这样参数只有两个x, y我们可以在二维平面上绘制出优化路径直观地看到不同优化器是如何“下山”的。迭代训练循环模拟一个完整的训练过程包括循环计算损失、计算梯度、调用优化器更新参数并记录轨迹。这个项目的价值在于“过程”而非“结果”。通过亲手实现你会对以下概念有肌肉记忆般的理解梯度为什么指向函数增长最快的方向学习率太大或太小会导致什么Momentum动量中的“惯性”是如何体现的Adam 优化器中的“自适应学习率”是怎么通过一阶矩和二阶矩估计实现的3. 基础构建从损失函数与梯度开始任何优化过程的起点都是一个需要被最小化的目标——损失函数。为了完全控制过程并理解原理我们从头开始定义一切。3.1 定义一个简单的损失函数我们选择一个在 (0, 0) 处有最小值的二次函数。这样梯度容易计算且便于可视化。def loss_function(params): 简单的二次损失函数: L(x, y) x^2 2 * y^2 参数: params: 一个包含两个元素的列表或元组 [x, y] 返回: 标量损失值 x, y params return x**2 2 * (y**2)这个函数的特点是它在x0, y0时取得最小值0。y前的系数2使得在y方向上的“曲率”更大优化起来会更“陡峭”这有助于我们观察不同优化器的特性。3.2 手动计算梯度梯度是损失函数对每个参数的偏导数组成的向量。对于我们的loss_function对x的偏导dL/dx 2*x对y的偏导dL/dy 4*y我们实现一个函数来计算给定参数点处的梯度def compute_gradient(params): 计算损失函数在给定参数点处的梯度。 参数: params: [x, y] 返回: grad: [grad_x, grad_y] x, y params grad_x 2 * x # dL/dx grad_y 4 * y # dL/dy return [grad_x, grad_y]注意在真实的机器学习训练中梯度是通过反向传播算法自动计算的。这里手动计算是为了强化理解。如果你要模拟一个更复杂的函数就需要手动推导并编写其梯度函数这是本项目“模拟”性质的一部分。3.3 最朴素的优化梯度下降Gradient Descent有了损失和梯度我们就可以实现最简单的优化器——梯度下降SGD这里指批梯度下降。它的更新规则就是开头提到的公式param param - learning_rate * gradient。class SimpleSGD: 简易梯度下降优化器模拟。 def __init__(self, params, lr0.1): 初始化优化器。 参数: params: 初始参数列表例如 [x, y]。 lr: 学习率 (learning rate)。 self.params params # 保存参数的引用 self.lr lr def step(self, grads): 执行一次参数更新。 参数: grads: 与params长度相同的梯度列表。 for i in range(len(self.params)): self.params[i] self.params[i] - self.lr * grads[i] def get_params(self): 返回当前参数。 return self.params.copy()这个SimpleSGD类就是我们的第一个“优化器”。它极其简单仅仅是把学习率乘以梯度并从参数中减去。3.4 第一个训练循环与可视化让我们用这个简单的优化器来跑一个完整的训练流程并看看它的优化路径。import numpy as np import matplotlib.pyplot as plt # 1. 初始化参数和优化器 init_params [3.0, 4.0] # 起点设在(3, 4)离最小值(0,0)较远 optimizer SimpleSGD(init_params, lr0.1) # 2. 训练循环 num_epochs 50 params_history [optimizer.get_params()] # 记录参数历史 loss_history [] for epoch in range(num_epochs): current_params optimizer.get_params() # 计算当前损失 loss loss_function(current_params) loss_history.append(loss) # 计算梯度 grads compute_gradient(current_params) # 优化器更新参数 optimizer.step(grads) # 记录更新后的参数 params_history.append(optimizer.get_params()) # 3. 转换为 numpy 数组便于处理 params_history np.array(params_history) x_path, y_path params_history[:, 0], params_history[:, 1] # 4. 绘制损失下降曲线和优化路径 fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 4)) # 损失曲线 ax1.plot(range(num_epochs), loss_history, markero, linestyle-, colorb) ax1.set_xlabel(Epoch) ax1.set_ylabel(Loss) ax1.set_title(Loss Decay (Simple SGD)) ax1.grid(True) # 优化路径等高线图 X, Y np.meshgrid(np.linspace(-4, 4, 100), np.linspace(-4, 4, 100)) Z X**2 2*Y**2 ax2.contour(X, Y, Z, levels20, cmapviridis) ax2.plot(x_path, y_path, markero, linestyle-, colorred, linewidth2, markersize4, labelOptimization Path) ax2.scatter(0, 0, colorgreen, s100, labelMinimum (0,0), zorder5) ax2.set_xlabel(x) ax2.set_ylabel(y) ax2.set_title(Optimization Path on Contour) ax2.legend() ax2.grid(True) ax2.axis(equal) plt.tight_layout() plt.show() # 打印最终结果 final_params optimizer.get_params() final_loss loss_function(final_params) print(f初始参数: [3.0, 4.0], 初始损失: {loss_function([3.0, 4.0]):.4f}) print(f最终参数: [{final_params[0]:.4f}, {final_params[1]:.4f}], 最终损失: {final_loss:.4f}) print(f经过 {num_epochs} 轮迭代参数更新轨迹点数为: {len(params_history)})运行这段代码你会看到两张图一张是损失值随着迭代次数下降的曲线另一张是在损失函数等高线图上绘制的参数更新路径从 (3,4) 点蜿蜒走向 (0,0) 点。实操心得一学习率的敏感性尝试将学习率lr改为0.01和0.5再运行。lr0.01更新步伐很小路径上的点非常密集需要很多轮迭代才能接近最小值收敛缓慢。lr0.5步伐很大可能会在“山谷”两侧来回震荡甚至可能发散损失越来越大。这就是学习率设置不当的典型表现。手动调整并观察学习率的影响是理解优化器调参的第一步。4. 进阶模拟给优化器加上“动量”Momentum观察 SimpleSGD 的路径你会发现它像“之”字形一样摇摆着下山。这是因为梯度方向只在当前点有效每次更新都只考虑当前瞬间的坡度。这好比一个球从山坡滚下如果只受瞬时重力影响路径会很不稳定。Momentum动量优化器的思想来源于物理学给这个球一个“惯性”。它不仅考虑当前的梯度还累积之前梯度的指数加权平均作为当前的更新方向。这有助于加速在稳定方向的收敛。减少在峡谷方向的震荡。其更新公式为v_t beta * v_{t-1} (1 - beta) * g_t param param - lr * v_t其中g_t是当前梯度v_t是动量项beta是动量系数通常取 0.9。4.1 实现 Momentum 优化器class SimpleMomentum: 简易带动量的梯度下降优化器模拟。 def __init__(self, params, lr0.1, beta0.9): 初始化优化器。 参数: params: 初始参数列表。 lr: 学习率。 beta: 动量系数。 self.params params self.lr lr self.beta beta self.velocity [0.0] * len(params) # 初始化速度动量向量为0 def step(self, grads): 执行一次带动量的参数更新。 for i in range(len(self.params)): # 更新速度融合历史速度和当前梯度 self.velocity[i] self.beta * self.velocity[i] (1 - self.beta) * grads[i] # 用速度来更新参数 self.params[i] self.params[i] - self.lr * self.velocity[i] def get_params(self): return self.params.copy()4.2 对比 SGD 与 Momentum让我们在同一个图上对比两种优化器的路径。# 重新初始化参数保证起点一致 init_params_sgd [3.0, 4.0] init_params_momentum [3.0, 4.0] # 创建两个优化器 optimizer_sgd SimpleSGD(init_params_sgd, lr0.1) optimizer_momentum SimpleMomentum(init_params_momentum, lr0.1, beta0.9) # 训练循环并记录路径 def train_and_record(optimizer, num_steps30): 训练优化器并返回参数历史。 history [optimizer.get_params().copy()] for _ in range(num_steps): current_params optimizer.get_params() grads compute_gradient(current_params) optimizer.step(grads) history.append(optimizer.get_params().copy()) return np.array(history) history_sgd train_and_record(optimizer_sgd) history_momentum train_and_record(optimizer_momentum) # 可视化对比 plt.figure(figsize(8, 6)) X, Y np.meshgrid(np.linspace(-4, 4, 100), np.linspace(-4, 4, 100)) Z X**2 2*Y**2 plt.contour(X, Y, Z, levels20, cmapviridis, alpha0.6) plt.plot(history_sgd[:, 0], history_sgd[:, 1], ro-, linewidth2, markersize5, labelSGD Path) plt.plot(history_momentum[:, 0], history_momentum[:, 1], bs-, linewidth2, markersize5, labelMomentum Path) plt.scatter(0, 0, colorgreen, s150, labelMinimum, zorder5) plt.xlabel(x) plt.ylabel(y) plt.title(Optimization Path Comparison: SGD vs Momentum) plt.legend() plt.grid(True) plt.axis(equal) plt.show()你会明显看到Momentum蓝色方块路径的轨迹比 SGD红色圆点路径更直接、更平滑震荡更小。尤其是在y方向曲率大的方向Momentum 能更快地冲过陡坡这正是“惯性”带来的好处。实操心得二动量系数beta的作用将beta设为0Momentum 就退化成了 SGD。将beta设为0.99你会发现初始的“启动”很慢因为初始速度v0被历史平均拖累但一旦获得速度冲劲会很大。beta控制了“历史”对当前的影响程度。通常 0.9 是一个不错的起点它意味着当前更新方向约等于过去10次梯度的平均方向。5. 模拟自适应学习率Adam 优化器的核心思想AdamAdaptive Moment Estimation是当前最流行的优化器之一。它结合了 Momentum 和 RMSProp 的思想分别为梯度的一阶矩均值和二阶矩未中心化的方差计算指数移动平均值并据此为每个参数自适应地调整学习率。Adam 的更新步骤稍复杂但我们可以将其核心拆解并模拟计算一阶矩估计动量m_t beta1 * m_{t-1} (1 - beta1) * g_t计算二阶矩估计自适应学习率分量v_t beta2 * v_{t-1} (1 - beta2) * g_t^2偏差校正由于m_t和v_t初始为0在训练初期会偏向0。Adam 进行了偏差校正m_hat_t m_t / (1 - beta1^t),v_hat_t v_t / (1 - beta2^t)参数更新param param - lr * m_hat_t / (sqrt(v_hat_t) epsilon)其中beta1和beta2是衰减率t是时间步epsilon是一个极小值防止除零。5.1 实现简易 Adam 优化器我们将严格遵循上述公式进行实现。class SimpleAdam: 简易 Adam 优化器模拟。 def __init__(self, params, lr0.001, beta10.9, beta20.999, epsilon1e-8): 初始化 Adam 优化器。 参数: params: 初始参数列表。 lr: 学习率。 beta1: 一阶矩估计的指数衰减率。 beta2: 二阶矩估计的指数衰减率。 epsilon: 数值稳定项防止除零。 self.params params self.lr lr self.beta1 beta1 self.beta2 beta2 self.epsilon epsilon self.m [0.0] * len(params) # 一阶矩估计 self.v [0.0] * len(params) # 二阶矩估计 self.t 0 # 时间步 def step(self, grads): 执行一次 Adam 参数更新。 self.t 1 # 更新时间步 for i in range(len(self.params)): # 更新一阶矩和二阶矩估计 self.m[i] self.beta1 * self.m[i] (1 - self.beta1) * grads[i] self.v[i] self.beta2 * self.v[i] (1 - self.beta2) * (grads[i] ** 2) # 偏差校正 m_hat self.m[i] / (1 - self.beta1 ** self.t) v_hat self.v[i] / (1 - self.beta2 ** self.t) # 参数更新 self.params[i] self.params[i] - self.lr * m_hat / (np.sqrt(v_hat) self.epsilon) def get_params(self): return self.params.copy()5.2 三种优化器同台竞技现在让我们把 SimpleSGD、SimpleMomentum 和 SimpleAdam 放在一起比较。为了公平我们为它们设置各自常用的学习率。# 初始化参数相同起点 init_params [3.0, 4.0] # 创建三个优化器注意学习率的差异Adam通常需要更小的学习率 optimizers { SGD (lr0.1): SimpleSGD(init_params.copy(), lr0.1), Momentum (lr0.1, beta0.9): SimpleMomentum(init_params.copy(), lr0.1, beta0.9), Adam (lr0.3, beta10.9, beta20.999): SimpleAdam(init_params.copy(), lr0.3, beta10.9, beta20.999) } # 训练并记录 num_steps 40 paths {} loss_records {} for name, opt in optimizers.items(): param_history [opt.get_params().copy()] loss_history [loss_function(opt.get_params())] for step in range(num_steps): grads compute_gradient(opt.get_params()) opt.step(grads) param_history.append(opt.get_params().copy()) loss_history.append(loss_function(opt.get_params())) paths[name] np.array(param_history) loss_records[name] loss_history # 绘制优化路径对比图 plt.figure(figsize(14, 5)) # 子图1优化路径 ax1 plt.subplot(1, 2, 1) X, Y np.meshgrid(np.linspace(-4, 4, 100), np.linspace(-4, 4, 100)) Z X**2 2*Y**2 ax1.contour(X, Y, Z, levels20, cmapviridis, alpha0.5) markers [o, s, ^] colors [r, b, g] for idx, (name, path) in enumerate(paths.items()): ax1.plot(path[:, 0], path[:, 1], markermarkers[idx], linestyle-, linewidth1.5, markersize4, colorcolors[idx], labelname) ax1.scatter(0, 0, colorblack, s200, labelMinimum, zorder5) ax1.set_xlabel(x) ax1.set_ylabel(y) ax1.set_title(Optimization Paths Comparison) ax1.legend() ax1.grid(True) ax1.axis(equal) # 子图2损失下降曲线 ax2 plt.subplot(1, 2, 2) for idx, (name, losses) in enumerate(loss_records.items()): ax2.plot(range(len(losses)), losses, markermarkers[idx], linestyle-, linewidth1.5, markersize4, colorcolors[idx], labelname) ax2.set_xlabel(Step) ax2.set_ylabel(Loss) ax2.set_title(Loss Decay Comparison) ax2.legend() ax2.grid(True) ax2.set_yscale(log) # 使用对数坐标更清晰地显示损失下降 plt.tight_layout() plt.show() # 打印最终损失对比 print(经过 40 步迭代后各优化器的最终损失) for name, losses in loss_records.items(): print(f{name}: {losses[-1]:.8f})从图中你可以观察到SGD路径曲折收敛速度一般。Momentum路径更平滑收敛更快。Adam路径非常独特它几乎是一条直线冲向最小值并且在损失曲线上尤其是对数坐标下显示出最快的收敛速度。这是因为 Adam 为x和y两个参数自适应地调整了“步长”。在y方向梯度大v二阶矩估计会变大使得有效学习率lr / sqrt(v)变小从而避免了震荡在x方向梯度小有效学习率相对较大从而加速更新。实操心得三Adam 的超参数敏感性Adam 虽然强大但并非“无脑用”。尝试以下操作将 Adam 的lr从0.3改为0.1或0.01观察收敛速度的变化。Adam 通常需要比 SGD 更小的学习率。将beta1改为0.5你会发现“动量”效应减弱优化路径会更“短视”。将epsilon改为一个很大的数如1你会发现更新几乎停止因为分母变得很大。epsilon是一个保障数值稳定的“安全阀”通常保持默认的1e-8即可。6. 模拟实战解决一个真实的小问题理解了原理我们用一个更贴近实际的小问题来巩固拟合一个简单的线性模型y w * x b。我们有一组数据点目标是找到最优的w和b使得预测的y和真实的y之间的均方误差MSE最小。6.1 定义问题、损失和梯度假设我们有5个数据点# 模拟数据 x_data np.array([1., 2., 3., 4., 5.]) y_data np.array([2., 4., 6., 8., 10.]) # 理想情况是 y 2*x我们的模型是y_pred w * x b。 损失函数是均方误差L(w, b) (1/N) * Σ (y_pred_i - y_data_i)^2我们需要手动推导L对w和b的梯度dL/dw (2/N) * Σ ( (w*x_i b) - y_data_i ) * x_idL/db (2/N) * Σ ( (w*x_i b) - y_data_i )def linear_loss(params, x_data, y_data): 线性模型的 MSE 损失。 w, b params N len(x_data) y_pred w * x_data b loss np.sum((y_pred - y_data) ** 2) / N return loss def linear_gradient(params, x_data, y_data): 计算线性模型损失的梯度。 w, b params N len(x_data) y_pred w * x_data b dw (2/N) * np.sum((y_pred - y_data) * x_data) db (2/N) * np.sum(y_pred - y_data) return np.array([dw, db])6.2 使用我们的 SimpleAdam 进行优化现在我们用自己实现的 SimpleAdam 来优化参数w和b。# 初始化参数随机值 init_wb np.array([0.5, 1.0]) # [w, b] # 使用我们的 SimpleAdam optimizer SimpleAdam(init_wb.tolist(), lr0.1) # 注意将 numpy 数组转为 list # 训练循环 num_epochs 200 loss_history [] param_history [optimizer.get_params().copy()] for epoch in range(num_epochs): current_params optimizer.get_params() loss linear_loss(current_params, x_data, y_data) loss_history.append(loss) grads linear_gradient(current_params, x_data, y_data) optimizer.step(grads.tolist()) # 梯度也转为 list param_history.append(optimizer.get_params().copy()) # 结果 final_w, final_b optimizer.get_params() print(f初始参数: w{init_wb[0]:.3f}, b{init_wb[1]:.3f}) print(f最终参数: w{final_w:.6f}, b{final_b:.6f}) print(f理论最优最小二乘解: w≈2.000, b≈0.000) print(f最终损失: {loss_history[-1]:.8f}) # 绘制损失下降和拟合直线 fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 4)) # 损失曲线 ax1.plot(loss_history) ax1.set_xlabel(Epoch) ax1.set_ylabel(MSE Loss) ax1.set_title(Loss during Linear Regression Training) ax1.grid(True) ax1.set_yscale(log) # 拟合结果 ax2.scatter(x_data, y_data, labelTrue Data, colorblue) x_line np.linspace(0, 6, 100) y_line_pred final_w * x_line final_b ax2.plot(x_line, y_line_pred, labelfFitted: y{final_w:.3f}x{final_b:.3f}, colorred, linewidth2) ax2.set_xlabel(x) ax2.set_ylabel(y) ax2.set_title(Linear Regression Fit) ax2.legend() ax2.grid(True) plt.tight_layout() plt.show()运行代码你会看到损失迅速下降并且拟合出的直线y w*x b非常接近真实的y 2*x。这证明了我们手写的 SimpleAdam 优化器在一个真实的、简单的机器学习任务上是完全可用的。7. 常见问题与排查技巧实录在模拟和实现过程中你可能会遇到或想到以下问题。这里我总结了一些关键点和排查思路。7.1 梯度计算错误这是手动实现时最容易出错的地方。症状损失不下降甚至爆炸NaN优化路径完全偏离。排查数值梯度检验在参数点附近施加一个微小扰动用定义计算梯度的近似值与你手写的解析梯度函数对比。def numerical_gradient(f, params, eps1e-5): 计算函数f在params处的数值梯度中心差分。 grad np.zeros_like(params) for i in range(len(params)): params_plus params.copy().astype(float) params_minus params.copy().astype(float) params_plus[i] eps params_minus[i] - eps grad[i] (f(params_plus) - f(params_minus)) / (2 * eps) return grad test_params [1.0, 2.0] analytic_grad compute_gradient(test_params) # 你的解析梯度函数 numeric_grad numerical_gradient(loss_function, test_params) print(f解析梯度: {analytic_grad}) print(f数值梯度: {numeric_grad}) print(f差异: {np.abs(analytic_grad - numeric_grad)})如果两者差异很大远大于1e-5说明你的解析梯度推导或代码有误。 2.检查公式重新推导偏导数特别注意链式法则和求和符号。7.2 学习率设置不当症状学习率太大损失剧烈震荡、爆炸变成 NaN。学习率太小损失下降极其缓慢几乎不变。排查绘制损失曲线。震荡说明学习率可能过大。使用一个非常小的学习率如1e-5开始尝试确保损失能稳定下降。然后逐步增大例如乘以3或10观察收敛速度的变化找到一个在稳定性和速度之间平衡的值。对于 Adam起始学习率通常在1e-3到1e-4之间尝试。7.3 参数初始化问题症状模型收敛到不好的局部最优或者初始损失就非常大。排查在我们的简单例子中初始化影响不大。但在复杂函数中初始化很重要。可以尝试不同的随机初始值观察优化路径是否都收敛到同一点。对于深度学习有 Xavier/Glorot 或 He 初始化等标准方法但在我们的“模拟”项目中手动设定一个合理的起点即可。7.4 优化器状态未重置症状当你用同一个优化器实例连续优化两个不同的问题时第二个问题的优化过程会异常因为m,v,t,velocity等状态还保留着上一个问题的信息。解决对于每个新的优化问题创建新的优化器实例。或者在优化器类中实现一个reset_state()方法在开始新任务前调用。7.5 与成熟框架如PyTorch的结果对比如果你想验证自己实现的正确性一个很好的方法是与 PyTorch 的对应优化器在同一个简单问题上跑出相同的结果。用 PyTorch 定义相同的模型和损失。使用 PyTorch 的torch.optim.SGD或torch.optim.Adam。设置完全相同的初始参数、学习率、动量系数等。在每一步确保从 PyTorch 的tensor.grad中取出的梯度与你手动计算的梯度数值一致可能需要.detach().numpy()。比较每一步更新后的参数值。由于浮点数精度和实现细节如偏差校正的公式写法可能会有细微差异但整体趋势和最终结果应该高度一致。这个过程是验证你“模拟”是否成功的终极测试也能让你对框架底层有更深的理解。