Pytorch学习(7)-损失函数

本节主要是关于几种损失函数的学习。

  • 损失函数的定义
  • 常用的损失函数
  •  

一、损失函数的定义

损失函数用于描述模型预测值f(x)和真实值y的差距大小。它是一个非负实值函数,通常用L(y,f(x))来表示。

损失函数越小,模型的鲁棒性就越好。

损失函数是经验风险函数的核心部分,也是结果风险函数的重要组成部分。模型的风险结果包括风险项和正则项,通常如下所示:

                                       

其中,前面的均值函数表示的是经验风险函数,L代表的是损失函数,后面的Φ正则化项(regularizer)或者叫惩罚项(penalty term),它可以是L1,也可以是L2,或者其他的正则函数。整个式子表示的意思是找到使目标函数最小时的θ值

二、常用的损失函数

  1. 铰链损失(Hinge Loss):主要用于支持向量机(SVM) 中;
  2. 互熵损失 (Cross Entropy Loss,Softmax Loss ):用于Logistic 回归与Softmax 分类中;
  3. 平方损失(Square Loss):主要是最小二乘法(OLS)中;
  4. 指数损失(Exponential Loss) :主要用于Adaboost 集成学习算法中;
  5. 三元组损失(Triplet-loss)

三、详细理解

这节主要介绍三元组损失(Triplet-loss)函数。十八种损失函数见:https://blog.csdn.net/u011995719/article/details/85107524

  • softmax最终的类别数是确定的,而triplet loss学到的是一个好的embedding,相似的图像在embedding空间里是相近的,可以判断是否是同一个人脸。
  • 在深度学习训练中,我们需要尽可能的学习hard样本。是因为这些样本对损失影响较大,如果我们学习了很多对损失函数影响比较小的样本,会导致效果不好且浪费资源。比如正负样本不均衡时,负样本很多,如果一直学习负样本的话,训练起来对损失影响较小,所以我们会采取一些措施,将正负样本分来还要把最难分的样例分开。在此三元组损失函数中hard也是这个意思。
  • triplet-loss的思想:

          

         三重损失使一个anchor和一个正样本之间的距离最小化,两者具有相同的特性,并使一个anchor和不同特性的一个负样本之间的距离最大化。其中anchor为训练数据集中随机选取的一个样本,positive为和anchor属于同一类别的样本,而negative则为和anchor不同类的样本。学习后,使得同类样本positive样本更靠近anchor,而不同类的样本negative则远离anchor。

  • triplet-loss的数学表达式:

             

          进一步的损失函数(目标函数)变为如下:

              

          上式中的||*||为欧氏距离,所以:

                               表示的是positive和anchor之间的欧氏距离

                               表示的是negative和anchor之间的欧式距离

          是指两者距离之间有一个最小的间隔(anchor和negative之间的距离和anchor和positive之间的距离)

         注意:这里距离用欧式距离度量,+表示[]内的值大于0的时候,取该值为损失;小于0的时候,损失为0。

         由损失函数可以看出:

                当a与n之间的距离 < (a与p之间的距离 + )时,[]内的值大于0,就会产生损失。

                当a与n之间的距离 > (a与p之间的距离 + )时,[]内的值小于0,损失为0。

  • triplet loss 梯度推导

        上述目标函数标记为L,则当第i个triplet损失大于0的时候,仅就上述公式而言,有:

       

   可以看到,对x_p和x_n特征表达的梯度刚好利用了求损失时候的中间结果,给的启示:如果在CNN中实现triplet loss layer,如果能够在前向传播中存储着两个中间结果,反向传播的时候就能避免重复计算。

  • 三元组理解

      我们的目的是使loss在训练迭代中下降的越小越好,也就是要使得anchor与positive越接近越好,anchor与negative越远越好。

      基于上面这些,分析一下margin值的取值:

              当margin值越小时,loss也就越容易趋近于0,于是anchor与positive都不需要拉的太近,anchor与negative不需要拉的太远。就能使得loss很快趋近于0.这样训练得到的结果,不能够很好的区分相似的图像。

               当margin越大时,就需要使得网络参数要拼命的拉近anchor、positive之间的距离,拉远anchor、negative之间的距离。如果margin值太大,很可能最后loss保持一个较大的值,难以趋近于0。

               因此,设置一个合理的margin值很关键,这是衡量相似度的重要指标。简而言之,margin值设置越小,loss越容易趋近于0,但很难区分相似的图像。margin值设置的越大,loss值较难趋近于0,甚至导致网络不收敛,但可以较有把握的区分较为相似的图像。

 

  • 损失函数确定好之后,如何在训练时寻找anchor对应的negative样本和positive样本成为一个要着重考虑的问题。

          那么这个损失函数具体是什么意思呢?

           先选定a-p两元数组,然后在不是同一个人的里面找一个距离此人的距离小于的样本(这句话就对应上面的损失函数),因为这个是最难分的(从上面满足条件的样本中随机选的),所以为hard需要注意的是其中同一个人的图片不能作为negative,所以将其距离设为无穷大。这样的话就排除了那些同一人的样本,因为同一人的样本距离肯定小于

四、triplet loss原理

  •      输入是一个三元组<a, p, n>,其中a:anchor,p:positive,与a是同一类别的样本,n:negative,与a是不同类别的样本。

 

五、训练方法

5.1、offline

  • 训练集所有数据经过计算得到对应的embeddings,可以得到很多<i, j, k>的三元组,然后再计算triplet loss.
  • 效率不高,因为需要过一遍所有的数据得到三元组,然后训练反向更新网络。

5.2、online

  • 从训练集中抽取B样本,然后计算B个embeddings,可以产生个triplets( 当然其中有不合法的,因为需要的是<a, p, n>)

  • 实际中采用此方法,又分为两种策略( 一篇实在行人重识别的论文中提到的In Defense of the Triplet Loss for Person Re-Identification),假设B = PK,其中p个身份的人,每个身份的人k张图片(一般k = 4
  •          Batch All:计算batch_size中所有validhard tripletsemi-hard triplet,然后取平均得到Loss
  •                注意因为很多easy triplets的情况,所有平均会导致Loss很小,所有是对所有valid的求平均
  •                可以产生PK(K - 1)(PK - K)triplet
  •                      PK个anchor
  •                      K - 1个positive
  •                       PK - K个negative
  •           Batch Hard:对应每一个anchor,选择距离最大的d(a, p)和距离最大的d(a, n)
  •                   所有共有PK个三元组triplets

六、具体实现

首先举个例子:

假设(batch_size, feat_dim)为(3,4),再简单点,令inputs为:

                                                  

那么第一行(1,2,3,4)就代表第一个样本(S1),第二行(5,6,7,8)代表第二个样本(S2),以此类推。看一下样本之间的距离(欧氏距离)分布,为方便,用<>表示距离:

距离分布(开方之后)可以表示为矩阵D

                                                       

其中D(i,j)D(i,j)D(i,j)表示样本i,ji,ji,j之间的距离。
因为 ,而矩阵相乘 embeddings×embeddings.T中不仅包含了a∗ba*ba∗b的值,同时对角线上是向量平方的值,所以可以直接使用矩阵计算。

下面开始解析代码:

首先输入和例子一样:

inputs = torch.arange(1,13).view(3,4).float()
>>> inputs
tensor([[ 1.,  2.,  3.,  4.],
        [ 5.,  6.,  7.,  8.],
        [ 9., 10., 11., 12.]])

n = inputs.size(0)	# n = 3,为batch_size
# 每个数平方后, 进行sum(保持行数n不变),再扩展为(n,n)
dist = torch.pow(inputs, 2).sum(dim=1, keepdim=True).expand(n, n)
>>> dist
tensor([[ 30.,  30.,  30.],
        [174., 174., 174.],
        [446., 446., 446.]])

# 这样每个dis[i][j]代表的是样本i与样本j的平方的和
# 其中dist.t()是求矩阵dist的转置矩阵
dist = dist + dist.t()
>>> dist
tensor([[ 60., 204., 476.],
        [204., 348., 620.],
        [476., 620., 892.]])

之后介绍一下函数addmm()的用法:1 * dist - 2(input@input.t())

# 1 * dist - 2(input@input.t())
dist.addmm_(1, -2, inputs, inputs.t())
>>> dist
tensor([[  0.,  64., 256.],
        [ 64.,   0.,  64.],
        [256.,  64.,   0.]])

最后,对上面进行开方,clamp做简单数值处理(为了数值的稳定性):小于min参数的dist元素值由min值取代。

根号下不能为0,0开根号没有问题的,但是梯度反向传播就会导致无穷大。

dist = dist.clamp(min=1e-12).sqrt()
>>> dist
tensor([[1.0000e-06, 8.0000e+00, 1.6000e+01],
        [8.0000e+00, 1.0000e-06, 8.0000e+00],
        [1.6000e+01, 8.0000e+00, 1.0000e-06]])

此时,样本对之间距离计算就到位了,下面进行的是hard样本挖掘。

七、hard样本挖掘

For each anchor, find the hardest positive and negative.

上一节操作输出的张量dist里面存储着各个样本之间的距离,首先看看targets

targets:样本对应的标签(ground truth labels with shape——num_classes)

首先获取mask,类似掩模,# 这里 mask[i][j]=1 代表 i 和 j 的 label 相同(属于同一类别), mask[i][j]=0 则相反。

mask用于后面提取正样本和负样本。

# targets有n个类别,所以将它扩展成n*n的矩阵,判断该矩阵和转置矩阵对应元素之间是否相等
# 是否属于同一类别
mask = targets.expand(n, n).eq(targets.expand(n, n).t())

下面分别提取出正样本和负样本,对每个样本,在上面生成的距离矩阵中:

  • 先过滤掉和它不同类别的样本对应的距离,剩下的就是和它同一类别的positive,然后再在剩下的positive中找到距离值最大的,就是我们需要的hard positive
  • 寻找negative也是同理
for i in range(n):
    dist_ap.append(dist[i][mask[i]].max().unsqueeze(0))
    dist_an.append(dist[i][mask[i] == 0].min().unsqueeze(0))

详细解答上面代码:
   在这之前已经得到了距离矩阵dist和掩膜mask。其中dist[i, j]和mask[i, j]:i和j表示两个样本,而dist[i, j]表示两个样本的距离,mask[i, j]表示两个样本是否为同一类别。

# 首先要指定所谓的hard样本是什么?
# 个人理解:它是同一人的不同姿态、衣服以及背景下的样本。
# 我们要做的就是找到同一类样本中距离最大的positive和不同类样本中距离最小的negative。
# 然后通过三元组损失,经过学习后,将上面距离大的缩小,将距离小的放大
# 下面详细解释一下
dist_ap.append(dist[i][mask[i]].max().unsqueeze(0))
dist_an.append(dist[i][mask[i] == 0].min().unsqueeze(0))
# ----------------------------------------------------------------
inputs = torch.arange(1, 13).view(3, 4).float()

n = inputs.size(0)  # n = 3,为batch_size
# 每个数平方后, 进行sum(保持行数n不变),再扩展为(n,n)
dist = torch.pow(inputs, 2).sum(dim=1, keepdim=True).expand(n, n)

# 这样每个dis[i][j]代表的是样本i与样本j的平方的和
dist = dist + dist.t()

dist.addmm_(1, -2, inputs, inputs.t())

dist = dist.clamp(min=1e-12).sqrt()
print(dist)

targets = torch.arange(1, 4)
targets[2] = 1
mask = targets.expand(n, n).eq(targets.expand(n, n).t())
print(mask)
dist_ap, dist_an = [], []
# -------------------------------------------------------------------
# 上面代码都已经在前面见过了,所有假设input
# 此时经过计算得到的dist和mask为
tensor([[1.0000e-06, 8.0000e+00, 1.6000e+01],
        [8.0000e+00, 1.0000e-06, 8.0000e+00],
        [1.6000e+01, 8.0000e+00, 1.0000e-06]])
tensor([[ True, False,  True],
        [False,  True, False],
        [ True, False,  True]])
# --------------------------------------------------------------------
# 现在开始解析
dist_ap.append(dist[i][mask[i]].max().unsqueeze(0))
dist_an.append(dist[i][mask[i] == 0].min().unsqueeze(0))
#--------------------------------------------------------------------
print(mask[0])  # 查看一下样本i=0与其它几个样本是否为同一类别,true为是。
tensor([ True, False,  True])  # 发现0与0、3是同一类别的
#--------------------------------------------------------------------
print(dist[0][mask[0]])  # 再看一下相同类别之间的距离
tensor([1.0000e-06, 1.6000e+01])  # 分别为0和0,0和3
# ------------------------------------------------------------------
print(dist[0][mask[0]].max())  # 取其中最大的
tensor(16.)
print(dist[0][mask[0]].max().unsqueeze(0))  # 增加一维
tensor([16.])
# -------------------------------------------------------------------
# 上面就是查找同一类别中距离最大的样本
# 下面同理查找不同样本中距离最小的样本,省略。

拼接为新的tensor,计算相应的损失。

# cat使用用于将所有的hard样本距离拼接起来
dist_ap = torch.cat(dist_ap)
dist_an = torch.cat(dist_an)

y = torch.ones_like(dist_an) 
loss = self.ranking_loss(dist_an, dist_ap, y)

 

 

 

 

 

 

 

 

 

 

 

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值