实例:
net = resnet50()
feature_extractor = nn.Sequential(*list(net.children())[:7])
conv4_block1 = feature_extractor[-1][0]
conv4_block1.conv1.stride = (1, 1)
conv4_block1.conv2.stride = (1, 1)
conv4_block1.downsample[0].stride = (1, 1)
output=feature_extractor(input_)
直接利用net.children()获取网络最外层的结构,同时规定你想要哪些段的结构
-
net.children返回的是一个生成器,所以需要一个遍历的工具,这里选择的是list,此时list里面装的就是net的各个最大的模块,由于resnet50一共由最外层的10个模块组成,它们分别是:
①Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
★
②BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
★
③ReLU(inplace=True)
★
④MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
★
⑤ s t a g e 1 stage_1 stage1★
⑥ s t a g e 2 stage_2 stage2★
⑦ s t a g e 3 stage_3 stage3★
⑧ s t a g e 4 stage_4 stage4
⑨AdaptiveAvgPool2d(output_size=(1, 1))
⑩Linear(in_features=2048, out_features=1000, bias=True)
其中①到④,⑨⑩都是类的初始化,⑤到⑧属于由Sequential包起来的大模块。 -
利用*号将列表里面的各个元素全部解耦出来然后装进Sequential里面组成一个新的网络。
-
所以下面的代码实际是想说从
feature_extractor
这个Sequential的对象里面利用索引来提取最后一个网络模块,但是好巧不巧,这最后一个模块还是一个Sequential的实例,我们记作A,所以还可以通过索引的方式来获取A下最后一个网络模块。 -
可是区别就来了,那么为什么conv4_block1 不能继续用索引的方式来获取conv1,conv2等,而要使用类似于调用方法的形式呢?
-
所以一定是Sequential的原因,进去看看源码,果然发现了里面带有魔法函数__getitem__,所以它可以使用索引的方式来获取里面的每个网络层。
-
而conv4_block1 不能使用block的原因是因为conv4_block1 本身是一个自定义的网络层,它里面有它自己的运算逻辑,所以需要把它认为是一个整体并且独立的网络结构,只能使用调用里面参数的形式(虽然长得像调用方法的形式,实际是调用里面的属性),说具体点conv4_block1的部分结构长这样↓:
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, in_channel, out_channel, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=1, stride=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channel)
所以conv4_block1 .conv1
就是第一个卷积层。conv4_block1 .conv1.stride
实际也是调用属性,修改该卷积的步长属性。这就是为什么有的网络结构采用索引,有的采用调用属性的方式的原因。
总结:我们可以使用别人已经预训练好的网络,例如resnet50来作为我们自己网络的backbone结构,但是完整的resnet50网络屁股后面有分类层,但是我们只需要它特征提取的部分,所以就可以采用上面的方式只讲特征提取的部分拿出来,剪枝掉后面的分类层。这里需要注意两个问题:
- 一个预训练的网络除了对它进行剪枝以后,并不代表其他参数就不能动,例如resnet50中任意一个卷积层,它实际保留的参数权重是卷积核里面参数的权重,换句话说我们在操作卷积核的时候,是可以改变卷积核的步长的,就像上面实例的操作一样。但是有一些参数则即使改变了也无效,例如卷积核的通道数或数量,这些在预训练的时候就已经确定死了,你就算把设计参数权重的信息修改了,最后的结果也不会变。
- 采用预训练网络,我们的目的除了是直接套用它成熟的提取特征的网络架构,主要还是想要为了减少训练时间,由于预训练网络的参数权重已经训练好了,尽管你连接的下游任务可能并不和它的训练任务完全匹配,但是也是具有一定的参考价值,所以可以选择将预训练部分的权重冻结,即将预训练部分的权重require_grad设置为False。同时也可以让预训练跟着下游任务一起训练,或许也可以减少训练时间,说不准,甚至最后的结果是好是坏也说不准,所以需要自己不断尝试。