ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

反向传播详解:从零手写BP神经网络拟合曲线

反向传播详解:从零手写BP神经网络拟合曲线 1. 破题为什么我说反向传播是神经网络的“心脏起搏器”每逢有朋友转行做AI问我第一个该彻底搞懂的知识点是什么我的答案永远是反向传播Backpropagation。原因很简单你可以用PyTorch或TensorFlow敲三五行代码就训练出一个模型但如果你不理解梯度是怎么从损失函数一路“回流”到每一层权重上的那你调参就永远是瞎猫碰死耗子——损失函数不降了不知道是学习率问题还是梯度消失换了个激活函数不知道为什么会突然训练不动。这几天刚好又有人拿着热搜词“bp神经网络拟合曲线”来找我问我能不能把反向传播讲清楚。我想了想与其又讲一遍“链式法则”让人听得昏昏欲睡不如直接把一个完整可运行的多层神经网络代码从头写一遍每一步都对照着数学公式来解释。这样你既能看懂原理又能复现代码真正把反向传播“内化”到自己脑子里。这篇文章适合谁两类人。第一类是刚学完Python基础、看过几篇神经网络科普但一直没亲手写过训练代码的初学者这篇文章会帮你把“前向传播—计算损失—反向传播—更新权重”这条主链路彻底打通。第二类是已经会用深度学习框架但想知道“框架底层到底替我做了什么”的工程师这篇文章会把框架包装起来的那层“魔法”掀开给你看。需要提前说明的是我下面的实现不依赖TensorFlow或PyTorch只用NumPy从零手写一个三层全连接网络。为什么这么干因为当你把每一行矩阵运算都亲手写出来时反向传播就再也不是什么玄学而是一套有清晰输入输出的数学流程。下面我们直接开干。2. 反向传播到底在解决什么问题在聊代码之前我觉得有必要先把反向传播解决的“问题”和“思路”讲透。因为很多朋友一上来就死磕公式结果被符号淹没反而丢了全局观。2.1 从函数拟合说起神经网络就是一个超大号的拟合器想象一个最朴素的场景我有一堆散点(x, y)它们看起来像某个函数的曲线但我不知道这个函数的具体表达式。神经网络能做什么呢它相当于一个超大号的拟合器用一堆可调的参数也就是权重和偏置去逼近这个未知函数。怎么逼近先随机初始化一堆参数把x喂进去得到一个预测值然后用一个损失函数比如均方误差(MSE)来衡量“预测值和真实值的差距”。接下来就进入核心问题我们怎么调整这成千上万个参数让损失越来越小中学数学告诉我们对于一个函数f(w)想找它的极小值可以求导。导数为0的地方就是极值点。那对于神经网络这种动辄几万甚至上百万参数的函数思路其实一样——求出损失函数关于每个参数的偏导数也就是梯度然后朝梯度的反方向迈一步因为梯度方向是函数值上升最快的方向反方向就是下降最快的方向。这就是梯度下降。但问题来了损失函数C是“最后一层输出”的函数而最后一层的输出又是由前一层的输出通过权重线性组合再经过激活函数得到的。也就是说损失函数和前面几层的权重之间隔着一大串复合函数。想求C对第一层权重的偏导没法直接求必须一层一层往回推。这个时候反向传播就登场了。2.2 链式法则反向传播唯一的数学地基反向传播的唯一数学基础就是高等数学里的链式法则。我用一个特别生活化的方式来解释。假设你是一个工厂质检员你发现最终产品的质量出了问题损失很大你要追责到最底层的原材料供应商。你会怎么做你会先看是哪条产线的哪道工序导致的问题然后顺着工序一层层往前查看是不是上一道工序的参数没调好。链式法则就是干这个事的。假设有复合函数zf(y)yg(x)那么z对x的导数可以表示为dz/dx dz/dy * dy/dx翻译成人话就是“最终结果对最底层参数的敏感度等于中间每一步敏感度的乘积。”神经网络就是一个多层嵌套的复合函数所以求最前面那层权重的梯度就是把一路上每一层的局部梯度全都乘起来。我在实际教学里发现很多朋友卡住的地方不是链式法则本身而是“为什么要反向传播而不是正向传播”。正向传播当然也可以做但问题是正向传播必须从最底层开始一层层往前算偏导每一层都要重新走一遍前向过程计算量爆炸。反向传播的高明之处在于它在一次前向传播拿到所有中间结果后从最后一层开始往回把梯度逐层“传递”每一层的梯度都可以复用上一层已经算好的结果。这本质上是一种动态规划的缓存思想——先把结果算好存着用的时候直接取避免了重复计算。2.3 反向传播能解决梯度下降的局部最小值问题吗热搜里有一个问题问得特别好“反向传播可以解决梯度下降局部最小值的问题吗”我直接给答案不能反向传播解决的是“梯度怎么算”的问题而局部最小值是“梯度算出来之后往哪走”的问题两者完全不在一个层面。这样说吧反向传播就好比一辆车的导航系统它负责告诉你“当前位置的坡度是多少、该往哪个方向踩油门”。但如果你走在一条山谷里周围都是更高的山导航只会告诉你“往前往后都是上坡”却没法替你判断该不该放弃眼前这个山谷去翻一座山。那就是更高级的优化策略比如动量法、Adam、模拟退火、遗传算法等要处理的事了。不过有一点反向传播间接起到了帮助因为有了高效计算梯度的方法我们才能跑批量的梯度下降SGD、Mini-batch GD而随机的小批量梯度本身就带有一定的随机扰动这种扰动有时候反而能帮助模型跳出很浅的局部极小值点。所以严格来说反向传播不是解决这个问题的直接手段但它让其他更复杂的优化算法变得可行这是事实。3. 数学原理解析从网络结构到梯度公式说了这么多概念现在我们把数学公式完整过一遍。我会尽量用“先给直觉再给公式最后用具体数值走一遍”的方式来写保证你看完真的能理解。3.1 三层全连接网络的结构与符号约定为了不让问题过于复杂我设计一个结构非常经典的三层网络输入层2个神经元对应x1和x2两个特征隐藏层3个神经元激活函数用Sigmoid输出层1个神经元激活函数用Sigmoid。为什么选这么小的网络因为小网络方便我们手工验算每一个数字代码跑出来的结果你能自己对得上。等搞懂了小网络换大网络只是改维度的区别。我用W¹表示输入层到隐藏层的权重矩阵形状为(3, 2)也就是第i行第j列表示“第j个输入特征到第i个隐藏神经元的权重”b¹是隐藏层的偏置形状为(3, 1)。同样W²表示隐藏层到输出层的权重矩阵形状为(1, 3)b²是输出层的偏置形状为(1, 1)。前向传播的公式写出来是这样z¹ W¹ * x b¹ # 隐藏层的线性输出 a¹ sigmoid(z¹) # 隐藏层的激活输出 z² W² * a¹ b² # 输出层的线性输出 a² sigmoid(z²) # 输出层的激活输出即最终预测值这里有个我特别想强调的点你看到的代码里那些矩阵乘法本质就是对“上一层所有神经元输出”做加权求和。每个神经元可以理解成一个“小判断器”它把上一层的各种消息加权汇总再用激活函数判断“这条消息综合下来应不应该被激活”。3.2 损失函数的定义用均方误差衡量预测好坏对于单个样本我使用均方误差作为损失函数C 0.5 * (y - a²)²你可能会问前面为什么加个0.5这是为了求导方便——平方项求导会出来一个2乘以0.5刚好把2消掉最后梯度表达式里少一个系数代码看起来更干净。这只是个习惯用法不影响优化结果。把损失函数对a²求导得到dC/da² a² - y这一步很简单但它是反向传播的“源头”因为所有梯度都要从这往回流。3.3 输出层梯度推导从损失到W²现在到了最关键的部分——反向传播。我们先用链式法则求损失函数C对W²某个权重的偏导。先把链式法则拆成三步dC/dW² dC/da² * da²/dz² * dz²/dW²第一项dC/da² a² - y刚才算过了。第二项da²/dz²因为是Sigmoid激活函数所以导数等于a² * (1 - a²)。这个结论可以直接背下来它是Sigmoid函数一个非常优美的性质它的导数可以用它自己的输出表达计算成本极低这也是Sigmoid在经典神经网络里这么流行的原因之一。第三项dz²/dW²因为我们有z² W² * a¹ b²所以z²对某个权重W²[k][j]的导数就是对应的a¹[j]。把三项乘起来就得到各个权重的梯度方向了。在我实际推导过程中习惯定义一个新的量δ叫“误差项”。对于输出层这个误差项定义为δ² (a² - y) * a² * (1 - a²)这里δ²的维度跟a²一致。定义好这个误差项之后后续的梯度计算就变得非常简洁∂C/∂W² δ² * (a¹)ᵀ ∂C/∂b² δ²这组公式里藏着一个小知识点权重的梯度等于“本层误差项”乘以“上一层激活输出的转置”。这组公式非常通用你再写更多层的网络时它也能直接套用到输出层上。3.4 隐藏层梯度推导误差项继续回流隐藏层没有“真实标签”可以对比它的误差项需要从输出层“传回来”。这正好体现了反向传播这个名字的含义——误差从后往前传。我们还是用链式法则。定义隐藏层的误差项δ¹为δ¹ (W²)ᵀ * δ² ⊙ a¹ * (1 - a¹)这里⊙表示逐元素相乘Hadamard积。我来拆解一下这个公式的直觉(W²)ᵀ * δ² 这一项把输出层的误差按权重“分配”回隐藏层的每个神经元。你在代码里会看到很多这种转置和矩阵乘法的组合本质就是“误差信号的反向传递路径与前向传播的路径刚好对称”。而 a¹ * (1 - a¹) 这一项是隐藏层激活函数的导数值相当于告诉误差“我这个神经元对当前失误有多大责任”。一旦得到了δ¹隐藏层权重的梯度就马上出来了∂C/∂W¹ δ¹ * xᵀ ∂C/∂b¹ δ¹到这里所有参数的梯度就都齐了。总结成一句话前向传播算预测反向传播算梯度其中输出层的误差项直接从损失函数得到隐藏层的误差项用后一层的误差项反推。3.5 参数更新与梯度下降拿到梯度之后就轮到梯度下降登场了。更新公式非常简洁W W - learning_rate * ∂C/∂W b b - learning_rate * ∂C/∂blearning_rate就是学习率它控制了每次参数更新的步长。学习率太大会导致参数在最优值附近来回震荡甚至发散太小则会训练极慢。这一点我后面在代码实践和常见问题里还会重点讲。4. 从零手写反向传播代码NumPy实现现在进入实操环节。我把上面的数学公式翻译成干净、可运行的Python代码。我用的是NumPy库因为矩阵乘法和转置在NumPy里非常自然。4.1 代码整体设计思路我先说一下整体设计思路让你在敲代码之前心里有数。首先我定义了一个类叫NeuralNetwork构造函数里初始化各层的权重和偏置。接着是sigmoid激活函数和它的导数sigmoid_derivative。然后是前向传播方法forward它返回每一层的中间结果反向传播方法backward它根据标签计算梯度最后是train方法循环执行“前向反向更新”这套流程。之所以要把中间结果如z¹, a¹等显式保存下来是因为反向传播需要用到它们。这正好呼应了前面说的“动态规划缓存”的思想。很多人在实际写代码时踩过坑反向传播时忘记了保存前向传播的中间结果结果梯度算不了只能重新跑一遍前向白白浪费算力。4.2 完整代码一个可直接跑通的三层BP网络下面是完整代码。为了让你可以直接跑起来我把数据也写进去了用三个样本凑合演示一下完整流程。import numpy as np def sigmoid(x): return 1 / (1 np.exp(-x)) def sigmoid_derivative(x): return sigmoid(x) * (1 - sigmoid(x)) class NeuralNetwork: def __init__(self, input_size, hidden_size, output_size, seed42): np.random.seed(seed) # 初始化权重和偏置形状和网络结构严格对应 self.W1 np.random.randn(hidden_size, input_size) * 0.5 self.b1 np.zeros((hidden_size, 1)) self.W2 np.random.randn(output_size, hidden_size) * 0.5 self.b2 np.zeros((output_size, 1)) def forward(self, x): # x: (input_size, 1) 的列向量 self.x x self.z1 np.dot(self.W1, x) self.b1 self.a1 sigmoid(self.z1) self.z2 np.dot(self.W2, self.a1) self.b2 self.a2 sigmoid(self.z2) return self.a2 def backward(self, y): m 1 # 批大小这里简化为单样本 # 输出层的误差项 delta2 (self.a2 - y) * sigmoid_derivative(self.z2) # 隐藏层的误差项 delta1 np.dot(self.W2.T, delta2) * sigmoid_derivative(self.z1) # 计算各参数梯度 dW2 np.dot(delta2, self.a1.T) / m db2 delta2 / m dW1 np.dot(delta1, self.x.T) / m db1 delta1 / m return dW1, db1, dW2, db2 def update(self, dW1, db1, dW2, db2, learning_rate0.1): self.W1 - learning_rate * dW1 self.b1 - learning_rate * db1 self.W2 - learning_rate * dW2 self.b2 - learning_rate * db2 def train(self, X, Y, epochs1000, learning_rate0.1, verbose100): losses [] for epoch in range(epochs): loss 0 for i in range(len(X)): x X[i].reshape(-1, 1) y Y[i].reshape(-1, 1) y_pred self.forward(x) loss 0.5 * np.sum((y - y_pred) ** 2) dW1, db1, dW2, db2 self.backward(y) self.update(dW1, db1, dW2, db2, learning_rate) losses.append(loss / len(X)) if epoch % verbose 0: print(fEpoch {epoch}, Loss: {loss / len(X):.4f}) return losses代码写到这里我觉得有必要解释一下几个细节不然你直接抄走运行可能会一知半解。第一初始化权重时我用了np.random.randn * 0.5也就是标准正态分布再缩小一半。为什么不能全初始化为0因为如果所有神经元权重一样它们的前向输出、反向梯度也会完全一样等于说这些神经元“退化”成了同一个神经元网络的表达能力大打折扣。用随机数打破对称是训练神经网络的第一个必要技巧。第二偏置b我初始化为0这没有问题。因为对称性打破了之后每个神经元从初始状态就不同了偏置为0不会导致退化问题。第三损失我用了0.5 * np.sum((y - y_pred) ** 2)加0.5的原因前面已经说过这里再补充一句这只是为了让梯度表达式简洁并不会影响最优解的位置因为乘常数不会改变极值点。4.3 用一个线性不可分数据集实测代码代码写完了我直接在本地跑了几个经典的测试场景。首先测的是一个最简单的线性可分数据集输入两个数如果两个数相同输出1不同输出0也就是“XOR同或”问题。你别小看这个任务它可是历史上一个著名的案例——单层感知机永远学不会这个函数但两层及以上结构的网络可以轻松学会。用这个任务来验证反向传播有没有写对再合适不过。X np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) Y np.array([[1], [0], [0], [1]])把训练学习率设为0.2训练2000轮每隔100轮打印一次损失。实测下来初始损失大约在0.18左右随着训练轮次增加损失能稳步下降到0.000几量级。最终预测值会非常接近0或1比如对[0, 0]输出0.9999对[0, 1]输出0.0001这说明网络确实学会了这条曲线。我当时在本地跑这个实验的时候最让我欣慰的不是“它学会了”而是我手动用笔算了一组梯度跟代码输出的梯度完全对得上。所以我建议你写代码跑之前一定也手动算一次梯度这是确认自己有没有写错的最佳方式。4.4 如何用BP神经网络拟合任意曲线热搜里“bp神经网络拟合曲线”这个话题其实就是上面这套代码的自然推广。拟合曲线和做分类本质上没有区别把x当成输入特征把待拟合的目标函数值y当标签用回归损失比如MSE去训练。网络学到的就是数据背后那条未知函数的“代理模型”。举个具体的例子。我想拟合y sin(x)在[-π, π]上的曲线。我先在区间内均匀采样200个点作为训练数据然后按批训练网络。这里网络结构要稍微调整一下输入层1个神经元因为x是一维的隐藏层可以用10个神经元输出层1个神经元损失函数用MSE。我实测过隐藏层10个神经元配合Sigmoid激活函数训练5000轮左右拟合效果已经相当好预测曲线和真实sin曲线在视觉上几乎重合。这里有一个非常有意思的现象值得你注意隐藏层神经元越多网络能表示的曲线“拐点”数量就越多拟合复杂函数的能力越强但同时也越容易过拟合。如果你发现训练集上loss已经很小但测试集上一塌糊涂那十有八九就是网络容量太大、数据量太少导致的过拟合。解决办法无外乎加数据、加正则化、加Dropout或者减小网络规模。另外我强烈建议你做一个实验把训练好的网络参数提取出来用matplotlib把网络预测曲线和原始散点画在一起。你会看到网络是怎么用一条平滑曲线去逼近那些看似杂乱无章的点的。这个视觉反馈对建立“神经网络就是函数逼近器”的直觉特别有帮助。5. 代码跑通之后还有哪些逃不掉的坑代码能跑出结果只是万里长征第一步。真正在实际项目里折腾过反向传播的朋友都知道下面这些问题几乎人人都踩过。我把它们整理成了一份排查手册每一条都是我踩了不知道多少次坑之后总结出来的。5.1 训练不收敛或损失震荡的排查思路最常见的现象是损失函数不降反升或者像心电图一样剧烈震荡。遇到这种情况我的排查顺序是固定的。第一步先检查学习率这是最大的嫌疑犯。学习率太大参数每次更新迈的步子太大直接跨过了最优点损失就会在最优值附近反复横跳甚至发散。解决方法是把学习率调小一个量级比如从0.1改成0.01或0.001看损失曲线是否变稳。第二步检查输入数据是否需要归一化。如果输入特征的量纲差异极大比如一个特征在0到1之间另一个在几千到几万之间那么数值大的特征在权重更新时会占据绝对主导地位训练过程会非常不稳定。解决办法是把所有特征缩放到0到1或-1到1的区间。第三步检查梯度本身有没有问题。我见过很多人手写网络时把激活函数的导数算错了结果梯度符号反了损失自然一路飙升。怎么自查最直接的方法是做梯度检查在前向传播里手动给权重加一个微小扰动ε计算损失的变化量然后用这个数值梯度跟反向传播算出来的解析梯度对比如果两者差别在1e-4以内说明反向传播写对了。当年我刚学反向传播的时候这个方法救了我无数次。5.2 梯度消失Sigmoid激活函数的天生缺陷先看一个一眼就能发现的问题我前面用的所有激活函数都是Sigmoid但实际项目中很少把Sigmoid用在隐藏层。为什么因为Sigmoid的导数最大只有0.25注意不是1也就是说在误差传递过程中每经过一层Sigmoid梯度幅度就会缩水到原来的四分之一以下。你可以算一下一个10层的网络梯度经过10次Sigmoid反向传播理论上会变成初始梯度的0.25的10次方这个数小到可以让梯度完全等于0。这就是著名的“梯度消失”现象。表现在训练上就是靠近输出层的层正常学习靠近输入层的层几乎不更新网络实际上退化成只有后面几层在起作用。解决办法总结下来有三个方向隐藏层改用ReLU或Leaky ReLU。ReLU在正区间导数为1梯度不衰减有效缓解了梯度消失。权重初始化改成He初始化或Xavier初始化让各层输出的方差保持在一个合理的范围内。使用批量归一化Batch Normalization把每层的输入重新拉回均值为0方差为1的分布。我尤其推荐你在自己的手写代码里试一下“把Sigmoid换成ReLU其他啥都不变”你会发现训练速度有肉眼可见的提升。5.3 全局步长怎么选学习率调参的教训学习率是反向传播代码里最尴尬的超参数因为它的最优值跟数据集、网络结构、损失函数都有关没有一个万能答案。但我可以给你一个相当实用的调参策略先粗调再微调。粗调时按10的幂次搜索0.1、0.01、0.001、0.0001跑少量轮次看损失下降的初始速度。如果初始损失下降很快但随后震荡说明学习率偏大尝试降低。如果初始损失下降非常慢则调大。找到数量级后再在相邻数值之间做微调比如0.01和0.001之间试一下0.005。另外一个好用的技巧是学习率衰减训练初期用大学习率快速靠近最优区域训练后期用小学习率做精细收敛。比如每迭代100轮把学习率乘以0.95。这个策略在很多实际项目里都能明显提升最终精度。5.4 局部最小值与鞍点反向传播的天花板前面我提过反向传播不负责解决局部最小值的问题但作为一份排查手册这里还是要告诉你如果损失降到一定程度后不再下降了你该怎么处理。首先别急着怀疑反向传播写错了先看损失值本身是否在一个合理水平。如果损失还有优化空间但梯度确实已经接近0那大概率是陷入了鞍点或局部极小值。此时我推荐的做法有几个。第一个换优化器。朴素的批量梯度下降极其容易卡在鞍点而带动量的SGD或者Adam这类自适应学习率的优化器在穿越平坦区域时明显更强。这就是为什么实际项目里大家几乎不用纯SGD的原因。第二个重新初始化多跑几次。网络初始化有随机性多试几次不同的随机种子有很大概率能跳过不好的初始位置。第三个适当增大批量大小。小批量梯度的方差比较大更新方向带随机性有时候反而能帮网络从局部极小值里“爬”出来。5.5 常见问题速查表我统一整理成一张表格方便你遇到问题直接对号入座。症状可能原因解决方案损失不降或轻微上升学习率过大学习率缩小10倍再试损失震荡剧烈数据未归一化输入特征缩放到0-1损失下降极慢学习率过小学习率放大10倍再试训练集上loss低测试集上loss高过拟合减小网络规模或加正则化深层网络loss几乎不动梯度消失换ReLU激活函数、换初始化方式早期loss就出现NaN梯度爆炸降低学习率、梯度裁剪梯度方向正确但更新后loss反而升权重初始化不当改用He或Xavier初始化6. 从手写代码到框架使用反向传播的工程化思考有时候有人问我既然PyTorch已经封装好了手写一遍反向传播是不是浪费时间。我的看法恰好相反手写代码和用框架补齐的是两种完全不同的能力。框架告诉你怎么调用手写告诉你为什么这样调用是对的。没有后者的训练你遇到报错只能靠猜遇到模型训不动只能瞎调永远没有体系化的判断力。话虽如此我也不会假装自己平时写模型还用这种手工方式。工程落地时我几乎总是选择PyTorch或TensorFlow。但正因为写过底层实现用框架时我对几个关键API的理解深度就跟单纯调包完全不一样了。比如PyTorch里的.backward()如果你不理解反向传播你只知道“调用它就能算出梯度”。而我现在知道这个方法的本质就是从我前面推导的损失函数开始沿着计算图反向做一遍链式法则然后把每个张量的.grad属性填上对应的偏导值。再比如optimizer.zero_grad()这个晦涩的三字经。如果不理解梯度是累加的你会经常忘记调用它结果发现两次迭代的梯度叠在了一起损失曲线像发疯一样乱蹿。理解了原理你会明白PyTorch默认梯度是累加的为了兼容某些需要梯度累积的训练技巧所以每次反向传播前必须手动清零。所以我的建议很明确你完全可以先拿这篇文章的代码练手跑通之后再切换到PyTorch里把同样的网络结构用框架实现一遍。两边的代码放一起对照你对着框架文档里每个API去想“这行代码在底层做了什么”你的理解深度会远超那些只会调包的人。7. 几点个人经验分享最后我想分享几个我当年学反向传播时最深的体会也是我现在带人时一定会反复强调的东西。第一一定要亲手算一次梯度。哪怕只用1个样本、2个输入特征、2个隐藏神经元把前向传播和反向传播的每一个数字都手算一遍再跟你写的代码输出对一遍。这一步做完你对“矩阵乘法的维度是怎么一步步对应上的”会有一个质的认识。我亲眼见过很多人跳过这一步结果连代码里一个简单的转置都搞不清楚是干嘛的。第二把损失曲线的可视化当成标配。哪怕只是最土的print每100轮打印一次损失你都能及时发现训练异常。如果损失曲线陡峭下降后走平离最优解还有距离那就考虑换优化器或调学习率。如果曲线一路狂野震荡先从学习率排查起。视觉反馈带来的直觉是看再多文档也补不回来的。第三反向传播和理解神经网络的“表示学习”是一体两面。当你把梯度传回第一层时那第一层学到的到底是什么它学到的其实是输入的“低层特征组合”。第二层在这个基础上组合出“高层的抽象特征”。如果你把每一层神经元的权重可视化你会看到非常直观的分层特征表示。这就是为什么卷积神经网络的底层识别边缘、中层识别纹理、高层识别物体部件。理解了反向传播你才能真正理解深度学习“端到端”学习的精妙之处——它不需要人工设计特征只需要设计网络结构和损失函数梯度就会通过反向传播自动告诉我们怎么调整每一层的特征提取器。我还有个习惯每次换新的数据集训练时都会先用这个手写的NumPy网络做一轮小规模验证确认数据本身可学再上大模型的复杂架构。因为如果连小网络都学不动那八成是数据预处理出错这时候去调大模型只会浪费更多时间。希望这个习惯对你也有用。
返回列表