原理介绍
写上了,待我补充上来,绝对不长,这里注重编码实现,所只介绍关键数据!!!
首先,前向传播:
s ( 1 ) = W T ( 1 ) X ( 0 ) − b ( 1 ) X ( 1 ) = ϕ ( s ( 1 ) ) s ( 2 ) = W T ( 1 ) X ( 1 ) − b ( 2 ) X ( 2 ) = ϕ ( s ( 2 ) ) \begin{aligned} s^{(1)} &= W^{T(1)}X^{(0)} - b^{(1)} & \quad X^{(1)} = \phi(s^{(1)}) \\ s^{(2)} &= W^{T(1)} X^{(1)} - b^{(2)} & \quad X^{(2)} = \phi(s^{(2)}) \end{aligned} s(1)s(2)=WT(1)X(0)−b(1)=WT(1)X(1)−b(2)X(1)=ϕ(s(1))X(2)=ϕ(s(2))
其次,后向传播,更新参数:
后向后弦传播关于
s
s
s的导数,也即
∂
C
∂
s
\frac{\partial C}{\partial s}
∂s∂C:
先写出一个通用的:
δ
(
L
)
=
(
X
L
−
Z
)
⊙
ϕ
′
(
s
(
L
)
)
δ
(
l
−
1
)
=
(
W
(
l
)
s
(
l
)
)
⊙
ϕ
(
s
(
l
−
1
)
)
\begin{aligned} \delta^{(L)} = (X^{L} - Z) \odot \phi'(s^{(L)}) \\ \delta^{(l-1)} = (W^{(l)} s^{(l)}) \odot \phi(s^{(l-1)}) \\ \end{aligned}
δ(L)=(XL−Z)⊙ϕ′(s(L))δ(l−1)=(W(l)s(l))⊙ϕ(s(l−1))
其中
δ
(
L
)
=
∂
C
∂
s
(
L
)
,
δ
(
l
−
1
)
=
∂
C
∂
s
(
l
−
1
)
\delta^{(L)} = \frac{\partial C}{\partial s^{(L)}}, \delta^{(l-1)} = \frac{\partial C}{\partial s^{(l-1)}}
δ(L)=∂s(L)∂C,δ(l−1)=∂s(l−1)∂C.
C
C
C是你所选择的损失函数,这里我选择的损失函数是平方损失函数,即
C
=
(
Y
−
Z
)
2
C = (Y - Z)^2
C=(Y−Z)2,
Y
Y
Y是你前向传播得到的一个输出的估计,
Z
Z
Z是你的理想输出,也就是你需要模拟的真实值的输出.
再写一下这里我用的两层的(是前面通用的一个两层实例):
δ
(
2
)
=
(
X
2
−
Z
)
⊙
ϕ
′
(
s
(
2
)
)
δ
(
1
)
=
(
W
(
2
)
s
(
2
)
)
⊙
ϕ
(
s
(
1
)
)
\begin{aligned} \delta^{(2)} = (X^{2} - Z) \odot \phi'(s^{(2)}) \\ \delta^{(1)} = (W^{(2)} s^{(2)}) \odot \phi(s^{(1)}) \\ \end{aligned}
δ(2)=(X2−Z)⊙ϕ′(s(2))δ(1)=(W(2)s(2))⊙ϕ(s(1))
刚介绍的
δ
(
L
)
\delta^{(L)}
δ(L)和
δ
(
l
−
1
)
\delta^{(l-1)}
δ(l−1)都是为了后来求损失函数关于未知参数系数向量
ω
\omega
ω和偏置
b
b
b而作准备的。
下面继续介绍损失函数关于
W
W
W和
b
b
b的偏导数,这一步是为了后面适用梯度下降法更新参数
W
W
W和
b
b
b做准备的。
∂
C
∂
W
(
l
)
=
X
(
l
−
1
)
δ
(
l
)
T
∂
C
∂
B
(
l
)
=
−
δ
(
l
)
\begin{aligned} &\frac{\partial C}{\partial W^{(l)}} = X^{(l-1)} \delta^{(l)T} \\ &\frac{\partial C}{\partial B^{(l)}} = -\delta^{(l)}\\ \end{aligned}
∂W(l)∂C=X(l−1)δ(l)T∂B(l)∂C=−δ(l)
下面继续按照前面的套路写一下损失函数
C
C
C关于第二层和第一层的
W
W
W和
B
B
B求偏导的过程:
∂
C
∂
W
(
2
)
=
∂
C
∂
s
(
2
)
⋅
∂
s
(
2
)
∂
W
(
2
)
=
X
(
1
)
δ
(
2
)
T
∂
C
∂
b
(
2
)
=
∂
C
∂
s
(
2
)
⋅
∂
s
(
2
)
∂
b
(
2
)
=
−
δ
(
2
)
∂
C
∂
W
(
1
)
=
∂
C
∂
s
(
1
)
⋅
∂
s
(
1
)
∂
W
(
1
)
=
X
(
0
)
δ
(
1
)
T
∂
C
∂
b
(
1
)
=
∂
C
∂
s
(
1
)
⋅
∂
s
(
1
)
∂
b
(
1
)
=
−
δ
(
1
)
\begin{aligned} & \cfrac{\partial C}{\partial W^{(2)}} = \cfrac{\partial C}{\partial s^{(2)}} \cdot \cfrac{\partial s^{(2)}}{\partial W^{(2)}} = X^{(1)} \delta^{(2)T} \\ & \cfrac{\partial C}{\partial b^{(2)}} = \cfrac{\partial C}{\partial s^{(2)}} \cdot \cfrac{\partial s^{(2)}}{\partial b^{(2)}} = - \delta^{(2)} \\ & \cfrac{\partial C}{\partial W^{(1)}} = \cfrac{\partial C}{\partial s^{(1)}} \cdot \cfrac{\partial s^{(1)}}{\partial W^{(1)}} = X^{(0)} \delta^{(1)T} \\ & \cfrac{\partial C}{\partial b^{(1)}} = \cfrac{\partial C}{\partial s^{(1)}} \cdot \cfrac{\partial s^{(1)}}{\partial b^{(1)}} = - \delta^{(1)} \\ \end{aligned}
∂W(2)∂C=∂s(2)∂C⋅∂W(2)∂s(2)=X(1)δ(2)T∂b(2)∂C=∂s(2)∂C⋅∂b(2)∂s(2)=−δ(2)∂W(1)∂C=∂s(1)∂C⋅∂W(1)∂s(1)=X(0)δ(1)T∂b(1)∂C=∂s(1)∂C⋅∂b(1)∂s(1)=−δ(1)
其中梯度下降法是这样更新的:
这里的
f
f
f是一个关于
x
x
x的函数。对应到我们这里就是:
W
(
n
+
1
)
=
W
(
n
)
−
η
∂
C
∂
W
b
(
n
+
1
)
=
b
(
n
)
−
η
∂
C
∂
b
\begin{aligned} W(n+1) = W(n) - \eta \cfrac{\partial C}{\partial W} \\ b(n+1) = b(n) - \eta \cfrac{\partial C}{\partial b} \end{aligned}
W(n+1)=W(n)−η∂W∂Cb(n+1)=b(n)−η∂b∂C
之前已经计算出了偏导,将偏导的值代入我们就可以使用梯度下降法来更新
W
W
W和
b
b
b啦:
W
(
2
)
(
n
+
1
)
=
w
(
2
)
(
n
)
−
η
⋅
X
(
1
)
(
n
)
⋅
δ
(
2
)
(
n
)
b
(
2
)
(
n
+
1
)
=
b
(
2
)
(
n
)
+
η
⋅
δ
(
2
)
(
n
)
W
(
1
)
(
n
+
1
)
=
w
(
1
)
(
n
)
−
η
⋅
X
(
0
)
(
n
)
⋅
δ
(
1
)
(
n
)
b
(
1
)
(
n
+
1
)
=
b
(
1
)
(
n
)
+
η
⋅
δ
(
1
)
(
n
)
\begin{aligned} & W^{(2)}(n+1) = w^{(2)}(n) - \eta \cdot X^{(1)}(n) \cdot\delta^{(2)}(n) \\ & b^{(2)}(n+1) = b^{(2)}(n) + \eta \cdot \delta^{(2)}(n) \\ & W^{(1)}(n+1) = w^{(1)}(n) - \eta \cdot X^{(0)}(n) \cdot\delta^{(1)}(n) \\ & b^{(1)}(n+1) = b^{(1)}(n) + \eta \cdot \delta^{(1)}(n) \\ \end{aligned}
W(2)(n+1)=w(2)(n)−η⋅X(1)(n)⋅δ(2)(n)b(2)(n+1)=b(2)(n)+η⋅δ(2)(n)W(1)(n+1)=w(1)(n)−η⋅X(0)(n)⋅δ(1)(n)b(1)(n+1)=b(1)(n)+η⋅δ(1)(n)
到这里,前向传播和后向传播的计算理论已经结束了。
此外,我还想添加一些东西:
损失函数的选择,样本输入,更新参数的问题
在我自己写代码的过程中,发现输入 N N N个训练样本不知道怎么传到神经网络中去,困惑点在于,两点:
- 我是一个样本进入一次网络,前向传播输出值,然后后向传播更新参数,每次都按照当前的梯度下降的方法来更新很多次参数;
- 还是说先我把输入就当作是N个神经元,这样来针对所有的输入来更新参数呢?
答案是什么?
两者都不是!!!
那到底是什么呀?
呜呜,我也不知道,我还要回去看我的代码,呜呜
呜呜,我知道了,关键点在于设置损失函数,它取为MSE,说我也说不清楚,我写个公式:
C
o
s
t
=
M
S
E
=
1
2
N
∑
i
=
1
N
(
Y
i
−
Z
i
)
2
Cost = MSE = \cfrac{1}{2N} \sum\limits_{i=1}^{N}(Y_i - Z_i)^2
Cost=MSE=2N1i=1∑N(Yi−Zi)2
均方误差的话,之前的分母上为啥要加上一个2呀?
之前在后向传播的时候用到了
C
C
C关于
W
W
W和
b
b
b的求导,求导,一个平方项有一个2,乘以前面的
1
2
\frac{1}{2}
21不就使得前面的常数系数为1了,多舒服。在代码中我有写了,只是有些蠢罢了,待我以后更新!!!
载入数据
在utils.py
文件中
"""
生成数据
"""
import numpy as np
import pandas as pd
def load_data(n=1000):
np.random.seed(0)
X = np.random.randn(n) * 10
Z = np.tan(X)
# X: input, Z: target ouput
return X, Z
前向传播实现
暂时在bp-matrix.py
中
"""
1. 初始化网络结构,参数、训练函数,训练次数
2. 输入训练样本,训练网络
3. 前向过程,对比实际输出与期望输出,计算MSE
4. 后向传播过程,使用梯度下降法,调整网络参数
5. 调整参数之后,计算MSE
6. 指定终止条件,如果满足,停止,不满足到第 3步骤
"""
import numpy as np
from numpy.core.defchararray import replace
import pandas as pd
from utils import *
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
class NotMatch(Exception):
pass
class BackPropagation:
def __init__(self, X, Z):
self.X = np.array(X)
self.Z = np.array(Z)
self.init_paras()
# 初始化权重、偏差(阈值)、训练的次数、学习率、episilon
def init_paras(self):
self.layer_size = [1, 10, 1] # 输入一个,输出一个,两层,中间隐藏层有10个(3个)隐藏的神经元
self.L = len(self.layer_size) - 1
self.Weights = []
self.Bias = []
for i in range(len(self.layer_size)-1):
self.Weights.append(
(
np.random.randn(self.layer_size[i]
* self.layer_size[i+1]) * 1 / np.sqrt(self.layer_size[i] / 2)
).reshape(self.layer_size[i], self.layer_size[i+1])
)
self.Bias.append((np.random.randn(self.layer_size[i+1])).reshape(-1, 1))
self.time_lim = 200
self._active = self.sig
self.eta = 0.15
self.epsilon = 0.1
# sigmoid 激活函数
def sig(self, x):
return 1 / (1 + np.exp(-x))
# ReLU 激活函数
def ReLU(self, x):
return np.vectorize(lambda x: max(x, 0))(x)
# 对激活函数求导
def de_phi(self, x):
if self._active == self.ReLU:
return np.vectorize(lambda x: 1 if x >=0 else 0)(x)
elif self._active == self.sig:
return self.sig(x) * (1 - self.sig(x))
def unit_loss(self, Y, Z):
return 0.5 * np.sqrt(np.sum((Y-Z) ** 2))
# 损失函数,这里是MSE
def loss(self, Y, Z, w_i, b_i):
Y = np.array(Y)
Z = np.array(Z)
# 气死我了,我居然不会适用这个NotMach异常!!!
# if len(Y) != len(Z):
# raise NotMatch("Y and Z must be the same length!")
actual_loss = 0
for i, x_i in enumerate(self.X):
_, x_out = self.forward_net(x_i, w_i, b_i)
actual_loss += self.unit_loss(x_out[-1], self.Z[i])
actual_loss = actual_loss / len(self.X)
actual_loss = actual_loss
return actual_loss
# 前向传播,仅仅计算输出值
def forward_net(self, X, w_i, b_i):
# 变成二维,方便之后进行矩阵运算
try:
# 输入是一维数据
X = np.array(X).reshape(1, 1)
except:
# 输入是多维数据
X = np.array(X).reshape(-1, 1)
S = []
X_out = []
for l in range(self.L):
# l = l + 1
# 此处的l是0, 1
# 实际上是1, 2
if l == 0:
s_l = (w_i[l].T @ X - b_i[l]).ravel()
else:
s_l = (w_i[l].T @ X_out[-1] - b_i[l]).ravel()
X_1 = self._active(s_l)
S.append(s_l)
X_out.append(X_1)
return S, X_out
# 后向传播
# 损失函数是所有输入数据的MSE
def backward_net(self, w_i, b_i):
# 初始化delta, C_de_W, C_de_b
delta = [0] * self.L
C_de_W = [0] * self.L
C_de_b = [0] * self.L
for i, x_i in enumerate(self.X):
S, X_out = self.forward_net(x_i, w_i, b_i)
for l in range(self.L-1, -1, -1):
if l == self.L - 1:
delta_l = (X_out[l] - self.Z[i]) * self.de_phi(S[l])
delta_l = delta_l.reshape(-1, 1)
else:
delta_l = (w_i[-1] @ delta[-1]).T * self.de_phi(S[l])
delta_l = delta_l.reshape(len(delta_l.ravel()), 1)
delta[l] = delta_l
# 计算关于w, b 的偏导
X_out_tmp = X_out[l-1].reshape(-1, 1)
C_de_W[l] += X_out_tmp @ delta_l.T
C_de_b[l] += -delta_l
# 按照1, ..., L 排序
C_de_W = [C_de_Wi / len(self.X) for C_de_Wi in C_de_W]
C_de_b = [C_de_bi / len(self.X) for C_de_bi in C_de_b]
# 其实delta不用传到后面的计算中去, 那么, 不传
return C_de_W, C_de_b
# 更新权重w, b
def update_wb(self):
w_i = self.Weights
b_i = self.Bias
# 初始化后向传播,计算C_de_W, C_de_b
# 这里的损失函数是输入数据的MSE
C_de_W, C_de_b = self.backward_net(w_i, b_i)
i = 1
MSE_plot = []
while i < self.time_lim: # 1, 2, ...
# 更新权重
for l in range(self.L-1, -1, -1): # l: 1, 0
b_i[l] -= self.eta * C_de_b[l]
w_i[l] -= self.eta * C_de_W[l]
# S, X_out = self.forward_net(self.X, w_i, b_i)
# 这里需要输出X_out, 将所有的输出的平方和都相加,即MSE
# 判断是否要跳出循环
MSE = self.loss(self.X, Z, w_i, b_i)
if MSE < self.epsilon:
break
# 存储上一步计算得到的偏导,这里的偏导数是loss的MSE分别对W, b 求偏导
C_de_W, C_de_b = self.backward_net(w_i, b_i)
i += 1
# break
MSE_plot.append(MSE)
plt.plot(MSE_plot)
plt.show()
# print(MSE) # 输出方便检查
return MSE
# 整合流程,执行函数
def do_net(self):
print(self.update_wb())
# def load_data(n=100):
# np.random.seed(0)
# X = np.random.randn(n) * 10
# Z = np.tan(X)
# # X: input, Z: target ouput
# return X, Z
if __name__ == "__main__":
X, Z = load_data()
a = BackPropagation(X, Z)
a.do_net()
还没有测试数据来测试模型咋样,呜呜,我又没完了。
待完善
- 添加原理介绍
- 添加代码思想介绍
- 添加测试集的测试
- 添加关于对收敛性的研究(画图,原理补充,使得模型的误差收敛)
参考
使用课本:
[1] Ovidiu Calin. Deep Learning Architectures
A Mathematical Approach.Springer Nature Switzerland AG 2020.
初始化权重问题:
深度学习中神经网络的几种权重初始化方法
原理问题:
神经网络BP反向传播算法原理和详细推导流程
讲述为什么要使用激活函数,我觉着很棒!
深度学习知识点整理(二)——神经网络理解 / 反向传播 / 激活函数 / 神经网络优化