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)
这里loss1-lossn就是softmax的输出,这样起名字好像是不太对,最终的L其实也不是这种表达形式,这里的L只是对CLSNLLCriterion这个表达式有效的,大体可以解释最终的softmax这里的梯度反传。对于CLSNLLCriterion这样的表达来讲,如果target是1的话,2~17是不影响的,这是因为强制置为0,就是一个常数对lossn求导了,所以一定是0,但在写softmax表达式的时候还是要写上。再来看LOGSoftmax的反传
总结来说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损失。
这段话给出了解释,假设输出的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
假设输入是NX17X64X64,标签也是NX17X64X64,每一个位置预测的概率和gt的概率相等的时候这个时候最后的损失是最小的,这和上面的两种损失又不一样,看名字。