Pytorch—模型微调(fine-tune)
????????隨著深度學習的發展,在大模型的訓練上都是在一些較大數據集上進行訓練的,比如Imagenet-1k,Imagenet-11k,甚至是ImageNet-21k等。但我們在實際應用中,我們自己的數據集可能比較小,只有幾千張照片,這時從頭訓練具有幾千萬參數的大型神經網絡是不現實的,因為越大的模型對數據量的要求越高,過擬合無法避免。
????????因為適用于ImageNet數據集的復雜模型,在一些小的數據集上可能會過擬合,同時因為數據量有限,最終訓練得到的模型的精度也可能達不到實用要求。
解決上述問題的方法:
1、模型微調(fine-tune)
????????微調(fine-tune)通過使用在大數據上得到的預訓練好的模型來初始化自己的模型權重,從而提升精度。這就要求預訓練模型質量要有保證。微調通常速度更快、精度更高。當然,自己訓練好的模型也可以當做預訓練模型,然后再在自己的數據集上進行訓練,來使模型適用于自己的場景、自己的任務。
先引入遷移學習(Transfer Learning)的概念:
????????當我們訓練好了一個模型之后,如果想應用到其他任務中,可以在這個模型的基礎上進行訓練,來作微調網絡。這也是遷移學習的概念,可以節省訓練的資源以及訓練的時間。
????????遷移學習的一大應用場景就是模型微調,簡單的來說就是把在別人訓練好的基礎上,換成自己的數據集繼續訓練,來調整參數。Pytorch中提供很多預訓練模型,學習如何進行模型微調,可以大大提升自己任務的質量和速度。
????????假設我們要識別的圖片類別是椅子,盡管ImageNet數據集中的大多數圖像與椅子無關,但在ImageNet數據集上訓練的模型可能會提取更通用的圖像特征,這有助于識別邊緣、紋理、形狀和對象組合。 這些類似的特征對于識別椅子也可能同樣有效。
為什么需要遷移學習:
1) 大數據與少標注的矛盾
雖然有大量的數據,但往往都是沒有標注的,無法訓練機器學習模型。人工進行數據標定太耗時。
2) 大數據與弱計算的矛盾
普通人無法擁有龐大的數據量與計算資源。因此需要借助于模型的遷移。
3) 普適化模型與個性化需求的矛盾
即使是在同一個任務上,一個模型也往往難以滿足每個人的個性化需求,比如特定的隱私設置。這就需要在不同人之間做模型的適配。
4) 特定應用(如冷啟動)的需求
遷移學習可以初步初始化網絡,因為對一些比較類似的任務,其實模型參數的值基本上相同,而且這些參數經過大量的訓練,已經有很好的特征提取能力,將backbone參數使用這類模型進行權重的初始化,后面做training的時候,模型收斂速度會更快。
負遷移問題:
???????負遷移(Negative Transfer)指的是,在源域上學習到的知識,對于目標域上的學習產生負面作用。
產生負遷移的原因主要有:
1、數據問題:源域和目標域壓根不相似,談何遷移?
2、方法問題:源域和目標域是相似的,但是,遷移學習方法不夠好,沒找到可遷移的成分。
負遷移給遷移學習的研究和應用帶來了負面影響。在實際應用中,找到合理的相似性,并且選擇或開發合理的遷移學習方法,能夠避免負遷移現象。
2.1、為什么要微調
????????因為預訓練模型用了大量數據做訓練,已經具備了提取淺層基礎特征和深層抽象特征的能力。
對于圖片來說,我們CNN的前幾層學習到的都是低級的特征,比如,點、線、面,這些低級的特征對于任何圖片來說都是可以抽象出來的,所以我們將他作為通用數據,只微調這些低級特征組合起來的高級特征即可,例如,這些點、線、面,組成的是園還是橢圓,還是正方形,這些代表的含義是我們需要后面訓練出來的。
????????如果我們自己的數據不夠多,泛化性不夠強,那么可能存在模型不收斂,準確率低,模型泛化能力差,過擬合等問題,所以這時就需要使用預訓練模型來做微調了。注意的是,進行微調時,應該使用較小的學習率。因為預訓練模型的權重相對于隨機初始化的權重來說已經很不錯了,所以不希望使用太大的學習率來破壞原本的權重。通常用于微調的初始學習率會比從頭開始訓練的學習率小10倍。
總結:對于不同的層可以設置不同的學習率,一般情況下建議,對于使用的原始數據做初始化的層設置的學習率要小于(一般可設置小于10倍)初始化的學習率,這樣保證對于已經初始化的數據不會扭曲的過快,而使用初始化學習率的新層可以快速的收斂。
2.2、需要微調的情況
????????其中微調的方法又要根據自身數據集和預訓練模型數據集的相似程度,以及自己數據集的大小來抉擇。
不同情況下的微調:
注意:
2.3、?模型微調的流程
微調的步驟有很多,看你自身數據和計算資源的情況而定。雖然各有不同,但是總體的流程大同小異。
步驟示例1:
1、在源數據集(如ImageNet數據集)上預訓練一個神經網絡模型,即源模型。
2、創建一個新的神經網絡模型,即目標模型。它復制了源模型上除了輸出層外的所有模型設計及其參數。
- 我們假設這些模型參數包含了源數據集上學習到的知識,且這些知識同樣適用于目標數據集。
- 我們還假設源模型的輸出層跟源數據集的標簽緊密相關,因此在目標模型中不予采用。
3、為目標模型添加一個輸出大小為目標數據集類別個數的輸出層,并隨機初始化該層的模型參數。
4、在目標數據集(如椅子數據集)上訓練目標模型。可以從頭訓練輸出層,而其余層的參數都是基于源模型的參數微調得到的。
步驟示例2:
2.4、參數凍結---指定訓練模型的部分層
????????我們所提到的凍結模型、凍結部分層,其實歸根結底都是對參數進行凍結。凍結訓練可以加快訓練速度。在這里,有兩種方式:全程凍結與非全程凍結。
????????非全程凍結比全程凍結多了一個步驟:解凍,因此這里就講解非全程凍結。看完非全程凍結之后,就明白全程凍結是如何進行的了。
????????非全程凍結訓練分為兩個階段,分別是凍結階段和解凍階段。當處于凍結階段時,被凍結的參數就不會被更新,在這個階段,可以看做是全程凍結;而處于解凍階段時,就和普通的訓練一樣了,所有參數都會被更新。
????????當進行凍結訓練時,占用的顯存較小,因為僅對部分網絡進行微調。如果計算資源不夠,也可以通過凍結訓練的方式來減少訓練時資源的占用。
因為一般需要保留Features Extractor的結構和參數,提出了兩種訓練方法:
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仍會進行梯度回傳,但是參數更新則只會發生在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) # 池化層1self.pool_two = nn.AdaptiveAvgPool2d(size) # 池化層2def 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 ——> 4096nn.Dropout(0.5), # 丟掉一些神經元nn.Linear(4096, 512), # 線性層的處理nn.ReLU(), # 激活層nn.BatchNorm1d(512), # 正則化處理nn.Linear(512,2),nn.LogSoftmax(dim=1), # 損失函數)return2.5.2、凍結方式2
因為ImageNet有1000個類別,所以提供的ImageNet預訓練模型也是1000分類。如果我需要訓練一個10分類模型,理論上來說只需要修改最后一層的全連接層即可。
如果前面的參數不凍結就表示所有特征提取的層會使用預訓練模型的參數來進行參數初始化,而最后一層的參數還是保持某種初始化的方式來進行初始化。
在模型中,每一層的參數前面都有前綴,比如conv1、conv2、fc3、backbone等等,我們可以通過這個前綴來進行判斷,也就是通過名字來判斷,如:if "backbone" ?in param.name,最終選擇需要凍結與不需要凍結的層。最后需要將訓練的參數傳入優化器進行配置。
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 = False2.5.2、凍結方式3
通過數字來遍歷模型中的層的參數,凍結所指定的若干個參數, 這種方式用的少
count = 0 for layer in model.children():count = count + 1if count < 10:for param in layer.parameters():param.requires_grad = False# 然后將需要訓練的參數傳入優化器,也就是過濾掉被凍結的參數。 optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=LR)2.6、修改模型參數
前面說道,凍結模型就是凍結參數,那么這里的修改模型參數更多的是修改模型參數的名稱。
值得一提的是,由于訓練方式(單卡、多卡訓練)、模型定義的方式不同,參數的名稱也會有所區別,但是此時模型的結構是一樣的,依舊可以加載預訓練模型。不過卻無法直接載入預訓練模型的參數,因為名稱不同,會出現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 torchclass 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,步長為4nn.ReLU(inplace=True), # 使用激活函數nn.MaxPool2d(kernel_size=3, stride=2), # 使用最大池化,這里的大小為3,步長為2nn.Conv2d(64, 192, kernel_size=5, padding=2), # 使用卷積層,輸入為64,輸出為192,核大小為5,步長為2nn.ReLU(inplace=True),# 使用激活函數nn.MaxPool2d(kernel_size=3, stride=2), # 使用最大池化,這里的大小為3,步長為2nn.Conv2d(192, 384, kernel_size=3, padding=1), # 使用卷積層,輸入為192,輸出為384,核大小為3,步長為1nn.ReLU(inplace=True),# 使用激活函數nn.Conv2d(384, 256, kernel_size=3, padding=1),# 使用卷積層,輸入為384,輸出為256,核大小為3,步長為1nn.ReLU(inplace=True),# 使用激活函數nn.Conv2d(256, 256, kernel_size=3, padding=1),# 使用卷積層,輸入為256,輸出為256,核大小為3,步長為1nn.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), # 全連接,輸出為4096nn.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 xdef alexnet(num_classes, device, pretrained_weights=""):net=AlexNet() # 定義AlexNetif 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數目,以此代替原來的全連接層。
你也可以先定義好具有指定全連接大小的網絡結構,然后除了最后一層全連接層之外,全部層都載入預訓練模型;你也可以先將權重載入全部網絡結構中,然后刪掉最后一層全連接層,最后再加入一層指定大小的全連接層。
總結
以上是生活随笔為你收集整理的Pytorch—模型微调(fine-tune)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ireport 循环_IReport 常
- 下一篇: h5在线聊天室(附源码)