内容简介:从零开始写NN (neural network) 系列第一篇,本篇博文将会从代码结构上介绍一下怎么写一个简单的上一篇结尾的地方给了一个实现Back Propagation算法的代码,既然反向传播都写成了博客,那干脆把整个神经网络算法也给介绍介绍好了。接下来我将分析一下上篇结尾给出的了解了BP后,要实现一个简单的神经网络就不难了,这里的代码相比简单的算法实现做了一点点延伸:$tag$
从零开始写NN (neural network) 系列第一篇,本篇博文将会从代码结构上介绍一下怎么写一个简单的 神经网络算法
,下篇打算使用一个示例介绍一下如何调整参数细节。当然,这里的所谓从0开始,其实还是使用了 numpy
,有点像使用 matlab
的感觉。
明确目标
上一篇结尾的地方给了一个实现Back Propagation算法的代码,既然反向传播都写成了博客,那干脆把整个神经网络算法也给介绍介绍好了。接下来我将分析一下上篇结尾给出的 算法代码 。
了解了BP后,要实现一个简单的神经网络就不难了,这里的代码相比简单的算法实现做了一点点延伸:$tag$
- 网络的深度、每一层的神经元个数都是可变的参数
- 激活函数提供多个选择
- 可以定义一个
batch
的大小,每计算一次batch
更新一次权重 - 可以定义
epoch
的次数,每个epoch
内的数据在进行训练前需要被打散 - 使用矩阵运算来并行化以提高效率
算法设计
算法基本思路:给定一个 batch
,里面包括一组sample,对于每个sample x
都会计算一次正传的值,保存每个神经元的值为 反向传播
计算所用;再进行反向计算得到 w
和 b
的梯度,之后使用梯度对模型参数也即 w
和 b
进行更新,每次迭代都会使用一组新的 batch
。当所有的sample都进行计算后,再将所有的sample顺序打乱,循环上面的过程。
其中,每一个 batch
都会使用矩阵运算,这样可以使用并行算法,这也是 前馈神经网络
要比 循环/递归神经网络
训练快的一个主要原因;公式表达如下:
下面是算法图解,从上往下看,每一个神经网络表示对每一个sample的计算,一共有 batch_size
个,图中
反向传播
过程中神经元上的值(误差累计),上篇博文中式(5);
表示
正向计算
神经元上的加权和(仿射值);
表示
正向计算
神经元上的激活值;
W_s[i]
表示两层之间的权重矩阵。
所以算法步骤如下:
步骤1:给定epoch次数,batch_size大小,学习率;输入数据,初始化权重参数;
步骤2:设置两层循环,1. 第一层循环:epoch迭代次数;2. 打乱epoch内数据顺序;3. 第二层循环,一个epoch按照下标顺序被分为多个batch,每个batch的大小相同;
步骤3:调用正向计算函数,得到神经元上的激活值和加权和(仿射计算值);
步骤4:调用反向计算函数,得到一个batch内每一个权重的更新梯度的平均值;
步骤5:使用学习率/步长参数对权重参数进行更新,得到更新后的权重参数;
步骤6:回到步骤3进行循环,batch循环结束后回到步骤2,进行epoch循环
正向传播函数
首先,需要写一个 正向计算
的函数,当input一个数组 x
时,函数将对 x
进行正向传播,使用权重参数 W
,逐层计算每一层神经元的激活函数值,最后输出 y
值,也即 a_s[-1]
。
每一个神经元的线性加权值 z_s
,激活值 a_s
以及权重参数 W
都需要被保存:
z_s
保存为矩阵形式,整体是个list,list的每一个元素都是一个 layer[i]*batch_size
的矩阵,其中 layer[i]
表示第i层网络神经元的个数,需要注意的是我们 不需要 保存input层( layer[0]
)的 z_s
;
a_s
保存为矩阵形式,整体是个list,list的每一个元素都是一个 layer[i]*batch_size
的矩阵,其中 layer[i]
表示第i层网络神经元的个数,需要注意的是我们 需要 保存input层的 a_s
,并且定义 a_s[0]
的值就是input数据 x
。
W
会在一个batch内的多个sample计算中被复用,保存为矩阵,整体是一个和 z_s
维度相同的list, W[i]
是一个维度为 layer[i+1]*layer[i]
的矩阵。
如图所示,对于input层来说, W_s[0]
为 layer[1]*layer[0]
的二维数组, a
为 layer[0]*bathc_size
的数组,两变量做 矩阵相乘
得到的是 layer[1]*bathc_size
的二维数组。
代码:
def feedforward(self, x): # 正向计算 #x 在train函数里为x_batch,x,y是一个矩阵:相当于对多笔数据进行并行计算 a = np.copy(x) z_s = [] a_s = [a] for i in range(len(self.weights)): activation_function = self.getActivationFunction(self.activations[i]) z_s.append(self.weights[i].dot(a) + self.biases[i]) a = activation_function(z_s[-1]) a_s.append(a) return (z_s, a_s)
矩阵相乘在数值计算上可以做很多优化,这点 matlab
最擅长了;使用 GPU
并行计算也可。
反向传播函数
如图所示,将正传得到的结果和
的距离做一个度量,也就是设计一个loss函数,这里简单将loss设置为二范数的形式;这样一来,(y-a_s[-1])
就是梯度
,接着让
(y-a_s[-1])
乘以
,得到传播的初始值
;再使
沿着反方向逐层计算,神经元上的值并保存在
内就好了;由公式
可知,
需要计算到第一层隐含层;最后将正向计算的
a_s[i]
和
delta[i]
做矩阵相乘就得到了每一个
W
的梯度,注意计算时一个batch内的
W
需要计算均值。
正向计算已经保存了 z_s
, a_s
以及 W
;反向传播涉及的变量有 delta
dw
db
:
delta
在函数内保存为和 w
维度相同的list,
layer[i]*batch_size
的矩阵,和
z_s[i]
进行element-wise的相乘。
需要注意的是,正向传播的时候用的是 w[i].dot(a)
,反向传播时则使用 w[i].T.dot(delta[i])
,这在数学上很好理解,把矩阵写成线性方程组就一目了然了。
dw[i]
在函数中必须要保存为和 w
的形式一模一样,如上篇博文的图3所示, delta[i]
和 a_s[i]
相乘;如下图所示, delta[i]
中的每一个列向量第i个元素组成一个向量 分别 和 a_s[i]
中的每一个列向量第i个元素组成的向量做 内积 ,得到的便是求和之后的权重矩阵,最后整体除以 batch_size
得到 dw[i]
矩阵。
如图所示,这里的 delta[i]
和 a_s[i]
相乘部分也是可以用矩阵计算来完成的,把 a_s
矩阵转置一下就可以相乘了。
db[i]
在求 dw
中乘以 a_s[i]
改为 乘以1 就行了,参考上篇博客的公式推导。
代码
def backpropagation(self,y, z_s, a_s): # 反向计算 dw = [] # dl/dW db = [] # dl/dB deltas = [None] * len(self.weights) # 存放每一层的error # deltas[-1] = sigmoid'(z)*[partial l/partial y] # 这里y是标注数据,a_s[-1]是最后一层的输出,差值就是二范数loss的求导 deltas[-1] =(y-a_s[-1])*(self.getDerivitiveActivationFunction(self.activations[-1]))(z_s[-1]) # Perform BackPropagation for i in reversed(range(len(deltas)-1)): deltas[i] = self.weights[i+1].T.dot(deltas[i+1])*(self.getDerivitiveActivationFunction(self.activations[i])(z_s[i])) batch_size = y.shape[1] db = [d.dot(np.ones((batch_size,1)))/float(batch_size) for d in deltas] dw = [d.dot(a_s[i].T)/float(batch_size) for i,d in enumerate(deltas)] # return the derivitives respect to weight matrix and biases return dw, db
训练函数
train
函数就是将整个计算流程表达出来,输入数据 (x,y)
, batch_size
epoch
以及步长/学习率 lr
;按照算法设计部分的步骤,调用 正向计算
和 反向计算
函数就可以更新权重参数了。
代码
def train(self, x, y, batch_size, epochs, lr): # update weights and biases based on the output for e in range(epochs): ''' # 使用下标来打乱数据,有点麻烦 x_num = x.shape[0] index = np.arange(x_num) # 生成下标 np.random.shuffle(index) i = index[0] ''' # 直接打乱源数据 nn=np.random.randint(1,1000) np.random.seed(nn) np.random.shuffle(x) np.random.seed(nn) np.random.shuffle(y) i = 0 while(i<len(y)): x_batch = x[i:i+batch_size].reshape(1, -1) # 转换成矩阵更加清晰明了 y_batch = y[i:i+batch_size].reshape(1, -1) i = i+batch_size z_s, a_s = self.feedforward(x_batch) dw, db = self.backpropagation(y_batch, z_s, a_s) # 一个batch更新一次参数 self.weights = [w+lr*dweight for w,dweight in zip(self.weights, dw)] self.biases = [w+lr*dbias for w,dbias in zip(self.biases, db)] print("loss = {}".format(np.linalg.norm(a_s[-1]-y_batch) ))
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
数据结构与算法分析(Java版)(英文原版)
(美)Clifford A.Shaffer / 电子工业出版社 / 2002-5 / 39.00元
《数据结构与算法分析(C++版)(第2版)》采用程序员最爱用的面向对象C++语言来描述数据结构和算法,并把数据结构原理和算法分析技术有机地结合在一起,系统介绍了各种类型的数据结构和排序、检索的各种方法。作者非常注意对每一种数据结构的不同存储方法及有关算法进行分析比较。书中还引入了一些比较高级的数据结构与先进的算法分析技术,并介绍了可计算性理论的一般知识。本版的重要改进在于引入了参数化的模板,从而提......一起来看看 《数据结构与算法分析(Java版)(英文原版)》 这本书的介绍吧!