有了前面的基础,我么就可以搭建语义分割的主干网络,对他进行训练,我们需要Dataloerd来提供数据,下面就来一一介绍dataloder及主干网络的搭建
**
datasets生成
**
我们前面定义好dataset类之后,生成一个VOCdatasets就非常容易了,我们只需对他进行实例化,传入相应参数就可以了。这里我们直接把训练集和验证集的datasets全部实例化,root参数传入的是数据集路径,num_classes是最后分类的数量,crop_size是裁剪的大小。
**
dataloader生成
**
到这里可以发现,我们的datasest只能根据index一张一张的读取图片,这样效率太低了。所以我们还必须实例一个dataloader帮助我们以batch的形式读取图片,以此提升效率。创建这个也非常简单,只用导入Dataloader模块,把datasets传入进去,填写相应参数即可。其中train_datasets是我们之前生成的datases,batch_size是每个批次传入多少张图片,num_work是使用多线程进行读取图片(这里建议windows用户不要填大于1的数,可能会导致崩溃),shuffle表示是否打乱数据,drop_last这个参数是是否丢弃最后无法被batch_size整除的图片,这里最好填入True,不然有可能导致程序出错。
主干网搭建
PSPNet使用的主干网络为ResNet50,是一个非常经典的网络。由于我们进行的是语义分割任务,我们希提取更多浅层的纹理特征,所以在ResNet50前面部分增加了几个卷积层。下面我们依次介绍搭建ResNet50需要的模块。
Conv3x3就是将卷积封装了一下,在我们调用的时候可以省略一些参数。
残差块
下面搭建残差结构,它是ResNet50网络最基本的单元,由1x1 3x3 1x1三个卷积即相应的BN,ReLU构成。
值得注意的是,作者对输入与输出的通道数进行了巧妙设计,使其能够容易堆叠。每个残差块都有shorcut连接,即forward函数中的residual部分,如果输入通道数与输出通道数相等,且步长为1,那么residual直接与残差块的结果相加,如果不相等,且步长不等于1,那么residual升级为downsample模块,通过1x1卷积调整大小和通道数后在与残差块的输出相加。
ResNet50
有了上面的残差块,剩下要做的就是按照条件堆叠出ResNet50即可。关于不同层级的ResNet应该如何堆叠,何凯明的论文中有详细介绍,表中也出了详细的配置方式。前面提到了,我们这里使用加深的ResNet50,相比于原论文,我们使用的resnet50多了两个3x3的卷积层,并且将通道数增加了,更多的浅层特征是有利于语义分割任务的。
接下来就是网络的主体部分,通过调用maker_layer来搭建layer1,2,3,4。它们重复的次数也就是上表中不同残差块后的x3,x4,x6,x3。
接下来我们进入make_layer,它需要传入4个参数,第一个参数是我们之前定义的残差块,第二个参数是一个通道数,第三个参数是该layer会将残差块重复记次,第四个参数是步长。由于四个layer的堆叠方式相同,只是次数不一样而已,我们就只以layer2的生成进行举例明。通过上面表可知layer1的输出的通道数是256,那么layer2的输入通道数应该和它一样,此时self.inplanes是等于256(644)的。
具体的,进入函数,第一步就是一个判断,我们layer的步长为2,所以这个判断是满足的,就会生成一个downsample模块。由于第一个残差块的第一个1x1卷积层的输入是128通道的,而最后一个1x1卷积的通道数是512通道的,无法直接使用shorcut,所以需要这个downsample模块对输入进行通道和大小的调整。
注意这个时候self.inplanes和plane是256和128 而block.expansion一直是4。
接着往下走,我们会把self.inplane,planes,stride,downsample传入残差模块生成第一个残差块,每个残差块的输入是它的第一个参数也就是self.inplanes(256),输出是inplane4(1284=512)。同时slef.inplanes在这里就变成plane4=128*4=512。后面再通过for循环依次添加,每次只需要将我们传入的值代进去即可。这里由于我们添加了第一个块,所以for循环从1开始,其余的块都是完全一样,只需要我们跟着代码的思路进去理解一遍即可明白其中的奥秘。
具体的细节部分,我们可以打印网络的结果显示出来,验证我们想法。下图为layer2的前两个block,我们可以清晰的发现只用第一个残差块有downsample,为什么其它的都没有呢?这是因为,我们residual没有写在初始化函数里,只存在于forward函数中,所以这里是不会显示的,单其实还是存在的。
分类层
虽然PSPNet是一个端到端的网络,并没有使用分类层,但是我们这里还是顺便搭建完整,避免网络出现错误的时候可以调用这个网络进行测试。它的结构非常简单,就是将最后一层全局平均池化,再通过线性层即可。
整个代码
import math
import os
import torch
import torch.nn as nn
import torch.utils.model_zoo as model_zoo
BatchNorm2d = nn.BatchNorm2d
def conv3x3(in_planes, out_planes, stride=1):
"定义带padding的3x3卷积"
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=1, bias=False)
class Bottleneck(nn.Module):
"搭建resnet使用的残差模块,如果输入通道数不等于输出通道数,会使用1x1卷积调整通道相加"
"其它情况直接通过residual相加"
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
self.bn1 = BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,
padding=1, bias=False)
self.bn2 = BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)
self.bn3 = BatchNorm2d(planes * 4)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
"使用加深的ResNet,在最开始的地方增加了几个卷积层,提取更多浅层特征"
self.inplanes = 128
super(ResNet, self).__init__()
self.conv1 = conv3x3(3, 64, stride=2)
self.bn1 = BatchNorm2d(64)
self.relu1 = nn.ReLU(inplace=True)
self.conv2 = conv3x3(64, 64)
self.bn2 = BatchNorm2d(64)
self.relu2 = nn.ReLU(inplace=True)
self.conv3 = conv3x3(64, 128)
self.bn3 = BatchNorm2d(128)
self.relu3 = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
"调用_maker_layer函数 循环生成多个残差结构"
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
self.avgpool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Linear(512 * block.expansion, num_classes)
# 给模型进行权值初始化
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
# # 判断是否使用1x1卷积升维,默认不采用 只有在输入与输出通道不相等 或者步长不为1的情况下使用
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion,
kernel_size=1, stride=stride, bias=False),
BatchNorm2d(planes * block.expansion),
)
layers = []
# 添加第一个残差块,因为第一个残差块是比较特殊的,可能存在步长为2或者downsample模块,所以单独添加
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
# 跟新self.inplanes 参数,后面的残差块结构相同,直接利用for循环添加
for i in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
x = self.relu1(self.bn1(self.conv1(x)))
x = self.relu2(self.bn2(self.conv2(x)))
x = self.relu3(self.bn3(self.conv3(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.view(x.size(0), -1)
x = self.fc(x)
return x
# 下载预训练权重的链接
model_urls = {
'resnet50': 'http://sceneparsing.csail.mit.edu/model/pretrained_resnet/resnet50-imagenet.pth',
}
def load_url(url, model_dir='./model_data', map_location=None):
if not os.path.exists(model_dir):
os.makedirs(model_dir)
filename = url.split('/')[-1]
cached_file = os.path.join(model_dir, filename)
if os.path.exists(cached_file):
return torch.load(cached_file, map_location=map_location)
else:
return model_zoo.load_url(url,model_dir=model_dir)
def resnet50(pretrained=True, **kwargs):
root = r'D:\1Apython\Pycharm_pojie\Semantic_segmentation\pytorch_segmentation-master\pretrained\resnet50s-a75c83cf.pth'
model = ResNet(Bottleneck, [3, 4, 6, 3], **kwargs)
if pretrained:
# dict = torch.load(root)
# for k,v in dict.items():
# print(k)
# model.load_state_dict()
# print('权值已加载')
model.load_state_dict(load_url(model_urls['resnet50']))
return model
if __name__ == '__main__':
r = resnet50()
print(r)
Dataloader
# 实例化 daloader
train_datasets = VOCDataset(root=voc_root,split='train',num_classes=num_classes,base_size=520,crop_size=crop_size)
val_datasets = VOCDataset(root=voc_root,split='val',num_classes=num_classes,base_size=520,crop_size=crop_size)
train_dataloader = DataLoader(train_datasets,batch_size=batch_size,num_workers=1,shuffle=True,drop_last=True)
val_dataloader = DataLoader(train_datasets,batch_size=batch_size,num_workers=1,shuffle=True,drop_last=True)