(七)詳解pytorch中的交叉熵損失函數nn.BCELoss()、nn.BCELossWithLogits(),二分類任務如何定義損失函數,如何計算准確率、如何預測


最近在做交叉熵的魔改,所以需要好好了解下交叉熵,遂有此文。

關於交叉熵的定義請自行百度,相信點進來的你對其基本概念不陌生。

本文將結合PyTorch,介紹離散形式的交叉熵在二分類以及多分類中的應用。注意,本文出現的二分類交叉熵和多分類交叉熵,本質上都是一個東西,二分類交叉熵可以看作是多分類交叉熵的一個特例,只不過在PyTorch中對應方法的實現方式不同(不同之處將在正文詳細講解)。

好了,廢話少敘,正文開始~

https://zhuanlan.zhihu.com/p/369699003

一、二分類交叉熵

[公式]

其中,[公式]是總樣本數,[公式]是第[公式]個樣本的所屬類別,[公式]是第[公式]個樣本的預測值,一般來說,它是一個概率值。

上栗子:

按照上面的公式,交叉熵計算如下:

[公式]

其實,在PyTorch中已經內置了BCELoss,它的主要用途是計算二分類問題的交叉熵,我們可以調用該方法,並將結果與上面手動計算的結果做個比較:

 

嗯,結果是一致的。

需要注意的是,輸入BCELoss中的預測值應該是個概率[公式]

上面的栗子直接給出了預測的[公式],這是符合要求的。但在更一般的二分類問題中,網絡的輸出取值是整個實數域(可正可負可為0)。

為了由這種輸出值得到對應的[公式],你可以在網絡的輸出層之后新加一個Sigmoid層,這樣便可以將輸出值的取值規范到0和1之間,這就是交叉熵公式中的[公式]

當然,你也可以不更改網絡輸出,而是在將輸出值送入交叉熵公式進行性計算之前,手動用Simgmoid函數做一個映射。

在PyTorch中,甚至提供了BCEWithLogitsLoss方法,它可以直接將輸入的值規范到0和1 之間,相當於將SigmoidBCELoss集成在了一個方法中。

還是舉個栗子來具體進行說明:假設pred是shape為[4,2]的tensor,其中4代表樣本個數,2代表該樣本分別屬於兩個類別的概率(前提是規范到了0和1之間,否則就是兩個實數域上的值,記住,現在我們討論的是二分類);target是shape為[4]的tensor,4即樣本數。

pred=torch.randn(4,2)#預測值
target=torch.rand(4).random_(0,2)#真實類別標簽

在使用任何一種方法之前,都需要先對target做獨熱編碼,否則target和pred維度不匹配:

#將target進行獨熱編碼
onehot_target=torch.eye(2)[target.long(), :]

在做編碼前,target看起來長這樣:

tensor([0., 1., 1., 1.])

編碼后,target變成了這樣:

tensor([[1., 0.],
        [0., 1.],
        [0., 1.],
        [0., 1.]])

現在,target的shape也是[4,2]了,和pred的shape一樣,所以下面可以開始計算交叉熵了。

  • 使用SigmoidBCELoss計算交叉熵
    先使用nn.Sigmoid做一下映射:


可以看到,映射后的取值已經被規范到了0和1之間。
然后使用BCELoss進行計算:

 

  • 只使用BCELossWithLogits計算交叉熵

 

兩種方法的計算結果完全一致。不過官方建議使用BCELossWithLogits,理由是能夠提升數值計算穩定性。

以后,當你使用PyTorch內置的二分類交叉熵損失函數時,只要保證輸入的預測值和真實標簽的維度一致(N,...),且輸入的預測值是一個概率即可。滿足這兩點,一般就能避免常見的錯誤了。

 

(BCELoss的使用)

關於二分類交叉熵的介紹就到這里,接下來介紹多分類交叉熵。

二、多分類交叉熵

[公式]

其中,N代表樣本數,K代表類別數,[公式]代表第i個樣本屬於類別c的概率,[公式][公式],可以看作一個one-hot編碼(若第i個樣本屬於類別c,則對應位置的[公式]取1,否則取0)。

這個公式乍看上去有點復雜,其實不難。不妨取第[公式]個樣本,計算這個樣本的交叉熵,公式如下:

[公式]

假設N=2, K=3,即總共3個樣本,3個類別,樣本的數據如下

|. | [公式][公式] |[公式]|[公式]|[公式]|[公式]| | :--------: | :--------:| :------: |:------:| | 第1個樣本 | 0| 1 |0|0.2|0.3|0.5| | 第2個樣本 | 1| 0 |0|0.3|0.2|0.5| | 第3個樣本 | 0| 0 |1|0.4|0.4|0.2|

[公式][公式][公式]

看吧,最終的交叉熵只不過是做了N這樣的計算,然后平均一下,加個負號:

[公式]

你可能已經發現,這里的[公式]之和為1。沒錯,這是網絡的輸出做了softmax后得到的結果。在上一部分關於二分類的問題中,輸入交叉熵公式的網絡預測值必須經過Sigmoid進行映射,而在這里的多分類問題中,需要將Sigmoid替換成Softmax,這是兩者的一個重要區別!

現在讓我們用代碼來實現上面的計算過程:

#預測值,假設已做softmax
pred=torch.tensor([[0.2,0.3,0.5],[0.3,0.2,0.5],[0.4,0.4,0.2]])
#真實類別標簽
target=torch.tensor([1,0,2])
# 對真實類別標簽做 獨熱編碼
one_hot = F.one_hot(target).float()
"""
one_hot:
tensor([[0., 1., 0.],
        [1., 0., 0.],
        [0., 0., 1.]])
"""
#對預測值取log
log=torch.log(pred)
#計算最終的結果
res=-torch.sum(one_hot*log)/target.shape[0]
print(res)# tensor(1.3391)

這和我們之前手動計算的結果是一樣的。代碼很簡單,只需注意代碼中的one_hot*log是逐元素做乘法。

以上是其內部實現原理。在實際使用時,為了方便,PyTorch已經封裝好了以上過程,你只需要調用一下相應的方法或函數即可。

在PyTorch中,有一個叫做nll_loss的函數,可以幫助我們更快的實現上述計算,此時無需對target進行獨熱編碼,於是代碼可簡化如下:

import torch.nn.functional as F
#預測值,已做softmax
pred=torch.tensor([[0.2,0.3,0.5],[0.3,0.2,0.5],[0.4,0.4,0.2]])
#真實類別標簽,此時無需再做one_hot,因為nll_loss會自動做
target=torch.tensor([1,0,2])
#對預測值取log
log=torch.log(pred)
#計算最終的結果
res=F.nll_loss(log, target)
print(res)# tensor(1.3391)

等等,還沒完。在PyTorch中,最常用於多分類問題的,是CrossEntropyLoss.

它可以看作是softmax+log+nll_loss的集成。

上面的栗子中的預測值是已經做完softmax之后的,為了說明CrossEntropyLoss的原理,我們換一個預測值沒有做過softmax的新栗子,這種栗子也是我們通常會遇到的情況:

#4個樣本,3分類
pred=torch.rand(4,3)
#真實類別標簽
target=torch.tensor([0,1,0,2])
先按照softmax+log+nll_loss的步驟走一遍:

logsoftmax=F.log_softmax(pred)
"""
logsoftmax:

tensor([[-0.8766, -1.4375, -1.0605],
        [-1.0188, -0.9754, -1.3397],
        [-0.8926, -1.0962, -1.3615],
        [-1.0364, -0.8817, -1.4645]])
"""
res=F.nll_loss(logsoftmax,target)
pritnt(res)#tensor(1.0523)
直接使用CrossEntropyLoss:

res=F.cross_entropy(pred, target)
print(res)#tensor(1.0523)

結果是一樣的。

(CrossEntropyLoss的使用)

三、總結

1、對於二分類任務,網絡輸出和標簽維度:

import torch
import torch.nn as nn
loss = nn.BCELoss()
pre = torch.tensor([0.8, 0.2, 0.6, 0.1])
label = torch.tensor([1., 0., 1., 1.])
print(loss(pre, label))

pre = torch.tensor([[0.2, 0.8], [0.8, 0.2], [0.4, 0.6], [0.9, 0.1]])
label = torch.tensor([[0., 1.], [1., 0.], [0., 1.], [0., 1.]])
print(loss(pre, label))

pre = torch.tensor([[0.8], [0.2], [0.6], [0.1]])
label = torch.tensor([[1.], [0.], [1.], [1.]])
print(loss(pre, label))

輸出為:

D:\Users\zxr20\Anaconda3\envs\pt\python.exe F:/semantics/wrapper/test.py
tensor(0.8149)
tensor(0.8149)
tensor(0.8149)

Process finished with exit code 0

也就是說,網絡輸出的維度是一維或者二維都可以, label不用one-hot編碼也可以。

前提是,網絡輸出必須是經過torch.sigmoid函數映射成[0,1]之間的小數。

如前面,一個batch是4, 也就是4個樣本。

如果使用第一種方式,計算准確率時候,要這樣:

一、第一種方式:

(1)網絡輸出時候,就要將數值進行sigmoid

(2)損失函數loss = nn.BCELoss()

(3)計算准確率時候如下:

    def binary_acc(self, preds, y):
        preds = torch.round(preds)
        correct = torch.eq(preds, y).float()
        acc = correct.sum() / len(correct)
        return acc

(4)預測時候:

    preds = torch.round(preds)

(5)送入critiation

loss = criterion(distence, label.float())

二、第二種方式:

(1)網絡輸出時候,不用sigmoid

    def forward(self, data1, data2):
        out1, (h1, c1) = self.lstm(data1)
        out2, (h2, c2) = self.lstm(data2)
        pre1 = out1[:, -1, :]
        pre2 = out2[:, -1, :]
        pre = torch.cat([pre1, pre2], dim=1)
        out = self.fc(pre)
        return out

(2)損失函數

        self.criterion = nn.BCEWithLogitsLoss().to(self.device)

(3)計算准確率時候如下:

    def binary_acc(self, preds, y):
        preds = torch.round(torch.sigmoid(preds))
        correct = torch.eq(preds, y).float()
        acc = correct.sum() / len(correct)
        return acc

(4)預測時候

preds = torch.round(torch.sigmoid(preds))

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM