深度學習之PyTorch實戰(5)——對CrossEntropyLoss損失函式的理解與學習

戰爭熱誠發表於2023-04-09

   其實這個筆記起源於一個報錯,報錯內容也很簡單,希望傳入一個三維的tensor,但是得到了一個四維。

RuntimeError: only batches of spatial targets supported (3D tensors) but got targets of dimension: 4

  檢視程式碼報錯點,是出現在pytorch計算交叉熵損失的程式碼。其實在自己手寫寫語義分割的程式碼之前,我一直以為自己是對交叉熵損失完全瞭解的。但是實際上還是有一些些認識不足,所以這裡打算複習一下,將其重新梳理一下,才算是透徹的理解了,特地記錄下來。

1,交叉熵損失的定義理解

  交叉熵是資訊理論中的一個概念,要想完全理解交叉熵的本質,需要從基礎的概念學習。

1.1 資訊量

  資訊量與事件發生的機率有關,某件事情越不可能發生,我們獲取的資訊量就越大,越可能發生,我們獲取的資訊量就越小。

  假設X是一個離散型隨機變數,其取值集合為x,機率分佈函式 p(x)=Pr(X=x),則定義事件 X=x0的資訊量為:

   由於是機率,所以P(x0)的取值範圍是[0, 1],繪圖如下:

 

   從影像可知,函式符合我們對資訊量的直覺,機率越大,資訊量越小。

1.2 熵

  對於某個事件來說,有多種可能性,每一種可能性都有一個機率 p(Xi),這樣就可能計算出某一種可能性的資訊量。因為我們上面定義了資訊量的定義,而熵就是表達所有資訊量的期望,即:

   而有一類比較特殊的分佈問題,就是0-1分佈,對於這類問題你,熵的計算方式可以簡化為圖下算式:

 

 1.3 相對熵(KL散度)

  如果我們對同一個隨機變數X有兩個單獨的機率分佈 P(x) 和 Q(x)(在機器學習中,P往往是用來表示樣本的真實分佈,而Q是表示模型預測的分佈。),我們可以使用KL散度來衡量這兩個分佈的差異。計算公式如下:

   n 為事件的所有可能性,Dkl的值越小,表示P和Q的分佈越接近。

1.4 交叉熵

  對上式變形可以得到:

   等式的前一部分是p的熵,後一部分是交叉熵:

  在機器學習中,我們需要評估label和predict之間的差距,使用KL散度剛剛好。由於KL散度的前一部分 -H(y)不變,故在最佳化過程中,只需要關注交叉熵就可以了,所以一般在機器學習中直接用交叉熵做loss,評估模型。因為交叉熵刻畫的是兩個機率分佈的距離,也就是說交叉熵值越小(相對熵的值越小),兩個機率分佈越接近。

1.5 交叉熵在單標籤分類問題的使用

  這裡的單標籤分類,就是深度學習最基本的分類問題,每個影像只有一個標籤,只能是label1或者label2。

   上圖是一個樣本loss的計算方式,n代表n種label,yi表示真實結果, yihat表示預測機率。如果是一個batch,則需要除以m(m為當前batch的樣本數)。

1.6 交叉熵在多標籤分類問題的使用

  這裡的多標籤分類是指,每一張影像樣本可以有多個類別,多分類標籤是n-hot,值得注意的是,這裡的pred不再用softmax計算了,採用的是Sigmoid了。將每一個節點的輸出歸一化到0-1之間。所以pred的值的和也不再是1。比如我們的語義分割,每個畫素的label都是獨立分佈的,相互之間沒有任何影響,所以交叉熵在這裡是單獨對每一個節點進行計算,每一個節點只有兩種可能性,所以是一個二項分佈。(上面有簡化後交叉熵的公式)

  每個樣本的loss即為 loss = loss1 + loss2 + ... lossn。

  每一個batch的loss就是:

   其中m為當前batch的樣本量,n為類別數。

2,Pytorch中CrossEntropy的形式

  語義分割的本質是對畫素的分類。因此語義分割也是使用這個損失函式。首先看程式碼定義:

def cross_entropy(input, target, weight=None, size_average=None, ignore_index=-100,
                  reduce=None, reduction='mean'):
    # type: (Tensor, Tensor, Optional[Tensor], Optional[bool], int, Optional[bool], str) -> Tensor

    if size_average is not None or reduce is not None:
        reduction = _Reduction.legacy_get_string(size_average, reduce)
    return nll_loss(log_softmax(input, 1), target, weight, None, ignore_index, None, reduction)

  從上面程式碼可知:input和target是Tensor格式,並且先計算log_softmax,再計算nll_loss。(實際上softmax計算+ log計算 + nll_loss 計算== 直接使用CrossEntropyLoss計算)

2.1 透過softmax+log+nll_loss 計算CrossEntropyLoss

  我們直接在語義分割中應用:

  下面softmax函式肯定輸出的是網路的輸出預測影像,假設維度為(1,2,2,2),從左到右dim依次為0,1,2,3,也就是說類別數所在的維度表示dim=1應在的維度上計算機率。所以dim=1

temp1 = F.softmax(pred_output,dim=1)
print("temp1:",temp1)

  

 

 log函式:就是對輸入矩陣的每個元素求對數,預設底數為e,也就是In函式

temp3 = torch.log(temp1)
print("temp3:",temp3)

 

nll_loss函式:這個函式的目的是把標籤影像的元素值,作為索引值,在上面選擇相應的值求平均。

target = target.long()
loss1 = F.nll_loss(temp3,target)
print('loss1: ', loss1)

  

 2.2   直接使用交叉熵損失計算

   直接使用交叉熵損失計算:

loss2 = nn.CrossEntropyLoss()
result2 = loss2(pred_output, target)
print('result2: ', result2)

  對比結果可以發現  透過  對CrossEntropyLoss函式分解並分步計算的結果,與直接使用CrossEntropyLoss函式計算的結果一致。

2.3  pytorch 和 tensorflow在損失函式計算方面的差異

  pytorch和tensorflow在損失函式計算方面有細微的差別的,為啥對比pytorch和tensorflow的差異,因為一個更符合人的想法,一個稍微有一些閹割的問題,導致我們按照常理寫程式碼,會遇到問題。

tensorflow的模型訓練:

 

   one-hot編碼:

 

   透過這兩步驟,我們就可以計算標籤和模型產生的預測結果之間的損失了。而在pytorch中,我們不需要對標籤進行one-hot編碼,且需要將通道這一維度壓縮。即標籤中的值為對應的類別數

  具體在程式碼中,如果是一個類別,就特別要注意(因為我就是沒注意,所以就有開頭的錯):

masks_pred = model(images)
if model.n_classes == 1:
    loss = criterion(masks_pred.squeeze(1), true_masks.float())
    loss += dice_loss(F.sigmoid(masks_pred.squeeze(1)), true_masks.float(), multiclass=False)
else:
    loss = criterion(masks_pred, true_masks)
    loss += dice_loss(
        F.softmax(masks_pred, dim=1).float(),
        F.one_hot(true_masks, model.n_classes).permute(0, 3, 1, 2).float(),
        multiclass=True
    )

  

3,Pytorch中,nn與nn.functional的相同點和不同點

3.1 相同點

  首先兩者的功能相同,nn.xx與nn.functional.xx的實際功能是相同的,只是一個是包裝好的類,一個是可以直接呼叫的函式。

  比如我們這裡學習的Crossentropy函式:

  在torch.nn中定義如下:

class CrossEntropyLoss(_WeightedLoss):
    __constants__ = ['ignore_index', 'reduction', 'label_smoothing']
    ignore_index: int
    label_smoothing: float

    def __init__(self, weight: Optional[Tensor] = None, size_average=None, ignore_index: int = -100,
                 reduce=None, reduction: str = 'mean', label_smoothing: float = 0.0) -> None:
        super(CrossEntropyLoss, self).__init__(weight, size_average, reduce, reduction)
        self.ignore_index = ignore_index
        self.label_smoothing = label_smoothing

    def forward(self, input: Tensor, target: Tensor) -> Tensor:
        return F.cross_entropy(input, target, weight=self.weight,
                               ignore_index=self.ignore_index, reduction=self.reduction,
                               label_smoothing=self.label_smoothing)

  在torch.nn.functional中定義如下:

def cross_entropy(
    input: Tensor,
    target: Tensor,
    weight: Optional[Tensor] = None,
    size_average: Optional[bool] = None,
    ignore_index: int = -100,
    reduce: Optional[bool] = None,
    reduction: str = "mean",
    label_smoothing: float = 0.0,
) -> Tensor:
    if has_torch_function_variadic(input, target, weight):
        return handle_torch_function(
            cross_entropy,
            (input, target, weight),
            input,
            target,
            weight=weight,
            size_average=size_average,
            ignore_index=ignore_index,
            reduce=reduce,
            reduction=reduction,
            label_smoothing=label_smoothing,
        )
    if size_average is not None or reduce is not None:
        reduction = _Reduction.legacy_get_string(size_average, reduce)
    return torch._C._nn.cross_entropy_loss(input, target, weight, _Reduction.get_enum(reduction), ignore_index, label_smoothing)

  可以看到torch.nn下面的CrossEntropyLoss類在forward時呼叫了nn.functional下的cross_entropy函式,當然最終的計算是透過C++編寫的函式計算的。

 3.2  不同點

  不同點1:在使用nn.CrossEntropyLoss()之前,需要先例項化,再輸入引數,以函式呼叫的方式呼叫例項化的物件並傳入輸入資料:

import torch.nn as nn

loss = torch.nn.CrossEntropyLoss()
output = loss(x, y)

  使用 F.cross_entropy()直接可以傳入引數和輸入資料,而且由於F.cross_entropy() 得到的是一個向量也就是對batch中每一個影像都會得到對應的交叉熵,所以計算出之後,會使用一個mean()函式,計算其總的交叉熵,再對其進行最佳化。

import torch.nn.functional as F

loss = F.cross_entropy(input, target).mean()

  不同點2:而且 nn.xxx 繼承於nn.Module,能夠很好的與nn.Sequential結合使用,而nn.functional.xxx 無法與nn.Sequential結合使用。舉個例子:

layer = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(num_features=64),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),
            nn.Dropout(0.2)
  )

  不同點3:nn.xxx 不需要自己定義和管理weight;而nn.functional.xxx需要自己定義weight,每次呼叫的時候都需要手動傳入weight,不利於程式碼複用。其實如果我們只保留了nn.functional下的函式的話,在訓練或者使用時,我們就需要手動去維護weight, bias, stride 這些中間量的值;而如果只保留nn下的類的話,其實就犧牲了一部分靈活性,因為做一些簡單的計算都需要建立一個類,這也與PyTorch的風格不符。

  比如使用nn.xxx定義一個網路,如下:

import torch
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

  以一個最簡單的五層網路為例。需要維持狀態的,主要是兩個卷積和三個線性變換,所以在構造Module是,定義了兩個Conv2d和三個nn.Linear物件,而在計算時,relu之類不需要儲存狀態的可以直接使用。

 

參考地址(這個只是個人筆記,不做商業):

https://blog.csdn.net/tsyccnh/article/details/79163834

https://www.zhihu.com/question/66782101

https://blog.csdn.net/weixin_39190382/article/details/114433884)

https://blog.csdn.net/Fcc_bd_stars/article/details/105158215

相關文章