资讯动态

深度前馈神经网络原理与NumPy实现:从反向传播到实战调优

发布时间:2026/10/13 18:24:09 来源:尧图企业网站定制
简介这份PDF是“机器学习系列1深度前馈神经网络”的完整讲解面向机器学习初学者与希望系统梳理神经网络基础的开发者重点解决从原理到数学推导再到Python落地之间的断层。内容围绕深度前馈神经网络的定义、变量约束、前向传播、反向传播、数据集划分展开并详细推导交叉熵损失、代价函数、梯度下降及链式法则求导过程帮助读者理解网络每一层如何计算、参数如何更新。资源仅含1个PDF文件压缩包大小1.38MB携带方便适合按章节反复研读。已有340人学习下载。通过这份资料读者可以一次性获取原理解释、公式推导与Python实现思路包括常见激活函数sigmoid、tanh、ReLU的运用、参数初始化方法、反向传播梯度计算细节等对完成分类或回归任务的网络搭建具有直接参考价值。1. 深度前馈神经网络从“万能近似”到可运行的代码这条路线最值得先走通如果你刚接触机器学习面对的第一个“看起来像那么回事”的模型大概率就是深度前馈神经网络Deep Feedforward Neural Network, DFNN。它没有卷积、没有循环、没有注意力就是密密麻麻的神经元一层接一层往前传却在理论上能逼近任何连续函数。这也是为什么几乎所有深度学习框架的入门教程第一个示例都是它。你不需要 GPU不需要分布式环境只要一台普通笔记本就能把原理、公式和代码完整串起来。这篇文章要解决的是三件事第一把“前馈”到底在做什么、激活函数为什么非线性的问题讲透第二把反向传播的公式从矩阵角度一步步推下来不是摆一个结论而是让你能自己写出来第三用纯 NumPy 实现一个可用于二分类和多分类的前馈网络并给出训练曲线和参数调节的实战经验。适合的读者是那些已经看过概念、但还没亲手写过反向传播的人以及那些用框架调参调了很久、想知道黑匣子里到底发生了什么的人。我讲一个非常反直觉的结论当隐藏层足够宽的时候深度前馈网络在中小规模表格数据上往往比你在框架里随手调出来的深层模型更稳原因后面会展开。2. 前馈网络的结构与数学原理为什么“深”和“宽”不是一回事2.1 从单层感知机到多层前馈线性分割的天花板被谁打破单层感知机本质上是一个线性分类器输入向量经过加权求和再过一个阶跃函数输出。它能解决 AND、OR 这类线性可分问题但遇到 XOR 会直接失效因为 XOR 在二维平面上无法用一条直线分割。这个致命缺陷在早期一度让神经网络研究陷入低谷。多层前馈网络的本质改进是在输入和输出之间插入若干隐藏层每一层先做线性变换再经过非线性激活函数。以 XOR 为例一个 2-2-1 结构的网络就能解决第一层把输入映射到另一个特征空间使得原本线性不可分的两类变得线性可分第二层再基于新特征做分类。这个“先变换特征、再分类”的套路是前馈网络的核心思想。如果去掉隐藏层的非线性激活函数多层线性变换堆叠的结果仍然是一个线性变换网络再深也等价于单层。这是理解前馈网络最重要的一个判断标准深度不是靠层数堆出来的而是靠每一层的非线性变换堆出来的。激活函数的选择直接决定网络的表达能力这也是后面调参时第一个要检查的地方。2.2 激活函数的数学角色Sigmoid、Tanh、ReLU 的选型逻辑激活函数的作用是引入非线性但不同函数的梯度特性差别很大。Sigmoid 输出范围在 0 到 1 之间适合作为输出层做二分类概率但它有两个已知问题函数饱和区的梯度几乎为 0反向传播时误差信号逐层衰减深层网络的参数几乎得不到更新也就是梯度消失另外它的输出不是零中心的这会让梯度更新路径出现锯齿效应收敛变慢。Tanh 解决了零中心问题输出在 -1 到 1 之间但仍存在饱和区梯度消失的问题。ReLU 的数学形式是 max(0, z)正区间梯度恒为 1不存在梯度消失问题但负区间梯度为 0一旦某个神经元输出落入负区间它的梯度永远是 0参数就再也更新不了也就是神经元死亡现象。实际工程里隐藏层默认用 ReLU输出层根据任务选择二分类用 Sigmoid多分类用 Softmax。import numpy as np def sigmoid(z): return 1 / (1 np.exp(-z)) def sigmoid_derivative(a): return a * (1 - a) def tanh(z): return np.tanh(z) def tanh_derivative(a): return 1 - a ** 2 def relu(z): return np.maximum(0, z) def relu_derivative(a): return (a 0).astype(float)这段代码的核心是同时缓存了激活值和导数值。反向传播要使用前向传播时的激活值来计算梯度所以实际工程中不会把激活和导数分开写而是前向时同时存下 a 和 z。relu_derivative里注意用astype(float)把布尔数组转成数值否则后续做矩阵乘法时类型会出错。2.3 损失函数与 Softmax分类任务的最后一块拼图二分类用二元交叉熵多分类用交叉熵加 Softmax。Softmax 把网络的原始输出logits转换成概率分布公式是 softmax(z)_i exp(z_i) / sum(exp(z_j))。实现时要防止 exp 溢出先减去最大值再做指数运算数学上等价数值上稳定。def softmax(z): shifted z - np.max(z, axis1, keepdimsTrue) exp_values np.exp(shifted) return exp_values / np.sum(exp_values, axis1, keepdimsTrue) def cross_entropy_loss(y_true, y_pred): m y_true.shape[0] # 加 1e-12 防止 log(0) return -np.sum(y_true * np.log(y_pred 1e-12)) / mnp.max(z, axis1, keepdimsTrue)这一行是数值稳定的关键它把每个样本的最大 logit 减掉结果完全不变但 exp 不会再溢出。1e-12是防御性噪声避免 log 出现无穷大。交叉熵和 Softmax 配合时反向传播的梯度形式会非常简洁这在下一章的公式推导里会看到它的价值。3. 反向传播的公式推导从标量链式法则到矩阵实现3.1 计算图视角下的前向与反向先走一遍再退回去深度前馈网络的训练过程可以拆成两个阶段。前向传播从输入 x 出发逐层计算 z W a b再经过激活函数得到 a activation(z)最后输出层经过 Softmax 得到预测概率。反向传播则从损失函数 L 出发利用链式法则逐层计算参数 W 和 b 的梯度。用计算图来理解最直观前向是沿着图从输入走到输出反向是沿着图从输出走回输入。每一层的梯度计算只依赖两样东西——该层前向时缓存的 z 和 a以及从上一层更靠近输出层传回来的误差信号。这个局部性原则让反向传播的实现变得模块化每一层只需要知道自己的输入、输出和激活函数。我在实现时习惯把每一层的梯度计算封装成独立函数输入是本层缓存和上游梯度输出是本层参数的梯度和传递给下一层的梯度。这样调试时只需要检查一个函数的输入输出不需要在几百行代码里找问题。3.2 输出层到隐藏层的误差传播公式推导设网络的输出经过 Softmax 后得到预测 a_L真实标签为 yone-hot 编码损失函数为交叉熵。可以证明输出层误差 delta_L a_L - y这个形式极其简洁是 Softmax 加交叉熵的组合带来的简化。推导关键点对第 i 个输出softmax 的偏导数需要区分 i 等于 j 和不等于 j 两种情况但当你把所有项累加进交叉熵的梯度里时来自其他输出节点的大部分项会相互抵消最终只留下预测值与真实值的差。这就是为什么实践中很少单独算 Softmax 的雅可比矩阵而是直接用 a_L - y 作为输出层误差。隐藏层的误差传播公式是 delta_l (W_{l1}^T delta_{l1}) * activation(z_l)。这里的转置操作对应着梯度从输出层向输入层“反向流动”的过程。*是逐元素乘法表示经过激活函数的局部梯度。这个公式是反向传播的脊柱后面的代码实现完全建立在这条公式上。3.3 参数梯度矩阵化把循环写成矩阵乘法这就是 NumPy 实现的核心有了每层的误差 delta_l该层参数的梯度就是 delta_l 与上一层激活值 a_{l-1} 的外积。具体来说dW_l (1/m) * delta_l.dot(a_{l-1}^T)db_l (1/m) * np.sum(delta_l, axis1, keepdimsTrue)。这里 m 是批次大小取平均是因为损失函数定义成了均值。def backward(params, caches, X, y): grads {} L len(params) // 2 m X.shape[0] # 输出层误差 A_last caches[A str(L)][A] delta A_last - y # 形状 (m, n_out) for l in reversed(range(1, L 1)): A_prev caches[A str(l - 1)][A] if l 1 else X W params[W str(l)] grads[dW str(l)] delta.T.dot(A_prev) / m grads[db str(l)] np.sum(delta, axis0, keepdimsTrue) / m if l 1: # 继续向更浅层传播误差 dz_prev delta.dot(W) a_prev caches[A str(l - 1)][A] delta dz_prev * relu_derivative(a_prev) return grads这段代码里delta.dot(W)的形状是 (m, n_prev)它把当前层误差投影到上一层神经元的输出维度然后用relu_derivative(a_prev)做逐元素过滤——ReLU 在正区间导数为 1负区间为 0这正好对应前向时哪些神经元被激活。注意这里reversed(range(1, L 1))从输出层往输入层遍历不能写反。4. 用 NumPy 纯手写前馈网络一份可直接运行的最小实现4.1 网络结构与参数初始化小随机数为什么重要又不能太小网络结构用字典描述例如[784, 128, 64, 10]表示输入维度 784、两个隐藏层、输出维度 10。参数初始化最常用的是 He 初始化W 按均值为 0、方差为 2/n_in 的正态分布随机生成b 初始化为零向量。ReLU 配 He 初始化是经验上最稳的组合。如果初始值太大前向传播时激活值会集中在 ReLU 的正区间深处隐藏层输出方差逐步放大反向传播时梯度可能爆炸如果初始值太小所有隐藏层输出都接近 0ReLU 梯度为 0网络根本学不动。用标准差 0.01 的正态分布初始化全连接网络在深层结构上收敛速度很慢但在浅层结构上通常没问题。def initialize_params(layer_dims): np.random.seed(42) params {} L len(layer_dims) - 1 for l in range(1, L 1): # He 初始化除以 sqrt(n_in/2) params[W str(l)] np.random.randn(layer_dims[l], layer_dims[l - 1]) * np.sqrt(2.0 / layer_dims[l - 1]) params[b str(l)] np.zeros((layer_dims[l], 1)) return paramslayer_dims[l]是本层神经元数layer_dims[l-1]是上一层神经元数np.sqrt(2.0 / layer_dims[l-1])就是 He 初始化的缩放因子。这种写法保证了每个隐藏层输出的方差经过 ReLU 后大致保持不变不会逐层放大或缩小。np.random.seed(42)只是方便复现实验结果正式训练时可去掉。4.2 前向传播模块缓存 z 和 a 是反向传播的前提前向传播不只是计算输出还要把每一层的 z 和 a 存进字典因为反向传播需要用到它们。这里用字典而非列表因为后续扩展时按层号索引更清晰。def forward(X, params): caches {} A X L len(params) // 2 for l in range(1, L 1): A_prev A Z params[W str(l)].dot(A_prev) params[b str(l)] if l L: A softmax(Z.T).T # 输出层用 softmax else: A relu(Z) # 隐藏层用 relu caches[Z str(l)] Z caches[A str(l)] A return A, caches注意Z params[W str(l)].dot(A_prev) params[b str(l)]这里的维度约定W 的形状是 (n_l, n_{l-1})A_prev 的形状是 (n_{l-1}, m)相加时 b 会借助 NumPy 的广播机制自动与每列相加。如果你更喜欢样本在行方向的约定代码需要相应调整建议全文统一用“样本在列”约定避免维度混乱。输出层的softmax(Z.T).T有点绕但这样可以复用前面定义好的 softmax 函数它按行处理不用单独再写一个按列版本。4.3 训练循环梯度下降、学习率与批处理训练的核心是一个循环前向传播算损失、反向传播算梯度、用梯度下降更新参数。最基础的小批量梯度下降公式是 W - learning_rate * dWb - learning_rate * db。学习率是全局最重要的超参数太大震荡不收敛太小训练速度慢到无法接受。我一般先用 0.1 试一小轮观察损失变化再按数量级调整。def train(X, y, layer_dims, learning_rate0.1, epochs1000, batch_size64, print_every100): params initialize_params(layer_dims) costs [] m X.shape[1] for epoch in range(epochs): # 每个 epoch 打乱数据顺序 permutation np.random.permutation(m) X_shuffled X[:, permutation] y_shuffled y[:, permutation] for i in range(0, m, batch_size): X_batch X_shuffled[:, i:i batch_size] y_batch y_shuffled[:, i:i batch_size] A_last, caches forward(X_batch, params) cost cross_entropy_loss(y_batch, A_last) grads backward(params, caches, X_batch, y_batch) for l in range(1, len(layer_dims)): params[W str(l)] - learning_rate * grads[dW str(l)] params[b str(l)] - learning_rate * grads[db str(l)] if epoch % print_every 0: costs.append(cost) print(fEpoch {epoch}, cost: {cost:.4f}) return params, costs这里每个 epoch 都做一次随机打乱是为了让小批量梯度更接近真实的梯度期望减少方差。batch_size选 64 是性能和稳定性的折中32 以下梯度噪声太大256 以上收敛变慢内存占用也更高。学习率在 epoch 循环里保持恒定这是最简单的策略如果损失不再下降我一般手动把学习率减半再继续训练效果比预先设计学习率调度器更直观。4.4 用模拟数据集验证实现二分类与多分类场景的自检方法动手验证手写网络是否正确最好的办法是用构造的模拟数据集因为你知道真实边界是什么。二分类可以用 sklearn 的 make_moons 生成月牙形数据多分类可以用 make_blobs 生成高斯簇。from sklearn.datasets import make_moons, make_blobs from sklearn.model_selection import train_test_split # 二分类月牙数据 X_moons, y_moons make_moons(n_samples500, noise0.1, random_state42) y_moons_onehot np.eye(2)[y_moons].T X_train, X_test, y_train, y_test train_test_split( X_moons.T, y_moons_onehot.T, test_size0.2, random_state42)如果前向传播和反向传播实现正确损失应该稳步下降训练集准确率能到 95% 以上。验证反向传播最有效的手段是梯度检查用数值梯度逼近解析梯度两者误差应该在 1e-7 量级。数值梯度公式是 (J(theta eps) - J(theta - eps)) / (2 * eps)eps 通常取 1e-5。def numerical_gradient_check(params, X, y, eps1e-5): grads_analytic backward(params, forward(X, params)[1], X, y) param_keys [k for k in params.keys() if k.startswith(W)] for key in param_keys: for i in range(params[key].size): old_val params[key].flat[i] params[key].flat[i] old_val eps loss_plus cross_entropy_loss(y, forward(X, params)[0]) params[key].flat[i] old_val - eps loss_minus cross_entropy_loss(y, forward(X, params)[0]) params[key].flat[i] old_val grad_numeric (loss_plus - loss_minus) / (2 * eps) grad_analytic grads_analytic[d key].flat[i] if abs(grad_numeric - grad_analytic) 1e-4: print(fMismatch at {key}[{i}]: numeric{grad_numeric:.6e}, analytic{grad_analytic:.6e}) return False print(Gradient check passed!) return True这段代码遍历每个参数做两次前向传播计算数值梯度整个网络的参数多时会非常慢所以梯度检查只在训练前对小规模网络做一次确认实现正确后立刻停用。误差阈值 1e-4 已经足够宽松如果解析梯度实现有 bug数值梯度和解析梯度的差距通常在 1e-2 以上不会误报。5. 深度前馈网络的精度调优数据预处理、正则化与超参数搜索5.1 特征标准化和权重初始化玄学背后的本质是数值条件数前馈网络对输入特征的尺度非常敏感。假设某个特征取值范围在 0 到 1另一个在 0 到 10000权重更新时大尺度特征对应的梯度会比小尺度特征大几个数量级参数空间会被拉成一个狭长的山谷梯度下降在谷壁之间震荡收敛极慢。标准化到零均值、单位方差是成本最低收益最大的预处理步骤。def standardize(X): mean np.mean(X, axis1, keepdimsTrue) std np.std(X, axis1, keepdimsTrue) # 防止除零 std[std 1e-8] 1.0 return (X - mean) / std, mean, std注意axis1表示按特征维度做标准化因为前面约定的数据形状是 (n_features, m_samples)。测试集必须使用训练集的均值和标准差不能重新计算否则数据分布不一致训练和测试的性能会有偏差。std[std 1e-8]是为了避免常数特征导致除零实际数据里如果一个特征全是同一个值标准化后全是 0这个特征就没有信息量了。5.2 正则化手段对比L2 权重衰减与 Dropout 的不同性格L2 正则化在损失函数上加上 lambda/2 * sum(W^2)梯度更新时多一项 -lambda * W等价于每次更新都把权重往零方向拉一点学名叫权重衰减。它惩罚的是大权重迫使网络分布更均匀而不是依赖少数几个神经元。L2 实现极其简单但效果对 lambda 敏感一般从 0.001 开始试。Dropout 是在训练时随机丢弃一部分神经元的输出让网络不能依赖某个特定神经元。它的效果更接近集成学习——每次前向传播都在训练一个不同的子网络。实现时需要注意反向传播的遮罩一致性前向时哪些神经元被丢弃反向时对应位置的梯度也要置零否则梯度计算和网络结构不一致训练就会出问题。def forward_with_dropout(X, params, keep_prob0.8, is_trainTrue): if not is_train: return forward(X, params) caches {} A X L len(params) // 2 np.random.seed(0) for l in range(1, L 1): A_prev A Z params[W str(l)].dot(A_prev) params[b str(l)] if l L: A softmax(Z.T).T else: A relu(Z) dropout_mask (np.random.rand(*A.shape) keep_prob) / keep_prob A * dropout_mask caches[dropout_mask str(l)] dropout_mask caches[Z str(l)] Z caches[A str(l)] A return A, caches(np.random.rand(*A.shape) keep_prob) / keep_prob这行有两个作用随机生成 0/1 遮罩然后除以 keep_prob 做反向缩放。这样训练时神经元的期望输出不变测试时不需要额外缩放。如果丢了这步除法测试集和训练集的激活值尺度不一致准确率会掉一大截。这是 Dropout 实现里最常见的坑。5.3 学习率、批大小、网络宽度的交互关系一张表看懂如何协同调整学习率、批大小、隐藏层宽度不是独立变量它们共同决定梯度更新的方向和幅度。学习率大批大小也应该大否则单个小批量的梯度噪声会被放大参数更新轨迹像随机游走网络越宽每层参数越多反向传播的梯度方差越小可以适当提高学习率。下面是我在实践中的一组常用调整顺序供参考参数初始值调优方向判断依据学习率0.1每 100 epoch 损失下降小于 0.01 时减半损失曲线震荡则减半连续平坦再减半批大小64损失震荡明显时增大到 128/256梯度噪声大表现为损失上下跳动幅度大隐藏层宽度128欠拟合则加宽过拟合则加正则化而非收窄训练损失高但验证损失不降是欠拟合隐藏层数1数据量超过 10 万且特征复杂时再加层层数加一训练时间翻倍但指标不升就回退深度前馈网络在表格数据上的表现有个特点宽度比深度更重要。一个 3 层、每层宽 256 的网络往往比一个 10 层、每层宽 64 的网络在中小规模数据上效果更好、训练更稳。原因是表格数据的特征维度有限深层网络容易过拟合到噪声而宽层网络更容易找到平滑的决策边界。如果你的数据量不大第一反应应该加宽而不是加深。6. 深度前馈网络的常见问题与排查我的三次翻车记录6.1 损失不降反升学习率过大的典型症状与处理顺序现象训练第一个 epoch 后损失从 2.3 跳到 8.9后续继续上涨甚至出现 NaN。原因几乎总是学习率太大参数更新越过了损失函数的“悬崖”进入梯度爆炸区域Softmax 的输出出现极端概率交叉熵变成无穷大。排查思路先把学习率降到 0.001确认损失能下降如果 0.001 都不行检查输入数据是否包含 NaN 或 Inf尤其是标准化之前。解决方法是按数量级缩小学习率比如 0.1 失败就试 0.01再不行试 0.001。同时检查梯度值如果 dW 中出现大于 100 的数说明前向传播或反向传播有 bug比如初始化过大、激活函数导数用错单纯调学习率治标不治本。我遇到过把 ReLU 导数写成a 0但忘记转 float结果梯度参与矩阵乘法时数据类型强制转换产生错误排查了很久才发现。6.2 训练准确率很高但测试准确率低Dropout 和 L2 的正确使用姿势现象训练准确率 99%测试准确率 75%典型的过拟合。原因可能是网络容量远大于数据需求也可能是训练时没有使用任何正则化手段。解决方法是先加 L2 正则化lambda 从 0.01 开始如果还是过拟合再加 Dropoutkeep_prob 从 0.8 开始调。需要注意的是L2 和 Dropout 可以同时用但 lambda 和 keep_prob 都要往保守方向调两者叠加会让训练变得很慢。另一种被忽视的原因是数据划分本身有问题比如划分前没有打乱数据训练集和测试集的数据分布不一致。我踩过的坑是用时间序列数据直接切分前 80% 做训练后 20% 做测试结果前向传播网络学到的是完全不同的分布测试损失崩溃。如果数据不是时序数据务必先随机打乱再划分。6.3 梯度检查通过但训练不收敛隐藏 bug 藏在维度约定里现象梯度检查通过损失曲线却震荡不降。梯度检查只验证了反向传播的数学正确性但如果数据维度约定不一致比如一个地方用 (样本, 特征)另一个地方用 (特征, 样本)矩阵乘法不会报错但梯度更新的方向是错的。我在一个模拟项目里就遇到过前向用 (m, n) 约定反向写成了 (n, m)结果是更新方向偏了 90 度损失在 0.7 附近来回横跳。排查方法是打印网络每一层的形状变化输入 X 的形状、每层 Z 的形状、输出 A 的形状重点检查从反向传播返回的 dW 形状是否和对应 W 完全一致。如果不一致一定存在转置错误。此外还要检查标签 y 的编码方式one-hot 的行方向是否与输出 A 的行方向一致。这个错误新手会犯老手在写新代码时也可能翻车因为它不会导致报错。7. 把模型从“能跑”变成“可信”用学习曲线诊断欠拟合与过拟合7.1 学习曲线的画法与三种读法训练完成后学习曲线是最快的诊断工具。横轴是训练轮数纵轴是损失。一种理想情况是训练损失和验证损失都持续下降并趋于平稳两者差距很小这代表模型容量合适欠拟合和过拟合都不存在。第二种情况是训练损失下降验证损失先降后升分叉点就是过拟合开始的地方应该在该点附近停止训练或加大正则化。第三种情况是两个损失都高且平稳说明模型容量不够需要加宽网络或增加特征。import matplotlib.pyplot as plt def plot_learning_curves(train_costs, val_costs): epochs range(len(train_costs)) plt.plot(epochs, train_costs, labelTrain loss) plt.plot(epochs, val_costs, labelValidation loss) plt.xlabel(Epoch) plt.ylabel(Loss) plt.legend() plt.title(Learning Curves) plt.show()验证损失的计算是同样的前向传播但不需要反向传播不需要 Dropout。如果验证损失出现 NaN检查验证集是否包含训练集没见过的新类别或者标准化参数是否用了验证集自己的统计量。学习曲线看到过拟合后不要急着加数据量先加正则化往往更有效。7.2 用早停法控制过拟合最简单的后悔药实现早停法就是在验证损失连续多个 epoch 不降低时停止训练并恢复验证损失最低时的参数。实践中我记录历史最优验证损失和对应的参数拷贝如果连续 20 个 epoch 验证损失没有创新低就停止并回滚。best_val_loss float(inf) best_params None patience 20 no_improve_count 0 for epoch in range(epochs): # ... 常规训练 ... val_loss compute_val_loss(val_X, val_y, params) if val_loss best_val_loss: best_val_loss val_loss best_params {k: v.copy() for k, v in params.items()} no_improve_count 0 else: no_improve_count 1 if no_improve_count patience: break params best_params这里的关键是v.copy()直接赋值两个变量会共享同一块内存后续训练更新再好回滚也拿不到最优参数。早停法是成本最低的正则化手段不需要调超参数也不需要额外的网络结构改动。如果你只想做一个改进先做早停再考虑 L2 和 Dropout。7.3 从前馈网络走向实战项目的检查清单把前馈网络验证到可用状态我建议按这个顺序过一遍检查清单第一标准化参数是否正确保存、测试集是否复用了训练集的统计量第二梯度检查是否在最终结构上执行过尤其是新增了 Dropout 后需要重新检查因为遮罩会让数值梯度计算不稳定此时先关闭 Dropout确认反向传播无误再打开第三学习曲线是否确认了无欠拟合、过拟合是否可控。第四做一次简单的超参数小搜索固定其他参数只调学习率画损失曲线对比第五确认输出层的概率是否和行为一致比如二分类输出每行两个概率之和是否为 1。我之前在一个模拟项目里加了 L2 正则化忘记在反向传播的 dW 里加回 lambda * W 的梯度结果正则化完全没生效学习曲线形态一模一样。检查清单本质上是帮助你把每个改动对应的数学变化核对清楚。7.4 下一步还能怎么改进从手写网络到框架迁移的路线图纯 NumPy 实现跑通之后我建议做两件事。第一件事数据量较大时把同一套结构迁移到框架里实现用框架的自动求导来计算梯度与你手写的反向传播做数值对比。这个对照训练能帮你确认手写实现是否在某些边界条件下出错。第二件事如果数据不是表格而是图像、文本或时间序列前馈网络就不合适了——图像用卷积文本用序列模型但前馈网络仍然值得保留在工具箱里因为它简单透明、训练快、可解释性最好。我自己每次做新数据集都会先跑一个前馈网络作为基线再尝试更复杂的模型。如果前馈网络学好特征后指标上不去更复杂模型的收益通常也有限如果前馈网络就能达到目标就完全没有必要引入额外的复杂度。这套工作流属于我自己比较喜欢的节奏。一个小习惯分享给你在参数文件里记录网络结构、学习率、标准化均值方差、以及每次训练的最佳验证损失这样隔一周回来还能想起当时为什么这么配。数据、代码、参数记录三件套齐全整个项目才能算是真正复盘过。希望这些经验对你有帮助。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑