神经网络是深度学习的基础,最重要的就是熟悉里面的前向传播和反向传播。本文首先从理论切入,首先讲了前向传播,对于反向传播部分,分为了两个部分分别是简单模型(两层)和扩展模型(多层),原理之后是代码部分。
有错误的地方欢迎指出~
1.原理
神经网络是机器学习里面非常重要的模型,下面就讲一讲具体的原理。
下面是参考博客:
1. CSND,推导有点抽象,反向传播有数字例子,比较详细,就是看起来有点累,都是图片
2. 博客园,推导比较详细
最简单的神经网络由三层组成,分别对应输入层、隐藏层、输出层,最主要的训练过程包含两个概念,分别是前向传播和反向传播,下面分别叙述。
1.1前向传播
假设存在这样一个神经网络,共有三层架构,分别对应了输入层(0)、隐藏层(1)和输出层(2),输入层对应的其实就是输入的维度,这里假设输入的是
I
×
1
I\times 1
I×1矩阵,隐藏层有
J
J
J个神经元,输出维度为
K
×
1
K\times 1
K×1。之所以下标从0开始,是因为这里的第一层就是单纯的输入,没有经过处理,我觉得不应该作为第一层(
其实毫无根据 )。
前向传播顾名思义就是向前传播,这里的前就是输入到输出。对于非输入层的神经元,这里就以这里的输入层(0)和隐藏层(1)为例,设两层之间的所有神经元都建立一个连接,系数记为
w
i
j
,
i
=
1
,
.
.
.
I
,
j
=
1
,
2
,
.
.
.
,
J
w_{ij},i=1,...I,j=1,2,...,J
wij,i=1,...I,j=1,2,...,J,第
1
1
1层第
j
j
j个神经元的输出记为
O
1
,
j
O_{1,j}
O1,j,第一层每个神经元对应一个偏置
b
1
,
j
b_{1,j}
b1,j。对于每一个隐藏层(1)的神经元,其输出
O
1
,
j
O_{1,j}
O1,j如下:
O
1
,
j
=
g
(
N
(
O
0
)
)
N
(
O
0
)
=
∑
i
=
1
I
w
i
j
O
0
,
i
+
b
1
,
j
O_{1,j}=g(N(O_{0}))\\ N(O_{0})=∑_{i=1}^{I}w_{ij}O_{0,i}+b_{1,j}
O1,j=g(N(O0))N(O0)=i=1∑IwijO0,i+b1,j其实
N
(
O
0
)
N(O_{0})
N(O0)就是对连接到第
j
j
j个点的所有神经元的输出
O
0
,
i
O_{0,i}
O0,i(也就是输入
x
x
x)进行加权求和。如图,每个隐藏层(1)的神经元都和输入层(0)的所有神经元有一个连接系数
w
i
j
w_{ij}
wij,将所有输入值加权求和(
∑
i
=
1
I
w
i
j
O
0
,
i
+
b
1
,
j
∑_{i=1}^{I}w_{ij}O_{0,i}+b_{1,j}
∑i=1IwijO0,i+b1,j),激活后就得到了隐藏层的输出。
g
(
x
)
g(x)
g(x)为激活函数,常用的有
t
a
n
h
、
s
i
g
m
o
i
d
tanh、sigmoid
tanh、sigmoid等函数,这里就以
s
i
g
m
o
i
d
sigmoid
sigmoid函数为例,记为
σ
\sigma
σ,表达式如下:
σ
(
x
)
=
1
1
+
e
−
x
\sigma(x)=\frac{1}{1+e^{-x}}
σ(x)=1+e−x1这个函数有一个比较好的性质,也就是求导的值
σ
′
(
x
)
\sigma'(x)
σ′(x)可以由求导前的函数值
σ
(
x
)
\sigma(x)
σ(x)表示:
σ
′
(
x
)
=
e
x
(
1
+
e
−
x
)
2
=
1
+
e
−
x
−
1
(
1
+
e
−
x
)
2
=
1
1
+
e
−
x
−
1
(
1
+
e
−
x
)
2
=
1
1
+
e
−
x
(
1
−
1
1
+
e
−
x
)
=
σ
(
x
)
(
1
−
σ
(
x
)
)
\sigma'(x)=\frac{e^x}{(1+e^{-x})^2}\\ =\frac{1+e^{-x}-1}{(1+e^{-x})^2}\\ =\frac{1}{1+e^{-x}}-\frac{1}{(1+e^{-x})^2}\\ =\frac{1}{1+e^{-x}}(1-\frac{1}{1+e^{-x}})\\ =\sigma(x)(1-\sigma(x))
σ′(x)=(1+e−x)2ex=(1+e−x)21+e−x−1=1+e−x1−(1+e−x)21=1+e−x1(1−1+e−x1)=σ(x)(1−σ(x))这使得对于
σ
(
x
)
\sigma(x)
σ(x)的求导变得非常简单。
对于隐藏层和输出层,其实和前面说的输入层和隐藏层类似,不同之处就是把输入换成了隐藏层的输出。
(具体的例子)
1.2.反向传播
下面是相对复杂的反向传播部分。正向传播就是基于所有的权重计算出相应的输出,然而一开始不知道正确的权重,那么输出和实际的答案一定是大相径庭,那么如何对照答案来更新参数,最主流的做法就是利用反向传播。
1.2.1.基本模型
![](https://img-blog.csdnimg.cn/direct/af410277c96e4b23b11cc422570e9179.jpeg)
输入层(0)有
J
J
J个神经元,输出层(1)有
K
K
K个神经元,直接对应了输入和输出的维度,每个输入和输出之间有一个系数
w
j
k
w_{jk}
wjk连接,第1层有偏置,第
k
k
k个偏置记为
b
1
,
k
b_{1,k}
b1,k。
这里引入损失函数的概念,所有的优化问题最后会归结为一个式子,记为
L
(
o
u
t
p
u
t
,
l
a
b
e
l
)
L(output,label)
L(output,label),这个函数由输出(
o
u
t
p
u
t
output
output)和目标输出(
l
a
b
e
l
label
label)计算出一个值,这个值指明了优化的方向,一般来说这个值越小越好。
在这里,我们希望的是输出和目标输出越接近越好,记:
L
(
o
u
t
p
u
t
,
l
a
b
e
l
)
=
1
2
∑
k
=
1
K
(
o
u
t
p
u
t
k
−
l
a
b
e
l
k
)
2
L(output,label)=\frac{1}{2}∑_{k=1}^{K}(output_k-label_k)^2
L(output,label)=21k=1∑K(outputk−labelk)2也就是这个值越小越好。
为了更新系数
w
j
k
w_{jk}
wjk和偏置
b
1
,
k
b_{1,k}
b1,k,一般基于梯度下降,本质是求偏导,表达式记为:
w
j
k
=
w
j
k
−
α
w
∂
L
∂
w
j
k
b
1
,
k
=
b
1
,
k
−
α
b
∂
L
∂
b
1
,
k
w_{jk}=w_{jk}-\alpha_w\frac{ \partial L}{ \partial w_{jk} }\\b_{1,k}=b_{1,k}-α_b\frac{ \partial L}{ \partial b_{1,k} }
wjk=wjk−αw∂wjk∂Lb1,k=b1,k−αb∂b1,k∂L
首先简单梳理一下从输入
X
=
O
0
,
j
X=O_{0,j}
X=O0,j到计算得到损失
L
L
L的过程,首先输入
X
X
X,经过以下变换:
- 线性求和(N), N = ∑ j = 1 J w j k O 0 , j + b 1 , k N=∑_{j=1}^{J}w_{jk}O_{0,j}+b_{1,k} N=∑j=1JwjkO0,j+b1,k
- 激活(O), O = σ ( N ) O=\sigma(N) O=σ(N)
- 计算损失(L), L = L ( O , l a b e l ) L=L(O,label) L=L(O,label)
不难看出,
L
L
L是一个关于
w
j
k
w_{jk}
wjk的复合函数,此时的求导需要用到链式求导法则,链式求导简单说就是逐层求导,定义如下:
∂
g
(
f
(
x
)
)
∂
x
=
∂
f
∂
x
∂
L
∂
f
\frac{ \partial g(f(x))}{ \partial x}=\frac{ \partial f}{ \partial x}\frac{ \partial L}{ \partial f }
∂x∂g(f(x))=∂x∂f∂f∂L利用链式求导写出求解
w
i
j
w_{ij}
wij和
b
1
,
k
b_{1,k}
b1,k的表达式:
∂
L
∂
w
j
k
=
∂
N
1
,
k
∂
w
j
k
∂
O
1
,
k
∂
N
1
,
k
∂
L
∂
O
1
,
k
∂
L
∂
b
1
,
k
=
∂
N
1
,
k
∂
b
1
,
k
∂
O
1
,
k
∂
N
1
,
k
∂
L
∂
O
1
,
k
\frac{ \partial L}{ \partial w_{jk}}=\frac{ \partial N_{1,k}}{ \partial w_{jk}}\frac{ \partial O_{1,k}}{ \partial N_{1,k}}\frac{ \partial L}{ \partial O_{1,k}}\\ \frac{ \partial L}{ \partial b_{1,k}}=\frac{ \partial N_{1,k}}{ \partial b_{1,k}}\frac{ \partial O_{1,k}}{ \partial N_{1,k}}\frac{ \partial L}{ \partial O_{1,k}}
∂wjk∂L=∂wjk∂N1,k∂N1,k∂O1,k∂O1,k∂L∂b1,k∂L=∂b1,k∂N1,k∂N1,k∂O1,k∂O1,k∂L下面先讲
w
j
k
w_{jk}
wjk,
b
1
,
k
b_{1,k}
b1,k要更简单,本质是一样的。
逐个求解,首先是对于
∂
L
∂
O
1
,
k
\frac{ \partial L}{ \partial O_{1,k}}
∂O1,k∂L,这个也就是损失函数对于输出求导:
L
(
O
,
l
a
b
e
l
)
=
1
2
∑
k
=
1
K
(
O
1
,
k
−
l
a
b
e
l
k
)
2
L(O,label)=\frac{1}{2}∑_{k=1}^{K}(O_{1,k}-label_k)^2\\
L(O,label)=21k=1∑K(O1,k−labelk)2这里的
l
a
b
e
l
label
label就是给定预期目标(标签),因为不会改变,相当于常数,可得:
∂
L
∂
O
1
,
k
=
O
1
,
k
−
l
a
b
e
l
k
\frac{ \partial L}{ \partial O_{1,k}}=O_{1,k}-label_k
∂O1,k∂L=O1,k−labelk因为只有一个神经元
w
j
k
w_{jk}
wjk建立连接,所以求和符号消除了。
下面求解
∂
O
1
,
k
∂
N
1
,
k
\frac{ \partial O_{1,k}}{ \partial N_{1,k}}
∂N1,k∂O1,k,写出表达式:
O
1
,
k
=
σ
(
N
1
,
k
)
O_{1,k}=\sigma(N_{1,k})
O1,k=σ(N1,k)对这个式子求导可以得到(根据前面提到的
σ
(
x
)
\sigma(x)
σ(x)求导规则):
∂
O
1
,
k
∂
N
1
,
k
=
O
1
,
k
(
1
−
O
1
,
k
)
\frac{ \partial O_{1,k}}{ \partial N_{1,k}}=O_{1,k}(1-O_{1,k})
∂N1,k∂O1,k=O1,k(1−O1,k)这主要是基于
σ
′
(
x
)
=
σ
(
x
)
(
1
−
σ
(
x
)
)
\sigma'(x)=\sigma(x)(1-\sigma(x))
σ′(x)=σ(x)(1−σ(x)),这一层的输出是
O
1
,
k
O_{1,k}
O1,k,因此导数也可以表达为输出的形式。
最后是
∂
N
1
,
k
∂
w
j
k
\frac{ \partial N_{1,k}}{ \partial w_{jk}}
∂wjk∂N1,k,还是先写出表达式:
N
1
,
k
=
∑
j
=
1
J
w
j
k
O
0
,
j
+
b
1
N_{1,k}=∑_{j=1}^{J}w_{jk}O_{0,j}+b_1
N1,k=j=1∑JwjkO0,j+b1这里其实和只有一条神经元连接(因为只考虑一个连接系数
w
j
k
w_{jk}
wjk),不难看出:
∂
N
1
,
k
∂
w
j
k
=
O
0
,
j
\frac{ \partial N_{1,k}}{ \partial w_{jk}}=O_{0,j}
∂wjk∂N1,k=O0,j而
O
0
,
j
O_{0,j}
O0,j表示第0层的第
j
j
j个点的输出,这其实就是第
j
j
j个输入值
x
j
x_j
xj。
汇总得到下式:
∂
L
∂
w
j
k
=
O
0
,
j
O
1
,
k
(
1
−
O
1
,
k
)
(
O
1
,
k
−
l
a
b
e
l
k
)
\frac{ \partial L}{ \partial w_{jk}}=O_{0,j}O_{1,k}(1-O_{1,k})(O_{1,k}-label_k)
∂wjk∂L=O0,jO1,k(1−O1,k)(O1,k−labelk)
对于
b
1
,
k
b_{1,k}
b1,k,唯一不同的只有
∂
N
1
,
k
∂
b
1
,
k
\frac{ \partial N_{1,k}}{ \partial b_{1,k}}
∂b1,k∂N1,k,因为
∂
N
1
,
k
∂
b
1
,
k
=
1
\frac{ \partial N_{1,k}}{ \partial b_{1,k}}=1
∂b1,k∂N1,k=1,所以:
∂
L
∂
b
1
,
k
=
O
1
,
k
(
1
−
O
1
,
k
)
(
O
1
,
k
−
l
a
b
e
l
k
)
\frac{ \partial L}{ \partial b_{1,k}}=O_{1,k}(1-O_{1,k})(O_{1,k}-label_k)
∂b1,k∂L=O1,k(1−O1,k)(O1,k−labelk)记:
δ
1
,
k
=
∂
O
1
,
k
∂
N
1
,
k
∂
L
∂
O
1
,
k
=
O
1
,
k
(
1
−
O
1
,
k
)
(
O
1
,
k
−
l
a
b
e
l
k
)
\delta_{1,k}=\frac{ \partial O_{1,k}}{ \partial N_{1,k}}\frac{ \partial L}{ \partial O_{1,k}}=O_{1,k}(1-O_{1,k})(O_{1,k}-label_k)
δ1,k=∂N1,k∂O1,k∂O1,k∂L=O1,k(1−O1,k)(O1,k−labelk)这个值也就是第1层第
k
k
k个神经元的损失,此时
w
j
k
w_{jk}
wjk和
b
1
,
k
b_{1,k}
b1,k的表达式可简化为:
∂
L
∂
w
j
k
=
O
0
,
j
δ
1
,
k
∂
L
∂
b
1
,
k
=
δ
1
,
k
\frac{ \partial L}{ \partial w_{jk}}=O_{0,j}δ_{1,k}\\ \frac{ \partial L}{ \partial b_{1,k}}=δ_{1,k}
∂wjk∂L=O0,jδ1,k∂b1,k∂L=δ1,k到此,两层神经网络结束。
1.2.2.扩展模型
下面将扩展到更多层,这就涉及到更长的链式求导,先考虑稍微复杂的三层神经网络,再搬出前面的图:
1、2层之间的
w
j
k
w_{jk}
wjk和
b
2
,
k
b_{2,k}
b2,k的更新和两层模型意思是一样的,不同的在于0、1层之间的
w
i
j
w_{ij}
wij和
b
1
,
j
b_{1,j}
b1,j的更新。
在三层模型中,对于0、1层之间的系数
w
i
j
,
w_{ij},
wij,其连接第0层某个神经元和第1层的某个神经元,然后所有第1层的神经元与所有第2层的神经元相连,因此这个系数影响第1层的某个神经元,影响第2层的所有神经元。
这个过程和两层结构类似:第0层的的神经元输出经过加权累加到了第1层进行激活,激活后的输出再加权求和到第2层激活,最后输出。不同于前面两层结构的就是多了一层加权求和再激活的过程,后面的操作和前面的是一致的,写出表达式:
∂
L
∂
w
i
j
=
∂
N
1
,
j
∂
w
i
j
∂
O
1
,
j
∂
N
1
,
j
∑
k
=
1
K
∂
N
2
,
k
∂
O
1
,
j
∂
O
2
,
k
∂
N
2
,
k
∂
L
∂
O
2
,
k
∂
L
∂
b
1
,
j
=
∂
N
1
,
j
∂
b
1
,
j
∂
O
1
,
j
∂
N
1
,
j
∑
k
=
1
K
∂
N
2
,
k
∂
O
1
,
j
∂
O
2
,
k
∂
N
2
,
k
∂
L
∂
O
2
,
k
\frac{ \partial L}{ \partial w_{ij}}=\frac{ \partial N_{1,j}}{ \partial w_{ij}}\frac{ \partial O_{1,j}}{ \partial N_{1,j}}∑_{k=1}^{K}\frac{ \partial N_{2,k}}{ \partial O_{1,j}}\frac{ \partial O_{2,k}}{ \partial N_{2,k}}\frac{ \partial L}{ \partial O_{2,k}}\\ \frac{ \partial L}{ \partial b_{1,j}}=\frac{ \partial N_{1,j}}{ \partial b_{1,j}}\frac{ \partial O_{1,j}}{ \partial N_{1,j}}∑_{k=1}^{K}\frac{ \partial N_{2,k}}{ \partial O_{1,j}}\frac{ \partial O_{2,k}}{ \partial N_{2,k}}\frac{ \partial L}{ \partial O_{2,k}}
∂wij∂L=∂wij∂N1,j∂N1,j∂O1,jk=1∑K∂O1,j∂N2,k∂N2,k∂O2,k∂O2,k∂L∂b1,j∂L=∂b1,j∂N1,j∂N1,j∂O1,jk=1∑K∂O1,j∂N2,k∂N2,k∂O2,k∂O2,k∂L这里多了
∑
k
=
1
K
∑_{k=1}^{K}
∑k=1K是因为
w
i
j
w_{ij}
wij连接的
j
j
j节点在后面与所有点
K
K
K个节点建立了连接(影响了后面的所有),因此要累加所有
k
k
k个节点的偏导。
后面两项的计算和前面类似,简单说就是第2层第
k
k
k个神经元的损失
δ
2
,
k
δ_{2,k}
δ2,k,如下:
∂
O
2
,
k
∂
N
2
,
k
∂
L
∂
O
2
,
k
=
δ
2
,
k
=
O
2
,
k
(
1
−
O
2
,
k
)
(
O
2
,
k
−
l
a
b
e
l
k
)
\frac{ \partial O_{2,k}}{ \partial N_{2,k}}\frac{ \partial L}{ \partial O_{2,k}}=δ_{2,k}\\ =O_{2,k}(1-O_{2,k})(O_{2,k}-label_k)
∂N2,k∂O2,k∂O2,k∂L=δ2,k=O2,k(1−O2,k)(O2,k−labelk)主要是
∂
N
1
,
j
∂
w
i
j
∂
O
1
,
j
∂
N
1
,
j
∑
k
=
1
K
∂
N
2
,
k
∂
O
1
,
j
\frac{ \partial N_{1,j}}{ \partial w_{ij}}\frac{ \partial O_{1,j}}{ \partial N_{1,j}}∑_{k=1}^{K}\frac{ \partial N_{2,k}}{ \partial O_{1,j}}
∂wij∂N1,j∂N1,j∂O1,j∑k=1K∂O1,j∂N2,k部分。从后往前一个个看,首先是
∂
N
2
,
k
∂
O
1
,
j
\frac{ \partial N_{2,k}}{ \partial O_{1,j}}
∂O1,j∂N2,k,因为
N
2
,
k
=
∑
j
=
1
J
w
j
k
O
1
,
j
+
b
2
,
k
N_{2,k}=∑_{j=1}^{J}w_{jk}O_{1,j}+b_{2,k}
N2,k=∑j=1JwjkO1,j+b2,k,实际上和
O
1
,
j
O_{1,j}
O1,j关联的只有
w
j
k
w_{jk}
wjk,可得:
∂
N
2
,
k
∂
O
1
,
j
=
w
j
k
\frac{ \partial N_{2,k}}{ \partial O_{1,j}}=w_{jk}
∂O1,j∂N2,k=wjk对于
∂
O
1
,
j
∂
N
1
,
j
\frac{ \partial O_{1,j}}{ \partial N_{1,j}}
∂N1,j∂O1,j,这其实就是第1层输出激活求导,表达式如
O
1
,
j
=
σ
(
N
1
,
j
)
O_{1,j}=\sigma(N_{1,j})
O1,j=σ(N1,j),可得:
∂
O
1
,
j
∂
N
1
,
j
=
O
1
,
j
(
1
−
O
1
,
j
)
\frac{ \partial O_{1,j}}{ \partial N_{1,j}}=O_{1,j}(1-O_{1,j})
∂N1,j∂O1,j=O1,j(1−O1,j)
N
1
,
j
N_{1,j}
N1,j关于
w
i
j
w_{ij}
wij的表达式为
N
1
,
j
=
∑
i
=
1
I
w
i
j
O
0
,
i
+
b
1
,
j
N_{1,j}=∑_{i=1}^{I}w_{ij}O_{0,i}+b_{1,j}
N1,j=∑i=1IwijO0,i+b1,j,因此:
∂
N
1
,
j
∂
w
i
j
=
O
0
,
i
∂
N
1
,
j
∂
b
1
,
j
=
1
\frac{ \partial N_{1,j}}{ \partial w_{ij}}=O_{0,i}\\ \frac{ \partial N_{1,j}}{ \partial b_{1,j}}=1
∂wij∂N1,j=O0,i∂b1,j∂N1,j=1那么可以写出最终的结果:
∂
L
∂
w
i
j
=
O
0
,
i
O
1
,
j
(
1
−
O
1
,
j
)
∑
k
=
1
K
w
j
k
δ
2
,
k
∂
L
∂
b
1
,
j
=
O
1
,
j
(
1
−
O
1
,
j
)
∑
k
=
1
K
w
j
k
δ
2
,
k
\frac{ \partial L}{ \partial w_{ij}}=O_{0,i}O_{1,j}(1-O_{1,j})∑_{k=1}^{K}w_{jk}δ_{2,k}\\ \frac{ \partial L}{ \partial b_{1,j}}=O_{1,j}(1-O_{1,j})∑_{k=1}^{K}w_{jk}δ_{2,k}
∂wij∂L=O0,iO1,j(1−O1,j)k=1∑Kwjkδ2,k∂b1,j∂L=O1,j(1−O1,j)k=1∑Kwjkδ2,k观察后边的
∑
k
=
1
K
w
j
k
δ
2
,
k
∑_{k=1}^{K}w_{jk}δ_{2,k}
∑k=1Kwjkδ2,k,
w
j
k
w_{jk}
wjk分别连接的是1、2层的第
j
,
k
j,k
j,k个神经元,而
δ
2
,
k
δ_{2,k}
δ2,k是第二层的第
k
k
k个神经元的损失,这其实是对损失加权求和。类似前面的定义,定义
δ
1
,
j
=
O
1
,
j
(
1
−
O
1
,
j
)
∑
k
=
1
K
w
j
k
δ
2
,
k
δ_{1,j}=O_{1,j}(1-O_{1,j})∑_{k=1}^{K}w_{jk}δ_{2,k}
δ1,j=O1,j(1−O1,j)∑k=1Kwjkδ2,k,也就是第1层第
j
j
j个神经元的损失,简化后的式子如:
∂
L
∂
w
i
j
=
O
0
,
i
δ
1
,
j
∂
L
∂
b
1
,
j
=
δ
1
,
j
\frac{ \partial L}{ \partial w_{ij}}=O_{0,i}δ_{1,j}\\ \frac{ \partial L}{ \partial b_{1,j}}=δ_{1,j}
∂wij∂L=O0,iδ1,j∂b1,j∂L=δ1,j总结一下,反向传播实际上是一个收集误差的过程。如图,对于第1层第一个神经元,其会分别以
w
11
w_{11}
w11和
w
22
w_{22}
w22的权值影响到第2层的第一个和第二个神经元,那么在反向传播的时候,在算出第2层第一个和第二个神经元的误差
δ
2
,
1
,
δ
2
,
2
δ_{2,1},δ_{2,2}
δ2,1,δ2,2后,其又会以加权求和的方式回到第1层第一个神经元,乘上激活函数求导得到的结果成为第1层第1个神经元的损失
δ
1
,
1
δ_{1,1}
δ1,1,进一步影响连接到连接该点的系数
w
11
w_{11}
w11。
扩展到更多层,本质上是在做同一件事,对于一个
n
n
n层网络,要算两层之间的系数
w
w
w和相关的
b
b
b,都可以抽象为三层架构(下一层不是输出层)和两层架构(下一层就是输出层)的第0层和第1层。假设要更新的是位于
L
a
y
e
r
−
1
Layer-1
Layer−1和
L
a
y
e
r
Layer
Layer之间的系数
w
i
j
w_{ij}
wij和第
L
a
y
e
r
Layer
Layer层的系数
b
L
a
y
e
r
,
j
b_{Layer,j}
bLayer,j,可以写为:
∂
L
∂
w
i
j
=
O
L
a
y
e
r
−
1
,
i
δ
L
a
y
e
r
,
j
∂
L
∂
b
L
a
y
e
r
,
j
=
δ
L
a
y
e
r
,
j
\frac{ \partial L}{ \partial w_{ij}}=O_{Layer-1,i}δ_{Layer,j}\\\frac{ \partial L}{ \partial b_{Layer,j}}=δ_{Layer,j}
∂wij∂L=OLayer−1,iδLayer,j∂bLayer,j∂L=δLayer,j其中:
δ
L
a
y
e
r
,
j
=
{
O
L
a
y
e
r
,
j
(
1
−
O
L
a
y
e
r
,
j
)
∑
k
=
1
K
w
j
k
δ
L
a
y
e
r
+
1
,
k
,
L
a
y
e
r
不为输出
O
L
a
y
e
r
,
j
(
1
−
O
L
a
y
e
r
,
j
)
(
O
L
a
y
e
r
,
j
−
l
a
b
e
l
k
)
,
L
a
y
e
r
为输出
δ_{Layer,j}=\left\{ \begin{array}{cc} O_{Layer,j}(1-O_{Layer,j})∑_{k=1}^{K}w_{jk}δ_{Layer+1,k},\;Layer不为输出 \\ O_{Layer,j}(1-O_{Layer,j})(O_{Layer,j}-label_k),\;\;\;Layer为输出 \end{array} \right.\\
δLayer,j={OLayer,j(1−OLayer,j)∑k=1KwjkδLayer+1,k,Layer不为输出OLayer,j(1−OLayer,j)(OLayer,j−labelk),Layer为输出
当然,不同的激活函数,前面的
O
L
a
y
e
r
,
j
(
1
−
O
L
a
y
e
r
,
j
)
O_{Layer,j}(1-O_{Layer,j})
OLayer,j(1−OLayer,j)是会变的。
到这里,多层神经网络的原理就算结束了。
2.代码
下面尝试用代码实现一个多层神经网络,使用的是一个关于糖尿病的数据集,其中输入是10维数据,输出是1维数据,我写的代码是可变层数和可变隐藏层大小的,但是隐藏层的大小必须一致。
一开始调试发现输出总是一样,后来发现是迭代轮数太少了…直接设置了比较大的学习率(5)和3000轮迭代,但是尽管如此最终的拟合有优度还是只能到0.7左右,可能是数据集太抽象了?或者应该分批次处理,一个个迭代更新感觉是没法得到很理想的结果。
(也有可能代码错了?最好不是…)
我设置了2层隐藏层,每层大小是10,下面是loss曲线,一共3000轮,还有下降的趋势,但是这样来看收敛实在太慢了。
下面是拟合结果和实际结果的对比,还是有靠近的趋势的,就是可能确实是太抽象了,没法拟合的太好。
下面贴出完整代码:
# 复现神经网络
import time
import numpy as np
from matplotlib import pyplot as plt
from numpy import dot
from numpy.linalg import inv
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
from sklearn.preprocessing import MinMaxScaler
from numpy.random import uniform
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 糖尿病数据集
from sklearn.datasets import load_diabetes
def load_data():
diabetes = load_diabetes()
X = diabetes.data # data
y = diabetes.target # label
# 对所有数据按列归一化
scalar = MinMaxScaler()
for i in range(X.shape[1]):
X[:, i] = scalar.fit_transform(X[:, i].reshape(-1, 1)).reshape(-1)
# print(len(y.shape),y.shape)
if len(y.shape) == 1:
y = y.reshape(-1, 1)
for i in range(y.shape[1]):
y[:, i] = scalar.fit_transform(y[:, i].reshape(-1, 1)).reshape(-1)
# print(X[:5])
# print(y[:5])
return X, y
class MyNet():
# sigmoid
def sigmoid(self, x):
return 1 / (1 + np.exp(-x))
# 损失函数
def loss_func(self, output, label): # 输入是列向量
return 1 / 2 * (output - label).T @ (output - label)
def fit(self, X, Y, epochs, alpha=0.01, layers=2, layer_size=3): # 中间的隐藏层层数和隐藏层的大小
if layers < 1:
assert "隐藏层至少为1,当前layers=%d" % (layers)
if len(Y.shape) == 1: # 一维
Y = Y.reshape(-1, 1)
in_features = X.shape[1]
out_features = Y.shape[1]
samples = X.shape[0]
print('in_features:', in_features)
print('out_features:', out_features)
print('samples:', samples)
# 矩阵初始化(随机初始化)
w_first = uniform(0,1,(in_features,layer_size)) # 每一行是一个输入神经元对所有下一层神经元的权值
w_last = uniform(0,1,(layer_size,out_features)) # 每一行是一个输入神经元对所有下一层神经元的权值
self.w=None # 防止后续报错
if layer_size > 1: # 刚好等于1就不需要了
w = uniform(0,1,(layers-1,layer_size,layer_size))
b_last = uniform(0,1,(out_features, 1)) # # 每一列是一层的系数b
b = uniform(0,1,(layers,layer_size,1)) # 隐藏层的
# 运行过程中的变量
delta = np.zeros((layers, layer_size, 1), dtype=float)
delta_last = np.zeros((out_features, 1), dtype=float) # 每一列是一层的δ
out_last = np.zeros((out_features, 1), dtype=float) # 每一列是一层的output
out = np.zeros((layers, layer_size, 1), dtype=float)
# 开始迭代
loss_list = []
for epoch in range(epochs):
loss = 0
for idx in range(samples):
x = X[idx].reshape(-1, 1)
y = Y[idx].reshape(-1, 1)
# 前向传播
for layer in range(layers + 1): # 0其实就对应了0和1层的w和1层的偏置
if layer == 0: # 第一层
out[layer] = self.sigmoid(w_first.T @ x + b[layer])
elif layer < layers: # 不是最后一层
# print('w[%d]'%(layer-1))
out[layer] = self.sigmoid(w[layer - 1].T @ out[layer - 1] + b[layer])
else: # 最后一层
out_last = self.sigmoid(w_last.T @ out[layer - 1] + b_last)
# 反向传播
for layer in range(layers, -1, -1): # layers,...,0
# 计算出每一层的损失
if layer == layers: # 最后一层(输出层)
# print('输出层')
delta_last = out_last * (1 - out_last) * (out_last - y) # 最后一层隐藏层
elif layer == layers - 1: # 隐藏层最后一层,连接输出层
# print('最后一层隐藏')
delta[layer] = out[layer] * (1 - out[layer]) * (w_last @ delta_last)
else: # 最后一层
# print('隐藏')
delta[layer] = out[layer] * (1 - out[layer]) * (w[layer] @ delta[layer + 1])
# 更新系数
for layer in range(layers + 1):
# print(layer)
if layer == 0: # 输入-隐藏
# print('输入-隐藏')
det_w = x @ delta[layer].T
w_first = w_first - alpha * det_w # # O_{Layer-1,i}δ_{Layer,j}
b[layer] = b[layer] - alpha * delta[layer] # δ_{Layer,j}
elif layer < layers: # 隐藏-隐藏
# print('隐藏-隐藏')
det_w = out[layer - 1] @ delta[layer].T
w[layer - 1] = w[layer - 1] - alpha * det_w
b[layer] = b[layer] - alpha * delta[layer]
else: # 隐藏-输出
# print('隐藏-输出')
det_w = out[layer - 1] @ delta_last.T
w_last = w_last - alpha * det_w
b_last = b_last - alpha * delta_last
this_loss = self.loss_func(out_last, y)
# print('this_loss',this_loss,' out=',out_last,' y=',y)
loss += this_loss
loss_list.append(loss[0][0]/samples)
print('epoch %d loss:%.6f' % (epoch + 1, loss / samples))
plt.plot(loss_list)
plt.show()
# 保存参数
self.w_first = w_first
self.w_last = w_last
self.w = w
self.b_last = b_last
self.b = b
self.in_features = in_features
self.out_features = out_features
self.layers = layers
self.layer_size = layer_size
def predict(self, X):
in_features = self.in_features
if X.shape[1] != in_features:
assert "输入维度不正确,应为%d×1" % in_features
w_first = self.w_first
w_last = self.w_last
w = self.w
b_last = self.b_last
b = self.b
layers = self.layers
out_features = self.out_features
layer_size = self.layer_size
samples = X.shape[0] # 数量
ans = np.zeros((samples, out_features))
out_last = np.zeros((out_features, 1), dtype=float) # 每一列是一层的output
out = np.zeros((layers, layer_size, 1), dtype=float)
for i in range(samples):
x = X[i].reshape(-1, 1)
# 前向传播
for layer in range(layers + 1): # 0其实就对应了0和1层的w和1层的偏置
if layer == 0: # 第一层
out[layer] = self.sigmoid(w_first.T @ x + b[layer])
elif layer < layers: # 不是最后一层
out[layer] = self.sigmoid(w[layer - 1].T @ out[layer - 1] + b[layer])
else: # 最后一层
out_last = self.sigmoid(w_last.T @ out[layer - 1] + b_last)
ans[i] = out_last.reshape(-1)
return ans
if __name__ == '__main__':
X, y = load_data() # 获取归一化后的数据
myNet = MyNet()
myNet.fit(X, y, epochs=3000, alpha=5, layers=2, layer_size=10)
pred=myNet.predict(X)
for i in range(y.shape[0]):
print('true=',y[i],'pred=',pred[i])
print('r^2=%.2f'%(r2_score(y,pred)))
plt.plot(y[:,0])
plt.plot(pred[:,0])
plt.show()
总的来说,简单的神经网络就是这样,但是神经网络实际上有很多种,花样百出,先浅入个门好了。