识别手写数字
摘要
使用单隐含层的反向传播神经网络(BP神经网络),使用梯度下降法作为优化方法,成功识别手写数字并且并且准确率在94%以上。
一、问题陈述
图像识别是近来人工智能领域的一大热门方向,而神经网络因为其良好的鲁棒性,非线性函数拟合能力而受到极大推崇,我们将通过大名鼎鼎的手写数字的MNIST 数据库1,来训练我们的网络,使其达到令人识别数字的目的,本文使用的数据集使用来自经过处理以后的MINIST数据库,它提前帮助我们分好了训练集2和测试集3,并且转为了csv格式。
例子:数字7
csv文件(部分)
原图是一张张大小为28*28大小的灰度单通道图(单位:像素),经过处理以后变为长度为784像素的列表。单个像素取值区间为[0,255],如上图,第一列数字代表这张图片的数字是多少,后面的列表就是灰度值列表,我们使用的训练集包括60000个样本,测试集包括10000个样本。
二.建立模型
二.1 符号说明
符号 | 说明 |
---|---|
W i j W_{ij} Wij | i,j层权重矩阵 |
W j k W_{jk} Wjk | j,k层权重矩阵 |
X | 输入矩阵 |
S(X) | 阈值函数 |
E | 误差 |
α \alpha α | 学习率 |
二.2 模型推导:
单个神经元工作原理为
输入向量
X
=
{
x
1
.
.
.
x
n
}
(1)
X = \left\{ \begin{matrix} x_1 \\ ... \\ x_n \end{matrix} \right\} \tag{1}
X=⎩⎨⎧x1...xn⎭⎬⎫(1)
权重矩阵
W
=
{
w
1
,
j
.
.
.
w
n
,
j
.
.
.
w
i
,
j
.
.
.
w
n
,
j
.
.
.
w
n
,
n
}
(2)
W = \left\{ \begin{matrix} w_{1,j} &... & w_{n,j} \\ ... & w_{i,j} & ... \\ w_{n,j} & ... & w_{n,n} \end{matrix} \right\} \tag{2}
W=⎩⎨⎧w1,j...wn,j...wi,j...wn,j...wn,n⎭⎬⎫(2)
输出
o
u
t
p
u
t
=
Y
(
∑
W
i
,
j
X
i
)
output = Y(\sum W^{i,j}X^{i})
output=Y(∑Wi,jXi)
阈值函数
Y
Y
Y选用Sigmod函数定义为
S
(
x
)
S(x)
S(x)
S
(
x
)
=
1
1
+
e
−
x
S(x) = \frac{1}{1+e^{-x}}
S(x)=1+e−x1
二.3反向误差传播
现在有一个三层神经网络
输出定义为
O
k
O_k
Ok,标记值为
t
k
t_k
tk,误差定义为
E
k
=
O
k
−
t
k
,
k
=
{
1
,
2
}
E_k=O_k - t_k,k=\{1,2\}
Ek=Ok−tk,k={1,2}
则最终层得到的误差矩阵为
E
=
{
e
1
,
e
2
}
(3)
E=\begin{Bmatrix}e_1,e_2\end{Bmatrix} \tag{3}
E={e1,e2}(3)
反向误差传播就是指将获得的误差输回网络,使其调整权重,以获得更小的误差,从而获得学习的能力,而我们传播误差的多寡是根据权重确定的,就是说,某条权重在这个网络中占的比重的多少,占得越多的分到的误差也就越多, 然后形成新的误差矩阵,再向下一层传播,举例来说:
隐藏层(j)获得的误差
E h i d d e n = [ w 1 , 1 w 2 , 1 + w 1 , 1 w 1 , 2 w 1 , 2 + w 2 , 2 w 2 , 1 w 2 , 1 + w 1 , 1 w 2 , 2 w 1 , 2 + w 2 , 2 ] [ e 1 e 2 ] (4) E_{hidden}=\begin{bmatrix} \frac{w_{1,1}}{w_{2,1} + w_{1,1}}&\frac{w_{1,2}}{w_{1,2} + w_{2,2}}\\\\\frac{w_{2,1}}{w_{2,1} + w_{1,1}}&\frac{w_{2,2}}{w_{1,2} + w_{2,2}} \end{bmatrix} \tag{4}\begin{bmatrix}e_1\\e_2\end{bmatrix} Ehidden=⎣⎡w2,1+w1,1w1,1w2,1+w1,1w2,1w1,2+w2,2w1,2w1,2+w2,2w2,2⎦⎤[e1e2](4)
不过为了计算的方便,我实际使用的是下面这个公式,我切除了归一化因子,这样只是失去了后馈误差的大小,实际证明这种简单的误差传播方式与复杂的一样有效。
E h i d d e n = [ w 1 , 1 w 1 , 2 w 2 , 1 w 2 , 2 ] [ e 1 e 2 ] (5) E_{hidden}=\begin{bmatrix} w_{1,1}&w_{1,2}\\\\w_{2,1}&w_{2,2} \end{bmatrix} \tag{5}\begin{bmatrix}e_1\\e_2\end{bmatrix} Ehidden=⎣⎡w1,1w2,1w1,2w2,2⎦⎤[e1e2](5)
所以,隐藏层的误差计算公式为
E
h
i
d
d
e
n
=
W
o
u
t
p
u
t
_
h
i
d
d
e
n
T
E
o
u
t
p
u
t
(6)
E_{hidden}=W_{output_hidden}^TE_{output}\tag{6}
Ehidden=Woutput_hiddenTEoutput(6)
二.4 使用梯度下降法来更新权重
首先定义一个损失函数
E
=
1
2
m
∑
i
m
(
O
k
−
t
k
)
2
(7)
E = \frac{1}{2m}\sum_i^m(O_k-t_k)^2\tag{7}
E=2m1i∑m(Ok−tk)2(7)
使用这个损失函数的原因是因为它可以消除正负误差所带来的抵消效应,同时平滑连续,这使得梯度下降法很好地发挥作用——没有间断,也没有突然的跳跃。
然后我们对
W
o
u
t
p
u
t
_
h
i
d
d
e
n
(
W
j
k
)
W_{output_hidden}(W_{jk})
Woutput_hidden(Wjk)求偏导
∂
E
∂
W
j
k
=
∂
1
2
m
∑
i
m
(
O
k
−
t
k
)
2
∂
W
j
k
(8)
\frac{\partial E}{\partial W_{jk}}=\frac{\partial \frac{1}{2m}\sum_i^m(O_k-t_k)^2}{\partial W_{jk}\tag{8}}
∂Wjk∂E=∂Wjk∂2m1∑im(Ok−tk)2(8)
但是实际上误差函数并不需要对所有节点求和,因为单个节点的输出只与与它相连的权重有关系,所以实际上应该写成
S
(
X
j
k
)
=
s
i
g
m
o
d
(
∑
W
j
k
O
j
)
∂
E
∂
W
j
k
=
∂
1
2
(
O
k
−
t
k
)
2
∂
W
j
k
=
∂
E
∂
O
k
∂
O
k
∂
W
j
k
=
−
(
O
k
−
t
k
)
S
(
X
j
k
)
)
(
1
−
S
(
X
j
k
)
)
O
j
T
(9)
\\S(X_{jk})=sigmod(\sum W_{jk}O{j})\\\frac{\partial E}{\partial W_{jk}}=\frac{\partial\frac{1}{2}(O_k-t_k)^2}{\partial W_{jk}}\\ =\frac{\partial E}{\partial O_k}\frac{\partial O_k}{\partial W_{jk}}\\ =-(O_k-t_k)S(X_{jk}))(1-S(X_{jk}))O_j^T\tag{9}
S(Xjk)=sigmod(∑WjkOj)∂Wjk∂E=∂Wjk∂21(Ok−tk)2 =∂Ok∂E∂Wjk∂Ok =−(Ok−tk)S(Xjk))(1−S(Xjk))OjT(9)
所以,迭代公式为
n
e
w
W
j
k
=
o
l
d
W
j
k
−
α
∂
E
∂
W
j
k
(
α
为
学
习
率
)
(10)
newW_{jk}=oldW_{jk}-\alpha \frac{\partial E}{\partial W_{jk}}\\(\alpha为学习率)\tag{10}
newWjk=oldWjk−α∂Wjk∂E(α为学习率)(10)
根据对称原理
n
e
w
W
i
j
=
o
l
d
W
i
j
−
α
∂
E
∂
W
i
j
(
α
为
学
习
率
)
(11)
newW_{ij}=oldW_{ij}-\alpha \frac{\partial E}{\partial W_{ij}}\\(\alpha为学习率)\tag{11}
newWij=oldWij−α∂Wij∂E(α为学习率)(11)
[
Δ
W
1
,
1
Δ
W
1
,
2
Δ
W
2
,
1
Δ
W
2
,
2
]
=
α
[
E
1
S
1
(
1
−
S
1
)
E
2
S
2
(
1
−
S
2
)
]
[
O
1
O
2
]
\begin{bmatrix}\Delta W_{1,1}&\Delta W_{1,2}\\\\\Delta W_{2,1}&\Delta W_{2,2} \end{bmatrix}=\alpha \begin{bmatrix}E_1S_1(1-S_1)\\\\E_2S_2(1-S_2)\end{bmatrix}\begin{bmatrix}O_1&O_2\end{bmatrix}
⎣⎡ΔW1,1ΔW2,1ΔW1,2ΔW2,2⎦⎤=α⎣⎡E1S1(1−S1)E2S2(1−S2)⎦⎤[O1O2]
Δ
W
j
k
=
α
E
k
O
k
(
1
−
O
k
)
O
j
T
(12)
\Delta W_{jk}=\alpha E_kO_k(1-O_k)O_j^T\tag{12}
ΔWjk=αEkOk(1−Ok)OjT(12)
三.模型求解
三.1 输入输出问题
从(12)式我们可以知道,权重的改变取决于阈值函数的梯度,而阈值函数的性质使得小梯度(大输入)会限制神经网络的学习能力,容易变成饱和神经网络,也就是说,我们应该保持尽量小的输入,同时,这个表达式也取决于输入信号 O j O_j Oj,因此,使用非常小的值也会出现丧失精度的问题,所以我的方案是重新调整输入值,将其范围控制在[0.0,1.0],为了避免 O j O_j Oj变为0导致权重更新表达式变为0,从而造成学习能力的丧失,则再次输入上加入一个小的偏移量0.1,避免输入0带来的麻烦,同时为了避免产生过大权重,对上限也采取同样的措施,所以最后输入输出的区间为[0.1,0.99]。
三.2 选择初始权重
我选用的是一条经验法则:从均值为0,标准方差等于节点传入连接数量平方根倒数的正态分布中进行采样,简单来说,就是一些过大的初始权重将会在偏置方向上偏置激活函数,非常大的权重将会使激活函数饱和。一个节点的传入链接越多,就有越多的信号被叠加在一起。因此,如果链接更多,那么就减小权重的范围。
四.模型检验
定义一个准确率,如果模型预测为正确答案则+1分,错误则+0分,最后用得分除以总分(10分),来判断神经网络的性能。
五.模型评价
五.1学习率
这个模型的一个缺点就是恒定的学习率,过小或者过大的学习率对于模型都是有害的,因此,找到一个合适的学习率是非常有用的,下图是我使用不同的学习率所得到的的性能图,从中我们可以发现,学习率在0.4表现最好,之后则大幅度下跌,这是过拟合的现象
五.2世代
多次进行试验可以为神经网络提供不同的初始权重进而得到不一样的梯度下降路径,这样有助于我们找到更好的点。不过世代数与准确率的结果出现了随机性,这可能是运行过程中出了问题,导致梯度被卡在了一个局部的最小值中。
参考:
附录A:神经网络类
class neuralNetwork:
def __doc__(self):
"""aaa"""
# 初始化函数设定输入层节点隐藏层节点和输出层节点的数量
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
self.lr = learningrate
# self.wih = (np.random.rand(self.hnodes,self.inodes))
# self.woh = (np.random.rand(self.onodes,self.hnodes))
# 使用正态分布采样权重,期望是0,方差是1/下一层节点数**-.5
self.wih = (np.random.normal(0.0, pow(self.hnodes, -.5), (self.hnodes, self.inodes)))
self.who = (np.random.normal(0.0, pow(self.hnodes, -.5), (self.onodes, self.hnodes)))
# 定义激活函数
self.activation_function = lambda x: ss.expit(x)
pass
# 训练 学习给定训练集样本,优化权重
def train(self, input_list, target_list):
# 构造目标矩阵
targets = np.array(target_list, ndmin=2).T
# 构建输入矩阵
inputs = np.array(input_list, ndmin=2).T
# 计算隐藏层输入
hidden_inputs = self.wih @ inputs
# 计算隐藏层输出
hidden_outputs = self.activation_function(hidden_inputs)
# 计算输出层输入
final_inputs = self.who @ hidden_outputs
# 计算输出层输出
final_outputs = self.activation_function(final_inputs)
# 计算输出层误差
output_error = targets - final_outputs
# 计算隐含层误差
hidden_errors = self.who.T @ output_error
# 更新隐藏层与输出层的权重
self.who += self.lr * np.dot((output_error * final_outputs * (1.0 - final_outputs)),
np.transpose(hidden_outputs))
# 更新隐藏层与输入层的权重
self.wih += self.lr * np.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), np.transpose(inputs))
# 查询 给定输入从输出街店给出答案
def query(self, input_list):
# 构建输入矩阵
inputs = np.array(input_list, ndmin=2).T
# 计算隐藏层输入
hidden_inputs = np.dot(self.wih, inputs)
# 计算隐藏层输出
hidden_outputs = self.activation_function(hidden_inputs)
# 计算输出层输入
final_inputs = np.dot(self.who, hidden_outputs)
# 计算输出层输出
final_outputs = self.activation_function(final_inputs)
# 返回最终输出
return final_outputs
# 反向查询神经网络
def backquery(self, target_list):
# 计算输出层输出信号,转换为列向量
final_outputs = np.array(target_list, ndmin=2).T
# 计算输出层输入信号,使用SIGMOD函数的逆函数
final_inputs = self.inverse_activation_function(final_outputs)
# 计算隐藏层输出信号
hidden_outputs = self.who.T @ final_inputs
# 将信号格式化(.01 ~ .99)
def setlearnning(self, lr):
self.lr = lr
附录B:学习率测试实例
def demo2_performance(lr=None,epochs=1):
# 构造神经网络
inputnodes = 784
hidennodes = 100
outputnodes = 10
learningeate = lr
n = neuralNetwork(inputnodes, hidennodes, outputnodes, learningeate)
# 导入手写图片训练集(60000)
training_data_file = open("mnist_train.csv", 'r')
training_data_list = training_data_file.readlines()
training_data_file.close()
# 训练神经网络
for e in range(epochs):
# 通过“,”分割数据
for record in training_data_list:
all_values = record.split(",")
# 归一化输入(防止0)
inputs = (np.asfarray(all_values[1:]) / 255.0 * .99) + .01
# 构造目标(target)矩阵
targets = np.zeros(outputnodes) + .01
targets[int(all_values[0])] = .99
n.setlearnning(lr)
n.train(inputs, targets)
pass
pass
# 导入测试集
test_data_file = open("mnist_test.csv", 'r')
test_data_list = test_data_file.readlines()
test_data_file.close()
# 测试神经网络
# 设置计分板列表
scorecard = []
# 计算所有数字的得分情况
for record in test_data_list:
all_values = record.split(",")
# 正确答案是第一位
correct_labble = int(all_values[0])
# 归一化输入
inputs = (np.asfarray(all_values[1:]) / 255.0 * .99) + .01
# 输出结果
outputs = n.query(inputs)
# 将输出的最高分作为答案
label = np.argmax(outputs)
# 将答案填入列表
if (label == correct_labble):
# 如果答案正确,加一分
scorecard.append(1)
else:
# 如果答案不正确,加0分
scorecard.append(0)
pass
pass
# 计算总分,算出回归率
scorecard_array = np.asfarray(scorecard)
performance = scorecard_array.sum() / scorecard_array.size
return performance
# print("preformance=", scorecard_array.sum() / scorecard_array.size)
def learnningplot(learnninglist, performance,epochs=1):
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"C:\Windows\Fonts\simhei.ttf", size=14) # 导入中文字体
preformancelist = [performance(i,epochs) for i in learnninglist]
plt.plot(learnninglist, preformancelist, 'go-')
plt.grid(True)
plt.xlabel("学习率", FontProperties=font)
plt.ylabel("准确率", FontProperties=font)
plt.title("学习率与准确率关系", FontProperties=font)
if __name__ == '__main__':
learnninglist = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
learnningplot(learnninglist, demo2_performance,1)
附录C:世代数测试实例
def demo2_performance(lr=None,epochs=1):
# 构造神经网络
inputnodes = 784
hidennodes = 100
outputnodes = 10
learningeate = lr
n = neuralNetwork(inputnodes, hidennodes, outputnodes, learningeate)
# 导入手写图片训练集(60000)
training_data_file = open("mnist_train.csv", 'r')
training_data_list = training_data_file.readlines()
training_data_file.close()
# 训练神经网络
for e in range(epochs):
# 通过“,”分割数据
for record in training_data_list:
all_values = record.split(",")
# 归一化输入(防止0)
inputs = (np.asfarray(all_values[1:]) / 255.0 * .99) + .01
# 构造目标(target)矩阵
targets = np.zeros(outputnodes) + .01
targets[int(all_values[0])] = .99
n.setlearnning(lr)
n.train(inputs, targets)
pass
pass
# 导入测试集
test_data_file = open("mnist_test.csv", 'r')
test_data_list = test_data_file.readlines()
test_data_file.close()
# 测试神经网络
# 设置计分板列表
scorecard = []
# 计算所有数字的得分情况
for record in test_data_list:
all_values = record.split(",")
# 正确答案是第一位
correct_labble = int(all_values[0])
# 归一化输入
inputs = (np.asfarray(all_values[1:]) / 255.0 * .99) + .01
# 输出结果
outputs = n.query(inputs)
# 将输出的最高分作为答案
label = np.argmax(outputs)
# 将答案填入列表
if (label == correct_labble):
# 如果答案正确,加一分
scorecard.append(1)
else:
# 如果答案不正确,加0分
scorecard.append(0)
pass
pass
# 计算总分,算出回归率
scorecard_array = np.asfarray(scorecard)
performance = scorecard_array.sum() / scorecard_array.size
return performance
# print("preformance=", scorecard_array.sum() / scorecard_array.size)
def epochsplot(epochslist,performance, lr):
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"C:\Windows\Fonts\simhei.ttf", size=14) # 导入中文字体
preformancelist = [performance(lr, i) for i in epochslist]
plt.plot(epochslist, preformancelist, 'bo-')
plt.grid(True)
plt.xlabel("世代数", FontProperties=font)
plt.ylabel("准确率", FontProperties=font)
plt.title("世代数与准确率关系", FontProperties=font)
if __name__ == '__main__':
lr=0.4
epochslist = range(1,10)
epochsplot(epochslist,demo2_performance,lr)