本文参考:神经网络中通过add和concate(cat)的方式融合特征的不同_陈知鱼的博客-CSDN博客_神经网络cat
一、cat与add
concat每个通道对应着相应的卷积核。而add形式则先将对应的特征图相加,再进行卷积操作,相当于加了一个先验:对应通道的特征图语义类似,从而对应的特征图共享一个卷积核。因此add可以被认为是特殊的concat形式。但是add的计算量要比concat的计算量小很多。
下面来举个例子说明共享一个卷积核:
(1)先分别卷积再add
value_branch1 = a1*1 + a2*2+ a3*3+ a4*4+ a5*5+ a6*6+ a7*7+ a8*8 + a9*9
value_branch2 = b1*1 + b2*2+ b3*3+ b4*4+ b5*5+ b6*6+ b7*7+ b8*8 + b9*9
add:value_branch1 + value_branch2 =
(a1 + b1) * 1 + (a2+ b2)*2+ (a3+ b3)*3 + (a4+ b4)*4+ (a5+ b5)*5 + (a6+ b6)*6+ (a7+ b7)*7+ (a8+ b8)*8+ (a9+ b9)*9
这里就可以看到:实际就是两个分支的特征图,使用了同一个卷积核。
(1)先add再卷积
卷积后:(a1 + b1) * 1 + (a2+ b2)*2+ (a3+ b3)*3 + (a4+ b4)*4+ (a5+ b5)*5 + (a6+ b6)*6+ (a7+ b7)*7+ (a8+ b8)*8+ (a9+ b9)*9
二、知乎上的(引自:如何理解神经网络中通过add的方式融合特征? - 知乎)
(1)
理解“对应通道”:对应通道就是指A特征图中的某层(比如第3层,A_3),则B特征图也是第3层,B_3,则A_3和B_3就是相互成为“对应通道”。
另外上面的公式,我觉得也有必要解释一下,担心有的小伙伴不知道。Xi 表示第i通道上的特征图,Ki表示第i个通道特征图Xi对应的卷积核。Xi * Ki表示第i通道上的特征图与卷积核做卷积!
为什么上面skip这个图BN层前面没有weight?(没有激活函数的卷积层,即affine)。经常看见BN插入的位置感到疑惑。
答:它和上一个block的weight是连接在一起的,你看这个block最后一个也是weight
注:
Q1:concat的话,会存在两路特征相互之间的权重,而add就没有了。
A1:对的,毕竟参数量少了一半。
Q2:是不是可以理解成add是两路输入对应通道的信息融合,而concat之后经过卷积核就会把所有通道的信息一起融合,所以add能避免不同语义信息的混杂?
A2:?这个暂定
Q3:cancat是比add 更加占用显存 毕竟channel数增加了, 但是如何理解cancat的计算量比add的计算量大呢?
A3:本身计算量不大,但导致它后面接的conv计算量翻倍了
Q4:在shufflenet v2中提出:add这种element-wise操作会在memory access cost上带来相比concat更大的消耗,实际运算时间会更多。add这种运算会增加memory access cost假设消耗时间是t1,concat 增加了通道数导致后续操作变慢假设消耗时间是t2,论文中阐述t1 > t2
A4:concat本身没运算,只是会增加通道数导致后续操作变慢。G4只是说去掉add会更快,没有和concat比较。而且,1. 论文里concat后面接的是1*1卷积,和普通卷积相比已经快很多了。2. 大部分concat之前都用了split,这已经减半了。
Q5:如果两路的channel数不同怎么办,先1*1 conv把top feature的channel数变小吗?那这个conv要不要加bn和relu呢?(add要求两路的channel数要一致,或符合广播机制也可以,使用广播达到两路可以进行add)
A5:对,用1x1卷积。官方实现bn和relu都是加的。
Q6:被Add的两个向量,如果同channel含义类似,从人类理解的角度来看应当是相同值域的,例如nlp中的word embeding和position embedding,pre active resnet中的激活前h,都是可正可负的连续实数。然而,实践中,仿佛并不必须是这样,例如relu激活之后再add position embedding,post active的resnet,被add的两个向量有的是恒大于零,有的是可正可负连续实数,也一样可以work。
A6:是的,其实“含义类似”只是从“对应通道卷积共享”理解出来的,不会太严谨。不过个人感觉,加了relu确实改变了值域,但只是做了截断,没有参数,因此也不会对语义做多大的改变吧。
Q6_2:嗯,截断,那倒是,性质改变的不多。保险点来说,concat不会获得比add更差的效果吧?
A6_2:数据集够的话是的,但参数会翻倍,会变慢,也有可能过拟合。
Q7:多谢大佬指点,最近刚好在疑惑这个问题。但是我想问如果语义特征类似的话,用concat会不会产生大量的冗余信息呢?
A7:数据够的话没啥问题,concat也能学出对应通道相似的参数。而且实际也很难出现特别相似的情况,除非你加loss去监督。
Q8:concate结果是增加了通道数,通道数的增加为什么可以理解成特征值的+运算?
A8:是和后面的卷积层一起看的。
(2)
我认为没有人能彻底回答这个问题。NN领域很多问题都这样。
add操作必然会带来信息损失。两头羊加两头羊等于四头羊,这时候加法没有损失信息。但是如果两个被加的向量不具备同类特征含义时怎么办?事实上,如果有了加法操作,训练过程会把加法之后那个特征分解为加法前的两个子特征。如果这两个子特征之和超过了某个阈值,哪怕一正一负,就有激活功能。这两个子特征未必需要是同类特征含义。信息损失如果“损失”得当,就是信息提取!
瞎白话了这么多,退一步,单个神经元是什么含义?本来就不清楚,何况相加呢?
NN优势和劣势都在这里,说得清的话,早就用特征工程搞定了,还用什么NN。
(3)
concat是肯定是计算量大于element-wise add的,但个人认为,concat避免了直接add对信息造成的负面影响。而且逐元素加和的方式要求不同层的feature map具有完全一致的channel数量,而cancat不受channel数量的限制(yolov3里就是concat,做concat操作的两层通道数不同)
(4)
我想这张图能消除一点疑惑。
concatenate和add可以相互转化
注:图来自文章Deepside: A general deep framework for salient object detection
(5)
add操作经典代表网络是ResNet,concate操作经典代表网络是Inception系统网络中的Inception结构和DenseNet。
正如之前的回答有人说的,add操作相当于加入一种先验知识。我觉得也就是相当于你对原始特征进行人为的特征融合。而你选择的特征处理的操作是element-wise add。通过add操作,会得到新的特征,这个新的特征可以反映原始特征的一些特性,但是原始特征的一些信息也会在这个过程中损失。
但是concate就是将原始特征直接拼接,让网络去学习,应该如何融合特征,这个过程中信息不会损失。
所以我认为add其实只是concate的一种特殊情况。但是concate带来的计算量较大,在明确原始特征的关系可以使用add操作融合的话,使用add操作可以节省计算代价。
Q1:你好,请问在做concate操作的时候,不同尺寸大小的特征图也可以直接拼接吗?还是要做一些尺度统一的操作(填0或把小图放大到大图的尺寸)?
A2:concat要保持W和H维度一致,不同尺寸的特征图concat,就需要上采样小尺寸的特征图,或者下采样大尺寸的特征图,得到相同的W和H。
(6)
add作为一种特征融合的方式在没有时序的网络中是无害的,concat是add的泛华形式,对比densenet和resnet即可知道。
但是在时序步共享的网络中,add简直是灾难性的,因为时序共享的网络诸如lstm和gru,要求每一个时间步共享参数,那么处在后时序步的网络输出值域要远远大于最开始前几部的值域,所以rnn会有梯度的问题,因此lstm和gru应运而生,不过个人觉得lstm和gru绝对称不上是好网络,解释性太差,每个门的动作仅仅基于启发式的yy来设计,我们需要有能力的学者探讨如何在时序网络中进行特征的融合。
(7)
我的理解是 一些重要特征会随着网络层的增加而消失,add是人为的让重要特征保存的更久,concat是给神经网络更多的机会,让神经网络自己去学习。
(8)
元素级add是给网络带来的新的信息,或者说噪声。正如和drop-out,batchNorm一样,同样也是加入噪声,起到防止网络过拟合的作用。这在一定程度上增强训练结果的鲁棒性。
(9)
cat和add 在pool上的例子(组合池化)
组合池化则是同时利用最大值池化与均值池化两种的优势而引申的一种池化策略。常见组合策略有两种:Cat与Add。其代码描述如下:
def add_avgmax_pool2d(x, output_size=1):
x_avg = F.adaptive_avg_pool2d(x, output_size)
x_max = F.adaptive_max_pool2d(x, output_size)
return 0.5 * (x_avg + x_max)
def cat_avgmax_pool2d(x, output_size=1):
x_avg = F.adaptive_avg_pool2d(x, output_size)
x_max = F.adaptive_max_pool2d(x, output_size)
return torch.cat([x_avg, x_max], 1)
ref: