共计 3285 个字符,预计需要花费 9 分钟才能阅读完成。
背景介绍
在机器学习中,使用神经网络拟合非线性函数是一项基础但重要的任务。sin 函数因其周期性、光滑性和非线性特性,成为验证神经网络能力的理想选择。特别是,sin 函数的非线性特性可以很好地测试神经网络的拟合能力,而不会因为过于复杂而让初学者感到困惑。

- 非线性挑战:sin 函数的输出在输入变化时是非线性的,这意味着简单的线性回归无法准确拟合它。神经网络通过引入非线性激活函数,能够学习这种复杂的映射关系。
- 周期性特性:sin 函数的周期性也要求神经网络能够捕捉重复模式,这对隐藏层的设计提出了要求。
网络架构设计
为了拟合 sin 函数,我们设计一个简单的三层神经网络(输入层、隐藏层、输出层)。
- 输入层:1 个节点,对应输入 x 值(如 0 到 2π 范围内的点)。
- 隐藏层:建议使用 1 - 2 层,每层节点数在 5 -20 之间。过多的节点可能导致过拟合,而过少则可能无法捕捉 sin 的复杂性。
- 输出层 :1 个节点,输出预测的 sin(x) 值。
关键实现细节
激活函数选择
激活函数决定了网络的非线性能力。以下是常见激活函数在 sin 拟合中的表现:
- sigmoid:输出范围(0,1),可能导致梯度消失问题。
- tanh:输出范围 (-1,1),更适合 sin 函数,因为 sin 的输出也在[-1,1] 之间。
- ReLU:简单高效,但可能在某些区域“死亡”。
对于 sin 拟合,tanh通常是更好的选择。
损失函数定义
我们使用均方误差(MSE)作为损失函数:
$$
MSE = \frac{1}{N} \sum_{i=1}^{N} (y_i – \hat{y_i})^2
$$
其中,$y_i$ 是真实值,$\hat{y_i}$ 是预测值。
反向传播的数学推导
反向传播通过链式法则计算梯度。以下是关键公式:
- 输出层误差:
$$
\delta^{L} = (\hat{y} – y) \cdot f'(z^{L})
$$ - 隐藏层误差:
$$
\delta^{l} = (W^{l+1})^T \delta^{l+1} \cdot f'(z^{l})
$$ - 权重更新:
$$
W^{l} = W^{l} – \eta \cdot \delta^{l} (a^{l-1})^T
$$
完整 Python 代码实现
以下是使用 NumPy 从零实现的代码:
import numpy as np
import matplotlib.pyplot as plt
# 数据生成
def generate_data(num_points=100):
x = np.linspace(0, 2 * np.pi, num_points)
y = np.sin(x)
return x.reshape(-1, 1), y.reshape(-1, 1)
# 初始化网络
def initialize_network(input_size, hidden_size, output_size):
np.random.seed(42)
W1 = np.random.randn(input_size, hidden_size) * 0.01
b1 = np.zeros((1, hidden_size))
W2 = np.random.randn(hidden_size, output_size) * 0.01
b2 = np.zeros((1, output_size))
return {'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}
# 激活函数(tanh)def tanh(x):
return np.tanh(x)
def tanh_derivative(x):
return 1 - np.tanh(x) ** 2
# 前向传播
def forward_propagation(x, params):
W1, b1, W2, b2 = params['W1'], params['b1'], params['W2'], params['b2']
z1 = np.dot(x, W1) + b1
a1 = tanh(z1)
z2 = np.dot(a1, W2) + b2
return {'a1': a1, 'z2': z2}
# 反向传播
def backward_propagation(x, y, params, forward_cache):
W1, W2 = params['W1'], params['W2']
a1, z2 = forward_cache['a1'], forward_cache['z2']
m = x.shape[0]
delta2 = (z2 - y) # MSE 导数
dW2 = np.dot(a1.T, delta2) / m
db2 = np.sum(delta2, axis=0, keepdims=True) / m
delta1 = np.dot(delta2, W2.T) * tanh_derivative(a1)
dW1 = np.dot(x.T, delta1) / m
db1 = np.sum(delta1, axis=0, keepdims=True) / m
return {'dW1': dW1, 'db1': db1, 'dW2': dW2, 'db2': db2}
# 参数更新
def update_params(params, grads, learning_rate):
params['W1'] -= learning_rate * grads['dW1']
params['b1'] -= learning_rate * grads['db1']
params['W2'] -= learning_rate * grads['dW2']
params['b2'] -= learning_rate * grads['db2']
return params
# 训练函数
def train(x, y, params, epochs, learning_rate):
loss_history = []
for epoch in range(epochs):
# 前向传播
cache = forward_propagation(x, params)
z2 = cache['z2']
loss = np.mean((z2 - y) ** 2)
loss_history.append(loss)
# 反向传播
grads = backward_propagation(x, y, params, cache)
# 更新参数
params = update_params(params, grads, learning_rate)
if epoch % 100 == 0:
print(f'Epoch {epoch}, Loss: {loss}')
return params, loss_history
# 主程序
if __name__ == '__main__':
x, y = generate_data()
params = initialize_network(1, 10, 1)
params, loss_history = train(x, y, params, 1000, 0.01)
# 可视化
plt.plot(loss_history)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.show()
# 预测
cache = forward_propagation(x, params)
y_pred = cache['z2']
plt.plot(x, y, label='True sin(x)')
plt.plot(x, y_pred, label='Predicted sin(x)')
plt.legend()
plt.show()
调优与避坑指南
学习率设置
- 学习率过大:可能导致损失震荡甚至发散。
- 学习率过小:训练速度慢,可能陷入局部最优。
建议从 0.01 开始尝试,根据损失曲线调整。
梯度消失问题
- 识别:如果损失长时间不下降,可能是梯度消失。
- 解决:使用 tanh 或 ReLU 激活函数,避免深层网络。
过拟合预防
- 数据量:确保足够的训练数据。
- 正则化:可以尝试 L2 正则化或 Dropout(虽然本例中不必要)。
效果验证
通过 Matplotlib 绘制训练过程中损失下降曲线和最终拟合效果。可以看到,随着 epoch 增加,预测曲线逐渐逼近真实的 sin 曲线。
思考题
如何将本方法推广到其他周期函数的拟合?
- 可以尝试调整隐藏层大小或激活函数。
- 对于更复杂的周期函数(如方波),可能需要更深的网络或不同的激活函数。
希望这篇文章能帮助你理解 BP 神经网络如何拟合非线性函数,并为你未来的机器学习项目打下基础!
正文完
