一般來說,監督學習的目標函數由損失函數和正則化項組成。(Objective = Loss + Regularization)
Pytorch中的損失函數一般在訓練模型時候指定。
注意Pytorch中內置的損失函數的參數和tensorflow不同,是y_pred在前,y_true在後,而Tensorflow是y_true在前,y_pred在後。
對於回歸模型,通常使用的內置損失函數是均方損失函數nn.MSELoss 。
對於二分類模型,通常使用的是二元交叉熵損失函數nn.BCELoss (輸入已經是sigmoid激活函數之後的結果)或者 nn.BCEWithLogitsLoss (輸入尚未經過nn.Sigmoid激活函數) 。
對於多分類模型,一般推薦使用交叉熵損失函數 nn.CrossEntropyLoss。(y_true需要是一維的,是類別編碼。y_pred未經過nn.Softmax激活。)
此外,如果多分類的y_pred經過了nn.LogSoftmax激活,可以使用nn.NLLLoss損失函數(The negative log likelihood loss)。這種方法和直接使用nn.CrossEntropyLoss等價。
如果有需要,也可以自定義損失函數,自定義損失函數需要接收兩個張量y_pred,y_true作為輸入參數,並輸出一個標量作為損失函數值。
Pytorch中的正則化項一般通過自定義的方式和損失函數一起添加作為目標函數。
如果僅僅使用L2正則化,也可以利用優化器的weight_decay參數來實現相同的效果。
一,內置損失函數import numpy as np
import pandas as pd
import torch
from torch import nn
import torch.nn.functional as F
y_pred = torch.tensor([[10.0,0.0,-10.0],[8.0,8.0,8.0]])
y_true = torch.tensor([0,2])
# 直接調用交叉熵損失
ce = nn.CrossEntropyLoss()(y_pred,y_true)
print(ce)
# 等價於先計算nn.LogSoftmax激活,再調用NLLLoss
y_pred_logsoftmax = nn.LogSoftmax(dim = 1)(y_pred)
nll = nn.NLLLoss()(y_pred_logsoftmax,y_true)
print(nll)tensor(0.5493)
tensor(0.5493)內置的損失函數一般有類的實現和函數的實現兩種形式。
如:nn.BCE 和 F.binary_cross_entropy 都是二元交叉熵損失函數,前者是類的實現形式,後者是函數的實現形式。
實際上類的實現形式通常是調用函數的實現形式並用nn.Module封裝後得到的。
一般我們常用的是類的實現形式。它們封裝在torch.nn模塊下,並且類名以Loss結尾。
常用的一些內置損失函數說明如下。
nn.MSELoss(均方誤差損失,也叫做L2損失,用於回歸)
nn.L1Loss (L1損失,也叫做絕對值誤差損失,用於回歸)
nn.SmoothL1Loss (平滑L1損失,當輸入在-1到1之間時,平滑為L2損失,用於回歸)
nn.BCELoss (二元交叉熵,用於二分類,輸入已經過nn.Sigmoid激活,對不平衡數據集可以用weigths參數調整類別權重)
nn.BCEWithLogitsLoss (二元交叉熵,用於二分類,輸入未經過nn.Sigmoid激活)
nn.CrossEntropyLoss (交叉熵,用於多分類,要求label為稀疏編碼,輸入未經過nn.Softmax激活,對不平衡數據集可以用weigths參數調整類別權重)
nn.NLLLoss (負對數似然損失,用於多分類,要求label為稀疏編碼,輸入經過nn.LogSoftmax激活)
nn.CosineSimilarity(餘弦相似度,可用於多分類)
nn.AdaptiveLogSoftmaxWithLoss (一種適合非常多類別且類別分布很不均衡的損失函數,會自適應地將多個小類別合成一個cluster)
更多損失函數的介紹參考如下知乎文章:
《PyTorch的十八個損失函數》
https://zhuanlan.zhihu.com/p/61379965
二,自定義損失函數自定義損失函數接收兩個張量y_pred,y_true作為輸入參數,並輸出一個標量作為損失函數值。
也可以對nn.Module進行子類化,重寫forward方法實現損失的計算邏輯,從而得到損失函數的類的實現。
下面是一個Focal Loss的自定義實現示範。Focal Loss是一種對binary_crossentropy的改進損失函數形式。
它在樣本不均衡和存在較多易分類的樣本時相比binary_crossentropy具有明顯的優勢。
它有兩個可調參數,alpha參數和gamma參數。其中alpha參數主要用於衰減負樣本的權重,gamma參數主要用於衰減容易訓練樣本的權重。
從而讓模型更加聚焦在正樣本和困難樣本上。這就是為什麼這個損失函數叫做Focal Loss。
詳見《5分鐘理解Focal Loss與GHM——解決樣本不平衡利器》
https://zhuanlan.zhihu.com/p/80594704
class FocalLoss(nn.Module):
def __init__(self,gamma=2.0,alpha=0.75):
super().__init__()
self.gamma = gamma
self.alpha = alpha
def forward(self,y_pred,y_true):
bce = torch.nn.BCELoss(reduction = "none")(y_pred,y_true)
p_t = (y_true * y_pred) + ((1 - y_true) * (1 - y_pred))
alpha_factor = y_true * self.alpha + (1 - y_true) * (1 - self.alpha)
modulating_factor = torch.pow(1.0 - p_t, self.gamma)
loss = torch.mean(alpha_factor * modulating_factor * bce)
return loss
#困難樣本
y_pred_hard = torch.tensor([[0.5],[0.5]])
y_true_hard = torch.tensor([[1.0],[0.0]])
#容易樣本
y_pred_easy = torch.tensor([[0.9],[0.1]])
y_true_easy = torch.tensor([[1.0],[0.0]])
focal_loss = FocalLoss()
bce_loss = nn.BCELoss()
print("focal_loss(hard samples):", focal_loss(y_pred_hard,y_true_hard))
print("bce_loss(hard samples):", bce_loss(y_pred_hard,y_true_hard))
print("focal_loss(easy samples):", focal_loss(y_pred_easy,y_true_easy))
print("bce_loss(easy samples):", bce_loss(y_pred_easy,y_true_easy))
#可見 focal_loss讓容易樣本的權重衰減到原來的 0.0005/0.1054 = 0.00474
#而讓困難樣本的權重只衰減到原來的 0.0866/0.6931=0.12496
# 因此相對而言,focal_loss可以衰減容易樣本的權重。focal_loss(hard samples): tensor(0.0866)
bce_loss(hard samples): tensor(0.6931)
focal_loss(easy samples): tensor(0.0005)
bce_loss(easy samples): tensor(0.1054)FocalLoss的使用完整範例可以參考下面中自定義L1和L2正則化項中的範例,該範例既演示了自定義正則化項的方法,也演示了FocalLoss的使用方法。
三,自定義L1和L2正則化項通常認為L1 正則化可以產生稀疏權值矩陣,即產生一個稀疏模型,可以用於特徵選擇。
而L2 正則化可以防止模型過擬合(overfitting)。一定程度上,L1也可以防止過擬合。
下面以一個二分類問題為例,演示給模型的目標函數添加自定義L1和L2正則化項的方法。
這個範例同時演示了上一個部分的FocalLoss的使用。
1,準備數據
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import torch
from torch import nn
import torch.nn.functional as F
from torch.utils.data import Dataset,DataLoader,TensorDataset
import torchkeras
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
#正負樣本數量
n_positive,n_negative = 200,6000
#生成正樣本, 小圓環分布
r_p = 5.0 + torch.normal(0.0,1.0,size = [n_positive,1])
theta_p = 2*np.pi*torch.rand([n_positive,1])
Xp = torch.cat([r_p*torch.cos(theta_p),r_p*torch.sin(theta_p)],axis = 1)
Yp = torch.ones_like(r_p)
#生成負樣本, 大圓環分布
r_n = 8.0 + torch.normal(0.0,1.0,size = [n_negative,1])
theta_n = 2*np.pi*torch.rand([n_negative,1])
Xn = torch.cat([r_n*torch.cos(theta_n),r_n*torch.sin(theta_n)],axis = 1)
Yn = torch.zeros_like(r_n)
#匯總樣本
X = torch.cat([Xp,Xn],axis = 0)
Y = torch.cat([Yp,Yn],axis = 0)
#可視化
plt.figure(figsize = (6,6))
plt.scatter(Xp[:,0],Xp[:,1],c = "r")
plt.scatter(Xn[:,0],Xn[:,1],c = "g")
plt.legend(["positive","negative"]);ds = TensorDataset(X,Y)
ds_train,ds_valid = torch.utils.data.random_split(ds,[int(len(ds)*0.7),len(ds)-int(len(ds)*0.7)])
dl_train = DataLoader(ds_train,batch_size = 100,shuffle=True,num_workers=2)
dl_valid = DataLoader(ds_valid,batch_size = 100,num_workers=2)2,定義模型
class DNNModel(torchkeras.Model):
def __init__(self):
super(DNNModel, self).__init__()
self.fc1 = nn.Linear(2,4)
self.fc2 = nn.Linear(4,8)
self.fc3 = nn.Linear(8,1)
def forward(self,x):
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
y = nn.Sigmoid()(self.fc3(x))
return y
model = DNNModel()
model.summary(input_shape =(2,))----
Layer (type) Output Shape Param #
================================================================
Linear-1 [-1, 4] 12
Linear-2 [-1, 8] 40
Linear-3 [-1, 1] 9
================================================================
Total params: 61
Trainable params: 61
Non-trainable params: 0
----
Input size (MB): 0.000008
Forward/backward pass size (MB): 0.000099
Params size (MB): 0.000233
Estimated Total Size (MB): 0.000340
----3,訓練模型
# 準確率
def accuracy(y_pred,y_true):
y_pred = torch.where(y_pred>0.5,torch.ones_like(y_pred,dtype = torch.float32),
torch.zeros_like(y_pred,dtype = torch.float32))
acc = torch.mean(1-torch.abs(y_true-y_pred))
return acc
# L2正則化
def L2Loss(model,alpha):
l2_loss = torch.tensor(0.0, requires_grad=True)
for name, param in model.named_parameters():
if 'bias' not in name: #一般不對偏置項使用正則
l2_loss = l2_loss + (0.5 * alpha * torch.sum(torch.pow(param, 2)))
return l2_loss
# L1正則化
def L1Loss(model,beta):
l1_loss = torch.tensor(0.0, requires_grad=True)
for name, param in model.named_parameters():
if 'bias' not in name:
l1_loss = l1_loss + beta * torch.sum(torch.abs(param))
return l1_loss
# 將L2正則和L1正則添加到FocalLoss損失,一起作為目標函數
def focal_loss_with_regularization(y_pred,y_true):
focal = FocalLoss()(y_pred,y_true)
l2_loss = L2Loss(model,0.001) #注意設置正則化項係數
l1_loss = L1Loss(model,0.001)
total_loss = focal + l2_loss + l1_loss
return total_loss
model.compile(loss_func =focal_loss_with_regularization,
optimizer= torch.optim.Adam(model.parameters(),lr = 0.01),
metrics_dict={"accuracy":accuracy})
dfhistory = model.fit(30,dl_train = dl_train,dl_val = dl_valid,log_step_freq = 30)Start Training ...
================================================================================2020-07-11 23:34:17
{'step': 30, 'loss': 0.021, 'accuracy': 0.972}
+--+--+++----+
| epoch | loss | accuracy | val_loss | val_accuracy |
+--+--+++----+
| 1 | 0.022 | 0.971 | 0.025 | 0.96 |
+--+--+++----+
================================================================================2020-07-11 23:34:27
{'step': 30, 'loss': 0.016, 'accuracy': 0.984}
+--+--+++----+
| epoch | loss | accuracy | val_loss | val_accuracy |
+--+--+++----+
| 30 | 0.016 | 0.981 | 0.017 | 0.983 |
+--+--+++----+
================================================================================2020-07-11 23:34:27
Finished Training...# 結果可視化
fig, (ax1,ax2) = plt.subplots(nrows=1,ncols=2,figsize = (12,5))
ax1.scatter(Xp[:,0],Xp[:,1], c="r")
ax1.scatter(Xn[:,0],Xn[:,1],c = "g")
ax1.legend(["positive","negative"]);
ax1.set_title("y_true");
Xp_pred = X[torch.squeeze(model.forward(X)>=0.5)]
Xn_pred = X[torch.squeeze(model.forward(X)<0.5)]
ax2.scatter(Xp_pred[:,0],Xp_pred[:,1],c = "r")
ax2.scatter(Xn_pred[:,0],Xn_pred[:,1],c = "g")
ax2.legend(["positive","negative"]);
ax2.set_title("y_pred");
四,通過優化器實現L2正則化如果僅僅需要使用L2正則化,那麼也可以利用優化器的weight_decay參數來實現。
weight_decay參數可以設置參數在訓練過程中的衰減,這和L2正則化的作用效果等價。
before L2 regularization:
gradient descent: w = w - lr * dloss_dw
after L2 regularization:
gradient descent: w = w - lr * (dloss_dw+beta*w) = (1-lr*beta)*w - lr*dloss_dw
so (1-lr*beta)is the weight decay ratio.Pytorch的優化器支持一種稱之為Per-parameter options的操作,就是對每一個參數進行特定的學習率,權重衰減率指定,以滿足更為細緻的要求。
weight_params = [param for name, param in model.named_parameters() if "bias" not in name]
bias_params = [param for name, param in model.named_parameters() if "bias" in name]
optimizer = torch.optim.SGD([{'params': weight_params, 'weight_decay':1e-5},
{'params': bias_params, 'weight_decay':0}],
lr=1e-2, momentum=0.9)