Pytorch模型微調(diào)fine-tune詳解
隨著深度學(xué)習(xí)的發(fā)展,在大模型的訓(xùn)練上都是在一些較大數(shù)據(jù)集上進行訓(xùn)練的,比如Imagenet-1k,Imagenet-11k,甚至是ImageNet-21k等。但我們在實際應(yīng)用中,我們自己的數(shù)據(jù)集可能比較小,只有幾千張照片,這時從頭訓(xùn)練具有幾千萬參數(shù)的大型神經(jīng)網(wǎng)絡(luò)是不現(xiàn)實的,因為越大的模型對數(shù)據(jù)量的要求越高,過擬合無法避免。
因為適用于ImageNet數(shù)據(jù)集的復(fù)雜模型,在一些小的數(shù)據(jù)集上可能會過擬合,同時因為數(shù)據(jù)量有限,最終訓(xùn)練得到的模型的精度也可能達不到實用要求。
解決上述問題的方法:
收集更多數(shù)據(jù)集,當(dāng)然這對于研究成本會大大增加應(yīng)用遷移學(xué)習(xí)(transfer learning):從源數(shù)據(jù)集中學(xué)到知識遷移到目標(biāo)數(shù)據(jù)集上。1、模型微調(diào)(fine-tune)
微調(diào)(fine-tune)通過使用在大數(shù)據(jù)上得到的預(yù)訓(xùn)練好的模型來初始化自己的模型權(quán)重,從而提升精度。這就要求預(yù)訓(xùn)練模型質(zhì)量要有保證。微調(diào)通常速度更快、精度更高。當(dāng)然,自己訓(xùn)練好的模型也可以當(dāng)做預(yù)訓(xùn)練模型,然后再在自己的數(shù)據(jù)集上進行訓(xùn)練,來使模型適用于自己的場景、自己的任務(wù)。
先引入遷移學(xué)習(xí)(Transfer Learning)的概念:
當(dāng)我們訓(xùn)練好了一個模型之后,如果想應(yīng)用到其他任務(wù)中,可以在這個模型的基礎(chǔ)上進行訓(xùn)練,來作微調(diào)網(wǎng)絡(luò)。這也是遷移學(xué)習(xí)的概念,可以節(jié)省訓(xùn)練的資源以及訓(xùn)練的時間。
遷移學(xué)習(xí)的一大應(yīng)用場景就是模型微調(diào),簡單的來說就是把在別人訓(xùn)練好的基礎(chǔ)上,換成自己的數(shù)據(jù)集繼續(xù)訓(xùn)練,來調(diào)整參數(shù)。Pytorch中提供很多預(yù)訓(xùn)練模型,學(xué)習(xí)如何進行模型微調(diào),可以大大提升自己任務(wù)的質(zhì)量和速度。
假設(shè)我們要識別的圖片類別是椅子,盡管ImageNet數(shù)據(jù)集中的大多數(shù)圖像與椅子無關(guān),但在ImageNet數(shù)據(jù)集上訓(xùn)練的模型可能會提取更通用的圖像特征,這有助于識別邊緣、紋理、形狀和對象組合。 這些類似的特征對于識別椅子也可能同樣有效。
2.1、為什么要微調(diào)
因為預(yù)訓(xùn)練模型用了大量數(shù)據(jù)做訓(xùn)練,已經(jīng)具備了提取淺層基礎(chǔ)特征和深層抽象特征的能力。
對于圖片來說,我們CNN的前幾層學(xué)習(xí)到的都是低級的特征,比如,點、線、面,這些低級的特征對于任何圖片來說都是可以抽象出來的,所以我們將他作為通用數(shù)據(jù),只微調(diào)這些低級特征組合起來的高級特征即可,例如,這些點、線、面,組成的是園還是橢圓,還是正方形,這些代表的含義是我們需要后面訓(xùn)練出來的。
如果我們自己的數(shù)據(jù)不夠多,泛化性不夠強,那么可能存在模型不收斂,準(zhǔn)確率低,模型泛化能力差,過擬合等問題,所以這時就需要使用預(yù)訓(xùn)練模型來做微調(diào)了。注意的是,進行微調(diào)時,應(yīng)該使用較小的學(xué)習(xí)率。因為預(yù)訓(xùn)練模型的權(quán)重相對于隨機初始化的權(quán)重來說已經(jīng)很不錯了,所以不希望使用太大的學(xué)習(xí)率來破壞原本的權(quán)重。通常用于微調(diào)的初始學(xué)習(xí)率會比從頭開始訓(xùn)練的學(xué)習(xí)率小10倍。
總結(jié):對于不同的層可以設(shè)置不同的學(xué)習(xí)率,一般情況下建議,對于使用的原始數(shù)據(jù)做初始化的層設(shè)置的學(xué)習(xí)率要小于(一般可設(shè)置小于10倍)初始化的學(xué)習(xí)率,這樣保證對于已經(jīng)初始化的數(shù)據(jù)不會扭曲的過快,而使用初始化學(xué)習(xí)率的新層可以快速的收斂。
2.2、需要微調(diào)的情況
其中微調(diào)的方法又要根據(jù)自身數(shù)據(jù)集和預(yù)訓(xùn)練模型數(shù)據(jù)集的相似程度,以及自己數(shù)據(jù)集的大小來抉擇。
不同情況下的微調(diào):
數(shù)據(jù)少,數(shù)據(jù)類似程度高:可以只修改最后幾層或者最后一層進行微調(diào)。數(shù)據(jù)少,數(shù)據(jù)類似程度低:凍結(jié)預(yù)訓(xùn)練模型的前幾層,訓(xùn)練剩余的層。因為數(shù)據(jù)集之間的相似度較低,所以根據(jù)自身的數(shù)據(jù)集對較高層進行重新訓(xùn)練會比較有效。數(shù)據(jù)多,數(shù)據(jù)類似程度高:這是最理想的情況。使用預(yù)訓(xùn)練的權(quán)重來初始化模型,然后重新訓(xùn)練整個模型。這也是最簡單的微調(diào)方式,因為不涉及修改、凍結(jié)模型的層。數(shù)據(jù)多,數(shù)據(jù)類似程度低:微調(diào)的效果估計不好,可以考慮直接重新訓(xùn)練整個模型。如果你用的預(yù)訓(xùn)練模型的數(shù)據(jù)集是ImageNet,而你要做的是文字識別,那么預(yù)訓(xùn)練模型自然不會起到太大作用,因為它們的場景特征相差太大了。
注意:
如果自己的模型中有fc層,則新數(shù)據(jù)集的大小一定要與原始數(shù)據(jù)集相同,比如CNN中輸入的圖片大小一定要相同,才不會報錯。如果包含fc層但是數(shù)據(jù)集大小不同的話,可以在最后的fc層之前添加卷積或者pool層,使得最后的輸出與fc層一致,但這樣會導(dǎo)致準(zhǔn)確度大幅下降,所以不建議這樣做2.3、 模型微調(diào)的流程
微調(diào)的步驟有很多,看你自身數(shù)據(jù)和計算資源的情況而定。雖然各有不同,但是總體的流程大同小異。
步驟示例1:
1、在源數(shù)據(jù)集(如ImageNet數(shù)據(jù)集)上預(yù)訓(xùn)練一個神經(jīng)網(wǎng)絡(luò)模型,即源模型。
2、創(chuàng)建一個新的神經(jīng)網(wǎng)絡(luò)模型,即目標(biāo)模型。它復(fù)制了源模型上除了輸出層外的所有模型設(shè)計及其參數(shù)。
我們假設(shè)這些模型參數(shù)包含了源數(shù)據(jù)集上學(xué)習(xí)到的知識,且這些知識同樣適用于目標(biāo)數(shù)據(jù)集。我們還假設(shè)源模型的輸出層跟源數(shù)據(jù)集的標(biāo)簽緊密相關(guān),因此在目標(biāo)模型中不予采用。
3、為目標(biāo)模型添加一個輸出大小為目標(biāo)數(shù)據(jù)集類別個數(shù)的輸出層,并隨機初始化該層的模型參數(shù)。
4、在目標(biāo)數(shù)據(jù)集(如椅子數(shù)據(jù)集)上訓(xùn)練目標(biāo)模型??梢詮念^訓(xùn)練輸出層,而其余層的參數(shù)都是基于源模型的參數(shù)微調(diào)得到的。
步驟示例2:
在已經(jīng)訓(xùn)練好的網(wǎng)絡(luò)上進行修改;凍結(jié)網(wǎng)絡(luò)的原來那一部分;訓(xùn)練新添加的部分;解凍原來網(wǎng)絡(luò)的部分層;聯(lián)合訓(xùn)練解凍的層和新添加的部分。

2.4、參數(shù)凍結(jié)---指定訓(xùn)練模型的部分層
我們所提到的凍結(jié)模型、凍結(jié)部分層,其實歸根結(jié)底都是對參數(shù)進行凍結(jié)。凍結(jié)訓(xùn)練可以加快訓(xùn)練速度。在這里,有兩種方式:全程凍結(jié)與非全程凍結(jié)。
非全程凍結(jié)比全程凍結(jié)多了一個步驟:解凍,因此這里就講解非全程凍結(jié)??赐攴侨虄鼋Y(jié)之后,就明白全程凍結(jié)是如何進行的了。
非全程凍結(jié)訓(xùn)練分為兩個階段,分別是凍結(jié)階段和解凍階段。當(dāng)處于凍結(jié)階段時,被凍結(jié)的參數(shù)就不會被更新,在這個階段,可以看做是全程凍結(jié);而處于解凍階段時,就和普通的訓(xùn)練一樣了,所有參數(shù)都會被更新。
當(dāng)進行凍結(jié)訓(xùn)練時,占用的顯存較小,因為僅對部分網(wǎng)絡(luò)進行微調(diào)。如果計算資源不夠,也可以通過凍結(jié)訓(xùn)練的方式來減少訓(xùn)練時資源的占用。
因為一般需要保留Features Extractor的結(jié)構(gòu)和參數(shù),提出了兩種訓(xùn)練方法:
- 固定預(yù)訓(xùn)練的參數(shù):requires_grad = False 或者 lr = 0,即不更新參數(shù);
- 將Features Extractor部分設(shè)置很小的學(xué)習(xí)率,這里用到參數(shù)組(params_group)的概念,分組設(shè)置優(yōu)化器的參數(shù)。
2.5、參數(shù)凍結(jié)的方式
我們經(jīng)常提到的模型,就是一個可遍歷的字典。既然是字典,又是可遍歷的,那么就有兩種方式進行索引:一是通過數(shù)字,二是通過名字。
其實使用凍結(jié)很簡單,沒有太高深的魔法,只用設(shè)置模型的參數(shù)requires_grad為False就可以了。
2.5.1、凍結(jié)方式1
在默認(rèn)情況下,參數(shù)的屬性??.requires_grad = True???,如果我們從頭開始訓(xùn)練或微調(diào)不需要注意這里。但如果我們正在提取特征并且只想為新初始化的層計算梯度,其他參數(shù)不進行改變。那我們就需要通過設(shè)置??requires_grad = False??來凍結(jié)部分層。在PyTorch官方中提供了這樣一個例程。
def set_parameter_requires_grad(model, feature_extracting):
if feature_extracting:
for param in model.parameters():
param.requires_grad = False在下面我們使用??resnet18??為例的將1000類改為4類,但是僅改變最后一層的模型參數(shù),不改變特征提取的模型參數(shù);
- 注意我們先凍結(jié)模型參數(shù)的梯度;
- 再對模型輸出部分的全連接層進行修改,這樣修改后的全連接層的參數(shù)就是可計算梯度的。
在訓(xùn)練過程中,model仍會進行梯度回傳,但是參數(shù)更新則只會發(fā)生在fc層。通過設(shè)定參數(shù)的??requires_grad??屬性,我們完成了指定訓(xùn)練模型的特定層的目標(biāo),這對實現(xiàn)模型微調(diào)非常重要。
import torchvision.models as models # 凍結(jié)參數(shù)的梯度 feature_extract = True model = models.resnet18(pretrained=True) set_parameter_requires_grad(model, feature_extract) # 修改模型, 輸出通道4, 此時,fc層就被隨機初始化了,但是其他層依然保存著預(yù)訓(xùn)練得到的參數(shù)。 model.fc = nn.Linear(in_features=512, out_features=4, bias=True)
我們直接拿??torchvision.models.resnet50 ??模型微調(diào),首先凍結(jié)預(yù)訓(xùn)練模型中的所有參數(shù),然后替換掉最后兩層的網(wǎng)絡(luò)(替換2層池化層,還有fc層改為dropout,正則,線性,激活等部分),最后返回模型:
# 8 更改池化層
class AdaptiveConcatPool2d(nn.Module):
def __init__(self, size=None):
super().__init__()
size = size or (1, 1) # 池化層的卷積核大小,默認(rèn)值為(1,1)
self.pool_one = nn.AdaptiveAvgPool2d(size) # 池化層1
self.pool_two = nn.AdaptiveAvgPool2d(size) # 池化層2
def forward(self, x):
return torch.cat([self.pool_one(x), self.pool_two(x), 1]) # 連接兩個池化層
# 7 遷移學(xué)習(xí):拿到一個成熟的模型,進行模型微調(diào)
def get_model():
model_pre = models.resnet50(pretrained=True) # 獲取預(yù)訓(xùn)練模型
# 凍結(jié)預(yù)訓(xùn)練模型中所有的參數(shù)
for param in model_pre.parameters():
param.requires_grad = False
# 微調(diào)模型:替換ResNet最后的兩層網(wǎng)絡(luò),返回一個新的模型
model_pre.avgpool = AdaptiveConcatPool2d() # 池化層替換
model_pre.fc = nn.Sequential(
nn.Flatten(), # 所有維度拉平
nn.BatchNorm1d(4096), # 256 x 6 x 6 ——> 4096
nn.Dropout(0.5), # 丟掉一些神經(jīng)元
nn.Linear(4096, 512), # 線性層的處理
nn.ReLU(), # 激活層
nn.BatchNorm1d(512), # 正則化處理
nn.Linear(512,2),
nn.LogSoftmax(dim=1), # 損失函數(shù)
)
return
2.5.2、凍結(jié)方式2
因為ImageNet有1000個類別,所以提供的ImageNet預(yù)訓(xùn)練模型也是1000分類。如果我需要訓(xùn)練一個10分類模型,理論上來說只需要修改最后一層的全連接層即可。
如果前面的參數(shù)不凍結(jié)就表示所有特征提取的層會使用預(yù)訓(xùn)練模型的參數(shù)來進行參數(shù)初始化,而最后一層的參數(shù)還是保持某種初始化的方式來進行初始化。
在模型中,每一層的參數(shù)前面都有前綴,比如conv1、conv2、fc3、backbone等等,我們可以通過這個前綴來進行判斷,也就是通過名字來判斷,如:if "backbone" in param.name,最終選擇需要凍結(jié)與不需要凍結(jié)的層。最后需要將訓(xùn)練的參數(shù)傳入優(yōu)化器進行配置。
if freeze_layers:
for name, param in model.named_parameters():
# 除最后的全連接層外,其他權(quán)重全部凍結(jié)
if "fc" not in name:
param.requires_grad_(False)
pg = [p for p in model.parameters() if p.requires_grad]
optimizer = optim.SGD(pg, lr=0.01, momentum=0.9, weight_decay=4E-5)或者判斷該參數(shù)位于模型的哪些模塊層中,如param in model.backbone.parameters(),然后對于該模塊層的全部參數(shù)進行批量設(shè)置,將requires_grad置為False。
if Freeze_Train:
for param in model.backbone.parameters():
param.requires_grad = False2.5.2、凍結(jié)方式3
通過數(shù)字來遍歷模型中的層的參數(shù),凍結(jié)所指定的若干個參數(shù), 這種方式用的少
count = 0
for layer in model.children():
count = count + 1
if count < 10:
for param in layer.parameters():
param.requires_grad = False
# 然后將需要訓(xùn)練的參數(shù)傳入優(yōu)化器,也就是過濾掉被凍結(jié)的參數(shù)。
optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=LR)2.6、修改模型參數(shù)
前面說道,凍結(jié)模型就是凍結(jié)參數(shù),那么這里的修改模型參數(shù)更多的是修改模型參數(shù)的名稱。
值得一提的是,由于訓(xùn)練方式(單卡、多卡訓(xùn)練)、模型定義的方式不同,參數(shù)的名稱也會有所區(qū)別,但是此時模型的結(jié)構(gòu)是一樣的,依舊可以加載預(yù)訓(xùn)練模型。不過卻無法直接載入預(yù)訓(xùn)練模型的參數(shù),因為名稱不同,會出現(xiàn)KeyError的錯誤,所以載入前可能需要修改參數(shù)的名稱。
比如說,使用多卡訓(xùn)練時,保存的時候每個參數(shù)前面多會多出'module.'這幾個字符,那么當(dāng)使用單卡載入時,可能就會報錯了。

通過以下方式,就可以使用'conv1'來替代'module.conv1'這個key的方式來將更新后的key和原來的value相匹配,再載入自己定義的模型中。
model_dict = pretrained_model.state_dict()
pretrained_dict={k: v for k, v in pretrained_dict.items() if k[7:] in model_dict}
model_dict.update(pretrained_dict)2.7、修改模型結(jié)構(gòu)
import torch.nn as nn
import torch
class AlexNet(nn.Module):
def __init__(self):
super(AlexNet, self).__init__()
self.features=nn.Sequential(
nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2), # 使用卷積層,輸入為3,輸出為64,核大小為11,步長為4
nn.ReLU(inplace=True), # 使用激活函數(shù)
nn.MaxPool2d(kernel_size=3, stride=2), # 使用最大池化,這里的大小為3,步長為2
nn.Conv2d(64, 192, kernel_size=5, padding=2), # 使用卷積層,輸入為64,輸出為192,核大小為5,步長為2
nn.ReLU(inplace=True),# 使用激活函數(shù)
nn.MaxPool2d(kernel_size=3, stride=2), # 使用最大池化,這里的大小為3,步長為2
nn.Conv2d(192, 384, kernel_size=3, padding=1), # 使用卷積層,輸入為192,輸出為384,核大小為3,步長為1
nn.ReLU(inplace=True),# 使用激活函數(shù)
nn.Conv2d(384, 256, kernel_size=3, padding=1),# 使用卷積層,輸入為384,輸出為256,核大小為3,步長為1
nn.ReLU(inplace=True),# 使用激活函數(shù)
nn.Conv2d(256, 256, kernel_size=3, padding=1),# 使用卷積層,輸入為256,輸出為256,核大小為3,步長為1
nn.ReLU(inplace=True),# 使用激活函數(shù)
nn.MaxPool2d(kernel_size=3, stride=2), # 使用最大池化,這里的大小為3,步長為2
)
self.avgpool=nn.AdaptiveAvgPool2d((6, 6))
self.classifier=nn.Sequential(
nn.Dropout(),# 使用Dropout來減緩過擬合
nn.Linear(256 * 6 * 6, 4096), # 全連接,輸出為4096
nn.ReLU(inplace=True),# 使用激活函數(shù)
nn.Dropout(),# 使用Dropout來減緩過擬合
nn.Linear(4096, 4096), # 維度不變,因為后面引入了激活函數(shù),從而引入非線性
nn.ReLU(inplace=True), # 使用激活函數(shù)
nn.Linear(4096, 1000), #ImageNet默認(rèn)為1000個類別,所以這里進行1000個類別分類
)
def forward(self, x):
x=self.features(x)
x=self.avgpool(x)
x=torch.flatten(x, 1)
x=self.classifier(x)
return x
def alexnet(num_classes, device, pretrained_weights=""):
net=AlexNet() # 定義AlexNet
if pretrained_weights: # 判斷預(yù)訓(xùn)練模型路徑是否為空,如果不為空則加載
net.load_state_dict(torch.load(pretrained_weights,map_location=device))
num_fc=net.classifier[6].in_features # 獲取輸入到全連接層的輸入維度信息
net.classifier[6]=torch.nn.Linear(in_features=num_fc, out_features=num_classes) # 根據(jù)數(shù)據(jù)集的類別數(shù)來指定最后輸出的out_features數(shù)目
return net在上述代碼中,我是先將權(quán)重載入全部網(wǎng)絡(luò)結(jié)構(gòu)中。此時,模型的最后一層大小并不是我想要的,因此我獲取了輸入到最后一層全連接層之前的維度大小,然后根據(jù)數(shù)據(jù)集的類別數(shù)來指定最后輸出的out_features數(shù)目,以此代替原來的全連接層。
你也可以先定義好具有指定全連接大小的網(wǎng)絡(luò)結(jié)構(gòu),然后除了最后一層全連接層之外,全部層都載入預(yù)訓(xùn)練模型;你也可以先將權(quán)重載入全部網(wǎng)絡(luò)結(jié)構(gòu)中,然后刪掉最后一層全連接層,最后再加入一層指定大小的全連接層。
到此這篇關(guān)于Pytorch模型微調(diào)(fine-tune)的文章就介紹到這了,更多相關(guān)Pytorch模型微調(diào)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python如何利用xlrd和xlwt模塊操作Excel表格
這篇文章主要給大家介紹了關(guān)于Python如何利用xlrd和xlwt模塊操作Excel表格的相關(guān)資料,其中xlrd模塊實現(xiàn)對excel文件內(nèi)容讀取,xlwt模塊實現(xiàn)對excel文件的寫入,需要的朋友可以參考下2022-03-03
Pytorch中的backward()多個loss函數(shù)用法
這篇文章主要介紹了Pytorch中的backward()多個loss函數(shù)用法,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-05-05
Python pandas如何獲取數(shù)據(jù)的行數(shù)和列數(shù)
這篇文章主要介紹了Python pandas如何獲取數(shù)據(jù)的行數(shù)和列數(shù)問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-02-02
pandas如何將dataframe中的NaN替換成None
這篇文章主要介紹了pandas如何將dataframe中的NaN替換成None問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08
Python面向?qū)ο笏枷肱c應(yīng)用入門教程【類與對象】
這篇文章主要介紹了Python面向?qū)ο笏枷肱c應(yīng)用,較為詳細的分析了Python面向?qū)ο笏枷肱c原理,并結(jié)合實例形式分析了類與對象相關(guān)定義、用法及操作注意事項,需要的朋友可以參考下2019-04-04

