查看原文
其他

《神经网络和深度学习》系列文章四十六:卷积网络的代码

Nielsen 哈工大SCIR 2021-02-05

出处: Michael Nielsen的《Neural Network and Deep Learning》,点击末尾“阅读原文”即可查看英文原文。

声明:我们将在每周四连载该书的中文翻译。

本节译者:朱小虎 、张广宇。转载已获得译者授权,禁止二次转载。


  • 使用神经网络识别手写数字

  • 反向传播算法是如何工作的

  • 改进神经网络的学习方法

  • 神经网络可以计算任何函数的可视化证明

  • 为什么深度神经网络的训练是困难的

  • 深度学习

    • 介绍卷积网络

    • 卷积神经网络在实际中的应用

    • 卷积网络的代码

    • 图像识别领域中的近期进展

    • 其他的深度学习模型

    • 神经网络的未来


好了,现在来看看我们的卷积网络代码,network3.py。整体看来,程序结构类似于 network2.py,尽管细节有差异,因为我们使用了 Theano。首先我们来看 FullyConnectedLayer 类,这类似于我们之前讨论的那些神经网络层。下面是代码

class FullyConnectedLayer(object): def __init__(self, n_in, n_out, activation_fn=sigmoid, p_dropout=0.0):        self.n_in = n_in        self.n_out = n_out        self.activation_fn = activation_fn        self.p_dropout = p_dropout        # Initialize weights and biases        self.w = theano.shared(            np.asarray(                np.random.normal(                    loc=0.0, scale=np.sqrt(1.0/n_out), size=(n_in, n_out)),                dtype=theano.config.floatX),            name='w', borrow=True)        self.b = theano.shared(            np.asarray(np.random.normal(loc=0.0, scale=1.0, size=(n_out,)),                       dtype=theano.config.floatX),            name='b', borrow=True)        self.params = [self.w, self.b]    def set_inpt(self, inpt, inpt_dropout, mini_batch_size):        self.inpt = inpt.reshape((mini_batch_size, self.n_in))        self.output = self.activation_fn(            (1-self.p_dropout)*T.dot(self.inpt, self.w) + self.b)        self.y_out = T.argmax(self.output, axis=1)        self.inpt_dropout = dropout_layer(            inpt_dropout.reshape((mini_batch_size, self.n_in)), self.p_dropout)        self.output_dropout = self.activation_fn(            T.dot(self.inpt_dropout, self.w) + self.b)    def accuracy(self, y):        "Return the accuracy for the mini-batch."        return T.mean(T.eq(y, self.y_out))

__init__ 方法中的大部分都是可以自解释的,这里再给出一些解释。我们根据正态分布随机初始化了权重和偏差。代码中对应这个操作的一行看起来可能很吓人,但其实只在进行载入权重和偏差到 Theano 中所谓的共享变量中。这样可以确保这些变量可在 GPU中进行处理。对此不做过深的解释。如果感兴趣,可以查看 Theano 的文档。而这种初始化的方式也是专门为 sigmoid 激活函数设计的。理想的情况是,我们初始化权重和偏差时会根据不同的激活函数(如 tanh 和 Rectified Linear Function)进行调整。这个在下面的问题中会进行讨论。初始方法 __init__ 以 self.params = [self.W, self.b] 结束。这样将该层所有需要学习的参数都归在一起。后面,Network.SGD 方法会使用 params 属性来确定网络实例中什么变量可以学习。

set_inpt 方法用来设置该层的输入,并计算相应的输出。我使用 inpt 而非 input 因为在python 中 input 是一个内置函数。如果将两者混淆,必然会导致不可预测的行为,对出现的问题也难以定位。注意我们实际上用两种方式设置输入的:self.input 和 self.inpt_dropout。因为训练时我们可能要使用 dropout。如果使用 dropout,就需要设置对应丢弃的概率 self.p_dropout。这就是在 set_inpt 方法的倒数第二行 dropout_layer 做的事。所以 self.inpt_dropout 和 self.output_dropout 在训练过程中使用,而 self.inpt 和 self.output 用作其他任务,比如衡量验证集和测试集模型的准确度。

ConvPoolLayer 和 SoftmaxLayer 类定义和 FullyConnectedLayer 定义差不多。所以我这儿不会给出代码。如果你感兴趣,可以参考本节后面的 network3.py 的代码。

尽管这样,我们还是指出一些重要的微弱的细节差别。明显一点的是,在 ConvPoolLayer 和 SoftmaxLayer 中,我们采用了相应的合适的计算输出激活值方式。幸运的是,Theano 提供了内置的操作让我们计算卷积、max-pooling和 softmax 函数。

不大明显的,在我们引入 softmax layer 时,我们没有讨论如何初始化权重和偏差。其他地方我们已经讨论过对 sigmoid 层,我们应当使用合适参数的正态分布来初始化权重。但是这个启发式的论断是针对 sigmoid 神经元的(做一些调整可以用于 tanh 神经元上)。但是,并没有特殊的原因说这个论断可以用在 softmax 层上。所以没有一个先验的理由应用这样的初始化。与其使用之前的方法初始化,我这里会将所有权值和偏差设置为0。这是一个 ad hoc 的过程,但在实践使用过程中效果倒是很不错。

好了,我们已经看过了所有关于层的类。那么 Network 类是怎样的呢?让我们看看 __init__ 方法:

class Network(object):    def __init__(self, layers, mini_batch_size):        """Takes a list of `layers`, describing the network architecture, and        a value for the `mini_batch_size` to be used during training        by stochastic gradient descent.        """        self.layers = layers        self.mini_batch_size = mini_batch_size        self.params = [param for layer in self.layers for param in layer.params]        self.x = T.matrix("x")          self.y = T.ivector("y")        init_layer = self.layers[0]        init_layer.set_inpt(self.x, self.x, self.mini_batch_size)        for j in xrange(1, len(self.layers)):            prev_layer, layer  = self.layers[j-1], self.layers[j]            layer.set_inpt(                prev_layer.output, prev_layer.output_dropout, self.mini_batch_size)        self.output = self.layers[-1].output        self.output_dropout = self.layers[-1].output_dropout

这段代码大部分是可以自解释的。self.params = [param for layer in …]此行代码对每层的参数捆绑到一个列表中。Network.SGD 方法会使用 self.params 来确定 Network 中哪些变量需要学习。而 self.x = T.matrix(“x”) 和 self.y = T.ivector(“y”) 则定义了 Theano 符号变量 x 和 y。这些会用来表示输入和网络得到的输出。

这里不是 Theano 的教程,所以不会深度讨论这些变量指代什么东西。但是粗略的想法就是这些代表了数学变量,而非显式的值。我们可以对这些变量做通常需要的操作:加减乘除,作用函数等等。实际上,Theano 提供了很多对符号变量进行操作方法,如卷积、最大值混合等等。但是最重要的是能够进行快速符号微分运算,使用反向传播算法一种通用的形式。这对于应用随机梯度下降在若干种网络结构的变体上特别有效。特别低,接下来几行代码定义了网络的符号输出。我们通过下面这行设置初始层的输入。

init_layer.set_inpt(self.x, self.x, self.mini_batch_size)

请注意输入是以每次一个 mini-batch 的方式进行的,这就是 mini-batch 大小为何要指定的原因。还需要注意的是,我们将输入 self.x 传了两次:这是因为我们我们可能会以两种方式(有dropout和无dropout)使用网络。for 循环将符号变量 self.x 通过 Network 的层进行前向传播。这样我们可以定义最终的输出 output 和 output_dropout 属性,这些都是 Network 符号式输出。

现在我们理解了 Network 是如何初始化了,让我们看看它如何使用 SGD 方法进行训练的。代码看起来很长,但是它的结构实际上相当简单。代码后面也有一些注解。

   def SGD(self, training_data, epochs, mini_batch_size, eta,            validation_data, test_data, lmbda=0.0):        """Train the network using mini-batch stochastic gradient descent."""        training_x, training_y = training_data        validation_x, validation_y = validation_data        test_x, test_y = test_data        # compute number of minibatches for training, validation and testing        num_training_batches = size(training_data)/mini_batch_size        num_validation_batches = size(validation_data)/mini_batch_size        num_test_batches = size(test_data)/mini_batch_size        # define the (regularized) cost function, symbolic gradients, and updates        l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])        cost = self.layers[-1].cost(self)+\               0.5*lmbda*l2_norm_squared/num_training_batches        grads = T.grad(cost, self.params)        updates = [(param, param-eta*grad)                   for param, grad in zip(self.params, grads)]        # define functions to train a mini-batch, and to compute the        # accuracy in validation and test mini-batches.        i = T.lscalar() # mini-batch index        train_mb = theano.function(            [i], cost, updates=updates,            givens={                self.x:                training_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],                self.y:                training_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]            })        validate_mb_accuracy = theano.function(            [i], self.layers[-1].accuracy(self.y),            givens={                self.x:                validation_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],                self.y:                validation_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]            })        test_mb_accuracy = theano.function(            [i], self.layers[-1].accuracy(self.y),            givens={                self.x:                test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],                self.y:                test_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]            })        self.test_mb_predictions = theano.function(            [i], self.layers[-1].y_out,            givens={                self.x:                test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size]            })        # Do the actual training        best_validation_accuracy = 0.0        for epoch in xrange(epochs):            for minibatch_index in xrange(num_training_batches):                iteration = num_training_batches*epoch+minibatch_index                if iteration                    print("Training mini-batch number {0}".format(iteration))                cost_ij = train_mb(minibatch_index)                if (iteration+1)                    validation_accuracy = np.mean(                        [validate_mb_accuracy(j) for j in xrange(num_validation_batches)])                    print("Epoch {0}: validation accuracy {1:.2                        epoch, validation_accuracy))                    if validation_accuracy >= best_validation_accuracy:                        print("This is the best validation accuracy to date.")                        best_validation_accuracy = validation_accuracy                        best_iteration = iteration                        if test_data:                            test_accuracy = np.mean(                                [test_mb_accuracy(j) for j in xrange(num_test_batches)])                            print('The corresponding test accuracy is {0:.2                                test_accuracy))        print("Finished training network.")        print("Best validation accuracy of {0:.2            best_validation_accuracy, best_iteration))        print("Corresponding test accuracy of {0:.2

前面几行很直接,将数据集分解成 x 和 y 两部分,并计算在每个数据集中 mini-batches 的数量。接下来的几行更加有意思,这也体现了 Theano 有趣的特性。那么我们就摘录详解一下:

 # define the (regularized) cost function, symbolic gradients, and updates  l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])  cost = self.layers[-1].cost(self)+\         0.5*lmbda*l2_norm_squared/num_training_batches  grads = T.grad(cost, self.params)  updates = [(param, param-eta*grad)             for param, grad in zip(self.params, grads)]

这几行,我们符号化地给出了规范化的对数似然代价函数,在梯度函数中计算了对应的导数,以及对应参数的更新方式。Theano 让我们通过这短短几行就能够获得这些效果。唯一隐藏的是计算 cost 包含一个对输出层 cost 方法的调用;该代码在 network3.py 中其他地方。但是,总之代码很短而且简单。有了所有这些定义好的东西,下面就是定义 train_mini_batch 函数,该 Theano 符号函数在给定 minibatch 索引的情况下使用 updates 来更新 Network 的参数。类似地,validate_mb_accuracy 和 test_mb_accuracy 计算在任意给定的 minibatch 的验证集和测试集合上 Network 的准确度。通过对这些函数进行平均,我们可以计算整个验证集和测试数据集上的准确度。

SGD 方法剩下的就是可以自解释的了~——~我们对次数进行迭代,重复使用训练数据的 mini-batches 来训练网络,计算验证集和测试集上的准确度。

好了,我们已经理解了 network3.py 代码中大多数的重要部分。让我们看看整个程序,你不需过分仔细地读下这些代码,但是应该享受粗看的过程,并随时深入研究那些激发出你好奇地代码段。理解代码的最好的方法就是通过修改代码,增加额外的特征或者重新组织那些你认为能够更加简洁地完成的代码。代码后面,我们给出了一些对初学者的建议。这儿是代码:。

在GPU 上使用 Theano 可能会有点难度。特别地,很容易在从 GPU 中拉取数据时出现错误,这可能会让运行变得相当慢。我已经试着避免出现这样的情况,但是也不能肯定在代码扩充后出现一些问题。对于你们遇到的问题或者给出的意见我洗耳恭听(mn@michaelnielsen.org)。

问题

  • 目前,SGD 方法要求用户手动选择用于训练 epochs 的数量。前文中,我们讨论了一种自动选择训练次数的方法,也就是提前停止。修改network3.py 以实现提前停止。

  • 增加一个 Network 方法来返回在任意数据集上的准确度。

  • 修改 SGD 方法来允许学习率 η 可以是训练次数的函数。

  • 在本章前面我曾经描述过一种通过应用微小的旋转、扭曲和变化来扩展训练数据的方法。改变 network3.py 来加入这些技术。注意:除非你有充分多的内存,否则显式地产生整个扩展数据集是不大现实的。所以要考虑一些变通的方法。

  • 在 network3.py 中增加 load 和 save 方法。

  • 当前的代码缺点就是只有很少的用来诊断的工具。你能想出一些诊断方法告诉我们网络过匹配到什么程度么?加上这些方法。

  • 我们已经对修正线性单元及 S 型和 tanh 函数神经元使用了同样的初始方法。正如这里所说,这种初始化方法只是适用于sigmoid 函数。假设我们使用一个全部使用 ReLU 的网络。试说明以常数 c 倍调整网络的权重最终只会对输出有常数 c 倍的影响。如果最后一层是 softmax,则会发生什么样的变化?对 ReLU 使用 sigmoid 函数的初始化方法会怎么样?有没有更好的初始化方法?注意:这是一个开放的问题,并不是说有一个简单的自包含答案。还有,思考这个问题本身能够帮助你更好地理解包含 ReLU 的神经网络。

  • 我们对于不稳定梯度问题的分析实际上是针对 sigmoid 神经元的。如果是 ReLU,那分析又会有什么差异?你能够想出一种使得网络不太会受到不稳定梯度问题影响的好方法么?注意:“好”这个词实际上就是一个研究性问题。实际上有很多容易想到的修改方法。但我现在还没有研究足够深入,能告诉你们什么是真正的好技术。



  • “哈工大SCIR”公众号

  • 编辑部:郭江,李家琦,施晓明,张文博,赵得志

  • 本期编辑:张文博


长按下图并点击 “识别图中二维码”,即可关注哈尔滨工业大学社会计算与信息检索研究中心微信公共号:”哈工大SCIR” 。点击左下角“阅读原文”,即可查看原文。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存