assignment_1_BP算法

算法简介

BP算法,全称为反向传播算法(Backpropagation),是训练人工神经网络(Artificial Neural Network,ANN)的一种有效且广泛应用的方法。它是一种监督学习算法,用于根据输入数据和对应的目标输出数据,调整神经网络的权重,使得网络能够对输入数据做出正确的预测。

算法原理

BP算法,即误差反向传播算法(Back Propagation),是一种在神经网络中用于训练多层前馈神经网络的监督学习算法。它通过两个主要的过程来实现:正向传播和误差反向传播。

  • 正向传播:在这个阶段,输入数据在网络中向前流动,经过每一层的神经元,每一层的输出成为下一层的输入。每一层的节点输出是通过对输入进行加权求和后,通过一个激活函数进行非线性转换得到的。如果输出层的实际输出与期望输出不符,则进入下一个阶段。
  • 误差反向传播:这个阶段是BP算法的核心,目的是调整网络中的权重和偏置,以减少输出误差。首先计算输出层的误差梯度,然后根据链式法则将这个误差反向传播回网络,逐层计算每个节点的误差贡献,并更新权重和偏置。这个过程涉及到梯度的计算,通常使用梯度下降法来最小化损失函数。

BP算法的关键是确定隐含层节点的数量,这通常需要一些经验。有一个经验公式可以帮助确定隐含层节点数目,但实际应用中可能需要根据具体问题进行调整。

算法步骤(个人理解)

BP算法是神经网络中加速更新训练参数过程的一个算法,通过结合链式法则,从而减少计算机重复计算的次数,进而提高神经网络的训练速度,是损失函数快速收敛

假设构建一个如下图的神经网络:(第0层是输入层(2个神经元),第1层是隐含层(3个神经元),第2层是隐含层(2个神经元),第3层是输出层。)

pk4Q528.png

我们抽出一条路径分析:

pk4lsJ0.png

  • 输入:\(X\)
  • 线性变换:\(Y_i = f(X) = W_i * X + b_i\)
  • 非线性变换:(\(sigmoid\)函数)\(\sigma(x)=\frac{1}{1+e^{-x}}\)
  • 损失函数\(Loss = loss(x,y)=1/n\sum(y_i-y'_i)^2\)
  • 真实值:\(y'_i\)

BP算法的实现涉及到多个步骤:

  • 初始化网络中的权重和偏置。

  • 进行前向传播,计算输出值。

    • input : \(X\)
    • 线性变换1:\(Y_1 = W_1 * X + b_1\)
    • 非线性变换1:\(Z_1 = \frac{1}{1+e^{-Y_1}}\)
    • 线性变换2:\(Y_2 = W_2 * Z_1 + b_2\)
    • 非线性变换2:\(Z_2 = \frac{1}{1+e^{-Y_2}}\)
  • 计算输出误差,并根据这个误差进行反向传播。

    • 损失函数计算:$Loss = 1/2 * (z_2-y’)^2 $
  • 使用链式法则计算每个权重对损失函数的贡献。

    • \(d(loss)/d(w_2) = d(Loss)/d(z_2) * d(z_2)/d(y_2) * d(y_2)/d(w_2)\)
    • \(d(loss)/d(w_1) = ....\)
    • \(d(loss)/d(b_2) = d(Loss)/d(z_2) * d(z_2)/d(y_2) * d(y_2)/d(b_2)\)
    • \(d(loss)/d(b_1) = ....\)
  • 更新权重和偏置以减少误差。

    • \(w_i = w_i - \eta * d(loss)/d(w_i)\)
    • \(b_i = b_i - \eta * d(loss)/d(b_i)\)

算法实现

法一:模拟BP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import numpy as np

## 随时函数
def Loss_func(y1, y2):
return np.sum(np.square(y1 - y2) / 2)

## 激活函数
def sigmoid(x):
return 1 / (1 + np.exp(-1 * x))

## 激活函数导数
def sigmoid_ds(x):
s = sigmoid(x)
return s * (np.ones(s.shape) - s)

## 损失函数导数
def LossFunc_ds(y1, y2):
return y1 - y2

## 前向传播过程
def forward(w1, w2, b1, b2, x, y):
y1 = np.matmul(x, w1) + b1
z1 = sigmoid(y1)
y2 = np.matmul(z1, w2) + b2
z2 = sigmoid(y2)
loss = Loss_func(z2, y)
return y1, z1, y2, z2, loss

## BP算法
def backward_update(epochs, learn_rate):
x = np.random.random((2,2))
y = np.random.randint((1,2))
w1 = np.random.random((2,3))
b1 = np.random.random((1,3))
w2 = np.random.random((3,2))
b2 = np.random.random((1,2))
# 前向传播
y1, z1, y2, z2, loss = forward(w1, w2, b1, b2, x, y)
# 开始迭代
for i in range(epochs):
ds2 = LossFunc_ds(z2, y) * sigmoid_ds(y2)
# 将z1看作输入dw2 = dL/dw2 = dL/dy2 * dy2/dw2 = dL/dz2 * dz2/dy2 * dy2/dw2
dw2 = np.matmul(z1.T, ds2)
# db2 = dL/db2 = dL/dy2 * dy2/db2 = dL/dz2 * dz2/dy2 * dy2/db2(1)
# 偏置值求和,将每个神经元的误差合并得到总的偏置误差梯度
db2 = np.sum(ds2, axis=0)
dx = np.matmul(ds2, w2.T)
ds1 = sigmoid_ds(y1) * dx
dw1 = np.matmul(x.T, ds1)
db1 = np.sum(ds1, axis=0)
# 更新偏置值和系数
w1 = w1 - learn_rate * dw1
b1 = b1 - learn_rate * db1
w2 = w2 - learn_rate * dw2
b2 = b2 - learn_rate * db2

y1, z1, y2, z2, loss = forward(w1, w2, b1, b2, x, y)
if i % 100 == 0:
print('训练轮数:' , i / 100)
print('当前损失:{:.4f}'.format(loss))
print("------")
print(z2)
# sigmoid激活函数将结果大于0.5的值分为正类,小于0.5的值分为负类
z2[z2 > 0.5] = 1
z2[z2 < 0.5] = 0
print(z2)
print("------------------------------")

## 测试算法
if __name__ == '__main__':
backward_update(30001, 0.02)

法二:使用类来定义一个神经网络并实现BP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import numpy as np

class BackPropagation:
# 初始化各层数量
def __init__(self, input_n, hidden_n, output_n):
self.input_n = input_n
self.hidden_n = hidden_n
self.output_n = output_n
# 随机初始化权重和阈值
self.i_h_weight = np.random.rand(self.input_n, self.hidden_n)
self.h_o_weight = np.random.rand(self.hidden_n, self.output_n)
self.h_threshold = np.random.rand(1, self.hidden_n)
self.o_threshold = np.random.rand(1, self.output_n)

# 激活函数sigmoid function
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))

# sigmiod函数求导
def sigmoid_derivative(self, x):
return x * (1 - x)

# 向前传播forward propagation
def forward_propagation(self, data_train):
# 输入层到隐藏层
hiddens_input = np.dot(data_train, self.i_h_weight) - self.h_threshold
hiddens_output = self.sigmoid(hiddens_input)
# 隐藏层到输出层
outputs_input = np.dot(hiddens_output, self.h_o_weight) - self.o_threshold
outputs_output = self.sigmoid(outputs_input)
return hiddens_output, outputs_output


# 向后传播back propagation
def backpropagation(self, hiddens_output, outputs_output, data_train, data_labels, learning_rate):
# 计算输出层的误差
output_error = data_labels - outputs_output
output_delta = output_error * self.sigmoid_derivative(outputs_output)

# 计算隐藏层的误差
hidden_error = output_delta.dot(self.h_o_weight.T)
hidden_delta = hidden_error * self.sigmoid_derivative(hiddens_output)

# 更新隐藏层到输出层的权重和阈值
self.h_o_weight += hiddens_output.T.dot(output_delta) * learning_rate
self.o_threshold -= np.sum(output_delta, axis=0, keepdims=True) * learning_rate

# 更新输入层到隐藏层的权重和阈值
self.i_h_weight += np.dot(data_train.T, hidden_delta) * learning_rate
self.h_threshold -= np.sum(hidden_delta, axis=0, keepdims=True) * learning_rate

def fit(self, data_train, data_labels, epochs=10, learning_rate=0.01):
for epoch in range(epochs):
hiddens_output, outputs_output = self.forward_propagation(data_train)
self.backpropagation(hiddens_output, outputs_output, data_train, data_labels, learning_rate)
_, output = self.forward_propagation(data_train)
return output

BP算法的局限性

收敛时是局部最小值

当有不止一个极小值时,参数调整到最近的极小值便停止更新了,而该极小值未必全局最优

梯度消失

梯度消失原因是链式求导,导致梯度逐层递减,我们BP第一节推倒公式时就是通过链式求导把各层连接起来的,但是因为激活函数是sigmod函数,取值在1和-1之间,因此每次求导都会比原来小,当层次较多时,就会导致求导结果也就是梯度接近于0

参考


assignment_1_BP算法
https://fu-jingqi.github.io/2024/07/12/assignment-1-BP算法/
作者
coderfjq
发布于
2024年7月12日
许可协议