前言

BP 神经网路作为深度学习中的一个重要网络框架,后面的网络都是基于 BP 神经网路,以下是来自 ChatGPT 的回答:

BP 神经网络(Backpropagation neural network)是一种基于误差反向传播算法的人工神经网络模型,常用于分类、回归等任务。它由输入层、隐藏层和输出层构成,其中隐藏层可以有多个。在 BP 神经网络中,神经元之间的连接权重是模型的参数,模型的训练就是通过调整权重来减小模型预测值与真实值之间的误差。

BP 神经网络的训练过程主要包括前向传播和反向传播两个阶段。前向传播时,输入样本经过输入层的神经元传递到隐藏层和输出层,每个神经元都计算一次加权和并通过激活函数进行非线性转换。输出层的预测值与真实值之间的误差被称为损失函数。

反向传播时,首先通过损失函数计算输出层的误差,并将误差反向传播回隐藏层和输入层。在每个神经元中,根据误差大小和神经元输入值计算对权重的偏导数,并使用梯度下降法对权重进行更新,以减小损失函数的值。

BP 神经网络需要大量的数据和计算资源来进行训练,同时容易出现过拟合问题,需要进行合理的正则化和超参数调整。然而,BP 神经网络仍然是一种基本的神经网络模型,被广泛应用于图像识别、自然语言处理、信号处理等领域。

BP 神经网络构造: 两层线性层+激活函数(两个线性层之间加了一个激活函数) - 第一层线性层:用于特征提取 - 第二层线性层:用于分类(分类器)

前导知识: 矩阵运算,矩阵求导

本文以 Mnist 手写数字识别讲解 BP 神经网络。

Mnist 数据集

数字识别是一个 10 分类问题。

分类流程

先将 28*28 的图片转为 1*784 的向量,再用这个向量乘以一个 W 矩阵,得到的结果可以判断出所属的分类。

One-hot

以下是 Chat-GPT 的介绍:

在机器学习和自然语言处理等领域中,one-hot 是一种将分类变量表示为二进制向量的技术。在这种表示方法中,每个分类变量被编码为一个唯一的二进制向量,其中只有一个元素为 1,其余元素为 0。这个唯一的元素所在的位置对应于该变量的类别。例如,如果有三个类别 A、B、C,它们可以分别表示为 (1, 0, 0)、(0, 1, 0)、(0, 0, 1) 三个向量。

使用one-hot表示方法有很多好处。首先,它允许我们对分类变量进行数学运算,例如计算向量之间的余弦相似度等。其次,one-hot向量不含有冗余信息,因此可以减少存储和计算的开销。最后,one-hot表示方法可以直接应用于许多机器学习算法中,如逻辑回归、支持向量机等。

需要注意的是,当类别数量很大时,使用one-hot表示方法会导致向量维度很高,这可能会导致计算和存储的问题。此外,当类别之间存在一定的层次关系时,one-hot表示方法可能不是最优的选择,因为它不能直接反映出类别之间的相似性或差异性。

one-hot 作用在一张图片中时,将一张图片的 label 数字变成一个向量。这样才方便做 Loss 和求导。

Softmax

常用于多分类,其公式为:\[S_i=\frac{e^{V_i}} {\sum_{j} e^{V_i}}\]

这里用指数来进行计算,是为了方便后面求导;此外,也是防止 V 是负数带来的计算的麻烦,这样也能作为正数在式子中运算。

利用 softmax 将预测出来的值映射成为 0~1 之间的向量(归一化),再拿处理过的向量和 label 向量进行 loss。

softmax 会放大大值,缩小小值,使得每个值之间差距变大。

BP 模型

单数据思路

act 是激活函数(sigmod/softmax),用来将数据映射到 0~1 之间。

多数据思路

Loss 对 W2 导数

想求 Loss 对 \(W_2\) 的导数⬅️求 Loss 对 P 导数⬅️求 Loss 对 pre 导数

先求出 Loss 对 pre 的导数,通过联合求导求出 Loss 对 p 的导数,再利用 \(A@B =C\) 的求导关系求出 Loss 对 \(W_2\) 的导数。

这里给出 Loss 对 P 的求导公式 :\[G_2=\frac{\partial Loss}{\partial P}=\frac{\partial Loss}{\partial pre}·\frac{\partial pre}{\partial P}=pre-Label\]

pre 和 Label 是对应位置相减

通过 \(A@B =C\) 的求导关系,求出 Loss 对 \(W_2\) 的求导公式:\[\bigtriangleup W_2=\frac{\partial Loss}{\partial W_2}=sig\_h^T@G_2\] 可以更新下一轮的 \(W_2\)\[W_2=W_2-lr·\frac{\partial Loss}{\partial W_2}\]

Loss 对 W1 导数

与求 Loss 对 \(W_2\) 导数同理,先求出 Loss 对 sig_h 的导数,再求出 Loss 对 h 导数,再求出 Loss 对 \(W_1\) 的导数。

\[\bigtriangleup sig\_h=\frac{\partial Loss}{\partial sig\_h}=sig\_h^T@G_2\]

\[\bigtriangleup h=G_1=\frac{\partial Loss}{\partial h}=\frac{\partial Loss}{\partial sig\_h}·\frac{\partial sig\_h}{\partial h}\]

其中 sigmoid 公式为:\[f=\frac {1} {1+e^{-x}}\]

sigmoid 的求导公式:\[f'=\frac {1} {1+e^{-x}}·(1-\frac {1} {1+e^{-x}})\]

所以 \(G_1\) 可以写为:\[\bigtriangleup h=G_1=\frac{\partial Loss}{\partial sig\_h}·[sig\_h·(1-sig\_h)]\]

所以 Loss 对 \(W_1\) 的导数为:\[\bigtriangleup W_1=\frac{\partial Loss}{\partial W_1}=X^T@G_1 \]

可以更新下一轮的 \(W_1\)\[W_1=W_1-lr·\frac{\partial Loss}{\partial W_1}\]

上面的两次求 Loss 对 W 的求解过程就是backward

复现

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import matplotlib.pyplot as plt  
import numpy as np
import struct

def load_labels(file): # 加载数据
with open(file, "rb") as f:
data = f.read()
return np.asanyarray(bytearray(data[8:]), dtype=np.int32)

def load_images(file): # 加载数据
with open(file, "rb") as f:
data = f.read()
magic_number, num_items, rows, cols = struct.unpack(">iiii", data[:16])
return np.asanyarray(bytearray(data[16:]), dtype=np.uint8).reshape(num_items, -1)

# 将label变成举证(60000*10)
def make_one_hot(labels,class_num=10):
result = np.zeros((len(labels),class_num))
for index,lab in enumerate(labels): # enumerate()函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据下标和数据
result[index][lab] = 1
return result

def sigmoid(x):
return 1/(1+np.exp(-x))

def softmax(x):
ex = np.exp(x) # 整个矩阵的每个元素都求指数
sum_ex = np.sum(ex, axis=1, keepdims=True) # 按行求指数结果的和,axis=1表示按行,keepdims=True表示保持原来形状
return ex/sum_ex


if __name__ == "__main__":
train_datas = load_images("data/train-images.idx3-ubyte") / 255 # (60000, 784)
train_label = make_one_hot(load_labels("data/train-labels.idx1-ubyte"),10) # # (60000,)
# print(train_datas.shape,train_label.shape) # print(train_label[0]) # 输出一行

# 测试集不做one_hot
test_datas = load_images("data/t10k-images.idx3-ubyte") / 255
test_label = load_labels("data/t10k-labels.idx1-ubyte")

epoch = 100
batch_size = 600 # 一次性处理多少图片
lr = 0.01

hidden_num = 256 # 隐层大小
w1 = np.random.normal(0,1,size=(784,hidden_num))
w2 = np.random.normal(0,1,size=(hidden_num,10))

batch_times = int(np.ceil(len(train_datas) / batch_size)) # np.ceil 向上取整

for e in range(epoch):
for batch_index in range(batch_times):

batch_x = train_datas[batch_index * batch_size : (batch_index + 1) * batch_size] # 按行为单位取出,每次取batch_size行
batch_label = train_label[batch_index * batch_size : (batch_index + 1) * batch_size]

# forward
h = batch_x @ w1
sig_h = sigmoid(h)
p = sig_h @ w2
pre = softmax(p)

# 计算数据
loss = -np.mean(batch_label * np.log(pre))/batch_size # 求平均loss

# backward G2 = (pre - batch_label)/batch_size # backward都和G2有关,G2会因为batch_size过大而梯度爆炸,这里除以batch_size可以避免梯度爆炸,
delta_w2 = sig_h.T @ G2
delta_sig_h = G2 @ w2.T
delta_h = delta_sig_h * sig_h * (1 - sig_h) # 1-sig_h是sig_h中每个元素都被1减
delta_w1 = batch_x.T @ delta_h

# 更新梯度
w1 = w1 - lr * delta_w1
w2 = w2 - lr * delta_w2

# 利用测试集计算精确度
h = test_datas @ w1
sig_h = sigmoid(h)
p = sig_h @ w2
pre = softmax(p) #pre是一个10000行1列的向量
# print(pre.shape)
pre = np.argmax(pre, axis=1) # 取一行最大值的下标,最终的pre是一个10000行1列的向量
# print(pre.shape)

acc = np.sum(pre==test_label)/10000

print(acc)

# 画图
# t = train_datas[1107]
# plt.imshow(t.reshape(28,28)) # plt.show() # print(train_label[1037])

相关链接

Bilibili
GitHub