购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.2 自定义神经网络框架的基本设计

本章学习自定义神经网络框架,稍微有点困难,建议有一定编程基础的读者掌握一下,其他读者了解一下即可。

对于一个普通的神经网络运算流程来说,最基本的过程包含两个阶段,即训练(training)和预测(predict)。而训练的基本流程包括输入数据、网络层前向传播、计算损失、网络层反向传播梯度、更新参数这一系列过程。对于预测来说,又分为输入数据、网络层前向传播和输出结果。

3.2.1 神经网络框架的抽象实现

神经网络的预测就是训练过程的一部分,因此,基于训练的过程,我们可以对神经网络中的基本组件进行抽象。在这里,神经网络的组件被抽象成4部分,分别是数据输入、计算层(包括激活层)、损失计算以及优化器,如图3-9所示。

图3-9 神经网络的组件抽象成的4部分

各个部分的作用如下。

· 输入数据:这个是神经网络中数据输入的基本内容,一般我们将其称为tensor。

· 计算层:负责接收上一层的输入,进行该层的运算,并将结果输出给下一层,由于tensor的流动有前向和反向两个方向,因此对于每种类型的网络层,我们都需要同时实现forward和backward两种运算。

· 激活层:通常与计算层结合在一起对每个计算层进行非线性分割。

· 损失计算:在给定模型预测值与真实值之后,使用该组件计算损失值以及关于最后一层的梯度。

· 优化器:负责使用梯度更新模型的参数。

基于上面的分析,我们可以按照抽象的认识完成深度学习代码的流程设计,如下所示:

     # define model
     net = Net(Activity([layer1, layer2, ...]))   #数据的激活与计算
     model = Model(net, loss_fn, optimizer)
     # training                                   #训练过程
     pred = model.forward(train_X)                #前向计算
     loss, grads = model.backward(pred, train_Y)  #反向计算
     model.apply_grad(grads)                      #参数优化
     # inference                                  #预测过程
     test_pred = model.forward(test_X)

上面代码中,我们定义了一个net计算层,然后将net、loss-fn、optimizer一起传给model。model实现了forward、backward和apply_grad三个接口,分别对应前向传播、反向传播和参数更新三个功能。下面我们分别对这些内容进行实现。

3.2.2 自定义神经网络框架的具体实现

本小节演示自定义神经网络框架的具体实现,这个实现较为困难,请读者结合本书配套源码包中的train.py文件按下面的说明步骤进行学习。

1.tensor数据包装

根据前面的分析,首先需要实现数据的输入输出定义,即张量的定义类型。张量是神经网络中的基本数据单位,为了简化起见,这里直接使用numpy.ndarray类作为tensor类的实现。

     import numpy as np
     tensor = np.random.random(size=(10,28,28,1))

上面代码中,我们直接使用NumPy包中的random函数生成数据。

2.layer计算层的基类与实现

计算层的作用是对输入的数据进行计算,在这一层中输入数据的前向计算在forward过程中完成,相对于普通的计算层来说,除了需要计算forward过程外,还需要实现一个参数更新的backward过程。因此,一个基本的计算层的基类如下:

下面实现一个基本的神经网络计算层——全连接层。关于全连接层的详细介绍,我们在后续章节中会讲解,在这里主要将其作为一个简单的计算层来实现。

在全连接层的计算过程中,forward接受上层的输入inputs实现 ωx + b 的计算;backward正好相反,接受来自反向的梯度。具体实现如下:

     class Dense(Layer):
        """A dense layer operates `outputs = dot(intputs, weight) + bias`
        :param num_out: A positive integer, number of output neurons
        :param w_init: Weight initializer
        :param b_init: Bias initializer
        """
        def __init__(self,
                  num_out,
                  w_init=XavierUniform(),
                  b_init=Zeros()):
           super().__init__()
     
           self.initializers = {"w": w_init, "b": b_init}
           self.shapes = {"w": [None, num_out], "b": [num_out]}
     
        def forward(self, inputs):
           if not self.is_init:
              self.shapes["w"][0] = inputs.shape[1]
              self._init_params()
           self.ctx = {"X": inputs}
           return inputs @ self.params["w"] + self.params["b"]
     
        def backward(self, grad):
           self.grads["w"] = self.ctx["X"].T @ grad
           self.grads["b"] = np.sum(grad, axis=0)
           return grad @ self.params["w"].T
     
        @property
        def param_names(self):
           return "w", "b"

在这里我们实现一个可以计算的forward函数,其目的是对输入的数据进行前向计算,具体计算结果如下:

     tensor = np.random.random(size=(10, 28, 28, 1))
     tensor = np.reshape(tensor,newshape=[10,28*28])
     res = Dense(512).forward(tensor)

上面代码生成了一个随机数据集,再通过reshape函数对其进行折叠,之后使用我们自定义的全连接层对其进行计算。最终结果请读者自行打印查看。

3.激活层的基类与实现

神经网络框架中的另一个重要的部分是激活函数。激活函数可以看作是一种网络层,同样需要实现forward和backward方法。我们通过继承Layer基类实现激活函数类,这里实现了常用的ReLU激活函数。forwar和backward方法分别实现对应激活函数的正向计算和梯度计算,代码如下:

     #activity_layer
     import numpy as np
     
     class Layer(object):
        def __init__(self, name):
           self.name = name
           self.params, self.grads = None, None
        def forward(self, inputs):
           raise NotImplementedError
        def backward(self, grad):
           raise NotImplementedError
     
     class Activation(Layer):
        """Base activation layer"""
        def __init__(self, name):
           super().__init__(name)
           self.inputs = None
        def forward(self, inputs):    #下面调用具体的forward实现函数
           self.inputs = inputs
           return self.forward_func(inputs)
        def backward(self, grad):     #下面调用具体的backward实现函数
           return self.backward_func(self.inputs) * grad
        def forward_func(self, x):    #具体的forward实现函数
           raise NotImplementedError
        def backward_func(self, x):   #具体的backward实现函数
           raise NotImplementedError
     
     class ReLU(Activation):
        """ReLU activation function"""
        def __init__(self):
           super().__init__("ReLU")
        def forward_func(self, x):
           return np.maximum(x, 0.0)
        def backward_func(self, x):
           return x > 0.0

这里需要注意,对于具体的forward和backward实现函数,需要实现一个特定的需求对应的函数,从而完成对函数的计算。

4.辅助网络更新的基类——Net

对于神经网络来说,误差需要在整个模型中传播,即正向(Forward)传播和反向(Backward)传播。正向传播的实现方法很简单,按顺序遍历所有层,每层计算的输出作为下一层的输入;反向传播则逆序遍历所有层,将每层的梯度作为下一层的输入。

这一部分的具体实现需要建立一个辅助网络参数更新的网络基类,其作用是对每一层进行forward和backward计算,并更新各个层中的参数。为了达成这个目标,我们建立一个model基类,其作用是将每个网络层参数及其梯度保存下来。具体实现的model类如下:

5.损失函数计算组件与优化器

对于神经网络的训练来说,损失的计算与参数优化是必不可少的操作。对于损失函数组件来说,给定了预测值和真实值,需要计算损失值和关于预测值的梯度。我们分别使用loss和grad两个方法来实现。

具体而言,我们需要实现基类的损失(loss)函数与优化器(optimizer)函数。损失函数如下:

     # loss
     class BaseLoss(object):
        def loss(self, predicted, actual):
           raise NotImplementedError
        def grad(self, predicted, actual):
           raise NotImplementedError

而优化器的基类需要实现根据当前的梯度,计算返回实际优化时每个参数改变的步长,代码如下:

下面是对这两个类的具体实现。对于损失函数来说,我们最常用的也就是第2章所使用的多分类损失函数——多分类Softmax交叉熵。具体的数学形式如下(关于此损失函数的计算,读者可对比3.1.2节有关CrossEntropy的计算进行学习):

具体实现形式如下:

     class CrossEntropyLoss(BaseLoss):
        def loss(self, predicted, actual):
           m = predicted.shape[0]
           exps = np.exp(predicted - np.max(predicted, axis=1, keepdims=True))
           p = exps / np.sum(exps, axis=1, keepdims=True)
           nll = -np.log(np.sum(p * actual, axis=1))
           return np.sum(nll) / m
        def grad(self, predicted, actual):
           m = predicted.shape[0]
           grad = np.copy(predicted)
           grad -= actual
           return grad / m

这里需要注意的是,我们在设计优化器时并没有进行归一化处理,因此在使用之前需要对分类数据进行one-hot表示,对其进行表示的函数如下:

     def get_one_hot(targets, nb_classes=10):
        return np.eye(nb_classes)[np.array(targets).reshape(-1)]

对于优化器来说,其公式推导较为复杂,我们在这里只实现常用的Adam优化器,具体数学推导部分有兴趣的读者可自行研究学习。

     class Adam(BaseOptimizer):
        def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, eps=1e-8, weight_decay=0.0):
           super().__init__(lr, weight_decay)
           self._b1, self._b2 = beta1, beta2
           self._eps = eps
           self._t = 0
           self._m, self._v = 0, 0
        def _compute_step(self, grad):
           self._t += 1
           self._m = self._b1 * self._m + (1 - self._b1) * grad
           self._v = self._b2 * self._v + (1 - self._b2) * (grad ** 2)
           # bias correction
           _m = self._m / (1 - self._b1 ** self._t)
           _v = self._v / (1 - self._b2 ** self._t)
           return -self.lr * _m / (_v ** 0.5 + self._eps)
6.整体model类的实现

Model类实现了我们一开始设计的3个接口:forward、backward和apply_grad。在forward方法中,直接调用net的forward方法,在backward方法中,把net、loss、optimizer串联起来,首先计算损失(loss),然后进行反向传播得到梯度,接着由optimizer计算步长,最后通过apply_grad对参数进行更新,代码如下:

在Model类中,我们串联了损失函数、优化器以及对应的参数更新方法,从而将整个深度学习模型作为一个完整的框架进行计算。

7.基于自定义框架的神经网络框架的训练

下面进行最后一步,基于自定义框架的神经网络模型的训练。如果读者遵循作者的提示,在一开始对应train.py方法对模型的各个组件进行学习,那么相信在这里能够比较轻松地完成本小节的最后一步。完整的自定义神经网络框架训练如下:

最终训练结果如下:

     train_loss: 1.52 accuracy: 0.73
     train_loss: 0.78 accuracy: 0.84
     train_loss: 0.54 accuracy: 0.88
     train_loss: 0.34 accuracy: 0.91
     train_loss: 0.33 accuracy: 0.88
     train_loss: 0.38 accuracy: 0.85
     train_loss: 0.28 accuracy: 0.93
     train_loss: 0.23 accuracy: 0.94
     train_loss: 0.31 accuracy: 0.9
     train_loss: 0.18 accuracy: 0.95

可以看到,随着训练的深入进行,此时损失值在降低,而准确率随着训练次数的增加在不停地增高,具体请读者自行演示学习。 ucmLyP7qqoOHKzHxx+e8BsNyRlGdL8s0WHY2ggtaA1m6ugP4GGSraF09+u9kPqb+

点击中间区域
呼出菜单
上一章
目录
下一章
×