神经网络数学原理(2)反向传播

在上一节【神经网络数学原理(1)前向传播】中已经描述过神经网络结构和输入层到输出层的基本数学原理,本章主要讲述如何通过反向传播来优化权重的过程和数学原理。神经网络通过一系列的计算(前向传播)和优化过程(反向传播)来调整网络中每一层的权重,直到网络能够最小化预测值与实际值之间的误差(通常通过某种损失函数来衡量误差)。

以下是一个简单的过程:

  1. 前向传播(Forward Propagation):输入数据通过神经网络的各层进行传递,经过每层的加权求和和激活函数处理,输出一个预测结果。

  2. 损失计算(Loss Calculation):将神经网络的预测结果与实际结果(标签)进行对比,计算出损失值。损失函数通常是均方误差(MSE)、交叉熵等,用于衡量模型预测的准确性。

  3. 反向传播(Backpropagation):通过梯度下降法或其他优化算法,反向传播损失,计算每一层的梯度,并根据梯度调整各层的权重。

  4. 权重更新(Weight Update):使用优化算法(如SGD、Adam等)根据梯度信息更新网络的权重,从而减少损失。

通过多次迭代(epoch),网络的权重逐渐调整到一个使得预测结果尽可能接近目标函数的状态,从而实现拟合目标函数的目的。

如果目标函数是 y=x2 ,神经网络的目标就是学习到一个映射关系,使得对于输入 x,网络的输出 y 能够尽可能地接近 x2假设我们有一个简单的神经网络(例如一个具有单层隐藏层的多层感知机),我们可以通过以下步骤来进行拟合:

1. 定义目标函数

我们要拟合的目标函数是 y=x2,这意味着给定一个输入 x,神经网络的目标输出应该是 x2

2. 设计神经网络结构

可以选择一个简单的神经网络,例如一个含有输入层、一个或多个隐藏层和输出层的全连接网络。假设输入为 x,输出为 y,其中 y 是神经网络的预测值。

  • 输入层:接受单一的 x 作为输入。
  • 隐藏层:可以有一个或多个神经元(为了能拟合复杂的非线性关系,可以使用非线性激活函数如 Leaky ReLU 或 Sigmoid)。
  • 输出层:输出一个标量值 y,即神经网络的预测值。

3. 损失函数

损失函数通常采用均方误差(MSE)来衡量预测值与真实值之间的差距:

其中 ypred(i) 是网络的预测值,ytrue(i)  是目标函数的真实值 x2 ,而 是样本数。

4. 反向传播与权重更新

  • 通过前向传播计算网络的输出(预测值)。
  • 通过损失函数计算损失值。
  • 使用反向传播算法计算梯度(相对于网络参数的梯度)。
  • 根据梯度更新网络中的权重,常用优化算法如梯度下降Adam来调整权重。

5. 训练过程

通过多次迭代(多个epoch),每次根据训练数据输入 x 更新权重,使得神经网络的输出越来越接近真实值 x2 

6. 结果

最终,经过足够的训练,神经网络会学到一个近似的关系,输出 ypred  将接近于 ytrue=x2 ,从而完成对目标函数 y=x2 的拟合。

一次迭代的详细过程

1. 初始化神经网络

首先,我们假设有一个非常简单的神经网络,结构如下:

  • 输入层:1个神经元(接收 xxx)
  • 隐藏层:2个神经元(使用 ReLU 激活函数)
  • 输出层:1个神经元(输出 yyy)

在训练开始时,网络中的权重和偏置是随机初始化的,或者采用某种默认值。假设初始化的权重和偏置如下:

  • 输入层到隐藏层的权重 W1  和偏置 b1 : 

  • 隐藏层到输出层的权重 W2 和偏置 b2: 

2. 选择一个训练样本

假设我们选择一个简单的训练样本 x=2 

3. 前向传播

步骤:

  • 输入 x=2 ,计算隐藏层的输出。 隐藏层的计算是:

    经过 ReLU 激活函数:

  • 计算输出层的输出:

    输出值为:

    预测结果:ypred=z2=0.49 ,目标结果:ytrue=4 ,这里如果求导(求梯度)。∂z2/∂a1=W2   ,  ∂z2/∂W2=a1 ,其中主要 ∂z2=ypre

求导过程

 对 W2 的求导

  对 a1 的求导

  对 b2 的求导

 

4. 计算损失函数

使用最小二乘来表示损失函数:

5. 反向传播

反向传播的目标是计算损失函数相对于各个参数(权重和偏置)的梯度,并使用这些梯度来更新权重。

(1) 计算输出层的梯度

首先,计算输出层的梯度:

为了得到 Loss 关于 ypred   的导数,我们应用链式法则。首先对平方项求导:

首先对外部的 1/2  常数进行求导,结果是:

因此,最终的导数是:

然后,计算输出层的梯度对权重 W2  和偏置 b2 的影响:

 

 

(2) 计算隐藏层的梯度

接下来,计算隐藏层的梯度。为了计算隐藏层的梯度,我们需要利用链式法则:

由于使用的是 ReLU 激活函数,我们还需要考虑 ReLU 的梯度(对正输入值的梯度为 1,对负值的梯度为 0):

然后计算隐藏层权重和偏置的梯度:

6. 更新权重

使用梯度下降法更新权重和偏置。假设学习率 α=0.1 ,那么更新后的权重和偏置为:

  • 更新输出层的权重和偏置:

  • 更新隐藏层的权重和偏置:

7. 重复训练

这个过程会持续进行多个 epoch,每次使用不同的训练样本计算前向传播、损失、反向传播并更新权重,直到网络的输出趋于目标结果

 通过迭代,反向传播不断优化权重,已经可以实现在-10,10之间最y=x2目标函数进行拟合。但是有些时候在训练过程中也会发生一些情况,导致最后预测函数是一条直线或者前半段和目标函数相近,但是后半段和目标函数差异过大。在训练过程中有时损失值在降低到一个数值之后无法下降。下面给出一些异常情况得最终预测函数得图像

使用权重过小作为默认权重和偏置

如果初始化的权重值过小,网络在训练时可能会因为激活函数(如ReLU)的饱和区域,导致梯度过小,进而无法进行有效的权重更新。这可能导致输出在某一阶段变成接近零的直线。

使用权重过大作为默认权重和偏置

初始化的权重和偏置过大,导致在前向传播和反向传播时激活函数的输入非常大,导致梯度爆炸或梯度消失,使得网络无法有效学习 如果使用的是 Sigmoid 或 Tanh 激活函数,输入过大也会导致函数的输出接近饱和区间(1 或 0 或 -1),梯度消失,导致网络无法更新权重,使得网络学习无效 如果使用的是 ReLU 或 Leaky ReLU,这些激活函数在输入非常大时可能会饱和,导致网络无法更新权重,最终结果趋向于一个平坦的直线。 如果权重和偏置值过大,神经网络的输出会失去多样性,特别是在隐藏层中,所有神经元的输出可能趋向于相同的值,导致网络无法学习到数据的多样性,从而只拟合出直线

附上测试代码

复制代码
import numpy as np
import matplotlib.pyplot as plt

# 创建训练数据:x 和 y = x^2
x_train = np.linspace(-10, 10, 1000)  # 从 -1010 的输入数据
y_train = x_train ** 2  # 目标输出是 x^2

# 神经网络参数初始化
input_size = 1  # 输入维度
hidden_size = 2  # 隐藏层神经元数量
output_size = 1  # 输出维度

# 随机初始化权重和偏置
W1 = np.random.randn(input_size, hidden_size)  # 输入到隐藏层的权重
b1 = np.random.randn(hidden_size)  # 隐藏层的偏置
W2 = np.random.randn(hidden_size, output_size)  # 隐藏层到输出层的权重
b2 = np.random.randn(output_size)  # 输出层的偏置

'''
# 自定义初始化权重和偏置
W1 = np.array([[6.42274909, -7.18634491]])
b1 = np.array([-9.94528287, -8.49532411])
W2 = np.array([[2.4350969], [5.61910466]])
b2 = np.array([5.458968])
'''
'''
# 使用 He 初始化权重和偏置
# He 初始化:标准差 = sqrt(2 / n_in)
W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2 / input_size)  # 输入到隐藏层的权重
b1 = np.zeros(hidden_size)  # 隐藏层的偏置(可以初始化为零)

W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2 / hidden_size)  # 隐藏层到输出层的权重
b2 = np.zeros(output_size)  # 输出层的偏置(可以初始化为零)
'''
'''
# 使用权重过小作为默认权重和偏置
# 如果初始化的权重值过小,网络在训练时可能会因为激活函数(如ReLU)的饱和区域,导致梯度过小,
# 进而无法进行有效的权重更新。这可能导致输出在某一阶段变成接近零的直线。
W1 = np.array([[-0.05841218, 0.0023438 ]])
b1 = np.array([-0.86497728, -0.107745  ])
W2 = np.array([[-0.88703299], [-0.27043475]])
b2 = np.array([33.39188819])
'''

'''
# 使用权重过大作为默认权重和偏置
# 初始化的权重和偏置过大,导致在前向传播和反向传播时激活函数的输入非常大,导致梯度爆炸或梯度消失,使得网络无法有效学习
# 如果使用的是 ReLU 或 Leaky ReLU,这些激活函数在输入非常大时可能会饱和,导致网络无法更新权重,最终结果趋向于一个平坦的直线。
# 如果使用的是 Sigmoid 或 Tanh 激活函数,输入过大也会导致函数的输出接近饱和区间(10 或 -1),使得网络学习无效
# 如果权重和偏置值过大,神经网络的输出会失去多样性,特别是在隐藏层中,所有神经元的输出可能趋向于相同的值,导致网络无法学习到数据的多样性,从而只拟合出直线
'''
'''
# 这是一个合适的权默认权重
W1 = np.array([[2.01811193, -1.96353533]])
b1 = np.array([-6.5184546, -6.34224452])
W2 = np.array([[6.61051412], [6.79430913]])
b2 = np.array([3.76652025])
'''

# 学习率
learning_rate = 0.0001  # 降低学习率

# 激活函数和它们的导数:Leaky ReLU
def leaky_relu(x, alpha=0.01):
    return np.where(x > 0, x, alpha * x)

def leaky_relu_derivative(x, alpha=0.01):
    return np.where(x > 0, 1, alpha)

# 选择激活函数
activation_function = leaky_relu
activation_function_derivative = leaky_relu_derivative

# 训练过程(使用梯度下降)
epochs = 60000
losses = []

# 训练前的预测
def predict(x):
    z1 = np.dot(x.reshape(-1, 1), W1) + b1  # 输入到隐藏层
    a1 = activation_function(z1)  # 隐藏层激活函数
    z2 = np.dot(a1, W2) + b2  # 隐藏层到输出层
    return z2

# 训练过程
for epoch in range(epochs):
    # 前向传播
    z1 = np.dot(x_train.reshape(-1, 1), W1) + b1  # 输入到隐藏层
    a1 = activation_function(z1)  # 隐藏层激活函数
    z2 = np.dot(a1, W2) + b2  # 隐藏层到输出层
    y_pred = z2  # 输出层的预测值

    # 计算损失(均方误差)
    loss = np.mean((y_pred - y_train.reshape(-1, 1)) ** 2)
    losses.append(loss)

    # 反向传播
    # 计算输出层的梯度
    d_loss_y_pred = 2 * (y_pred - y_train.reshape(-1, 1)) / y_train.shape[0]
    d_y_pred_z2 = 1
    d_loss_z2 = d_loss_y_pred * d_y_pred_z2

    # 输出层的梯度
    d_loss_W2 = np.dot(a1.T, d_loss_z2)
    d_loss_b2 = np.sum(d_loss_z2, axis=0)

    # 隐藏层的梯度
    d_z2_a1 = W2
    d_loss_a1 = d_loss_z2.dot(d_z2_a1.T)
    d_a1_z1 = activation_function_derivative(z1)
    d_loss_z1 = d_loss_a1 * d_a1_z1

    # 隐藏层的梯度
    d_loss_W1 = np.dot(x_train.reshape(-1, 1).T, d_loss_z1)
    d_loss_b1 = np.sum(d_loss_z1, axis=0)

    # 更新权重和偏置
    W1 -= learning_rate * d_loss_W1
    b1 -= learning_rate * d_loss_b1
    W2 -= learning_rate * d_loss_W2
    b2 -= learning_rate * d_loss_b2

    # 每1000次迭代输出一次损失
    if epoch % 1000 == 0:
        print(f'Epoch {epoch}, Loss: {loss}')

# 训练结束后的结果
print(f"Trained weights and biases:")
print("W1:", W1)
print("b1:", b1)
print("W2:", W2)
print("b2:", b2)

# 绘制损失曲线
plt.figure(figsize=(12, 6))

# 绘制损失函数图
plt.subplot(1, 2, 1)
plt.plot(losses)
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.title("Loss Curve")

# 绘制训练前和训练后的预测图
plt.subplot(1, 2, 2)
# 训练前的预测图
initial_pred = predict(x_train)
plt.plot(x_train, initial_pred, label="Initial Prediction (Random Weights)", color="red")
# 训练后的预测图
final_pred = predict(x_train)
plt.plot(x_train, final_pred, label="Final Prediction (Trained)", color="green")
# 真实函数图
plt.plot(x_train, y_train, label="True Function (y = x^2)", color="blue")
plt.title("Before vs After Training")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()

plt.tight_layout()
plt.show()
View Code
复制代码

深度神经网络在训练过程中,在若干迭代次后使损失向最小值缓慢收敛,但不能在有限得步数内非常精确地达到最小值。对于深度神经网络这样得复杂模型来说,损失平面上通常包含多个最小值。深度学习实践者很少会花费大力气去寻找一组参数来使损失达到最小值,事实上,更难做到是寻找一组参数使在未见过得数据上实现较小得损失,这一挑战称为“泛化”。

posted @   Hi同学  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示