ARTICLE DETAIL

资讯详情

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

手写BP神经网络:从公式推导到头歌实验全解析

手写BP神经网络:从公式推导到头歌实验全解析 1. 先摸清“头歌机器学习 --- 神经网络”这个实验在考什么头歌实践教学平台上的机器学习实验题目往往起得特别朴素“神经网络”这种就属于典型的“名字越短坑越多”。你要是直接奔着把代码填完、拿满分去大概率会漏掉实验真正想让你掌握的东西。先说结论这类实验通常不是让你从零搓一个深度学习框架也不会让你直接调用现成的库完事。它更接近“带约束的动手实现”——平台会给一个半成品代码框架把前向传播、反向传播、梯度更新这些关键位置挖空让你补全。有些版本还会要求用 BP 神经网络拟合一元函数曲线比如拟合 y sin(2πx) 或者带噪声的二次函数另一些版本可能要求完成一个简单的二分类任务数据是二维平面上的点集类别是线性不可分的。无论哪种底层考察的都是同一套能力前馈神经网络的结构设计、前向传播公式、BP 算法推导、梯度下降实现。我当时刷这个实验的时候正好也在准备机器学习期末考试所以顺手把西瓜书、邱锡鹏老师的《神经网络与深度学习》对应章节都过了一遍。回头看这个实验其实是一条很清晰的“分水岭”能独立做完的人说明对神经网络的基本功是扎实的做不完只靠搜答案的后面学到卷积神经网络、RNN、Transformer 都会觉得地基是虚的。这个实验适合谁来看呢三类人。第一类是在头歌平台刷题、卡在神经网络实验上的学生可以直接对着后面的实现步骤和排查思路走第二类是正在准备机器学习期末或者在看吴恩达机器学习、周志华西瓜书但缺少动手环节的人第三类是想搞清楚“BP 到底怎么反向传播”的初学者这篇文章会把符号定义、公式推导和代码一一对应上尽量做到不靠死记硬背。还有个容易被忽略的点头歌平台判分用的是预先写好的测试用例它会调用你实现的函数比对输出值或训练后的模型效果。这意味着你的代码不仅要“能跑”还要“输出格式正确”“数值误差在允许范围内”。很多人明明算法写对了最后卡在打印多余字符、返回了错误形状、或者浮点误差超了阈值这类细节上。这些我也会在后面单独拿出来说。2. 前馈神经网络与 BP 算法把公式和直觉对齐2.1 从感知机到多层网络为什么要“深”一点先聊一个最基础的问题为什么单层感知机不够用1969 年 Minsky 和 Papert 在《Perceptrons》里已经指出单层感知机连异或XOR这种简单的非线性函数都学不出来。原因很直接单层感知机只能学到一个线性决策边界它本质上就是在一个特征空间里画一条直线或者一个超平面而 XOR 的两个类别在二维平面里是交错分布的没有任何一条直线能把它们分开。加上隐藏层之后就不一样了。隐藏层的作用可以理解成“特征变换”网络不再直接拿原始输入做判断而是先把输入映射到一个新的特征空间在这个新空间里类别可能就变得线性可分了。神经网络的万能近似定理进一步告诉我们只要隐藏层神经元数量足够多一个带非线性激活函数的前馈网络就可以逼近任意复杂的连续函数。这就是“头歌”实验里让你用神经网络拟合曲线的理论底气。讲个小类比如果说单层感知机是一个只会“一刀切”的厨师那带隐藏层的神经网络就是一个会先处理食材、再摆盘、最后做造型的厨师团队。隐藏层的每一层神经元都在对上一层的输出做加工加工完之后输出层只需要学会“在最终形态上切一刀”就够了。2.2 前向传播的完整过程前馈神经网络Feedforward Neural Network的结构一般长这样输入层 - 若干个隐藏层 - 输出层。数据从输入层进入经过每一层的线性变换和非线性激活逐层向前传递最后在输出层得到预测值。这个过程叫前向传播Forward Propagation。我用一个具体的例子来定义符号后面所有公式和代码都基于这套符号假设我们要拟合一个一元函数输入 x 是一个实数希望网络输出一个实数 y_pred。网络结构设为“1-8-8-1”输入层 1 个神经元隐藏层 1 有 8 个神经元隐藏层 2 有 8 个神经元输出层 1 个神经元。每一层的计算可以拆成两步第一步是线性变换也叫加权求和。比如第一层隐藏层的第 j 个神经元它的输入是z_j^{(1)} w_{1j}^{(1)} * x b_j^{(1)}这里 w_{1j}^{(1)} 表示输入层第 1 个神经元也就是 x连接到第一隐藏层第 j 个神经元的权重b_j^{(1)} 是偏置。如果输入是向量比如一张图片拉平成 784 维那一个隐藏层所有神经元的线性输出可以写成矩阵形式Z^{(l)} W^{(l)} A^{(l-1)} b^{(l)}其中 A^{(l-1)} 是上一层激活后的输出W^{(l)} 是第 l 层的权重矩阵b^{(l)} 是偏置向量。第二步是激活。线性变换的结果如果不加非线性那不管叠多少层整个网络本质上还是线性的这就退化了。所以每一层线性变换后都要过一个非线性激活函数比如 Sigmoid、tanh 或者 ReLUA^{(l)} f(Z^{(l)})把这两步交替执行从输入层一直算到输出层就得到了最终的预测值 y_pred。这就是“前馈”的含义——数据只往前走没有反馈连接。激活函数的选择是有讲究的。早年 BP 网络最爱用 Sigmoid因为它把输出压到 (0,1) 区间很符合“概率”的直觉而且导数形式好算sigmoid(x) sigmoid(x) * (1 - sigmoid(x))。但 Sigmoid 有两个问题输出不是以 0 为中心的会让梯度更新效率变低而且两端导数几乎为 0深层网络中容易梯度消失。后来大家更多用 tanh以 0 为中心性能比 Sigmoid 好再后来是 ReLU计算简单、稀疏激活收敛快。头歌实验里如果题目限制只能用 Sigmoid那你就老老实实用 Sigmoid如果没限制拟合小规模曲线用 tanh 通常效果更好收敛更快。2.3 反向传播到底在“传播”什么反向传播BackpropagationBP是整个实验的核心也是最容易把人绕晕的地方。我见过不少同学代码能跑通但你问他“为什么这么求梯度”他是答不上来的。一句话解释BP 是在用链式法则计算损失函数对每个参数的偏导数然后让参数沿着负梯度方向更新。我们用一个具体例子推一遍。假设用均方误差作为损失函数L (1/2) * (y_pred - y_true)^2为什么很多教材在前面乘一个 1/2纯粹是为了求导方便2 次方求导后出来的系数 2 和 1/2 约掉不乘也无所谓。现在看输出层的权重怎么更新。设输出层的线性输出为 z_out激活函数为 f_out拟合任务中输出层通常不用激活函数直接输出 z_out则 y_pred z_out若输出层有激活则 y_pred f_out(z_out)原理相同。损失 L 对输出层某个权重 w_out 的梯度按链式法则拆成两步∂L/∂w_out (∂L/∂y_pred) * (∂y_pred/∂w_out) (y_pred - y_true) * A_hidden其中 A_hidden 是最后一个隐藏层的输出也就是输出层的输入。这个式子特别关键它告诉我们输出层权重的梯度大小由“预测误差”和“该权重对应的输入”共同决定。预测错得越离谱梯度越大参数更新越快——这正是神经网络学习的朴素逻辑。再看隐藏层的权重。假设我们要更新第一隐藏层某个权重 w_hidden链式法则要跨更多层∂L/∂w_hidden (∂L/∂y_pred) * (∂y_pred/∂z_hidden2) * (∂z_hidden2/∂A_hidden1) * (∂A_hidden1/∂z_hidden1) * (∂z_hidden1/∂w_hidden)这里一层比一层长。BP 算法聪明的做法是先算出输出层的“误差项”δ_out然后逐层往前传每一层只需要用上一层的误差项和本层的导数就能算出本层的误差项再拿它去算梯度。用公式表示δ^{(l)} (W^{(l1)})^T δ^{(l1)} * f(z^{(l)})∂L/∂W^{(l)} δ^{(l)} * (A^{(l-1)})^T这就实现了梯度的“反向传播”误差从输出层出发一层层往回传每经过一层就乘上权重的转置和激活函数的导数路径上的每个参数都能拿到属于自己的梯度。我最初学这块时容易犯的一个错误是符号记混。后来我找到一个笨办法随便挑一个具体的小网络比如 1-2-1 结构手推一遍所有参数的梯度再和矩阵形式对照一下就通了。建议你看完这篇之后也手动推一遍不要偷懒。这个能力在期末考试时尤其救命因为计算题大概率就是让你推一个三层网络的梯度。3. 从零手写一个 BP 神经网络拟合曲线实战3.1 实验目标与环境准备这一节我直接复现头歌实验的典型任务用 BP 神经网络拟合目标函数y sin(2πx) 0.3 * cos(5πx)这个函数是多个频率的叠加曲线弯来弯去线性模型根本不可能拟合好很适合检验神经网络的表达能力。实验中 x 取值范围设为 [-1, 1]训练样本加一点高斯噪声模拟真实数据的观测误差。环境只需要 Python NumPy Matplotlib不需要 PyTorch 或 TensorFlow。为什么不用深度学习框架因为实验考察的是你对 BP 算法的理解框架一封装梯度计算就变成黑盒了。手写 NumPy 版虽然代码多一点但能让每个变量的形状和每一步的计算过程都暴露在眼前出了 bug 也好排查。如果你非要用 MATLAB 实现思路完全一样只是矩阵运算语法换成 MATLAB 风格。不少学校期末上机就是用 MATLAB 写 BP 拟合曲线原理是通用的。3.2 数据生成与预处理先生成实验数据。样本量 200x 在 [-1, 1] 上均匀采样y 按函数计算后加入标准差为 0.05 的高斯噪声。然后按 8:2 的比例切分成训练集和验证集验证集用来观察模型是否过拟合。import numpy as np import matplotlib.pyplot as plt def target_func(x): return np.sin(2 * np.pi * x) 0.3 * np.cos(5 * np.pi * x) np.random.seed(42) n_samples 200 x np.linspace(-1, 1, n_samples).reshape(-1, 1) y target_func(x) 0.05 * np.random.randn(n_samples, 1) # 划分训练集和验证集 indices np.random.permutation(n_samples) train_count int(n_samples * 0.8) train_idx, val_idx indices[:train_count], indices[train_count:] x_train, y_train x[train_idx], y[train_idx] x_val, y_val x[val_idx], y[val_idx]归一化这一步容易被新手跳过但我建议不要跳。虽然这里 x 范围是 [-1, 1]y 范围大约在 [-1.3, 1.3]看起来已经比较温和但把 y 也做一下归一化到 [-1, 1] 区间能减少训练初期梯度过大的风险让收敛更稳。如果是真实数据集特征尺度差异可能很大不归一化的话梯度下降会在某些维度上震荡损失很难降下去。3.3 搭建网络结构与初始化网络结构定为 1-8-8-1输入层 1 个神经元两个隐藏层各 8 个神经元输出层 1 个神经元。隐藏层激活函数用 tanh输出层不加激活函数回归任务的标准做法。权重初始化我用了“Xavier 初始化”的简化版对于第 l 层的权重矩阵用均值为 0、标准差为 sqrt(1 / fan_in) 的正态分布随机初始化。fan_in 指的是该层输入神经元的数量。也就是说第一层隐藏层的权重矩阵形状是 (1, 8)fan_in 1标准差就是 1.0第二层隐藏层的权重矩阵形状是 (8, 8)标准差就是 sqrt(1/8) ≈ 0.354。为什么要控制初始化范围如果初始化权重太大神经元很容易落在 tanh 的饱和区导数接近 0梯度一传就消失模型根本学不动。如果太小信号在层层传递中会衰减深层网络也学不动。Xavier 初始化的思想就是让每一层输出的方差保持稳定既不爆炸也不消失。input_dim 1 hidden_dim 8 output_dim 1 def init_weights(input_dim, hidden_dim, output_dim): W1 np.random.randn(input_dim, hidden_dim) * np.sqrt(1.0 / input_dim) b1 np.zeros((1, hidden_dim)) W2 np.random.randn(hidden_dim, hidden_dim) * np.sqrt(1.0 / hidden_dim) b2 np.zeros((1, hidden_dim)) W3 np.random.randn(hidden_dim, output_dim) * np.sqrt(1.0 / hidden_dim) b3 np.zeros((1, output_dim)) return W1, b1, W2, b2, W3, b3偏置全部初始化为 0 是安全的。权重不初始化为 0 的原因大家应该都知道如果所有权重都是 0同一层所有神经元的梯度完全一样更新后依然一样网络就退化成只有一种“隐神经元”表达能力等于零。3.4 前向传播与反向传播实现接下来是核心代码。我在每个函数里都写上形状注释方便你对照矩阵维度排查 bug。前向传播def forward(X, W1, b1, W2, b2, W3, b3): # X: (batch_size, 1) Z1 X.dot(W1) b1 # (batch_size, 8) A1 np.tanh(Z1) Z2 A1.dot(W2) b2 # (batch_size, 8) A2 np.tanh(Z2) Z3 A2.dot(W3) b3 # (batch_size, 1) y_pred Z3 cache (X, Z1, A1, Z2, A2, Z3) return y_pred, cache反向传播def backward(y_pred, y_true, cache, W1, W2, W3): X, Z1, A1, Z2, A2, Z3 cache m y_true.shape[0] dz3 y_pred - y_true # (batch_size, 1) dW3 A2.T.dot(dz3) / m # (8, 1) db3 np.sum(dz3, axis0, keepdimsTrue) / m # (1, 1) da2 dz3.dot(W3.T) # (batch_size, 8) dz2 da2 * (1 - np.tanh(Z2) ** 2) # tanh 的导数, (batch_size, 8) dW2 A1.T.dot(dz2) / m # (8, 8) db2 np.sum(dz2, axis0, keepdimsTrue) / m # (1, 8) da1 dz2.dot(W2.T) # (batch_size, 8) dz1 da1 * (1 - np.tanh(Z1) ** 2) # (batch_size, 8) dW1 X.T.dot(dz1) / m # (1, 8) db1 np.sum(dz1, axis0, keepdimsTrue) / m # (1, 8) return dW1, db1, dW2, db2, dW3, db3代码里的核心地方都踩过坑必须说明几点。第一为什么 dz3 y_pred - y_true 不再乘 2/m因为前面损失函数用的是 L (1/2m) * sum((y_pred - y_true)^2)求导后 1/2 和 2 抵消了剩下 (y_pred - y_true)/m所以写除法的时候除以 m。有些教程里损失函数前不乘 1/2那梯度就要多一个系数 2。这两种写法最后都能收敛差别只是学习率的刻度不同但你不搞清楚就会觉得“为什么我的梯度和书上差了一个系数”。第二tanh 的导数是 1 - tanh^2。使用 Sigmoid 时导数是 a(1-a)用 tanh 时是 1-a^2。这个式子里面的 a 是激活之后的值不是激活前的 z。很多新手在这里把 Z2 和 A2 搞混一旦写错梯度方向就全乱了。第三梯度除以 m 是对批量样本的平均这样损失函数变成“平均损失”梯度大小与 batch size 无关。如果是随机梯度下降一次只取一个样本就不用除以 m。3.5 训练循环与收敛效果训练参数这样设置学习率 0.1迭代 3000 轮批量大小 32每轮随机抽一批样本计算梯度并更新参数。学习率这个值不是随便定的我试过 0.5损失在初期疯狂震荡降不下去试过 0.01收敛太慢3000 轮不够用。0.1 对这个网络结构和归一化后的数据来说既稳定又够快。完整训练过程def train(X_train, y_train, X_val, y_val, epochs3000, lr0.1, batch_size32): W1, b1, W2, b2, W3, b3 init_weights(input_dim, hidden_dim, output_dim) train_losses, val_losses [], [] n X_train.shape[0] for epoch in range(epochs): perm np.random.permutation(n) X_shuffled X_train[perm] y_shuffled y_train[perm] for i in range(0, n, batch_size): X_batch X_shuffled[i:ibatch_size] y_batch y_shuffled[i:ibatch_size] y_pred, cache forward(X_batch, W1, b1, W2, b2, W3, b3) dW1, db1, dW2, db2, dW3, db3 backward(y_pred, y_batch, cache, W1, W2, W3) W1 - lr * dW1 b1 - lr * db1 W2 - lr * dW2 b2 - lr * db2 W3 - lr * dW3 b3 - lr * db3 if epoch % 100 0: y_pred_train, _ forward(X_train, W1, b1, W2, b2, W3, b3) y_pred_val, _ forward(X_val, W1, b1, W2, b2, W3, b3) train_loss np.mean((y_pred_train - y_train) ** 2) val_loss np.mean((y_pred_val - y_val) ** 2) train_losses.append(train_loss) val_losses.append(val_loss) print(fepoch {epoch}, train_loss {train_loss:.4f}, val_loss {val_loss:.4f}) return W1, b1, W2, b2, W3, b3, train_losses, val_losses实际跑下来loss 曲线是这样的走势前 500 轮从 0.6 左右快速降到 0.05之后缓慢下降到 2000 轮左右基本稳定在 0.005 量级。验证集 loss 与训练集 loss 差距很小说明没有明显过拟合。把预测曲线和真实曲线叠加画出来能看到网络已经很好地拟合了目标函数的起伏。有个小细节值得提头歌实验的判题环境不允许你打印太多内容打印多了或者 print 内容格式不对可能会被判输出格式错误。虽然训练过程中的打印方便你本地观察但最终提交前记得把打印语句删掉或者注释掉。3.6 头歌平台上的“填空式”提交技巧头歌的实验通常不给完整的 main 文件而是让你在规定区域补全代码。观察下来常见的挖空位置有这几个forward 函数、backward 函数、更新参数的三行代码、以及激活函数的选择。在这种平台上有个容易吃大亏的点平台会用一个你完全看不到的测试脚本调用你写的函数传入的数组形状可能和你本地假设的不一样。比如你本地一直按 (1, 8) 的输入形状训练平台测试时传进去的却可能是 (8, 1) 的。这时候如果不做形状兼容处理矩阵乘法直接报错。我自己的做法是在函数开头加两行 reshape把输入统一成 (batch_size, input_dim) 的形状然后再继续往下算。比如if X.ndim 1: X X.reshape(1, -1) if y_true.ndim 1: y_true y_true.reshape(-1, 1)还有一点平台判分时对数值误差的容忍度通常设在 1e-4 或 1e-5 级别。这意味着你不能用随机梯度下降之后的结果直接提交而要保证模型已经充分收敛。如果本地 loss 还在下降就急着交很可能因为误差超阈值被判错。建议训练轮数宁可多设一些跑完看 loss 曲线已经平了再提交。4. 常见问题与排查技巧实录这个实验的报错归纳起来就那么几大类。下面按我实际遇到的频率排个序。4.1 矩阵维度不匹配这是出现频率最高的问题。前向传播阶段还好因为有明确的公式Z XW b只要照着写形状一般不会错。最容易出错的是反向传播阶段的 dW A.T.dot(dz)。我的排查方法是在每次反向传播前打印所有相关矩阵的形状对照我之前给的形状注释逐行比对。核心规则只有一条dW 的形状必须和 W 的形状完全一致否则更新参数时形状就对不上。所以你先确定 W 的形状是 (fan_in, fan_out)再倒推 dW 也应该是 (fan_in, fan_out)就知道 A.T.dot(dz) 的顺序了。4.2 loss 不降或者下降极慢原因通常有三个。一是数据没归一化特征范围差异大导致梯度路径曲折二是学习率太小参数更新像蜗牛爬三是初始化范围太小梯度信号在初始阶段就几乎为 0。排查顺序建议是先检查归一化再调学习率最后检查初始化标准差。还有一个非常常见的原因是激活函数用错了输出层如果还带着 Sigmoid而标签值范围是 [-1.3, 1.3]Sigmoid 输出上限是 1模型永远学不到落在 1 以上的值loss 会一直卡在一个平台。4.3 训练集 loss 低验证集 loss 高过拟合的标志。8 个神经元的隐藏层在这个拟合任务里过拟合概率不大但如果你把隐藏层神经元加到 64 个或者训练轮数拉到 2 万过拟合就来了。解决办法从三个方向入手降低模型容量减少神经元数量、加正则项在 loss 里加一项 λ*sum(W^2)通常 λ 取 0.001 左右、引入早停验证集 loss 连续若干轮不降就停止训练。期末考试如果考简答题问“如何防止神经网络过拟合”你就答这三板斧再补一句数据增强基本满分。4.4 梯度消失或梯度爆炸用 Sigmoid 的时候梯度消失很常见因为 Sigmoid 导数最大只有 0.25多层链式相乘以后梯度直接趋近于 0。解决方法最有效的一招就是把激活函数换成 tanh 或 ReLU其次是使用 Xavier 初始化再配合归一化。梯度爆炸相对少见一旦出现loss 会直接变成 nan这种情况检查学习率是不是设大了、数据是不是有异常大值。4.5 平台上输出格式不匹配这种报错和代码逻辑无关纯粹是格式问题。譬如 local_test.py 期望你的函数返回一个 (n,) 形状的数组你返回的是 (n,1)或者某些题要求把训练好的权重 W1 保存为 .npy 文件你再另外打印了东西这些都会导致判分失败。技巧仔细看题目给的函数签名和返回值示例先设计一个假的返回值让平台跑通“格式”再替换成真实计算。这样可以先把格式问题排除再专注算法逻辑。4.6 手推梯度对不上如果你在实现过程中想验证自己的反向传播逻辑是否正确可以用数值梯度检验gradient checking。原理很简单用差分近似代替解析梯度比较两者是否一致。def numerical_gradient(f, params, idx, epsilon1e-6): params_plus params.copy() params_minus params.copy() params_plus[idx] epsilon params_minus[idx] - epsilon return (f(params_plus) - f(params_minus)) / (2 * epsilon)对每个参数把解析梯度和数值梯度做相对误差比较如果误差小于 1e-6说明反向传播写对了。这个技巧本身也是面试题常客用熟了之后遇到任何自定义网络结构你都敢说“我可以手写 BP”。5. 从“能跑通实验”到“真正理解神经网络”5.1 为什么图像处理不用前馈神经网络头歌实验里网络结构是 1-8-8-1输入是一维标量。可到了图像场景很多人会问为什么不直接全连接前馈网络偏要用 CNN核心原因是参数量。一张 32x32x3 的图片拉平后是 3072 维。如果第一层隐藏层有 256 个神经元那第一层的权重矩阵就有 3072x256 ≈ 78.6 万个参数。这还只是第一层再往后参数量直接爆炸训练需要的数据量和计算量根本不现实。卷积神经网络用“局部连接 权值共享”解决了这个问题。卷积核只在局部感受野上滑动同一张特征图的所有位置共享同一组权重参数量大幅减少同时天然具备平移等变性——猫往左移几个像素特征依然能被同一个卷积核检测到。这就是图像处理用 CNN 而不用普通前馈网络的根本原因。5.2 序列数据为什么走向 RNN 和 Transformer前馈神经网络还有个硬伤它不知道“顺序”。你把一个句子的词序打乱喂进普通前馈网络只要特征向量不变输出就不变。但自然语言、语音、股票价格这些数据顺序就是信息本身。RNN 的解决思路是把上一个时间步的隐藏状态拼进当前时间步的输入形成一个“记忆通道”。但 RNN 长距离依赖问题严重训练时梯度在时间维度上反复相乘容易爆炸或消失。LSTM 引入了门控机制让信息可以选择性遗忘和更新缓解了长程问题。再到 Transformer直接用注意力机制取代循环结构让任意两个位置可以直接交互并行性也更好。理解这些演进脉络你会发现自己做过的 BP 实验其实是一切的起点——只要你会推梯度就能理解这些复杂模型的梯度怎么流动。5.3 实验之后还可以往哪些方向扩展读完这块你可以做几个渐进式的练习帮自己把知识体系搭起来。第一个练习把拟合曲线的网络改造成多分类器用 MNIST 手写数字数据。任务变成 784-64-10 结构输出层用 Softmax损失函数换成交叉熵。你会发现反向传播的推导比回归复杂一截但核心还是链式法则。第二个练习自己实现一个 mini-batch 梯度下降的变体比如加动量Momentum或 RMSProp。头歌实验一般只要求基本的梯度下降但面试和考试里常常会问“动量是怎么解决局部最小值和平坦区域问题的”动手实现一遍体会完全不同。第三个练习读一读邱锡鹏《神经网络与深度学习》的前四章配合西瓜书第五章“神经网络”把符号系统统一。这两本书的推导风格正好互补一个偏数学严谨一个偏直觉举例。我看下来觉得搭配食用效果最好。第四个练习如果你还有精力可以自己写一个极简的文件存取系统把训练好的权重保存下来再写一个 predict 函数读取权重做推理。这件事头歌实验没要求但解释了“模型部署”的第一步——训练时产出权重文件部署时加载权重跑前向传播。整个机器学习应用流程里的这个环节很多学生直到毕业都没亲手做过很可惜。5.4 我对这个实验的最终体会刷完“头歌机器学习 — 神经网络”这个实验我最大的感受是神经网络的学习过程并不神秘它不过是在做一件非常简单的事——不断调整参数让预测值往真实值的方向靠一点。BP 算法之所以看起来复杂是因为它同时要处理很多参数的偏导数一旦你静下心来选一个 1-2-1 的小网络手推一遍迷雾就散了。还有一点是针对考试的。西电、山大、国科大这些学校的机器学习期末神经网络几乎是必考内容。考来考去就是三种题型给定网络结构让你手算前向传播和反向传播、让你比较不同激活函数的优劣、让你设计网络结构解决某个任务。这些能力不是背出来的都是像这个实验一样一笔一笔算、一行一行写代码才能练出来的。最后分享一个小经验做这类实验别急着打开搜索引擎找代码先自己推三遍公式再闭卷写一遍实现。写完和参考实现比对你会发现自己对“为什么这么写”的理解深入了一大截。
返回列表