PyTorch学习记录——PyTorch进阶训练技巧
PyTorch學習記錄——PyTorch進階訓練技巧
- 1.自定義損失函數
- 1.1 以函數的方式定義損失函數
- 1.2 以類的方式定義損失函數
- 1.3 比較與思考
- 2.動態調整學習率
- 2.1 官方提供的scheduler API
- 2.2 自定義scheduler
- 2.3 問題
- 3.模型微調
- 3.1 模型微調流程
- 3.2 Pytorch中已有模型結構及預訓練參數的復用
- 3.3 Pytorch中模型微調的實現
- 3.3.1 固定微調部分,訓練差異部分
- 3.3.2 不同學習率訓練不同部分
- 3.4 timm庫
- 補充知識
- filter的基礎用法
- 4.半精度訓練
- 5.使用argparse進行調參
- 參考資料
1.自定義損失函數
自定義損失函數的方法主要包括兩種,即以函數的方式定義和以類的方式定義。
1.1 以函數的方式定義損失函數
以函數的方式定義與定義python函數沒有什么區別,通過將參與損失計算的張量(即Tensor)作為函數的形參進行定義,例如
def my_loss(output: torch.Tensor, target: torch.Tensor):loss = torch.mean((output - target) ** 2)return loss在上述定義中,我們使用了MSELoss損失函數。同時可以看到,在損失函數編寫過程中,可以直接使用我們熟悉的Python中的運算符,包括加減乘除等等,但牽涉到矩陣運算,如矩陣乘法則需要使用Pytorch提供的張量計算接口torch.matmul。采用這樣的方式定義損失函數實際上就僅需要把計算過程定義清楚即可,或者說是把計算圖或數據流定義清楚。
1.2 以類的方式定義損失函數
以類的方式定義損失函數需要讓我們定義的類繼承nn.Module類。采用這樣的方式定義損失函數類,可以讓我們把定義的損失函數作為一個神經網絡層來對待。Pytorch現有的損失函數也大都采用這種類的方式進行定義的。事實上,在Pytorch中,Loss函數部分繼承自_loss, 部分繼承自_WeightedLoss, 而_WeightedLoss繼承自_loss, _loss繼承自 nn.Module。例如,通過查看Pytorch中CrossEntropyLoss的代碼,我們可以看到上述關系,如下。
class CrossEntropyLoss(_WeightedLoss):...class _WeightedLoss(_Loss):...class _Loss(Module):...1.3 比較與思考
教程中有說到,相比于以函數的方式定義的損失函數,類的方式定義更為常用。
雖然以函數定義的方式很簡單,但是以類方式定義更加常用,…
然而,從教程中給出的例子,比如DiceLoss損失函數的定義
class DiceLoss(nn.Module):def __init__(self,weight=None,size_average=True):super(DiceLoss,self).__init__()def forward(self,inputs,targets,smooth=1):inputs = F.sigmoid(inputs) inputs = inputs.view(-1)targets = targets.view(-1)intersection = (inputs * targets).sum() dice = (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth) return 1 - dice確實又難以體現出其相比函數方法的優越之處。考慮到類這種面向對象的設計方式,上述采用類的方式設計損失函數可能存在如下兩個方面的優勢:
-
當損失函數計算過程中出現一些類似滑動平均等需要動態緩存一些數的時候,采用類的方式可以直接將這樣的數存放在實體對象中;
-
采用類的方式可以通過繼承的方式梳理清楚不同損失函數的關系,并有可能能復用一些父類損失函數的特性和方法。
2.動態調整學習率
無論是在深度學習任務中還是深度強化學習任務中,學習率對于神經網絡的訓練非常重要。因為本質上講,兩者都是通過數據驅動的手段,通過梯度下降類算法,對神經網絡的參數進行尋優。對于一個任務,在起始時,我們可能設定了一個比較好的學習率。這使得我們的算法在訓練初期收斂的效率和效果都較好。但隨著訓練的進行,特別是當網絡參數非常靠近我們期待的位置時(神經網絡參數空間中的理想點),我們初期設置的學習率可能就會顯得偏大,導致梯度下降過程步長過長,從而使得神經網絡參數在理想點附近震蕩。
為解決上述問題,一種方式是通過手動調整學習率,來適應神經網絡訓練不同的時期,以及神經網絡所達到的不同性能。但這樣的方式就要求我們要能夠自行設計出一套學習率變化的算法,這無疑為我們程序訓練的編寫又增加了復雜度。另一種方式下,我們可以使用Pytorch中的scheduler進行動態的學習率調整。
Pytorch的scheduler可以提供兩種使用方式的支持:官方提供的scheduler API和自定義的scheduler。
2.1 官方提供的scheduler API
官方提供的scheduler API主要放在torch.optim.lr_scheduler中,具體包括
| lr_scheduler.LambdaLR | 學習率lr為一個初始值乘以一個函數,當last_epoch=-1時,lr取值為初始值 | * optimizer (Optimizer) – Wrapped optimizer. * lr_lambda (function or list) – A function which computes a multiplicative factor given an integer parameter epoch, or a list of such functions, one for each group in optimizer.param_groups. |
* last_epoch (int) – The index of last epoch. Default: -1. | ||
* verbose (bool) – If True, prints a message to stdout for each update. Default: False. | ||
| lr_scheduler.MultiplicativeLR | 學習率lr為一個初始值乘以一個函數,當last_epoch=-1時,lr取值為初始值 | * optimizer (Optimizer) – Wrapped optimizer. |
* lr_lambda (function or list) – A function which computes a multiplicative factor given an integer parameter epoch, or a list of such functions, one for each group in optimizer.param_groups. | ||
* last_epoch (int) – The index of last epoch. Default: -1. | ||
* verbose (bool) – If True, prints a message to stdout for each update. Default: False. | ||
| lr_scheduler.StepLR | 每step_size個epoch,學習率lr變為其當前值乘以gamma,當last_epoch=-1時,lr取值為初始值 | * optimizer (Optimizer) – Wrapped optimizer. |
* step_size (int) – Period of learning rate decay. | ||
* gamma (float) – Multiplicative factor of learning rate decay. Default: 0.1. | ||
* last_epoch (int) – The index of last epoch. Default: -1. | ||
* verbose (bool) – If True, prints a message to stdout for each update. Default: False. | ||
| lr_scheduler.MultiStepLR | 當epoch數達到milestones數量時,學習率lr變為其當前值乘以gamma,當last_epoch=-1時,lr取值為初始值 | * optimizer (Optimizer) – Wrapped optimizer. |
* milestones (list) – List of epoch indices. Must be increasing. | ||
* gamma (float) – Multiplicative factor of learning rate decay. / * Default: 0.1. | ||
* last_epoch (int) – The index of last epoch. Default: -1. | ||
* verbose (bool) – If True, prints a message to stdout for each update. Default: False. | ||
| lr_scheduler.ExponentialLR | 每個epoch,學習率lr變為其當前值乘以gamma,當last_epoch=-1時,lr取值為初始值 | * optimizer (Optimizer) – Wrapped optimizer. |
* gamma (float) – Multiplicative factor of learning rate decay. | ||
* last_epoch (int) – The index of last epoch. Default: -1. | ||
* verbose (bool) – If True, prints a message to stdout for each update. Default: False. | ||
| lr_scheduler.CosineAnnealingLR | 采用cos衰減的方式調整學習率,當last_epoch=-1時,lr取值為初始值 | * optimizer (Optimizer) – Wrapped optimizer. |
* T_max (int) – Maximum number of iterations. | ||
* eta_min (float) – Minimum learning rate. Default: 0. | ||
* last_epoch (int) – The index of last epoch. Default: -1. | ||
* verbose (bool) – If True, prints a message to stdout for each update. Default: False. | ||
| lr_scheduler.ReduceLROnPlateau | 當某項指標不再下降時削減學習率 | * ptimizer (Optimizer) – Wrapped optimizer. |
* mode (str) – One of min, max. In min mode, lr will be reduced when the quantity monitored has stopped decreasing; in max mode it will be reduced when the quantity monitored has stopped increasing. Default: ‘min’. | ||
* factor (float) – Factor by which the learning rate will be reduced. new_lr = lr * factor. Default: 0.1. | ||
* patience (int) – Number of epochs with no improvement after which learning rate will be reduced. For example, if patience = 2, then we will ignore the first 2 epochs with no improvement, and will only decrease the LR after the 3rd epoch if the loss still hasn’t improved then. Default: 10. | ||
* threshold (float) – Threshold for measuring the new optimum, to only focus on significant changes. Default: 1e-4. | ||
* threshold_mode (str) – One of rel, abs. In rel mode, dynamic_threshold = best * ( 1 + threshold ) in ‘max’ mode or best * ( 1 - threshold ) in min mode. In abs mode, dynamic_threshold = best + threshold in max mode or best - threshold in min mode. Default: ‘rel’. | ||
* cooldown (int) – Number of epochs to wait before resuming normal operation after lr has been reduced. Default: 0. | ||
min_lr (float or list) – A scalar or a list of scalars. A lower bound on the learning rate of all param groups or each group respectively. Default: 0. | ||
* eps (float) – Minimal decay applied to lr. If the difference between new and old lr is smaller than eps, the update is ignored. Default: 1e-8. | ||
* verbose (bool) – If True, prints a message to stdout for each update. Default: False. | ||
| lr_scheduler.CyclicLR | 以某種循環策略調整學習率 | 詳見https://pytorch.org/docs/stable/generated/torch.optim.lr_scheduler.CyclicLR.html#torch.optim.lr_scheduler.CyclicLR |
| lr_scheduler.OneCycleLR | 以某種單次循環策略調整學習率 | 詳見https://pytorch.org/docs/stable/generated/torch.optim.lr_scheduler.OneCycleLR.html#torch.optim.lr_scheduler.OneCycleLR |
| lr_scheduler.CosineAnnealingWarmRestarts | 采用cos衰減的方式調整學習率,當last_epoch=-1時,lr取值為初始值 |
在訓練中,上述scheduler API通過實例化創建scheduler實例,再通過在optimizer優化一步(即調用step()方法)后,調用step()方法進行學習率調整,如下:
# 選擇優化器 optimizer = torch.optim.Adam(...) # 選擇一種或多種動態調整學習率方法 scheduler = torch.optim.lr_scheduler.... # 進行訓練 for epoch in range(100):train(...)validate(...)optimizer.step()# 在優化器參數更新之后再動態調整學習率scheduler.step() ...2.2 自定義scheduler
自定義scheduler的方法是通過構建自定義函數adjust_learning_rate,來改變optimizer的param_group中lr的值實現的,例如:
def adjust_learning_rate(optimizer, epoch):lr = args.lr * (0.1 ** (epoch // 30))for param_group in optimizer.param_groups:param_group['lr'] = lr基于此定義的scheduler,我們便可以在訓練時進行使用,如下:
# 選擇優化器 optimizer = torch.optim.Adam(...) # 進行訓練 for epoch in range(100):train(...)validate(...)optimizer.step()# 調用自定義函數調整學習率adjust_learning_rate(optimizer, epoch)...2.3 問題
在使用自定義學習率調整函數時,自定義學習率調整函數是否也要放在optimizer.step()語句之后?
3.模型微調
當前,模型參數的規模持續膨脹,能夠達到的能力水平,甚至跨任務的泛化水平也在不斷提高,但這些模型往往均是通過在大數據集上訓練的。因此,當前在很多深度學習任務求解上的做法是基于一個在很大的數據集上訓練的模型進行進一步調整實現的。這其實就是模型微調——基于預訓練模型在當前任務上進行進一步訓練。也正是基于這樣的思想,近年來預訓練大模型開始成為熱點。從BERT到GPT-3,到大模型的出現一方面促進了AI模型泛化能力的提升,另一方面也削減了下游任務(具體任務)的訓練成本,催生了“大模型預訓練+微調”的應用研發范式。
Pytorch提供了許多預訓練好的網絡模型(VGG,ResNet系列,mobilenet系列…),這些模型都是PyTorch官方在相應的大型數據集訓練好的。在面對具體下游任務時,我們可以從中選擇與我們任務接近的模型,換成我們的數據進行精調,也就是我們經常見到的finetune。
3.1 模型微調流程
模型精調分為如下幾步:
在源數據集上預訓練一個神經網絡模型,即源模型。這一步實際上預訓練模型制作方為我們準備好了,即我們在Pytorch中拿到的就已經是預訓練好的模型了。
創建新的神經網絡模型,即目標模型。將源模型中除了最終的輸出層外所有部分的模型和相應的參數復制到目標模型中。這一步是通過模型結構和參數的拷貝,將源模型(預訓練模型)預訓練中的經驗賦予目標模型。但由于目標模型與源模型面對的任務不同,因此,目標模型中最后的輸出層保留獨立。我認為,這里不僅限于輸出層,擴充一些。只要是針對當前任務特有的層都可以保留相對于源模型的獨立性。
為目標模型添加一個與目標模型任務想匹配的輸出層,并隨機初始化該層的模型參數。
在目標數據集上訓練目標模型。對于輸出層(即目標模型特有的部分),我們將從頭訓練,而其余層的參數都是基于源模型的參數微調得到的。
在上述流程下,我們可以實現對模型的精調。下面,需要考慮如下幾個方面的細節:
- Pytorch中已有模型結構及預訓練參數的復用
- Pytorch中模型微調的實現
3.2 Pytorch中已有模型結構及預訓練參數的復用
Pytorch中提供了許多預訓練好的網絡模型,包括它們的網絡結構和預訓練模型權重,均可通過torchvision獲取,各個預訓練網絡模型實例的創建如下:
import torchvision.models as models resnet18 = models.resnet18() alexnet = models.alexnet() vgg16 = models.vgg16() squeezenet = models.squeezenet1_0() densenet = models.densenet161() inception = models.inception_v3() googlenet = models.googlenet() shufflenet = models.shufflenet_v2_x1_0() mobilenet_v2 = models.mobilenet_v2() mobilenet_v3_large = models.mobilenet_v3_large() mobilenet_v3_small = models.mobilenet_v3_small() resnext50_32x4d = models.resnext50_32x4d() wide_resnet50_2 = models.wide_resnet50_2() mnasnet = models.mnasnet1_0()上述預訓練模型實例創建中調用的初始化函數都包含有一個pretrained參數,該參數默認為False。故采用上述代碼,我們僅獲得了預訓練網絡模型的結構,而參數是隨機初始化的。為載入預訓練好的模型參數,需將pretrained參數設置為True,如下:
import torchvision.models as models resnet18 = models.resnet18(pretrained=True)注意:
-
程序運行時會首先檢查默認路徑(在Linux和Mac的是用戶根目錄下的.cache文件夾。在Windows下是C:\Users\<username>\.cache\torch\hub\checkpoint)中是否有已經下載的模型權重,一旦權重被下載,下次加載就不需要下載了。
-
我們也可以將自己的權重下載下來放到同文件夾下,然后再將參數加載網絡,例如:
import torchvision.models as models import torch model = models.resnet18(pretrained=False) model.load_state_dict(torch.load('/models/resnet18-f37072fd.pth'))
3.3 Pytorch中模型微調的實現
在上述模型微調流程中,我們僅對與當前任務密切相關且與預訓練模型有差異的部分進行完整訓練,而對與預訓練模型一致的部分進行微調。微調方式分為兩種:
-
方法1:先不訓練微調部分,集中訓練差異部分,而后在以較小的學習率整體微調訓練
-
方法2:采用不同的學習率訓練微調部分和差異部分
3.3.1 固定微調部分,訓練差異部分
在默認情況下,模型參數的屬性.requires_grad = True。如果我們正在提取特征并且只想為新初始化的層計算梯度,其他參數不進行改變,那我們就需要將設置requires_grad = False來凍結部分層,例如:
import torchvision.models as models# 載入預訓練模型 model = models.resnet18(pretrained=True)# 凍結預訓練模型部分梯度 for param in model.parameters():param.requires_grad = False# 修改模型 num_ftrs = model.fc.in_features model.fc = nn.Linear(in_features=num_ftrs, out_features=4, bias=True)上述設計下,訓練過程中梯度僅會回傳至fc層,而不會影響預訓練模型部分。
3.3.2 不同學習率訓練不同部分
具體而言,得益于Pytorch的靈活性,我們可以采用不同的學習率對不同的模型層進行訓練,以實現采用較大的學習率進行完整訓練,采用較小的學習率進行微調訓練。現假設我們僅對上述resnet18的最后一層進行調整,以實現一個二分類任務。
首先,我們對原有的resnet18的網絡結構和模型參數進行“繼承”
import torchvision.models as models import torch import torch.nn as nn import torch.optim as optimmodel = models.resnet18(pretrained=False) model.load_state_dict(torch.load('/models/resnet18-f37072fd.pth'))修改最后的輸出層
model.fc = nn.Linear(512, 2)然后,分割出特征提取部分模型參數(預訓練模型參數)和輸出層模型參數
output_params = list(map(id, model.fc.parameters())) feature_params = list(filter(lambda p: id(p) not in output_params, model.parameters()))接著,我們通過在優化器中指定不同部分的模型采用不同的學習率進行訓練,即可實現基于預訓練模型的精調,如下:
lr = 0.001 optimizer = opim.SGD([{'params': feature_params},{'params': model.fc.parameters(), 'lr': lr * 10}], lr=lr, weight_decay=0.001)3.4 timm庫
上述預訓練模型微調中,我們主要使用的是torchvision庫。除了該庫以外,還有一個常見的預訓練模型庫,叫做timm,這個庫是由來自加拿大溫哥華Ross Wightman創建的。里面提供了許多計算機視覺的SOTA模型,可以當作是torchvision的擴充版本,并且里面的模型在準確度上也較高。
在得到我們想要使用的預訓練模型后,我們可以通過timm.create_model()的方法來進行模型的創建,我們可以通過傳入參數pretrained=True,來使用預訓練模型。同樣的,我們也可以使用跟torchvision里面的模型一樣的方法查看模型的參數,類型。關于預訓練模型的修改,似乎僅能通過timm.create_model()方法接口在創建和載入模型時進行修改,但尚不確定。如果后續使用,可參考官網或者Github鏈接。
import timm import torchmodel = timm.create_model('resnet34',pretrained=True) # 修改模型(將1000類改為10類輸出) model = timm.create_model('resnet34',num_classes=10,pretrained=True) # 改變輸入通道數 model = timm.create_model('resnet34',num_classes=10,pretrained=True,in_chans=1)補充知識
filter的基礎用法
filter是一個過濾器,其作用是從列表(或其他序列類型)中篩選出滿足條件的子列表。對于列表(或其他序列類型),如果希望從中篩選出滿足某個約束條件的子列表,我們一般的做法是使用一個for循環遍歷每個元素然后執行相同約束條件判斷,將滿足條件的放入新的子列表中。例如,從列表中找出所有偶數子列表,并按對應的先后順序放入子列表中:
a = [1, 2, 3, 4, 5] b = [] for i in a:if i % 2 == 0:b.append(i)那么如果使用filter的話,使用filter函數使得代碼變得更簡潔:
a = [1, 2, 3, 4, 5] def check(i): return i % 2 == 0 b = list(filter(check, a))4.半精度訓練
GPU的性能主要分為兩部分:算力和顯存,前者決定了顯卡計算的速度,后者則決定了顯卡可以同時放入多少數據用于計算。在可以使用的顯存數量一定的情況下,每次訓練能夠加載的數據更多(也就是batch size更大),則也可以提高訓練效率。另外,有時候數據本身也比較大(比如3D圖像、視頻等),顯存較小的情況下可能甚至batch size為1的情況都無法實現。因此,合理使用顯存也就顯得十分重要。
PyTorch默認的浮點數存儲方式用的是torch.float32,小數點后位數更多固然能保證數據的精確性,但絕大多數場景其實并不需要這么精確,只保留一半的信息也不會影響結果,也就是使用torch.float16格式。由于數位減了一半,因此被稱為“半精度”,半精度能夠減少顯存占用,使得顯卡可以同時加載更多數據進行計算。
在PyTorch中使用autocast配置半精度訓練,同時需要在下面三處加以設置:
-
import autocast
from torch.cuda.amp import autocast -
模型設置
在模型定義中,使用python的裝飾器方法,用autocast裝飾模型中的forward函數。
@autocast() def forward(self, x):...return x -
訓練過程
在訓練過程中,只需在將數據輸入模型及其之后的部分放入“with autocast():“即可
for x in train_loader:x = x.cuda()with autocast():output = model(x)...
半精度訓練主要適用于數據本身的size比較大(比如說3D圖像、視頻等)。當數據本身的size并不大時(比如手寫數字MNIST數據集的圖片尺寸只有28*28),使用半精度訓練則可能不會帶來顯著的提升。
5.使用argparse進行調參
argparse是python的命令行解析的標準模塊,可以讓我們直接在命令行中就可以向程序中傳入參數,通過argparse將命令行傳入的其他參數進行解析、保存和使用。在使用argparse后,我們在命令行輸入的參數就可以以這種形式python file.py --lr 1e-4 --batch_size 32來完成對常見超參數的設置。
總的來說,我們可以將argparse的使用歸納為以下三個步驟:
總的來說,argparse確實能夠便捷化和靈活化命令行參數傳遞和解析,但即便將其封裝在方法里,如果存在多處獲取,獲取過程還是要反復調用該方法/函數。因此,如果是多處調用的配置,可以采用argparse和配置文件相結合的方式進行參數配置,動態性較強的通過argparse進行配置,而靜態性較強的則可以采用配置文件的形式進行配置。
參考資料
深入淺出PyTorch:第六章 PyTorch進階訓練技巧
總結
以上是生活随笔為你收集整理的PyTorch学习记录——PyTorch进阶训练技巧的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【2021-2022 春学期】人工智能-
- 下一篇: 微信小程序中转换时间格式IOS不兼容的问