在上篇文章中,我们阐述了手工神经网络的数学原理,详见https://jl-sky.github.io/2023/02/18/%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%8E%9F%E7%90%86%E6%A6%82%E8%BF%B0/
在本篇文章中,我们将使用python手工搭建一个神经网络,并使用mnist手写数字数据集进行测试
网络模型搭建
创建网络类bpNetWork,类内定义构造函数;query()函数,用于前向传播信号;train()函数用于训练网络,对权重矩阵进行更新。
网络初始化
参数:输入层节点个数,隐藏层节点个数,输出层节点个数,学习率
初始化连接矩阵
输入层到隐藏层连接矩阵
self.wih=np.random.normal(0.0,pow(self.hidnodes,-0.5),(self.hidnodes,self.innodes))#矩阵大小为hidnodes×innodes
隐藏层到输出层连接矩阵
self.who=np.random.normal(0.0,pow(self.outnodes,-0.5),(self.outnodes,self.hidnodes))#矩阵大小为outnodes×hidnodes
定义激活函数
self.activeFuction=lambda x:scipy.special.expit(x)
完整代码:
def __init__(self,inputNodes,hiddenNodes,outputNodes,learningRate):
self.innodes=inputNodes#输入层节点个数
self.hidnodes=hiddenNodes#隐藏层节点个数
self.outnodes=outputNodes#输出层节点个数
# 初始化训练矩阵,随机正态分布矩阵
self.wih=np.random.normal(0.0,pow(self.hidnodes,-0.5),(self.hidnodes,self.innodes))#矩阵大小为hidnodes×innodes
self.who=np.random.normal(0.0,pow(self.outnodes,-0.5),(self.outnodes,self.hidnodes))#矩阵大小为outnodes×hidnodes
# 初始化学习率
self.lr=learningRate
# 定义sigmod激活函数
self.activeFuction=lambda x:scipy.special.expit(x)
# return
前向传播信号
信号从输入层经过连接矩阵后到达隐藏层
hiddenInput=np.dot(self.wih,inputs)
信号经隐藏层激活后输出
hiddenOut=self.activeFuction(hiddenInput)
信号从隐藏层经连接矩阵到达输出层
outInput = np.dot(self.who, hiddenOut)
信号经过输出层激活后输出
finalOut = self.activeFuction(outInput)
完整代码:
def query(self,inputsList):
# 转置
inputs=np.array(inputsList,ndmin=2).T#ndmin表示矩阵最低维数是2维
# 隐藏层输入值
hiddenInput=np.dot(self.wih,inputs)
# 激活后的隐藏输出
hiddenOut=self.activeFuction(hiddenInput)
# 输出层输入值
outInput = np.dot(self.who, hiddenOut)
# 激活后的输出层输出
finalOut = self.activeFuction(outInput)
return finalOut
网络训练
计算误差及误差的反向传播
outErrors=targets-finalOut
hiddenErrors=np.dot(self.who.T,outErrors)
权重矩阵更新
由上篇文章我们知道,权重更新表达式为:
注:*乘法是正常的对应元素的乘法,·点乘是矩阵点积,且来自上一层的输出矩阵被转置了。实际上,这意味着输出矩阵的列变成了行。
故权重更新表达式为:
self.who+=self.lr*np.dot((outErrors*finalOut*(1-finalOut)),np.transpose(hiddenOut))#更新隐藏层到输出层的权重矩阵
self.wih+=self.lr*np.dot((hiddenErrors*hiddenOut*(1-hiddenOut)),np.transpose(inputs))#更新输入层到隐藏层的权重矩阵
完整代码:
def train(self,inputsList,targetList):
inputs=np.array(inputsList,ndmin=2).T# ndmin表示矩阵最低维数是2维
targets=np.array(targetList,ndmin=2).T# 期望值
# 正向传播
hiddenInput=np.dot(self.wih,inputs)#隐藏层输入值
hiddenOut=self.activeFuction(hiddenInput)#隐藏层输出值
finalInput=np.dot(self.who,hiddenOut)#最终层输入值
finalOut=self.activeFuction(finalInput)#最终层输出值
# 计算损失(误差)
outErrors=targets-finalOut
hiddenErrors=np.dot(self.who.T,outErrors)
# 权重更新
self.who+=self.lr*np.dot((outErrors*finalOut*(1-finalOut)),np.transpose(hiddenOut))
self.wih+=self.lr*np.dot((hiddenErrors*hiddenOut*(1-hiddenOut)),np.transpose(inputs))
return
手工网络完整代码
# -*- codeing=utf-8 -*-
# @Author:姜磊
# 人间烟火气,最抚凡人心
import numpy as np
import scipy.special#激活函数
# https://blog.csdn.net/weixin_41822392/article/details/89639783
# 三层网络模型
# 参数:输入层节点数,隐藏层节点数,输出层节点数,学习率
class bpNetWork:
def __init__(self,inputNodes,hiddenNodes,outputNodes,learningRate):
self.innodes=inputNodes#输入层节点个数
self.hidnodes=hiddenNodes#隐藏层节点个数
self.outnodes=outputNodes#输出层节点个数
# 初始化训练矩阵,随机正态分布矩阵
self.wih=np.random.normal(0.0,pow(self.hidnodes,-0.5),(self.hidnodes,self.innodes))
self.who=np.random.normal(0.0,pow(self.outnodes,-0.5),(self.outnodes,self.hidnodes))
# 初始化学习率
self.lr=learningRate
# 定义sigmod激活函数
self.activeFuction=lambda x:scipy.special.expit(x)
# return
# 计算网络输出与标准值之间的误差,以训练网络权重
def train(self,inputsList,targetList):
inputs=np.array(inputsList,ndmin=2).T# ndmin表示矩阵最低维数是2维
targets=np.array(targetList,ndmin=2).T
# 正向传播
hiddenInput=np.dot(self.wih,inputs)#隐藏层输入值
hiddenOut=self.activeFuction(hiddenInput)#隐藏层输出值
finalInput=np.dot(self.who,hiddenOut)#最终层输入值
finalOut=self.activeFuction(finalInput)#最终层输出值
# 计算损失(误差)
outErrors=targets-finalOut
hiddenErrors=np.dot(self.who.T,outErrors)
# 权重更新
self.who+=self.lr*np.dot((outErrors*finalOut*(1-finalOut)),np.transpose(hiddenOut))
self.wih+=self.lr*np.dot((hiddenErrors*hiddenOut*(1-hiddenOut)),np.transpose(inputs))
return
# 给定初始信号值,计算初始输出值
def query(self,inputsList):
# 转置
inputs=np.array(inputsList,ndmin=2).T
# 隐藏层输入值
hiddenInput=np.dot(self.wih,inputs)
# 激活后的隐藏输出
hiddenOut=self.activeFuction(hiddenInput)
# 输出层输入值
outInput = np.dot(self.who, hiddenOut)
# 激活后的输出层输出
finalOut = self.activeFuction(outInput)
return finalOut
MNIST数据集制作
MNIST数据集介绍
该数据集是一个手写数字数据集,可由https://pjreddie.com/projects/mnist-in-csv/获取。
这个网站提供了两个CSV文件:
- 训练集http://www.pjreddie.com/media/files/mnist_train.csv·
- 测试集http://www.pjreddie.com/media/files/mnist_test.csv
其csv文件中,每一行记录代表一个手写数字信息,这些记录或这些行的内容为:
- 第一个值是标签,即书写者实际希望表示的数字,如“7”或“9”。这是我们希望神经网络学习得到的正确答案。
- 随后的值,由逗号分隔,是手写体数字的像素值。像素数组的尺寸是28乘以28,因此在标签后有784个值。
导入数据集:
# 导入训练集
with open('../data/mnist_train.csv','r') as f:
trainDataFiles=f.readlines()
f.close()
# 导入测试集
with open('../data/mnist_test.csv','r') as f:
testDataFiles=f.readlines()#读取所有行,将文件的每行数据作为一个列表,testData为2维数据
f.close()
构建网络
输入数据是每行记录的像素值,也就是某个手写数字的像素值,每行共784个像素值,因此输入层定义784个节点
隐藏层我们定义200个节点
要求神经网络对图像进行分类,分配正确的标签。这些标签是0到9共10个数字中的一个。这意味着神经网络应该有10个输出层节点,每个节点对应一个可能的答案或标签。如果答案是“0”,输出层第一个节点激发,而其余的输出节点则保持抑制状态。如果答案是“9”,输出层的最后节点会激发,而其余的输出节点则保持抑制状态。下图详细阐释了这个方案,并显示了一些示例输出。
inputNodes=784#输入节点
hiddenNodes=200#隐藏节点
outNodes=10#输出节点
learningRate=0.1#学习率
bp=bpNetWork(inputNodes,hiddenNodes,outNodes,learningRate)
网络训练
数据处理
输入数据处理
我们需要做的第一件事情是将输入颜色值从较大的0到255的范围,缩放至较小的0.01 到1.0的范围。
我们刻意选择0.01作为范围最低点,是为了避免先前观察到的0值输入最终会人为地造成权重更新失败。
我们没有选择0.99作为输入的上限值,是因为不需要避免输入1.0会造成这个问题。我们只需要避免输出值为1.0。
将在0到255范围内的原始输入值除以255,就可以得到0到1范围的输入值。然后,需要将所得到的输入乘以0.99,把它们的范围变成0.0到0.99。接下来,加上0.01,将这些值整体偏移到所需的范围0.01到1.00。
trainData=(np.asfarray(data[1:])/255*0.99)+0.01
期望数据处理
如果训练样本的标签为“5”,那么需要创建输出节点的目标数组,其中除了对应于标签“5”的节点,其他所有节点的值应该都很小,这个数组看起来可能如[0,0,0,0,0,1,0,0,0,0]。
但是试图让神经网络生成0和1的输出,对于激活函数而言是不可能的,这会导致大的权重和饱和网络,因此需要重新调整这些数字。我们将使用值0.01和0.99来代替0和1,这样标签为“5”的目标输出数组为[0.01, 0.01,0.01, 0.01, 0.01, 0.99, 0.01, 0.01, 0.01, 0.01]。
因此目标矩阵为:
targets=np.zeros(outNodes)+0.01
targets[int(data[0])]=0.99
模型训练
bp.train(trainData,targets)
i = 1
for record in trainList:
data=record.split(',')
# 处理输入数据,asfarray将字符串数据转换为浮点型,并将像素值归一化到0.01~1范围内
# 目标值不能存在0.0和1.0,隐式sigmod函数的极限达不到
trainData=(np.asfarray(data[1:])/255*0.99)+0.01
# 处理输出数据
targets=np.zeros(outNodes)+0.01
targets[int(data[0])]=0.99
# 网络训练
bp.train(trainData,targets)
print("正在训练第"+str(i)+"条数据!")
i+=1
网络测试
# 网络测试
# 记录识别准确率
scoreCard=[]
# 样本真实值
real=[]
# 测试值
testval=[]
for record in testList:
data=record.split(',')
testData=(np.asfarray(data[1:])/255*0.99)+0.01
# 手写数字真实值
value=int(data[0])
real.append(value)
# 训练测试值
testAnswer=bp.query(testData)
answer=np.argmax(testAnswer)
testval.append(answer)
if(value==answer):
scoreCard.append(1)
else:
scoreCard.append(0)
scoreCard=np.asfarray(scoreCard)
accuracy=np.sum(scoreCard)/np.size(scoreCard)#计算准确率
MNIST手写数字识别完整代码
# -*- codeing=utf-8 -*-
# @Author:姜磊
# 人间烟火气,最抚凡人心
import numpy as np
# 手写数字数据集
# https://pjreddie.com/projects/mnist-in-csv/
# 该数据集的共有60000行训练集,每行数据的第一个数字表示标准值,其余的784个数字表示该手写数字图的像素值
from BpNetWork import bpNetWork
# 构建网络
inputNodes=784#输入节点
hiddenNodes=200#隐藏节点
outNodes=10#输出节点
learningRate=0.1#学习率
bp=bpNetWork(inputNodes,hiddenNodes,outNodes,learningRate)
# 导入训练集
with open('../data/mnist_train.csv','r') as f:
trainDataFiles=f.readlines()
f.close()
# 导入测试集
with open('../data/mnist_test.csv','r') as f:
testDataFiles=f.readlines()#读取所有行,将文件的每行数据作为一个列表,testData为2维数据
f.close()
# 数据处理
# trainList=trainDataFiles[:100]
trainList=trainDataFiles
# testList=testDataFiles[:10]
testList=testDataFiles
i = 1
for record in trainList:
data=record.split(',')
# 处理输入数据,asfarray将字符串数据转换为浮点型,并将像素值归一化到0.01~1范围内
# 目标值不能存在0.0和1.0,隐式sigmod函数的极限达不到
trainData=(np.asfarray(data[1:])/255*0.99)+0.01
# 处理输出数据
targets=np.zeros(outNodes)+0.01
targets[int(data[0])]=0.99
# 网络训练
bp.train(trainData,targets)
print("正在训练第"+str(i)+"条数据!")
i+=1
# 网络测试
# 记录识别准确率
scoreCard=[]
# 样本真实值
real=[]
# 测试值
testval=[]
for record in testList:
data=record.split(',')
testData=(np.asfarray(data[1:])/255*0.99)+0.01
# 手写数字真实值
value=int(data[0])
real.append(value)
# 训练测试值
testAnswer=bp.query(testData)
answer=np.argmax(testAnswer)
testval.append(answer)
if(value==answer):
scoreCard.append(1)
else:
scoreCard.append(0)
scoreCard=np.asfarray(scoreCard)
accuracy=np.sum(scoreCard)/np.size(scoreCard)#计算准确率
print(scoreCard)
print(real)
print(testval)
print(accuracy)