本文介绍了一个具有两个隐藏层的三层神经网络的实现过程。首先,通过符号确认,明确了权重和偏置的表示方法。然后,详细介绍了各层间信号传递的实现,包括输入层到第一隐藏层、激活函数的计算过程、第一隐藏层到第二隐藏层以及第二隐藏层到输出层。其中,激活函数采用了sigmoid函数,并介绍了如何使用NumPy实现神经网络的矩阵运算。最后,通过整理代码,展示了神经网络的前向传播过程。这篇文章详细阐述了三层神经网络的实现细节,对于理解神经网络的工作原理具有很好的参考价值。
前言
3层神经网络:输入层(第0层)有 2个神经元,第1个隐藏层(第1层)有 3个神经元, 第2个隐藏层(第2层)有 2个神经元,输出层(第3层)有 2个神经元
一、符号确认
神经网络的运算可以通过矩阵运算来实现。神经网络的每一层运算都可以看作是通过矩阵乘法来实现的(从宏观角度看)
我们首先从定义符号开始。请看下图,突出显示了从输入层神经元 x 2 x_2 x2 到后一层的神经元的权重。
权重和隐藏层的神经元的右上角有一个“(1)”,表示权重和神经元的层号(即第1层的权重、第1层的神经元)。此外,权重的右下角有两个数字,它们分别是后一层的神经元和前一层的神经元的索引号。比如, w 12 ( 1 ) w_{12}^{(1)} w12(1) 表示前一层的第2个神经元 x 2 x_2 x2 到后一层的第1个神经元的权重。权重右下角按照“后一层的索引号、前一层的索引号”的顺序排列。
二、各层间信号传递的实现
输入层到第1层
从输入层到第1层的第1个神经元的信号传递过程
图中增加了表示偏置的神经元 “1”。
请注意, 偏置的右下角的索引号只有一个。这是因为前一层的偏置神经元 (神经元 “ 1 ”) 只有一个 1 { }^1 1 。
a
1
(
1
)
a_1^{(1)}
a1(1) 通过加权信号和偏置的和按如下方式进行计算:
a
1
(
1
)
=
w
11
(
1
)
x
1
+
w
12
(
1
)
x
2
+
b
1
(
1
)
a_1^{(1)}=w_{11}^{(1)} x_1+w_{12}^{(1)} x_2+b_1^{(1)}
a1(1)=w11(1)x1+w12(1)x2+b1(1)
第 1 层的加权和的矩阵乘法运算:
A
(
1
)
=
X
W
(
1
)
+
B
(
1
)
\boldsymbol{A}^{(1)}=\boldsymbol{X} \boldsymbol{W}^{(1)}+\boldsymbol{B}^{(1)}
A(1)=XW(1)+B(1)
其中,
A
(
1
)
、
X
、
B
(
1
)
、
W
(
1
)
\boldsymbol{A}^{(1)} 、 \boldsymbol{X} 、 \boldsymbol{B}^{(1)} 、 \boldsymbol{W}^{(1)}
A(1)、X、B(1)、W(1) 如下所示。
A
(
1
)
=
(
a
1
(
1
)
a
2
(
1
)
a
3
(
1
)
)
,
X
=
(
x
1
x
2
)
,
B
(
1
)
=
(
b
1
(
1
)
b
2
(
1
)
b
3
(
1
)
)
W
(
1
)
=
(
w
11
(
1
)
w
21
(
1
)
w
31
(
1
)
w
12
(
1
)
w
22
(
1
)
w
32
(
1
)
)
\begin{aligned} \boldsymbol{A}^{(1)} & =\left(\begin{array}{lll} a_1^{(1)} & a_2^{(1)} & a_3^{(1)} \end{array}\right), \boldsymbol{X}=\left(\begin{array}{ll} x_1 & x_2 \end{array}\right), \boldsymbol{B}^{(1)}=\left(\begin{array}{lll} b_1^{(1)} & b_2^{(1)} & b_3^{(1)} \end{array}\right) \\ \boldsymbol{W}^{(1)} & =\left(\begin{array}{lll} w_{11}^{(1)} & w_{21}^{(1)} & w_{31}^{(1)} \\ w_{12}^{(1)} & w_{22}^{(1)} & w_{32}^{(1)} \end{array}\right) \end{aligned}
A(1)W(1)=(a1(1)a2(1)a3(1)),X=(x1x2),B(1)=(b1(1)b2(1)b3(1))=(w11(1)w12(1)w21(1)w22(1)w31(1)w32(1))
NumPy 多维数组实现(这里将输人信号、权重、偏置设置成任意值)
X=np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3,0.5], [0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2,0.3])
print(W1.shape) #(2, 3)
print(X.shape) #(2,)
print(B1.shape) #(3,)
A1 = np.dot(X, W1) + B1 # [0.3, 0.7, 1.1]
激活函数的计算过程
如图所示, 隐藏层的加权和 (加权信号和偏置的总和)用 a a a 表示, 被激活函数转换后的信号用 z z z 表示。此外, 图中 h ( ) h() h() 表示激活函数, 这里我们使用的是 sigmoid 函数。
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
Z1 = sigmoid(A1)
print(Z1) #[0.57444252, 0.66818777, 0.75026011]
sigmoid()函数接收NumPy数组, 并返回元素个数相同的NumPy数组。
第1层到第2层
代码如下
W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
B2 = np.array([0.1, 0.2])
print(Z1.shape) # (3,)
print(W2.shape) # (3, 2)
print(B2.shape) # (2,)
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)
除了第1层的输出(Z1)变成了第2层的输入这一点以外,这个实现和刚才的代码完全相同。
第2层到输出层
输出层的实现也和之前的实现基本相同。但最后的激活函数和之前的隐藏层有所不同。
代码如下
def identity_function(x):
return x
W3 = np.array([[0.1, 0.3], [0.2, 0.4]])
B3 = np.array([0.1, 0.2])
A3 = np.dot(Z2, W3) + B3
Y = identity_function(A3) # 或者Y = A3
这里我们定义了identity_function()函数(也称为“恒等函数”),并将其作为输出层的激活函数。恒等函数会将输入按原样输出,当然也没有必要特意定义identity_function()。这里只是为了和之前的流程保持统一。另外,输出层的激活函数用 σ ( ) σ() σ() 表示,不同于隐藏层的激活函数 h ( ) h() h()( σ σ σ 读作sigma)。
激活函数的选择通常根据问题的性质来确定:
- 对于回归问题,可以使用恒等函数,将神经网络的输出直接作为预测值。
- 对于二元分类问题,可以使用Sigmoid函数,将输出压缩到0到1之间,表示概率。
- 对于多元分类问题,可以使用Softmax函数,将输出转换为表示各个类别概率的向量。
三、代码整理
至此,我们已经介绍完了3层神经网络的实现。现在我们把之前的代码 实现全部整理一下。这里,我们按照神经网络的实现惯例,只把权重记为大 写字母W1,其他的(偏置或中间结果等)都用小写字母表示。
def init_network():
network = {}
network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
network['b1'] = np.array([0.1, 0.2, 0.3])
network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
network['b2'] = np.array([0.1, 0.2])
network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
network['b3'] = np.array([0.1, 0.2])
return network
def forward(network, x):
W1, W2, W3 = network['W1'], network['W2'], network['W3']
b1, b2, b3 = network['b1'], network['b2'], network['b3']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
z2 = sigmoid(a2)
a3 = np.dot(z2, W3) + b3
y = identity_function(a3)
return y
network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y) # [ 0.31682708 0.69627909]
这里定义了init_network()和forward()函数。init_network()函数会进行权重和偏置的初始化,并将它们保存在字典变量network中。这个字典变量network中保存了每一层所需的参数(权重和偏置)。
forward()函数中则封装了将输入信号转换为输出信号的处理过程。 forward(前向)表示的是从输入到输出方向 的传递处理。