深度学习基础之卷积神经网络

    技术2022-07-17  83

    摘要

        受Hubel和Wiesel对猫视觉皮层电生理研究启发,有人提出卷积神经网络(CNN),Yann Lecun 最早将CNN用于手写数字识别并一直保持了其在该问题的霸主地位。近年来卷积神经网络在多个方向持续发力,在语音识别、人脸识别、通用物体识别、运动分析、自然语言处理甚至脑电波分析方面均有突破。这听起来像是一个奇怪的生物学和数学的结合,但是这些网络已经成为计算机视觉领域最具影响力的创新之一。2012年是神奇网络成长的第一年,Alex Krizhevsky用它们赢得了当年的ImageNet竞赛(基本上是计算机视觉年度奥运会),把分类错误记录从26%降到了15%,这个惊人的提高从那以后,许多公司一直在以服务为核心进行深度学习。Facebook使用自动标记算法的神经网络,谷歌的照片搜索,亚马逊的产品推荐,Pinterest的家庭饲料个性化和Instagram的搜索基础设施。然而,经典的,可以说是最流行的,这些网络的用例是用于图像处理。在图像处理中,如何使用这些CNN进行图像分类,以下进行深度学习基础讲解,以及使用CNN卷积神经网络进行图像模拟训练。

     

    一.引言

    (1)问题空间

        图像分类是获取输入图像和输出类(猫,狗等)或类的概率最好描述图像的任务。对于人类来说,承认这项任务是我们从出生那一刻起学到的第一个技能之一,并且是成年人自然而不费吹灰之力的人。即使没有两次思考,我们也能够快速无缝地识别我们所处的环境以及周围的物体。当我们看到一张图像或者只是看着周围的世界时,大部分时间我们都能够立刻刻画这个场景,给每个对象一个标签,所有这些都没有自觉地注意到。

     

                                                     图1-1

     

    (2)输入和输出

        当一台电脑看到一个图像(以图像作为输入)时,它会看到一个像素值的数组。根据图像的分辨率和大小,它将看到一个32×32×3的数字数组(3指的是RGB值)。为了说明这一点,假设我们有一个JPG格式的彩色图像,它的大小是480 x 480.代表性的数组将是480 x 480 x 3.这些数字中的每一个都有一个从0到255的值,它描述该点的像素强度。这些数字对于我们进行图像分类时毫无意义,这是计算机唯一可用的输入。这个想法是,你给计算机这个数组的数组,它会输出的数字,描述了图像是一个类的概率(0.80为猫,0.15为狗,0.05为鸟等)。

     

    (3)如何实现

        现在我们知道这个问题以及输入和输出了,我们来思考如何解决这个问题。我们希望计算机做的是能够区分所有的图像,并找出使狗成为狗或使猫成为猫的独特功能。这也是下意识地在我们的脑海中继续的过程。当我们看一张狗的照片时,如果照片具有可识别的特征,例如爪子或四条腿,我们可以将其分类。以类似的方式,计算机能够通过查找诸如边缘和曲线等低级特征来执行图像分类,然后通过一系列卷积层来构建更抽象的概念。这是一个CNN的一般概述。

     

    (4)生物连接

        第一次听说卷积神经网络这个术语的时候,可能已经想到了一些与神经科学或生物学有关的东西。CNN确实从视觉皮层中获得了生物启发。视觉皮层具有对视野特定区域敏感的细胞区域。这个想法是由1962年在一个迷人的实验由胡贝尔和威塞尔(在扩展视频)在那里他们表明,大脑中的一些个体神经元细胞只有在某个方位的边缘存在的情况下才会响应(或发射)。例如,一些神经元在暴露于垂直边缘时发射,而另一些在显示水平或对角边缘时发射。Hubel和Wiesel发现,所有这些神经元都是以柱状结构组织的,并且能够产生视觉感知。在具有特定任务的系统内部(视觉皮层中寻找特定特征的神经元细胞)内部的专门组件的想法也是机器使用的,并且是CNN背后的基础。

     

    (5).结构体

    有关CNN做的更详细的概述是,您将图像传递给一系列卷积,非线性,汇聚(下采样)和完全连接的图层,并获得输出。正如我们前面所说的那样,输出可以是一个类或者一个最能描述图像的类的概率。现在,困难的部分是了解每个层次都做了什么。所以让我们进入最重要的一个。

     

     

    二.系统结构

    (一)了解神经网络和反向传播方法

    1.神经网络

        一个全连接(full connected, FC)神经网络,通过观察上面的图,我们可以发现它的规则包括:

    (1)神经元按照层来布局。最左边的层叫做输入层,负责接收输入数据;最右边的层叫输出层,我们可以从这层获取神经网络输出数据。输入层和输出层之间的层叫做隐藏层,因为它们对于外部来说是不可见的。

    (2)同一层的神经元之间没有连接。

    (3)第N层的每个神经元和第N-1层的所有神经元相连(这就是full connected的含义),第N-1层神经元的输出就是第N层神经元的输入。

    (4)每个连接都有一个权值。

        上面这些规则定义了全连接神经网络的结构。事实上还存在很多其它结构的神经网络,比如卷积神经网络(CNN)、循环神经网络(RNN),他们都具有不同的连接规则。

     

    2.计算神经网络的输出

    神经网络实际上就是一个输入向量到输出向量的函数,即:

    根据输入计算神经网络的输出,需要首先将输入向量的每个元素的值赋给神经网络的输入层的对应神经元,依次向前计算每一层的每个神经元的值,直到最后一层输出层的所有神经元的值计算完毕。最后,将输出层每个神经元的值串在一起就得到了输出向量。

    如上图,输入层有三个节点,我们将其依次编号为1、2、3;隐藏层的4个节点,编号依次为4、5、6、7;最后输出层的两个节点编号为8、9。因为我们这个神经网络是全连接网络,所以可以看到每个节点都和上一层的所有节点有连接。

     

    每一层的算法都是一样的。比如,对于包含一个输入层,一个输出层和三个隐藏层的神经网络,我们假设其权重矩阵分别为,每个隐藏层的输出分别是,神经网络的输入为,神经网络的输入为,如下图所示:

    则每一层的输出向量的计算可以表示为:

     

    3.神经网络的训练

    以监督学习为例来解释反向传播算法。设神经元的激活函数f为函数。假设每个训练样本为,其中向量是训练样本的特征,而是样本的目标值。

        首先,我们根据上一节介绍的算法,用样本的特征,计算出神经网络中每个隐藏层节点的输出,以及输出层每个节点的输出。然后,我们按照下面的方法计算出每个节点的误差项:

        对于输出层节点i,

        其中,是节点i的误差项,是节点i的输出值,是样本对应于节点i的目标值。举个例子,根据上图,对于输出层节点8来说,它的输出值是,而样本的目标值是,带入上面的公式得到节点8的误差项应该是:

        对于隐藏层节点,

        其中,是节点i的输出值,是节点i到它的下一层节点k的连接的权重,是节点i的下一层节点k的误差项。例如,对于隐藏层节点4来说,计算方法如:

        最后,更新每个连接上的权值:

        其中,是节点i到节点j的权重,是一个成为学习速率的常数,是节点j的误差项,是节点i传递给节点j的输入。例如,权重的更新方法如下:

     

    4.神经网络的实现

    先做一个基本的模型:

    可以分解出5个领域对象来实现神经网络:

    (1)Network 神经网络对象,提供API接口。它由若干层对象组成以及连接对象组成。

    (2)Layer 层对象,由多个节点组成。

    (3)Node 节点对象计算和记录节点自身的信息(比如输出值a、误差项等),以及这个节点相关的上下游的连接。

    (4)Connection 每个连接对象都要记录该连接的权重。

    (5)Connections 仅仅作为Connection的集合对象,提供一些集合操作。

     

    Node实现如下:

    # 节点类,负责记录和维护节点自身信息以及与这个节点相关的上下游连接,实现输出值和误差项的计算。

    1.import random   2.from numpy import *   3.from functools import reduce   4.   5.   6.def sigmoid(inX):   7.    return 1.0 / (1 + exp(-inX))   8.   9.   10.class Node(object):   11.    def __init__(self, layer_index, node_index):   12.        self.layer_index = layer_index   13.        self.node_index = node_index   14.        self.downstream = []   15.        self.upstream = []   16.        self.output = 0   17.        self.delta = 0   18.   19.    def set_output(self, output):   20.        self.output = output   21.   22.    def append_downstream_connection(self, conn):   23.        self.downstream.append(conn)   24.   25.    def append_upstream_connection(self, conn):   26.        self.upstream.append(conn)   27.   28.    def calc_output(self):   29.        # 每个节点的输出算法,N元一次方程求和   30.        output = reduce(lambda ret, conn: ret + conn.upstream_node.output * conn.weight, self.upstream, 0)   31.        # 结果放入激活函数   32.        self.output = sigmoid(output)   33.   34.    def calc_hidden_layer_delta(self):   35.        downstream_delta = reduce(   36.            lambda ret, conn: ret + conn.downstream_node.delta * conn.weight,   37.            self.downstream, 0.0)   38.        self.delta = self.output * (1 - self.output) * downstream_delta   39.   40.    def calc_output_layer_delta(self, label):   41.        self.delta = self.output * (1 - self.output) * (label - self.output)   42.   43.    def __str__(self):   44.        node_str = '%u-%u: output: %f delta: %f' % (self.layer_index, self.node_index, self.output, self.delta)   45.        downstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.downstream, '')   46.        upstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.upstream, '')   47.        return node_str + '\n\tdownstream:' + downstream_str + '\n\tupstream:' + upstream_str  

     

    ConstNode对象,为了实现一个输出恒为1的节点(计算偏置项时需要)

    1.class ConstNode(object):   2.    def __init__(self, layer_index, node_index):   3.        self.layer_index = layer_index   4.        self.node_index = node_index   5.        self.downstream = []   6.        self.output = 1   7.   8.    def append_downstream_connection(self, conn):   9.        self.downstream.append(conn)   10.   11.    def calc_hidden_layer_delta(self):   12.        downstream_delta = reduce(   13.            lambda ret, conn: ret + conn.downstream_node.delta * conn.weight,   14.            self.downstream, 0.0)   15.        self.delta = self.output * (1 - self.output) * downstream_delta   16.   17.    def __str__(self):   18.        node_str = '%u-%u: output: 1' % (self.layer_index, self.node_index)   19.        downstream_str = reduce(lambda ret, conn: ret + '\n\t' + str(conn), self.downstream, '')   20.        return node_str + '\n\tdownstream:' + downstream_str  

     

    Layer对象,负责初始化一层。此外,作为Node的集合对象,提供对Node集合的操作。

    1.class Layer(object):   2.    def __init__(self, layer_index, node_count):   3.        self.layer_index = layer_index   4.        self.nodes = []   5.        # 初始化节点对象   6.        for i in range(node_count):   7.            self.nodes.append(Node(layer_index, i))   8.        self.nodes.append(ConstNode(layer_index, node_count))   9.   10.    def set_output(self, data):   11.        for i in range(len(data)):   12.            self.nodes[i].set_output(data[i])   13.   14.    def calc_output(self):   15.        for node in self.nodes[:-1]:   16.            node.calc_output()   17.   18.    def dump(self):   19.        for node in self.nodes:   20.            print(node)  

     

    Connection对象,主要职责是记录连接的权重,以及这个连接所关联的上下游节点。

    1.class Connection(object):   2.    def __init__(self, upstream_node, downstream_node):   3.        self.upstream_node = upstream_node   4.        self.downstream_node = downstream_node   5.        self.weight = random.uniform(-0.1, 0.1)   6.        self.gradient = 0.0   7.   8.    def calc_gradient(self):   9.        self.gradient = self.downstream_node.delta * self.upstream_node.output   10.   11.    def update_weight(self, rate):   12.        self.calc_gradient()   13.        self.weight += rate * self.gradient   14.   15.    def get_gradient(self):   16.        return self.gradient   17.   18.    def __str__(self):   19.        return '(%u-%u) -> (%u-%u) = %f' % (   20.            self.upstream_node.layer_index,   21.            self.upstream_node.node_index,   22.            self.downstream_node.layer_index,   23.            self.downstream_node.node_index,   24.            self.weight)  

     

    Connections对象,提供Connection集合操作。

    1.class Connections(object):   2.    def __init__(self):   3.        self.connections = []   4.   5.    def add_connection(self, connection):   6.        self.connections.append(connection)   7.   8.    def dump(self):   9.        for conn in self.connections:   10.            print(conn)  

     

    Network对象,提供API。

    1.class Network(object):   2.    def __init__(self, layers):   3.        self.connections = Connections()   4.        self.layers = []   5.        # 计算网络层数   6.        layer_count = len(layers)   7.        node_count = 0   8.        # 初始化网络层,网错层对象append在self.layers 里面,而节点对象又在layer里面被初始化   9.        # Connections 仅仅作为Connection的集合对象,提供一些集合操作, 而layer有是节点对象合集   10.        for i in range(layer_count):   11.            self.layers.append(Layer(i, layers[i]))   12.        for layer in range(layer_count - 1):   13.            connections = [Connection(upstream_node, downstream_node)   14.                           for upstream_node in self.layers[layer].nodes   15.                           for downstream_node in self.layers[layer + 1].nodes[:-1]]   16.            for conn in connections:   17.                self.connections.add_connection(conn)   18.                conn.downstream_node.append_upstream_connection(conn)   19.                conn.upstream_node.append_downstream_connection(conn)   20.   21.    def train(self, labels, data_set, rate, epoch):   22.        for i in range(epoch):   23.            for d in range(len(data_set)):   24.                self.train_one_sample(labels[d], data_set[d], rate)   25.                # print 'sample %d training finished' % d   26.   27.    def train_one_sample(self, label, sample, rate):   28.        self.predict(sample)   29.        self.calc_delta(label)   30.        self.update_weight(rate)   31.   32.    def calc_delta(self, label):   33.        output_nodes = self.layers[-1].nodes   34.        for i in range(len(label)):   35.            output_nodes[i].calc_output_layer_delta(label[i])   36.        for layer in self.layers[-2::-1]:   37.            for node in layer.nodes:   38.                node.calc_hidden_layer_delta()   39.   40.    def update_weight(self, rate):   41.        for layer in self.layers[:-1]:   42.            for node in layer.nodes:   43.                for conn in node.downstream:   44.                    conn.update_weight(rate)   45.   46.    def calc_gradient(self):   47.        for layer in self.layers[:-1]:   48.            for node in layer.nodes:   49.                for conn in node.downstream:   50.                    conn.calc_gradient()   51.   52.    def get_gradient(self, label, sample):   53.        self.predict(sample)   54.        self.calc_delta(label)   55.        self.calc_gradient()   56.   57.    def predict(self, sample):   58.        self.layers[0].set_output(sample)   59.        for i in range(1, len(self.layers)):   60.            self.layers[i].calc_output()   61.        return list(map(lambda node: node.output, self.layers[-1].nodes[:-1]))   62.   63.    def dump(self):   64.        for layer in self.layers:   65.            layer.dump()  

     

    梯度检查。如果我们想检查参数的梯度是否正确,我们需要以下几个步骤:

    (1)首先使用一个样本对神经网络进行训练,这样就能获得每个权重的梯度。

    (2)将加上一个很小的值(),重新计算神经网络在这个样本d下的。

    (3)将减上一个很小的值(),重新计算神经网络在这个样本d下的。

    (4)根据式6计算出期望的梯度值,和第一步获得的梯度值进行比较,它们应该几乎想等(至少4位有效数字相同)。

    1.def gradient_check(network, sample_feature, sample_label):   2.    '''''  3.    梯度检查  4.    network: 神经网络对象  5.    sample_feature: 样本的特征  6.    sample_label: 样本的标签  7.    '''   8.    # 计算网络误差   9.    network_error = lambda vec1, vec2: \   10.        0.5 * reduce(lambda a, b: a + b,   11.                     list(map(lambda v: (v[0] - v[1]) * (v[0] - v[1]),   12.                              zip(vec1, vec2))))   13.   14.    # 获取网络在当前样本下每个连接的梯度   15.    network.get_gradient(sample_feature, sample_label)   16.   17.    # 对每个权重做梯度检查       18.    for conn in network.connections.connections:   19.        # 获取指定连接的梯度   20.        actual_gradient = conn.get_gradient()   21.   22.        # 增加一个很小的值,计算网络的误差   23.        epsilon = 0.0001   24.        conn.weight += epsilon   25.        error1 = network_error(network.predict(sample_feature), sample_label)   26.   27.        # 减去一个很小的值,计算网络的误差   28.        conn.weight -= 2 * epsilon  # 刚才加过了一次,因此这里需要减去2倍   29.        error2 = network_error(network.predict(sample_feature), sample_label)   30.   31.        # 根据式6计算期望的梯度值   32.        expected_gradient = (error2 - error1) / (2 * epsilon)   33.   34.        # 打印   35.        print('expected gradient: \t%f\nactual gradient: \t%f' % (   36.            expected_gradient, actual_gradient))  

     

    结果如下:

     

     

    (二)cnn卷积层次结构

    1.数学部分

        CNN中的第一层始终是一个卷积层。首先要确保你记得这个转换(我将使用这个缩写很多)的输入是什么。就像我们之前提到的那样,输入是一个32×32×3的像素值数组。现在,解释一个conv层的最好方法就是想象一个闪烁在图像左上角的手电筒。假设这个手电筒照射的光线覆盖了5×5的区域。现在,让我们想象这个手电筒滑过输入图像的所有区域。在机器学习方面,这种手电筒被称为滤波器(有时也称为神经元或内核),而它所照射的区域称为接受场。现在这个过滤器也是一个数组数组(数字称为权重或参数)。一个非常重要的注意事项是,这个过滤器的深度必须和输入的深度相同(这可以确保数学运算出来),所以这个过滤器的尺寸是5 x 5 x 3。现在,我们来看看例如过滤器的第一个位置。这将是左上角。当滤波器在输入图像周围滑动或卷积时,它将滤波器中的值与图像的原始像素值相乘(也称为计算元素智能乘法)。所有这些乘法都被总结出来(从数学上讲,这将是总共75次乘法)。所以,现在你有一个单一的数字。记住,这个数字只是过滤器位于图像左上角的代表。现在,我们对输入音量上的每个位置重复这个过程。(下一步将过滤器向右移动1个单位,然后再向右移动1,依此类推)。输入卷上的每个唯一位置都会生成一个数字。将过滤器滑过所有位置后,您将发现所剩下的是一个28 x 28 x 1的数字数组,我们称之为激活图或功能图。你得到一个28×28阵列的原因是有一个5×5的滤波器可以放在一个32×32输入图像上的784个不同的位置。这784个数字被映射到一个28×28数组。

        假设我们现在使用两个5 x 5 x 3滤镜而不是一个。那么我们的输出量将是28 x 28 x 2.通过使用更多的过滤器,我们能够更好地保留空间尺寸。在数学上,这是卷积层中发生的事情。

     

    2.高层次的视角

        每个这些过滤器都可以被认为是功能标识符。当我说功能时,我正在谈论的是直线边缘,简单的颜色和曲线。想想所有图像的共同点,最简单的特点。假设我们的第一个过滤器是7 x 7 x 3并且将成为曲线检测器。(在本节中,为了简单起见,让我们忽略过滤器深度为3单位的事实,并且只考虑过滤器和图像的顶部深度切片)。作为曲线检测器,过滤器将具有像素结构,沿曲线形状的区域是更高的数值(请记住,我们正在讨论的这些滤波器只是数字!)。

     

        现在,回到数学上的可视化。当我们在输入体积的左上角有这个滤波器时,它将计算该区域的滤波器和像素值之间的乘法。现在让我们举一个想要分类的图像的例子,让我们把我们的过滤器放在左上角。

     

        我们所要做的就是将滤镜中的值与图像的原始像素值相乘。

     

        基本上,在输入图像中,如果有一个通常类似于这个滤波器所代表的曲线的形状,那么所有相乘的相加将会产生一个大的值!现在让我们看看当我们移动过滤器时会发生什么。

     

        这个conv层的输出是一个激活图。因此,在单一滤波器卷积的简单情况下(如果该滤波器是曲线检测器),激活图将显示图片中最有可能是曲线的区域。在这个例子中,我们的26 x 26 x 1激活图的左上角(26是因为7x7滤镜而不是5x5)将是6600.这个高值意味着在输入中可能有某种曲线导致过滤器激活的音量。在我们的激活地图右上角的值将是0,因为没有任何东西在输入音量导致过滤器激活(或者更简单的说,在原始图像的该区域中没有曲线)。请记住,这只是一个过滤器。信息范范范读范范范亦内范亦会信息及信息范信信息范辛辛 我们可以有其他的过滤器,用于向左弯曲或为直线边缘的线条。更多的过滤器,激活图的深度越大,我们对输入量的信息也越多。

     

        在下面的图片中,将看到一些经过训练的网络的第一个conv层过滤器的实际可视化示例。尽管如此,主要论点仍然是一样的。第一层上的过滤器在输入图像周围进行卷积,并在其正在查找的特定功能位于输入体积中时"激活"(或计算高值)。

     

    3.深入的网络

        一个经典的CNN架构看起来就像这样

     

        然而,最后一层是一个重要的层面。第一个conv层中的过滤器是用来检测的。他们检测低级功能,如边缘和曲线。正如人们所想象的,为了预测图像是否是一种对象,我们需要网络能够识别更高层次的特征,如手或爪子或耳朵。思考第一个conv层之后的网络输出结果。这将是一个28×28×3的体积(假设我们使用三个5×5×3滤波器)。当我们经过另一个conv层时,第一个conv层的输出成为第二个的输入conv层。现在,这看起来有点难以想象。当我们在谈论第一层时,输入只是原始图像。然而,当我们谈论第二层次的时候,输入是第一层产生的激活图。因此,输入的每一层都基本上描述了原始图像中某些低级特征出现的位置。现在当你在上面应用一组过滤器时(通过第二个过滤器)conv层),则输出将是代表更高级特征的激活。这些特征的类型可以是半圆(曲线和直边的组合)或正方形(几个直边的组合)。当您浏览网络并通过更多的转发层时,您将获得代表越来越复杂功能的激活地图。在网络结束时,您可能会有一些过滤器在图像中有手写时激活,过滤器在看到粉红色的物体时激活,等等。如果您想要了解关于在ConvNets中可视化过滤器的更多信息,Matt Zeiler和Rob Fergus一个很好的研究论文讨论的话题。杰森Yosinski也有一个视频在YouTube上提供了一个很好的视觉表现。另一个值得注意的事情是,当你深入到网络中时,过滤器开始具有越来越大的接受范围,这意味着他们能够从原始输入量的较大区域中考虑信息(另一种放置方式它们对像素空间的较大区域更敏感)。

     

    4.完全连接层

        这个图层基本上需要一个输入量(无论输出是在其之前的conv或ReLU还是pool层),并输出一个N维向量,其中N是程序必须从中选择的类的数量。例如,如果你想要一个数字分类程序,N将是10,因为有10个数字。这个N维向量中的每个数字表示某个类别的概率。例如,如果用于数字分类程序的结果向量是[0.1.175 0 0 0 0 0 .05],那么这代表10%的概率,即图像是1,10%的概率图像是2,图像是3的概率是75%,图像是9的概率是5%(注意:还有其他方法可以表示输出,但我只是展示了softmax方法)。完全连接图层的工作方式是查看上一层的输出(我们记得它应该代表高级特征的激活图),并确定哪些特征与特定类最相关。例如,如果程序预测某些图像是狗,则在激活图中将具有高值,例如爪子或4条腿等的高级特征。类似地,如果程序预测某图像是鸟,它将在激活地图中具有很高的价值,代表像翅膀或喙等高级特征。基本上,FC层看着什么高级特征与特定类最强关联,并具有特定的权重,以便当你计算权重与上一层之间的乘积。

     

    5.训练

        以上提到的神经网络的一个方面,它可能是最重要的部分。第一个conv层中的过滤器如何知道要查找边和曲线?完全连接的图层如何知道要查看的激活图?每层中的过滤器如何知道有什么值?计算机能够调整其过滤值(或权重)的方式是通过称为反向传播的训练过程。

        在我们进入反向传播之前,我们必须先退后一步,讨论神经网络的工作需求。现在我们都出生了,我们的思想是新鲜的。我们不知道什么是猫,狗或鸟。以类似的方式,在CNN开始之前,权重或筛选值是随机的。过滤器不知道寻找边缘和曲线。在更高层的过滤器不知道寻找爪子和喙。然而,随着年龄的增长,我们的父母和老师向我们展示了不同的图片和图片,并给了我们相应的标签。被赋予形象和标签的想法是CNN经历的培训过程。在深入研究之前,我们假设我们有一套训练集,其中包含成千上万的狗,猫和鸟的图像,每个图像都有一个这个图像是什么动物的标签。

        所以反向传播可以分为4个不同的部分,正向传递,丢失函数,反向传递和权重更新。在正向传球过程中,您将会看到一张训练图像,我们记得这是一个32 x 32 x 3的数字数组,并将其传递给整个网络。在我们的第一个训练样例中,由于所有的权值或过滤值都是随机初始化的,因此输出结果可能类似[.1.1.1.1.1.1.1.1.1.1],基本上是输出不特别优先考虑任何数字。网络以其当前的权重无法查找这些低级特征,因此无法就分类的可能性作出任何合理的结论。这转到损失功能反向传播的一部分。请记住,我们现在使用的是培训数据。这个数据有一个图像和一个标签。例如,假设输入的第一个训练图像是3,图像的标签是[0 0 0 1 0 0 0 0 0 0]。损失函数可以用许多不同的方式来定义,但常见的是MSE(均方误差),是实际预测的平方的1.5倍。

     

        假设变量L等于该值。正如你可以想象的那样,第一对训练图像的损失将非常高。现在,让我们直观地思考这个问题。我们希望达到预测的标签(ConvNet的输出)与训练标签相同的点(这意味着我们的网络得到了预测权)。为了达到这个目的,我们希望最小化损失量我们有。将这看作是微积分中的一个优化问题,我们想要找出哪些输入(权重在我们的情况下)是最直接导致网络损失(或错误)的因素。

     

        这是dL / dW的数学等价物,其中W是特定层的权重。现在,我们要做的是通过网络进行反向传递,即确定哪些权重对损失贡献最大,并设法调整损失,从而减少损失。一旦我们计算出这个导数,我们就会进入权重更新的最后一步。这是我们取得所有过滤器的权重,并更新它们,使它们在梯度的相反方向变化。

       

        该学习速率是由程序员选择的参数。高学习率意味着在权重更新中采取更大的步骤,因此,模型可能花费较少的时间来收敛于最优权重集合。但是,如果学习速度过高,可能会导致跳跃过大,不够精确,无法达到最佳点。

        正向传递,丢失函数,反向传递和参数更新的过程是一次训练迭代。程序将重复这个过程,对每组训练图像(通常称为批次)进行固定次数的迭代。一旦你完成了最后一个训练样例的参数更新,希望网络应该被训练得足够好,这样层的权重才能被正确地调整。

     

     

    三.实现代码

    (一)神经网络实战——手写数字识别

        首先,我们需要把MNIST数据集处理为神经网络能够接受的形式。MNIST训练集的文件格式可以参考官方网站,这里不在赘述。每个训练样本是一个28*28的图像,我们按照行优先,把它转化为一个784维的向量。每个标签是0-9的值,我们将其转换为一个10维的one-hot向量:如果标签值为n,我们就把向量的第n维(从0开始编号)设置为0.9,而其它维设置为0.1。例如,向量[0.1,0.1,0.9,0.1,0.1,0.1,0.1,0.1,0.1,0.1]表示值2。

     

    类:FullConnectedLayer,它实现了全连接层的前向和后向计算:

    1.from functools import reduce   2.   3.import numpy as np   4.   5.from activators import SigmoidActivator   6.   7.   8.# 全连接层实现类   9.class FullConnectedLayer(object):   10.    def __init__(self, input_size, output_size,   11.                 activator):   12.        '''''  13.        构造函数  14.        input_size: 本层输入向量的维度  15.        output_size: 本层输出向量的维度  16.        activator: 激活函数  17.        '''   18.        self.input_size = input_size   19.        self.output_size = output_size   20.        self.activator = activator   21.        # 权重数组W   22.        self.W = np.random.uniform(-0.1, 0.1,   23.                                   (output_size, input_size))   24.        # 偏置项b   25.        self.b = np.zeros((output_size, 1))   26.        # 输出向量   27.        self.output = np.zeros((output_size, 1))   28.   29.    def forward(self, input_array):   30.        '''''  31.        前向计算  32.        input_array: 输入向量,维度必须等于input_size  33.        '''   34.        # 式2   35.        self.input = input_array   36.        self.output = self.activator.forward(   37.            np.dot(self.W, input_array) + self.b)   38.   39.    def backward(self, delta_array):   40.        '''''  41.        反向计算W和b的梯度  42.        delta_array: 从上一层传递过来的误差项  43.        '''   44.        # 式8   45.        self.delta = self.activator.backward(self.input) * np.dot(   46.            self.W.T, delta_array)   47.        self.W_grad = np.dot(delta_array, self.input.T)   48.        self.b_grad = delta_array   49.   50.    def update(self, learning_rate):   51.        '''''  52.        使用梯度下降算法更新权重  53.        '''   54.        self.W += learning_rate * self.W_grad   55.        self.b += learning_rate * self.b_grad   56.   57.    def dump(self):   58.        print('W: %s\nb:%s' % (self.W, self.b))  

     

    Network类稍作修改,使之用到FullConnectedLayer:

    1.# 神经网络类   2.class Network(object):   3.    def __init__(self, layers):   4.        '''''  5.        构造函数  6.        '''   7.        self.layers = []   8.        for i in range(len(layers) - 1):   9.            self.layers.append(   10.                FullConnectedLayer(   11.                    layers[i], layers[i + 1],   12.                    SigmoidActivator()   13.                )   14.            )   15.   16.    def predict(self, sample):   17.        '''''  18.        使用神经网络实现预测  19.        sample: 输入样本  20.        '''   21.        output = sample   22.        for layer in self.layers:   23.            layer.forward(output)   24.            output = layer.output   25.        return output   26.   27.    def train(self, labels, data_set, rate, epoch):   28.        '''''  29.        训练函数  30.        labels: 样本标签  31.        data_set: 输入样本  32.        rate: 学习速率  33.        epoch: 训练轮数  34.        '''   35.        for i in range(epoch):   36.            for d in range(len(data_set)):   37.                self.train_one_sample(labels[d],   38.                                      data_set[d], rate)   39.   40.    def train_one_sample(self, label, sample, rate):   41.        self.predict(sample)   42.        self.calc_gradient(label)   43.        self.update_weight(rate)   44.   45.    def calc_gradient(self, label):   46.        delta = self.layers[-1].activator.backward(   47.            self.layers[-1].output   48.        ) * (label - self.layers[-1].output)   49.        for layer in self.layers[::-1]:   50.            layer.backward(delta)   51.            delta = layer.delta   52.        return delta   53.   54.    def update_weight(self, rate):   55.        for layer in self.layers:   56.            layer.update(rate)   57.   58.    def dump(self):   59.        for layer in self.layers:   60.            layer.dump()   61.   62.    def loss(self, output, label):   63.        return 0.5 * ((label - output) * (label - output)).sum()   64.   65.    def gradient_check(self, sample_feature, sample_label):   66.        '''''  67.        梯度检查  68.        network: 神经网络对象  69.        sample_feature: 样本的特征  70.        sample_label: 样本的标签  71.        '''   72.   73.        # 获取网络在当前样本下每个连接的梯度   74.        self.predict(sample_feature)   75.        self.calc_gradient(sample_label)   76.   77.        # 检查梯度   78.        epsilon = 10e-4   79.        for fc in self.layers:   80.            for i in range(fc.W.shape[0]):   81.                for j in range(fc.W.shape[1]):   82.                    fc.W[i, j] += epsilon   83.                    output = self.predict(sample_feature)   84.                    err1 = self.loss(sample_label, output)   85.                    fc.W[i, j] -= 2 * epsilon   86.                    output = self.predict(sample_feature)   87.                    err2 = self.loss(sample_label, output)   88.                    expect_grad = (err1 - err2) / (2 * epsilon)   89.                    fc.W[i, j] += epsilon   90.                    print('weights(%d,%d): expected - actural %.4e - %.4e' % (                           i, j, expect_grad, fc.W_grad[i, j])) 

     

    (二)图像MNIST手写数字识别测试

    首先下载mnist datasets数据集

    #download mnist datasets

    #55000 * 28 * 28 55000image

    1.import numpy as np   2.import tensorflow as tf   3.   4.from tensorflow.examples.tutorials.mnist import input_data   5.mnist=input_data.read_data_sets('mnist_data',one_hot=True)#参数一:文件目录。参数二:是否为one_hot向量  

     

    第一维度均一化

    1.#one_hot is encoding format   2.#None means tensor 的第一维度可以是任意维度   3.#/255. 做均一化   4.input_x=tf.placeholder(tf.float32,[None,28*28])/255.   5.#输出是一个one hot的向量   6.output_y=tf.placeholder(tf.int32,[None,10])   7.   8.#输入层 [28*28*1]   9.input_x_images=tf.reshape(input_x,[-1,28,28,1])   10.#从(Test)数据集中选取3000个手写数字的图片和对应标签   11.   12.test_x=mnist.test.images[:3000] #image   13.test_y=mnist.test.labels[:3000] #label 

     

    #隐藏层

    #conv1 5*5*32

    #layers.conv2d parameters

    #inputs 输入,是一个张量

    #filters 卷积核个数,也就是卷积层的厚度

    #kernel_size 卷积核的尺寸

    #strides: 扫描步长

    #padding: 边边补0 valid不需要补0,same需要补0,为了保证输入输出的尺寸一致,补多少不需要知道

    #activation: 激活函数

    1.conv1=tf.layers.conv2d(   2.    inputs=input_x_images,   3.    filters=32,   4.    kernel_size=[5,5],   5.    strides=1,   6.    padding='same',   7.    activation=tf.nn.relu)   8.print(conv1)  

     

    #pooling layer1 2*2

    #tf.layers.max_pooling2d

    #inputs 输入,张量必须要有四个维度

    #pool_size: 过滤器的尺寸

    1.pool1=tf.layers.max_pooling2d(   2.    inputs=conv1,   3.    pool_size=[2,2],   4.    strides=2)   5.print(pool1)  

     

    flat(平坦化)

    1.conv2=tf.layers.conv2d(   2.    inputs=pool1,   3.    filters=64,   4.    kernel_size=[5,5],   5.    strides=1,   6.    padding='same',   7.    activation=tf.nn.relu   8.)   9.   10.pool2=tf.layers.max_pooling2d(   11.    inputs=conv2,   12.    pool_size=[2,2],   13.    strides=2   14.)   15.   16.flat=tf.reshape(pool2,[-1,7*7*64]) 

     

    #densely-connected layers 全连接层 1024

    #tf.layers.dense

    #inputs: 张量

    #units: 神经元的个数

    #activation: 激活函数

    1.dense=tf.layers.dense(   2.    inputs=flat,   3.    units=1024,   4.    activation=tf.nn.relu   5.)   6.print(dense)  

     

    #dropout

    #tf.layers.dropout

    #inputs 张量

    #rate 丢弃率

    #training 是否是在训练的时候丢弃

    #输出层,不用激活函数(本质就是一个全连接层)

    1.dropout=tf.layers.dropout(   2.    inputs=dense,   3.    rate=0.5,   4.)   5.print(dropout)   6.   7.logits=tf.layers.dense(   8.    inputs=dropout,   9.    units=10   10.)   11.print(logits) 

     

    #计算误差 cross entropy(交叉熵),再用Softmax计算百分比的概率

    #tf.losses.softmax_cross_entropy

    #onehot_labels: 标签值

    #logits: 神经网络的输出值

    # 用Adam 优化器来最小化误差,学习率0.001 类似梯度下降

    #精度。计算预测值和实际标签的匹配程度

    #tf.metrics.accuracy

    #labels:真实标签

    #predictions: 预测值

    #Return: (accuracy,update_op)accuracy 是一个张量准确率,update_op 是一个op可以求出精度。

    1.loss=tf.losses.softmax_cross_entropy(onehot_labels=output_y,   2.                                     logits=logits)   3.print(loss)   4.   5.train_op=tf.train.GradientDescentOptimizer(learning_rate=0.001).minimize(loss)   6.   7.accuracy_op=tf.metrics.accuracy(   8.    labels=tf.argmax(output_y,axis=1),   9.    predictions=tf.argmax(logits,axis=1)   10.)  

     

    #创建会话

    #初始化变量

    #group 把很多个操作弄成一个组

    #初始化变量,全局,和局部

    1.sess=tf.Session()   2.init=tf.group(tf.global_variables_initializer(),   3.              tf.local_variables_initializer())   4.sess.run(init)   5.   6.for i in range(1000):   7.    batch=mnist.train.next_batch(50) #从Train(训练)数据集中取‘下一个’样本   8.    train_loss,train_op_=sess.run([loss,train_op],{input_x:batch[0],output_y:batch[1]})   9.    if i0==0:   10.        test_accuracy=sess.run(accuracy_op,{input_x:test_x,output_y:test_y})   11.        print("Step=%d, Train loss=%.4f,[Test accuracy=%.2f]"%(i,train_loss,test_accuracy))    

     

    #测试: 打印20个预测值和真实值

    1.test_output=sess.run(logits,{input_x:test_x[:20]})   2.inferenced_y=np.argmax(test_output,1)   3.print(inferenced_y,'Inferenced numbers')#推测的数字   4.print(np.argmax(test_y[:20],1),'Real numbers')   5.sess.close() 

     

    四.实验

    (一)神经网络实战——手写数字识别

        我们每训练10轮,评估一次准确率。当准确率开始下降时(出现了过拟合)终止训练。结果如下:

     

    (二)图像MNIST手写数字识别测试

        为了看看我们的CNN是否有效,我们有一套不同的图像和标签(在训练和测试之间不能一蹴而就),并通过CNN传递图像。我们将输出与实际情况进行比较,看看我们的网络是否正常工作!

        当range在1000时,测试结果如下:

     

        当range在3000时,测试结果如下:

    由此可见,进行的培训迭代次数越多,可以进行的权重更新越多,调整到网络的时间越长,则预测值和真实值的结果更为接近。

     

     

    五.总结和展望

        数据,数据,数据。为网络提供的训练数据越多,可以进行的培训迭代次数越多,可以进行的权重更新越多,调整到网络的时间越长。Facebook(和Instagram)可以使用目前拥有的十亿用户的所有照片,Pinterest可以使用其网站上500亿个引脚的信息,Google可以使用搜索数据,Amazon可以使用数百万个产品每天都买。

        在这中没有讨论的东西包括非线性和合并层以及网络的超参数,如过滤器大小,步长和填充。还没有讨论网络架构,批量归一化,消失梯度,丢失,初始化技术,非凸优化,偏差,丢失函数的选择,数据增强,正则化方法,计算考虑,反向传播的修改等主题)。

     

     

    参考文献:

    [1]Tom M. Mitchell, "机器学习", 曾华军等译, 机械工业出版社

    [2]CS 224N / Ling 284, Neural Networks for Named Entity Recognition

    [3]LeCun et al. Gradient-Based Learning Applied to Document Recognition 1998

    [4]RECURRENT NEURAL NETWORKS TUTORIAL

    [5]Understanding LSTM Networks

    [6]The Unreasonable Effectiveness of Recurrent Neural Networks

    [7]Attention and Augmented Recurrent Neural Networks

    [8]On the difficulty of training recurrent neural networks, Bengio et al.

    [9]Recurrent neural network based language model, Mikolov et al.

    [10]Neural Network Classification, Categorical Data, Softmax Activation, and Cross Entropy Error, McCaffrey

    [11]CS231n Convolutional Neural Networks for Visual Recognition

    [12]ReLu (Rectified Linear Units) 激活函数

    [13]Jake Bouvrie, Notes on Convolutional Neural Networks, 2006

    Processed: 0.014, SQL: 9