神经网络原理与代码实现(从小白入门到系统性理解)
一、神经网络的基本组成与基本步骤
神经网络通常由输入层、隐藏层、输出层组成,隐藏层可以有多层,最基本的神经网络就是三层。
每个层由若干神经元构成,其中输入层神经元个数通常与特征数相关、输出层个数与类别数相同、隐藏层的神经元数可以自定义。
以最简单的三层全连接神经网络为例,如图1。
图1
神经网络的执行步骤:初始化 --> 前向传播 --> 计算损失 --> 反向传播 --> 更新权重 --> 迭代至结束条件
神经网络的目的:输入特征,映射到输出结果,寻找最优的参数,使得预测的损失最小,得到较为成功的模型。
二、前向传播forward pass
1、初始化参数
如上图1,一个三层神经网络,一纵列为一层。
输入层神经单元2个+偏置项1个,x1和x2是两个输入值;隐藏层一层,5个神经元,隐藏层每个神经元的输出为Oi;输出层1个,为y。
各层之间为全连接,每条连接边代表一个权重参数w。例子中有输入层权重和输出层权重。
每一层合起来,以及各层之间的权重系数合起来,就组成对应的矩阵。输入层与隐藏层之间的权重,为输入权重矩阵input_weights;隐藏层与输出层之间的权重,为输出权重矩阵output_weights。
参数的初始化方法有多种,根据需要合理设计,能够在一定程度上防止梯度消失、梯度爆炸,每一层网络的输出值不宜过小、过大。
几种初始化方法:
零初始化 zero initialization:导致神经元输出相同,网络无法有效学习
随机初始化 Random initialization: 打破对称性,但随机值过大则容易导致梯度爆炸。
正态分布初始化 Normal distribution
预训练模型初始化:利用预训练好的模型权重作为初始化。
初始化的代码实现如下:
import random
import math
def rand(a, b):
# random()产生随机小数,乘以差值,则能产生a到b之间的随机数
return (b-a) * random.random() + a
def make_matrix(m, n, fill=0.0): # 生成一个m*n的矩阵
mat = []
for i in range(m):
mat.append([fill] * n) # 浮点数
return mat
class BP():
def setup(self, ni, nh, no):
# 初始化输入数据元的个数ni、隐藏层神经元个数nh、输出层神经元数no
self.input_n = ni + 1 # 多加一个偏置神经元,提供可控输入修正
self.hidden_n = nh
self.output_n = no
# 初始化神经元
self.input_cells = self.input_n * [1.0]
self.hidden_cells = self.hidden_n * [1.0]
self.output_cells = self.output_n * [1.0]
'''
全连接的,一个输入单元,与其他所有的隐藏层单元都有连接,每个连接有一个权重参数,则共有i*h个,为输入权重矩阵。
输入单元*权重,对所有连接同一个下层单元的求和,就得到下层单元的值value。
同理,隐藏层单元*输出层单元,则有输出权重矩阵
'''
# 初始化权重矩阵
self.input_weights = make_matrix(self.input_n, self.hidden_n)
self.output_weights = make_matrix(self.hidden_n, self.output_n)
# 权重矩阵随机激活
for i in range(self.input_n):
for h in range(self.hidden_n):
self.input_weights[i][h] = rand(-0.2, 0.2) # 为何是这个范围?小值随机初始化,以防过大
for h in range(self.hidden_n):
for o in range(self.output_n):
self.output_weights[h][o] = rand(-0.2, 0.2)
'''
矫正矩阵机制:为了加快学习的效率。
记录上一次反向传播过程中的EjOi,即误差*神经元输出
'''
# 初始化矫正矩阵, 0初始化
self.input_correction = make_matrix(self.input_n, self.hidden_n)
self.output_correction = make_matrix(self.hidden_n, self.output_n)
2、单个神经元的计算
每个神经元实质是一系列计算。如上图1隐藏层的第一个神经元,可看到一个神经元中进行了两步计算,一是求和,二是非线性激活。单独可看下图:
(1)求和
单个神经元,需要对与上一层连接的所有神经元的值进行加权求和,权就是边上的值w。求和公式:e =ΣWijXi + b(偏置,如果有)
以隐藏层第一个神经元为例,求和如下:
e = w11x1 + w21x2 + b
(2)激活
激活选用非线性激活,这样在多个隐藏层的情况下,神经网络才有了深度的意义,使网络能够拟合复杂非线性模型;否则,如果是线性激活,层数再多,线性嵌套后仍旧是一个线性,都等同于一个隐藏层。
常用的激活函数(activation function)有:sigmoid、ReLu、tanh等。不同函数的特性与优缺点不同。
代码实现中采用sigmoid函数作为网络的激活函数,具有非线性特性和稳定性,其导数在0-1之间,梯度的变化不会过大。
sigmoid函数的导数是:
推导过程:
前一步求和的结果e,经过激活函数f后,得到隐藏层第一个神经元的输出o1 = f(e),f就是sigmoid函数。
其余神经元计算同理,包括输出层神经元的输出,也是对所有连接的隐藏层神经元输出加权求和并激活。
直到获得输出层的预测输出,这个过程就是前向传播的过程。
代码实现如下,遍历每一个神经元进行计算:
## 前向传播网络
def predict(self, inputs):
# 激活输入层
for i in range(self.input_n - 1):
self.input_cells[i] = inputs[i]
# 激活隐藏层 计算隐藏层的值,前向传播
for j in range(self.hidden_n):
total = 0.0
for i in range(self.input_n):
total += self.input_cells[i] * self.input_weights[i][j]
# 激活函数激活,对前一层的累加结果进行激活
self.hidden_cells[j] = sigmoid(total)
# 计算输出层的值
for k in range(self.output_n):
total = 0.0
for j in range(self.hidden_n):
total += self.hidden_cells[j] * self.output_weights[j][k]
self.output_cells[k] = sigmoid(total)
return self.output_cells[:]
三、计算损失,反向传播(BackPropagation,BP)
1、什么是损失函数 Loss Function
每一个样本经过前向传播(forward)后,都会得到一个预测输出值。而每个样本应该有个标签值或真实值(GroundTruth)。
预测值与真实值之间的差值,就成为损失,损失值越小,证明模型越成功。
有许多不同种类的损失函数,经过pytorch、TensorFlow等库封装后,有了具体的名字。
学习的目的是让预测值无限接近真实值,因此损失函数可以作为衡量的方法。
常用损失函数:L2 Loss均方误差(Mean Squared Error, MSE)、L1 Loss平均绝对值误差(Mean Absolute Error,MAE)
代码实现中选取L2 Loss均方误差MSE。MSE的公式如下:
label为真实值,self.output_cells为预测值,代码如下:
error = 0.0
for o in range(len(label)):
error += 0.5 * (label[o] - self.output_cells[o]) ** 2
2、反向传播算法
反向传播算法,是“误差反向传播”的简称。建立在梯度下降算法的基础上。
网络为了找到合适的参数w、b,使得代价函数取值最小,利用梯度下降法求解代价函数的最优化问题。
梯度下降算法:找到目标函数L2 Loss的最小值。梯度是一个向量,目标函数在具体某点沿着梯度相反的方向下降最快。每次梯度下降都遍历整个数据集。每走一步就迭代更新一次。
在反向传播过程中,我们计算损失函数L相对于每个权重的梯度,并基于此更新权重。
推导如下:
其中,σ 是 Sigmoid 函数(对应前面的f), z 是线性组合(对应前面的e),a 是激活后的输出(对应前面的o),
y
^
\hat{y}
y^ 是预测值。
①计算输出层误差
损失函数
L = 1 2 ( y ^ − y ) 2 L=\frac{1}{2}(\hat{y}-y)^2 L=21(y^−y)2
输出层误差项
δ
o
=
∂
L
∂
y
^
⋅
∂
y
^
∂
z
o
\delta_o = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial z_o}
δo=∂y^∂L⋅∂zo∂y^
其中:
∂
L
∂
y
^
=
y
^
−
y
\frac{\partial L}{\partial \hat{y}} = \hat{y} - y
∂y^∂L=y^−y
∂
y
^
∂
z
o
=
σ
′
(
z
o
)
\frac{\partial \hat{y}}{\partial z_o} = \sigma'(z_o)
∂zo∂y^=σ′(zo)
因此:
δ
o
=
(
y
^
−
y
)
⋅
σ
′
(
z
o
)
\delta_o = (\hat{y} - y) \cdot \sigma'(z_o)
δo=(y^−y)⋅σ′(zo)
由于激活函数sigmoid的导数为 σ ′ ( z o ) = σ ( z o ) ⋅ ( 1 − σ ( z o ) ) \sigma'(z_o) = \sigma(z_o) \cdot (1 - \sigma(z_o)) σ′(zo)=σ(zo)⋅(1−σ(zo)),
最终我们有输出层误差公式:
δ
o
=
(
y
^
−
y
)
⋅
y
^
⋅
(
1
−
y
^
)
\delta_o = (\hat{y} - y) \cdot \hat{y} \cdot (1 - \hat{y})
δo=(y^−y)⋅y^⋅(1−y^)
即,预测误差 * sigmoid’(预测值)
实现代码如下:
# 获取输出层误差
output_deltas = [0.0] * self.output_n
for o in range(self.output_n):
error = label[o] - self.output_cells[o]
output_deltas[o] = sigmoid_derivate(self.output_cells[o]) * error
②计算隐藏层误差
由于隐藏层没有label值,所以隐藏层误差使用下一层误差的加权和代替,进行反向传播:
δ
h
=
∑
o
δ
o
w
h
o
⋅
σ
′
(
z
h
)
\delta_h = \sum_{o} \delta_o w_{ho} \cdot \sigma'(z_h)
δh=∑oδowho⋅σ′(zh)
其中:
σ
′
(
z
h
)
=
σ
(
z
h
)
⋅
(
1
−
σ
(
z
h
)
)
\sigma'(z_h) = \sigma(z_h) \cdot (1 - \sigma(z_h))
σ′(zh)=σ(zh)⋅(1−σ(zh))
因此:
δ
h
=
(
∑
o
δ
o
w
h
o
)
⋅
a
h
⋅
(
1
−
a
h
)
\delta_h = \left( \sum_{o} \delta_o w_{ho} \right) \cdot a_h \cdot (1 - a_h)
δh=(∑oδowho)⋅ah⋅(1−ah)
即,隐藏层误差=连接的所有输出单元误差的加权和*sigmoid’(隐藏层输出值)
代码实现如下:
# 获取隐藏层误差, 输出层的误差*输出权重,开始反向传播
hidden_deltas = [0.0] * self.hidden_n
for h in range(self.hidden_n):
# 先计算每个error的值,因为隐藏层无label,使用下一层的误差的加权作为error
error = 0.0
for o in range(self.output_n):
error += output_deltas[o] * self.output_weights[h][o]
# 再计算每个隐藏层神经元的误差
hidden_deltas[h] = sigmoid_derivate(self.hidden_cells[h]) * error
③更新权重
权重更新公式为:
w
i
j
=
w
i
j
−
η
∂
L
∂
w
i
j
w_{ij} = w_{ij} - \eta \frac{\partial L}{\partial w_{ij}}
wij=wij−η∂wij∂L
根据链式法则:
∂
L
∂
w
h
o
=
δ
o
a
h
\frac{\partial L}{\partial w_{ho}} = \delta_o a_h
∂who∂L=δoah
∂
L
∂
w
i
h
=
δ
h
x
i
\frac{\partial L}{\partial w_{ih}} = \delta_h x_i
∂wih∂L=δhxi
因此,权重更新公式为:
w
h
o
=
w
h
o
−
η
δ
o
a
h
w_{ho} = w_{ho} - \eta \delta_o a_h
who=who−ηδoah
w
i
h
=
w
i
h
−
η
δ
h
x
i
w_{ih} = w_{ih} - \eta \delta_h x_i
wih=wih−ηδhxi
实际中,为了加快学习的效率,我们引入矫正矩阵机制,矫正矩阵记录上一次反向传播过程中的σo ah或σh xi, 记为Cij(将对应代码中的change变量),μ为矫正率参数,对应代码中的correct变量。
于是,公式变为:
w
=
w
+
η
δ
a
+
μ
C
i
j
w= w + \eta \delta a + μ Cij
w=w+ηδa+μCij
代码实现如下:
# 更新输出权重
for h in range(self.hidden_n):
for o in range(self.output_n):
# Wij = Wij + λ Ej Oi + μCij
# 公式:W' = W + 学习率η或者λ * Δ(误差和σ * 偏导权重) * cells
change = output_deltas[o] * self.hidden_cells[h] # 输出误差 * 隐藏层的单元值
self.output_weights[h][o] += learn * change + correct * self.output_correction[h][o]
self.output_correction[h][o] = change
# 更新输入权重
for i in range(self.input_n):
for h in range(self.hidden_n):
# Wij = Wij + λ [h] * self.input_cells[i]
change = hidden_deltas[h] * self.input_cells[i]
self.input_weights[i][h] += learn * change + correct * self.input_correction[i][h]
self.input_correction[i][h] = change # 记录上一次的权重更新的EjOi,有记忆性,有点类似动量梯度下降法
3、整个反向传播过程
## 反向传播(误差反向传播),更新权重,返回最终预测误差
def back_propagate(self, case, label, learn, correct):
# 执行前馈
self.predict(case)
'''
计算误差的方法有很多:误差平方和、熵Entropy、交叉熵
'''
# 获取输出层误差
output_deltas = [0.0] * self.output_n
for o in range(self.output_n):
error = label[o] - self.output_cells[o] ## 损失:真实值与预测值的差值,损失越小,模型越佳。pytorch、tensorflow库封装有不同的损失函数
output_deltas[o] = sigmoid_derivate(self.output_cells[o]) * error
# 获取隐藏层误差, 输出层的误差*输出权重,开始反向传播
hidden_deltas = [0.0] * self.hidden_n
for h in range(self.hidden_n):
# 先计算每个error的值,因为隐藏层无label,使用下一层的误差的加权作为error
error = 0.0
for o in range(self.output_n):
error += output_deltas[o] * self.output_weights[h][o]
# 再计算每个隐藏层神经元的误差
hidden_deltas[h] = sigmoid_derivate(self.hidden_cells[h]) * error
# 更新输出权重
for h in range(self.hidden_n):
for o in range(self.output_n):
# Wij = Wij + λ Ej Oi + μCij
# 公式:W' = W + 学习率η或者λ * Δ(误差和σ * 偏导权重) * cells
change = output_deltas[o] * self.hidden_cells[h] # 输出误差 * 隐藏层的单元值
self.output_weights[h][o] += learn * change + correct * self.output_correction[h][o]
self.output_correction[h][o] = change
# 更新输入权重
for i in range(self.input_n):
for h in range(self.hidden_n):
# Wij = Wij + λ [h] * self.input_cells[i]
change = hidden_deltas[h] * self.input_cells[i]
self.input_weights[i][h] += learn * change + correct * self.input_correction[i][h]
self.input_correction[i][h] = change # 记录上一次的权重更新的EjOi,有记忆性,类似动量梯度下降法
### 获取全局误差
error = 0.0
for o in range(len(label)):
error += 0.5 * (label[o] - self.output_cells[o]) ** 2
return error
四、迭代至结束
每轮次epoch都会遍历训练样本,每个样本都会前向传播一次以及反向传播一次,并更新一次整个网络权重。
学习率(learning rate,lr):控制模型的学习进度, 也叫步长stride。反向传播中的η。
学习率 | 大 | 小 |
---|---|---|
学习速度 | 快 | 慢 |
使用时间 | 刚开始训练时 | 一定轮数后 |
缺点 | 易损失值爆炸;易震荡 | 易过拟合;收敛速度慢 |
代码实现中,采用0.05的学习率,无学习率衰减策略。
代码实现:
## 训练迭代,cases为训练数据输入集,labels为训练数据标签集,可以修改最大迭代次数limit, learn学习率λ(一般在0-0.1上取值), correct矫正率μ三个参数
def train(self, cases, labels, limit=10000, learn=0.05, correct=0.1):
# 设置了最大迭代次数,最简单的训练终止条件,不能保证训练结果的精确度
for i in range(limit):
error = 0.0
# 每一个训练样本都会更新一次整个网络的参数
for i in range(len(cases)):
label = labels[i]
case = cases[i]
error += self.back_propagate(case, label, learn, correct)
def test(self):
# 训练数据的输入集
cases = [
[0, 0],
[0, 1],
[1, 0],
[1, 1],
]
# 训练数据的标签集
labels = [[0], [1], [1], [0]]
self.setup(2, 5, 1) # 设置各层的神经元数量
self.train(cases, labels, 10000, 0.5, 0.1)
for case in cases:
print(self.predict(case))
五、神经网络整体实现代码
该神经网络的目的是学习异或逻辑,相同为0,相异为1.输入的数据集为四个,分别为(0,0),(0,1),(1,0),(1,1)。对应的标签分别为0,1,1,0.
网络的预测结果输出为:
import random
import math
def rand(a, b):
# random()产生随机小数,乘以差值,则能产生a到b之间的随机数
return (b-a) * random.random() + a
def make_matrix(m, n, fill=0.0): # 生成一个m*n的矩阵
mat = []
for i in range(m):
mat.append([fill] * n) # 浮点数
return mat
# 传入的x,实际为神经元中计算上一层得到的累加和,然后激活
def sigmoid(x): # sigmoid函数
return 1.0 / (1.0 + math.exp(-x))
# sigmoid' = sigmoid * (1-sigmoid), 这里传入的x需是sigmoid(原始x),即经过激活后的值,与上一个x不同
def sigmoid_derivate(x): # sigmoid的导数
return x * (1-x)
class BP():
def setup(self, ni, nh, no):
# 初始化输入数据元的个数、隐藏层个数?、输出层单元数
self.input_n = ni + 1 # why? 多加一个偏置神经元,提供可控输入修正?如何理解?
self.hidden_n = nh
self.output_n = no
# 初始化神经元
self.input_cells = self.input_n * [1.0]
self.hidden_cells = self.hidden_n * [1.0]
self.output_cells = self.output_n * [1.0]
'''
全连接的,一个输入单元,与其他所有的隐藏层单元都有连接,每个连接有一个权重参数,则共有i*h个,为输入权重矩阵。
输入单元*权重,对所有连接同一个下层单元的求和,就得到下层单元的值value。
同理,隐藏层单元*输出层单元,则有输出权重矩阵
'''
# 初始化权重矩阵
self.input_weights = make_matrix(self.input_n, self.hidden_n)
self.output_weights = make_matrix(self.hidden_n, self.output_n)
# 权重矩阵随机激活
for i in range(self.input_n):
for h in range(self.hidden_n):
self.input_weights[i][h] = rand(-0.2, 0.2) # 为何是这个范围?
for h in range(self.hidden_n):
for o in range(self.output_n):
self.output_weights[h][o] = rand(-0.2, 0.2)
'''
合理设计初始化,防止梯度消失、梯度爆炸,每一层的网络输出值不能太大也不能太小。均匀分布初始化会爆炸?
零初始化zero initialization:导致神经元输出相同,网络无法有效学习
随机初始化 Random initialization: 打破对称性,但随机值过大则容易导致梯度爆炸
正态分布初始化 Normal distribution
预训练模型初始化:利用预训练好的模型权重作为初始化。
以上均为找来的结论,我自己没有实践验证,俗称答案。
'''
'''
矫正矩阵机制:为了加快学习的效率。
记录上一次反向传播过程中的EjOi,即误差*神经元输出
'''
# 初始化矫正矩阵, 0初始化
self.input_correction = make_matrix(self.input_n, self.hidden_n)
self.output_correction = make_matrix(self.hidden_n, self.output_n)
## 前向传播网络
def predict(self, inputs):
# 激活输入层
for i in range(self.input_n - 1):
self.input_cells[i] = inputs[i]
# 激活隐藏层 计算隐藏层的值,前向传播
for j in range(self.hidden_n):
total = 0.0
for i in range(self.input_n):
total += self.input_cells[i] * self.input_weights[i][j]
# 激活函数激活,对前一层的累加和进行激活
self.hidden_cells[j] = sigmoid(total)
# 计算输出层的值
for k in range(self.output_n):
total = 0.0
for j in range(self.hidden_n):
total += self.hidden_cells[j] * self.output_weights[j][k]
self.output_cells[k] = sigmoid(total)
return self.output_cells[:]
## 反向传播(误差反向传播),更新权重,返回最终预测误差
def back_propagate(self, case, label, learn, correct):
# 执行前馈
self.predict(case)
'''
计算误差的方法有很多:误差平方和、熵Entropy、交叉熵
'''
# 获取输出层误差
output_deltas = [0.0] * self.output_n
for o in range(self.output_n):
error = label[o] - self.output_cells[o] ## 损失:真实值与预测值的差值,损失越小,模型越佳。pytorch、tensorflow库封装有不同的损失函数
output_deltas[o] = sigmoid_derivate(self.output_cells[o]) * error ## 为何求导?因为反向更新权重的时候,需要计算每个权重的偏导数,起缩放因子作用,占比不同。实质是损失函数L相对于每个权重的梯度,推出的整体公式,并非是只单纯地对sigmoid求导。
# 获取隐藏层误差, 输出层的误差*输出权重,开始反向传播
hidden_deltas = [0.0] * self.hidden_n
for h in range(self.hidden_n):
# 先计算每个error的值,因为隐藏层无label,使用下一层的误差的加权作为error
error = 0.0
for o in range(self.output_n):
error += output_deltas[o] * self.output_weights[h][o]
# 再计算每个隐藏层神经元的误差
hidden_deltas[h] = sigmoid_derivate(self.hidden_cells[h]) * error
# 更新输出权重
for h in range(self.hidden_n):
for o in range(self.output_n):
# Wij = Wij + λ Ej Oi + μCij
# 公式:W' = W + 学习率η或者λ * Δ(误差和σ * 偏导权重) * cells
change = output_deltas[o] * self.hidden_cells[h] # 输出误差 * 隐藏层的单元值
self.output_weights[h][o] += learn * change + correct * self.output_correction[h][o]
## why don't renew the output_correction here
self.output_correction[h][o] = change
# 更新输入权重
for i in range(self.input_n):
for h in range(self.hidden_n):
# Wij = Wij + λ [h] * self.input_cells[i]
change = hidden_deltas[h] * self.input_cells[i]
self.input_weights[i][h] += learn * change + correct * self.input_correction[i][h]
self.input_correction[i][h] = change # 记录上一次的权重更新的EjOi,有记忆性,类似动量梯度下降法?
### 获取全局误差
'''
损失函数/代价函数
均方误差MSE,Mean Squared Error = 1/2 Σ(y - y尖)²
目的是:找到合适的参数w,b,使得代价函数的取值最小,监督学习问题就转变为最优化问题。常用的最优化实现算法是,梯度下降算法
相较于设置最大训练次数,更好的办法是使用损失函数(loss function)作为终止训练的依据
'''
error = 0.0
for o in range(len(label)):
error += 0.5 * (label[o] - self.output_cells[o]) ** 2
return error
## 训练迭代,cases为训练数据输入集,labels为训练数据标签集,可以修改最大迭代次数limit, learn学习率λ(一般在0-0.1上取值), correct矫正率μ三个参数
def train(self, cases, labels, limit=10000, learn=0.05, correct=0.1):
# 设置了最大迭代次数,最简单的训练终止条件,不能保证训练结果的精确度
for i in range(limit):
error = 0.0
# 每一个训练样本都会更新一次整个网络的参数
for i in range(len(cases)):
label = labels[i]
case = cases[i]
error += self.back_propagate(case, label, learn, correct)
def test(self):
# 训练数据的输入集
cases = [
[0, 0],
[0, 1],
[1, 0],
[1, 1],
]
# 训练数据的标签集
labels = [[0], [1], [1], [0]]
self.setup(2, 5, 1) # 设置各层的神经元数量
self.train(cases, labels, 10000, 0.5, 0.1)
for case in cases:
print(self.predict(case))
bp = BP()
bp.test()
###### 下一步计划,利用pytorch实现一个bp
六、参考资料
反向传播原理:https://blog.csdn.net/fsfjdtpzus/article/details/106256925
BP神经网络的实现:https://blog.csdn.net/qq_43350003/article/details/106986510
激活函数sigmoid:https://www.cnblogs.com/chenlin163/p/7676939.html
梯度下降法:https://blog.csdn.net/weixin_36811328/article/details/81348350
动量梯度下降法:https://blog.csdn.net/weixin_36811328/article/details/83451096
损失函数:https://blog.csdn.net/weixin_57643648/article/details/122704657
https://blog.csdn.net/qq_40280673/article/details/134293907
学习率:https://blog.csdn.net/jningwei/article/details/79243800
初始化方法:https://blog.csdn.net/u011852872/article/details/120407182