Pytorch學習(六)——
6.1 自定義損失函式
PyTorch在torch.nn模組為我們提供了許多常用的損失函式,比如:MSELoss,L1Loss,BCELoss...... 但是隨著深度學習的發展,出現了越來越多的非官方提供的Loss,比如DiceLoss,HuberLoss,SobolevLoss...... 這些Loss Function專門針對一些非通用的模型,PyTorch不能將他們全部新增到庫中去,因此這些損失函式的實現則需要我們通過自定義損失函式來實現。另外,在科學研究中,我們往往會提出全新的損失函式來提升模型的表現,這時我們既無法使用PyTorch自帶的損失函式,也沒有相關的部落格供參考,此時自己實現損失函式就顯得更為重要了。
6.1.1 以函式方式定義
事實上,損失函式僅僅是一個函式而已,因此我們可以通過直接以函式定義的方式定義一個自己的函式,如下所示:
python
def my_loss(output, target):
loss = torch.mean((output - target)**2)
return loss
6.1.2 以類方式定義
雖然以函式定義的方式很簡單,但是以類方式定義更加常用,在以類方式定義損失函式時,我們如果看每一個損失函式的繼承關係我們就可以發現Loss
函式部分繼承自_loss
, 部分繼承自_WeightedLoss
, 而_WeightedLoss
繼承自_loss
,_loss
繼承自 nn.Module。我們可以將其當作神經網路的一層來對待,同樣地,我們的損失函式類就需要繼承自nn.Module類,在下面的例子中我們以DiceLoss為例向大家講述。
Dice Loss是一種在分割領域常見的損失函式,定義如下:
$$ DSC = \frac{2|X∩Y|}{|X|+|Y|} $$ 實現程式碼如下:
```python 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
使用方法
criterion = DiceLoss() loss = criterion(input,targets) ```
除此之外,常見的損失函式還有BCE-Dice Loss,Jaccard/Intersection over Union (IoU) Loss,Focal Loss......
```python class DiceBCELoss(nn.Module): def init(self, weight=None, size_average=True): super(DiceBCELoss, 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_loss = 1 - (2.*intersection + smooth)/(inputs.sum() + targets.sum() + smooth)
BCE = F.binary_cross_entropy(inputs, targets, reduction='mean')
Dice_BCE = BCE + dice_loss
return Dice_BCE
class IoULoss(nn.Module): def init(self, weight=None, size_average=True): super(IoULoss, 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()
total = (inputs + targets).sum()
union = total - intersection
IoU = (intersection + smooth)/(union + smooth)
return 1 - IoU
ALPHA = 0.8 GAMMA = 2
class FocalLoss(nn.Module): def init(self, weight=None, size_average=True): super(FocalLoss, self).init()
def forward(self, inputs, targets, alpha=ALPHA, gamma=GAMMA, smooth=1):
inputs = F.sigmoid(inputs)
inputs = inputs.view(-1)
targets = targets.view(-1)
BCE = F.binary_cross_entropy(inputs, targets, reduction='mean')
BCE_EXP = torch.exp(-BCE)
focal_loss = alpha * (1-BCE_EXP)**gamma * BCE
return focal_loss
更多的可以參考連結1
```
注:
在自定義損失函式時,涉及到數學運算時,我們最好全程使用PyTorch提供的張量計算介面,這樣就不需要我們實現自動求導功能並且我們可以直接呼叫cuda,使用numpy或者scipy的數學運算時,操作會有些麻煩,大家可以自己下去進行探索。關於PyTorch使用Class定義損失函式的原因,可以參考PyTorch的討論區(連結6)
本節參考
【1】http://www.kaggle.com/bigironsphere/loss-function-library-keras-pytorch/notebook
【2】http://www.zhihu.com/question/66988664/answer/247952270
【3】http://blog.csdn.net/dss_dssssd/article/details/84103834
【4】http://zj-image-processing.readthedocs.io/zh_CN/latest/pytorch/%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0/
【5】http://blog.csdn.net/qq_27825451/article/details/95165265
【6】http://discuss.pytorch.org/t/should-i-define-my-custom-loss-function-as-a-class/89468
6.2 動態調整學習率
學習率的選擇是深度學習中一個困擾人們許久的問題,學習速率設定過小,會極大降低收斂速度,增加訓練時間;學習率太大,可能導致引數在最優解兩側來回振盪。但是當我們選定了一個合適的學習率後,經過許多輪的訓練後,可能會出現準確率震盪或loss不再下降等情況,說明當前學習率已不能滿足模型調優的需求。此時我們就可以通過一個適當的學習率衰減策略來改善這種現象,提高我們的精度。這種設定方式在PyTorch中被稱為scheduler,也是我們本節所研究的物件。
6.2.1 使用官方scheduler
- 瞭解官方提供的API
在訓練神經網路的過程中,學習率是最重要的超引數之一,作為當前較為流行的深度學習框架,PyTorch已經在torch.optim.lr_scheduler
為我們封裝好了一些動態調整學習率的方法供我們使用,如下面列出的這些scheduler。
lr_scheduler.LambdaLR
lr_scheduler.MultiplicativeLR
lr_scheduler.StepLR
lr_scheduler.MultiStepLR
lr_scheduler.ExponentialLR
lr_scheduler.CosineAnnealingLR
lr_scheduler.ReduceLROnPlateau
lr_scheduler.CyclicLR
lr_scheduler.OneCycleLR
-
使用官方API
關於如何使用這些動態調整學習率的策略,PyTorch
官方也很人性化的給出了使用例項程式碼幫助大家理解,我們也將結合官方給出的程式碼來進行解釋。
```python
選擇一種優化器
optimizer = torch.optim.Adam(...)
選擇上面提到的一種或多種動態調整學習率的方法
scheduler1 = torch.optim.lr_scheduler.... scheduler2 = torch.optim.lr_scheduler.... ... schedulern = torch.optim.lr_scheduler....
進行訓練
for epoch in range(100): train(...) validate(...) optimizer.step() # 需要在優化器引數更新之後再動態調整學習率 scheduler1.step() ... schedulern.step() ```
注:
我們在使用官方給出的torch.optim.lr_scheduler
時,需要將scheduler.step()
放在optimizer.step()
後面進行使用。
6.2.2 自定義scheduler
雖然PyTorch官方給我們提供了許多的API,但是在實驗中也有可能碰到需要我們自己定義學習率調整策略的情況,而我們的方法是自定義函式adjust_learning_rate
來改變param_group
中lr
的值,在下面的敘述中會給出一個簡單的實現。
假設我們現在正在做實驗,需要學習率每30輪下降為原來的1/10,假設已有的官方API中沒有符合我們需求的,那就需要自定義函式來實現學習率的改變。
python
def adjust_learning_rate(optimizer, epoch):
lr = args.lr * (0.1 ** (epoch // 30))
for param_group in optimizer.param_groups:
param_group['lr'] = lr
有了adjust_learning_rate
函式的定義,在訓練的過程就可以呼叫我們的函式來實現學習率的動態變化
python
def adjust_learning_rate(optimizer,...):
...
optimizer = torch.optim.SGD(model.parameters(),lr = args.lr,momentum = 0.9)
for epoch in range(10):
train(...)
validate(...)
adjust_learning_rate(optimizer,epoch)
本節參考
【1】PyTorch官方文件
6.3 模型微調
隨著深度學習的發展,模型的引數越來越大,許多開源模型都是在較大資料集上進行訓練的,比如Imagenet-1k,Imagenet-11k,甚至是ImageNet-21k等。但在實際應用中,我們的資料集可能只有幾千張,這時從頭開始訓練具有幾千萬引數的大型神經網路是不現實的,因為越大的模型對資料量的要求越大,過擬合無法避免。
假設我們想從影象中識別出不同種類的椅⼦,然後將購買連結推薦給使用者。一種可能的方法是先找出100種常見的椅子,為每種椅子拍攝1000張不同⻆度的影象,然後在收集到的影象資料集上訓練一個分類模型。這個椅子資料集雖然可能比Fashion-MNIST資料集要龐⼤,但樣本數仍然不及ImageNet資料集中樣本數的十分之⼀。這可能會導致適用於ImageNet資料集的複雜模型在這個椅⼦資料集上過擬合。同時,因為資料量有限,最終訓練得到的模型的精度也可能達不到實用的要求。
為了應對上述問題,一個顯⽽易⻅的解決辦法是收集更多的資料。然而,收集和標註資料會花費大量的時間和資⾦。例如,為了收集ImageNet資料集,研究人員花費了數百萬美元的研究經費。雖然目前的資料採集成本已降低了不少,但其成本仍然不可忽略。
另外一種解決辦法是應用遷移學習(transfer learning),將從源資料集學到的知識遷移到目標資料集上。例如,雖然ImageNet資料集的影象大多跟椅子無關,但在該資料集上訓練的模型可以抽取較通用的影象特徵,從而能夠幫助識別邊緣、紋理、形狀和物體組成等。這些類似的特徵對於識別椅子也可能同樣有效。
遷移學習的一大應用場景是模型微調(finetune)。簡單來說,就是我們先找到一個同類的別人訓練好的模型,把別人現成的訓練好了的模型拿過來,換成自己的資料,通過訓練調整一下引數。 在PyTorch中提供了許多預訓練好的網路模型(VGG,ResNet系列,mobilenet系列......),這些模型都是PyTorch官方在相應的大型資料集訓練好的。學習如何進行模型微調,可以方便我們快速使用預訓練模型完成自己的任務。
6.3.1 模型微調的流程
- 在源資料集(如ImageNet資料集)上預訓練一個神經網路模型,即源模型。
- 建立一個新的神經網路模型,即目標模型。它複製了源模型上除了輸出層外的所有模型設計及其引數。我們假設這些模型引數包含了源資料集上學習到的知識,且這些知識同樣適用於目標資料集。我們還假設源模型的輸出層跟源資料集的標籤緊密相關,因此在目標模型中不予採用。
- 為目標模型新增一個輸出⼤小為⽬標資料集類別個數的輸出層,並隨機初始化該層的模型引數。
- 在目標資料集上訓練目標模型。我們將從頭訓練輸出層,而其餘層的引數都是基於源模型的引數微調得到的。
6.3.2 使用已有模型結構
這裡我們以torchvision中的常見模型為例,列出瞭如何在影象分類任務中使用PyTorch提供的常見模型結構和引數。對於其他任務和網路結構,使用方式是類似的:
- 例項化網路
``python
import torchvision.models as models
resnet18 = models.resnet18()
# resnet18 = models.resnet18(pretrained=False) 等價於與上面的表示式
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`引數
通過True
或者False
來決定是否使用預訓練好的權重,在預設狀態下pretrained = False
,意味著我們不使用預訓練得到的權重,當pretrained = True
,意味著我們將使用在一些資料集上預訓練得到的權重。
python
import torchvision.models as models
resnet18 = models.resnet18(pretrained=True)
alexnet = models.alexnet(pretrained=True)
squeezenet = models.squeezenet1_0(pretrained=True)
vgg16 = models.vgg16(pretrained=True)
densenet = models.densenet161(pretrained=True)
inception = models.inception_v3(pretrained=True)
googlenet = models.googlenet(pretrained=True)
shufflenet = models.shufflenet_v2_x1_0(pretrained=True)
mobilenet_v2 = models.mobilenet_v2(pretrained=True)
mobilenet_v3_large = models.mobilenet_v3_large(pretrained=True)
mobilenet_v3_small = models.mobilenet_v3_small(pretrained=True)
resnext50_32x4d = models.resnext50_32x4d(pretrained=True)
wide_resnet50_2 = models.wide_resnet50_2(pretrained=True)
mnasnet = models.mnasnet1_0(pretrained=True)
注意事項:
-
通常PyTorch模型的擴充套件為
.pt
或.pth
,程式執行時會首先檢查預設路徑中是否有已經下載的模型權重,一旦權重被下載,下次載入就不需要下載了。 -
一般情況下預訓練模型的下載會比較慢,我們可以直接通過迅雷或者其他方式去 這裡 檢視自己的模型裡面
model_urls
,然後手動下載,預訓練模型的權重在Linux
和Mac
的預設下載路徑是使用者根目錄下的.cache
資料夾。在Windows
下就是C:\Users\<username>\.cache\torch\hub\checkpoint
。我們可以通過使用torch.utils.model_zoo.load_url()
設定權重的下載地址。 -
如果覺得麻煩,還可以將自己的權重下載下來放到同文件夾下,然後再將引數載入網路。
python
self.model = models.resnet50(pretrained=False)
self.model.load_state_dict(torch.load('./model/resnet50-19c8e357.pth'))
- 如果中途強行停止下載的話,一定要去對應路徑下將權重檔案刪除乾淨,要不然可能會報錯。
6.3.3 訓練特定層
在預設情況下,引數的屬性.requires_grad = True
,如果我們從頭開始訓練或微調不需要注意這裡。但如果我們正在提取特徵並且只想為新初始化的層計算梯度,其他引數不進行改變。那我們就需要通過設定requires_grad = False
來凍結部分層。在PyTorch官方中提供了這樣一個例程。
python
def set_parameter_requires_grad(model, feature_extracting):
if feature_extracting:
for param in model.parameters():
param.requires_grad = False
在下面我們仍舊使用resnet18
為例的將1000類改為4類,但是僅改變最後一層的模型引數,不改變特徵提取的模型引數;注意我們先凍結模型引數的梯度,再對模型輸出部分的全連線層進行修改,這樣修改後的全連線層的引數就是可計算梯度的。
```python import torchvision.models as models
凍結引數的梯度
feature_extract = True model = models.resnet18(pretrained=True) set_parameter_requires_grad(model, feature_extract)
修改模型
num_ftrs = model.fc.in_features model.fc = nn.Linear(in_features=512, out_features=4, bias=True) ```
之後在訓練過程中,model仍會進行梯度回傳,但是引數更新則只會發生在fc層。通過設定引數的requires_grad屬性,我們完成了指定訓練模型的特定層的目標,這對實現模型微調非常重要。
本節參考
【1】引數更新
【2】給不同層分配不同的學習率
6.4 半精度訓練
我們提到PyTorch時候,總會想到要用硬體裝置GPU的支援,也就是“卡”。GPU的效能主要分為兩部分:算力和視訊記憶體,前者決定了顯示卡計算的速度,後者則決定了顯示卡可以同時放入多少資料用於計算。在可以使用的視訊記憶體數量一定的情況下,每次訓練能夠載入的資料更多(也就是batch size更大),則也可以提高訓練效率。另外,有時候資料本身也比較大(比如3D影象、視訊等),視訊記憶體較小的情況下可能甚至batch size為1的情況都無法實現。因此,合理使用視訊記憶體也就顯得十分重要。
我們觀察PyTorch預設的浮點數儲存方式用的是torch.float32,小數點後位數更多固然能保證資料的精確性,但絕大多數場景其實並不需要這麼精確,只保留一半的資訊也不會影響結果,也就是使用torch.float16格式。由於數位減了一半,因此被稱為“半精度”,具體如下圖:
顯然半精度能夠減少視訊記憶體佔用,使得顯示卡可以同時載入更多資料進行計算。本節會介紹如何在PyTorch中設定使用半精度計算。
經過本節的學習,你將收穫:
- 如何在PyTorch中設定半精度訓練
- 使用半精度訓練的注意事項
6.4.1 半精度訓練的設定
在PyTorch中使用autocast配置半精度訓練,同時需要在下面三處加以設定:
- import autocast
python
from torch.cuda.amp import autocast
- 模型設定
在模型定義中,使用python的裝飾器方法,用autocast裝飾模型中的forward函式。關於裝飾器的使用,可以參考這裡:
python
@autocast()
def forward(self, x):
...
return x
- 訓練過程
在訓練過程中,只需在將資料輸入模型及其之後的部分放入“with autocast():“即可:
python
for x in train_loader:
x = x.cuda()
with autocast():
output = model(x)
...
注意:
半精度訓練主要適用於資料本身的size比較大(比如說3D影象、視訊等)。當資料本身的size並不大時(比如手寫數字MNIST資料集的圖片尺寸只有28*28),使用半精度訓練則可能不會帶來顯著的提升。