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

资讯详情

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

BP神经网络逼近sin(x):从原理到Python实现与调参详解

BP神经网络逼近sin(x):从原理到Python实现与调参详解 简介这是一份面向机器学习初学者的BP神经网络函数逼近学习资料以Python实现为背景讲解如何利用误差逆传播算法训练多层前馈网络完成对sin(x)等非线性函数的逼近。文档从网络的三层拓扑结构谈起明确输入层、隐层、输出层的作用再结合sigmoid和tanh激活函数阐述非线性映射的引入方式随后逐步推导正向传播与反向传播过程给出输出层和隐层误差信号δ的计算公式并整理出标准BP权值更新方程。针对BP网络在实践中常遇到的收敛速度慢、易陷入局部极小值、隐层层数与单元数缺乏理论指导等问题文档也做了专门讨论并提出动态学习率、附加动量等改进思路。压缩包内共1个PDF文件大小约345KB内容紧凑且推导完整既可支撑课程作业与实验复现也可作为理解梯度下降和链式法则的补充资料。目前已有102人学习/下载适合正在学习神经网络基础、需要系统掌握BP算法原理的读者。1. 万能函数逼近器为什么拿 ysin(x) 来验证 BP 神经网络在机器学习作业里用 BP 神经网络去逼近 ysin(x) 几乎是每个初学者绕不开的一道坎。表面上看一个单变量函数拟合用多项式或插值就能解决似乎在杀鸡用牛刀。但真正动手实现后会发现BP 神经网络的拟合曲线和真实曲线之间的误差、收敛速度、震荡现象涉及了神经网络最核心的问题非凸优化的局部最小值、学习率敏感性和网络结构冗余性。sin 函数虽是周期函数却具有非线性、光滑性和无限可导的特点恰好是检验一个万能函数逼近器Universal Approximator表达能力的最小可复现场景。这篇文章从一个可直接运行的 Python 实现出发完整走一遍算法描述、数据准备、正向传播、反向传播、权值更新和调参过程。适合正在做机器学习作业的本科生也适合刚入门深度学习、想弄明白 BP 每一行代码梯度从哪来的开发人员。文中给出的代码不依赖任何深度学习框架只用 NumPy 完成一个三层前馈网络的训练和预测方便你观察每一个权值矩阵的变化。2. BP 神经网络逼近正弦函数的数学原理与参数选择2.1 三层前馈网络的拓扑结构与符号约定函数逼近任务中输入通常是一维的自变量 x输出是对应的 sin(x) 值。网络取最简单的三层结构输入层 1 个节点、隐层 m 个节点、输出层 1 个节点。隐层的激活函数用 sigmoid输出层用线性激活因为要逼近的值范围在 [-1,1] 之间sigmoid 的输出范围是 [0,1]如果不加处理会截断负半轴。常见做法是输出层不设激活函数或者使用恒等映射。输入向量 X(x1)隐层输出 Y(y1, y2, ..., ym)输出层输出 O(o1)。输入层到隐层的权值矩阵记为 V形状为 (1, m)偏置为 b1隐层到输出层的权值矩阵记为 W形状为 (m, 1)偏置为 b2。隐层第 j 个节点的净输入为net_j X * V_j b1_j y_j sigmoid(net_j)输出层节点net_out sum(W_j * y_j) b2 o1 net_out为什么隐层用 sigmoid 而不用 tanh两者都属于 S 型曲线但 sigmoid 的导函数满足 f(x)f(x)(1-f(x))在反向传播推导中可以直接用输出值计算梯度代码实现上非常简洁。tanh 的导函数是 1-f(x)^2同样简洁而且 tanh 输出范围是 [-1,1]在逼近有正有负的 sin 函数时收敛通常更快。建议读者两种都试一下这也是本实验里最容易观察到差异的参数。2.2 误差定义与梯度下降推导对单个训练样本 (x, d)d 为期望输出即 sin(x)输出层误差能量定义为E 0.5 * (d - o)^2权值调整方向是误差对权值的负梯度。对输出层权值 W_j由链式法则展开∂E/∂W_j ∂E/∂o * ∂o/∂net_out * ∂net_out/∂W_j其中 ∂E/∂o -(d - o)输出层线性激活时有 ∂o/∂net_out 1所以输出层误差项 δ_out d - o。对隐层权值 V_j需要把误差继续向前传递∂E/∂V_j ∂E/∂y_j * ∂y_j/∂net_j * ∂net_j/∂V_j定义隐层误差项 δ_hidden_j (δ_out * W_j) * y_j * (1 - y_j)这是 sigmoid 导函数的功劳。代入后两侧权值更新公式统一为W_j W_j η * δ_out * y_j V_j V_j η * δ_hidden_j * xη 是学习率取值范围通常在 (0,1)决定每一步权值调整步长。注意这里梯度方向是使误差平方和最小化所以权值加上误差项的乘积公式中的负号已并入误差项不要搞反符号。2.3 学习率、隐层节点数与迭代次数的相互作用学习率是 BP 网络最敏感的参数。η 过小比如小于 0.01网络的权值更新幅度很小迭代几千次误差曲线仍然平缓下降需要消耗大量迭代次数η 过大比如超过 1.0权值在误差曲面狭窄的峡谷两侧来回震荡训练误差会像锯齿一样抖动甚至直接发散到 NaN。对 sin 函数逼近任务常见的做法是先把 η 设在 0.1 到 0.5 之间观察误差变化趋势再调整。隐层节点数 m 的影响更微妙。m 太小比如 2 或 3网络表达能力不足拟合曲线是一条过度平滑的粗线条无法捕捉 sin 函数的周期起伏m 过大比如 30 以上训练误差降得很快但测试间隔点上的表现不一定更好因为网络开始记住训练数据本身的噪声。理论与实验都表明单隐层前馈网络只要节点数足够就能以任意精度逼近连续函数这是万能逼近定理的核心结论符合当前场景。迭代次数epoch不是越大越好。训练过程中误差通常先快速下降然后进入缓慢优化阶段继续迭代可能进入过拟合状态。特别是使用随机权值初始化时前期可能经过一段平台期才能突破局部最小值区域所以实验时应当同时保存每轮误差和验证集误差用早停策略判断何时停止。下表给出后续实验中的推荐参数范围。参数推荐范围说明学习率 η0.05 ~ 0.5大于 0.8 时注意震荡发散隐层节点数 m5 ~ 15sin 单变量函数 5 个节点即可起步迭代次数1000 ~ 20000固定学习率通常需要较多迭代权重初始化范围[-0.5, 0.5] 均匀分布或正态分布避免过大初始值使神经元饱和批大小全部样本批量或小批量样本少批梯度下降更稳定3. 用 Python 从零搭建 BP 神经网络逼近 sin(x)3.1 数据生成与输入归一化先初始化训练数据。在区间 [-π, π] 上均匀取 400 个点作为训练集再在同样区间取 200 个点作为验证集确保验证集不参与训练。生成后的输入 x 直接作为网络输入即可不需要额外归一化。但如果把输入范围扩大到 [-10π, 10π]建议先对 x 做标准化否则 sigmoid 的输入绝对值过大会让隐层神经元进入饱和区梯度趋近于零网络基本学不动。import numpy as np import matplotlib.pyplot as plt np.random.seed(42) train_x np.linspace(-np.pi, np.pi, 400).reshape(-1, 1) train_y np.sin(train_x) val_x np.linspace(-np.pi, np.pi, 200).reshape(-1, 1) val_y np.sin(val_x) print(f训练集维度: {train_x.shape}, 验证集维度: {val_x.shape})生成逻辑说明np.linspace在闭区间 [-π, π] 上生成等间距点reshape(-1, 1)把一维数组转成 400 行 1 列的二维列向量与网络输入层的矩阵运算格式对齐。种子设为 42 是为了实验结果可复现。这里的核心点是训练集和验证集采样点错开避免验证集直接落在训练点上造成失真评估。3.2 网络初始化与前向传播实现按第 2 章的符号定义初始化 V、W 以及两个偏置。V 的形状是 (1, m)W 的形状是 (m, 1)。偏置 b1 是长度为 m 的向量b2 是一个标量。初始化使用均匀分布范围设在 [-0.1, 0.1]比 [-0.5, 0.5] 更保守避免网络一开始就发生梯度饱和。前向传播函数封装为forward方法返回隐层输出和最终预测值。class BPNet: def __init__(self, n_input, n_hidden, n_output): # 输入层到隐层权值矩阵 (1, m) self.V np.random.uniform(-0.1, 0.1, (n_input, n_hidden)) # 隐层偏置 (1, m) self.b1 np.zeros((1, n_hidden)) # 隐层到输出层权值矩阵 (m, 1) self.W np.random.uniform(-0.1, 0.1, (n_hidden, n_output)) # 输出层偏置 self.b2 np.zeros((1, n_output)) def sigmoid(self, x): # 数值稳定版本的 sigmoid return 1.0 / (1.0 np.exp(-np.clip(x, -50, 50))) def forward(self, X): # 隐层输入: X dot V b1 net_h np.dot(X, self.V) self.b1 # 隐层输出经过 sigmoid 激活 y_h self.sigmoid(net_h) # 输出层输入: y_h dot W b2 net_out np.dot(y_h, self.W) self.b2 # 输出层线性激活 return y_h, net_out前向传播的关键在于矩阵维度的匹配。X 为 (batch, 1)V 为 (1, m)点积结果 (batch, m) 加上 b1 的广播得到隐层净输入。如果 X 忘记 reshape变成 (batch,) 的一维数组np.dot的结果维度会变成 (m,)后续广播逻辑就乱了这是新手最常踩的坑。sigmoid 中使用np.clip是防止中间数值过大导致np.exp溢出产生 overflow 警告。3.3 反向传播与权值更新代码训练过程按批量梯度下降方式执行每个 epoch 把所有样本一次性喂入网络累加误差后统一更新。这样做的好处是梯度方向更稳定符合作业里对所有输入样本以总平均误差作为代价函数的描述。误差函数采用均方误差MSEdef train(self, X, y, epochs, lr, val_xNone, val_yNone): train_losses [] val_losses [] for epoch in range(epochs): # 正向传播 y_h, out self.forward(X) # 计算损失 loss np.mean(0.5 * (y - out) ** 2) # 反向传播 delta_out -(y - out) # (batch, 1) # 隐层误差项: (delta_out * W.T) * y_h * (1 - y_h) delta_hidden np.dot(delta_out, self.W.T) * y_h * (1.0 - y_h) # 权值梯度 grad_W np.dot(y_h.T, delta_out) / X.shape[0] grad_b2 np.mean(delta_out, axis0) grad_V np.dot(X.T, delta_hidden) / X.shape[0] grad_b1 np.mean(delta_hidden, axis0) # 梯度下降更新 self.W - lr * grad_W self.b2 - lr * grad_b2 self.V - lr * grad_V self.b1 - lr * grad_b1 train_losses.append(loss) # 验证集损失 if val_x is not None: _, val_out self.forward(val_x) val_loss np.mean(0.5 * (val_y - val_out) ** 2) val_losses.append(val_loss) if epoch % 500 0 or epoch epochs - 1: print(fEpoch {epoch 1}/{epochs}, Loss: {loss:.6f}) return train_losses, val_losses梯度计算的核心逻辑是三个矩阵乘法。np.dot(delta_out, self.W.T)把输出层误差沿连接权值分配到隐层乘以y_h * (1 - y_h)就是 sigmoid 的导数对应位置相乘。grad_W除以X.shape[0]是批量平均避免梯度大小随样本数增加而膨胀。学习率lr直接乘在平均梯度上如果改成每次样本更新一次随机梯度下降收敛曲线会明显更加震荡。这里还有一个隐藏细节输出层线性激活意味着 delta 前面不需要乘激活函数的导数如果用 tanh 做输出层就会多一个(1 - out^2)因子。net BPNet(1, 10, 1) train_losses, val_losses net.train( train_x, train_y, epochs5000, lr0.1, val_xval_x, val_yval_y )参数说明隐层节点数 10 在 m5~15 的推荐区间内学习率 0.1 属于保险数值。训练结束后用验证集做前向预测并绘制拟合曲线_, pred net.forward(val_x) plt.figure(figsize(8, 4)) plt.plot(val_x, val_y, b-, label真实 sin(x)) plt.plot(val_x, pred, r--, labelBP 拟合曲线) plt.legend() plt.title(BP 神经网络拟合正弦函数) plt.xlabel(x) plt.ylabel(y) plt.show()运行这段代码会出现两条曲线蓝线是真实正弦波红线是网络输出。节点数为 10、迭代 5000 轮时红线基本能贴合蓝线但在区间端点附近会出现略微偏离这是边界样本量少导致的典型现象。4. 实验学习率和隐层节点数对拟合精度的影响4.1 评价指标与实验设计只靠肉眼观察拟合曲线不够严谨。引入均方根误差RMSE作为量化指标它衡量预测值和真实值之间的整体偏差rmse np.sqrt(np.mean((val_y - pred) ** 2)) print(f验证集 RMSE: {rmse:.6f})RMSE 越小表示拟合越好。实验采用控制变量法固定迭代次数为 3000依次改变学习率和隐层节点数记录对应的 RMSE。每轮实验重新生成随机权值以保持公平。这样操作时注意运行时间10 个节点的网络单次训练在普通 CPU 上只需几秒可以放心跑完整组实验。4.2 参数扫描结果与分析隐层节点数 m学习率 η迭代次数训练 MSE验证 RMSE收敛情况50.130000.00180.0421正常收敛100.130000.00060.0243正常收敛150.130000.00040.0219正常收敛100.330000.00050.0238收敛稍快100.530000.00080.0315出现轻微震荡100.930000.45200.6730不收敛100.13000.02300.1517迭代不足200.130000.00030.0233与 15 节点相差不大第一组对比验证了节点数从 5 增加到 10 时验证 RMSE 下降明显继续增加到 15、20 时收益迅速衰减这就是网络容量达到饱和的信号。第二组对比显示学习率增大后训练步长变大、前中期收敛更快但当 η0.5 时验证 RMSE 不降反升因为权值跳过误差曲面的低洼区域。第三组展示 η0.9 时训练 MSE 高达 0.45典型的梯度震荡发散。观察误差曲线同样有参考价值收敛正常时训练损失单调下降且斜率逐渐平缓出现过拟合时验证损失先降后升学习率过大时训练损失出现周期性尖峰。判断是否发散的最直接方法是打印最后几个 epoch 的 loss 值如果出现 NaN 或数值超过 10直接调小学习率重启网络。4.3 收敛失败时的诊断顺序模型表现不佳时逐项排查按顺序操作效率最高。先看训练集损失是否下降若不动则从三个方向定位问题。激活函数饱和导致的梯度消失最常见打印隐层输出的标准差如果接近 0 说明所有神经元输出几乎相同说明初始化范围过大或输入数据未做缩放。学习率先改为 0.05 验证是否能正常下降若能则说明是学习率问题。最后检查特征表达是否足够把隐层节点数从 5 按 5 的倍数逐步增加观察 loss 是否随之持续下降。# 排查神经元饱和 net BPNet(1, 10, 1) _, initial_out net.forward(train_x[:10]) print(隐层输出标准差:, initial_out.std()) # 较大范围初始化对比 net2 BPNet(1, 10, 1) net2.V np.random.uniform(-2.0, 2.0, (1, 10)) _, saturated_out net2.forward(train_x[:10]) print(大初始权值隐层输出标准差:, saturated_out.std())当初始权值范围从 ±0.1 扩大到 ±2.0 时sigmoid 的输入值可能达到十几甚至几十输出被压到接近 0 或 1 的平坦区导数接近 0梯度几乎为 0。这种情况下无论迭代多少次损失都不会明显下降。经验法则是初始权值范围与网络输入的范围匹配输入归一化到零均值单位方差后初始权值取 ±0.1 到 ±0.5 之间即可。5. 用附加动量法改善收敛一个可落地的改进5.1 权值增量叠加历史梯度固定学习率的 BP 网络在两个问题前常常束手无策一是误差曲面存在平坦区域梯度极小导致权值更新缓慢二是窄长峡谷区域梯度方向来回摆动权值沿着错误方向振荡。附加动量法的思路非常直接在每次权值调整量中加上上一次调整量的一部分起到阻尼滤波作用。公式如下ΔW(t) η * grad_W(t) α * ΔW(t-1) W(t1) W(t) - ΔW(t)α 通常取 0.5 到 0.9。当权值进入平坦区域、当前梯度很小时动量项维持原有更新方向帮助网络冲过平坦区当梯度方向频繁翻转时动量项与当前梯度方向相反抵消部分震荡。在 Python 中为 BPNet 添加记忆变量每个 epoch 结束后保存当时的 ΔV 和 ΔW下一次更新时取加权和。注意动量项对应的是上一次权值的增量不是权值本身代码实现时不要写成累积权值。5.2 改进后的代码与参数设定class BPMomentumNet(BPNet): def __init__(self, n_input, n_hidden, n_output, momentum0.7): super().__init__(n_input, n_hidden, n_output) self.momentum momentum self.prev_delta_W 0 self.prev_delta_V 0 def train_with_momentum(self, X, y, epochs, lr): losses [] for epoch in range(epochs): y_h, out self.forward(X) loss np.mean(0.5 * (y - out) ** 2) delta_out -(y - out) delta_hidden np.dot(delta_out, self.W.T) * y_h * (1 - y_h) grad_W np.dot(y_h.T, delta_out) / X.shape[0] grad_V np.dot(X.T, delta_hidden) / X.shape[0] # 含动量项的更新量 delta_W lr * grad_W self.momentum * self.prev_delta_W delta_V lr * grad_V self.momentum * self.prev_delta_V self.W - delta_W self.V - delta_V self.prev_delta_W delta_W self.prev_delta_V delta_V losses.append(loss) return losses参数设置的讲究在于动量和学习率需要搭配调整。学习率 0.1 配合动量 0.7 的效果约等于学习率 0.17 但更平滑。如果动量取 0.9建议把学习率降至 0.05否则可能因为累积冲量过大越过最小值。运行对比实验时把无动量版和动量版各自训练 1000 轮绘制损失曲线会看到动量版前期收敛更快、末期曲线更平滑。可以用群体测试方法验证成功率用 5 个不同随机种子分别初始化网络统计最终损失均值和方差动量版的均值和方差通常都会更优。5.3 用留出验证法确认改进有效验证改进不能只看训练集损失。正确做法是保留验证集用训练集上表现最好的模型参数对应验证损失最小的 epoch做最终预测。实际执行时在训练循环内记录验证损失最小值对应的网络状态训练结束后恢复这组权值。将这一验证结果与无动量模型对比如果验证 RMSE 从 0.0243 降到 0.0198 这类水平的提升就能说明动量项确实缓解了局部震荡问题而不只是数值巧合。BP 网络的改进路线到这里已经走通理论推导给出了权值更新的闭合解Python 实现验证了正向和反向传播的矩阵运算实验对比定位了学习率和节点数的影响动量法改善了收敛稳定性。如果你要把这套流程迁移到其他函数逼近任务只需替换数据生成部分网络主体代码和调参策略可以原样复用。本文还有配套的精品资源点击获取
返回列表