前面學習了如何構建模型、模型初始化,本章學習損失函數。本章從3個方面學習,(1)損失函數的概念以及作用;(2)學習交叉熵損失函數;(3)學習其他損失函數NLL、BCE、BCEWithLogits Loss
損失函數概念
損失函數:衡量模型輸出與真實標簽的差異。

如圖1所示,一元線性回歸模型中綠色點是訓練樣本,藍色曲線是訓練好的模型,從圖中可以看出,該模型並未完全擬合到每1個數據點,即未能使得所有數據點都在模型上,因此,數據點產生了Loss。如第5個數據點,模型輸出與真實輸出產生了差異,這里采用了絕對值損失函數\(\vert \hat{y}-y\vert\) 來衡量模型輸出與真實標簽之前的差異。
通常,說到損失函數會出現3個概念:
(1)損失函數(Loss Function):計算單樣本的差異
(2)代價函數(Cost Function):計算整個訓練集、樣本集(所有樣本)Loss的平均值
(3)目標函數(Objective Function):訓練模型的最終目標—目標包含cost和Regularization
①Cost代價函數衡量模型輸出與真實標簽之間的差異,即要求模型輸出與真實標簽之間的差異要盡量小。那么,是不是模型輸出與真實標簽之間的差異越小越好呢? 其實,並不一定,因為有時候可能會造成模型過擬合。

如圖2,模型完全擬合了每個數據點,代價函數Cost為0,但卻不是1個好的模型,模型太復雜而造成過擬合。
②正則項Regularization Term:在追求模型輸出與真實標簽之間差異較小時,同時也要對模型做出限制、約束(抑制過擬合),這些約束項就稱之為正則項。通常采用L1、L2加在Cost項之后,構成目標函數。
后文,不失一般性,用損失函數Loss Function代替代價函數Cost Function。衡量模型輸出與真實標簽之間差異統稱為Loss Function。
交叉熵損失函數
Pytorch中交叉熵損失函數的實現
1、nn.CrossEntropyLoss
nn.CrossEntropyLoss
功能:nn.LogSoftmax()與nn.NLLLoss()結合,進行交叉熵計算
將nn模塊中的Logsoftmax激活函數與NLL結合,這一函數並不是公式意義上的交叉熵函數計算,不同之處在於,它采用了softmax對數據進行了歸一化:將數據值歸一化到概率輸出的區間。這是因為交叉熵函數常用於分類任務中,分類任務通常需要計算兩個輸出的概率值,交叉熵衡量兩個概率分布之間的差異。交叉熵值越低,兩個分布越相似。
1.1 分析
①交叉熵

觀察上述公式,\(P\)是訓練集中樣本真實概率分布, \(Q\)是模型輸出分布,機器學習模型中優化、最小化交叉熵等價於優化相對熵。由於訓練集是固定,P的信息熵 \(H(P)\)是常數,在優化時常數可忽略,因此,優化交叉熵則等價於優化相對熵。
②Logsoftmax函數
softmax可以將輸出數據值可以歸一到概率區間,再根據Log、NLLLoss計算交叉熵。

在沒有weight權值時,接收輸出概率值x、class類別值,然后對其取指數exp即softmax,在取-log即得到交叉熵損失函數(這里計算時取了某1個樣本,故\(P(x_i)\)=1)
nn.CrossEntropyLoss主要參數
nn.CrossEntropyLoss(weight=None,
size_average=None,
ignore_index=-100,
reduce=None,
reduction='mean')
功能:nn.LogSoftmax()與nn.NLLLoss()結合,進行交叉熵計算
主要參數:
weight:各個類別的Loss設置權值
ignore_index:忽略某個類別
reduction:計算模式可為none/sum/mean
none-逐個元素計算
sum-所有元素求和,返回標量
mean-加權平均,返回標量
解釋
(1)weight:為各個類別的Loss設置權值
比如,想讓第0類的Loss更大一些,模型更關注第0類,可以設置該類別的權值weight=1.2,可以使得Loss被放大1.2倍。
(2)ignore_index:用來指示某個類別不計算Loss。例如1000類分類任務中,不想計算第999類的Loss,即可設置ignore_index=999;
(3)reduction:用來計算Loss的模型,具體有3類none/sum/mean。 none逐個樣本計算Loss,有多少個獨立樣本,就返回多少個Loss;sum、mean均返回標量。
(4)現在函數中仍存在的size_average和reduce在早期是用來設定計算模式的,現已通過reduction來設置。
1.2 實驗
1.2.1 通過實驗認識CrossEntropyLoss交叉熵的不同計算模式reduction
3個樣本分別是輸入輸出神經元,即第1個樣本輸入為1,輸出值為3,第2個樣本輸入為1,輸出為3,第3個樣本輸入為1,輸出為3。
//構建虛擬數據 batch_size=3即3個樣本
inputs = torch.tensor([[1, 2], [1, 3], [1, 3]], dtype=torch.float)
//設置標簽、類型為長整型,第1個樣本為第0類,第2、3個樣本為第1類,注意類別數class是從0算起的
target = torch.tensor([0, 1, 1], dtype=torch.long)
//通過nn.CrossEntropyLoss構造損失函數,觀察3種reduction模式下Loss計算
# ----------------------------------- CrossEntropy loss: reduction -----------------------------------
# flag = 0
flag = 1
if flag:
# def loss function
loss_f_none = nn.CrossEntropyLoss(weight=None, reduction='none')//維度上不衰減,有3個樣本就有3個Loss
loss_f_sum = nn.CrossEntropyLoss(weight=None, reduction='sum')//求和模式:把所有樣本的Loss加起來
loss_f_mean = nn.CrossEntropyLoss(weight=None, reduction='mean')//默認模式-均分
# forward
loss_none = loss_f_none(inputs, target)
loss_sum = loss_f_sum(inputs, target)
loss_mean = loss_f_mean(inputs, target)
# view
print("Cross Entropy Loss:\n ", loss_none, loss_sum, loss_mean)
實驗結果

1.2.2 通過實驗認識CrossEntropyLoss交叉熵中weight參數的作用
# flag = 0
flag = 1
if flag:
# def loss function
//weight設置時需要注意是“向量形式”,有多少個類別就需要設置多長的向量,每個類別都需要設置weight,不想關注的類別可以設置為1.設置weight,不需要關注尺度,只需要關注各類別之間的比例。
//設置第0類權重為1,第1類權重為2
weights = torch.tensor([1, 2], dtype=torch.float)
# weights = torch.tensor([0.7, 0.3], dtype=torch.float)
loss_f_none_w = nn.CrossEntropyLoss(weight=weights, reduction='none')
loss_f_sum = nn.CrossEntropyLoss(weight=weights, reduction='sum')
loss_f_mean = nn.CrossEntropyLoss(weight=weights, reduction='mean')
# forward
loss_none_w = loss_f_none_w(inputs, target)
loss_sum = loss_f_sum(inputs, target)
loss_mean = loss_f_mean(inputs, target)
# view
print("\nweights: ", weights)
print(loss_none_w, loss_sum, loss_mean)
實驗結果

在帶有權值weight模式下求均值不是求樣本的個數,而是求樣本占的權值份數。
其他損失函數NLL、BCE、BCEWithLogits Loss
一、NLL實現負對數似然函數中負號功能
nn.NLLLoss
nn.NLLLoss
功能:實現負對數似然函數中負號功能
主要參數(與nn.CrossEntropyLoss相似):
weight:各個類別的Loss設置權限
ignore_index:忽略某個類別
reduction:計算模式可為none-逐個元素計算/sum-所有元素求和,返回標量/mean-加權平均,返回標量
實驗
# fake data
inputs = torch.tensor([[1, 2], [1, 3], [1, 3]], dtype=torch.float)
target = torch.tensor([0, 1, 1], dtype=torch.long)
flag = 1
if flag:
weights = torch.tensor([1, 1], dtype=torch.float)
loss_f_none_w = nn.NLLLoss(weight=weights, reduction='none')
loss_f_sum = nn.NLLLoss(weight=weights, reduction='sum')
loss_f_mean = nn.NLLLoss(weight=weights, reduction='mean')

二、二分類交叉熵損失函數BCE
它是交叉熵損失函數的特例,BCELoss是真正意義上的(和公式定義上一樣的)求取給定inputs和target標簽之間的交叉熵。

nn.BCELoss
nn.BCELoss
注意事項:輸入輸出取值在[0,1]
主要參數(與nn.CrossEntropyLoss相似):
weight:各個類別的Loss設置權限
ignore_index:忽略某個類別
reduction:計算模式可為none-逐個元素計算/sum-所有元素求和,返回標量/mean-加權平均,返回標量
解釋:由於交叉熵衡量兩個概率分布之間的差異,所以給BCELoss函數輸入值取值范圍應為[0,1],如果不在這個范圍,則會報錯。
實驗
flag = 1
if flag:
# 虛擬輸入:二分類所以有2個神經元,4個樣本,第1個樣本:第1個神經元輸出值為1,第2個神經元輸出值為2
inputs = torch.tensor([[1, 2], [2, 2], [3, 4], [4, 5]], dtype=torch.float)
# BCE與CrossEntropyLoss的標簽不同,是浮點類數據,每個神經元一一對應的計算Loss,而非一整個神經元向量計算Loss
target = torch.tensor([[1, 0], [1, 0], [0, 1], [0, 1]], dtype=torch.float)
target_bce = target
weights = torch.tensor([1, 1], dtype=torch.float)
loss_f_none_w = nn.BCELoss(weight=weights, reduction='none')
loss_f_sum = nn.BCELoss(weight=weights, reduction='sum')
loss_f_mean = nn.BCELoss(weight=weights, reduction='mean')
# forward
loss_none_w = loss_f_none_w(inputs, target_bce)
loss_sum = loss_f_sum(inputs, target_bce)
loss_mean = loss_f_mean(inputs, target_bce)
實驗結果

輸入值並沒有在[0,1]之間,由於BCE二分類交叉熵損失函數衡量的是兩個概率分布之間的差異,因此輸入應當在[0,1]之間。因此,需要將輸入值經過激活函數 sigmoid()進行壓縮,使其輸出值在(0,1)之間。

從圖中可以看出Loss是8個數值,1個樣本有2個神經元,4個樣本共計8個神經元,正好也計算了8個Loss。對於每個神經元一一對應計算Loss。總和為8個Loss求和,均分為總和除以8。
三、BCEWithLogits Loss
如果輸入不在[0,1]之間,BCELoss就會報錯,為此Pytorch專門提供了一種函數BCEWithLogits Loss結合sigmoid函數、BCE二分類交叉熵函數。因此,該損失函數網絡的最后一層不能再加sigmoid。由於有時候希望模型最后一層是sigmoid,但是求取Loss又需要輸入值在[0,1]區間,因此Pytorch提供了將sigmoid放入到損失函數中的BCEWithLogits Loss。
nn.BCEWithLogits Loss
注意事項:網絡最后不加sigmoid函數
主要參數(與nn.CrossEntropyLoss相似):
與其它不同:pos_weight:正樣本的權值
weight:各個類別的Loss設置權限
ignore_index:忽略某個類別
reduction:計算模式可為none-逐個元素計算/sum-所有元素求和,返回標量/mean-加權平均,返回標量
解釋:pos_weight正樣本的權值,該參數用來均衡正負樣本,對正樣本的Loss乘以該系數。比如,正樣本有100個,負樣本有300個,正負樣本比例1:3;若設置pos_weight=3,對正樣本的Loss乘以該系數3,這樣等價於正、負樣本均有300個,從而實現正負樣本的均衡性。
實驗
①BCEWithLogits Loss網絡最后不加sigmoid
if flag:
inputs = torch.tensor([[1, 2], [2, 2], [3, 4], [4, 5]], dtype=torch.float)
target = torch.tensor([[1, 0], [1, 0], [0, 1], [0, 1]], dtype=torch.float)
target_bce = target
weights = torch.tensor([1, 1], dtype=torch.float)
loss_f_none_w = nn.BCEWithLogitsLoss(weight=weights, reduction='none')
loss_f_sum = nn.BCEWithLogitsLoss(weight=weights, reduction='sum')
loss_f_mean = nn.BCEWithLogitsLoss(weight=weights, reduction='mean')
# forward
loss_none_w = loss_f_none_w(inputs, target_bce)
loss_sum = loss_f_sum(inputs, target_bce)
loss_mean = loss_f_mean(inputs, target_bce)

②BCEWithLogits Loss網絡最后加sigmoid
if flag:
inputs = torch.tensor([[1, 2], [2, 2], [3, 4], [4, 5]], dtype=torch.float)
target = torch.tensor([[1, 0], [1, 0], [0, 1], [0, 1]], dtype=torch.float)
target_bce = target
inputs = torch.sigmoid(inputs)
weights = torch.tensor([1, 1], dtype=torch.float)
loss_f_none_w = nn.BCEWithLogitsLoss(weight=weights, reduction='none')
loss_f_sum = nn.BCEWithLogitsLoss(weight=weights, reduction='sum')
loss_f_mean = nn.BCEWithLogitsLoss(weight=weights, reduction='mean')
# forward
loss_none_w = loss_f_none_w(inputs, target_bce)
loss_sum = loss_f_sum(inputs, target_bce)
loss_mean = loss_f_mean(inputs, target_bce)
BCEWithLogits Loss本就結合了sigmoid函數,如果在網絡后又再一次進行了sigmoid,那么求解得到Loss就是錯誤結果。

③BCEWithLogits Loss之pos_weight參數
# flag = 0
flag = 1
if flag:
inputs = torch.tensor([[1, 2], [2, 2], [3, 4], [4, 5]], dtype=torch.float)
target = torch.tensor([[1, 0], [1, 0], [0, 1], [0, 1]], dtype=torch.float)
target_bce = target
# itarget
# inputs = torch.sigmoid(inputs)
weights = torch.tensor([1], dtype=torch.float)
pos_w = torch.tensor([3], dtype=torch.float) # 3
loss_f_none_w = nn.BCEWithLogitsLoss(weight=weights, reduction='none', pos_weight=pos_w)
loss_f_sum = nn.BCEWithLogitsLoss(weight=weights, reduction='sum', pos_weight=pos_w)
loss_f_mean = nn.BCEWithLogitsLoss(weight=weights, reduction='mean', pos_weight=pos_w)
# forward
loss_none_w = loss_f_none_w(inputs, target_bce)
loss_sum = loss_f_sum(inputs, target_bce)
loss_mean = loss_f_mean(inputs, target_bce)
