神經網絡學習小記錄59——Pytorch搭建常見分類網絡平台(VGG16、MobileNetV2、ResNet50)
- 學習前言
- 源碼下載下傳
- 分類網絡的常見形式
- 分類網絡介紹
-
- 1、VGG16網絡介紹
- 2、MobilenetV2網絡介紹
- 3、ResNet50網絡介紹
-
- a、什麼是殘差網絡
- b、什麼是ResNet50模型
- 分類網絡的訓練
-
- 1、LOSS介紹
- 2、利用分類網絡進行訓練
學習前言
才發現做了這麼多的部落格和視訊,居然從來沒有系統地做過分類網絡,做一個科學的分類網絡,對身體好。

源碼下載下傳
https://github.com/bubbliiiing/classification-pytorch
喜歡的可以點個star噢。
分類網絡的常見形式
常見的分類網絡都可以分為兩部分,一部分是特征提取部分,另一部分是分類部分。
特征提取部分的功能是對輸入進來的圖檔進行特征提取,優秀的特征可以幫助更容易區分目标,是以特征提取部分一般由各類卷積組成,卷積擁有強大的特征提取能力;
分類部分會利用特征提取部分擷取到的特征進行分類,分類部分一般由全連接配接組成,特征提取部分擷取到的特征一般是一維向量,可以直接進行全連接配接分類。
通常情況下,特征提取部分就是我們平常了解到的各種神經網絡,比如VGG、Mobilenet、Resnet等等;而分類部分就是一次或者幾次的全連接配接,最終我們會獲得一個長度為num_classes的一維向量。
分類網絡介紹
1、VGG16網絡介紹
VGG是由Simonyan 和Zisserman在文獻《Very Deep Convolutional Networks for Large Scale Image Recognition》中提出卷積神經網絡模型,其名稱來源于作者所在的牛津大學視覺幾何組(Visual Geometry Group)的縮寫。
該模型參加2014年的 ImageNet圖像分類與定位挑戰賽,取得了優異成績:在分類任務上排名第二,在定位任務上排名第一。
它的結構如下圖所示:
這是一個VGG16被用到爛的圖,但确實很好的反應了VGG16的結構,整個VGG16由三種不同的層組成,分别是卷積層、最大池化層、全連接配接層。
VGG16的具體執行方式如下:
1、一張原始圖檔被resize到(224,224,3)。
2、conv1:進行兩次[3,3]卷積網絡,輸出的特征層為64,輸出為(224,224,64),再進行2X2最大池化,輸出net為(112,112,64)。
3、conv2:進行兩次[3,3]卷積網絡,輸出的特征層為128,輸出net為(112,112,128),再進行2X2最大池化,輸出net為(56,56,128)。
4、conv3:進行三次[3,3]卷積網絡,輸出的特征層為256,輸出net為(56,56,256),再進行2X2最大池化,輸出net為(28,28,256)。
5、conv4:進行三次[3,3]卷積網絡,輸出的特征層為512,輸出net為(28,28,512),再進行2X2最大池化,輸出net為(14,14,512)。
6、conv5:進行三次[3,3]卷積網絡,輸出的特征層為512,輸出net為(14,14,512),再進行2X2最大池化,輸出net為(7,7,512)。
7、對結果進行平鋪。
8、進行兩次神經元為4096的全連接配接層。
8、全連接配接到1000維上,用于進行分類。
最後輸出的就是每個類的預測。
實作代碼如下:
import torchvision
import torch
import torch.nn as nn
from torchvision.models.utils import load_state_dict_from_url
model_urls = {
'vgg16': 'https://download.pytorch.org/models/vgg16-397923af.pth',
}
class VGG(nn.Module):
def __init__(self, features, num_classes=1000, init_weights=True):
super(VGG, self).__init__()
self.features = features
self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, num_classes),
)
if init_weights:
self._initialize_weights()
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
def make_layers(cfg, batch_norm=False):
layers = []
in_channels = 3
for v in cfg:
if v == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
return nn.Sequential(*layers)
cfgs = {
'D': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
}
def vgg16(pretrained=False, progress=True, num_classes=1000):
model = VGG(make_layers(cfgs['D']))
if pretrained:
state_dict = load_state_dict_from_url(model_urls['vgg16'], model_dir='./model_data',
progress=progress)
model.load_state_dict(state_dict,strict=False)
if num_classes!=1000:
model.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
nn.Linear(4096, num_classes),
)
return model
2、MobilenetV2網絡介紹
MobileNetV2是MobileNet的更新版,它具有一個非常重要的特點就是使用了Inverted resblock,整個mobilenetv2都由Inverted resblock組成。
Inverted resblock可以分為兩個部分:
左邊是主幹部分,首先利用1x1卷積進行升維,然後利用3x3深度可分離卷積進行特征提取,然後再利用1x1卷積降維。
右邊是殘差邊部分,輸入和輸出直接相接。
整體網絡結構如下:(其中Inverted resblock進行的操作就是上述結構)
在利用特征提取部分完成輸入圖檔的特征提取後,我們會利用全局平均池化将特征層調整成一個特征長條,我們可以将特征長條進行全連接配接,獲得最終的分類結果。
實作代碼如下:
from torch import nn
from torchvision.models.utils import load_state_dict_from_url
__all__ = ['MobileNetV2', 'mobilenet_v2']
model_urls = {
'mobilenet_v2': 'https://download.pytorch.org/models/mobilenet_v2-b0353104.pth',
}
def _make_divisible(v, divisor, min_value=None):
if min_value is None:
min_value = divisor
new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
if new_v < 0.9 * v:
new_v += divisor
return new_v
class ConvBNReLU(nn.Sequential):
def __init__(self, in_planes, out_planes, kernel_size=3, stride=1, groups=1):
padding = (kernel_size - 1) // 2
super(ConvBNReLU, self).__init__(
nn.Conv2d(in_planes, out_planes, kernel_size, stride, padding, groups=groups, bias=False),
nn.BatchNorm2d(out_planes),
nn.ReLU6(inplace=True)
)
class InvertedResidual(nn.Module):
def __init__(self, inp, oup, stride, expand_ratio):
super(InvertedResidual, self).__init__()
self.stride = stride
assert stride in [1, 2]
hidden_dim = int(round(inp * expand_ratio))
self.use_res_connect = self.stride == 1 and inp == oup
layers = []
if expand_ratio != 1:
layers.append(ConvBNReLU(inp, hidden_dim, kernel_size=1))
layers.extend([
ConvBNReLU(hidden_dim, hidden_dim, stride=stride, groups=hidden_dim),
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
])
self.conv = nn.Sequential(*layers)
def forward(self, x):
if self.use_res_connect:
return x + self.conv(x)
else:
return self.conv(x)
class MobileNetV2(nn.Module):
def __init__(self, num_classes=1000, width_mult=1.0, inverted_residual_setting=None, round_nearest=8):
super(MobileNetV2, self).__init__()
block = InvertedResidual
input_channel = 32
last_channel = 1280
if inverted_residual_setting is None:
inverted_residual_setting = [
# t, c, n, s
[1, 16, 1, 1],
[6, 24, 2, 2],
[6, 32, 3, 2],
[6, 64, 4, 2],
[6, 96, 3, 1],
[6, 160, 3, 2],
[6, 320, 1, 1],
]
if len(inverted_residual_setting) == 0 or len(inverted_residual_setting[0]) != 4:
raise ValueError("inverted_residual_setting should be non-empty "
"or a 4-element list, got {}".format(inverted_residual_setting))
input_channel = _make_divisible(input_channel * width_mult, round_nearest)
self.last_channel = _make_divisible(last_channel * max(1.0, width_mult), round_nearest)
features = [ConvBNReLU(3, input_channel, stride=2)]
for t, c, n, s in inverted_residual_setting:
output_channel = _make_divisible(c * width_mult, round_nearest)
for i in range(n):
stride = s if i == 0 else 1
features.append(block(input_channel, output_channel, stride, expand_ratio=t))
input_channel = output_channel
features.append(ConvBNReLU(input_channel, self.last_channel, kernel_size=1))
self.features = nn.Sequential(*features)
self.classifier = nn.Sequential(
nn.Dropout(0.2),
nn.Linear(self.last_channel, num_classes),
)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out')
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, nn.BatchNorm2d):
nn.init.ones_(m.weight)
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.zeros_(m.bias)
def forward(self, x):
x = self.features(x)
x = x.mean([2, 3])
x = self.classifier(x)
return x
def mobilenet_v2(pretrained=False, progress=True, num_classes=1000):
model = MobileNetV2()
if pretrained:
state_dict = load_state_dict_from_url(model_urls['mobilenet_v2'], model_dir='./model_data',
progress=progress)
model.load_state_dict(state_dict)
if num_classes!=1000:
model.classifier = nn.Sequential(
nn.Dropout(0.2),
nn.Linear(model.last_channel, num_classes),
)
return model
3、ResNet50網絡介紹
a、什麼是殘差網絡
Residual net(殘差網絡):
将靠前若幹層的某一層資料輸出直接跳過多層引入到後面資料層的輸入部分。
意味着後面的特征層的内容會有一部分由其前面的某一層線性貢獻。
其結構如下:
深度殘差網絡的設計是為了克服由于網絡深度加深而産生的學習效率變低與準确率無法有效提升的問題。
b、什麼是ResNet50模型
ResNet50有兩個基本的塊,分别名為Conv Block和Identity Block,其中Conv Block輸入和輸出的次元是不一樣的,是以不能連續串聯,它的作用是改變網絡的次元;Identity Block輸入次元和輸出次元相同,可以串聯,它的作用是加深網絡的。
Conv Block的結構如下,由圖可以看出,Conv Block可以分為兩個部分,左邊部分為主幹部分,存在兩次卷積、标準化、激活函數和一次卷積、标準化;右邊部分為殘差邊部分,存在一次卷積、标準化,由于殘差邊部分存在卷積,是以我們可以利用Conv Block改變輸出特征層的寬高和通道數:
Identity Block的結構如下,由圖可以看出,Identity Block可以分為兩個部分,左邊部分為主幹部分,存在兩次卷積、标準化、激活函數和一次卷積、标準化;右邊部分為殘差邊部分,直接與輸出相接,由于殘差邊部分不存在卷積,是以Identity Block的輸入特征層和輸出特征層的shape是相同的,可用于加深網絡:
Conv Block和Identity Block都是殘差網絡結構。
總的網絡結構如下:
實作代碼如下:
import torch
import torch.nn as nn
from torchvision.models.utils import load_state_dict_from_url
model_urls = {
'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth',
}
def conv3x3(in_planes, out_planes, stride=1, groups=1, dilation=1):
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=dilation, groups=groups, bias=False, dilation=dilation)
def conv1x1(in_planes, out_planes, stride=1):
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
class Bottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None, groups=1,
base_width=64, dilation=1, norm_layer=None):
super(Bottleneck, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.)) * groups
# Both self.conv2 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv1x1(inplanes, width)
self.bn1 = norm_layer(width)
self.conv2 = conv3x3(width, width, stride, groups, dilation)
self.bn2 = norm_layer(width)
self.conv3 = conv1x1(width, planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = 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:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000, zero_init_residual=False,
groups=1, width_per_group=64, replace_stride_with_dilation=None,
norm_layer=None):
super(ResNet, self).__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
self._norm_layer = norm_layer
self.inplanes = 64
self.dilation = 1
if replace_stride_with_dilation is None:
replace_stride_with_dilation = [False, False, False]
if len(replace_stride_with_dilation) != 3:
raise ValueError("replace_stride_with_dilation should be None "
"or a 3-element tuple, got {}".format(replace_stride_with_dilation))
self.groups = groups
self.base_width = width_per_group
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3,
bias=False)
self.bn1 = norm_layer(self.inplanes)
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,
dilate=replace_stride_with_dilation[0])
self.layer3 = self._make_layer(block, 256, layers[2], stride=2,
dilate=replace_stride_with_dilation[1])
self.layer4 = self._make_layer(block, 512, layers[3], stride=2,
dilate=replace_stride_with_dilation[2])
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
def _make_layer(self, block, planes, blocks, stride=1, dilate=False):
norm_layer = self._norm_layer
downsample = None
previous_dilation = self.dilation
if dilate:
self.dilation *= stride
stride = 1
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
conv1x1(self.inplanes, planes * block.expansion, stride),
norm_layer(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample, self.groups,
self.base_width, previous_dilation, norm_layer))
self.inplanes = planes * block.expansion
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes, groups=self.groups,
base_width=self.base_width, dilation=self.dilation,
norm_layer=norm_layer))
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 = torch.flatten(x, 1)
x = self.fc(x)
return x
def resnet50(pretrained=False, progress=True, num_classes=1000):
model = ResNet(Bottleneck, [3, 4, 6, 3])
if pretrained:
state_dict = load_state_dict_from_url(model_urls['resnet50'], model_dir='./model_data',
progress=progress)
model.load_state_dict(state_dict)
if num_classes!=1000:
model.fc = nn.Linear(512 * model.block.expansion, num_classes)
return model
分類網絡的訓練
1、LOSS介紹
一般而言,分類網絡所使用的損失函數為交叉熵損失函數,英文名為Cross Entropy,實作公式如下。
其中:
- [ M M M] ——類别的數量;
- [ y i c y_{ic} yic] ——真實标簽(0或1),當第i個樣本屬于c類時,值為1,否則為0;
- [ p i c p_{ic} pic] ——預測結果,第i個樣本屬于c類的預測機率;
- [ i i i] ——表示第幾個樣本。
2、利用分類網絡進行訓練
整個項目結構如下:
datasets檔案夾下存放的是訓練圖檔,分為兩部分,train裡面是訓練圖檔,test裡面是測試圖檔。
在訓練之前需要首先準備好資料集,資料集格式為在train和test檔案夾下分不同的檔案夾,每個檔案夾的名稱為對應的類别名稱,檔案夾下面的圖檔為這個類的圖檔。
在準備好資料集後,需要在根目錄運作txt_annotation.py生成訓練所需的cls_train.txt,運作前需要修改其中的classes,将其修改成自己需要分的類。
之後修改model_data檔案夾下的cls_classes.txt,使其也對應自己需要分的類。
在train.py裡面調整自己要選擇的網絡和權重後,就可以開始訓練了!