暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

经典卷积架构的PyTorch实现:ResNeXt

南极Python 2021-06-04
1748

ResNeXt

在前面的文章中,我们已经介绍了ResNetInception的原理及其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:

class block(nn.Module):
    
    #stride只针对第中间的3x3卷积
    #1x1卷积的stride始终是1,1x1卷积只改变通道数,不改变特征图尺寸
    def __init__(self,in_channels,out_channels,identity_downsample=None,stride=1,groups=1,width_per_group=64):
        super().__init__()
        #groups:分组数
        #width_per_group:每个组内的卷积核个数
        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)#stride=2,尺寸减半;stride=1,尺寸不变
        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:

class ResNeXt(nn.Module):#每个残差block重复次数:[3,4,6,3]
    def __init__(self,block,layers,image_channels,num_classes,groups=1,width_per_group=64):
        super().__init__()
        
        self.in_channels=64
        
        #conv1
        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)
        
        #ResNet layers: conv2_x,conv3_x,conv4_x,conv5_x
        self.layer1=self._make_layer(block,layers[0],out_channels=64,stride=1,groups=groups,width_per_group=width_per_group)#stride=1? True ;in_channels=out_channels*4?  False
        self.layer2=self._make_layer(block,layers[1],out_channels=128,stride=2,groups=groups,width_per_group=width_per_group)#stride=1? False ;in_channels=out_channels*4?  False
        self.layer3=self._make_layer(block,layers[2],out_channels=256,stride=2,groups=groups,width_per_group=width_per_group)#stride=1? False ;in_channels=out_channels*4?  False
        self.layer4=self._make_layer(block,layers[3],out_channels=512,stride=2,groups=groups,width_per_group=width_per_group)#stride=1? False ;in_channels=out_channels*4?  False
        
        self.avgpool=nn.AdaptiveAvgPool2d((1,1))
        self.fc=nn.Linear(512*4,num_classes)
        
    def forward(self,x):
        # 输入x的shape: [4,3,224,224]
        
        x=self.conv1(x)
        x=self.bn1(x)
        x=self.relu(x)
        #print(x.shape)#torch.Size([4, 64, 112, 112]),经过conv1,尺寸减半
        
        x=self.maxpool(x)
        #print(x.shape)#torch.Size([4, 64, 56, 56]),经过池化,尺寸减半(严格来说,这个池化层属于conv2_i)
        x=self.layer1(x)
        #print(x.shape)#torch.Size([4, 256, 56, 56])#经过conv2_x,由于stride=1,尺寸不变
        x=self.layer2(x)
        #print(x.shape)#torch.Size([4, 512, 28, 28])#经过conv3_x,由于stride=2,尺寸减半
        x=self.layer3(x)
        #print(x.shape)#torch.Size([4, 1024, 14, 14])#经过conv4_x,由于stride=2,尺寸减半
        x=self.layer4(x)
        #print(x.shape)#torch.Size([4, 2048, 7, 7])#经过conv5_x,由于stride=2,尺寸减半
        
        
        x=self.avgpool(x)
        x=x.reshape(x.shape[0],-1)
        x=self.fc(x)
        
        return x
    
    #每个layer(conv2_i,conv3_i,conv4_i,conv5_i)都有几个重复块,只需要对第一个重复块做downsample就能做跳连了,其余重复块的尺寸和通道数都不会变,因此直接跳连即可
    def _make_layer(self,block,num_residual_blocks,out_channels,stride,groups,width_per_group):
        identity_downsample=None
        layers=[]
        #只有conv2_x的stride=1,其余都为2
        #原始输入需要做些改变,才能做残差连接
        if stride !=1 or self.in_channels!=out_channels*4:
            #print('stride=1?',stride==1,';in_channels=out_channels*4? ',self.in_channels==out_channels*4)
            identity_downsample=nn.Sequential(nn.Conv2d(self.in_channels,out_channels*4,kernel_size=1,stride=stride),#stride=2时,尺寸减半,通道数变了,做downsample才能做跳连
                                             nn.BatchNorm2d(out_channels*4))#stride=1时,尺寸不变,但通道数变了,此时也需要做downsample,这样才能做跳连
        layers.append(block(self.in_channels,out_channels,identity_downsample,stride,groups,width_per_group))#stride=2,尺寸减半;或者stride=1,尺寸不变,但输出通道数变了。这也就是需要downsample的原因。
        self.in_channels=out_channels*4
        
        #其余重复块的stride采用默认值1,不改变尺寸
        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的文章。

最后,还是老规矩,来测试一下:

参考:

  • [1] https://arxiv.org/pdf/1611.05431.pdf
  • [2] https://www.bilibili.com/video/BV1Ap4y1p71v?from=search&seid=18074835348334633376




南极Python交流群已成立,长按下方二维码添加我的微信,备注加群即可,欢迎进群学习交流(划水


              原创不易,感谢点赞,分享和在看的你!

文章转载自南极Python,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论