尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零实现BP神经网络:原理推导、代码实战与调优指南

从零实现BP神经网络:原理推导、代码实战与调优指南 1. 项目概述从“黑箱”到“白盒”理解深度学习的基石如果你刚开始接触深度学习可能会被各种复杂的模型结构、层出不穷的框架和眼花缭乱的数学公式搞得晕头转向。很多人把深度学习模型当作一个“黑箱”数据进去结果出来中间过程不甚了了。但如果你想真正掌握这门技术而不是停留在调包和调参的表面功夫就必须深入理解其最核心、最经典的原理。而BP神经网络正是打开这个“黑箱”的第一把钥匙也是所有现代深度学习模型的“祖师爷”。简单来说BP神经网络Backpropagation Neural Network是一种通过误差反向传播算法来训练的多层前馈神经网络。它的核心思想可以用一个生活中的例子来类比教一个小孩认苹果。你第一次给他看一个红苹果告诉他这是“苹果”。他可能记住了红色和圆形。然后你给他看一个青苹果他可能就认不出来了因为颜色不对。这时你告诉他“这也是苹果”并纠正他之前的认知偏差。这个过程反复进行他大脑中关于“苹果”的判断规则比如形状、大小、有无果柄就在一次次“犯错-纠正”中被不断调整和优化。BP神经网络的学习过程与此高度相似输入数据苹果图片网络给出预测这是苹果吗计算预测与真实标签你告诉他的答案之间的误差然后将这个误差从输出层逐层反向传播回去调整网络中的每一个“连接强度”权重使得下一次预测更准确。为什么在今天卷积神经网络CNN、循环神经网络RNN、Transformer大行其道的时代我们还要花大力气去啃这个“老古董”原因有三第一万变不离其宗。无论是多么复杂的网络结构其参数更新的核心逻辑——梯度下降与链式法则——都源于BP算法。理解了BP你就掌握了理解所有深度学习模型优化过程的“元技能”。第二调试的基石。当你的模型训练出现梯度消失、爆炸、不收敛等问题时如果不理解BP过程你几乎无法进行有效的诊断和调优只能盲目尝试。第三创新的起点。许多新的网络结构如ResNet的残差连接和优化技巧如各种归一化方法其设计初衷都是为了解决传统BP训练中的固有缺陷。知其然更要知其所以然。因此本文的目标不是简单地复述教科书上的公式而是带你像一位工程师一样亲手“搭建”并“训练”一个BP神经网络。我们将从最基础的数学模型和计算图开始一步步推导出那个看似复杂的反向传播公式并用代码实现一个完整的训练流程。我会分享在实际编码和调试中积累的经验比如如何初始化权重、如何选择激活函数、如何监控训练过程以及如何避开那些教科书上不会写的“坑”。无论你是刚入门的新手还是希望夯实理论基础的有经验者相信这篇深入原理的剖析都能让你对深度学习的运作机制有焕然一新的认识。2. BP神经网络的核心原理与数学模型拆解要理解BP我们必须先彻底搞懂它的“前向传播”和“反向传播”这两个核心过程。很多人觉得反向传播的数学推导很难其实只要我们用好“计算图”这个工具一切都会变得清晰明了。2.1 前向传播信息如何从输入流向输出前向传播是神经网络进行预测的过程。我们以一个最简单的三层网络输入层、一个隐藏层、输出层为例来拆解每一步的计算。假设我们的网络结构如下输入层有n个神经元输入向量记为x。隐藏层有m个神经元。连接输入层和隐藏层的权重矩阵为W1形状[n, m]偏置向量为b1形状[m,]。输出层有k个神经元例如二分类问题k1十分类问题k10。连接隐藏层和输出层的权重矩阵为W2形状[m, k]偏置向量为b2形状[k,]。前向传播的计算步骤如下隐藏层输入z1 x · W1 b1这里·表示矩阵乘法。z1是一个长度为m的向量代表了隐藏层神经元接收到的“原始信号”。隐藏层激活a1 f(z1)f是激活函数如Sigmoid、ReLU等。激活函数的作用是引入非线性。如果没有激活函数无论堆叠多少层整个网络等价于一个线性模型无法学习复杂模式。a1是隐藏层的输出。输出层输入z2 a1 · W2 b2将隐藏层的输出作为输入进行线性变换。输出层激活a2 g(z2)g是输出层的激活函数。对于不同任务g的选择不同二分类通常用Sigmoid函数将输出压缩到(0,1)表示概率。多分类用Softmax函数将输出转化为概率分布。回归通常用恒等函数即不激活直接输出数值。a2就是网络的最终预测输出y_hat。这个过程就像一条生产线原材料x经过第一道工序W1, b1, f加工成半成品a1再经过第二道工序W2, b2, g加工成最终产品y_hat。注意这里的权重W的 shape 定义是关键。常见的两种约定是“权重在前”(input_dim, output_dim)和“权重在后”(output_dim, input_dim)。本文采用“权重在前”的约定即W1的列数等于隐藏层神经元数。这在推导梯度时会影响矩阵转置的位置务必在实现时保持一致。2.2 损失函数衡量预测与真实的差距网络做出预测后我们需要一个标准来衡量它预测得好不好这个标准就是损失函数L。常见的损失函数有均方误差L 1/2 * (y_hat - y)^2常用于回归问题。交叉熵损失L - [y * log(y_hat) (1-y) * log(1-y_hat)]常用于二分类。多分类则为L - Σ y_i * log(y_hat_i)。我们的目标就是通过调整网络的所有参数W1, b1, W2, b2使得损失L的值最小化。2.3 反向传播误差如何指导参数更新这是BP算法的精髓。其核心是链式求导法则。我们的目标是求出损失L对每一个参数如W2_ij的偏导数即梯度然后沿着梯度的反方向更新参数因为梯度方向是函数值增长最快的方向反方向就是下降最快的方向。我们依然从后往前利用计算图来推导。假设我们使用均方误差损失和Sigmoid激活函数仅用于示例实际中隐藏层多用ReLU。1. 计算输出层的梯度损失对网络最终输出a2的梯度∂L/∂a2 a2 - y对于MSE损失。a2是z2经过激活函数g得到的a2 g(z2)。所以∂a2/∂z2 g‘(z2)例如Sigmoid的导数为g‘(z) g(z)*(1-g(z))。根据链式法则损失对z2的梯度δ2 ∂L/∂z2 (∂L/∂a2) * (∂a2/∂z2) (a2 - y) * g‘(z2)。这个δ2非常重要它被称为输出层的“误差信号”。现在z2 a1 · W2 b2。所以∂L/∂W2 a1^T · δ2注意这里的矩阵乘法维度a1^T是[m, 1]δ2是[1, k]结果是[m, k]与W2同形。∂L/∂b2 δ2偏置的梯度就是误差信号本身在求和时通常会对 batch 维度求和。2. 计算隐藏层的梯度误差信号需要继续反向传播到隐藏层。z1经过f得到a1而a1参与了z2的计算。损失对a1的梯度∂L/∂a1 δ2 · W2^T。这可以理解为输出层的误差δ2按照权重W2的比例分配回隐藏层的每个神经元。然后a1 f(z1)所以∂a1/∂z1 f‘(z1)。根据链式法则损失对z1的梯度δ1 ∂L/∂z1 (∂L/∂a1) * (∂a1/∂z1) (δ2 · W2^T) * f‘(z1)。这里的*是逐元素相乘Hadamard积。最后z1 x · W1 b1所以∂L/∂W1 x^T · δ1∂L/∂b1 δ13. 参数更新得到所有参数的梯度后使用梯度下降法进行更新W W - learning_rate * ∂L/∂Wb b - learning_rate * ∂L/∂b其中learning_rate是学习率一个需要手动设置的重要超参数。这个过程可以推广到任意多层网络。每一层都先计算该层的“误差信号”δ然后用这个δ和前一层的激活值来计算本层权重的梯度。误差像涟漪一样从最后的损失函数一层层反向传播回去故名“反向传播”。实操心得理解计算图是掌握反向传播的钥匙。不要死记硬背公式。在纸上或脑海里画出计算图标出每个中间变量然后从损失函数开始问自己“L如何受到这个变量影响”并应用链式法则。多推导几次你就会发现其中的模式。现代深度学习框架如PyTorch、TensorFlow的自动微分功能本质上就是在构建和遍历这样一个计算图。3. 从零实现一个BP神经网络代码与细节理解了数学原理最好的巩固方式就是动手实现。我们将使用纯NumPy来实现一个用于MNIST手写数字识别的BP神经网络这能让你摆脱框架的“魔法”看清每一个细节。3.1 网络结构与初始化我们构建一个包含一个隐藏层的网络输入层784维28x28图片展平隐藏层256个神经元输出层10个神经元对应0-9十个数字。import numpy as np import struct from sklearn.preprocessing import OneHotEncoder class SimpleBPNet: def __init__(self, input_size, hidden_size, output_size, lr0.01): self.input_size input_size self.hidden_size hidden_size self.output_size output_size self.lr lr # 学习率 # 权重初始化这是训练成功的关键第一步 # 使用He初始化适用于ReLU激活函数 self.W1 np.random.randn(input_size, hidden_size) * np.sqrt(2. / input_size) self.b1 np.zeros((1, hidden_size)) self.W2 np.random.randn(hidden_size, output_size) * np.sqrt(2. / hidden_size) self.b2 np.zeros((1, output_size)) def relu(self, x): ReLU激活函数及其导数用于反向传播 return np.maximum(0, x), (x 0).astype(float) def softmax(self, x): 稳定的Softmax函数 exp_x np.exp(x - np.max(x, axis1, keepdimsTrue)) # 减去最大值防止溢出 return exp_x / np.sum(exp_x, axis1, keepdimsTrue) def cross_entropy_loss(self, y_pred, y_true): 计算交叉熵损失y_true是one-hot编码 m y_true.shape[0] # 添加一个小常数防止log(0) log_likelihood -np.log(y_pred[range(m), np.argmax(y_true, axis1)] 1e-8) loss np.sum(log_likelihood) / m return loss关键细节解析权重初始化为什么不用np.random.randn()直接生成如果权重初始值过大或过小在深层网络中经过多次连乘会导致梯度爆炸或消失。He初始化sqrt(2 / fan_in)是为ReLU函数设计的它能在网络初始时保持各层输出的方差大致稳定极大地改善了训练的动态性。这是实践中一个非常重要且有效的技巧。3.2 前向传播实现def forward(self, x): 前向传播同时保存中间变量用于反向传播 self.x x # 保存输入 # 隐藏层 self.z1 np.dot(x, self.W1) self.b1 self.a1, self.a1_mask self.relu(self.z1) # 保存激活前的值和激活掩码导数 # 输出层 self.z2 np.dot(self.a1, self.W2) self.b2 self.a2 self.softmax(self.z2) # 输出概率分布 return self.a2这里我们保存了z1,a1,z2,a2以及ReLU的掩码a1_mask即z1 0的布尔矩阵其浮点数形式就是ReLU在正区间的导数为1负区间为0。这些中间结果在反向传播时是必需的。3.3 反向传播与参数更新这是核心中的核心我们将前文推导的公式转化为代码。def backward(self, y_true): 反向传播计算梯度并更新参数 m y_true.shape[0] # 样本数量batch size # 1. 输出层梯度 # Softmax 交叉熵的梯度有简洁形式dz2 a2 - y_true dz2 self.a2 - y_true # 形状 (m, output_size) # 计算W2和b2的梯度 dW2 np.dot(self.a1.T, dz2) / m # 除以m是求平均梯度对应损失函数里的平均 db2 np.sum(dz2, axis0, keepdimsTrue) / m # 2. 隐藏层梯度 # 误差传播回隐藏层 da1 np.dot(dz2, self.W2.T) # (m, hidden_size) # 通过ReLU的导数 dz1 da1 * self.a1_mask # 逐元素相乘负半轴梯度为0 # 计算W1和b1的梯度 dW1 np.dot(self.x.T, dz1) / m db1 np.sum(dz1, axis0, keepdimsTrue) / m # 3. 使用梯度下降更新参数 self.W2 - self.lr * dW2 self.b2 - self.lr * db2 self.W1 - self.lr * dW1 self.b1 - self.lr * db1关键细节解析梯度计算与平均注意在计算dW2和dW1时我们最后都除以了m/ m。这是因为我们的损失函数cross_entropy_loss是对一个批次batch内所有样本的损失求了平均。因此梯度也应该是对所有样本梯度的平均。这是一个容易忽略但至关重要的细节它确保了学习率的意义与batch size无关。db2和db1的np.sum(...) / m也是同理。3.4 训练循环与评估def train(self, X_train, y_train_onehot, X_val, y_val, epochs50, batch_size64): 训练函数包含简单的验证 n_samples X_train.shape[0] train_loss_history [] val_acc_history [] for epoch in range(epochs): # 打乱数据 indices np.arange(n_samples) np.random.shuffle(indices) X_shuffled X_train[indices] y_shuffled y_train_onehot[indices] epoch_loss 0 # 小批量梯度下降 for i in range(0, n_samples, batch_size): X_batch X_shuffled[i:ibatch_size] y_batch y_shuffled[i:ibatch_size] # 前向传播 y_pred self.forward(X_batch) # 计算损失 batch_loss self.cross_entropy_loss(y_pred, y_batch) epoch_loss batch_loss * X_batch.shape[0] # 累加最后再平均 # 反向传播 self.backward(y_batch) # 计算本轮平均训练损失 avg_train_loss epoch_loss / n_samples train_loss_history.append(avg_train_loss) # 在验证集上评估 val_acc self.evaluate(X_val, y_val) val_acc_history.append(val_acc) if epoch % 5 0: print(fEpoch {epoch:3d} | Train Loss: {avg_train_loss:.4f} | Val Acc: {val_acc:.4f}) return train_loss_history, val_acc_history def evaluate(self, X, y): 评估准确率y是原始标签非one-hot y_pred_prob self.forward(X) y_pred np.argmax(y_pred_prob, axis1) accuracy np.mean(y_pred y) return accuracy def predict(self, X): 预测 prob self.forward(X) return np.argmax(prob, axis1)训练流程要点打乱数据每个epoch开始前打乱训练数据防止模型学习到数据顺序带来的偏差这是确保训练效果的基础。小批量梯度下降每次只用一个batch的数据计算梯度并更新参数。相比全批量梯度下降它更快、更省内存并且由于噪声的引入有时能帮助跳出局部最优。相比随机梯度下降batch_size1它计算的梯度方向更稳定。验证集监控每个epoch结束后在未见过的验证集上计算准确率这是判断模型是否过拟合、以及何时停止训练的关键依据。4. 实战调试让BP网络真正跑起来有了代码我们还需要数据、超参数调优和一系列工程技巧才能让这个网络高效、稳定地学习。4.1 数据准备与预处理我们使用经典的MNIST数据集。数据预处理是机器学习项目的重中之重。# 假设已下载MNIST数据文件train-images-idx3-ubyte等 def load_mnist(images_path, labels_path): with open(images_path, rb) as f: magic, num, rows, cols struct.unpack(IIII, f.read(16)) images np.fromfile(f, dtypenp.uint8).reshape(num, rows*cols) with open(labels_path, rb) as f: magic, num struct.unpack(II, f.read(8)) labels np.fromfile(f, dtypenp.uint8) return images, labels # 加载数据 X_train, y_train load_mnist(train-images-idx3-ubyte, train-labels-idx1-ubyte) X_test, y_test load_mnist(t10k-images-idx3-ubyte, t10k-labels-idx1-ubyte) # 数据预处理 # 1. 归一化将像素值从[0,255]缩放到[0,1]或[-1,1]有助于梯度稳定 X_train X_train.astype(np.float32) / 255.0 X_test X_test.astype(np.float32) / 255.0 # 2. 划分验证集从训练集中分出一部分 val_size 10000 X_val, y_val X_train[:val_size], y_train[:val_size] X_train, y_train X_train[val_size:], y_train[val_size:] # 3. 标签one-hot编码 encoder OneHotEncoder(sparse_outputFalse, categoriesauto) y_train_onehot encoder.fit_transform(y_train.reshape(-1, 1)) # 注意验证集和测试集的标签不需要one-hot编码用于评估但反向传播时需要 y_val_onehot encoder.transform(y_val.reshape(-1, 1))预处理的核心思想让输入数据处于一个相对稳定、规整的分布中。归一化可以避免某些维度因数值过大而主导梯度加速收敛。One-hot编码将类别标签转化为模型易于处理的概率分布形式。4.2 超参数调优与训练监控现在我们可以初始化网络并开始训练了。超参数的选择没有银弹需要根据实际情况调整。# 初始化网络 input_size 784 hidden_size 256 output_size 10 learning_rate 0.1 # 初始学习率可以设大一点 model SimpleBPNet(input_size, hidden_size, output_size, lrlearning_rate) # 开始训练 epochs 30 batch_size 128 train_loss_hist, val_acc_hist model.train(X_train, y_train_onehot, X_val, y_val, epochs, batch_size) # 测试集最终评估 test_acc model.evaluate(X_test, y_test) print(f\n最终测试集准确率: {test_acc:.4f})训练过程中我们需要监控两个关键的曲线训练损失曲线理想情况下应平滑下降最终趋于平缓。如果震荡剧烈可能是学习率太大如果下降极慢可能是学习率太小或网络初始化有问题。验证集准确率曲线随着训练进行准确率应上升。当验证准确率不再提升甚至开始下降而训练损失仍在下降时很可能发生了过拟合。我个人的调参经验通常遵循以下顺序先确定一个合适的网络容量隐藏层神经元太少如16会导致欠拟合太多如1024可能导致过拟合且训练慢。256-512是一个不错的起点。调整学习率这是最重要的超参数。可以从0.01、0.1开始尝试。如果训练损失出现NaN爆炸立刻降低学习率如除以10。也可以尝试学习率衰减策略如在验证准确率平台期将学习率减半。调整批量大小较大的batch如256使梯度估计更准训练更稳定但可能收敛到尖锐的极小点较小的batch如32引入噪声可能帮助泛化但训练波动大。通常选择64、128、256。尝试不同的激活函数隐藏层用ReLU及其变种Leaky ReLU, PReLU几乎总是优于Sigmoid/Tanh因为它能缓解梯度消失问题计算也更快。考虑加入正则化如果发现验证集准确率明显低于训练集过拟合可以加入L2权重衰减在损失函数中加入lambda * ||W||^2或者Dropout在训练时随机丢弃一部分神经元。4.3 常见问题排查与解决实录在实际编码和训练中你几乎一定会遇到下面这些问题。这里是我的“踩坑”记录和解决方案。问题1损失Loss变成NaNNot a Number。可能原因1学习率过大。这是最常见的原因。过大的梯度更新会导致参数值爆炸进而使后续计算出现溢出。解决立即停止训练将学习率降低一个数量级例如从0.1降到0.01再试。可能原因2数据未归一化。如果输入特征的尺度差异巨大比如一个特征范围是0-1另一个是0-10000梯度也会不稳定。解决确保所有输入特征被归一化到相近的范围内如[0,1]或均值为0、方差为1。可能原因3权重初始化不当。如果初始权重过大第一层激活值就可能非常大经过多层传播后爆炸。解决使用Xavier或He初始化而不是简单的标准正态分布。可能原因4损失函数中的log(0)。在计算交叉熵损失时如果预测概率y_hat为0log(0)是负无穷。解决在计算log时加一个极小的常数epsilon如np.log(y_hat 1e-8)。问题2训练损失几乎不下降。可能原因1学习率过小。参数更新步伐太慢陷入局部平原。解决增大学习率或使用自适应学习率优化器如我们实现的是SGD可以尝试Adam但理解SGD是基础。可能原因2梯度消失。特别是在使用Sigmoid/Tanh激活的深层网络中梯度在反向传播时连乘小于1的数会指数级衰减到接近0导致底层权重几乎不更新。解决使用ReLU及其变体激活函数使用残差连接ResNet思想仔细检查梯度计算代码是否正确。可能原因3错误的梯度实现。这是自己实现反向传播时最容易出错的地方。调试方法实现梯度检查。使用数值梯度通过微小扰动参数计算损失的变化与你反向传播计算的解析梯度进行对比。如果两者差异很大说明你的反向传播代码有bug。这是调试神经网络代码的终极利器。问题3模型在训练集上表现很好但在验证集上准确率低过拟合。可能原因模型过于复杂记住了训练数据的噪声而非一般规律。解决收集更多数据最有效的方法但通常不易实现。降低模型复杂度减少隐藏层神经元数量或层数。正则化L2正则化在损失函数中加入所有权重平方和的一项惩罚大的权重。这会使权重趋向于较小的值模型更平滑。Dropout在训练时以前向传播时随机将一部分神经元如50%的输出置零。这强迫网络不依赖于任何单个神经元学习更鲁棒的特征。注意在测试时所有神经元都参与预测但权重需要乘以Dropout保留概率如0.5以保持输出期望一致。早停持续监控验证集损失当验证损失不再下降反而开始上升时停止训练。问题4训练过程波动很大损失曲线上下震荡。可能原因1批量大小太小。小批量估计的梯度噪声大。解决适当增大batch size。可能原因2学习率太大。解决降低学习率或使用学习率衰减计划。可能原因3数据没有充分打乱。如果每个batch的数据都来自同一个类别或具有某种顺序梯度更新方向会有偏。解决确保每个epoch前彻底打乱训练数据。5. 超越基础BP算法的局限与现代优化我们实现的简单BP网络在MNIST上可能达到97%以上的准确率但这离解决现实世界的复杂问题还差得很远。理解经典BP的局限才能明白现代深度学习技术为何如此发展。5.1 梯度消失与梯度爆炸问题这是困扰传统BP神经网络尤其是使用Sigmoid激活函数的致命问题。回顾链式法则δ_l δ_{l1} · W_{l1}^T * f‘(z_l)。如果权重W初始化得较小且f‘(z)对于Sigmoid最大值0.25也小于1那么梯度在反向传播时会层层衰减导致网络浅层的权重更新非常缓慢几乎学不到东西——这就是梯度消失。反之如果权重初始化过大梯度会层层放大导致参数更新步长巨大引发数值溢出——这就是梯度爆炸。解决方案激活函数革命ReLU及其变种Leaky ReLU, PReLU的导数在正区间恒为1彻底解决了连乘导致的梯度衰减问题是深度学习复兴的关键技术之一。权重初始化技巧Xavier初始化配合Tanh和He初始化配合ReLU根据前一层的神经元数量来调整初始权重的方差确保前向传播时信号方差稳定反向传播时梯度方差也稳定。归一化技术批量归一化在每一层的激活前将数据归一化为均值为0、方差为1。这不仅能加速训练还能让网络对初始权重不那么敏感并具有一定的正则化效果同时也在一定程度上缓解了梯度问题。残差连接这是ResNet的核心思想。通过增加一条“快捷路径”让梯度可以直接绕过一些层进行传播极大地缓解了深度网络中的梯度消失问题使得训练成百上千层的网络成为可能。5.2 优化算法的演进从SGD到Adam我们实现的是最基础的随机梯度下降。它简单但存在一些问题学习率难以选择对所有参数使用相同的学习率容易在沟壑状的损失函数中震荡。更先进的优化器动量法不仅考虑当前梯度还累积之前的梯度方向作为“动量”有助于加速在正确方向的收敛并抑制震荡。想象成小球滚下山坡有了惯性。AdaGrad/RMSProp为每个参数自适应地调整学习率。对于频繁更新的参数通常对应重要的特征给予较小的学习率对于不常更新的参数给予较大的学习率。Adam结合了动量法和RMSProp的思想是目前最常用、默认效果往往不错的优化器。它计算梯度的一阶矩估计动量和二阶矩估计自适应学习率并进行偏差校正。在实际项目中我们通常直接使用框架提供的Adam优化器它省去了大量调参工作。但理解SGD和动量法的原理对于调试和理解训练过程依然至关重要。5.3 从全连接到卷积BP思想的延伸我们实现的网络是全连接网络其参数量巨大784*256 256*10 ≈ 20万且忽略了图像的空间局部性。卷积神经网络通过两个核心思想解决了这个问题局部连接每个神经元只与上一层的局部区域连接大幅减少参数量。权值共享同一个卷积核在图像上滑动提取相同类型的特征进一步减少参数量并增强了平移不变性。CNN的反向传播原理与全连接BP完全相同只是计算更复杂因为涉及卷积操作和池化操作的反向传播。现代框架的自动微分完美地处理了这些细节。重要的是理解BP算法是CNN、RNN乃至Transformer所有模型训练的通用底层引擎。手动实现一个BP神经网络就像学开车先学手动挡。它让你深刻理解离合器、变速箱和发动机是如何协同工作的。虽然现在自动挡深度学习框架是主流但掌握手动挡的原理能让你在车子出现异常时知道问题可能出在哪里而不是束手无策。当你下次在PyTorch中调用loss.backward()和optimizer.step()时希望你的脑海中能清晰地浮现出误差如何从损失函数开始沿着计算图一路回溯精细地调整网络中每一个权重的画面。这种深刻的理解是你从深度学习“使用者”迈向“创造者”的关键一步。
返回列表