python手写神经网络之用im2col实现卷积层、池化层

简介

im2col就是img to colomn主,要是把图像转成column,原因和用途也很清晰,CNN中数据是四维的,并且有滑动窗口的存在,如果用for循环,计算效率不敢看。

 

那么原理也很简单,展开、复制、向量化。

 

但是从示意图到实现,还是有一个地方比较绕,所以我一下也没想到实现,还是看着参考代码分析了一下才明白。

 

示意图:

其实示意图可以写两种,不带batch的,和带batch的,前者更简单,但是只算一个循序渐进的推导过程,无论如何最终都要用带batch的版本,所以我就直接带batch画展开图了。(前者的一个好处是图不复杂,方便再一次展开观察广播过程,比如把filter复制N次等操作,所以两种最好都画一下,因为广播操作交给numpy去做了,所以带batch的版本就隐藏广播细节了,不然没法画)

其实宏观的我这样画应该很直观了,就是把图片按一个滑窗一个行来转换,batch反应在行数上,其实不增加理解的难度。

filter每个作为一列,最后行列直接矩阵乘法,每个行和每个列都有交集,对应地,每个具体的窗口数据和每个具体的filter都应该有交集。

 

 

这是具体实现的shape操作,col有6个维度,这里可能是稍微有那么点抽象的,但是核心思路就一样,实现图一的展开效果,具体怎么抽象,其实都只是实现细节,不过为了便于理解这个细节,我还是按照这个过程画了图四。

 

 

 

图三:代码部分:

for循环略微抽象!

抽象的点主要在哪?在于,7*7的输入,5*5的kernel,3*3的输出。不是以窗口视角通过3*3的循环提取了9次5*5的块。而是提取了25次3*3的块。

其实很好理解,其实就是抠出来了(5,5)份的(3,3)个对应位置的数值,仔细看y_max=y+stride*out_h和y:y_max:stride,这两个细节,证明每次的切块都不是一个原图中的实际(紧密形状)的切块,而是有跳跃的。

直觉:我想要3*3个5*5的切块,结果,得到5*5个3*3的切块,每一个3*3的切块,包含了全部9个输出各自的一个组成部分(二十五个组成部分之一,如果你要算上bias,相当于输出的二十六个组成部分之一)。这种多维空间的思考很看几何直觉的,如果实在不理解,可以多画图,多用变量描述,对于我,到这一步已经很容易理解了,所以不再过多的展开,过于繁琐。建议实在不行就跳过(我在后边已经做了我认为更符合直觉的实现方案,供参考)不理解也没关系,为什么有这么“抽象”的一步,主要就是为了下边的reshape能够按意愿去操作(这操作其实是和图一计算的shape呼应的),最后,transpose之后就符合直觉了!

 

 

图四:专门针对循环内部的那个用OH和OW“滑窗”的操作做的等价转换。

尽量用比较直觉的形式去表达,但是因为再画就太大了,所以点到为止,可以看到,5*5个3*3的切片,都拿出切块中的第九个点,就组成了原始的第九个窗口(对应第九个输出所对应的输入)

 

那么问题来了,为什么不能写一个符合直觉的循环呢?为什么不直接用滑窗的形式去提取?并且也省略了transpose那一步。因为效率?transpose操作本身不花时间吗?是和内存排列有关,这种切法效率更高?

 

所以我怎么可能不去尝试呢(实现我个人的im2col)?

一方面是探究如果不这样是不是就不能实现,另一方面也为了给大家展示一个符合直觉的过程!

(专门去搜了一下,网上关于im2col的中文内容,大多是复制了代码,加了解释,目前没看到有像我一样换方法实现的,所以至少作为一个探索吧)

下面是我自己的实现——以滑窗方块视角去操作,二维上(实际是四维的,有N和C),每次循环一个5*5的方块,一共3*3次循环(size按之前的假定):

注意一致性,col的定义变了,循环要变,reshape也要按照图一的目的做相应改变(transpose不能完全省略,还是要改变channel的位置的)。

 

 

至少从实现结果上,应该是一致的,那么效率呢,直觉上,他的切块比我还跳跃,也不太像是根据内存去跳的(显然不是),所以至少他没理由比我快吧。

 

 

下面计算一下时间,反而我的实现还更快一些!!

 

 

虽然自己做的更改看起来是能达到同样效果,并且效率还更高,不过保留其他可能存在的局限性,比如反向操作col2img的难度(col2img作为一个函数,input肯定是固定的,output既然也一样,那么好像col2img哪怕共享同一种实现,也是不干扰的。)

 

另一方面,我好像也不能完全说第一个参考实现不符合直觉,可能是视角不同,但是我感觉那种略抽象。他是以一个卷积核元素为单位去看的。for循环内,一次一个卷积核元素和它所有对应的滑动输入。另一个角度看,也许他为了反向传播更符合直觉?并且两者更一致!!

 

另外一个就是整体赋值的自动匹配问题

col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]
col[:,:,y,x,:,:] = img[:,:,y_start:y_start + filter_h, x_start:x_start+filter_w]

切片的形状分别是(N,C,3,3)和(N,C,5,5),col定义中,前两者是NC,赋值中,固定了y,x,剩下那(3,3)和(5,5)就自动匹配上了。

 

 

 

随便给出im2col两个例子:

其实很直观,batch是叠加在外边的,等于“行”数shape[0]翻倍。

shape[1]很好计算,只和filter size和channel数量有关

 

 

最后是把im2col应用到卷积网络,实现一个卷积层。

前向传播:

主要就是注意im2col的输出形状,结合图二完成相应转换和dot操作,然后就是四维形状的还原(图二中的N*OH*OW是关键,现在reshape就按这个,先折叠成四维,最后再把channel移到axis=2)

注意自定义的b的形状,如果写错,经过广播,out就错了!一个输出通道只有一个b,也就是按下边的FN通用就可以了。

 x是随机的三维图像,7*7像素,完成一个简单的卷积层和前向传播。

 

反向传播相关

首先看一下网络部分,不考虑col2im的话,宏观看,其实很简单,就当一个FC层去看,几乎一样,除了一些shape关系,要和前向传播对应,我也画了图。结合图一图二的话,会很简单。

 

 

同样的,col2im也会绕一些,建议画图理解,或者跳过此部分。其实绕的也就是6维数组的操作,整体是很简单的,建立空的img,pad,把梯度从col再逐渐放回到img中,(结果img其实是dimg,或者叫dx)

 

 

接下来是池化层:

如果只是单纯看一下《深度学习入门》,大概看一下内容,就会觉得“你说的都对”,但是当我思考细节的时候,因为共用了im2col接口,那么im2col接口是按卷积核切片来做的操作(channel并不是一个独立层级,而是以切块的形式和每个窗口绑定,也就是C*FH*FW),怎么会是图中那样呢——整个C(1)里的所有FH*FW整齐排列(一共OH*OW行),然后再下一个C(2)?仔细一推,发现书里有一个小bug!!(也可能他为了便于读者理解吧,im2col和池化层代码并非作者原创,但是可能不便于他表达,所以就改了,至少图片和代码确实是不一致的,但是两种过程是可以等价的,只要轴撸的清楚,怎么操作都行)

C在最后,就注定不可能出现图中的形状!图中的形状,C的逻辑维度要比OH和OW高!

 

 

那么根据我对代码的理解,实际流程是这样的(图中上半部分,而书中应该对应图中下半部分,书中的图忽略了N,我带上了,其实在最外围,不影响,变量太多,不知道怎么表达合适了,核心思路大概是这样。)

如果想要书中那种效果,其实需要reshape+transpose+reshape,略微繁琐,但是只要搞清楚轴的关系,其实也不复杂!

 

 

下面用代码验证一下:

手动生成通道1的数据,后边的被遮挡了,就随便复制几个。观察通道1即可。

 

在np.max之前的这一步,和书中的图示不匹配,通道1的四个输出并没有连续分布,而是被分割开,证明了我前边说的

 

这类bug在深度学习中太常见了,经常有论文的描述和代码不一致,比如按论文的操作(记得是ResNet和RCNNs之类的,读者广泛地存在困惑),224*224的图最后得到7*7的结果那里就很困惑,其实前边有一层padding,把图变成230*230了,所以还是要多研究代码,看论文千遍不如过代码一遍。

 

 

最后是pooling的bp过程:

代码和示意图有些不同(代码是新建的一个dmax,而非在原数据上扩充维度),意思是一样的,总之,可以参考图一的流程和图二的关键节点形状。核心思路也是一样的,从(N,C,H,W)的形状变成平铺,(BP过程)再通过col2im恢复到img形状。

 

 

发布了183 篇原创文章 · 获赞 376 · 访问量 116万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章