ARTICLE DETAIL

资讯详情

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

Python手写BP神经网络:反向传播每一步的代码详解

Python手写BP神经网络:反向传播每一步的代码详解 简介面向机器学习初学者与Python开发者的BP神经网络实现资料主题是用Python从零搭建多层前馈神经网络。文档先梳理网络基本结构包括输入层、隐藏层与输出层的单元设置以及Theta1、Theta2权重矩阵的含义再讲解Sigmoid激活函数、交叉熵代价函数和正则化项的计算方式最后结合反向传播算法给出核心推导与可运行的代码实现。资源打包为1个PDF文档大小373KB内含关键代码片段和注释可直接对照学习或复用到分类项目中。目前已有6975人学习下载。读者借助这份材料可以搞清BP神经网络的前向传播、误差反向传播与权重更新流程理解如何通过正则化防止过拟合也能据此实现多类分类模型。整体紧凑实用适合算法入门、课程设计与项目参考。1. BP神经网络不是黑匣子这份Python代码把反向传播每一步都摊开了BP神经网络在机器学习里是个经典得不能再经典的模型但真让你不调库、用Python手写一遍很多人卡在反向传播的矩阵维度上。这份资源不是贴一段调包代码而是把神经网络从代价函数、前向传播、反向传播到梯度检查全部拆开每一步都有可运行的Python实现。它解决的不是“怎么调用”而是“推导怎么落到代码”为什么Theta1是隐藏层数×(输入数1)为什么反向传播要先算输出层误差为什么权重不能全零初始化。适合正在学机器学习原理、想用代码验证BP推导或者要在作业/实训里手写神经网络的读者。代码里的注释和函数边界都标得很清楚照着跑一遍很多“玄学”就变成具体张量。2. 三层网络结构与代价函数先把前向传播和正则化算明白2.1 网络尺寸与权重矩阵的维度约定代码里默认的三层网络是“输入层-隐藏层-输出层”的结构。输入层有input_layer_size个特征单元隐藏层有hidden_layer_size个单元输出层有num_labels个类别单元。每一层除了特征单元还会额外补一个偏置单元bias通常设为1。这个偏置单元不接收上一层信号只负责给线性变换加一个可学习的偏移让模型不会被迫穿过原点。权重矩阵的维度是这份代码最容易踩坑的地方。Theta1表示输入层到隐藏层的权重它的形状是hidden_layer_size × (input_layer_size 1)Theta2表示隐藏层到输出层的权重形状是num_labels × (hidden_layer_size 1)。为什么列数多1因为要对应偏置单元特征维度和偏置拼接在一起后才能做矩阵乘法。在后续的checkGradient里作者用了一个小网络输入3维、隐藏层5个单元、输出3类、样本数5。这个尺寸故意设置得很小就是为了让数值梯度算得快一点。如果你要用自己的数据先确认input_layer_size是特征个数不包含偏置num_labels是类别数比如手写数字0-9就是10而不是9。我见过有人把num_labels设成9结果预测时少了一类训练集准确率怎么都上不去。更麻烦的是这种错误在代价函数里不会直接报错只会表现为模型预测结果总是缺一个类别。2.2 代价函数代码拆解标签映射与正则化项的坑多分类问题不能直接拿y值去算损失要先做one-hot映射。代码里用class_y[:,i] np.int32(yi).reshape(1,-1)把每个样本的标签变成0/1向量。这个reshape(1,-1)很关键因为yi返回的是bool数组直接赋值到二维列上会形状不匹配加上reshape才能广播进去。如果你在复现时发现赋值报错先检查这一行。正则化项的处理同样有讲究。Theta1_x Theta1[:,1:]和Theta2_x Theta2[:,1:]把权重矩阵的第一列偏置对应的权重去掉因为正则化只惩罚真正的特征权重偏置项不参与。然后term np.dot(...)把两段权重向量化后求内积等价于sum(Theta1_x**2) sum(Theta2_x**2)。代码写成向量内积是为了和后续梯度展开保持一致的形状读起来不如np.sum直观但结果一样。下面给出代价函数的完整实现对应资源里的nnCostFunctiondef nnCostFunction(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda): # 从参数向量中还原两个权重矩阵 length nn_params.shape[0] Theta1 nn_params[0:hidden_layer_size*(input_layer_size1)].reshape( hidden_layer_size, input_layer_size1) Theta2 nn_params[hidden_layer_size*(input_layer_size1):length].reshape( num_labels, hidden_layer_size1) m X.shape[0] class_y np.zeros((m, num_labels)) # 把y映射成one-hot向量 for i in range(num_labels): class_y[:, i] np.int32(y i).reshape(1, -1) # 去掉正则化不参与的第一列 Theta1_x Theta1[:, 1:Theta1.shape[1]] Theta2_x Theta2[:, 1:Theta2.shape[1]] # 计算所有非偏置权重的平方和 term np.dot( np.vstack((Theta1_x.reshape(-1,1), Theta2_x.reshape(-1,1))).T, np.vstack((Theta1_x.reshape(-1,1), Theta2_x.reshape(-1,1))) ) # 前向传播 a1 np.hstack((np.ones((m,1)), X)) z2 np.dot(a1, Theta1.T) a2 sigmoid(z2) a2 np.hstack((np.ones((m,1)), a2)) z3 np.dot(a2, Theta2.T) h sigmoid(z3) # 交叉熵代价 正则项 J -( np.dot(class_y.reshape(-1,1).T, np.log(h.reshape(-1,1))) np.dot((1-class_y).reshape(-1,1).T, np.log(1-h.reshape(-1,1))) - Lambda*term/2 ) / m return np.ravel(J)这段代码有几个参数值得说明。Lambda是正则化系数代价函数里它出现在减号后面所以Lambda越大代价被惩罚得越厉害。如果你发现代价变成负的或者忽大忽小先检查是不是少除了m或者term的形状不是标量。h是前向传播的输出形状是m×num_labelsclass_y也是同样形状这样np.log(h)和np.log(1-h)才能逐元素运算。注意这里的class_y.reshape(-1,1).T是1×mn的行向量np.log(h.reshape(-1,1))是mn×1的列向量两者点乘得到一个标量正是所有样本所有类别的交叉熵之和。2.3 前向传播的矩阵写法别漏了偏置列前向传播的代码在代价函数和梯度函数里都会出现套路固定每次进入下一层之前先给当前激活值左侧补一列1。a1 np.hstack((np.ones((m,1)), X))就是给原始特征加偏置a2经过sigmoid后再补一列1变成a2的输入形状对应隐藏层到输出层的偏置。漏掉第二步的hstack是新手最常见的错误结果就是np.dot维度对不上报错说“shapes not aligned”。sigmoid函数的定义也很简单标准写法是1/(1np.exp(-z))。在使用时注意z可以是矩阵np.exp会对每个元素求指数所以整个函数不需要额外写循环。前向传播的三步核心是线性变换z a·Theta.T、激活a sigmoid(z)、补偏置a hstack(ones, a)。顺序不能乱尤其是补偏置必须在线性变换之前完成否则矩阵乘法根本没法算。如果你把每一步的shape写在本子上会看得更清楚输入X是m×3补偏置后a1是m×4Theta1是5×4z2是m×5a2补偏置后是m×6Theta2是3×6z3是m×3h是m×3。所有矩阵乘法都是“左边样本数右边参数列”只要shape对得上前向传播就不会错。3. 反向传播求梯度链式法则的代码落地3.1 误差从输出层往回传的向量化思路反向传播的目的只有一个求代价函数对每个权重的偏导。代码实现里先算输出层的误差delta3 h - class_y这是交叉熵损失对输出层线性输出z3的导数。接着算隐藏层误差delta2 np.dot(delta3, Theta2_x) * sigmoidGradient(z2)。注意这里的Theta2_x是去掉偏置列的因为偏置单元没有从上一层来的输入反向传播时不需要回传它的误差。np.dot(delta3, Theta2_x)的维度和z2不一致delta3是m×3Theta2_x是3×5去掉了偏置列相乘得到m×5正好对应隐藏层m个样本、5个单元。然后再逐元素乘上sigmoidGradient(z2)这个导数是sigmoid在z2处的斜率用来把“输出误差”折算成“本层误差”。如果某点的z2很大或很小sigmoid导数接近0误差就会被挡住这就是梯度消失的雏形。sigmoidGradient(z)是sigmoid的导数数值上等于sigmoid(z)*(1-sigmoid(z))。代码里直接调用这个函数没有单独实现可以在前面定义一下def sigmoidGradient(z): return sigmoid(z) * (1 - sigmoid(z))3.2 反向传播代码逐行说明完整梯度函数如下我加了注释方便你对照。def nnGradient(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda): # 还原Theta1、Theta2形状和代价函数里一致 length nn_params.shape[0] Theta1 nn_params[0:hidden_layer_size*(input_layer_size1)].reshape( hidden_layer_size, input_layer_size1) Theta2 nn_params[hidden_layer_size*(input_layer_size1):length].reshape( num_labels, hidden_layer_size1) m X.shape[0] class_y np.zeros((m, num_labels)) for i in range(num_labels): class_y[:, i] np.int32(y i).reshape(1, -1) # 去掉偏置列后续计算误差矩阵时用 Theta1_x Theta1[:, 1:Theta1.shape[1]] Theta2_x Theta2[:, 1:Theta2.shape[1]] Theta1_grad np.zeros(Theta1.shape) Theta2_grad np.zeros(Theta2.shape) # 先把Theta的第一列置零保证正则化梯度不对偏置生效 Theta1[:, 0] 0 Theta2[:, 0] 0 # 前向传播和代价函数完全一样 a1 np.hstack((np.ones((m,1)), X)) z2 np.dot(a1, Theta1.T) a2 sigmoid(z2) a2 np.hstack((np.ones((m,1)), a2)) z3 np.dot(a2, Theta2.T) h sigmoid(z3) # 初始化误差矩阵 delta3 np.zeros((m, num_labels)) delta2 np.zeros((m, hidden_layer_size)) # 逐样本累加梯度教学式写法便于理解 for i in range(m): delta3[i, :] h[i, :] - class_y[i, :] Theta2_grad np.dot(delta3[i, :].reshape(1,-1), a2[i, :].reshape(1,-1)) delta2[i, :] np.dot(delta3[i, :].reshape(1,-1), Theta2_x) * \ sigmoidGradient(z2[i, :]) Theta1_grad np.dot(delta2[i, :].reshape(1,-1), a1[i, :].reshape(1,-1)) # 最终梯度数据项 正则项再除以样本数 grad (np.vstack((Theta1_grad.reshape(-1,1), Theta2_grad.reshape(-1,1))) Lambda * np.vstack((Theta1.reshape(-1,1), Theta2.reshape(-1,1)))) / m return np.ravel(grad)这段代码里有几个参数要留意。Theta1_grad和Theta2_grad的形状和对应权重矩阵完全一致初始化全零。在累加之前Theta1[:,0]0是提前把偏置列清掉这样后面加正则项时Lambda*Theta不会对偏置列产生梯度。逐样本的for i in range(m)是复杂度为O(m)的循环数据量大时跑得慢但它把整个累加过程摊开了适合验证逻辑。工程上可以完全向量化但那份代码里保持循环形式老读者看起来更好理解。如果你把delta2的计算拆开看会发现它就是链式法则的一个手写版本输出层误差经过Theta2_x加权回传再乘上sigmoid在z2处的导数得到隐藏层误差。这个Theta2_x必须是不含偏置列的因为偏置列对应的权重只连接隐藏层偏置单元而那个单元没有回传的误差源。如果这里用了完整Theta2数值梯度检查会立刻暴露问题。3.3 梯度检查用数值梯度验证BP写没写对反向传播代码写完后最怕的是代价正确但梯度算错。作者在资源里专门放了checkGradient用导数的定义来验证BP结果。原理很简单对每个参数θ计算(J(θe)-J(θ-e))/(2e)这就是数值梯度如果BP梯度正确两者应该非常接近。为了避免数值计算太慢检查时用了一个小网络输入层3、隐藏层5、输出层3、样本数5Lambda0先不掺正则。代码构造了debugInitializeWeights生成非对称的初始权重确保每个神经元不会退化成相同输出。核心循环如下def checkGradient(Lambda0): input_layer_size 3 hidden_layer_size 5 num_labels 3 m 5 initial_Theta1 debugInitializeWeights(input_layer_size, hidden_layer_size) initial_Theta2 debugInitializeWeights(hidden_layer_size, num_labels) X debugInitializeWeights(input_layer_size - 1, m) y 1 np.transpose(np.mod(np.arange(1, m 1), num_labels)) y y.reshape(-1, 1) nn_params np.vstack((initial_Theta1.reshape(-1,1), initial_Theta2.reshape(-1,1))) grad nnGradient(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) num_grad np.zeros(nn_params.shape[0]) step np.zeros(nn_params.shape[0]) e 1e-4 for i in range(nn_params.shape[0]): step[i] e loss1 nnCostFunction(nn_params - step.reshape(-1,1), input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) loss2 nnCostFunction(nn_params step.reshape(-1,1), input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) num_grad[i] (loss2 - loss1) / (2 * e) step[i] 0 res np.hstack((num_grad.reshape(-1,1), grad.reshape(-1,1))) print(res)e1e-4是数值微分的典型步长太大导数误差大太小容易出现数值精度损失。np.hstack把数值梯度和BP梯度并排打印你只需要看两列是否一致。通常误差在1e-5以内就说明BP写对了。如果相差很大优先检查反向传播里的Theta2_x是否去偏置以及梯度是否除了m。验证正确后正式训练就可以跳过这一步因为数值梯度每算一个参数要跑两遍代价函数开销太大。4. 权重初始化与训练闭环从随机Theta到预测结果4.1 为什么不能全零初始化对称性失效逻辑回归可以把参数全部初始化为0神经网络不行。如果所有权重都一样隐藏层每个神经元会得到相同的输入、输出相同的激活值反向传播时连接到同一层的权重梯度也完全相同。这意味着不管训练多少轮神经元之间始终对称整个网络退化成一个只含一个特征的模型永远学不出多样化的表达。解决方法是随机初始化让每个权重落在零附近的一个小区间内。代码里的randInitializeWeights(L_in, L_out)返回一个形状为L_out × (1L_in)的矩阵第0列对应偏置权重也会被随机赋值。这个做法比全零好得多因为它打破了对称性。4.2 randInitializeWeights的epsilon取值初始化不是随便取一个随机数就行。代码里使用了def randInitializeWeights(L_in, L_out): W np.zeros((L_out, 1 L_in)) epsilon_init (6.0 / (L_out L_in)) ** 0.5 W np.random.rand(L_out, 1 L_in) * 2 * epsilon_init - epsilon_init return Wepsilon_init sqrt(6/(L_outL_in))是Xavier初始化的简化版。这个值的含义是让每层权重的方差控制在和输入/输出规模相关的范围内避免信号在层层传递时过大或过小。如果权重初始太大sigmoid会进入饱和区梯度接近零学习几乎停滞如果初始太小反向传播的信号也会很弱。np.random.rand生成[0,1)均匀分布乘以2*epsilon_init再减去epsilon_init就变成[-epsilon, epsilon]的均匀分布。使用这个函数时要注意传入的是“本层单元数”和“下一层单元数”而不是权重矩阵的实际形状。L_in是上一层特征数不包含偏置L_out是当前层单元数函数内部会自动加1作为偏置列。如果你传入的数值反了得到的矩阵形状会不同后续前向传播会报维度错误。调试时最好的办法是先打印Theta1.shape确认是(hidden_layer_size, input_layer_size1)。4.3 predict函数与准确率统计训练完成后用前向传播做预测。代码里的predict(Theta1, Theta2, X)流程和训练时的前向传播一致加偏置、算z2、激活、再补偏置、算z3、激活最后从输出层取每行最大值所在的列号作为类别。def predict(Theta1, Theta2, X): m X.shape[0] num_labels Theta2.shape[0] X np.hstack((np.ones((m,1)), X)) h1 sigmoid(np.dot(X, Theta1.T)) h1 np.hstack((np.ones((m,1)), h1)) h2 sigmoid(np.dot(h1, Theta2.T)) p np.array(np.where(h2[0,:] np.max(h2, axis1)[0])) for i in np.arange(1, m): t np.array(np.where(h2[i,:] np.max(h2, axis1)[i])) p np.vstack((p, t)) return p这个实现用np.where找出最大值位置其中h2[i,:]是第i个样本在每个类别上的概率np.max(h2, axis1)返回每行最大概率。需要注意np.where返回的是元组取[0]才是列索引。循环逐行处理虽然慢但结果直观可以换成np.argmax(h2, axis1)一行搞定效果相同。预测准确率就是mean(p y)不过要先把p的形状对齐到y的维度。资源里在展示“训练集预测准确度”和“归一化后训练集预测准确度”时应该是在同一套训练流程下对比了原始特征和标准化特征的效果。如果你拿这份代码跑自己的数据建议先做特征归一化否则输入范围差异过大会让训练很不稳定。4.4 从展开参数到端到端训练整合调用流程前面的函数都以nn_params一维向量作为输入这是为了配合scipy.optimize等优化器。训练时先初始化initial_Theta1 randInitializeWeights(input_layer_size, hidden_layer_size) initial_Theta2 randInitializeWeights(hidden_layer_size, num_labels) nn_params np.vstack((initial_Theta1.reshape(-1,1), initial_Theta2.reshape(-1,1)))然后把J和grad封装成优化器的回调函数。如果用scipy.optimize.minimize常见写法from scipy.optimize import minimize fun lambda p: nnCostFunction(p, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) jac lambda p: nnGradient(p, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) result minimize(fun, nn_params, methodCG, jacjac, options{maxiter: 200})methodCG是共轭梯度法不需要手动设学习率适合中小规模网络。maxiter控制迭代轮数代价曲线下降缓慢时可以加到500。注意minimize要求fun返回标量nnCostFunction里已经np.ravel(J)成了1维数组但scipy会自动压缩最好再取float(J)。训练完成后从result.x还原Theta1和Theta2再传给predict。这里有个细节还原时要把result.x按和初始化相同的顺序reshape先取前hidden_layer_size*(input_layer_size1)个元素作为Theta1剩下的作为Theta2。很多人直接result.x.reshape(-1, ...)乱试结果维度对不上。只要你始终用同一套展开-还原逻辑就不会出错。5. BP神经网络实现避坑指南几个常见翻车现场这一章不讨论理论直接聊我复现这份代码时踩过的坑。下面的每条都是“现象→原因→解决”的结构如果你在跑别的BP实现遇到类似问题也可以对照排查。注意这里的坑都是实际会发生的不是理论假设。5.1 现象代价函数J(θ)越训越大原因最常见的是学习率设置过大导致参数在最优解周围来回震荡甚至发散其次是特征没有归一化不同维度量级差异让梯度方向被少数特征主导正则化系数Lambda也可能过强导致代价里的惩罚项越训练越大模型反而没学到数据规律。解决先把Lambda设为0排除正则干扰再把学习率调到0.01以下观察代价曲线。如果训练流程里没有单独的学习率而是用高级优化器比如minimize的CG通常不需要手调学习率但如果你是自己写梯度下降务必加一个alpha并逐步衰减。特征归一是必须做的可以把X每个维度减去均值除以标准差再传入网络。我一般还会把代价函数的返回值J打印出来每迭代一轮看一眼如果从第2轮开始就单调上升99%是学习率问题。5.2 现象梯度检查数值梯度与BP梯度差到数量级原因反向传播代码里有一处细节没对齐——Theta2_x本应去掉偏置列但你可能误用了完整的Theta2导致误差回传时把偏置权重也算了梯度另一个高频原因是代价函数J里漏掉了/m或者梯度里漏掉了/m导致数值梯度和BP梯度差一个样本数量的倍数还有可能是数值求导的步长e太大或太小e1e-4通常是最好的折中。解决先在Lambda0下跑梯度检查如果差得多逐行比对nnGradient和nnCostFunction里关于Theta2_x的切片是否一致。再把m单独打印出来确认两个函数里的m是同一个值。如果两列基本接近但后几位有差异可以把e改成1e-5看看但不要小于1e-6否则浮点误差会占主导。我遇到过一种很隐蔽的情况数值梯度用step.reshape(-1,1)改变参数向量但nnCostFunction内部又把nn_params当成1维数组导致切片错位。如果你也这么写记住reshape后的向量在函数内部要重新ravel或保持列向量形状一致。5.3 现象预测结果全是同一个类别原因权重初始化过大使sigmoid输出接近0或1梯度消失网络无法学习或者num_labels虚增了一类导致标签映射错位还有可能是输入X没有补偏置列前向传播从第一层开始就错了。解决检查randInitializeWeights里的epsilon_init是否用了sqrt(6/(L_inL_out))如果直接np.random.randn初始化会让权重方差过大。打印class_y前几行确认每个样本的one-hot位置正确。在predict之前跑一次h2的shape确认输出列数等于实际类别数。如果二分类问题用了num_labels1sigmoid输出和标签映射方式也要相应调整——我建议多分类直接用one-hot二分类也保持one-hot别去特殊处理。5.4 现象正则化项把梯度吞掉了原因Lambda设得过大比如从10开始梯度里正则部分Lambda*Theta远大于数据部分网络为了降低代价干脆把权重全部压向零训练集和测试集准确率都上不去。另一种情况是正则化没有排除偏置列把偏置对应的权重也惩罚了导致网络输出偏移。解决正规做法是梯度里先把Theta1[:,0]和Theta2[:,0]置零再加Lambda*Theta最后除以m。资源代码里已经在nnGradient开头做了置零。调参时先跑Lambda0.01依次试0.1、1如果效果变差就回退。观察训练集准确率和验证集准确率的差距差距大说明过拟合可以加大Lambda差距不大而准确率低说明欠拟合要减小Lambda或者增加隐藏层单元数。5.5 现象梯度检查通过但训练后准确率低原因梯度没问题但网络结构表达能力不够。隐藏层只有几个单元对于手写数字这类非线性很强的任务模型容量不足或者特征是原始像素值范围0-255没有归一化导致反向传播梯度过大或过小还有可能是迭代次数太少代价还没有收敛就停了。解决先把隐藏层单元数提到25或50观察训练集准确率是否明显上升。如果上升说明是容量问题如果没变化再检查特征归一化。迭代次数可以调maxiter到500甚至1000同时监控J的下降曲线确保它已经平坦。一般来说只要梯度检查通过网络结构不太离谱训练集准确率至少能达到90%以上。如果还是低把X的每一列减去均值除以标准差重跑一遍效果立竿见影。6. 训练后还能做什么验证梯度、可视化权重与超参调整技巧6.1 用小型网络验证梯度后再放大规模我拿到这份代码后第一件事不是直接跑手写数字而是先把checkGradient跑通。小网络只要几秒就能验证一旦数值梯度和BP梯度对齐后面训练再出问题就能排除反向传播的错误。放大规模时只改input_layer_size、hidden_layer_size、num_labels不要改网络逻辑本身。这个习惯帮我排掉了很多“看似是优化器问题、其实是梯度问题”的坑。6.2 可视化Theta1权重训练完成后把Theta1去掉偏置列剩下的每一行就是一个隐藏单元学到的“模板”。用手写数字数据集时可以把某一行权重reshape成和图片相同的尺寸用matplotlib的imshow画出来你会看到边缘、笔画等特征。这是判断网络有没有真正学到结构而不是死记硬背的最直观方法。fig, axes plt.subplots(1, hidden_layer_size, figsize(12, 3)) for i in range(hidden_layer_size): weight Theta1[i, 1:] # 去掉偏置列 axes[i].imshow(weight.reshape(img_w, img_h), cmapgray) axes[i].axis(off)如果画出来的权重图全是噪声说明训练可能没收敛或者正则化太强如果隐约能看到笔画结构说明网络学到的特征是有意义的。这个可视化不需要额外数据只要把Theta1导出即可算是一个低成本的“体检”手段。6.3 超参调整的优先级顺序应当是先固定Lambda0调隐藏层单元数让训练集准确率达到可接受范围再开正则化调Lambda控制过拟合最后才考虑学习率、批大小等。隐藏层单元数不是越多越好单元过多容易过拟合而且训练变慢。对于一份教学代码20-50个隐藏单元通常就够用了。有一次我为了赶进度跳过梯度检查直接上大网络结果代价函数一路飙升排查了两小时才发现在nnCostFunction里把Lambda*term/2写成了Lambda*term。从那以后我每次实现BP都会强制跑一遍checkGradient确认两列误差在1e-5以内再开始训练。这份资源里的代码顺序也推荐你这么做代价函数 → 梯度 → 梯度检查 → 随机初始化 → 预测。希望帮到你。本文还有配套的精品资源点击获取
返回列表