1、交叉熵的criterion融合了Logsoftmax和ClassNLLCriterion两个类
1)ClassNLLCriterion
是一个负对数的一个criterion,用来做对n个类计算分类损失
这个类在forward的时候的input是一个1D的Tensor,长度为n,也即n各类。input是通过softmax来获得的。此后如果不想加入新的层,可以直接使用交叉熵的criterion。criterion在进行forward的时候,forward(input, target) ,target必须制定一个数,数的范围是1到n,表示是第几类。
最终的损失表示是

loss(x, class) = -x[class]
loss(x, class) = -weights[class] * x[class]
loss(x, class) = class != ignoreIndex ? -weights[class] * x[class] : 0

weight表示对该类损失的权重,ignore表示是否忽略本类带来的损失

2)
forward(input, target),其中input是1D的Tensor,target也必须是一个数字,用来指定是哪一个类别。交叉熵损失函数的定义如下

loss(x, class) = -log(exp(x[class]) / (\sum_j exp(x[j])))
               = -x[class] + log(\sum_j exp(x[j]))
loss(x, class) = weights[class] * (-x[class] + log(\sum_j exp(x[j])))

2、torch中softmax,Logsoftmax和ClassNLLCriterion的代码实现
1)首先是softmax的updateOutput的过程,送入输入,然后输出输出,假设输入是nx17x64x64的map的大小,nframe=n,dim=12,stride=64x64

void THNN_(SoftMax_updateOutput)(
          THNNState *state,
          THTensor *input,
          THTensor *output)
{

softmax函数在沿着dim,也即17的这个方向,首先找出17个位置的最大值,然后用每个位置的值减去最大值再做softmax,


exp(xi−max)/∑j(exp(xj−max)),j=1,2,...,17 e x p ( x i − m a x ) / ∑ j ( e x p ( x j − m a x ) ) , j = 1 , 2 , . . . , 17


代码如下:

for (d = 0; d < dim; d++)
    {
      if (input_ptr[d*stride] >= inputMax) inputMax = input_ptr[d*stride];
    }                 //找出最大值

    sum = 0;
    for (d = 0; d < dim; d++)
    {
      real z = exp(input_ptr[d*stride] - inputMax);
      output_ptr[d*stride] = z;
      sum += z;
    }

    for (d = 0; d < dim; d++)
    {
      output_ptr[d*stride] *= 1/sum;
    }
  }

2)Logsoftmax和softmax略有不同


outputi=xi−max+log(∑jexp(xj−max)) o u t p u t i = x i − m a x + l o g ( ∑ j e x p ( x j − m a x ) )


这里的输出也有减去max的操作。


3)


然后是ClassNLLCriterion的updateoutput的过程

void THNN_(ClassNLLCriterion_updateOutput)(
          THNNState *state,
          THTensor *input,                        #softmax的输出
          THIndexTensor *target,                  #回归目标
          THTensor *output,                       #ClassNLL的输出
          bool sizeAverage,
          THTensor *weights,
          THTensor *total_weight,
          long ignore_index)
{

在具体的实施中,分输入是一维的还是二维的。如果是一维的

if (THTensor_(nDimension)(input) == 1) {
    int cur_target = target_data[0] - TH_INDEX_BASE;
    if (cur_target != ignore_index) {
      THAssert(cur_target >= 0 && cur_target < n_classes);
      total_weight_data[0] = weights ? weights_data[cur_target] : 1.0f;
      output_data[0] = -input_data[cur_target] * total_weight_data[0];  输出的就是-loss[cur_target]
    }

如果是二维的话

else if (THTensor_(nDimension)(input) == 2) {
    int batch_size = THTensor_(size)(input, 0);
    THAssert(THIndexTensor_(size)(target, 0) == batch_size);

    int n_target = THTensor_(size)(input, 1);

    int i;
    for (i = 0; i < batch_size; i++) {
      int cur_target = target_data[i] - TH_INDEX_BASE;
      if (cur_target != ignore_index) {
        THAssert(cur_target >= 0 && cur_target < n_classes);

        real cur_weight = weights ? weights_data[cur_target] : 1.0f;
        total_weight_data[0] += cur_weight;
        output_data[0] -= input_data[i * n_target + cur_target] * cur_weight;
      }
    }
  }

因为这个时候的输入是N x n_classes,N是一个batch的样本数,n_classes是类别数,最终输出的outputdata[0]是N个结果的相加。需要注意的是!!claNLLcriterion仅仅针对一维和二维的情况,也就是仅仅能用于分类,可能在许多任务上都是用不到的。

3、二者的updateGradInput的过程。这个时候先来看ClassNLLCriterion的反传,也分为一维和二维的情况
首先对于一维的情形,在ClassNLLCriterion.lua文件里面,首先会对gradInput_data全部元素初始化为0,最后只对target的那一位进行更新处理,如果需要weight进行权衡的话,就是-weights_data[cur_target],否则就是-1。因为该位输入时-loss[cur_target],所以再没有weight权衡的条件下对loss[cur_target]求导的结果就是-1。其它地位置输出都是0,都是常数,求导后肯定是0。就不用再管了

gradInput_data[cur_target] =
     (!sizeAverage && weights) ? -weights_data[cur_target] : -1;
  }

二维的是这样的,每个batch对应的

for (i = 0; i < batch_size; i++){
      int cur_target = target_data[i] - TH_INDEX_BASE;

      if (cur_target != ignore_index) {
        THAssert(cur_target >= 0 && cur_target < n_classes);

        gradInput_data[i * n_target + cur_target] =
          -(weights ? weights_data[cur_target] : 1.0f);

        if (sizeAverage && *total_weight_data) {
          gradInput_data[i * n_target + cur_target] /= *total_weight_data;
  }

再看softmax.c的updateGradInput,输入就是该层的输入,self.nll.gradInput是上一层传回来的对于本层输出的梯度

self.lsm:updateGradInput(input, self.nll.gradInput)
#pragma omp parallel for private(t)
  for (t = 0; t < stride*nframe; t++)
  {
    real *gradInput_ptr = gradInput_data + (t/stride)*dim*stride + t % stride;
    real *output_ptr = output_data + (t/stride)*dim*stride + t % stride;
    real *gradOutput_ptr = gradOutput_data + (t/stride)*dim*stride + t % stride;

    ptrdiff_t d;
    accreal sum = 0;
    for (d = 0; d < dim; d++)
      sum += (accreal)gradOutput_ptr[d*stride] * output_ptr[d*stride];

    for (d = 0; d < dim; d++)
      gradInput_ptr[d*stride] = output_ptr[d*stride] * (gradOutput_ptr[d*stride] - sum);
  }

  THTensor_(free)(gradOutput);
  THTensor_(free)(output);
}

用语言描述就是,先沿着dim求一个sum,这个sum是dim每个位置的对该层该元素输出的梯度值(gradOutput_ptr)乘以该元素值(output_ptr)的和。假设dim是17,最终就是17个结果相加。然后每一个位置的gradInput就等于该位置元素的值(output_ptr)乘以该位置对应的输出的梯度减去刚刚求出来的sum(gradOutput_ptr[d*stride] - sum)

pytorch中criterion pytorch中criterion函数_二维


这里loss1-lossn就是softmax的输出,这样起名字好像是不太对,最终的L其实也不是这种表达形式,这里的L只是对CLSNLLCriterion这个表达式有效的,大体可以解释最终的softmax这里的梯度反传。对于CLSNLLCriterion这样的表达来讲,如果target是1的话,2~17是不影响的,这是因为强制置为0,就是一个常数对lossn求导了,所以一定是0,但在写softmax表达式的时候还是要写上。再来看LOGSoftmax的反传

pytorch中criterion pytorch中criterion函数_二维_02


总结来说torch中的CrossEntropyCriterion只是适用于二维或者一维的,大多用于分类的情况做softmax,对于空间上的损失,这个函数显然做不到还。如果想做类似于MSE的二元的交叉熵损失,就要用BSECriterion了。

4、对于SpatialClassNLLCriterion.c这个函数也是要求输入时NCWH,标签是NWH,所以对于spatial这样的设计的话,标签制作是有特殊要求的。

输入

标签

criterion

NCWH

NWH

SpatialClassNLLCriterion

NCWH

NCWH

BCECriterion(加上sigmoid约束)

NC

N

ClassNLLCriterion & CrossEntropyCriterion

5 更好的代码示例

6 再来回顾一下detectron里面的softmax loss损失。

pytorch中criterion pytorch中criterion函数_ide_03


这段话给出了解释,假设输出的map是NKHW,那么首先reshape成(NK,HW),然后进行损失的计算,标签是(NK,1).这里的注释文字也说了,这里的softmax和传统的spatial softmax是不一样的,传统的spatial softmax是沿着channel,每一个像素位置在channel上面做softmax。但是这里的softmax是空间位置上的softmax,应用在了wh这个平面上,是空间位置的softmax。

几点注意:

1)传统意义的spatial softmax negtive log loss的话,假设输入是Nx17x64x64,那么gt一定是NX64X64,每个(w,h)位置都是一个分类,标签是一个0-17的数字

2)另外就是detectron关键点的这个softmax,比较不一样

3)还有这篇论文中的pixel sigmoid cross entropy loss function

pytorch中criterion pytorch中criterion函数_ide_04


假设输入是NX17X64X64,标签也是NX17X64X64,每一个位置预测的概率和gt的概率相等的时候这个时候最后的损失是最小的,这和上面的两种损失又不一样,看名字。