在阅读本文之前,请确保你已经有了一定的神经网络基础(可先看一遍西瓜书)。
本文采用的是标准BP算法,即每次仅针对一个样例更新权重和阈值。
本文将搭建用于分类的单隐层BP神经网络。
目录
一、理论部分
1.1 正向计算
符
号
说
明
\textcolor{red}{符号说明}
符号说明
设我们的单隐层BP神经网络有
m
m
m 个**输入神经元**,
n
n
n 个**输出神经元**,
h
h
h 个**隐层神经元**。
- 权重: 第 i i i 个输入神经元到第 j j j 个隐层神经元的权重记为 v i j v_{ij} vij,第 i i i 个隐层神经元到第 j j j 个输出神经元的权重记为 w i j w_{ij} wij;
- 阈值: 第 i i i 个隐层神经元的阈值记为 γ i \gamma_i γi,第 i i i 个输出神经元的阈值记为 θ i \theta_i θi;
- 输入: 第 k k k 个输入神经元接收到的输入记为 x k x_k xk,第 k k k 个隐层神经元接收到的输入记为 α k \alpha_k αk,第 k k k 个输出神经元接收到的输入记为 β k \beta_k βk;
- 输出: 第 j j j 个隐层神经元的输出记为 b j b_j bj,第 j j j 个输出神经元的输出记为 y ^ j \hat{y}_j y^j。
记
x
=
(
x
1
,
x
2
,
⋯
,
x
n
)
T
\boldsymbol{x}=(x_1,x_2,\cdots,x_n)^{\mathrm T}
x=(x1,x2,⋯,xn)T,激活函数为
σ
(
x
)
\sigma(x)
σ(x),我们定义:
σ
(
x
)
=
(
σ
(
x
1
)
,
σ
(
x
2
)
,
⋯
,
σ
(
x
n
)
)
T
\sigma(\boldsymbol{x})=(\sigma(x_1),\sigma(x_2),\cdots,\sigma(x_n))^{\mathrm T}
σ(x)=(σ(x1),σ(x2),⋯,σ(xn))T
公
式
推
导
\textcolor{red}{公式推导}
公式推导
注意到
α
k
=
∑
i
=
1
m
v
i
k
x
i
\alpha_k=\sum_{i=1}^m v_{ik}x_i
αk=∑i=1mvikxi,如果我们记
α
=
[
α
1
α
2
⋮
α
h
]
h
×
1
,
V
=
[
v
11
v
21
⋯
v
m
1
v
12
v
22
⋯
v
m
2
⋮
⋮
⋮
v
1
h
v
2
h
⋯
v
m
h
]
h
×
m
,
x
=
[
x
1
x
2
⋮
x
m
]
m
×
1
\boldsymbol{\alpha}= \begin{bmatrix} \alpha_1 \\ \alpha_2 \\ \vdots \\ \alpha_h \end{bmatrix}_{h\times 1}, \quad \boldsymbol{V}= \begin{bmatrix} v_{11} & v_{21} & \cdots & v_{m1} \\ v_{12} & v_{22} & \cdots & v_{m2} \\ \vdots & \vdots & & \vdots \\ v_{1h} & v_{2h} & \cdots & v_{mh} \\ \end{bmatrix}_{h\times m},\quad \boldsymbol{x}= \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_m \end{bmatrix}_{m\times 1}
α=⎣⎢⎢⎢⎡α1α2⋮αh⎦⎥⎥⎥⎤h×1,V=⎣⎢⎢⎢⎡v11v12⋮v1hv21v22⋮v2h⋯⋯⋯vm1vm2⋮vmh⎦⎥⎥⎥⎤h×m,x=⎣⎢⎢⎢⎡x1x2⋮xm⎦⎥⎥⎥⎤m×1
则有
α
=
V
x
\boldsymbol{\alpha}=\boldsymbol{Vx}
α=Vx
再令
b
=
(
b
1
,
b
2
,
⋯
,
b
h
)
T
,
γ
=
(
γ
1
,
γ
2
,
⋯
,
γ
h
)
T
\boldsymbol{b}=(b_1,b_2,\cdots,b_h)^{\mathrm T},\,\boldsymbol{\gamma}=(\gamma_1,\gamma_2,\cdots,\gamma_h)^{\mathrm T}
b=(b1,b2,⋯,bh)T,γ=(γ1,γ2,⋯,γh)T,从而
b
=
σ
(
α
−
γ
)
=
σ
(
V
x
−
γ
)
\boldsymbol{b}=\sigma(\boldsymbol \alpha-\boldsymbol{\gamma})=\sigma(\boldsymbol{Vx}-\boldsymbol{\gamma})
b=σ(α−γ)=σ(Vx−γ)。
同理可得,
W
n
×
h
\boldsymbol{W}_{n\times h}
Wn×h 为隐层和输出层之间的**权重矩阵**,且有
β
=
W
b
,
y
^
=
σ
(
β
−
θ
)
=
σ
(
W
b
−
θ
)
\boldsymbol{\beta}=\boldsymbol{W}\boldsymbol{b},\quad \boldsymbol{\hat{y}}=\sigma(\boldsymbol{\beta}-\boldsymbol{\theta})=\sigma(\boldsymbol{W}\boldsymbol{b}-\boldsymbol{\theta})
β=Wb,y^=σ(β−θ)=σ(Wb−θ)
进一步有
y
^
=
σ
(
W
σ
(
V
x
−
γ
)
−
θ
)
\boldsymbol{\hat{y}}=\sigma(\boldsymbol{W}\sigma(\boldsymbol{Vx}-\boldsymbol{\gamma})-\boldsymbol{\theta})
y^=σ(Wσ(Vx−γ)−θ)。
于是我们就得到了神经网络的输出。
1.2 反向传播
对于给定的训练样例
(
x
,
y
)
(\boldsymbol{x},\boldsymbol{y})
(x,y),神经网络的输出为
y
^
=
σ
(
β
−
θ
)
\boldsymbol{\hat{y}}=\sigma(\boldsymbol{\beta}-\boldsymbol{\theta})
y^=σ(β−θ),从而误差为
E
=
∥
y
^
−
y
∥
2
=
∑
j
=
1
n
(
y
^
j
−
y
j
)
2
E=\Vert \boldsymbol{\hat{y}}-\boldsymbol{y} \Vert^2=\sum_{j=1}^n(\hat{y}_j-y_j)^2
E=∥y^−y∥2=j=1∑n(y^j−yj)2
为消掉求导后多出来的系数
2
2
2,我们做一个处理,即令
E
=
1
2
∑
j
=
1
n
(
y
^
j
−
y
j
)
2
E=\frac12\sum_{j=1}^n(\hat{y}_j-y_j)^2
E=21j=1∑n(y^j−yj)2
我们采用梯度下降策略来最小化误差,即
w
i
j
:
=
w
i
j
−
η
∂
E
∂
w
i
j
,
θ
j
:
=
θ
j
−
η
∂
E
∂
θ
j
w_{ij}:=w_{ij}-\eta\frac{\partial E}{\partial w_{ij}},\quad \theta_j:=\theta_j-\eta\frac{\partial E}{\partial \theta_j}
wij:=wij−η∂wij∂E,θj:=θj−η∂θj∂E
其中
∂
E
∂
w
i
j
=
∂
E
∂
y
^
j
⋅
∂
y
^
j
∂
β
j
⋅
∂
β
j
∂
w
i
j
=
(
y
^
j
−
y
j
)
⋅
σ
′
(
β
j
−
θ
j
)
⋅
b
i
\begin{aligned} \frac{\partial E}{\partial w_{ij}}&=\textcolor{red}{\frac{\partial E}{\partial \hat{y}_j}} \cdot \textcolor{blue}{\frac{\partial \hat{y}_j}{\partial \beta_j}} \cdot \textcolor{green}{\frac{\partial \beta_j}{\partial w_{ij}}}\\ &=\textcolor{red}{(\hat{y}_j-y_j)} \cdot \textcolor{blue}{\sigma'(\beta_j-\theta_j)}\cdot \textcolor{green}{b_i} \\ \end{aligned}
∂wij∂E=∂y^j∂E⋅∂βj∂y^j⋅∂wij∂βj=(y^j−yj)⋅σ′(βj−θj)⋅bi
令
g
j
=
−
(
y
^
j
−
y
j
)
⋅
σ
′
(
β
j
−
θ
j
)
g_j =- \textcolor{red}{(\hat{y}_j-y_j)} \cdot \textcolor{blue}{\sigma'(\beta_j-\theta_j)}
gj=−(y^j−yj)⋅σ′(βj−θj),则可得知
Δ
w
i
j
=
η
g
j
b
i
\Delta w_{ij}=\eta g_jb_i
Δwij=ηgjbi。另一方面
∂
E
∂
θ
j
=
∂
E
∂
y
^
j
⋅
∂
y
^
j
∂
θ
j
=
(
y
^
j
−
y
j
)
⋅
(
−
σ
′
(
β
j
−
θ
j
)
)
=
g
j
\begin{aligned} \frac{\partial E}{\partial \theta_j}&= \textcolor{red}{\frac{\partial E}{\partial \hat{y}_j}} \cdot \textcolor{blue}{\frac{\partial \hat{y}_j}{\partial \theta_j}} \\ &=\textcolor{red}{(\hat{y}_j-y_j)} \cdot \textcolor{blue}{(-\sigma'(\beta_j-\theta_j))}=g_j \end{aligned}
∂θj∂E=∂y^j∂E⋅∂θj∂y^j=(y^j−yj)⋅(−σ′(βj−θj))=gj
因此
Δ
θ
j
=
−
η
g
j
\Delta \theta_j=-\eta g_j
Δθj=−ηgj。
记
Δ
W
=
η
g
b
T
\Delta\boldsymbol{W}=\eta \boldsymbol{g}\boldsymbol{b}^{\mathrm{T}}
ΔW=ηgbT,
Δ
θ
=
−
η
g
\Delta \boldsymbol{\theta}=-\eta\boldsymbol{g}
Δθ=−ηg,于是得到权重与阈值的更新公式:
W
:
=
W
+
Δ
W
,
θ
:
=
θ
+
Δ
θ
\boldsymbol{W}:=\boldsymbol{W}+\Delta\boldsymbol{W},\quad \boldsymbol{\theta}:= \boldsymbol{\theta}+ \Delta\boldsymbol{\theta}
W:=W+ΔW,θ:=θ+Δθ
此外,还有
v
i
j
:
=
v
i
j
−
η
∂
E
∂
v
i
j
,
γ
j
:
=
γ
j
−
η
∂
E
∂
γ
j
v_{ij}:=v_{ij}-\eta\frac{\partial E}{\partial v_{ij}},\quad \gamma_j:=\gamma_j-\eta\frac{\partial E}{\partial \gamma_j}
vij:=vij−η∂vij∂E,γj:=γj−η∂γj∂E
其中
∂
E
∂
v
i
j
=
∑
k
=
1
n
∂
E
∂
y
^
k
⋅
∂
y
^
k
∂
β
k
⋅
∂
β
k
∂
b
j
⋅
∂
b
j
∂
α
j
⋅
∂
α
j
∂
v
i
j
=
∑
k
=
1
n
(
−
g
k
)
⋅
w
j
k
⋅
σ
′
(
α
j
−
γ
j
)
⋅
x
i
\begin{aligned} \frac{\partial E}{\partial v_{ij}}&=\sum_{k=1}^n \textcolor{red}{\frac{\partial E}{\partial \hat{y}_k}}\cdot \textcolor{blue}{\frac{\partial \hat{y}_k}{\partial \beta_k}}\cdot \textcolor{green}{\frac{\partial \beta_k}{\partial b_j}}\cdot \textcolor{magenta}{\frac{\partial b_j}{\partial\alpha_j}}\cdot \textcolor{orange}{\frac{\partial \alpha_j}{\partial v_{ij}}}\\ &=\sum_{k=1}^n (-g_k)\cdot \textcolor{green}{w_{jk}}\cdot \textcolor{magenta}{\sigma'(\alpha_j-\gamma_j)}\cdot \textcolor{orange}{x_i}\\ \end{aligned}
∂vij∂E=k=1∑n∂y^k∂E⋅∂βk∂y^k⋅∂bj∂βk⋅∂αj∂bj⋅∂vij∂αj=k=1∑n(−gk)⋅wjk⋅σ′(αj−γj)⋅xi
令
e
j
=
−
∑
k
=
1
n
(
−
g
k
)
⋅
w
j
k
⋅
σ
′
(
α
j
−
γ
j
)
e_j=-\sum_{k=1}^n (-g_k)\cdot \textcolor{green}{w_{jk}}\cdot \textcolor{magenta}{\sigma'(\alpha_j-\gamma_j)}
ej=−∑k=1n(−gk)⋅wjk⋅σ′(αj−γj),则得到
Δ
v
i
j
=
η
e
j
x
i
\Delta v_{ij}=\eta e_jx_i
Δvij=ηejxi。另一方面
∂
E
∂
γ
j
=
∑
k
=
1
n
∂
E
∂
y
^
k
⋅
∂
y
^
k
∂
β
k
⋅
∂
β
k
∂
b
j
⋅
∂
b
j
∂
γ
j
=
∑
k
=
1
n
(
−
g
k
)
⋅
w
j
k
⋅
(
−
σ
′
(
α
j
−
γ
j
)
)
=
e
j
\begin{aligned} \frac{\partial E}{\partial \gamma_j}&=\sum_{k=1}^n \textcolor{red}{\frac{\partial E}{\partial \hat{y}_k}}\cdot \textcolor{blue}{\frac{\partial \hat{y}_k}{\partial \beta_k}}\cdot \textcolor{green}{\frac{\partial \beta_k}{\partial b_j}}\cdot \textcolor{magenta}{\frac{\partial b_j}{\partial\gamma_j}}\\ &=\sum_{k=1}^n (-g_k)\cdot \textcolor{green}{w_{jk}}\cdot \textcolor{magenta}{(-\sigma'(\alpha_j-\gamma_j))}=e_j \\ \end{aligned}
∂γj∂E=k=1∑n∂y^k∂E⋅∂βk∂y^k⋅∂bj∂βk⋅∂γj∂bj=k=1∑n(−gk)⋅wjk⋅(−σ′(αj−γj))=ej
因此
Δ
γ
j
=
−
η
e
j
\Delta \gamma_j=-\eta e_j
Δγj=−ηej。
记
Δ
V
=
η
e
x
T
\Delta\boldsymbol{V}=\eta \boldsymbol{e}\boldsymbol{x}^{\mathrm{T}}
ΔV=ηexT,
Δ
γ
=
−
η
e
\Delta \boldsymbol{\gamma}=-\eta\boldsymbol{e}
Δγ=−ηe,于是得到权重与阈值的更新公式:
V
:
=
V
+
Δ
V
,
γ
:
=
γ
+
Δ
γ
\boldsymbol{V}:=\boldsymbol{V}+\Delta\boldsymbol{V},\quad \boldsymbol{\gamma}:= \boldsymbol{\gamma}+ \Delta\boldsymbol{\gamma}
V:=V+ΔV,γ:=γ+Δγ
二、实现部分
2.1 算法伪码
两个梯度项可以表示成:
g
=
(
y
−
y
^
)
∗
σ
′
(
β
−
θ
)
e
=
W
T
g
∗
σ
′
(
α
−
γ
)
\begin{aligned} \boldsymbol{g}&=(\boldsymbol{y}-\boldsymbol{\hat{y}}) \;*\; \sigma'(\boldsymbol{\beta}-\boldsymbol{\theta}) \\ \boldsymbol{e}&=\boldsymbol{W}^{\mathrm T}\boldsymbol{g}\;*\;\sigma'(\boldsymbol{\alpha}-\boldsymbol{\gamma})\\ \end{aligned}
ge=(y−y^)∗σ′(β−θ)=WTg∗σ′(α−γ)
其中
∗
*
∗ 代表**按元素**运算。基于此,我们可以写出算法伪码:
输入
:训练集
D
,学习率
η
过程:
1. 随机初始化
V
,
W
,
γ
,
θ
2.
r
e
p
e
a
t
3.
f
o
r
a
l
l
(
x
,
y
)
∈
D
d
o
4.
计算输入和输出
α
,
b
,
β
,
y
^
;
5.
计算梯度项
g
,
e
;
6.
计算改变量
Δ
W
,
Δ
θ
,
Δ
V
,
Δ
γ
;
7.
更新
W
,
θ
,
V
,
γ
;
8.
e
n
d
f
o
r
9.
u
n
t
i
l
E
<
ϵ
或迭代次数达到某个值
输出
:训练好的神经网络
\begin{aligned} &\text{{\bf 输入}:训练集 $D$,学习率 $\eta$} \\ &\text{\bf 过程:} \\ &\text{1. 随机初始化 $\boldsymbol{V, W, \gamma, \theta}$} \\ &\text{2. $\mathrm{\bf repeat}$} \\ &\text{3. $\quad\;$ $\mathrm{\bf for\; all\;}$ $\!(\boldsymbol{x},\boldsymbol{y})\in D$ $\mathrm{\bf \,do}$} \\ &\text{4. $\quad\quad\;\;\;$ 计算输入和输出 $\boldsymbol{\alpha, b, \beta, \hat{y}}$; } \\ &\text{5. $\quad\quad\;\;\;$ 计算梯度项 $\boldsymbol{g, e}$; } \\ &\text{6. $\quad\quad\;\;\;$ 计算改变量 $\boldsymbol{\Delta W,\Delta \theta, \Delta V,\Delta \gamma}$; } \\ &\text{7. $\quad\quad\;\;\;$ 更新 $\boldsymbol{W, \theta, V,\gamma}$; } \\ &\text{8. $\quad\;$ $\mathrm{\bf end\;for}$} \\ &\text{9. $\mathrm{\bf until}\,$ $E<\epsilon\,$ 或迭代次数达到某个值} \\ &\text{{\bf 输出}:训练好的神经网络} \\ \end{aligned}
输入:训练集 D,学习率 η过程:1. 随机初始化 V,W,γ,θ2. repeat3. forall (x,y)∈D do4. 计算输入和输出 α,b,β,y^; 5. 计算梯度项 g,e; 6. 计算改变量 ΔW,Δθ,ΔV,Δγ; 7. 更新 W,θ,V,γ; 8. endfor9. until E<ϵ 或迭代次数达到某个值输出:训练好的神经网络
有了矩阵和向量化的表述,我们的单隐层神经网络可以简洁地表示成如下形式,这种形式也方便我们后续的编程。
假设我们的激活函数采用 Sigmoid 函数,利用它的性质:
σ
′
(
x
)
=
σ
(
x
)
∗
(
1
−
σ
(
x
)
)
\sigma'(\boldsymbol{x})=\sigma(\boldsymbol{x})\;*\;(\boldsymbol{1}-\sigma(\boldsymbol{x}))
σ′(x)=σ(x)∗(1−σ(x)),我们的两个梯度项变为:
g
=
(
y
−
y
^
)
∗
y
^
∗
(
1
−
y
^
)
e
=
W
T
g
∗
b
∗
(
1
−
b
)
\begin{aligned} \boldsymbol{g}&=(\boldsymbol{y}-\boldsymbol{\hat{y}}) \;*\; \boldsymbol{\hat{y}}\;*\;(\boldsymbol{1}-\boldsymbol{\hat{y}}) \\ \boldsymbol{e}&=\boldsymbol{W}^{\mathrm T}\boldsymbol{g}\;*\;\boldsymbol{b}\;*\;(\boldsymbol{1}-\boldsymbol{b})\\ \end{aligned}
ge=(y−y^)∗y^∗(1−y^)=WTg∗b∗(1−b)
2.2 算法实现
2.2.1 初始化
首先我们需要创建一个神经网络类:
import numpy as np
classNeuralNetwork:def__init__(self):pass
那我们应该初始化哪些参数呢?输入层结点数
m
m
m,隐层结点数
h
h
h 和输出层结点数
n
n
n 都是必须初始化的,它们决定了神经网络的结构。此外,根据算法伪码,我们还应该初始化
η
,
W
,
V
,
θ
,
γ
\eta, \boldsymbol{W},\boldsymbol{V},\boldsymbol{\theta},\boldsymbol{\gamma}
η,W,V,θ,γ。
注意到
η
,
m
,
n
,
h
\eta,m,n,h
η,m,n,h 是必须人工输入的,而权重矩阵和阈值向量则由后面三个参数决定。
当然,容忍度
tol
和 最大迭代次数
max_iter
也需要人工输入,这两个参数决定了我们的神经网络何时停止训练。
def__init__(
self,
learningrate,
inputnodes,
hiddennodes,
outputnodes,
tol,
max_iter
):# 学习率
self.lr = learningrate
# 输入层结点数
self.ins = inputnodes
# 隐层结点数
self.hns = hiddennodes
# 输出层结点数
self.ons = outputnodes
# 容忍度
self.tol = tol
# 最大迭代次数
self.max_iter = max_iter
权重矩阵和阈值向量在
(
0
,
1
)
(0,1)
(0,1) 范围内初始化:
# 权重矩阵和阈值向量
self.V = np.random.rand(self.hns, self.ins)
self.W = np.random.rand(self.ons, self.hns)
self.gamma = np.random.rand(self.hns)
self.theta = np.random.rand(self.ons)
当然别忘了还有激活函数,我们采用匿名函数的形式:
# Sigmoid
self.sigmoid =lambda x:1/(1+ np.power(np.e,-x))
到此为止,我们的
__init__
函数算是完成了:
def__init__(
self,
learningrate,
inputnodes,
hiddennodes,
outputnodes,
tol,
max_iter
):
self.lr = learningrate
self.ins, self.hns, self.ons = inputnodes, hiddennodes, outputnodes
self.tol = tol
self.max_iter = max_iter
self.V = np.random.rand(self.hns, self.ins)
self.W = np.random.rand(self.ons, self.hns)
self.gamma = np.random.rand(self.hns)
self.theta = np.random.rand(self.ons)
self.sigmoid =lambda x:1/(1+ np.power(np.e,-x))
2.2.2 正向传播
我们需要定义一个正向传播函数用来计算
α
,
b
,
β
,
y
^
\boldsymbol{\alpha},\boldsymbol{b},\boldsymbol{\beta},\boldsymbol{\hat{y}}
α,b,β,y^,该函数需要一个样本
(
x
,
y
)
(\boldsymbol{x},\boldsymbol{y})
(x,y) 作为输入,我们用
(inputs, targets)
来表示,输出
y
^
\boldsymbol{\hat{y}}
y^ 用
outputs
来表示。
当然,我们还需要计算误差
E
E
E,用
error
来表示。
defpropagate(self, inputs, targets):# 计算输入和输出
alpha = np.dot(self.V, inputs)
b = self.sigmoid(alpha - self.gamma)
beta = np.dot(self.W, b)
outputs = self.sigmoid(beta - self.theta)# 计算误差
error = np.linalg.norm(targets - outputs,ord=2)**2/2return alpha, b, beta, outputs, error
2.2.3 反向传播
反向传播需要先计算两个梯度项
g
,
e
\boldsymbol{g},\boldsymbol{e}
g,e,然后再进行参数更新:
W
:
=
W
+
η
g
b
T
,
θ
:
=
θ
+
−
η
g
V
:
=
V
+
η
e
x
T
,
γ
:
=
γ
+
−
η
e
\boldsymbol{W}:=\boldsymbol{W}+\eta \boldsymbol{g}\boldsymbol{b}^{\mathrm{T}},\quad \boldsymbol{\theta}:= \boldsymbol{\theta}+ -\eta \boldsymbol{g}\\ \boldsymbol{V}:=\boldsymbol{V}+\eta \boldsymbol{e}\boldsymbol{x}^{\mathrm{T}},\quad \boldsymbol{\gamma}:= \boldsymbol{\gamma}+ -\eta\boldsymbol{e} \\
W:=W+ηgbT,θ:=θ+−ηgV:=V+ηexT,γ:=γ+−ηe
可以看出我们的函数需要用到这些参数:
x
,
y
,
α
,
b
,
β
,
y
^
\boldsymbol{x},\boldsymbol{y},\boldsymbol{\alpha},\boldsymbol{b},\boldsymbol{\beta},\boldsymbol{\hat{y}}
x,y,α,b,β,y^。
defback_propagate(self, inputs, targets, alpha, b, beta, outputs):# 计算两个梯度项
g =(targets - outputs)* outputs *(np.ones(len(outputs))- outputs)
e = np.dot(self.W.T, g)* b *(np.ones(len(b))- b)# 更新参数
self.W += self.lr * np.outer(g, b)
self.theta +=-self.lr * g
self.V += self.lr * np.outer(e, inputs)
self.gamma +=-self.lr * e
2.2.3 训练
训练需要传入数据集,假设数据集的表示形式为
X
,
y
X,y
X,y(不知道该记号的读者可参考我的这一篇文章)。
我们需要对传入的数据集做一些处理。在遍历数据集时,
X[i]
是向量,即
inputs
,
y[i]
是向量对应的标签,它仅仅是一个数,而非像
targets
这样的向量。
那么如何将
y[i]
处理成
targets
呢?
考虑像手写数字识别这样的十分类任务,对应的神经网络的输出层有十个神经元,每个神经元对应着一个数字。例如,当我们输入一张
5
5
5 的图片时,
5
5
5 对应的神经元的输出值应该是最高的,其他神经元的输出值应该是最低的。
因为激活函数是Sigmoid函数,它的输出区间为
(
0
,
1
)
(0,1)
(0,1)。我们将输出层神经元按照其对应的数字从小到大进行排列,从而输出层的情况应当**十分接近**下面这个样子:
[
0
,
0
,
0
,
0
,
0
,
1
,
0
,
0
,
0
,
0
]
[0,0,0,0,0,1,0,0,0,0]
[0,0,0,0,0,1,0,0,0,0]
y
中包含了所有向量的标签,若要判断我们面临的问题是一个几分类问题,我们需要对
y
进行去重,再将结果从小到大进行排序:
self.classes = np.unique(y)
例如,
y = [2, 0, 0, 1, 0, 1, 0, 2, 2]
,则
self.classes = [0, 1, 2]
。即
self.classes
中按序存储了所有的标签种类。
获得
targets
的步骤如下:
首先创建全为
0
0
0 的数组,即
targets = np.zeros(len(self.classes))
然后我们要获取
inputs
的标签在
self.classes
中的索引,即
index =list(self.classes).index(y[i])
然后再赋值
targets[index]=1
上述过程可以统一为两步:
targets = np.zeros(len(self.classes))
targets[list(self.classes).index(y[i])]=1
我们前面提到,训练停止当且仅当下面两个条件满足其中一个:
- 迭代收敛,即 E < ϵ E<\epsilon E<ϵ;
- 迭代次数达到最大值,即训练了
max_iter
次。
如果第一个条件先达到,则我们立刻停止训练;如果第二个条件先达到,说明我们训练了
max_iter
次也没有收敛,此时应当抛出一个错误。
deftrain(self, X, y):# 初始时未收敛
convergence =False
self.classes = np.unique(y)# 开始迭代 max_iter 次for _ inrange(self.max_iter):# 遍历数据集for idx inrange(len(X)):
inputs, targets = X[idx], np.zeros(len(self.classes))
targets[list(self.classes).index(y[idx])]=1# 正向计算
alpha, b, beta, outputs, error = self.propagate(inputs, targets)# 若误差不超过容忍度,则判定收敛if error <= self.tol:
convergence =Truebreakelse:# 反向传播
self.back_propagate(inputs, targets, alpha, b, beta, outputs)if convergence:breakifnot convergence:raise RuntimeError('神经网络的训练未收敛,请调大max_iter的值')
2.2.4 预测
训练好的神经网络理应能对单个样本完成分类,我们选择
outputs
中输出最高的神经元对应的类别标签作为我们对样本的预测标签:
defpredict(self, sample):
alpha = np.dot(self.V, sample)
b = self.sigmoid(alpha - self.gamma)
beta = np.dot(self.W, b)
outputs = self.sigmoid(beta - self.theta)# 计算输出最高的神经元在outputs中的索引
idx =list(outputs).index(max(outputs))
label = self.classes[idx]return label
2.2.5 完善网络
到此为止,我们的神经网络就已经完成
99
%
99\%
99% 了,但还有一些细节要处理。
- 我们希望神经网络能够根据数据集自动调整输入层结点数和输出层结点数。
- 一些参数可以预先设置好,例如学习率,容忍度等。
- 我们需要进行一些数据预处理,即将 X X X 归一化。
对于前两个,我们需要修改
__init__()
,如下:
def__init__(self, X, y, hiddennodes, learningrate=0.01, tol=1e-3, max_iter=1000):
self.X, self.y = X, y
self.classes = np.unique(self.y)
self.ins, self.hns, self.ons =len(X[0]), hiddennodes,len(self.classes)
self.lr = learningrate
self.tol = tol
self.max_iter = max_iter
self.V = np.random.rand(self.hns, self.ins)
self.W = np.random.rand(self.ons, self.hns)
self.gamma = np.random.rand(self.hns)
self.theta = np.random.rand(self.ons)
self.sigmoid =lambda x:1/(1+ np.power(np.e,-x))
同时还需要修改一下
train()
:
deftrain(self):
convergence =Falsefor _ inrange(self.max_iter):for idx inrange(len(self.X)):
inputs, targets = self.X[idx], np.zeros(self.ons)
targets[list(self.classes).index(self.y[idx])]=1
alpha, b, beta, outputs, error = self.propagate(inputs, targets)if error <= self.tol:
convergence =Truebreakelse:
self.back_propagate(inputs, targets, alpha, b, beta, outputs)if convergence:breakifnot convergence:raise RuntimeError('神经网络的训练未收敛,请调大max_iter的值')
对于第三个,我们希望将
X
X
X 归一化,但又不希望
X
X
X 中有太多的
0
0
0,否则会造成权重更新失败,因此需要作变换:
X
:
=
(
X
−
min
(
X
)
max
(
X
)
−
min
(
X
)
)
⋅
0.99
+
0.01
X:=\left(\frac{X-\min(X)}{\max(X)-\min(X)}\right)\cdot 0.99 +0.01
X:=(max(X)−min(X)X−min(X))⋅0.99+0.01
该变换能将
X
X
X 中的元素映射到
[
0.01
,
1
]
[0.01,1]
[0.01,1] 中。
该步骤需要在数据集传入到神经网络实例之前完成。
完整的单隐层神经网络代码如下:
import numpy as np
classNeuralNetwork:def__init__(self, X, y, hiddennodes, learningrate=0.01, tol=1e-3, max_iter=1000):
self.X, self.y = X, y
self.classes = np.unique(self.y)
self.ins, self.hns, self.ons =len(X[0]), hiddennodes,len(self.classes)
self.lr = learningrate
self.tol = tol
self.max_iter = max_iter
self.V = np.random.rand(self.hns, self.ins)
self.W = np.random.rand(self.ons, self.hns)
self.gamma = np.random.rand(self.hns)
self.theta = np.random.rand(self.ons)
self.sigmoid =lambda x:1/(1+ np.power(np.e,-x))defpropagate(self, inputs, targets):
alpha = np.dot(self.V, inputs)
b = self.sigmoid(alpha - self.gamma)
beta = np.dot(self.W, b)
outputs = self.sigmoid(beta - self.theta)
error = np.linalg.norm(targets - outputs,ord=2)**2/2return alpha, b, beta, outputs, error
defback_propagate(self, inputs, targets, alpha, b, beta, outputs):
g =(targets - outputs)* outputs *(np.ones(len(outputs))- outputs)
e = np.dot(self.W.T, g)* b *(np.ones(len(b))- b)
self.W += self.lr * np.outer(g, b)
self.theta +=-self.lr * g
self.V += self.lr * np.outer(e, inputs)
self.gamma +=-self.lr * e
deftrain(self):
convergence =Falsefor _ inrange(self.max_iter):for idx inrange(len(self.X)):
inputs, targets = self.X[idx], np.zeros(self.ons)
targets[list(self.classes).index(self.y[idx])]=1
alpha, b, beta, outputs, error = self.propagate(inputs, targets)if error <= self.tol:
convergence =Truebreakelse:
self.back_propagate(inputs, targets, alpha, b, beta, outputs)if convergence:breakifnot convergence:raise RuntimeError('神经网络的训练未收敛,请调大max_iter的值')defpredict(self, sample):
alpha = np.dot(self.V, sample)
b = self.sigmoid(alpha - self.gamma)
beta = np.dot(self.W, b)
outputs = self.sigmoid(beta - self.theta)return self.classes[list(outputs).index(max(outputs))]
我们将其保存到
nn.py
中。
三、实战演练
本章节中,我们使用
sklearn
中的鸢尾花数据集来训练并测试神经网络。
另起一个新的
.py
文件,首先需要导入:
from nn import NeuralNetwork # 导入我们的神经网络from sklearn.model_selection import train_test_split # 划分训练集和测试集from sklearn.metrics import accuracy_score # 计算准确率from sklearn.datasets import load_iris # 鸢尾花数据集import numpy as np
先做数据预处理:
X, y = load_iris(return_X_y=True)
X =((X - X.min())/(X.max()- X.min()))*0.99+0.01
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
创建一个神经网络实例,将隐藏层结点数设为 10,然后进行训练:
bpnn = NeuralNetwork(X_train, y_train,10, max_iter=3000)# max_iter过小会导致不收敛
bpnn.train()
定义一个测试函数用来测试神经网络在测试集上的分类准确率:
deftest(bpnn, X_test, y_test):
y_pred =[bpnn.predict(X_test[i])for i inrange(len(X_test))]return accuracy_score(y_test, y_pred)print(test(bpnn, X_test, y_test))# 1.0
可见训练完成的神经网络在测试集上的分类准确率达
100
%
100\%
100%。
完整的训练/测试代码如下:
from nn import NeuralNetwork
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_iris
import numpy as np
deftest(bpnn, X_test, y_test):
y_pred =[bpnn.predict(X_test[i])for i inrange(len(X_test))]return accuracy_score(y_test, y_pred)
X, y = load_iris(return_X_y=True)
X =((X - X.min())/(X.max()- X.min()))*0.99+0.01
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
bpnn = NeuralNetwork(X_train, y_train,10, max_iter=3000)
bpnn.train()print(test(bpnn, X_test, y_test))
版权归原作者 serity 所有, 如有侵权,请联系我们删除。