resnet 和 vgg 是重要的backbone大诸,而且許多網(wǎng)絡(luò)結(jié)構(gòu)都是以vgg 和resnet為原型進(jìn)行創(chuàng)作抵卫,充分的理解這兩個(gè)基本網(wǎng)絡(luò)結(jié)構(gòu)的代碼是十分重要的,本次就詳細(xì)解讀一下這兩個(gè)基本網(wǎng)絡(luò)結(jié)構(gòu)代碼,代碼來(lái)自 torchvision.models :
1 resnet代碼:
隨便建立一個(gè) .py 文件架专,然后:
from torchvision.models import *
m1 = resnet34()
就可以找到resnet的pytorch源碼。下面先看一下resnet的層次結(jié)構(gòu)圖:
注意觀察一下玄帕,resnet18 部脚、34、50裤纹、101委刘、152的是非常相似的丧没,都是分為5個(gè)stage(stage是什么一查便知;而且通常分辨率降低同時(shí)我們會(huì)增加通道數(shù)目锡移,因?yàn)橐话阏J(rèn)為深層的網(wǎng)絡(luò)可以提取出更加豐富的語(yǔ)義信息呕童。隨著網(wǎng)絡(luò)的加深一般我們會(huì)讓分辨率降低而讓通道數(shù)增加:也就是一般認(rèn)為通道是攜帶高級(jí)語(yǔ)義信息的基本單位,隨著網(wǎng)絡(luò)越深淆珊,提取的語(yǔ)義信息越豐富夺饲,需要用來(lái)表達(dá)語(yǔ)義信息的單位也就越來(lái)越多,所以需要通道數(shù)目越多施符。)往声。
開始都是一個(gè)卷積接著一個(gè)maxpooling,只不過(guò)在后面的每個(gè)stage中戳吝,resnet18 浩销、34、50听哭、101撼嗓、152的卷積層個(gè)數(shù)是不一樣的。他們都是通過(guò)往上堆疊一個(gè)個(gè)的基本模塊欢唾,然后使得網(wǎng)絡(luò)達(dá)到較深的層數(shù)且警。
在較為淺層的resnet中(resnet1,resnet34)中使用的基本模塊叫BasicBlock,它由兩個(gè) (3, 3, out_plane)的Conv2d 堆疊而成礁遣。在使用這個(gè)BasicBlock時(shí)候斑芜,只需要根據(jù) 堆疊具體參數(shù):輸入輸出通道數(shù)目,堆疊幾個(gè)BasicBlock祟霍,就能確定每個(gè)stage中basicblock的基本使用情況杏头;在較為深層的resnet中(resnt50,resnet101沸呐,resnet152)醇王,既能增加模塊深度,又能減少參數(shù)量崭添,使用的是一種瓶頸結(jié)構(gòu)Bottleneck寓娩,它由 (1,1, ) ,(3,3)呼渣,(1,1)堆疊而成棘伴,使用方法和BasicBlock基本類似。
在2,3,4,5個(gè)stage中屁置,resnet18的在各個(gè)stage中使用的基本模塊的數(shù)目為:[2,2,2,2]焊夸;resnet34的在各個(gè)stage中的基本模塊的數(shù)目為:[3,4,6,3];resnet50的在各個(gè)stage中的基本模塊的數(shù)目為:[3,4,6,3]蓝角;resnet101的在各個(gè)stage中的基本模塊的數(shù)目為:[3,4,23,3]阱穗;resnet18的在各個(gè)stage中的基本模塊的數(shù)目為:[3,8,36,3]饭冬;
下面以 resnet18 和 resnet 50 為代表詳細(xì)解釋代碼:
(1)resnet 18建立(寫完resnet18的建立發(fā)現(xiàn)根本沒(méi)有必要寫resnet 50了,哈哈哈哈哈揪阶。伍伤。。遣钳。。麦乞。蕴茴。。)
通過(guò)調(diào)用 已經(jīng)定義好的 resnet18()函數(shù)姐直,return 一個(gè)resnet18 model實(shí)例倦淀, 建立resnet18實(shí)例:
def resnet18(pretrained=False, **kwargs):
"""Constructs a ResNet-18 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet18']))
return model
——————————————————————————————————
1)BasicBlock() 殘差塊 解釋
resnet18() 函數(shù)調(diào)用ResNet() 類,通過(guò)輸入初始化參數(shù):BasicBlock 声畏,[2,2,2,2]撞叽,實(shí)例化一個(gè)resnet18 model:
先看下ResNet()大類,輸入哪些初始化話參數(shù)就可以實(shí)例化為 resnet18 模型:BasicBlock
class BasicBlock(nn.Module):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None):
super(BasicBlock, self).__init__()
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = nn.BatchNorm2d(planes)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x # 其實(shí)這里不應(yīng)該叫residual插龄,應(yīng)該寫為:identity mapping = x愿棋,用identity mapping代替residual
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
下面附上 基礎(chǔ)模塊BasicBlock 也就是 殘差塊residual block 的圖解釋:
BasicBlock要解決的一個(gè)重要問(wèn)題就是,identity mapping這個(gè)直連的維度 和 F(x) 輸出的維度不一樣無(wú)法直接相加的問(wèn)題:采用一個(gè)kernel=1的conv2d卷積核融合并降低通道信息均牢,如果H/W尺度也不一樣就設(shè)計(jì)stride糠雨。下面是在ResNet()定義中 定義的一個(gè)下采樣模塊,在BasicBlock實(shí)例化的時(shí)候作為了輸入?yún)?shù)徘跪。
只要 stride>1 或者 輸入和輸出通道數(shù)目不同 都可以斷定 residul F(x)部分產(chǎn)生的 feature maps 相對(duì)于原來(lái)的feature maps的分辨率降低了甘邀,此時(shí)的 identity map 都要進(jìn)行下采樣。也就是identity map部分要和 residual 部分進(jìn)行相同的尺寸變換(包括H/W 和 channel)垮庐,這兩部分 的輸入輸出通道(planes)要相同松邪, stride(H/W)也相同。
————————————————————————————————————————
2)制作stage的函數(shù) __make_layer() 解釋
注意在resnet18()函數(shù)中哨查,直接調(diào)用了BasicBlock類作為 實(shí)參逗抑,并沒(méi)有使用BasicBlock 實(shí)例:
而是在實(shí)例化 ResNet()類的時(shí)候?qū)嵗?BasicBlock()類(這是定義的 ResNet()類 中的成員函數(shù)_make
_layer() 的代碼,下面先解釋完這個(gè)十分重要的成員函數(shù)代碼寒亥,然后再附上ResNet()類代碼):
_make_layer() 成員函數(shù)锋八,是用來(lái)制作每個(gè)stage中的網(wǎng)絡(luò)結(jié)構(gòu)的函數(shù),其的 形參 包含block, planes, blocks, stride 四個(gè)參數(shù):
block:基本模塊選擇誰(shuí)(前面提到過(guò)基本模塊包含 BasicBlock 和 Bottleneck兩個(gè)基本模塊)
planes:這是每個(gè)stage中护盈,與每個(gè)block的輸出通道相關(guān)的參數(shù)( 查看代碼就知道挟纱,如果使用的是BasicBlock那么planes就等于這個(gè)block的輸出通道數(shù)目,如果使用的是Bottleneck那么這個(gè)block的輸出通道數(shù)目就是planes的4倍)腐宋。
_make_layer()要解決:根據(jù)不同的基本block紊服,完成一個(gè)stage 網(wǎng)絡(luò)結(jié)構(gòu)的構(gòu)建檀轨。
3) __make_layer() 中用到的重要參數(shù) 類屬性expansion 和 成員變量self.inplanes
BasicBlock()(或Bottleneck())類中的類屬性expandsion,用來(lái)指定下一個(gè)BasicBlock的輸入通道是多少欺嗤。因?yàn)榫退阍趕tage中参萄,第一個(gè)block結(jié)束之后,下一個(gè)block的輸入通道數(shù)目已經(jīng)變化了煎饼,已經(jīng)不是 同一個(gè)stage 的 第一個(gè)block 的輸入通道數(shù)目讹挎。self.inplanes 的重要作用:self.inplanes一共有在block中兩次使用:
每個(gè)stage中(一個(gè)_make_layer()就是一個(gè)stage),第一次使用時(shí)吆玖,self.inplanes 等于上一個(gè)stage的輸出通道數(shù)筒溃,后面self.inplanes都等于同一個(gè)數(shù)目,就是每個(gè)block的輸出通道數(shù)目沾乘。
因?yàn)榉譃锽asicBlock()和Bottleneck() 兩個(gè)基本的block類怜奖,對(duì)應(yīng)不同深度的resnet,這兩種block最后的輸出通道是不一樣的翅阵,為了標(biāo)記這兩個(gè)類輸出通道數(shù)目的不同歪玲,設(shè)置了一個(gè)類屬性expansion。根據(jù)類屬性expansion和我們指定的輸出通道參數(shù)planes掷匠,可以確定對(duì)于這兩種block 結(jié)束之后的輸出通道數(shù)目滥崩。
———————————————————————————————————————————
4) Resnet類 代碼
說(shuō)完了上面的部分resnet的代碼已經(jīng)沒(méi)有完全可以看懂了。
ResNet() 類代碼:
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
self.inplanes = 64 # 每一個(gè)block的輸入通道數(shù)目
super(ResNet, self).__init__()
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
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.AvgPool2d(7, stride=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, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
def _make_layer(self, block, planes, blocks, stride=1):
downsample = None
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),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
for i in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
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.view(x.size(0), -1)
x = self.fc(x)
return x
整體思路就是把每個(gè)stage保存為一個(gè)單元讹语。
在制作每一個(gè)stage的時(shí)候夭委,把這stage中的 每個(gè)Basicblock,按照順序append到一個(gè) 列表layer 中募强,當(dāng)添加完這個(gè)stage中的所有block株灸,把這個(gè)列表放入nn.Sequential()中,就把構(gòu)建好的這個(gè)stage網(wǎng)絡(luò)模型放到了計(jì)算圖中擎值。