利用深度學習來給機器學習賦能(1)

  • 2021 年 3 月 29 日
  • AI

這篇主要講下將torch用於lightgbm的一個比較有意思的操作,和之前的autograd的方式類似,不過更簡單方便.

眾所周知,lightgbm和xgboost這類框架的內置損失函數都不夠”風騷”,僅僅實現了常見的一些損失函數,而作為風騷之首,深度學習,各種稀奇古怪的loss滿天飛,下面用一個例子簡單介紹一下,怎麼把torch內置的或者是github上torch實現的一些有意思的損失函數放到lightgbm中.

其實方法很簡單:

第一種:使用torch自帶的損失函數

我們以torch的smooth l1損失函數為例子:

torch.nn.SmoothL1Loss(reduction='mean')

其中:

import torch
from torch import autograd
import numpy as np
y_pred=np.array([1.5,1.4,1.3,1.2,1.4])
y_pred=torch.from_numpy(y_pred)
y_pred.requires_grad=True

y_true=np.array([1.2,1.3,1.2,1.1,1.5])
y_true=torch.from_numpy(y_true)
y_true.requires_grad=False

因為lgb或者xgb的內置損失函數輸出為numpy形式的y_pred和y_true,所以這個地方需要注意要將numpy轉化為tensor,torch將numpy轉tensor的方式有兩種,一種是torch.tensor,一種是torch.from_numpy,前者開闢了新的記憶體空間來存放原始的numpy,也就是重新複製了一份數據,速度相對慢一些,而torch.from_numpy和torch.numpy都是共享記憶體的,轉換速度很快.

然後注意需要將我們要求梯度的變數的requires_grad設置為True,這樣torch才知道這個向量是一個變數,才能在後續的計算中對其計算梯度.

然後就很簡單了:

from torch import autograd
loss=torch.nn.SmoothL1Loss()(y_pred,y_true)
dy_dx = torch.autograd.grad(loss,y_pred,create_graph=True,retain_graph=True)[0]

注意y_pred在前,y_true在後,torch這個地方的loss的設計比較反傳統…

注意這裡要create_graph創建計算圖然後retain_graph保留計算圖,因為二階梯度是在一階梯度的基礎上進行的,所以要保留計算一階梯度的計算圖便於後續的程式在這一步的計算圖上繼續計算, 可以想像為計算圖上的不同程式運行的計算可以按照順序連接起來:

這樣我們就得到了我們的一階梯度了:

dy_dx2 = torch.autograd.grad(dy_dx,y_pred,
                          grad_outputs=torch.ones(y_pred.shape), 
                          create_graph=False)[0]

計算二階梯度的時候需要注意,要設置grad_outputs=torch.ones(y_pred.shape),

主要是因為計算一階梯度的時候,我們的loss是一個標量

torch這個地方的設計非常的像小學生思考的過程(這也是torch設計人性化的地方),因為小學生只會計算偏導,但是對向量形式的導數計算無能為力,比如下面的例子

Z要是一個標量才會算,不是標量就不知道咋算了,其實我們只要轉化為標量就可以了:

求和之後分別計算偏導就可以了

例子來源於:

marsggbo:Pytorch autograd,backward詳解zhuanlan.zhihu.com圖標

在autograd中我們只要grad_outputs=torch.ones(y_pred.shape)就可以了,因為向量點乘一個相同大小的全1向量就是求和的操作了.

這樣我們就得到了smoothL1的一階和二階梯度了:

因為dy_dx創建了計算圖,並且在計算圖中,dy_dx2沒有繼續retain_graph所以已經不在計算圖上了,但是dy_dx還在,我們通過detach將其從計算圖中分離出來.

torch的函數裡面經常會有detach和detach_這樣大體相同但是名字後面會多一個”_”的情況,前者的操作是copy一份新數據出來創建新的記憶體空間存放,後者則是原地替換的操作,不開闢新的記憶體空間,省記憶體(Python通過id函數可以很方便的查看數據存放的記憶體地址,比較一下就知道了)

最後將dy_dx作為grad,將dy_dx2作為hessian按照lgb或者xgb的自定義損失函數要求的格式回傳就可以了.

第二種:github上用torch寫的損失函數

以 torch的loss toolbox這個開源library為例

from torch import nn
from torch.nn import functional as F
class BinaryFocalLoss(nn.Module):
    """
    This is a implementation of Focal Loss with smooth label cross entropy supported which is proposed in
    'Focal Loss for Dense Object Detection. (//arxiv.org/abs/1708.02002)'
        Focal_Loss= -1*alpha*(1-pt)*log(pt)
    :param alpha: (tensor) 3D or 4D the scalar factor for this criterion
    :param gamma: (float,double) gamma > 0 reduces the relative loss for well-classified examples (p>0.5) putting more
                    focus on hard misclassified example
    :param reduction: `none`|`mean`|`sum`
    :param **kwargs
        balance_index: (int) balance class index, should be specific when alpha is float
    """

    def __init__(self, alpha=3, gamma=2, ignore_index=None, reduction='mean',**kwargs):
        super(BinaryFocalLoss, self).__init__()
        self.alpha = alpha
        self.gamma = gamma
        self.smooth = 1e-6 # set '1e-4' when train with FP16
        self.ignore_index = ignore_index
        self.reduction = reduction

        assert self.reduction in ['none', 'mean', 'sum']

        # if self.alpha is None:
        #     self.alpha = torch.ones(2)
        # elif isinstance(self.alpha, (list, np.ndarray)):
        #     self.alpha = np.asarray(self.alpha)
        #     self.alpha = np.reshape(self.alpha, (2))
        #     assert self.alpha.shape[0] == 2, \
        #         'the `alpha` shape is not match the number of class'
        # elif isinstance(self.alpha, (float, int)):
        #     self.alpha = np.asarray([self.alpha, 1.0 - self.alpha], dtype=np.float).view(2)

        # else:
        #     raise TypeError('{} not supported'.format(type(self.alpha)))

    def forward(self, output, target):
        prob = torch.sigmoid(output)
        prob = torch.clamp(prob, self.smooth, 1.0 - self.smooth)

        valid_mask = None
        if self.ignore_index is not None:
            valid_mask = (target != self.ignore_index).float()

        pos_mask = (target == 1).float()
        neg_mask = (target == 0).float()
        if valid_mask is not None:
            pos_mask = pos_mask * valid_mask
            neg_mask = neg_mask * valid_mask

        pos_weight = (pos_mask * torch.pow(1 - prob, self.gamma)).detach()
        pos_loss = -torch.sum(pos_weight * torch.log(prob)) / (torch.sum(pos_weight) + 1e-4)
        
        
        neg_weight = (neg_mask * torch.pow(prob, self.gamma)).detach()
        neg_loss = -self.alpha * torch.sum(neg_weight * F.logsigmoid(-output)) / (torch.sum(neg_weight) + 1e-4)
        loss = pos_loss + neg_loss

        return loss

用法基本上是一樣的,一般來說git上常見的torch的自定義損失函數也是按照自定義model的形式來寫的,可以參考:

Pytorch如何自定義損失函數(Loss Function)?www.zhihu.com圖標

這裡的回答.

torch的設計理念:萬物皆可nn.module,自定義模型,自定義layer,自定義損失函數都是一套api的模板就可以搞定了.