ResNeXt 在前面的文章中,我们已经介绍了ResNet以及Inception的原理及其PyTorch实现。而今天要介绍的ResNeXt,正是在ResNet的基础上,结合Inception得到的。
在Inception中,其多个分支的结构是不同的,就像这样:
ResNeXt的作者提议将每个分支的结构搞成一样的,以减少网络复杂性,便于扩展;再加上一个skip connection,就得到了ResNeXt block。
下面是论文中给出的一个ResNeXt block:
它总共有32个分支,每个分支的结构都是完全相同的,且输入与输出之间做了跳连(skip connection)。
其实,上面的这个结构还是可以简化的,作者在论文中指出,以下3种结构是等价的:
因此,为了方便,编码时我们就采用(c)结构。
现在把ResNeXt block的(c)结构单独拎出来:
再把ResNet block拿过来:
对比来看,两者在结构上的区别在于,前者的3x3卷积是分组卷积。在论文中,作者将分组数用Cardinality来表示,并且指出: increasing cardinality is more effective than going deeper or wider when we increase the capacity.
ResNet有许多版本,对应的ResNeXt也有许多不同版本。我们之前已经实现过ResNet-50,这里我们将实现ResNeXt-50。
ResNet50和ResNeXt-50的网络结构图如下:
其中的C指的是Cardinality,也就是分组数。
可以看到,除了上面所提到的分组卷积外,ResNeXt-50 block的第一个1x1卷积和3x3卷积的输出特征图个数是ResNet-50对应的二倍。
鉴于此,我们可以很轻松的通过修改ResNet-50的代码来实现ResNeXt-50。
PyTorch 实现 ResNeXt 在实现之前,还有一点需要说明:
在上面的网络结构图中,出现了32x4d
,这里的32指的是分组数,4指的是每个组内的卷积核个数。这两个参数的不同,ResNeXt-50的性能也会有所不同,作者经过实现发现,使用32x4d
的搭配能够取得较好的效果,所以在这里,比”我们要实现ResNeXt-50”更准确的说法是”实现ResNeXt-50(32x4d)”。
首先实现ResNeXt block,也就是网络结构图中conv2到conv5都遵循的block:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class block (nn.Module ): def __init__ (self,in_channels,out_channels,identity_downsample=None ,stride=1 ,groups=1 ,width_per_group=64 ): super ().__init__() width=int (out_channels*(width_per_group/64 ))*groups self.expansion=4 self.conv1=nn.Conv2d(in_channels,width,kernel_size=1 ,stride=1 ,padding=0 ) self.bn1=nn.BatchNorm2d(width) self.conv2=nn.Conv2d(width,width,kernel_size=3 ,stride=stride,padding=1 ,groups=groups) self.bn2=nn.BatchNorm2d(width) self.conv3=nn.Conv2d(width,out_channels*self.expansion,kernel_size=1 ,stride=1 ,padding=0 ) self.bn3=nn.BatchNorm2d(out_channels*self.expansion) self.relu=nn.ReLU() self.identity_downsample=identity_downsample def forward (self,x ): identity=x x=self.conv1(x) x=self.bn1(x) x=self.relu(x) x=self.conv2(x) x=self.bn2(x) x=self.relu(x) x=self.conv3(x) x=self.bn3(x) if self.identity_downsample is not None : identity=self.identity_downsample(identity) x+=identity x=self.relu(x) return x
上面的代码是从我们之前发过的ResNet文章中复制并加以微小修改得到的。
具体地,添加了两个参数:groups,width_per_group。这两个参数就是上面提到的”分组数”和”每个组内的卷积核个数”。
width=int(out_channels (width_per_group/64))groups 这句代码实现了将ResNet-50中第一个1x1卷积和3x3卷积的输出特征图个数增加一倍的操作,这样就得到了ResNeXt-50中相应的输出特征图个数。
当这两个参数采用默认值时,就是ResNet-50的block。
现在来实现完整的ResNeXt-50:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 class ResNeXt (nn.Module ): def __init__ (self,block,layers,image_channels,num_classes,groups=1 ,width_per_group=64 ): super ().__init__() self.in_channels=64 self.conv1=nn.Conv2d(image_channels,64 ,kernel_size=7 ,stride=2 ,padding=3 ) self.bn1=nn.BatchNorm2d(64 ) self.relu=nn.ReLU() self.maxpool=nn.MaxPool2d(kernel_size=3 ,stride=2 ,padding=1 ) self.layer1=self._make_layer(block,layers[0 ],out_channels=64 ,stride=1 ,groups=groups,width_per_group=width_per_group) self.layer2=self._make_layer(block,layers[1 ],out_channels=128 ,stride=2 ,groups=groups,width_per_group=width_per_group) self.layer3=self._make_layer(block,layers[2 ],out_channels=256 ,stride=2 ,groups=groups,width_per_group=width_per_group) self.layer4=self._make_layer(block,layers[3 ],out_channels=512 ,stride=2 ,groups=groups,width_per_group=width_per_group) self.avgpool=nn.AdaptiveAvgPool2d((1 ,1 )) self.fc=nn.Linear(512 *4 ,num_classes) def forward (self,x ): x=self.conv1(x) x=self.bn1(x) x=self.relu(x) x=self.maxpool(x) x=self.layer1(x) x=self.layer2(x) x=self.layer3(x) x=self.layer4(x) x=self.avgpool(x) x=x.reshape(x.shape[0 ],-1 ) x=self.fc(x) return x def _make_layer (self,block,num_residual_blocks,out_channels,stride,groups,width_per_group ): identity_downsample=None layers=[] if stride !=1 or self.in_channels!=out_channels*4 : identity_downsample=nn.Sequential(nn.Conv2d(self.in_channels,out_channels*4 ,kernel_size=1 ,stride=stride), nn.BatchNorm2d(out_channels*4 )) layers.append(block(self.in_channels,out_channels,identity_downsample,stride,groups,width_per_group)) self.in_channels=out_channels*4 for i in range (num_residual_blocks-1 ): layers.append(block(self.in_channels,out_channels,groups=groups,width_per_group=width_per_group)) return nn.Sequential(*layers)
这段代码同样是复制于ResNet-50,并做了一点修改。具体地,添加groups和width_per_group这两个参数,并在_make_layer
方法中调用block
类的地方传入这两个参数。
看,我们只是在ResNet-50的基础上做了一点点修改,就得到了ResNeXt-50。
如果你对上述代码中某些细节有困惑,请阅读我们之前推送的关于ResNet的文章。
最后,还是老规矩,来测试一下:
参考: