机器学习之神经网络实现MNIST手写字识别

一、神经网络原理

线性回归(Linear Regression)和逻辑回归(Logistic Regression)通常用来处理线性模型,如果利用线性回归或逻辑回归对多特征的非线性问题进行分类,则涉及太多特征组合的计算,往往导致计算负荷增大,并不适合解决这类问题。

假设我们需要训练一个模型用来判断一张图片中是否出现汽车,可能有很多用来训练模型的数据,这些图片有的包含小汽车,有的没有,利用这些图片的一个个像素值作为特征,训练一个满足这样功能的模型。训练过程需要处理可能百万级别甚至更多的数据,对于这样问题通常采用神经网络(Neural Networks)解决。

1.1 模型

这个一个简单的3层神经网络,第一层为输入层(Input Layers),最后一层为输出层(Output Layers),中间层称为隐藏层(Hidden Layers)

1.2 前向传播

1.2.1 激活函数

(1)Sigmoid函数

一个常见的激活函数,其数学表达式为

Python实现代码为:

1
2
3
import numpy as np
def sigmoid(x):
return 1/(1+np.exp(-x))

(2)ReLU函数

计算速度更快,是目前的主流激活函数。数学表达式为:

Python实现代码为:

1
2
3
import numpy as np
def relu(x):
return np.maximum(0,x)

1.2.2 前向传播过程

除输入层外,每一层神经元都有前一层的神经元作为本层神经元的输入,本层神经元的输出又可以作为下一层神经元的输入,依次向前传播最终得到一个输出值,这个由输入值经输入层经过一系列处理最终到达输出层得到输出值的过程称为前向传播。

其中x1, x2, x3是输入单元,即原始的输入数据,a1, a2, a3是中间单元,负责将输入的数据处理然后传递到下一层,最后是输出单元,负责计算。计算的过程中为每一层都添加了一个偏置(bias unit)。

上图中分别代表输入层到隐藏层的权重和隐藏层到输出层的权重,对于上图的网络模型激活单元和输出分别表达为:

如此,从左到右的算法称为前向传播算法,实际应用中为了计算方便通常是以矩阵方式计算的。

Python实现示例(示例不能直接运行):

1
2
3
4
5
6
7
8
9
10
def predict(self,X):
# 前向传播算法forward_propagation,实现预测
self.a1 = X.T
self.z2 = np.dot(self.W1,self.a1)+self.b1
self.a2 = relu(self.z2)
self.z3 = np.dot(self.W2,self.a2)+self.b2
self.a3 = relu(self.z3)
out = self.a3
p = np.argmax(out, axis=0) # 输出层的最大索引下标即为标签值,标签值0-9
return p

1.3 代价函数

通过代价函数观察预测的结果与真实情况的误差有多大。

1.4 反向传播算法

一般的训练算法可以分为两个阶段:

(1)求解代价函数关于权值(参数)的导数。(BP)

(2)用得到的导数进一步计算权值的调整量。(梯度下降等优化算法)

反向传播(BP)算法主要应用第一阶段,非常高效的计算这些导数。

1.4.1 推导过程

假设有一个四层的神经网络,其相关参数为: Sl=4, L=4(其中L表示网络层数,Sl表示l层有多少个神经元),用表示误差,结合前面介绍的前向传播过程,则:

前一层误差为:

其中是Sig函数的导数。

接着第二层的误差为:

第一层是输入变量,不存在误差,此时不考虑正则项,则有:

要求的导数 = 权值输出端单元的误差项 * 权值输入端单元的激活值。

Python实现示例(示例不能直接运行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#反向传播:
#1、m表示样本个数
#2、梯度下降,更新W和b
def backward(self, dAL):
m=60000
dZ3=np.multiply(dAL,relu_derivative(self.z3))
dW2=np.dot(dZ3, self.a2.T)/m
db2=np.mean(dZ3,axis=1)
dAL_1 = np.dot(self.W2.T, dZ3)
dZ2 = np.multiply(dAL_1, relu_derivative(self.z2))
dW1 = np.dot(dZ2, self.a1.T) / m
db1 = np.mean(dZ2, axis=1)
# 更新权值
self.W2-=self.lr*dW2
self.b2-=self.lr*db2
self.W1 -= self.lr * dW1
self.b1 -= self.lr * db1

二、数据集解析

数据集来源:http://yann.lecun.com/exdb/mnist,选用MNIST手写字数据集训练神经网络,数据集使用Python模块Struct解析二进制文件。手写字特征为28*28=784个像素点,输出为手写字值。解析过程不做详细介绍,其数据解析的一种Python实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import numpy as np
import matplotlib.pyplot as plt
import struct

file1="./MNIST_data/train-images.idx3-ubyte"
file2="./MNIST_data/train-labels.idx1-ubyte"

def train_images_ana(filepath):
"""解析图片数据集 .idx3-ubyte格式"""
# 以二进制方式读取文件
with open(filepath,'rb') as fbj:
bin_data=fbj.read()
offset=0
magic_num,image_num,rows_num,column_num=struct.unpack_from('>iiii',bin_data,offset)
offset+=struct.calcsize('>iiii')
imgsize=image_num*rows_num*column_num
fmt_image='>'+str(imgsize)+'B' # 训练集数据有60000*28*28
images=struct.unpack_from(fmt_image,bin_data,offset)
img=np.reshape(images,(image_num,rows_num*column_num)) # 构造一个60000*784的矩阵
return img

def train_labels_ana(filepath):
"""解析特征数据集 .idx1-ubyte格式"""
# 以二进制格式处理文件
with open(filepath,'rb') as fbj:
bin_data=fbj.read()
offset=0
magic_num,items_num=struct.unpack_from('>ii',bin_data,offset)
offset+=struct.calcsize('>ii')
fmt_label='>'+str(items_num)+'B'
labels=struct.unpack_from(fmt_label,bin_data,offset)
label=np.reshape(labels,[items_num])
return label

if __name__=='__main__':
imgs=train_images_ana(file1)
print(np.shape(imgs[1]))
labels = train_labels_ana(file2)
print(labels)
print(np.shape(labels[1]))
# 查看前10个手写字灰度图
for i in range(10):
img=np.reshape(imgs[i],[28,28])
plt.imshow(img,cmap='gray')
print(labels[i])
plt.show()

三、神经网络搭建

搭建一个三层神经网络,输入层、隐藏层、输出层节点分别为:784,100,10。

四、附录

network.py文件,构建神经网络类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import numpy as np

from activation_func import *
from loss import *

class Network:
def __init__(self,inputnodes,hidnodes,outputnodes,learning_rate):
self.innodes = inputnodes
self.hidnodes = hidnodes
self.outnodes = outputnodes
self.lr = learning_rate
#各层权重 偏置
self.W1 = np.random.randn(self.hidnodes, self.innodes) * 0.01
self.W2 = np.random.randn(self.outnodes, self.hidnodes) * 0.01
self.b1 = np.random.randn(self.hidnodes, 1) * 0.01
self.b2 = np.random.randn(self.outnodes, 1) * 0.01

def predict(self,X):
# 前向传播算法forward_propagation,实现预测
self.a1 = X.T
self.z2 = np.dot(self.W1,self.a1)+self.b1
self.a2 = relu(self.z2)
self.z3 = np.dot(self.W2,self.a2)+self.b2
self.a3 = relu(self.z3)
out = self.a3
p = np.argmax(out, axis=0) # 输出层的最大索引下标即为标签值,标签值0-9
return p

#反向传播:
#1、m表示样本个数
#2、梯度下降,更新W和b
def backward(self, dAL):
m=60000
dZ3=np.multiply(dAL,relu_derivative(self.z3))
dW2=np.dot(dZ3, self.a2.T)/m
db2=np.mean(dZ3,axis=1)
dAL_1 = np.dot(self.W2.T, dZ3)
dZ2 = np.multiply(dAL_1, relu_derivative(self.z2))
dW1 = np.dot(dZ2, self.a1.T) / m
db1 = np.mean(dZ2, axis=1)
# 梯度下降
self.W2-=self.lr*dW2
self.b2-=self.lr*db2
self.W1 -= self.lr * dW1
self.b1 -= self.lr * db1

loss.py文件,损失函数计算代价。

1
2
3
4
5
6
7
8
9
10
import numpy as np

#交叉熵损失函数
def cross_entropy(y, y_predict):
y_predict = np.clip(y_predict,1e-10,1-1e-10) #防止0*log(0)出现。导致计算结果变为NaN
return -(y * np.log(y_predict) + (1 - y) * np.log(1 - y_predict))

#交叉熵损失函数的导函数
def cross_entropy_der(y,y_predict):
return -y/y_predict+(1-y)/(1-y_predict)

activation.py文件,激活函数及其导数实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np

"""
激活函数
选择非线性的激活函数处理非线性假设
常用激活函数relu、sigmoid
"""

def sigmoid(x):
return 1/(1+np.exp(-x))

def relu(x):
return np.maximum(0,x)

def sig_derivative(x):
#sig函数求导
fx=sigmoid(x)
return fx*(1-fx)

def relu_derivative(x):
return (x>=0).astype(np.float64)

load_data.py文件,加载数据集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import numpy as np
import matplotlib.pyplot as plt
import struct

file1="./MNIST_data/train-images.idx3-ubyte"
file2="./MNIST_data/train-labels.idx1-ubyte"

def train_images_ana(filepath):
"""解析图片数据集 .idx3-ubyte格式"""
# 以二进制方式读取文件
with open(filepath,'rb') as fbj:
bin_data=fbj.read()

offset=0
magic_num,image_num,rows_num,column_num=struct.unpack_from('>iiii',bin_data,offset)
offset+=struct.calcsize('>iiii')
imgsize=image_num*rows_num*column_num
fmt_image='>'+str(imgsize)+'B' # 训练集数据有60000*28*28
images=struct.unpack_from(fmt_image,bin_data,offset)
img=np.reshape(images,(image_num,rows_num*column_num)) # 构造一个60000*784的矩阵

return img

#print(magic_num,image_num,rows_num,column_num)

def train_labels_ana(filepath):
"""解析特征数据集 .idx1-ubyte格式"""
# 以二进制格式处理文件
with open(filepath,'rb') as fbj:
bin_data=fbj.read()

offset=0
magic_num,items_num=struct.unpack_from('>ii',bin_data,offset)
offset+=struct.calcsize('>ii')
fmt_label='>'+str(items_num)+'B'
labels=struct.unpack_from(fmt_label,bin_data,offset)
label=np.reshape(labels,[items_num])
return label

train.py文件,训练模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from network import *
from load_data import *

input_nodes = 784
hidden_nodes = 100
output_nodes = 10
learning_rate = 0.1
n = Network(input_nodes, hidden_nodes, output_nodes, learning_rate)

X=train_images_ana(file1)
Y=train_labels_ana(file2)

# 训练神经网络
for epoch in range(10):
cnt = 0
for i in range(60000):
Y_predict = n.predict(np.mat(X[i]))
if (Y[i]==Y_predict):
cnt+=1
dA = cross_entropy_der(np.mat(Y[i]),Y_predict)
n.backward(dA)

print('epoch %d:accurac=%f'%(epoch,cnt/60000))
Author: wnxy
Link: https://wnxy.xyz/2020/10/25/Neural-networks-realizes-MNIST-handwriting-recognition/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.