resnet为什么能够这么深,而性能不发生退化,确实是一件百思不得其解的现象。而最近我终于明白了里面的深层次的原因。
根本原因是,不用残差的模型本质上是在将feature在空间中线性变换,然后进行裁剪,如此可将不同的feature映射为同一个值,发生了坍缩现象,本应不同类别的feature非线性变换为同一个feature值,从而造成信息丢失,造成欠拟合的现象。那么为什么resnet没有发生这种情况呢?甚至网络越深,resnet性能越好。
为了解释这个现象,首先让我们玩一个推箱子的游戏。
这个游戏的内容如下:
在二维平面上,有四个箱子,其中两个为红色,记box1,box2,另外两个为蓝色box3,box4。红色的其中一个分布在第一象限,另外一个分布在第三象限。蓝色的其中一个分布在第二象限,另外一个分布在第四象限。你可以进行的操作是可以选择任意一个箱子向任何方向移动一。游戏的目标是经过不停地移动,使得红色的箱子在第一象限,蓝色的箱子在第三项象限。
这个游戏的机器学习含义显而易见,就是一个异或分类。这个游戏虽然简单,但事实上,resnet就是在玩这个游戏。下面具体解释。
如果在这个游戏中要进行操作,很明显,首先要选定一个箱子,然后移动它。假定选定函数为f(box),移动距离为offset=(x,y)。比如要将box1移动(1,2)个距离,则
f(box)=1当box=box1; f(box)=0当box!=box1,
offset的值为(1,2)。
所以这样一个操作使得
coordinate(box)=coordinate(box)+f(box)*offset。
整个游戏的操作序列是(f1,offset1),(f2,offset2),(f3,offset3).....
如果要玩这个游戏,最终要优化的模型是:
box-state1=box-state+f1(box-state)*offset1
box-state2=box-state+f2(box-state1)*offset2
box-state3=box-state+f3(box-state2)*offset3
....
box-staten=fn(box-staten-1)+offsetn
loss函数是使得box-staten为,红色的箱子在第一象限,蓝色的箱子在第三象限。
可以看出每一个步骤并没有使得信息有所损失,这才是resnet的关键所在。没有残差的网络是有信息损失的。而resnet完全没有。
如果,聪明的话,你可以发现,为什么要限定每一个步骤中只移动一次呢?每一个步骤我可以移动多次。比方说每一个步骤,我移动2次。
则新的移动函数为
coordinate(box)=coordinate(box)+f1(box)乘offset1+f2(box)乘offset2
则新的模型变成了
box-state1=box-state+f1(box-state)*offset1+f2(box-state)*offset2
box-state2=box-state+f3(box-state1)*offset3+f4(box-state1)*offset4
box-state3=box-state+f5(box-state2)*offset5+f6(box-state2)*offset3
....
box-staten=box-staten-1+f2n-1(box-staten-1)*offset2n-1+f2n(box-staten-1)*offset2n
loss函数是使得box-staten为,红色的箱子在第一象限,蓝色的箱子在第三象限。
这个模型如此熟悉,完全可以转化为神经网络语言模型。
如果把每个box理解为训练对象的feature,颜色理解为类别,可以将上面的模型转化为一个神经网络模型。每个box-statei相当于网络的一层,比方说上面就一共n层。每一层的输出维度,就是在一层中移动了多少次。比方说inputs为36维的向量,输出维度为72维。就是将36维的箱子移动了72次。
根据上面类比,可以用python语言构建一个神经网络分类器。
inputs:feature,为2维向量。
第一层,使用3个分类器对feature进行二分类。假设feature的3次分类结果为c=[c1,...c3],则第一层的分类输出为
outputs=feature+c1 乘offset1+c2乘offset2+c3乘offset3.
其中offset3为2维向量。
到现在就非常清楚了,如果我假设
offset1=[1,0,0],offset2=[0,1,0],offset3=[0,0,1],
是固定的参数,而非能学习的参数,此时
feature=feature+[c1,c2,c3],
立即可以得到resnet的残差形式。
所以resnet就是在干这样一件事情,每一个残差块就是,对于n维的向量,根据[0,0,...,1,0,...,0],做一次决策,决定该向量是否应该执行该移动,共做n次决策。
那为什么resnet的性能与层数没什么关系呢。因为每一层做的移动决策完全可以放在下一层做,下一层的移动决策完全可以放在上一层做。就好像移动箱子的游戏。我可以在一个步骤中移动多次箱子,也可以多个步骤移动一次箱子,有什么区别呢?事实上,性能只跟做决策的次数有关系,做的决策越多,效果越好。
事实上即便resnet也不过是偶然的产物,真正解决深度问题的模型应当是上面的模型。
1.首先分类器不应该用relu激活,应该用sigmoid激活,否则不是线性移动,会产生偏差,很有可能造成信息丢失。
2.其次不应当假设offset为固定参数,应该设有专门的offset变量来进行学习。
3.最后对n维特征向量,每个残差块,不仅仅只能做n次决策,事实上可以做任意次决策。
4.上面的模型给出了一种不基于梯度训练神经网络的可能。也就是说可能存在类似于训练决策树的训练算法来训练神经网络。并且这种训练方式是可扩展的,也就是说随时可以增加新的神经元来解决新的问题。
5.残差中分类器个数与性能的关系。虽然保证了信息的无损失,但是这并不代表模型可以取得高性能。因为神经网络权重是随机采样,而采样密度直接决定了模型的性能。假设第一个残差块的分类器个数是n,第二个残差块的分类器个数是n.则对空间的划分个数是2^n+2^n.如果分类器个数是2n,则对空间的划分个数是2^(2n)。可以看出2^(n+1)远小于2^(2n)。也就是说,分类器个数与性能的关系并不是简单的加法关系.
6.将最后的offset变量全部初始化为0(也就是残差的最后一层升维卷积层的权重全设置为0),整个网络仍可训练。
事实上,因为上面的实现差异,resnet也无法避免欠拟合的问题,在文本检测的数据集上,用resnet直接训练,不用预训练模型,效果远远比不上vgg。说明resnet仍然避免不了信息丢失的情况。
最后给出上面意义上的深度学习残差公式:
假设
inputs的shape为[N,H,W,C],
分类结果class=tf.nn.sigmoid(conv(inputs)),shape为[N,H,W,C1],分类器可以用任意可梯度传导的模型,不必限于线性模型。
创建offset变量,shape为[C1,C]
残差公式为:
class=tf.reshape(class,[N H W,C1])
offset=tf.matmul(class,offset)
offset=tf.reshape(offset,[N,H,W,C])
outputs=inputs+offset
当C1=C,offset为单位矩阵,激活函数为relu时,退化为resnet网络。
补充:
上面的推导是基于resnet的残差分支的最后一层是被激活的。如果最后一层没有被激活,则与选定-移动模型完全等价。此时残差分支的倒数第二层的输出是选定结果,最后一层的权重是移动参数offset。
但是resnet有两个非常严重的问题。
第一,因为只是在做平移变换,如果需要学习旋转变换就有问题了。即便能做到在数据集上过拟合,也只是用平移变换模拟旋转变换,不具备泛化能力。理论上,应该学习仿射变换,而不仅仅是平移变换,但是仿射变换参数量爆炸,一个比较好的选择是学习镜像变换。理论上可以证明连续的镜像变换复合可以组成任意不变性操作。
第二,对于取值范围为无穷的异或问题,resnet无法解决。resnet只能解决取值范围有界的异或问题。但是用镜像变换取代平移变换就可以解决取值范围为无穷的异或问题。但是我在cifar100上测试,用镜像变换的测试集结果低于用平移变换的测试集结果,但是训练集结果差不多。不知道为什么。
其次,如果聪明的话,可以发现作弊的技巧。完全可以创建新的维度,然后在新的维度上进行移动,这就是densenet。
最后,假设激活函数为relu函数,可以根据这个模型创建一个不受internal corivate shift(ICS)影响的改进版resnet,不受died neuron影响.具体残差公式如下:
input为输入。
output=tf.matmul(OFFSET,tf.nn.relu(tf.matmul(W,input))+tf.matmul(-OFFSET,tf.nn.relu(-tf.matmul(W,input))。
OFFSET为移动矩阵,W为选择矩阵。该残差公式具有明确的意义。如果x被选中,则将其移动offset距离;如果没被选中,则将其移动-offset距离。该残差函数不存在死亡神经元。所以不受ICS的影响。所以不用BN,也能正常训练,不过需要控制好方差。在具体实验过程中,发现resnet如果使用leaky_relu激活,也不会受ICS的影响;但是如果VGG使用leaky_relu激活,仍然会受ICS影响。不过总的来说,我觉得使用上面的残差公式比leaky_relu好一些。毕竟激活函数的导数在全值域上的绝对值都为1.
下面讨论残差函数与配对函数的关系,以及怎么设计残差函数。
配对函数是指三个函数f,f1,f2有如下性质:
1.如果f(a,v)=z,f1(z)=a,f2(z)=v
2.如果a1!=a2,则f(a1,v1)!=f(a2,v2),对于任意v1,v2
3.如果v1!=v2,则f(a1,v1)!=f(a2,v2),对于任意a1,a2
形象化来说,如果n个不同的特征向量ai具备相同的label v,如果存在一个函数f,f2,使得如果ai!=aj,则f(ai,v)!=f(aj,v)。并且f2(f(ai,v))=v,此时可以说将共性特征v编码到了这n个向量中,并且存在解码函数,解码出共性特征。这样的函数可以使得信息无损失,并且使得具备相同label的特征向量具备共性特征。
理论上来说,如果特征向量集合A={a1,a2,a3..}具备共性特征向量v.如果存在某个函数f和反解码函数f1,f2,使得f(a,v)=a',f1(a')=a,f2(a')=v。并且如果a1!=a2,必有f(a1,v)!=f(a2,v),那么该函数可以作为残差函数。
最完美的模型是f(a,v)函数完全为配对函数。但是理论上不可能实现。但是如果约束A空间和V空间,事实上还是可能达到完全配对函数的效果。不过需要时间去寻找这样的约束和配对函数。不过,我们可以假设这样的配对函数存在,设为f,f1,f2,看看会发生什么有趣的事情。
inputs为输入向量.
第一步:
假设残差输出为res=convolution(inputs)
使用f进行编码outputs1=f(inputs,res)
第二步:
outputs1作为第二次残差卷积的输入
使用f1,和f2对ouputs解码得,source=f1(outputs1),res=f2(outputs1)
source2=convolution(source)
res2=convolution(res2)
res2=source2*res2#该乘法具有实际意义,首先在res2空间分类,然后根据分类结果再在source空间中分类。可以想一想densenet。
使用f进行编码,outputs2=f(source,res2)
依次类推。
会发现整个残差模块的函数形式为:
outputs=f(f1(inputs),convolution(f1(inputs),f2(inputs)))
因为如果输入不同,则编码结果肯定不同,所以划分不必一定要还原出原空间,可以直接在编码空间中划分,所以假定f1为恒等函数,
此时outputs=f(inputs,convolution(inputs,f2(inputs)),可以看出天然具备跨层连接形式。此时f为残差函数。