-
说在前面
虽然说在python中通过调tensorflow、pytorch等工具库包构建神经网络模型真的很方便,但是通过编写自己动手编写代码来实现能够更好地理解整个神经网络模型,今天就以最简单的bp神经网络为例,来给大家展示一下如何自己编写一个bp神经网络模型,俺第一次写博客,有错误欢迎指出哈哈
-
神经网络简要概括
神经网络模型是机器学习模型之一,通过类比人脑智能设计出的一个机器学习算法
与人脑一样,它也是由许多的神经元构成;我们将整个神经网络分为三个层次:
输入层(input layer):输入层为1层,输入层的神经元个数与输入维度对应;输入层的神经元,其输入等于输出,将输入到神经网络模型的数据通过输入层原封不动地传递给隐藏层
隐藏层(hidden layer):隐藏层可以有多层,隐藏层的层数多了就是一个传统的深度神经网络,是深度学习的一种,不过隐藏层层数太多的模型比较难以训练;在隐藏层中的每一个神经元都具有一个参数:阈值,阈值这个东西实际就是类比人脑神经元而提出的,人脑神经元只有接受了一定强度的刺激才能有明显的神经脉冲,在神经网络中的阈值描述的就是这样的意思,我神经元的输入只有到达我阈值的门槛,这样的输入才能让我神经元被激活;而如何评判激活表现,这是由神经元中的另一个属性——激活函数决定的,常见的激活函数有:Relu函数、Sigmoid函数、tanh函数,他们都具有的特点:处处可导、求导轻松;本次编写代码时使用的是Sigmoid函数作为激活函数
输出层(output layer):输出层为1层,输出层的神经元个数与输出的维度对应;与隐藏层神经元一样具有阈值与激活函数的属性(如果是解决回归问题,输出层就无须激活)
权值:这是神经网络需要优化的除了阈值外的另一个参数,不同层之间的神经元通过赋有权值的线相连,通过线将神经元自己的输出乘上对应权值传递到连线下一头的神经元的输入;一个神经元会接收到所有相连线传递来的值,通过简单的求和作为自己的输入;一个神经元的输入来自于上一层所有的神经元的输出;具体的神经网络的框架可以表示为如下的样子
一个简单的神经网络模型,可以长以下这样:
如上即是一个神经网络的模型;可以看见输入层的神经元的输入与输出是一致的;隐藏层与输出层的神经元都具有阈值(西格玛求和符号代表的是对所有连线传递来的值进行求和后成为神经元的输入)
如上一个一般性的神经网络,为了进行代数表示以及接下来的误差传递公式推导;我在图中也为一些参数进行标注了名称:
如上需要特别注意我用了不同的下标i、h、j来分别指代不同层的某一个神经元;这样在参数过多的神经网络模型中看得更清楚
-
激活函数
我们使用的激活函数是sigmoid函数,我们之前也讲了其是在神经元激活后产生输出时使用到的;激活的方式是:
神经元输出 = sigmoid(神经元输入-神经元阈值)
除此之外,我还想提的是之前说的激活函数处处可导且求导容易的问题;我们可以求一求sigmoid函数的导数,用fx表示sigmoid函数,我这里就不列出sigmoid函数的函数关系式;学过求导的都能算出其导数与原函数具有以下关系式:
f‘x = fx *(1 - fx)
我们可以发现,sigmoid函数求导后尽然依旧还是其本身的运算组合,这对于利用梯度下降法求得修正误差时十分有用
最后,sigmoid函数为神经网路赋予了非线性因素,在数学上已经证明,一个单隐藏层的bp神经网络模型能够逼近任意映射关系 -
参数修正公式——梯度下降法
bp神经网络的初始参数是随机初始的,接着正向传播(正向计算一遍网络到输出),之后反向传播(从输出开始,根据误差,反向更新每层的参数,从输出层开始向输入层方向修改参数,优化误差),循环上述过程直到参数能让模型收敛
这里我们需要推导出误差反向传播的参数修正公式,我们需要用到的数学知识储备:梯度下降法、求导链式法则,以下是证明过程:
如上即是一个单隐藏层bp神经网络的参数修正公式的完整推导;把它推广到多隐藏层也是十分容易,我们很容易就能看出这些修改参数的公式是有规律可循的,但是多隐藏层不建议使用梯度下降,因为难以收敛的,可以尝试使用随机梯度下降,这里就不多介绍了 -
代码实现
使用python编写,调用numpy库包,可以直接使用上述推导公式进行代码编写
激活函数:
def sigmoid(z): return 1 / (1 + np.exp(-z))
参数随机初始化:
# x为输入层神经元个数,y为隐层神经元个数,z输出层神经元个数 def init_parameter(x, y, z): # 隐层阈值 gamma = np.random.randint(-5, 5, (1, y)).astype(np.float64) # 输出层阈值 theta = np.random.randint(-5, 5, (1, z)).astype(np.float64) # 输入层与隐层的连接权重 v = np.random.randint(-5, 5, (x, y)).astype(np.float64) # x行,每行y个的矩阵 # 隐层与输出层的连接权重 w = np.random.randint(-5, 5, (y, z)).astype(np.float64) return v, w, gamma, theta
数据输入:
def getTrainSet(): dataSet = pd.read_csv('train_set.csv') dataSetNP = np.array(dataSet) # 将数据由dataframe类型转换为数组类型 # [,]: 逗号前代表多少行的数据,逗号后代表提取指定连续列的数据 # 训练集 x_train = dataSetNP[:, 0:dataSetNP.shape[1] - 1] y_train = dataSetNP[:, dataSetNP.shape[1] - 1] return x_train, y_train def getTestSet(): dataSet = pd.read_csv('test_set.csv') dataSetNP = np.array(dataSet) # 将数据由dataframe类型转换为数组类型 # [,]: 逗号前代表多少行的数据,逗号后代表提取指定连续列的数据 # 训练集 x_test = dataSetNP[:, 0:dataSetNP.shape[1] - 1] y_test = dataSetNP[:, dataSetNP.shape[1] - 1] return x_test, y_test
bp神经网络模型训练:
def trainning(x_train, y_train, v, w, gamma, theta): # x为学习率 x = 0.03 for i in range(len(x_train)): #遍历每一行数据,当此时为行索引为i的数据时 # 输入数据 # np.mat(rix) 生成矩阵 矩阵的每行是dataset中的一行 x = np.mat(x_train[i]).astype(np.float64) # 数据标签(真实值 Y) Y = np.mat(y_train[i]).astype(np.float64) # 开始正向传播,计算预测值 y # 隐层输入 alpha = np.dot(x, v).astype(np.float64) # 隐层输出 # value1为隐藏层阙值,阙值即是一个门槛,当输入值达到了该值才会有明显的激活现象 b = sigmoid(alpha - gamma).astype(np.float64) # 输出层输入 input2 = np.dot(b, w).astype(np.float64) # 输出层输出 y = sigmoid(input2 - theta).astype(np.float64) # 更新公式由矩阵运算表示 g = np.multiply(np.multiply(y, 1 - y), Y - y) e = np.multiply(np.dot(g, np.transpose(w)), np.multiply(b, 1 - b)) delta_gamma = -x * e delta_theta = -x * g delta_v = x * np.dot(np.transpose(x), e) delta_w = x * np.dot(np.transpose(b), g) # 更新参数 gamma += delta_gamma theta += delta_theta v += delta_v w += delta_w return v, w, gamma, theta
测试
def testing(x_test, y_test, v, w, gamma, theta): # 记录预测正确的个数 rightcount = 0 for i in range(len(x_test)): # 计算每一个样例通过该神经网路后的预测值 x = np.mat(x_test[i]).astype(np.float64) outputset = np.mat(y_test[i]).astype(np.float64) b = sigmoid(np.dot(x, v) - gamma) y = sigmoid(np.dot(b, w) - theta) # 确定其预测标签 if y > 0.5: flag = 1 else: flag = 0 if y_test[i] == flag: rightcount += 1 # 输出预测结果 print(flag, y_test[i]) # 返回正确率 return rightcount / len(x_test)
主函数
if __name__ == '__main__': x_train, y_train = getTrainSet() x_test, y_test = getTrainSet() #初始化一个输入层神经元数:len(x_train[0]) 隐层神经元数:6 输出层神经元数:2 #随机初始权值与阈值 v, w, gamma, theta = init_parameter(len(x_train[0]), 6, 2) for i in range(500): v, w, gamma, theta = trainning(x_train, y_train, v, w, gamma, theta) rate = testing(x_test, y_test, v, w, gamma, theta) print("正确率为%f" % (rate))
以上只是核心代码,需要根据具体数据集的不同,变化输入层与输出层神经元数,适当变化隐藏层的神经元个数,网上有一个衡量隐藏层神经元个数的公式可以参考;训练的数据集需要经过一些处理,例如归一化(离差归一、均值归一等),自行打标签(监督学习);选取数据集的80%作为训练集;剩余20%作为测试集;二者间的比例可适当自行调节