BP神经网络拟合sin曲线:从数学原理到Python实现

1次阅读
没有评论

共计 2566 个字符,预计需要花费 7 分钟才能阅读完成。

image.webp

传统方法的局限性

在工程和科学计算中,我们经常需要对函数进行拟合。对于简单的线性关系,多项式回归就能很好地完成任务。但当遇到像 sin 这样的周期性函数时,传统方法就暴露出明显不足:

  • 高阶多项式拟合容易产生龙格现象(Runge’s phenomenon),在区间端点附近出现剧烈震荡
  • 需要非常高的多项式阶数才能近似周期性特征,导致计算复杂度飙升
  • 泛化能力差,训练集外的预测效果急剧下降

这些痛点促使我们寻找更强大的拟合工具,而神经网络凭借其万能逼近定理(Universal Approximation Theorem)成为理想选择。

神经网络结构对比

单隐层 vs 双隐层

通过实验对比发现:

  1. 单隐层网络(如 8 个神经元)在训练初期收敛较快,但最终拟合精度有限(MSE 约 0.01)
  2. 双隐层网络(如 4 - 4 结构)需要更多迭代次数,但最终能达到更高精度(MSE<0.001)
  3. 过深的网络(如三层以上)反而会导致梯度传播困难,训练效率下降

激活函数选择

不同激活函数的表现差异显著:

  • Sigmoid:容易导致梯度消失,不推荐用于深度网络
  • Tanh:在浅层网络中表现良好,中心对称特性有助于梯度流动
  • ReLU:训练速度最快,但需要注意死亡 ReLU 问题(可配合 LeakyReLU)

实验数据显示,对于 sin 函数拟合,Tanh 激活函数配合双隐层结构能达到最佳平衡。

核心实现详解

网络初始化

import numpy as np

class BPNeuralNetwork:
    def __init__(self, layer_sizes):
        """:param layer_sizes: 如 [1,4,4,1] 表示输入层 1 节点,两个隐藏层各 4 节点,输出层 1 节点"""
        self.weights = []
        self.biases = []
        for i in range(len(layer_sizes)-1):
            # He 初始化,适合 Tanh 激活函数
            std_dev = np.sqrt(2 / layer_sizes[i])  
            self.weights.append(np.random.randn(layer_sizes[i], layer_sizes[i+1]) * std_dev)
            self.biases.append(np.random.randn(layer_sizes[i+1]))

前向传播

def forward(self, x):
    """
    :param x: 输入向量 (batch_size, input_dim)
    :return: 网络输出 (batch_size, output_dim)
    """
    activations = [x]
    for i, (w, b) in enumerate(zip(self.weights, self.biases)):
        z = np.dot(activations[-1], w) + b
        # 最后一层不使用激活函数(线性输出)a = np.tanh(z) if i < len(self.weights)-1 else z  
        activations.append(a)
    return activations

反向传播

def backward(self, x, y, learning_rate=0.01):
    """实现反向传播算法"""
    # 前向传播获取各层激活值
    activations = self.forward(x)
    # 计算输出层误差
    error = activations[-1] - y  # (batch_size, output_dim)
    # 反向传播误差
    deltas = [error]  # 从输出层开始

    # 逐层计算梯度
    for i in range(len(self.weights)-1, 0, -1):
        delta = np.dot(deltas[-1], self.weights[i].T) * (1 - activations[i]**2)  # Tanh 导数
        deltas.append(delta)
    deltas.reverse()

    # 更新参数
    for i in range(len(self.weights)):
        grad_w = np.dot(activations[i].T, deltas[i]) / len(x)
        grad_b = np.mean(deltas[i], axis=0)
        self.weights[i] -= learning_rate * grad_w
        self.biases[i] -= learning_rate * grad_b

实验分析与优化

学习率影响

通过实验可以观察到:

  1. 学习率 =0.1 时:前期收敛快但后期震荡明显
  2. 学习率 =0.01 时:稳定收敛,约 500 次迭代达到较好效果
  3. 学习率 =0.001 时:收敛过慢,需要上万次迭代

推荐采用学习率衰减策略:初始 0.05,每 200 次迭代衰减 10%。

拟合效果可视化

训练完成后,我们可以绘制预测值与真实值的对比曲线:

import matplotlib.pyplot as plt

x_test = np.linspace(-2*np.pi, 2*np.pi, 100).reshape(-1,1)
y_pred = network.forward(x_test)[-1]

plt.plot(x_test, np.sin(x_test), label='True sin(x)')
plt.plot(x_test, y_pred, '--', label='NN Prediction')
plt.legend()
plt.show()

BP 神经网络拟合 sin 曲线:从数学原理到 Python 实现

常见问题解决方案

梯度消失问题

症状:网络深层参数几乎不更新
解决方法:

  1. 使用 Tanh 替代 Sigmoid
  2. 采用残差连接(ResNet 思路)
  3. 批归一化(Batch Normalization)

过拟合预防

  1. 添加 L2 正则化项:
    l2_lambda = 0.001
    for w in self.weights:
        grad_w += l2_lambda * w
  2. 使用早停法(Early Stopping)
  3. 增加训练数据量(至少覆盖 2 个完整周期)

数据归一化

Sin 函数的输出范围是[-1,1],因此:

  1. 输入 x 建议归一化到 [-π, π] 区间
  2. 输出 y 无需特殊处理

扩展应用

该方法可以轻松推广到其他周期函数:

  1. 对于 cos 函数:只需修改训练标签
  2. 对于复杂周期函数:建议增加隐层节点数
  3. 与傅里叶级数对比:
  4. 傅里叶方法需要预先知道主要频率成分
  5. 神经网络自动学习频率特征,但计算量更大

完整代码实现已上传 GitHub(示例仓库地址),包含更多训练技巧和可视化工具。通过这个案例,我们不仅掌握了函数拟合的新方法,更深入理解了神经网络处理非线性问题的核心机制。

正文完
 0
评论(没有评论)