最近由于在准备论文的相关事宜,导致博客的更新速度有点缓慢,望大家见谅。不过该更新还是要更新的,所以今天我就挤出一点时间来更新一篇。由于之前的博文已经将tiny_cnn中相关的网络层结构介绍的差不多,接下来的博文中着重介绍卷积神经网络的训练流程和测试流程,重点就是前向传播算法和反向传播算法。不过我在研究CNN前向传播算法的流程时,发现作者在前向传播算法的调用过程中,很好的体现了C++的多态性特点,考虑到多态性是各个招聘单位面试时饱受青睐的一道考题,于是决定单独拿出一篇博文来从tiny_cnn的角度介绍作者是如何运用C++的多态性的。
一、多态性
关于C++的多态性归类问题网上的观点也是参差不齐。稍稍查了一下资料,有人热衷于将多态性分为两类:静多态和动多态。所谓静多态即是通过模板和重载实现,在程序的编译阶段即完成模板或重载函数的实参演绎,故称之为静多态。而动多态性则是通过继承和虚函数来实现,即程序在编译阶段只负责检查语法,并不能确定在运行的时候会具体实例化哪些函数对象,可能会实例化基类的虚函数,也可能会实例化各个子类中间相应的对虚函数进行重载之后的成员函数,这就是所谓的动多态。从这个角度看的话,和C++中的静态联编以及动态联编的概念比较相近。当然也有的人认为C++的多态性默认就是动多态,也就是说多态性只能通过继承和虚函数来实现。当然我们这里的重点不是争论谁对谁错,而是分析tiny_cnn中是如何实现多态性的,或者说是动多态。
二、layers类容器结构
tiny_cnn之所以能够表现出明显的实例多态性,主要是得益于layers这个类容器结构。这个容器中保存了组成tiny_cnn网络模型的各种网络结构:
从上图中可以看出layers容器中保存的各个层结构通过指向layer_base类型的指针next_和prev_相互联系,并且通过head()成员函数返回第一个元素(Input_Layer),通过tail()成员函数返回最后一个元素(fully_connected_layer)。
三、前向传播函数forward_propagation()的继承体系
在分析forward_propagation()函数的调用以及实例化过程之前,首先需要明确都有哪些类定义了forward_propagation()或者其虚函数的形式,这里给出forward_propagation函数的继承体系结构图:
首先,在基类layer_base中,定义了forward_propagation()的纯虚函数形式,供各个子类进行实例化:
然后在各个子类中对其进行了重写(注意,纯虚函数必须在子类中进行重写),在input_layer中:
可见在input_layer层中的前向传播算法只是起到一个“调用下一层前向传播算法”的作用,并未做实质性卷积运算。接下来是convolutional_layer和average_pooling_layer层的前向传播算法。由于这两层的前向传播操作相同,因此作者选择将其对应的forward_propagation函数封装在他们公共的基类partial_connected_layer中:
可见卷积层和下采样层的前向传播函数承担了真正的前向传播任务,包括与映射核进行卷积、添加加性偏执、经过激活函数处理等等。接下来是全连接层中的forward_propagation函数:
全连接层是卷积神经网络的最后一层,前向传播到此结束。
四、实例化流程
在完成以上继承结构的分析之后,接下里通过程序的实际运行来观察各个前向传播函数的实例化过程,进一步体会在程序运行中所体现出的多态性。首先需要在上面三处forward_propagation函数定义中加上断点,以便观察函数的实例化先后顺序。F5调试运行程序,发现程序首先命中Input_Layer中的forward_propagation函数,即先实例化Input_Layer中的函数对象:
这是由于在训练函数中,调用了fprop函数作为前向传播算法的启动开关,而启动时以layers_.head()为入口,.head()函数返回的是指向layers容器中第一个元素的layer_base类型指针,即Input_Layer,因此程序选择先实例化Input_Layer类中对应的forward_propagation函数。继续F5运行,命中partial_connected_layer类中的forward_propagation函数:
原因很明确,Input_Layer中的next_指针指向了layers容器中的下一个层结构,也就是卷积层,但编译器发现卷积层类中并没有实例化forward_propagation函数,因此继续搜索其基类,于是命中并实例化了partial_connected_layer类中的forward_propagation函数。接下里连按若干次F5继续调试,发现程序会连续命中partial_connected_layer的断点,这当然不是程序BUG了,而是因为接下来的几层均为卷积层或下采样层,而这两层中都没有实例化forward_propagation函数,因此程序都是毫不例外的选择实例化其基类中的重载虚函数。按到第4次或者第5次F5的时候(视程序的结构而定),命中全连接层fully_connected_layer中的断点,说明该轮到实例化fully_connected_layer类对应的forward_propagation函数了:
对整个网络模型中的forward_propagation函数都完成实例化之后,程序将采用递归的方式返回。
五、总结
在这个实例化过程中,已经充分体现出了程序在执行过程中所表现出的多态性。通俗的讲,就是需要实例化那个子类层中对应的虚函数了,就去实例化那个虚函数,而这些在编译期肯定是无法确定的,编译期间编译器只能检查语法错误,但并不能知道用户是怎样安排网络结构,换句话说,卷积层之后一定就是均值下采样层吗?不一定,还可能是最大值下采样层。不过这种多态性依赖严谨的继承体系,并且指向各个层的指针必须是基类类型的,也就是layer_base*类型。