从零开始写NN(上)

栏目: 编程工具 · 发布时间: 4年前

内容简介:从零开始写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 都会计算一次正传的值,保存每个神经元的值为 反向传播 计算所用;再进行反向计算得到 wb 的梯度,之后使用梯度对模型参数也即 wb 进行更新,每次迭代都会使用一组新的 batch 。当所有的sample都进行计算后,再将所有的sample顺序打乱,循环上面的过程。

其中,每一个 batch 都会使用矩阵运算,这样可以使用并行算法,这也是 前馈神经网络 要比 循环/递归神经网络 训练快的一个主要原因;公式表达如下:

下面是算法图解,从上往下看,每一个神经网络表示对每一个sample的计算,一共有 batch_size 个,图中

表示 反向传播 过程中神经元上的值(误差累计),上篇博文中式(5); 表示 正向计算 神经元上的加权和(仿射值); 表示 正向计算 神经元上的激活值; W_s[i]

表示两层之间的权重矩阵。

从零开始写NN(上)

所以算法步骤如下:

步骤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] 的二维数组, alayer[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] 矩阵。

从零开始写NN(上)

如图所示,这里的 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) ))

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Effective Ruby:改善Ruby程序的48条建议

Effective Ruby:改善Ruby程序的48条建议

Peter J. Jones / 杨政权、秦五一、孟樊超 / 机械工业出版社 / 2016-1 / 49

如果你是经验丰富的Rub程序员,本书能帮助你发挥Ruby的全部力量来编写更稳健、高效、可维护和易执行的代码。Peter J.Jones凭借其近十年的Ruby开发经验,总结出48条Ruby的最佳实践、专家建议和捷径,并辅以可执行的代码实例。 Jones在Ruby开发的每个主要领域都给出了实用的建议,从模块、内存到元编程。他对鲜为人知的Ruby方言、怪癖、误区和强力影响代码行为与性能的复杂性的揭......一起来看看 《Effective Ruby:改善Ruby程序的48条建议》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具