“工欲善其事,必先利其器”,掌握ResNet網(wǎng)絡(luò)有必要先了解其原理和源碼。本文分別從原理、源碼、運(yùn)用三個(gè)方面出發(fā)行文,先對(duì)ResNet原理進(jìn)行闡述,然后對(duì)pytorch中的源碼進(jìn)行詳細(xì)解讀,最后再基于遷移學(xué)習(xí)對(duì)模型進(jìn)行調(diào)整、實(shí)戰(zhàn)。本文若有疏漏、需更正、改進(jìn)的地方,望讀者予以指正?。?!
筆者的運(yùn)行環(huán)境:CPU (AMD Ryzen? 5 4600U) + pytorch (1.13,CPU版) + jupyter;本文所用的資源鏈接:https://pan.baidu.com/s/1YWZJTbA7BkmbRnBRFU1qdw ;提取碼:1212。
1. ResNet網(wǎng)絡(luò)原理
1.1. 深度網(wǎng)絡(luò)的退化問題
從經(jīng)驗(yàn)來看,網(wǎng)絡(luò)的深度對(duì)模型的性能至關(guān)重要,當(dāng)增加網(wǎng)絡(luò)層數(shù)后,網(wǎng)絡(luò)可以進(jìn)行更加復(fù)雜的特征模式的提取,所以當(dāng)模型更深時(shí)理論上可以取得更好的結(jié)果。但是更深的網(wǎng)絡(luò)其性能一定會(huì)更好嗎?實(shí)驗(yàn)發(fā)現(xiàn),深度網(wǎng)絡(luò)出現(xiàn)了退化問題(Degradation problem):網(wǎng)絡(luò)深度增加時(shí),網(wǎng)絡(luò)準(zhǔn)確度度出現(xiàn)飽和,甚至下降。這個(gè)現(xiàn)象可以在圖1中直觀看出來:56層的網(wǎng)絡(luò)比20層網(wǎng)絡(luò)效果還要差。這不會(huì)是過擬合問題,因?yàn)?6層網(wǎng)絡(luò)的訓(xùn)練誤差同樣高。我們知道深層網(wǎng)絡(luò)存在著梯度消失或者爆炸的問題,這使得深度學(xué)習(xí)模型很難訓(xùn)練,但是現(xiàn)在已經(jīng)存在一些技術(shù)手段如BatchNorm(簡(jiǎn)稱BN)來緩解這個(gè)問題。因此,出現(xiàn)深度網(wǎng)絡(luò)的退化問題是非常令人詫異的。
1.2. 殘差學(xué)習(xí)
深度網(wǎng)絡(luò)的退化問題說明深度網(wǎng)絡(luò)不容易訓(xùn)練。但是我們考慮這樣一個(gè)事實(shí):現(xiàn)在你有一個(gè)淺層網(wǎng)絡(luò),你想通過向上堆積新層來建立深層網(wǎng)絡(luò),一個(gè)極端情況是這些增加的層什么也不學(xué)習(xí),僅僅復(fù)制淺層網(wǎng)絡(luò)的特征,即這樣新層是恒等映射(Identity mapping)。在這種情況下,深層網(wǎng)絡(luò)應(yīng)該至少和淺層網(wǎng)絡(luò)性能一樣,也不應(yīng)該出現(xiàn)退化現(xiàn)象。好吧,你不得不承認(rèn)肯定是目前的訓(xùn)練方法有問題,才使得深層網(wǎng)絡(luò)很難去找到一個(gè)好的參數(shù)。
這個(gè)有趣的假設(shè)讓何博士靈感爆發(fā),他提出了殘差學(xué)習(xí)來解決退化問題。對(duì)于一個(gè)堆積層結(jié)構(gòu)(幾層堆積而成)當(dāng)輸入為 x 時(shí)其學(xué)習(xí)到的特征記為 H(x) ,現(xiàn)在我們希望其可以學(xué)習(xí)到殘差 F(x)=H(x)?x ,這樣其實(shí)原始的學(xué)習(xí)特征是 F(x)+x 。之所以這樣是因?yàn)闅埐顚W(xué)習(xí)相比原始特征直接學(xué)習(xí)更容易。當(dāng)殘差為0時(shí),此時(shí)堆積層僅僅做了恒等映射,至少網(wǎng)絡(luò)性能不會(huì)下降,實(shí)際上殘差不會(huì)為0,這也會(huì)使得堆積層在輸入特征基礎(chǔ)上學(xué)習(xí)到新的特征,從而擁有更好的性能。殘差學(xué)習(xí)的結(jié)構(gòu)如圖2所示。這有點(diǎn)類似與電路中的“短路”,所以是一種短路連接(shortcut connection)。為什么殘差學(xué)習(xí)相對(duì)更容易,從直觀上看殘差學(xué)習(xí)需要學(xué)習(xí)的內(nèi)容少,因?yàn)闅埐钜话銜?huì)比較小,學(xué)習(xí)難度小點(diǎn)。
1.3. ResNet的網(wǎng)絡(luò)結(jié)構(gòu)
ResNet網(wǎng)絡(luò)是參考了VGG19網(wǎng)絡(luò),在其基礎(chǔ)上進(jìn)行了修改,并通過短路機(jī)制加入了殘差單元,如圖3所示。變化主要體現(xiàn)在ResNet直接使用stride=2的卷積做下采樣,并且用global average pool層替換了全連接層。ResNet的一個(gè)重要設(shè)計(jì)原則是:當(dāng)feature map大小降低一半時(shí),feature map的數(shù)量增加一倍,這保持了網(wǎng)絡(luò)層的復(fù)雜度。從圖3中可以看到,ResNet相比普通網(wǎng)絡(luò)每?jī)蓪娱g增加了短路機(jī)制,這就形成了殘差學(xué)習(xí),其中虛線表示feature map數(shù)量發(fā)生了改變。圖3展示的是34-layer的ResNet,當(dāng)然,還可以構(gòu)建更深的網(wǎng)絡(luò)如圖4所示。從圖4中可以看到,對(duì)于18-layer和34-layer的ResNet,其進(jìn)行的兩層間的殘差學(xué)習(xí),當(dāng)網(wǎng)絡(luò)更深時(shí),其進(jìn)行的是三層間的殘差學(xué)習(xí),三層卷積核分別是1x1,3x3和1x1,一個(gè)值得注意的是隱含層的feature map數(shù)量是比較小的,并且是輸出feature map數(shù)量的1/4。
下面我們?cè)俜治鲆幌職埐顔卧?,ResNet使用兩種殘差單元BasicBlock(圖5左圖)和Bottleneck(圖5右圖),BasicBlock對(duì)應(yīng)的是淺層網(wǎng)絡(luò),而Bottleneck對(duì)應(yīng)的是深層網(wǎng)絡(luò)。對(duì)于短路連接,當(dāng)輸入和輸出維度一致時(shí),可以直接將輸入加到輸出上。但是當(dāng)維度不一致時(shí)(對(duì)應(yīng)的是維度增加一倍),這就不能直接相加。有兩種策略可以解決 :(1)采用zero-padding增加維度,此時(shí)一般要先做一個(gè)downsamp,可以采用strde=2的pooling,這樣不會(huì)增加參數(shù);(2)采用新的映射(projection shortcut),一般采用1x1的卷積,這樣會(huì)增加參數(shù),也會(huì)增加計(jì)算量。短路連接除了直接使用恒等映射,當(dāng)然都可以采用projection shortcut。
2. pytorch中的resnet源碼解讀
在閱讀pytorch中的源碼時(shí),可以參考下圖右三中的網(wǎng)絡(luò)示意,這有助于理解。
2.1. resnet模塊中的類和函數(shù)
首先來看一下我們可以從resnet模塊中導(dǎo)入哪些類和函數(shù),from torchvision.models.resnet import *
導(dǎo)入的類和函數(shù):
__all__ = [
"ResNet",
"ResNet18_Weights",
"ResNet34_Weights",
"ResNet50_Weights",
"ResNet101_Weights",
"ResNet152_Weights",
"ResNeXt50_32X4D_Weights",
"ResNeXt101_32X8D_Weights",
"ResNeXt101_64X4D_Weights",
"Wide_ResNet50_2_Weights",
"Wide_ResNet101_2_Weights",
"resnet18",
"resnet34",
"resnet50",
"resnet101",
"resnet152",
"resnext50_32x4d",
"resnext101_32x8d",
"resnext101_64x4d",
"wide_resnet50_2",
"wide_resnet101_2",
]
2.2. 殘差塊的構(gòu)建
殘差塊是resnet的構(gòu)建單元。根據(jù)深度的不同有兩種殘差塊,分別是BasicBlock
(resnet18和resnet34中的殘差塊)和Bottleneck
(resnet50、resnet101和resnet152中的殘差塊)。兩種殘差塊的的構(gòu)成如下圖所示,
可知,需要1x1,3x3兩種卷積層構(gòu)建,這兩種卷積的定義如下。
def conv3x3(in_planes: int, out_planes: int, stride: int = 1, groups: int = 1, dilation: int = 1) -> nn.Conv2d:
"""3x3 卷積與填充"""
return nn.Conv2d(
in_planes, # 輸入通道(特征圖)數(shù)
out_planes, # 輸出通道(特征圖)數(shù)
kernel_size=3, # 卷積核尺寸
stride=stride, # 卷積時(shí)的步長(zhǎng)
padding=dilation, # 邊緣填充層大小,卷積核為3x3,故padding應(yīng)為1
groups=groups,
bias=False, # 是否采用偏置值
dilation=dilation,
)
def conv1x1(in_planes: int, out_planes: int, stride: int = 1) -> nn.Conv2d:
"""1x1 convolution"""
return nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False)
從上圖以及殘差學(xué)習(xí)原理可知,BasicBlock
的構(gòu)建除卻卷積層外還需標(biāo)準(zhǔn)化層、激活函數(shù)、恒等映射函數(shù),部分BasicBlock
還需要下采樣層用以減少特征圖數(shù)量。因此,BasicBlock
類的定義如下。
class BasicBlock(nn.Module):
expansion: int = 1 # expansion屬性
def __init__( # 初始化
self,
inplanes: int, # 輸入通道(特征圖)數(shù)
planes: int, # 輸出通道(特征圖)數(shù)
stride: int = 1, # 卷積時(shí)的步長(zhǎng),默認(rèn)為1
downsample: Optional[nn.Module] = None, # 下采樣,可選
groups: int = 1, # 分組卷積個(gè)數(shù),默認(rèn)為1,即默認(rèn)采用普通卷積
base_width: int = 64, # 基通道數(shù),常規(guī)resnet不用管,wide resnet就是調(diào)整這個(gè)參數(shù)
dilation: int = 1,
norm_layer: Optional[Callable[..., nn.Module]] = None, # 卷積后數(shù)據(jù)標(biāo)準(zhǔn)化,可選
) -> None:
super().__init__() # 繼承nn.Module的初始化
if norm_layer is None: # 如果不指定數(shù)據(jù)標(biāo)準(zhǔn)化方式,默認(rèn)使用nn.BatchNorm2d
norm_layer = nn.BatchNorm2d
if groups != 1 or base_width != 64: # 采用BasicBlock殘差塊的resnet不允許使用分組卷積和wide resnet
raise ValueError("BasicBlock only supports groups=1 and base_width=64")
if dilation > 1: # 采用BasicBlock殘差塊的resnet不允許使用空洞卷積
raise NotImplementedError("Dilation > 1 not supported in BasicBlock")
# Both self.conv1 and self.downsample layers downsample the input when stride != 1
self.conv1 = conv3x3(inplanes, planes, stride) # 3x3卷積層,若stride=1,特征圖數(shù)量不做改變;若stride!=1進(jìn)行下采樣
self.bn1 = norm_layer(planes) # 數(shù)據(jù)標(biāo)準(zhǔn)化
self.relu = nn.ReLU(inplace=True) # ReLU激活函數(shù)
self.conv2 = conv3x3(planes, planes) # 3x3卷積層,特征圖數(shù)量不做改變
self.bn2 = norm_layer(planes) # 數(shù)據(jù)標(biāo)準(zhǔn)化
self.downsample = downsample # 下采樣,用于處理短路相加時(shí)維度不同的情況
self.stride = stride # 步長(zhǎng)
def forward(self, x: Tensor) -> Tensor: # 前向傳播計(jì)算
identity = x # 輸入張量,(B, C, H, W)
out = self.conv1(x) # 卷積操作
out = self.bn1(out) # 數(shù)據(jù)標(biāo)準(zhǔn)化操作
out = self.relu(out) # 激活函數(shù)操作
out = self.conv2(out) # 卷積操作
out = self.bn2(out) # 數(shù)據(jù)標(biāo)準(zhǔn)化操作
if self.downsample is not None: # 如果為identity指定了下采樣方式則使用該方式,否則做恒等變換
identity = self.downsample(x)
out += identity # 殘差塊學(xué)習(xí)到的特征
out = self.relu(out) # 將該特征經(jīng)激活函數(shù)操作
return out # 返回激活后的特征
Bottleneck
殘差塊與BasicBlock
的區(qū)別在于卷積層的不同,其源碼如下。
class Bottleneck(nn.Module):
"""
注意:原論文中,在虛線殘差結(jié)構(gòu)的主分支上,第一個(gè)1x1卷積層的步距是2,第二個(gè)3x3卷積層步距是1。
但在pytorch官方實(shí)現(xiàn)過程中是第一個(gè)1x1卷積層的步距是1,第二個(gè)3x3卷積層步距是2,
這么做的好處是能夠在top1上提升大概0.5%的準(zhǔn)確率。
可參考Resnet v1.5 https://ngc.nvidia.com/catalog/model-scripts/nvidia:resnet_50_v1_5_for_pytorch
"""
expansion: int = 4 # expansion屬性
def __init__(
self,
inplanes: int,
planes: int,
stride: int = 1,
downsample: Optional[nn.Module] = None,
groups: int = 1,
base_width: int = 64,
dilation: int = 1,
norm_layer: Optional[Callable[..., nn.Module]] = None,
) -> None:
super().__init__()
if norm_layer is None:
norm_layer = nn.BatchNorm2d
width = int(planes * (base_width / 64.0)) * 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) # 卷積層可實(shí)現(xiàn)普通卷積、分組卷積和空洞卷積
self.bn2 = norm_layer(width)
self.conv3 = conv1x1(width, planes * self.expansion) # 需要將隱含層的特征圖數(shù)量擴(kuò)大4倍
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample # 下采樣,用于處理短路相加時(shí)維度不同的情況
self.stride = stride
def forward(self, x: Tensor) -> Tensor:
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
2.3. 構(gòu)建ResNet網(wǎng)絡(luò)
ResNet基礎(chǔ)塊的構(gòu)建已經(jīng)了解,現(xiàn)在讓我們看一下pytorch中的ResNet網(wǎng)絡(luò)。ResNet網(wǎng)絡(luò)構(gòu)建的要點(diǎn)就是卷積堆積層的構(gòu)建,這個(gè)部分搞懂的話就沒什么難點(diǎn)了。
class ResNet(nn.Module):
# 初始化函數(shù)
def __init__(
self,
block: Type[Union[BasicBlock, Bottleneck]], # 選擇基礎(chǔ)塊
layers: List[int], # len(layers)個(gè)卷積堆積層分別具有殘差塊的個(gè)數(shù)
num_classes: int = 1000, # 類別大小
zero_init_residual: bool = False,
groups: int = 1,
width_per_group: int = 64,
replace_stride_with_dilation: Optional[List[bool]] = None,
norm_layer: Optional[Callable[..., nn.Module]] = None, # 數(shù)據(jù)標(biāo)準(zhǔn)化
) -> None:
super().__init__()
_log_api_usage_once(self) # 和API有關(guān)
if norm_layer is None: # 如果不指定數(shù)據(jù)標(biāo)準(zhǔn)化方式,默認(rèn)使用nn.BatchNorm2d
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為None時(shí)重新賦值
replace_stride_with_dilation = [False, False, False]
if len(replace_stride_with_dilation) != 3:
# resnet中共有四個(gè)卷積堆積層,其中后三個(gè)需要下采樣,下采樣有池化、普通卷積、空洞卷積
# replace_stride_with_dilation列表的三個(gè)bool值決定是否使用空洞卷積替換普通卷積
# 由于空洞卷積計(jì)算量大,一般情況下不建議使用
raise ValueError(
"replace_stride_with_dilation should be None "
f"or a 3-element tuple, got {replace_stride_with_dilation}"
)
self.groups = groups
self.base_width = width_per_group
# 使用7x7卷積核,縮小圖像為原來的1/2
self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False)
# 數(shù)據(jù)標(biāo)準(zhǔn)化
self.bn1 = norm_layer(self.inplanes)
self.relu = nn.ReLU(inplace=True)
# 最大值池化,繼續(xù)縮小圖像至1/2,實(shí)際上這個(gè)池化層可以看作是layer1的下采樣層
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 卷積堆積層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])
# 自適應(yīng)平均池化層,輸出固定尺寸的特征圖
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):
# 利用He初始化方法初始化模型中卷積層的權(quán)重
nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
# init.constant初始化權(quán)重
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
# 對(duì)每個(gè)殘差分支中的最后一個(gè) BN 進(jìn)行零初始化,使殘差分支以零開頭,
# 并且每個(gè)殘差塊的行為類似于一個(gè)恒等式。這使模型提高了0.2~0.3%
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck) and m.bn3.weight is not None:
nn.init.constant_(m.bn3.weight, 0) # type: ignore[arg-type]
elif isinstance(m, BasicBlock) and m.bn2.weight is not None:
nn.init.constant_(m.bn2.weight, 0) # type: ignore[arg-type]
# 定義卷積堆積層函數(shù)
def _make_layer(
self,
block: Type[Union[BasicBlock, Bottleneck]],
planes: int,
blocks: int,
stride: int = 1,
dilate: bool = False,
) -> nn.Sequential:
norm_layer = self._norm_layer
downsample = None
previous_dilation = self.dilation
# 若dilate==True,則卷積時(shí)的步長(zhǎng)變?yōu)?;下采樣改為空洞采樣,且self.dilation *= stride
if dilate:
self.dilation *= stride
stride = 1
# 定義殘差塊中的下采樣器,該采樣器僅可能在每個(gè)卷積堆積層的第一個(gè)殘差塊中出現(xiàn)
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 = []
# 將第一個(gè)殘差塊添加進(jìn)layers
layers.append(
block(
self.inplanes, planes, stride, downsample, self.groups, self.base_width, previous_dilation, norm_layer
)
)
# 第一個(gè)殘差塊以后的全部的輸入特征圖數(shù)量為planes * block.expansion
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,
)
)
# 返回一個(gè)順序封裝的網(wǎng)絡(luò)塊,即一個(gè)卷積堆積層
return nn.Sequential(*layers)
# 定義前向傳播計(jì)算函數(shù)
def _forward_impl(self, x: Tensor) -> Tensor:
# See note [TorchScript super()]
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 forward(self, x: Tensor) -> Tensor:
return self._forward_impl(x)
2.4. 實(shí)現(xiàn)不同深度的resnet
ResNet網(wǎng)絡(luò)構(gòu)建好后,如何實(shí)現(xiàn)不同深度的網(wǎng)絡(luò)呢?這里我們看看resnet34的源碼,其他深度的網(wǎng)絡(luò)大同小異。實(shí)例化模型時(shí)最為關(guān)鍵的一點(diǎn)就是權(quán)重的初始化,pytorch提供了基于ImageNet訓(xùn)練的權(quán)重。我們可以選擇加載預(yù)訓(xùn)練權(quán)重進(jìn)行初始化或者使用默認(rèn)定義的初始化。
@register_model()
@handle_legacy_interface(weights=("pretrained", ResNet34_Weights.IMAGENET1K_V1))
def resnet34(*, weights: Optional[ResNet34_Weights] = None, progress: bool = True, **kwargs: Any) -> ResNet:
# 初始化權(quán)重,當(dāng)pretrained==True時(shí)使用預(yù)訓(xùn)練權(quán)重,否則按ResNet類中的定義初始化權(quán)重
weights = ResNet34_Weights.verify(weights)
# 返回一個(gè)resnet網(wǎng)絡(luò)模型
return _resnet(BasicBlock, [3, 4, 6, 3], weights, progress, **kwargs)
def _resnet(
block: Type[Union[BasicBlock, Bottleneck]],
layers: List[int],
weights: Optional[WeightsEnum],
progress: bool,
**kwargs: Any,
) -> ResNet:
if weights is not None:
_ovewrite_named_param(kwargs, "num_classes", len(weights.meta["categories"]))
# 實(shí)例化resnet網(wǎng)絡(luò)
model = ResNet(block, layers, **kwargs)
if weights is not None:
# 按傳入的參數(shù)初始化模型
model.load_state_dict(weights.get_state_dict(progress=progress))
return model
2.5. 解讀resnet34的參數(shù)
from torchvision.models.resnet import *
net=resnet34(pretrained=True)
for param in net.parameters():
print(param.size())
2.6. 加載torchvision中的預(yù)訓(xùn)練resnet34模型和參數(shù)
torchvision提供了預(yù)訓(xùn)練的resnet34模型和參數(shù)權(quán)重,加載預(yù)訓(xùn)練模型和參數(shù)可以參考這篇文章:https://www.cnblogs.com/wpx123/p/17616101.html。
3. 基于遷移學(xué)習(xí)得到自己的resnet模型
3.1. 遷移學(xué)習(xí)概述
遷移學(xué)習(xí)是一種機(jī)器學(xué)習(xí)的方法,指的是一個(gè)預(yù)訓(xùn)練的模型被重新用在另一個(gè)任務(wù)中。
遷移學(xué)習(xí)(Transfer learning) 顧名思義就是把已訓(xùn)練好的模型(預(yù)訓(xùn)練模型)參數(shù)遷移到新的模型來幫助新模型訓(xùn)練??紤]到大部分?jǐn)?shù)據(jù)或任務(wù)都是存在相關(guān)性的,所以通過遷移學(xué)習(xí)我們可以將已經(jīng)學(xué)到的模型參數(shù)(也可理解為模型學(xué)到的知識(shí))通過某種方式來分享給新模型從而加快并優(yōu)化模型的學(xué)習(xí)效率不用像大多數(shù)網(wǎng)絡(luò)那樣從零學(xué)習(xí)。
在CNN中實(shí)現(xiàn)遷移學(xué)習(xí)主要有以下兩種常見的方法:
- 微調(diào)(Fine-tuning):凍結(jié)預(yù)訓(xùn)練模型的部分卷積層(通常是靠近輸入的多數(shù)卷積層,因?yàn)檫@些層保留了大量底層信息)或不凍結(jié)任何網(wǎng)絡(luò)層,訓(xùn)練剩下的卷積層(通常是靠近輸出的部分卷積層)和全連接層。
- 提取特征向量(Extract Feature Vector):凍結(jié)除全連接層外的所有網(wǎng)絡(luò)的權(quán)重,最后的全連接層用一個(gè)具有隨機(jī)權(quán)重的新層來替換,并且僅訓(xùn)練該層。
以上兩種遷移方法應(yīng)如何選擇呢?
數(shù)據(jù)集大小 | 和預(yù)訓(xùn)練模型使用數(shù)據(jù)集的相似度 | 一般選擇 |
---|---|---|
小 | 高 | 特征提取 |
大 | 高 | 參數(shù)微調(diào) |
小 | 低 | 特征提取+SVM |
大 | 低 | 從頭訓(xùn)練或參數(shù)微調(diào)(推薦) |
3.2. 基于遷移學(xué)習(xí)得到自己的resnet模型
以下示例中所用數(shù)據(jù)集的信息如下:
種類 | 訓(xùn)練集 | 驗(yàn)證集 |
---|---|---|
貓 | 130 | 100 |
狗 | 222 | 102 |
螞蟻 | 123 | 70 |
蜜蜂 | 121 | 83 |
合計(jì) | 596 | 355 |
參數(shù)微調(diào)
本文使用預(yù)訓(xùn)練的參數(shù)來初始化我們的網(wǎng)絡(luò)模型,修改全連接層后再訓(xùn)練所有層。
制作訓(xùn)練集
# 導(dǎo)入模塊
import os
import cv2
import matplotlib.pyplot as plt
import torch
import torchvision
import numpy as np
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision.datasets import ImageFolder
import torchvision.models as models
import torch.nn as nn
import torch.optim as optim
import time
import copy
from tqdm import tqdm
# 數(shù)據(jù)集的制作
# train/eval/test數(shù)據(jù)集目錄
root_train="D:\\Users\\CV learning\\pytorch\\data\\cat_dog_ants_bees\\train"
root_eval="D:\\Users\\CV learning\\pytorch\\data\\cat_dog_ants_bees\\eval"
root_test="D:\\Users\\CV learning\\pytorch\\data\\cat_dog_ants_bees\\test"
# 計(jì)算數(shù)據(jù)集的normMean、normStd,結(jié)果用于輸入transforms.Normalize(mean_train, std_train)
def cal_mean_std(root):
img_h, img_w = 300, 300 # 根據(jù)自己數(shù)據(jù)集適當(dāng)調(diào)整,影響不大
means = [0, 0, 0]
stdevs = [0, 0, 0]
img_list = []
imgs_path = root
imgs_path_list = os.listdir(imgs_path)
num_imgs = 0
for data in imgs_path_list:
data_path = os.path.join(imgs_path, data)
data_list = os.listdir(data_path)
for pic in data_list:
num_imgs += 1
img = cv2.imread(os.path.join(data_path+'\\', pic))
try:
img.shape
except:
print(os.path.join(data_path+'\\', pic))
print("Can not read this image !")
img = img.astype(np.float32) / 255.
for i in range(3):
means[i] += img[:, :, i].mean()
stdevs[i] += img[:, :, i].std()
means.reverse()
stdevs.reverse()
means = np.asarray(means) / num_imgs
stdevs = np.asarray(stdevs) / num_imgs
return list(np.around(means, 3)), list(np.around(stdevs, 3))
# train,eval數(shù)據(jù)集的mean、std
mean_train = cal_mean_std(root_train)[0]
std_train = cal_mean_std(root_train)[1]
mean_eval = cal_mean_std(root_train)[0]
std_eval = cal_mean_std(root_train)[1]
# 數(shù)據(jù)增強(qiáng)
transformer={"train":transforms.Compose([transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean_train, std_train)]),
"eval":transforms.Compose([transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean_eval, std_eval)])}
# 制作數(shù)據(jù)集
dataset_train = ImageFolder(root_train, transform=transformer["train"])
dataset_eval = ImageFolder(root_train, transform=transformer["eval"])
loader_train = DataLoader(dataset=dataset_train, batch_size=6, shuffle=True, worker_init_fn=6)
loader_eval = DataLoader(dataset=dataset_eval, batch_size=6, shuffle=True, worker_init_fn=6)
class_name = dataset_train.classes
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
# 查看制作完成的數(shù)據(jù)集
print(dataset_train.classes)
print(dataset_train.class_to_idx)
print(dataset_train.imgs[0])
# 訓(xùn)練數(shù)據(jù)可視化
batch_imgs, lables = next(iter(loader_eval))
grid_img = torchvision.utils.make_grid(batch_imgs)
# 反歸一化
grid_img = grid_img.permute(1, 2, 0)
grid_img = grid_img*torch.Tensor(std_train)+torch.Tensor(mean_train)
# 可視化展示
plt.title([class_name[i] for i in lables])
plt.imshow(grid_img)
plt.show()
運(yùn)行以上代碼的輸出:
['0_cat', '1_dog', '2_ants', '3_bees']
{'0_cat': 0, '1_dog': 1, '2_ants': 2, '3_bees': 3}
('D:\Users\CV learning\pytorch\data\cat_dog_ants_bees\train\0_cat\cat.0.jpg', 0)
對(duì)resnet34模型進(jìn)行微調(diào)
# 對(duì)resnet34模型進(jìn)行微調(diào)
net = models.resnet34()
net.load_state_dict(torch.load("D:\\Users\\CV learning\\pytorch\\data\\resnet34-b627a593.pth"))
# 加載預(yù)訓(xùn)練模型的全連接層參數(shù)
for i in net.fc.parameters():
print(i.size())
num_ftrs = net.fc.in_features
# 調(diào)整全連接層的輸出數(shù)為len(class_name),此時(shí)net.fc.parameters()的size也一起進(jìn)行了調(diào)整
net.fc = nn.Linear(num_ftrs, len(class_name))
# 將模型放到cpu/gpu
net = net.to(device)
# 定義損失函數(shù)
loss_func = nn.CrossEntropyLoss()
# 定義優(yōu)化器
optimizer = optim.Adam(net.parameters(), lr=0.0001)
# 調(diào)整后的net.fc.parameters()
for i in net.fc.parameters():
print(i.size())
運(yùn)行以上代碼的輸出:
torch.Size([1000, 512])
torch.Size([1000])
torch.Size([4, 512])
torch.Size([4])
訓(xùn)練與評(píng)估
# 定義訓(xùn)練評(píng)估函數(shù)
def train_and_eval(model, epochs, loss_func, optimizer, loader_train, loader_eval):
# 初始化參數(shù)
t_start = time.time()
best_weights = copy.deepcopy(model.state_dict())
best_acc = 0.0
for epoch in range(epochs):
print("-"*125)
# train
model.train()
running_loss = 0.0
running_acc = 0.0
train_bar = tqdm(loader_train)
train_bar.desc = f"第{epoch+1}次訓(xùn)練,Processing"
for inputs, lables in train_bar:
inputs = inputs.to(device)
lables = lables.to(device)
# 梯度歸零
optimizer.zero_grad()
# 前向傳播
outputs = model(inputs)
# 損失值
loss = loss_func(outputs, lables)
# 輸出最大的概率的類
pred = torch.argmax(outputs, 1)
# 反向傳播
loss.backward()
# 參數(shù)更新
optimizer.step()
# 統(tǒng)計(jì)這個(gè)batch的損失值和與分類正確數(shù)
running_loss += loss.item()*inputs.size(0)
running_acc += (pred==lables.data).sum()
# 計(jì)算本epoch的損失值和正確率
train_loss = running_loss/len(dataset_train)
train_acc = running_acc/len(dataset_train)
print(f"第{epoch+1}次訓(xùn)練,train_loss:{train_loss:.6f}, train_acc:{train_acc:.6f}")
# eval
model.eval()
running_loss = 0.0
running_acc = 0.0
with torch.no_grad():
eval_bar = tqdm(loader_eval)
eval_bar.desc = f"第{epoch+1}次評(píng)估,Processing"
for inputs, lables in eval_bar:
inputs = inputs.to(device)
lables = lables.to(device)
outputs = model(inputs)
loss = loss_func(outputs, lables)
pred = torch.argmax(outputs, 1)
running_loss += loss.item()*inputs.size(0)
running_acc += (pred==lables.data).sum()
val_loss = running_loss/len(dataset_eval)
val_acc = running_acc/len(dataset_eval)
print(f"第{epoch+1}次評(píng)估,val_loss:{val_loss:.6f}, val_acc:{val_acc:.6f}")
if val_acc > best_acc:
best_acc = val_acc
best_weights = copy.deepcopy(model.state_dict())
t_end = time.time()
total_time = t_end - t_start
print("-"*125)
print(f"{epochs}次訓(xùn)練與評(píng)估共計(jì)用時(shí){total_time//60:.0f}m{total_time%60:.0f}s")
print(f"最高正確率是{best_acc:.6f}")
# 加載最佳的模型權(quán)重
model.load_state_dict(best_weights)
return model
# 創(chuàng)建參數(shù)微調(diào)模型
net_fine_tune = train_and_eval(net, 10, loss_func, optimizer, loader_train, loader_eval)
# 保存訓(xùn)練好的模型參數(shù)
torch.save(net_fine_tune.state_dict(), "D:\\Users\\CV learning\\pytorch\\data\\cat_dog_ants_bees\\resnet34_fine_tune.pth")
可以看到,前三次的訓(xùn)練和評(píng)估val_acc已經(jīng)達(dá)到了0.986577,說明模型的泛化能力已經(jīng)挺不錯(cuò)了,此時(shí)train_acc也高達(dá)0.869128。最終的結(jié)果如下圖,
由于筆者是用cpu跑的因此使用了近20分鐘的時(shí)間。十輪訓(xùn)練后,模型的val_acc可以高達(dá)0.998322,已經(jīng)是一個(gè)很好的結(jié)果了。
特征提取
該方法凍結(jié)除全連接層外的所有層的權(quán)重,修改全連接層后僅訓(xùn)練全連接層。
特征提取僅僅在構(gòu)造模型時(shí)與參數(shù)微調(diào)方法有所區(qū)別,其他內(nèi)容都是一樣的。
特征提取文章來源:http://www.zghlxwxcb.cn/news/detail-642152.html
import torchvision.models as models
import torch.nn as nn
import torch.optim as optim
net = models.resnet34()
net.load_state_dict(torch.load("D:\\Users\\CV learning\\pytorch\\data\\resnet34-b627a593.pth"))
# 加載預(yù)訓(xùn)練權(quán)重的net.fc.parameters()
for i in net.fc.parameters():
print(i.size())
for param in net.parameters():
param.requires_grad = False
num_ftrs = net.fc.in_features
# 調(diào)整全連接層的輸出數(shù)為len(class_name),此時(shí)net.fc.parameters()也一起調(diào)整
net.fc = nn.Linear(num_ftrs, len(class_name))
# 將模型放到cpu/gpu
net = net.to(device)
# 定義損失函數(shù)
loss_func = nn.CrossEntropyLoss()
# 定義優(yōu)化器
optimizer = optim.Adam(net.parameters(), lr=0.0001)
# 調(diào)整后的net.fc.parameters()
for i in net.fc.parameters():
print(i.size())
最終運(yùn)行結(jié)果如下:
文章來源地址http://www.zghlxwxcb.cn/news/detail-642152.html
4. 參考內(nèi)容
- 你必須要知道CNN模型:ResNet
- torchvision.models.resnet — Torchvision 0.15 documentation
- 【PyTorch】遷移學(xué)習(xí)教程(計(jì)算機(jī)視覺應(yīng)用實(shí)例)_Xavier Jiezou的博客-CSDN博客
到了這里,關(guān)于【pytorch】ResNet源碼解讀和基于遷移學(xué)習(xí)的實(shí)戰(zhàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!