摘要: 在本文中,我想带领你们看一看最近在Keras中实现的体系结构中一系列重要的卷积组块。
做为一名计算机科学家,我常常在翻阅科学技术资料或者公式的数学符号时碰壁。我发现经过简单的代码来理解要容易得多。所以,在本文中,我想带领你们看一看最近在Keras中实现的体系结构中一系列重要的卷积组块。git
当你在GitHub网站上寻找经常使用架构实现时,必定会对它们里面的代码量感到惊讶。若是标有足够多的注释并使用额外的参数来改善模型,将会是一个很是好的作法,但与此同时这也会分散体系结构的本质。为了进一步简化和缩短代码,我将使用一些别名函数:github
defconv(x, f, k=3, s=1, p='same', d=1, a='relu'): return Conv2D(filters=f, kernel_size=k, strides=s, padding=p, dilation_rate=d, activation=a)(x) def dense(x, f, a='relu'): return Dense(f, activation=a)(x) defmaxpool(x, k=2, s=2, p='same'): return MaxPooling2D(pool_size=k, strides=s, padding=p)(x) defavgpool(x, k=2, s=2, p='same'): return AveragePooling2D(pool_size=k, strides=s, padding=p)(x) defgavgpool(x): return GlobalAveragePooling2D()(x) defsepconv(x, f, k=3, s=1, p='same', d=1, a='relu'): return SeparableConv2D(filters=f, kernel_size=k, strides=s, padding=p, dilation_rate=d, activation=a)(x)
在删除模板代码以后的代码更易读。固然,这只有在你理解个人首字母缩写后才有效。算法
defconv(x, f, k=3, s=1, p='same', d=1, a='relu'): return Conv2D(filters=f, kernel_size=k, strides=s, padding=p, dilation_rate=d, activation=a)(x) def dense(x, f, a='relu'): return Dense(f, activation=a)(x) defmaxpool(x, k=2, s=2, p='same'): return MaxPooling2D(pool_size=k, strides=s, padding=p)(x) defavgpool(x, k=2, s=2, p='same'): return AveragePooling2D(pool_size=k, strides=s, padding=p)(x) defgavgpool(x): return GlobalAveragePooling2D()(x) defsepconv(x, f, k=3, s=1, p='same', d=1, a='relu'): return SeparableConv2D(filters=f, kernel_size=k, strides=s, padding=p, dilation_rate=d, activation=a)(x)
一个卷积层的参数数量取决于卷积核的大小、输入过滤器的数量和输出过滤器的数量。你的网络越宽,3x3卷积耗费的成本就越大。网络
def bottleneck(x, f=32, r=4): x = conv(x, f//r, k=1) x = conv(x, f//r, k=3) return conv(x, f, k=1)
瓶颈组块背后的思想是,使用一个低成本的1x1卷积,按照必定比率r将通道的数量下降,以便随后的3x3卷积具备更少的参数。最后,咱们用另一个1x1的卷积来拓宽网络。架构
模块提出了经过并行的方式使用不一样的操做而且合并结果的思想。经过这种方式网络能够学习不一样类型的过滤器。app
defnaive_inception_module(x, f=32): a = conv(x, f, k=1) b = conv(x, f, k=3) c = conv(x, f, k=5) d = maxpool(x, k=3, s=1) return concatenate([a, b, c, d])
在这里,咱们将使用卷积核大小分别为一、3和5的卷积层与一个MaxPooling层进行合并。这段代码显示了Inception模块的原始实现。实际的实现结合了上述的瓶颈组块思想,这使它稍微的复杂了一些。ide
definception_module(x, f=32, r=4): a = conv(x, f, k=1) b = conv(x, f//3, k=1) b = conv(b, f, k=3) c = conv(x, f//r, k=1) c = conv(c, f, k=5) d = maxpool(x, k=3, s=1) d = conv(d, f, k=1) return concatenate([a, b, c, d])
ResNet是由微软的研究人员提出的一种体系结构,它容许神经网络具备任意多的层数,同时还提升了模型的准确度。如今你可能已经习惯使用它了,但在ResNet以前,状况并不是如此。函数
defresidual_block(x, f=32, r=4): m = conv(x, f//r, k=1) m = conv(m, f//r, k=3) m = conv(m, f, k=1) return add([x, m])
ResNet的思路是将初始的激活添加到卷积组块的输出结果中。利用这种方式,网络能够经过学习过程决定用于输出的新卷积的数量。值得注意的是,Inception模块链接这些输出,而剩余组块是用于求和。性能
根据它的名称,你能够猜到ResNeXt与ResNet是密切相关的。做者们将术语“基数(cardinality)”引入到卷积组块中,做为另外一个维度,如宽度(通道数量)和深度(网络层数)。学习
基数是指在组块中出现的并行路径的数量。这听起来相似于以并行的方式出现的4个操做为特征的Inception模块。然而,基数4不是指的是并行使用不一样类型的操做,而是简单地使用相同的操做4次。
它们作的是一样的事情,那么为何你还要把它们并行放在一块儿呢?这个问题问得好。这个概念也被称为分组卷积,能够追溯到最先的AlexNet论文。尽管当时它主要用于将训练过程划分到多个GPU上,而ResNeXt则使用ResNeXt来提升参数的效率。
defresnext_block(x, f=32, r=2, c=4): l = [] for i in range(c): m = conv(x, f//(c*r), k=1) m = conv(m, f//(c*r), k=3) m = conv(m, f, k=1) l.append(m) m = add(l) return add([x, m])
这个想法是把全部的输入通道分红一些组。卷积将只会在其专用的通道组内进行操做,而不会影响其它的。结果发现,每组在提升权重效率的同时,将会学习不一样类型的特征。
想象一个瓶颈组块,它首先使用一个为4的压缩率将256个输入通道减小到64个,而后将它们再恢复到256个通道做为输出。若是想引入为32的基数和2的压缩率,那么咱们将使用并行的32个1x1的卷积层,而且每一个卷积层的输出通道是4(256/(32*2))个。随后,咱们将使用32个具备4个输出通道的3x3的卷积层,而后是32个1x1的卷积层,每一个层则有256个输出通道。最后一步包括添加这32条并行路径,在为了建立剩余链接而添加初始输入以前,这些路径会为咱们提供一个输出。
这有很多的东西须要消化。用上图能够很是直观地了解都发生了什么,而且能够经过复制这些代码在Keras中本身建立一个小型网络。利用上面9行简单的代码能够归纳出这些复杂的描述,这难道不是很好吗?
顺便提一下,若是基数与通道的数量相同,咱们就会获得一个叫作深度可分卷积(depthwise separable convolution)的东西。自从引入了Xception体系结构以来,这些技术获得了普遍的应用。
密集组块是剩余组块的极端版本,其中每一个卷积层得到组块中以前全部卷积层的输出。咱们将输入激活添加到一个列表中,而后输入一个能够遍历块深度的循环。每一个卷积输出还会链接到这个列表,以便后续迭代得到愈来愈多的输入特征映射。这个方案直到达到了所须要的深度才会中止。
defdense_block(x, f=32, d=5): l = x for i in range(d): x = conv(l, f) l = concatenate([l, x]) return l
尽管须要数月的研究才能获得一个像DenseNet这样出色的体系结构,可是实际的构建组块其实就这么简单。
SENet曾经在短时间内表明着ImageNet的较高水平。它是创建在ResNext的基础之上的,主要针对网络通道信息的建模。在常规的卷积层中,每一个通道对于点积计算中的加法操做具备相同的权重。
SENet引入了一个很是简单的模块,能够添加到任何现有的体系结构中。它建立了一个微型神经网络,学习如何根据输入对每一个过滤器进行加权。正如你看到的那样,SENet自己不是一个卷积组块,可是由于它能够被添加到任何卷积组块中,而且可能会提升它的性能,所以我想将它添加到混合体中。
defse_block(x, f, rate=16): m = gavgpool(x) m = dense(m, f // rate) m = dense(m, f, a='sigmoid') return multiply([x, m])
每一个通道被压缩为一个单值,并被馈送到一个两层的神经网络里。根据通道的分布状况,这个网络将根据通道的重要性来学习对其进行加权。最后,再用这个权重跟卷积激活相乘。
SENets只用了很小的计算开销,同时还可能会改进卷积模型。在我看来,这个组块并无获得应有的重视。
这就是事情变得丑陋的地方。咱们正在远离人们提出的简捷而有效的设计决策的空间,并进入了一个设计神经网络体系结构的算法世界。NASNet在设计理念上是使人难以置信的,但实际的体系结构是比较复杂的。咱们所了解的是,它在ImageNet上表现的很优秀。
经过人工操做,做者们定义了一个不一样类型的卷积层和池化层的搜索空间,每一个层都具备不一样的可能性设置。他们还定义了如何以并行的方式、顺序地排列这些层,以及这些层是如何被添加的或链接的。一旦定义完成,他们会创建一个基于递归神经网络的强化学习(Reinforcement Learning,RL)算法,若是一个特定的设计方案在CIFAR-10数据集上表现良好,就会获得相应的奖励。
最终的体系结构不只在CIFAR-10上表现良好,并且在ImageNet上也得到了至关不错的结果。NASNet是由一个标准单元(Normal Cell)和一个依次重复的还原单元(Reduction Cell)组成。
defnormal_cell(x1, x2, f=32): a1 = sepconv(x1, f, k=3) a2 = sepconv(x1, f, k=5) a = add([a1, a2]) b1 = avgpool(x1, k=3, s=1) b2 = avgpool(x1, k=3, s=1) b = add([b1, b2]) c2 = avgpool(x2, k=3, s=1) c = add([x1, c2]) d1 = sepconv(x2, f, k=5) d2 = sepconv(x1, f, k=3) d = add([d1, d2]) e2 = sepconv(x2, f, k=3) e = add([x2, e2]) return concatenate([a, b, c, d, e])
这就是如何在Keras中实现一个标准单元的方法。除了这些层和设置结合的很是有效以外,就没有什么新的东西了。
到如今为止,你已经了解了瓶颈组块和可分离卷积。如今就把它们放在一块儿。若是你作一些测试,就会注意到,由于可分离卷积已经减小了参数的数量,所以进行压缩可能会损害性能,而不是提升性能。
做者们提出了与瓶颈组块和剩余组块相反的想法。他们使用低成本的1x1卷积来增长通道的数量,由于随后的可分离卷积层已经大大减小了参数的数量。在把通道添加到初始激活以前,下降了通道的数量。
definv_residual_block(x, f=32, r=4): m = conv(x, f*r, k=1) m = sepconv(m, f, a='linear') return add([m, x])
问题的最后一部分是在可分离卷积以后没有激活函数。相反,它直接被添加到了输入中。这个组块被证实当被放到一个体系结构中的时候是很是有效的。
利用AmoebaNet,咱们在ImageNet上达到了当前的最高水平,而且有可能在通常的图像识别中也是如此。与NASNet相似,AmoebaNet是经过使用与前面相同的搜索空间的算法设计的。惟一的纠结是,他们放弃了强化学习算法,而是采用了一般被称为“进化”的遗传算法。可是,深刻了解其工做方式的细节超出了本文的范畴。故事的结局是,经过进化,做者们可以找到一个比NASNet的计算成本更低的更好的解决方案。这在ImageNet-A上得到了名列前五的97.87%的准确率,也是第一次针对单个体系结构的。
我但愿本文能让你对这些比较重要的卷积组块有一个深入的理解,而且可以认识到实现起来可能比想象的要容易。要进一步了解这些体系结构,请查看相关的论文。你会发现,一旦掌握了一篇论文的核心思想,就会更容易理解其他的部分了。另外,在实际的实现过程当中一般将批量规范化添加到混合层中,而且随着激活函数应用的的地方会有所变化。
本文为云栖社区原创内容,未经容许不得转载。