65.9K
CodeProject 正在变化。 阅读更多。
Home

直方图神经元 - 解决 CNN 图像分类中的“小猫炖菜”问题

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2017年12月28日

CPOL

17分钟阅读

viewsIcon

7105

我用来让 CNN 以更直观的方式运行的方法

引言

尽管深度学习/卷积神经网络(DL/CNN)在图像分类方面已经取得了极高的准确率,但其背后的一些运作机制仍然让我感到疑惑。 

其中一个让我觉得“开玩笑呢”的情况,就是我称之为“小猫炖菜”的问题。本文将介绍我用来让CNN在这种情况下表现得更符合直觉、更理智的方法。

问题

如果你运行一个用于图像分类的CNN,并对最大化激活各种特征图的图像进行反卷积,你会发现有些图像符合直觉……而有些则不然。

最显而易见的符合直觉的是“早期”的特征,靠近输入层——类似Gabor滤波器及其变体。这很有道理:检测边缘和纹理是基础,而这些特征作为更抽象特征的构建块也是合乎逻辑的。但是,越靠近分类头,结果就越……奇怪。例如,为了识别一只小猫,最大化激活“小猫”的合成图像看起来并不像一只小猫。它们有很多小猫似的纹理。它们散布着一些易于识别的小猫的眼睛。它们零星地分布着一些容易辨认的小猫的鼻子。它们甚至可能随机地混合了一些小猫的头、爪子和尾巴。但它们根本不像一只小猫。它们看起来像小猫炖菜。

这意味着,即使网络整体准确率非常高,在某个深层水平上,CNN并没有真正识别出小猫。在某种概念上,CNN看到图像并说:“嗯……我们看到了鼻子,还有爪子,还有一些眼睛,还有一些尾巴,还有一大堆毛……我敢打赌这来自一只小猫!” 

这在我看来是根本错误的。

事实证明,这个问题也会对最终的分类结果产生影响。对抗性CNN就证明了这一点,它们可以轻易地微调几乎任何图像,使得人类说“那是一辆翻斗车!”(或者图像本来的样子),但CNN却说“小猫!”——这是因为CNN关注的特征实际上并不是小猫的本质。

另一个我看到分类CNN出现问题的例子是,当图像呈现一个大致呈六边形排列的暗点模式,背景为浅色时——如果这些点的尺寸、行内间距和行间距都恰到好处,CNN就会确信这是只狗——看,那都是狗的眼睛和鼻子!

这意味着CNN有一些根本性的东西我们可以做得更好。

我们如何解决这个问题?

我们从倾听孩子们是如何学习的开始。对于一张猫的图片,我们不会说“仔细看看它的毛。你看,狗的毛会稍微粗糙一点。还有,看看身体的边缘,你能看到猫的毛是如何在背景中突出的。这和狗的毛有点不同。”不。我们会说“那里是猫咪的两个耳朵。那里是它的眼睛(一只,两只),这是它的嘴巴,这是它的尾巴。”这样教学的方式是有效的。

这说明CNN缺少的一点就是……计数。一只猫有一个鼻子,两只眼睛,一条尾巴。如果你看不到其中一些,没关系,但识别的信心应该会稍微减弱——也许猫的头转过去了,或者它坐在尾巴上,之类的。但如果你什么都看不到,那么识别的信心可能应该很弱。如果你看到了太多——比如3只眼睛——那它就不是一只猫。也许是两只猫。也许是别的什么。但CNN不应该对图像有多么像猫变得过度兴奋,无论它有多少猫毛可见,如果它有3只眼睛的话。 

“计算特征”是什么意思?

在CNN中,“计算特征”究竟意味着什么?肯定不是指激活的“神经元”数量,这取决于输入分辨率和我们池化层的侵略性。经过一番思考,我提出了这个:

一个特征图中的“特征数量”,是指激活值超过某个阈值的连续神经元区域的数量。

示例 1

000000000000000000
000111000000001110
000110000000111100
000011110000001100
000000000000000000
000000000000000110
000000000000001110

如果网格代表超过某个阈值的特征图中的神经元激活值,那么特征的数量就是3:它有3个连续的1区域。

如果特征图代表的是比如幼犬的爪子,那么知道有三个似乎非常有用的信息。

我们如何让CNN计数?

计算机图形学提供了几种标准的算法,可以做到我们想要的,例如“洪水填充”算法的变体。所以计算特征并不难。然而,如果我们把这些算法集成到深度学习网络中,结果会……糟糕透顶。悲剧性的。灾难性的。因为所有这些算法都是为没有GPU的旧式迭代、顺序编程世界设计的。而且我发现它们(我能找到的所有算法)都会让并行化的深度学习网络几乎瘫痪。

如何更快地计数?

在更快地计数特征的道路上,关键的认识是,在分析三维世界的二维图像时,低估是固有的,而且没关系。我们经常看到只有一只眼睛可见的猫,但我们仍然能认出它们是猫。我们期望CNN在进行图像分类时也能具备同样的能力。所以,如果我们的算法低估了某个特征——比如可见的有4只爪子,但我们只数了3只或2只——那也没关系。但我们不希望计数是0(知道有爪子存在很有用!),我们绝对不希望高估(有超过4只爪子的东西不是猫!),而且我们希望计数尽可能准确(因为准确性总是有益的)。

鉴于准确性是可取的,但低估是可以接受的限制,我们可以创建一个这样的矩阵中心程序: 

  1. 给定一个HxW的激活图A,表示某个特征,近似的1表示激活值高于某个水平(我称之为“近似1”,因为这些数字将是挤压函数的结果,不会精确为1)。
  2. 计算C=AxB,其中B是一个W个1的列向量,得到一个H个值的列向量C。
  3. 将C中的值挤压到0-1。
  4. 在C的两端各填充一个0,然后沿着C运行一个2x1的卷积,卷积配置为检测从~0到~1的过渡。(这只是一个边缘检测器)。这将产生一个在检测到边缘的地方显示~1的列向量。
  5. 对从4中得到的列向量求和。这就是包含该特征的连续行序列的数量。

以第一个示例为例,这个过程得到的结果是“2”,因为第1-3行序列包含一些1,第5-6行序列包含一些1。

嗯,这个“2”给了我们一些关于特征数量的见解,但是a)它是错误的(实际特征数量是3),b)很容易设计输入,使得这个过程得到的特征数量会**严重**错误。例如:

示例 2

0000000000000000000
0011100000000110000
0011001100000110000
0001101101100110001
0000000101100000100
0000000001110001100
0000000001110000000

实际特征数量:6

我们过程得到的特征数量:1

哎哟。

但我们不是完全抛弃这个方法,而是先观察到,我们使用的过程对于以张量为中心的计算框架来说计算速度非常快:整个过程都以深度学习框架喜欢的方式表达。

所以,让我们看看是否有办法在此基础上进行改进。

有,如果我们使用一点比喻:想象网格是一张桌子。0是平坦的桌面,1的区域是放在桌子上的物体。想象我们蹲在桌子的一端,这样我们就能沿着它的长度方向看。另一端有一盏灯,所以我们只能看到轮廓上的物体。在示例1中,我们数了2个突出的物体,尽管实际上是3个。

直观上,我们知道这表示桌子上**至少**有两个物体,但可能更多。我们如何得知?通过改变我们的视角:绕着桌子的边缘移动,仍然与桌面齐平,看看是否能找到一个角度,从那个角度可以看到更多的物体——也就是说,从那个位置可以看到更多的轮廓缝隙。 

很明显,我们可以实现第二个视角。像这样: 

  1. 转置矩阵。
  2. 再次运行上述过程。
  3. 现在我们得到了包含1的列序列的数量,而不是行序列的数量。这相当于从侧面看桌子,而不是从头部或脚部看。

对于示例1,我们这个过程得到的结果仍然是错误的(2而不是3),但对于示例2,这使得我们从严重错误(1而不是6)到接近正确(我们数了5个物体)。

这表明,通过取两个过程结果的最大值(一个从桌子头部“看”,一个从侧面“看”),我们可以提高准确性,这从我们的比喻来看是很有意义的:无论哪个视角能看到最多的物体间隔,都最接近物体的真实数量。

从2个视角到4个视角(稍微复杂一点)

这是我们能做的最好的吗?答案显然是否定的——应该有办法从其他角度观察“桌面”。问题是如何高效地做到这一点,事实证明这很简单。

所有深度学习/张量基础框架都支持各种矩阵变换方式。有些直接支持以下操作,有些则不支持,但只要它们能高效地实现矩阵的新低层重排操作,它们都能支持。

我们不转置矩阵,而是对其进行倾斜,使其从左到右逐列下降。

示例1的原始矩阵

000000000000000000
000111000000001110
000110000000111100
000011110000001100
000000000000000000
000000000000000110
000000000000001110

示例1的矩阵,倾斜后从左到右每列下降一行

0.................
00................
000...............
0000..............
00010.............
000110............
0000110...........
.0001000..........
..0001000.........
...0001000........
....0001000.......
.....0000000......
......0000000.....
.......0000000....
........0000100...
.........0000110..
..........0000110. 
...........0001110
............000100
.............00000
..............1100
...............110
................10
.................0

为了便于阅读,这里显示了我们需要添加的填充点,但在实际应用中我们会使用零。

如果我们现在对这个倾斜的矩阵应用原始过程,我们将得到结果3,表示有三个包含1的连续行序列(第4-10行,第14-18行,第20-22行)。这就是正确答案!原始激活图中存在三个连续的1区域,对应于目标特征的实例。

从比喻上说,这相当于从右上角观看“桌面”。

显然,通过将矩阵**向上**倾斜,使其从左到右逐列上升,我们可以相应地从“视角”的左上角计算特征。

通过结合这些观察矩阵的方式,我们可以有效地从四个不同的视角(自上而下、自左向右,以及两条对角线)观察它,从而有很大的机会准确地计数特征的数量(通过取四个结果的最大值)。

那么……这样够了吗?

嗯,这取决于。这个方法有两个显著的特点:

  1. 它可能会低估,但如果特征存在,它永远不会返回零。
  2. 它永远不会高估特征的数量。 

由于深度学习网络需要能够处理一定程度的低估(因为隐藏的特征是三维物体二维图像的固有特征),因此“四个视角是否足够”的问题是一个实际问题,需要权衡计算额外视角所带来的成本与准确性略微提高所带来的好处,而不是一个有正确答案的问题。就我个人而言,我发现4个视角工作得很好,但可能在某些应用中,准确性的提高更值得计算。

从4个视角到8个视角(再次变得复杂)

如果我们决定想要8个视角而不是4个(我还没有发现真正需要8个视角的场景),我们将构造4个额外的矩阵:一个以每列2行下降的速率,一个以每列1行下降的速率,一个以每列2行**上升**的速率,以及一个以每列1行上升的速率。 

下面是一个(人为构造的)特征激活图示例,其中“8视角”方法可以提高准确性。 

示例 3

110
100
101
101

实际特征数量:2

从任何4个“主要”视角(顶部、侧面和对角线)计算得到的特征数量:1
这里是激活图以“每列2行下降”的比例重新倾斜的图

1..
1..
11.
10.
.00
.00
..1
..1

实际特征数量:2

计算得到的特征数量:2

这意味着“8视角”方法实际上可以在某些情况下提高准确性。

然而,这也有危险,如下所示。  

示例 4

00000
01110
00000

以2:1的斜率重新倾斜

0....
0....
00...
.1...
.00..
..1..
..00.
...1.
...00
....0
....0

激活图中的实际特征数量:1

计算得到的特征数量:3

这不仅是错误的,而且是严重且具有误导性的错误。虽然低估是不可取的,但高估是灾难性的:如果我们的深度学习网络对高估的容忍度很高,我们就失去了区分“小猫炖菜”和真正小猫的能力。

所以,如果我们想使用“8视角”方法,就必须修复这个问题。纯粹通过实验,我开发出的解决方案是,在创建2:1上升和2:1下降矩阵之前,将激活图向上偏移一行并与自身相加。例如,对于示例3:

110   ...   110
100   110   210 
101 + 100 = 201
101   101   202
...   101   101

当以2:1倾斜时,这仍然给出正确的特征数量(“2”)。

更重要的是,它也修复了示例4的特征数量。这种“偏移和相加”有效地在垂直方向上对矩阵进行了轻微“模糊”。在某些情况下,这会降低准确性而不是提高它——但当它降低准确性时,它会朝着低估的方向进行,而不是高估。

……更多的视角也无法保证准确性

示例

000000000000
011001100110
011000000110
011111111110
000000000000

实际特征数量:2

从任何角度观察到的计数:1

所以,没有哪个数量的视角可以保证准确性。没关系。说得够多了。

我们如何在深度学习网络中使用它?

到目前为止,我专注于创建一个矩阵中心、高效的计数特征的方法,怀着一种模糊的信念,即由此产生的信息将有助于图像分类(尽管我认为它也可能对回归有用)。

但我们如何将这种能力实际连接到网络中,使其发挥作用呢?

首先需要注意的是,这个过程从每个特征图只产生一个额外的输出——因为它是一种汇总信息,完全没有空间范围。因此,它不适合以任何明显的方式被输入到CNN的“下一层”。

显而易见的方法是直接将其作为基本输入馈送到网络的最终全连接(FC)层或最大池化部分:“图像中有两只猫的眼睛”是分类的非常有用的信息。

这种方法有效,但事实证明它需要额外的FC层才能很好地工作。这是因为“3只幼犬爪子”这个数字对分类来说并不是我们真正想要的;我们理想上想要的是这样的: 

  • 0只幼犬脚:我猜它仍然可能是只幼犬,但这并没有证据。
  • 1只幼犬脚:也许那是只幼犬。
  • 2只幼犬脚:对幼犬来说是合理的证据。
  • 3-4只幼犬脚:对幼犬来说是确凿的证据。
  • 5只幼犬脚:呃,也许我们数错了,但“5只脚”并不是幼犬的证据。
  • 6只幼犬脚:那不是真正的幼犬脚,那也不是幼犬。

对我们的计数结果应用一对非线性层可以轻松实现这个结果(尤其是使用PRELU激活函数),但如果我们是在FC层中进行这种学习,那就意味着需要添加更多FC层才能将结果转换成有用的分类形式。简单地说,这是愚蠢的:FC层在训练参数方面成本极高,再增加它们来学习直方图的期望形状(因为“直方图”就是我上面给出的定义)是疯狂的。

所以,我们不是将特征计数直接输入到最左边的FC层,而是将其输入到一个“直方图神经元”,它可以学习最有用直方图的形状,然后将**这个**结果输入到FC层。

我设计的直方图神经元很简单:

权重和偏置,PRELU,权重和偏置,PRELU。就是这样。

这个序列有效地为我们提供了四个线段来调整曲线的形状,这足以满足大多数图像分类所需的直方图。(事实上,我怀疑我们只需要学习一个峰值和两条斜率就足够了,但我还没有测试过)。

所以,总结一下:

  1. 我们的每个特征图旁边都会计算一个特征计数,作为该特征图的附加特征。
  2. 这些计数然后进入直方图神经元,学习计数的期望分布。
  3. 每个直方图神经元的激活水平然后成为网络最终分类(或回归)层(或不表达空间范围的网络其他部分)的输入。 

反向传播和学习呢?

显而易见,我们可以并且应该通过“直方图神经元”进行反向传播,以学习期望的直方图形状。

进一步向后反向传播,我们会遇到计数过程本身。这些过程以矩阵运算和重排的形式表示,因此可以对它们进行反向传播。我提出了三种明显的方法来处理这个问题,基于我们希望如何建立计数的截止水平(即,我们需要确定哪些激活水平被视为1-ish vs 0-ish)。以下是它们:

  1. 我们根本不通过计数进行反向传播。网络中的每个特征图最终都会有一个非线性层(这是常规模式),该非线性层的参数将基于常规通道进行学习。我们可以将计数过程附加到这些现有非线性层产生的激活图上,并使用它们学习到的任何阈值。
  2. 我们通过每个计数过程进行反向传播,并将此反向传播作为学习相关非线性层的输入,就像来自网络其他部分的 backprop 是学习非线性层的输入一样。(这是显而易见的方法)。
  3. 我们为每个特征图创建一个单独的非线性层——与常规非线性层并行——并将我们的计数过程附加到这些新的激活图上。这使得用于计数的阈值可以独立于网络其余部分使用的阈值进行学习。当然,我们对整个过程进行反向传播。 

#1的优点是不需要我们为那些丑陋的矩阵倾斜编写反向传播代码,但显而易见的原因是效果不如#2和#3。#2是最干净的,#3是最灵活的。就像深度学习网络设计中的大多数事情一样,确定在特定应用中哪种最好需要反复试验。

但无论你如何做,你都能得到一个不太可能在看到一堆黑点时就说“汪汪!”的东西。

历史

  • 2017年12月28日:初稿
© . All rights reserved.