日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Pytorch模型微調fine-tune詳解_python

作者:ytusdc ? 更新時間: 2023-02-10 編程語言

????????隨著深度學習的發(fā)展,在大模型的訓練上都是在一些較大數據集上進行訓練的,比如Imagenet-1k,Imagenet-11k,甚至是ImageNet-21k等。但我們在實際應用中,我們自己的數據集可能比較小,只有幾千張照片,這時從頭訓練具有幾千萬參數的大型神經網絡是不現實的,因為越大的模型對數據量的要求越高,過擬合無法避免。
????????因為適用于ImageNet數據集的復雜模型,在一些小的數據集上可能會過擬合,同時因為數據量有限,最終訓練得到的模型的精度也可能達不到實用要求。

解決上述問題的方法:

收集更多數據集,當然這對于研究成本會大大增加應用遷移學習(transfer learning):從源數據集中學到知識遷移到目標數據集上。1、模型微調(fine-tune)

????????微調(fine-tune)通過使用在大數據上得到的預訓練好的模型來初始化自己的模型權重,從而提升精度。這就要求預訓練模型質量要有保證。微調通常速度更快、精度更高。當然,自己訓練好的模型也可以當做預訓練模型,然后再在自己的數據集上進行訓練,來使模型適用于自己的場景、自己的任務。

先引入遷移學習(Transfer Learning)的概念:

????????當我們訓練好了一個模型之后,如果想應用到其他任務中,可以在這個模型的基礎上進行訓練,來作微調網絡。這也是遷移學習的概念,可以節(jié)省訓練的資源以及訓練的時間。

????????遷移學習的一大應用場景就是模型微調,簡單的來說就是把在別人訓練好的基礎上,換成自己的數據集繼續(xù)訓練,來調整參數。Pytorch中提供很多預訓練模型,學習如何進行模型微調,可以大大提升自己任務的質量和速度。

????????假設我們要識別的圖片類別是椅子,盡管ImageNet數據集中的大多數圖像與椅子無關,但在ImageNet數據集上訓練的模型可能會提取更通用的圖像特征,這有助于識別邊緣、紋理、形狀和對象組合。 這些類似的特征對于識別椅子也可能同樣有效。

2.1、為什么要微調

????????因為預訓練模型用了大量數據做訓練,已經具備了提取淺層基礎特征和深層抽象特征的能力。

對于圖片來說,我們CNN的前幾層學習到的都是低級的特征,比如,點、線、面,這些低級的特征對于任何圖片來說都是可以抽象出來的,所以我們將他作為通用數據,只微調這些低級特征組合起來的高級特征即可,例如,這些點、線、面,組成的是園還是橢圓,還是正方形,這些代表的含義是我們需要后面訓練出來的。

????????如果我們自己的數據不夠多,泛化性不夠強,那么可能存在模型不收斂,準確率低,模型泛化能力差,過擬合等問題,所以這時就需要使用預訓練模型來做微調了。注意的是,進行微調時,應該使用較小的學習率。因為預訓練模型的權重相對于隨機初始化的權重來說已經很不錯了,所以不希望使用太大的學習率來破壞原本的權重。通常用于微調的初始學習率會比從頭開始訓練的學習率小10倍。

總結:對于不同的層可以設置不同的學習率,一般情況下建議,對于使用的原始數據做初始化的層設置的學習率要小于(一般可設置小于10倍)初始化的學習率,這樣保證對于已經初始化的數據不會扭曲的過快,而使用初始化學習率的新層可以快速的收斂。

2.2、需要微調的情況

????????其中微調的方法又要根據自身數據集和預訓練模型數據集的相似程度,以及自己數據集的大小來抉擇。
不同情況下的微調:

數據少,數據類似程度高:可以只修改最后幾層或者最后一層進行微調。數據少,數據類似程度低:凍結預訓練模型的前幾層,訓練剩余的層。因為數據集之間的相似度較低,所以根據自身的數據集對較高層進行重新訓練會比較有效。數據多,數據類似程度高:這是最理想的情況。使用預訓練的權重來初始化模型,然后重新訓練整個模型。這也是最簡單的微調方式,因為不涉及修改、凍結模型的層。數據多,數據類似程度低:微調的效果估計不好,可以考慮直接重新訓練整個模型。如果你用的預訓練模型的數據集是ImageNet,而你要做的是文字識別,那么預訓練模型自然不會起到太大作用,因為它們的場景特征相差太大了。

注意:

如果自己的模型中有fc層,則新數據集的大小一定要與原始數據集相同,比如CNN中輸入的圖片大小一定要相同,才不會報錯。如果包含fc層但是數據集大小不同的話,可以在最后的fc層之前添加卷積或者pool層,使得最后的輸出與fc層一致,但這樣會導致準確度大幅下降,所以不建議這樣做2.3、?模型微調的流程

微調的步驟有很多,看你自身數據和計算資源的情況而定。雖然各有不同,但是總體的流程大同小異。

步驟示例1:

1、在源數據集(如ImageNet數據集)上預訓練一個神經網絡模型,即源模型。

2、創(chuàng)建一個新的神經網絡模型,即目標模型。它復制了源模型上除了輸出層外的所有模型設計及其參數。

我們假設這些模型參數包含了源數據集上學習到的知識,且這些知識同樣適用于目標數據集。我們還假設源模型的輸出層跟源數據集的標簽緊密相關,因此在目標模型中不予采用。

3、為目標模型添加一個輸出大小為目標數據集類別個數的輸出層,并隨機初始化該層的模型參數。

4、在目標數據集(如椅子數據集)上訓練目標模型。可以從頭訓練輸出層,而其余層的參數都是基于源模型的參數微調得到的。

步驟示例2:

在已經訓練好的網絡上進行修改;凍結網絡的原來那一部分;訓練新添加的部分;解凍原來網絡的部分層;聯(lián)合訓練解凍的層和新添加的部分。

2.4、參數凍結---指定訓練模型的部分層

????????我們所提到的凍結模型、凍結部分層,其實歸根結底都是對參數進行凍結。凍結訓練可以加快訓練速度。在這里,有兩種方式:全程凍結與非全程凍結。
????????非全程凍結比全程凍結多了一個步驟:解凍,因此這里就講解非全程凍結。看完非全程凍結之后,就明白全程凍結是如何進行的了。
????????非全程凍結訓練分為兩個階段,分別是凍結階段和解凍階段。當處于凍結階段時,被凍結的參數就不會被更新,在這個階段,可以看做是全程凍結;而處于解凍階段時,就和普通的訓練一樣了,所有參數都會被更新。
????????當進行凍結訓練時,占用的顯存較小,因為僅對部分網絡進行微調。如果計算資源不夠,也可以通過凍結訓練的方式來減少訓練時資源的占用。

因為一般需要保留Features Extractor的結構和參數,提出了兩種訓練方法:

  • 固定預訓練的參數:requires_grad = False 或者 lr = 0,即不更新參數;
  • Features Extractor部分設置很小的學習率,這里用到參數組(params_group)的概念,分組設置優(yōu)化器的參數。

2.5、參數凍結的方式

我們經常提到的模型,就是一個可遍歷的字典。既然是字典,又是可遍歷的,那么就有兩種方式進行索引:一是通過數字,二是通過名字。

其實使用凍結很簡單,沒有太高深的魔法,只用設置模型的參數requires_grad為False就可以了。

2.5.1、凍結方式1

在默認情況下,參數的屬性??.requires_grad = True???,如果我們從頭開始訓練或微調不需要注意這里。但如果我們正在提取特征并且只想為新初始化的層計算梯度,其他參數不進行改變。那我們就需要通過設置??requires_grad = False??來凍結部分層。在PyTorch官方中提供了這樣一個例程。

def set_parameter_requires_grad(model, feature_extracting):
    if feature_extracting:
        for param in model.parameters():
            param.requires_grad = False

在下面我們使用??resnet18??為例的將1000類改為4類,但是僅改變最后一層的模型參數,不改變特征提取的模型參數;

  • 注意我們先凍結模型參數的梯度;
  • 再對模型輸出部分的全連接層進行修改,這樣修改后的全連接層的參數就是可計算梯度的。

在訓練過程中,model仍會進行梯度回傳,但是參數更新則只會發(fā)生在fc層。通過設定參數的??requires_grad??屬性,我們完成了指定訓練模型的特定層的目標,這對實現模型微調非常重要。

import torchvision.models as models
# 凍結參數的梯度
feature_extract = True
model = models.resnet18(pretrained=True)
set_parameter_requires_grad(model, feature_extract)
# 修改模型, 輸出通道4, 此時,fc層就被隨機初始化了,但是其他層依然保存著預訓練得到的參數。
model.fc = nn.Linear(in_features=512, out_features=4, bias=True)

我們直接拿??torchvision.models.resnet50 ??模型微調,首先凍結預訓練模型中的所有參數,然后替換掉最后兩層的網絡(替換2層池化層,還有fc層改為dropout,正則,線性,激活等部分),最后返回模型:

# 8 更改池化層
class AdaptiveConcatPool2d(nn.Module):
    def __init__(self, size=None):
        super().__init__()
        size = size or (1, 1) # 池化層的卷積核大小,默認值為(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 遷移學習:拿到一個成熟的模型,進行模型微調
def get_model():
    model_pre = models.resnet50(pretrained=True) # 獲取預訓練模型
    # 凍結預訓練模型中所有的參數
    for param in model_pre.parameters():
        param.requires_grad = False
    # 微調模型:替換ResNet最后的兩層網絡,返回一個新的模型
    model_pre.avgpool = AdaptiveConcatPool2d() # 池化層替換
    model_pre.fc = nn.Sequential(
        nn.Flatten(), # 所有維度拉平
        nn.BatchNorm1d(4096), # 256 x 6 x 6   ——> 4096
        nn.Dropout(0.5),  # 丟掉一些神經元
        nn.Linear(4096, 512),  # 線性層的處理
        nn.ReLU(), # 激活層
        nn.BatchNorm1d(512), # 正則化處理
        nn.Linear(512,2),
        nn.LogSoftmax(dim=1), # 損失函數
    )
 
    return

2.5.2、凍結方式2

因為ImageNet有1000個類別,所以提供的ImageNet預訓練模型也是1000分類。如果我需要訓練一個10分類模型,理論上來說只需要修改最后一層的全連接層即可。

如果前面的參數不凍結就表示所有特征提取的層會使用預訓練模型的參數來進行參數初始化,而最后一層的參數還是保持某種初始化的方式來進行初始化。

在模型中,每一層的參數前面都有前綴,比如conv1、conv2、fc3、backbone等等,我們可以通過這個前綴來進行判斷,也就是通過名字來判斷,如:if "backbone" ?in param.name,最終選擇需要凍結與不需要凍結的層。最后需要將訓練的參數傳入優(yōu)化器進行配置。

if freeze_layers:
       for name, param in model.named_parameters():
           # 除最后的全連接層外,其他權重全部凍結
           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)

或者判斷該參數位于模型的哪些模塊層中,如param in model.backbone.parameters(),然后對于該模塊層的全部參數進行批量設置,將requires_grad置為False。

if Freeze_Train:
   for param in model.backbone.parameters():
       param.requires_grad = False

2.5.2、凍結方式3

通過數字來遍歷模型中的層的參數,凍結所指定的若干個參數, 這種方式用的少

count = 0
for layer in model.children():
   count = count + 1
   if count < 10:
       for param in layer.parameters():
           param.requires_grad = False
 
# 然后將需要訓練的參數傳入優(yōu)化器,也就是過濾掉被凍結的參數。
optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=LR)

2.6、修改模型參數

前面說道,凍結模型就是凍結參數,那么這里的修改模型參數更多的是修改模型參數的名稱。

值得一提的是,由于訓練方式(單卡、多卡訓練)、模型定義的方式不同,參數的名稱也會有所區(qū)別,但是此時模型的結構是一樣的,依舊可以加載預訓練模型。不過卻無法直接載入預訓練模型的參數,因為名稱不同,會出現KeyError的錯誤,所以載入前可能需要修改參數的名稱。

比如說,使用多卡訓練時,保存的時候每個參數前面多會多出'module.'這幾個字符,那么當使用單卡載入時,可能就會報錯了。

通過以下方式,就可以使用'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、修改模型結構

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),  # 使用激活函數
           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),# 使用激活函數
           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),# 使用激活函數
           nn.Conv2d(384, 256, kernel_size=3, padding=1),# 使用卷積層,輸入為384,輸出為256,核大小為3,步長為1
           nn.ReLU(inplace=True),# 使用激活函數
           nn.Conv2d(256, 256, kernel_size=3, padding=1),# 使用卷積層,輸入為256,輸出為256,核大小為3,步長為1
           nn.ReLU(inplace=True),# 使用激活函數
           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),# 使用激活函數
           nn.Dropout(),# 使用Dropout來減緩過擬合
           nn.Linear(4096, 4096),  # 維度不變,因為后面引入了激活函數,從而引入非線性
           nn.ReLU(inplace=True),  # 使用激活函數
           nn.Linear(4096, 1000),   #ImageNet默認為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:  # 判斷預訓練模型路徑是否為空,如果不為空則加載
       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) # 根據數據集的類別數來指定最后輸出的out_features數目
   return net

在上述代碼中,我是先將權重載入全部網絡結構中。此時,模型的最后一層大小并不是我想要的,因此我獲取了輸入到最后一層全連接層之前的維度大小,然后根據數據集的類別數來指定最后輸出的out_features數目,以此代替原來的全連接層。

你也可以先定義好具有指定全連接大小的網絡結構,然后除了最后一層全連接層之外,全部層都載入預訓練模型;你也可以先將權重載入全部網絡結構中,然后刪掉最后一層全連接層,最后再加入一層指定大小的全連接層。

原文鏈接:https://blog.csdn.net/ytusdc/article/details/128522887

欄目分類
最近更新