作为Pytorch的最大优势之一的“动态性”体现在哪里呢?
在没有遇到问题前我对这个问题并没有一个明确的答案,看了网上大家略显“官方”的解答,总觉地似懂非懂。动态性对我们平常的使用究竟有什么影响呢?看了下面这个例子也许你会和我一样感到豁然开朗。
一、具体问题:
假设我们在做一个“车载识别”项目,要求输入的是stereo图像(简单理解就是左右“眼”两张图像)。这个时候我们在github上找到了一个项目,它的网络结构设计简直完美,我们非常想借鉴一下,但不巧的是:该项目的输入是mono图像(单张图像);怎么能够在不幅度改变网络结构的情况下,满足我们的项目要求呢?
再说明白一点:我们希望能够基于原作者已经训练好的网络进行fine tuning
二、可能的解决方案
这一节我大概罗列一下我想到的解决方法,不足之处大家可以在评论区补充:
“我太强了,我重构吧”;这个方法的要求首先当然是你??(但我不是),其次就是所有的网络结构需要与原项目一致,仅在我们需要的地方进行调整,之后再一点一点来实现state load;“既然是输入的问题,我能不能保留后面所有结构不变,只对第一层进行重新构造呢?”;猛地一项好像是没有问题,但是要知道我们现在进行的是fine tuning,后层网络的参数依赖于前面的特征学习过程(这里涉及部分专业知识),如果我们强行打断了网络的输入部分,那么对于后面原来已经学习好的参数而言会产生非常大的影响。这也是为什么一般在进行fine tuning时,我们只截断网络的后段,前面的特征学习阶段是不会进行大幅度调整的。“我都想要,既想保留原来的网络结构,又希望能够使输入stereo图像”;笔者的最终解决方案就是如此,下面让我们具体来看看。
三、解决方案
我们来将问题具体化,假设原来网络结构的base_layer(接收输入的层次)结构是:
torch.nn.Conv2d(input_channels=3, output_channels=16, kernel_size = 3)
可以清晰的看出,网络接收的图像为3个channel,也就是一张RGB图像,而我们希望将其变为:
torch.nn.Conv2d(6, 16, 3)
这样我们就能够接收6个channel,只需要将left以及right图像stack一下就能够满足我们的输入需求,那么我们如何实现既保留原来的参数同时改变层次结构呢?
掰开来看,假设现在我们的网络整个就这一层,那么解决方案是:
# 原层次,假设已经完成了state load
origin_conv = nn.Conv2d(3, 16, 3)
# 获取原训练权重
origin_weight = origin_conv.weight # 注意:这里我们没有获取bias,只获取了weight
origin_dict = origin_conv.state_dict()
new_weights = torch.cat((origin_weight, origin_weight), dim=1) # 这里使对左右图像的参数相同
# 构建新层次
new_conv = nn.Conv2d(6, 16, 3)
new_conv.load_state_dict({'weight': new_weights, 'bias':origin_dict['bias])
好啦,至此新的conv层就具有了原来的训练参数。但是到目前为止我们仍然是对一个单独的层次进行调整,那么如何对一个大型网络结构中的某个小层进行调整呢?
假设我们有如下的网络结构:
NET(
(base): baseNET(
(base_layer): Sequential(
(0): Conv2d(3, 16, kernel_size=(7, 7), stride=(1, 1), padding=(3, 3), bias=False)
(1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
)
(level0): Sequential(
(0): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(1): BatchNorm2d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
)
(level1): Sequential(
(0): Conv2d(16, 32, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
)
(level2): Tree(
(tree1): BasicBlock(
(conv1): Conv2d(32, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
......下面还有老大一串
在这种情况下,我们如何调整NET中的baseNET中的base_layer中的第一个Conv层呢?(累死了。。。)看到这里我相信大家已经有点明白“动态性”的意思了,在网络结构已经成型的情况下,Pytorch仍然允许我们对结构进行调整,这当然极大地方便了我们的操作,下面来看具体的解决方案:
# 假设外层网络为model
model = NET(*args, **kargs)
# 获取baseNET
base_Net = model._modules['baseNET'] # 注意这里是“隐藏变量”_modules,而不是modules,modules是not subscripable的
# 获取base_layer
base_layer = base_Net._modules['base_layer']
# 得到原来的conv层
origin_conv = base_layer[0] # 注意,base_layer是nn.Sequential,所以可以直接索引
# 接下来的操作相信不用我多说啦,就是上面的简单情况
.......
# 假设已经得到了新的conv层
new_conv = ...
base_layer[0] = new_conv # 结束!!
至此,我们就对一个复杂网络中的某一个小层次完成了结构替换,同时也“保留”了原网络的参数,效果好像还不错?
四、小节
本篇文章是笔者在实际的项目中碰到的一个小问题,记录一下解决方法,可能还有很多其他更方便更快捷的方法我没有想到,毕竟小白一枚,各位看官如果想到了其他更加方便有效的方法,欢迎在评论区留言讨论!
点赞 + 关注 !!!