最近在做交叉熵的魔改,所以需要好好了解下交叉熵,遂有此文。
關於交叉熵的定義請自行百度,相信點進來的你對其基本概念不陌生。
本文將結合PyTorch,介紹離散形式的交叉熵在二分類以及多分類中的應用。注意,本文出現的二分類交叉熵和多分類交叉熵,本質上都是一個東西,二分類交叉熵可以看作是多分類交叉熵的一個特例,只不過在PyTorch中對應方法的實現方式不同(不同之處將在正文詳細講解)。
好了,廢話少敘,正文開始~
https://zhuanlan.zhihu.com/p/369699003
一、二分類交叉熵
其中,是總樣本數,
是第
個樣本的所屬類別,
是第
個樣本的預測值,一般來說,它是一個概率值。
上栗子:
按照上面的公式,交叉熵計算如下:
其實,在PyTorch中已經內置了BCELoss
,它的主要用途是計算二分類問題的交叉熵,我們可以調用該方法,並將結果與上面手動計算的結果做個比較:
嗯,結果是一致的。
需要注意的是,輸入BCELoss
中的預測值應該是個概率。
上面的栗子直接給出了預測的,這是符合要求的。但在更一般的二分類問題中,網絡的輸出取值是整個實數域(可正可負可為0)。
為了由這種輸出值得到對應的,你可以在網絡的輸出層之后新加一個
Sigmoid
層,這樣便可以將輸出值的取值規范到0和1之間,這就是交叉熵公式中的。
當然,你也可以不更改網絡輸出,而是在將輸出值送入交叉熵公式進行性計算之前,手動用Simgmoid
函數做一個映射。
在PyTorch中,甚至提供了BCEWithLogitsLoss
方法,它可以直接將輸入的值規范到0和1 之間,相當於將Sigmoid
和BCELoss
集成在了一個方法中。
還是舉個栗子來具體進行說明:假設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一樣,所以下面可以開始計算交叉熵了。
- 使用
Sigmoid
和BCELoss
計算交叉熵
先使用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))