Loss 是深度学习算法中重要的一部分,它的主要功能是评价网络预测的准确性和指导权重更新。合适 Loss 可以让网络收敛更快,预测更准。这个项目介绍了损失函数的基本概念以及7种常用损失函数的形式,性质,参数,使用场景及区别,并给出了开箱即用的Paddle实现。目前包含的Loss有:
L1(Mean Absolute Error)
L2(Mean Square Error)
Huber Loss
LogCosh Loss
Cross Entropy(Log Loss)
Focal Loss
Hinge Loss
未完待续…
关键概念
先引入一些概念,方便后面介绍。功能角度,我们可以形容深度学习是一种从训练数据中学习,进而形成问题解决方案的算法。如果训练数据是有标签的,那么它就属于一个监督学习任务(比如房价预测或手写数字识别)。形式化的,我们可以描述监督学习为: 给算法一个有标签的训练数据集
(
�
,
�
)
(X,Y),希望算法能学习出一个
�
−
�
X−>Y 的映射关系
�
f,使得不仅对于训练集中的
�
x,
�
(
�
)
f(x) 与对应的
�
y 接近,而且对不在训练集中的
�
x,
�
(
�
)
f(x) 也与实际的
�
y 接近。
听起来很美好,但具体怎么操作呢? 这里就要用到Loss。我们在理解监督学习概念的时候可以用 “接近” 这样定性的描述,但是算法需要一个函数来量化模型给出的预测和训练数据中对应标签的差异,我们把这个函数叫做Loss。形式化的,可以将Loss定义为网络预测值
�
p 和实际标签
�
y 的函数
�
(
�
,
�
)
L(p,y)。
Loss的性质
Loss函数需要满足一些性质,首先最明显的,它应该能够描述我们上面概念中的"接近"。预测值接近标签 Loss 应该小,否则 Loss 应该大。 其次,深度学习使用的梯度下降优化方法需要让模型权重沿着 Loss 导数的反向更新,所以这个函数需要是可导的,而且不能导数处处为0(这样权重动不了)。 此外有一些加分项,比如 MSE Loss 的梯度正比于 Loss 值,因此用 MSE 做训练收敛速度一般比 MAE 快。
Loss的分类
不同的深度学习任务定义 “接近” 的标准不同,因此 Loss 也有很多种,但大致可以分为两类: 回归 Loss 和 分类 Loss。两类问题最明显的区别是回归预测的结果是连续的(比如房价),而分类预测的结果是离散的(比如手写识别输出这个数字是几)。虽然很多实际的深度学习应用比上面的两个例子复杂但是也可以用回归和分类来描述。比如语义分割可以看成对图像中的每一个像素点进行分类,目标检测可以看成对Bounding Box在图像中的位置和大小进行回归,NLP总情感倾向分析可以看成对句子进行积极和消极分类。
符号
下面的表达式都按照这个符号标准:
x:训练数据集中的一个输入
y:训练数据集中的一个输出
p:网络针对一个输入给出的预测值
M:分类问题中的类别数量
N:输入loss的数据条数,可以理解为Batch Size
i:第i条数据
j:第j个输出
开篇的废话就这么多,下面首先回归之后分类进入正题。
回归 Loss
这里画出了所有 Loss 的函数图像,可以对照查看。
1
�
∑
∣
�
−
�
∣
L1Loss=
N
1
∑∣y−p∣
1
�
∑
(
�
−
�
)
2
L2Loss=
N
1
∑(y−p)
2
L1和L2 Loss很类似,而且一些性质比较着说更清楚,因此放在一起。首先计算方法上 L1 Loss 是求所有预测值和标签距离的平均数,L2 Loss 是求预测值和标签距离平方的平均数。二者的结构很类似,区别就是一个用了绝对值一个用了平方。在性质上,二者主要有三个方面的不同:离群点鲁棒性,梯度和是否可微。
离群点鲁棒性
离群点是数据中明显偏离整体分布的点。在深度学习任务中,离群点一般代表噪声,比如整理数据的时候 1.5 忘写小数点了变成了 15,其他数据还都是1.x,那这条数据就变成了离群点。但是在有些场景下一些数据点可能就是比较特殊,它们的确离群但不是数据中的错误。如果是第一种情况,我们在训练的过程中会希望降低脏数据对网络的影响。如果是第二种,离群点是特殊情况,我们会希望考虑这些特殊的信息,但不希望他们给网络整体性能带来太大的影响。L2 Loss 对于预测值和标签的距离做了平方,当距离大于 1 的时候平方操作会放大误差,因此离群点的Loss会非常大,导致 L2 Loss 对其倾斜更多的权重。相比之下 L1 Loss 的情况会好一些,因为只是做了绝对值,所以对离群点不如 L2 敏感。比如下面这组数据:
预测值 标签 L1 L1(离群) 变化 L2 L2(离群) 变化比率
1.1 1 0.1 0.1 0.01 0.01
1.2 1.2 0 0 0 0
1.3 1.5/3 0.2 1.7 0.04 2.89
0.1 0.6 6 0.016 0.97 60.6
网络预测值相同的情况下,如果数据1.5变成一个离群点,L1 Loss变大了6倍,L2 Loss变大了60倍。显然用L2 Loss拟合出的结果会对这个离群点更敏感:
黑线为用L1的结果,红线为用L2的结果
此外可以想象一个直观的例子,拟合一批
�
x 都一样的数据,比如
�
y 轴上的一些点,这样
�
x 都为0。拟合出来的结果和
�
y 轴的交点,如果用 L1 Loss 应该是中位数,如果用 L2 Loss 应该是平均数。对离群点来说,中位数比平均数显然更鲁棒。
梯度
对 L1 和 L2 Loss求导可以知道 L1 Loss 的梯度一直是
±
1
±1,而 L2 Loss 的梯度是正比于 Loss 值的,Loss 值越大梯度越大。在梯度的性质上L2是优于L1的,体现在两个方面。当Loss非常大的时候,L1的梯度一直是
1
1,这样收敛的速度比L2慢。其次当 Loss 很接近0的时候L1的梯度还是1,这个大梯度容易让网络越过 Loss 最低点,导致 Loss 在最低点附近震荡,相比之下 L2 在 Loss 接近0的时候梯度也接近0,不存在这样震荡的问题。
可微
因为L1 Loss是个分段函数,所以在最低点是不可微的。L2则全程可微,最后一定会稳定收敛到一个最优解。但是需要注意的是L2不是一个凸函数,因此收敛到的解不一定是全局最优解,也有可能是一个局部最优解。
L1和L2的选择
总体上来说L2训练收敛的速度快,选择L2的情况比较多。如果训练数据中存在比较多的脏数据应该选择L1 Loss避免其影响结果。
下面是实现:
In [1]
import paddle.fluid as fluid
import numpy as np
places = fluid.CPUPlace()
exe = fluid.Executor(places)
In [23]
def l1_loss(pred, label):
loss = fluid.layers.abs(pred - label)
loss = fluid.layers.reduce_mean(loss)
return loss
l1_program = fluid.Program()
with fluid.program_guard(l1_program):
pred = fluid.data(‘pred’, [3,1], dtype=“float32”)
gt = fluid.data(‘gt’, [3,1], dtype=“float32”)
loss = l1_loss(pred, gt)
pred_val = np.array([[1], [2], [3] ],dtype=“float32”)
gt_val = np.array([[1], [3], [6] ],dtype=“float32”)
loss_value=exe.run(l1_program, feed={ ‘pred’: pred_val , “gt”: gt_val },fetch_list=[loss])
print(“L1 Loss:”, loss_value)
L1 Loss: [array([1.3333334], dtype=float32)]
In [24]
def l2_loss(pred, label):
loss = (pred - label) ** 2
loss = fluid.layers.reduce_mean(loss)
return loss
l2_program = fluid.Program()
with fluid.program_guard(l2_program):
pred = fluid.data(‘pred’, shape=[3,1], dtype=‘float32’)
gt = fluid.data(‘gt’, shape=[3,1], dtype=‘float32’)
loss = l2_loss(pred, gt)
pred_val = np.array([[3], [2], [1]], dtype=‘float32’)
gt_val = np.array([[1], [2], [3]], dtype=‘float32’)
1
�
(
�
−
�
)
MeanBiasError=
N
1
(p−y)
MBE是不做绝对值的L1 Loss,它的一个主要的问题是正负 Loss 会相互抵消,在深度学习的应用很少。但是因为没做绝对值所以可以看出网络预测的结果是偏大还是偏小。
In [25]
def mean_bias_error(pred, label):
loss = pred - label
loss = fluid.layers.reduce_mean(loss)
return loss
mbe_program = fluid.Program()
with fluid.program_guard(mbe_program):
pred = fluid.data(‘pred’, shape=[3,1], dtype=‘float32’)
gt = fluid.data(‘gt’, shape=[3,1], dtype=‘float32’)
loss = mean_bias_error(pred, gt)
pred_val = np.array([[3], [2], [1]], dtype=‘float32’)
gt_val = np.array([[1], [2], [3]], dtype=‘float32’) # 这组数据上正负的bias就抵消了,这也是深度学习基本不用这个loss的原因
loss_value = exe.run(mbe_program, feed={‘pred’: pred_val, ‘gt’: gt_val}, fetch_list= [loss])
print(“MBE:”, loss_value)
MBE: [array([0.], dtype=float32)]
Huber loss(Smooth L1 Loss)
注:Smooth L1 Loss 并不是Huber的别名,而是一个特殊情况。
{
1
2
(
�
−
�
)
2
∣
�
−
�
∣
<
�
�
∣
�
−
�
∣
−
1
2
�
2
�
�
ℎ
�
�
�
�
�
�
HuberLoss={
2
1
(y−p)
2
δ∣y−p∣−
2
1
δ
2
∣y−p∣<δ
otherwise
Huber是 L1 和 L2 Loss 的分段组合。前面我们已经知道 L1 在有离群点时性能好,L2 在接近零点处稳定收敛, 于是将二者组合:在零点附近用L2,其余位置用 L1 就形成了 Huber Loss。具体的选择范围用
�
δ 划分。
�
δ 取 1 的 Huber Loss 也叫 Smooth L1 Loss,所以说Smooth L1是Huber的一种特殊情况。分段组合克服了两个 Loss 各自的一部分弱点,Huber Loss对离群点没有 L2 敏感,在零点附近也不会出现 L1 的震荡。通过调节
�
δ 可以调节 Huber Loss 对离群点的敏感度,
�
δ 越大,使用L2的区间越大,对离群点越敏感; 反之
�
δ 越小越不敏感。
In [26]
def huber_loss(pred, label, delta=1): # 这个实现里面去掉了 1/2常数项 ,这样不需要用 if,更简洁
l2 = (pred - label) ** 2
l1 = fluid.layers.abs(pred - label) * delta
loss = fluid.layers.elementwise_min(l1, l2)
loss = fluid.layers.reduce_mean(loss)
return loss
huber_program = fluid.Program()
with fluid.program_guard(huber_program):
pred = fluid.data(‘pred’, shape=[3,1], dtype=‘float32’)
gt = fluid.data(‘gt’, shape=[3,1], dtype=‘float32’)
loss = huber_loss(pred, gt, delta=2)
pred_val = np.array([[4], [2], [1]], dtype=‘float32’)
gt_val = np.array([[1], [2], [3]], dtype=‘float32’)
�
�
�
(
�
�
�
ℎ
(
�
−
�
)
)
LogCosh=log(cosh(y−p))
形如其名,Log-cosh 计算上是先做cosh之后做log。这个函数的特点是在 Loss 比较小的时候,值近似于
1
2
�
2
2
1
x
2
,而在值比较大的时候近似于
∣
�
−
�
∣
−
�
�
�
(
2
)
∣y−p∣−log(2)。 他基本和 Huber Loss的性质相同,但是处处二阶可微。(一些优化方法有这个要求)
红色为Log-Cosh,紫色为Huber Loss
In [27]
def logcosh_loss(pred, label):
loss = pred - label
e = np.e * fluid.layers.ones_like(loss)
cosh = ( fluid.layers.elementwise_pow(e, loss) + fluid.layers.elementwise_pow(e, 0-loss) ) / 2
log = fluid.layers.log(cosh)
loss= fluid.layers.reduce_mean(log)
return loss
logcosh_program = fluid.Program()
with fluid.program_guard(logcosh_program):
pred = fluid.data(‘pred’, shape=[3,1], dtype=‘float32’)
gt = fluid.data(‘gt’, shape=[3,1], dtype=‘float32’)
loss = logcosh_loss(pred, gt)
pred_val = np.array([[4], [2], [1]], dtype=‘float32’)
gt_val = np.array([[1], [2], [3]], dtype=‘float32’)
∑
�
:
�
�
�
�
(
1
−
�
)
∣
�
�
−
�
�
∣
+
∑
�
:
�
�
<
�
�
�
∣
�
�
−
�
�
∣
QuantileLoss=∑
i:p
i
y
i
(1−γ)∣y
i
−p
i
∣+∑
i:p
i
γ∣y
i
−p
i
∣
在一些场景下(比如商业决策),用户可能会希望了解预测中的不确定性,并基于此进行决策。这种情况下希望算法给出一个预测区间而不是单一的一个值。 此外,前面的 Loss 关注的都是给出单一值的点预测,这种预测的假设是输入数据符合一个目标函数
�
(
�
)
f(x) 加上一个方差恒定的独立变量残差。 如果训练数据不满足这种性质那么线性回归模型就不成立,预测的效果不会好。即便对于具有变化方差或非正态分布的残差,基于分位数损失的回归也能给出合理的预测区间。
分位值
�
�
�
�
�
gamma 的选择取决于我们对高估和低估的重视程度。 从公式形式上,
�
γ 是低估部分的斜率,因此
�
γ 越大,对低估的惩罚越大,区间会偏高。
经常会看到的拟合
�
�
�
(
�
)
sin(x)
In [28]
def quantile_loss(pred, label, gamma):
dist = fluid.layers.abs(pred - label)
cond = fluid.layers.greater_than(pred, label)
ie = fluid.layers.IfElse(cond)
with ie.true_block():
loss = ie.input(dist)
loss = (1 - gamma) * loss
ie.output(loss)
with ie.false_block():
loss = ie.input(dist)
loss = gamma * loss
ie.output(loss)
loss = ie()[0] # 返回的是一个list
loss = fluid.layers.reduce_mean(loss)
return loss
quantile_program = fluid.Program()
with fluid.program_guard(quantile_program):
pred = fluid.data(‘pred’, shape=[4,1], dtype=‘float32’)
gt = fluid.data(‘gt’, shape=[4,1], dtype=‘float32’)
loss = quantile_loss(pred, gt, 0.5)
pred_val = np.array([[4], [2], [1], [3]], dtype=‘float32’)
gt_val = np.array([[1], [2], [3], [4]], dtype=‘float32’)
loss_value = exe.run(quantile_program, feed={‘pred’: pred_val, ‘gt’: gt_val}, fetch_list= [loss])
print(“quantile loss:”, loss_value)
quantile loss: [array([0.75], dtype=float32)]
最后是一张回归Loss的全家福:
在这里查看更清楚
分类 Loss
先说说为什么前面我们有了回归的 Loss 还需要特别分出来一类用来分类的 Loss,性质上分类 Loss 和回归 Loss 有什么不同。首先分类问题和回归问题最明显的区别是分类问题输出的是一些概率,范围是0~1。我们一般在网络的输出层用 Sigmoid 函数
1
1
+
�
−
�
1+e
−x
1
实现这个限制。为了说明方便,下文将网络最后一层进入启动函数 Sigmoid 之前的值称为
�
O,将经过 Sigmoid 之后的概率称为
�
P。下面是 Sigmoid 的函数图像
可以看出,这个函数的函数值在输入比较大和比较小的时候都极其接近1(函数值在4的时候已经是0.982),而且很平,导数很小。我们可以设想训练中的一个情况:使用MSE Loss而且网络最后一层的
�
O 比较大(初始化如果做的不好很可能第一个batch就是这样)。这种情况对应Sigmoid图像中X轴右侧的一个点,我们来分析此时 Loss 相对
�
O 的梯度。在这个位置 Sigmoid 很平,所以就算
�
O 有很大的变化对应的
�
P 变化也很小,反映到 Loss 上变化也很小。因此当
�
O 较大或较小的时候 Loss 的梯度都很小,会导致训练缓慢。相比之下分类 Loss 在输出接近0或1的时候极其敏感,一点很小的变化都会给 Loss 带来很大的变化,因此训练更快。
额外要说一下,分类问题里面有二分类(比如猫狗二分类)也有分成多类(比如ImageNet)的情况,虽然二分类可以看成一种分成两类的多分类情况,但是因为用的比较多而且好理解所以我们这里基本以二分类为例介绍。后期会逐步完善分成多类Loss的实现。
1
�
[
�
�
�
�
�
(
�
�
)
+
(
1
−
�
�
)
�
�
�
(
1
−
�
�
)
]
BinaryCrossEntropy=−
N
1
∑
i=1
N
[y
i
log(p
i
)+(1−y
i
)log(1−p
i
)]
1
�
�
�
�
�
�
�
(
�
�
�
)
CategoricalCrossEntropy=−
N
1
∑
i=1
N
∑
j=1
M
y
ij
log(p
ij
)
分类问题中最常用的是交叉熵Loss。这个Loss来自香农的信息论,原理介绍就略过了,但是通过图像可以十分直观的看出来为什么这种 Log 的形式适合做分类的 Loss 。
0
y=0 时,如果预测结果
�
p 也是 0 ,那么皆大欢喜 Loss 为 0。但是如果
�
p 不是 0,那么
�
p 越接近 1 Loss 越大而且增长的非常快。
In [29]
def bce_loss(pred, label, epsilon=1e-05): # 标签都是 0或1,但是计算上log(0)不合法,所以一般将label和pred卡到[eps, 1-eps]范围内
label = fluid.layers.clip(label, epsilon, 1-epsilon)
pred = fluid.layers.clip(pred, epsilon, 1-epsilon) # 防止出现log(0)
loss = -1 * (label * fluid.layers.log(pred) + (1 - label) * fluid.layers.log(1 - pred))
loss = fluid.layers.reduce_mean(loss)
return loss
bce_program = fluid.Program()
with fluid.program_guard(bce_program):
pred = fluid.data(‘pred’, shape=[3,1], dtype=‘float32’)
gt = fluid.data(‘gt’, shape=[3,1], dtype=‘float32’)
loss = bce_loss(pred, gt)
pred_val = np.array([[0.9], [0.1], [1]], dtype=‘float32’)
gt_val = np.array([[1], [0], [1]], dtype=‘float32’)
1
�
�
�
�
�
�
�
(
�
�
)
+
(
1
−
�
�
)
�
�
�
(
1
−
�
�
)
WeighedCrossEntropy=−
N
1
∑
i=1
N
wy
i
log(p
i
)+(1−y
i
)log(1−p
i
)
1
�
�
�
�
�
�
�
(
1
−
�
�
)
+
(
1
−
�
)
(
1
−
�
�
)
�
�
�
(
�
�
)
BalancedCrossEntropy=−
N
1
∑
i=1
N
βy
i
log(1−p
i
)+(1−β)(1−y
i
)log(p
i
)
0
y=0 的类别在数据集中的比例正好能平衡两个类别的不均衡现象。
In [30]
def wce_loss(pred, label, w=1, epsilon=1e-05): # w 是给到 y=1 类别的权重,越大越重视
label = fluid.layers.clip(label, epsilon, 1-epsilon)
pred = fluid.layers.clip(pred, epsilon, 1-epsilon)
loss = -1 * (w * label * fluid.layers.log(pred) + (1 - label) * fluid.layers.log(1 - pred))
loss = fluid.layers.reduce_mean(loss)
return loss
wce_program = fluid.Program()
with fluid.program_guard(wce_program):
pred = fluid.data(‘pred’, shape=[3,1], dtype=‘float32’)
gt = fluid.data(‘gt’, shape=[3,1], dtype=‘float32’)
loss = wce_loss(pred, gt)
pred_val = np.array([[0.9], [0.1], [1]], dtype=‘float32’)
gt_val = np.array([[1], [0], [1]], dtype=‘float32’)
loss_value = exe.run(wce_program, feed={‘pred’: pred_val, ‘gt’: gt_val}, fetch_list= [loss])
print(“WCE Loss:”, loss_value)
WCE Loss: [array([0.07029679], dtype=float32)]
In [31]
def balanced_ce_loss(pred, label, beta=0.5, epsilon=1e-05): # beta 是给到 y=1 类别的权重,越大越重视,范围在(0-1)
label = fluid.layers.clip(label, epsilon, 1-epsilon)
pred = fluid.layers.clip(pred, epsilon, 1-epsilon)
loss = -1 * (beta * label * fluid.layers.log(pred) + (1-beta) * (1 - label) * fluid.layers.log(1 - pred))
loss = fluid.layers.reduce_mean(loss)
return loss
balanced_ce_program = fluid.Program()
with fluid.program_guard(balanced_ce_program):
pred = fluid.data(‘pred’, shape=[3,1], dtype=‘float32’)
gt = fluid.data(‘gt’, shape=[3,1], dtype=‘float32’)
loss = balanced_ce_loss(pred, gt)
pred_val = np.array([[0.9], [0.1], [1]], dtype=‘float32’)
gt_val = np.array([[1], [0], [1]], dtype=‘float32’)
1
�
−
(
�
(
1
−
�
�
)
�
�
�
�
�
�
(
�
�
)
)
+
(
1
−
�
)
�
�
�
(
1
−
�
�
�
�
�
(
1
−
�
�
)
)
FocalLoss=−
N
1
∑
i=1
N
−(α(1−p
i
)
γ
y
i
log(p
i
))+(1−α)p
i
γ
(1−y
i
log(1−p
i
))
0
p=0 ,如果此时
�
p 已经接近 0 了那么
�
�
�
�
�
�
p
gamma
就会很接近0,这样这项的 Loss 就会很小,反之会很大。因此通过这一项Focal Loss实现了让网络关注更难的case的功能。
In [32]
def focal_loss(pred, label, alpha=0.25,gamma=2,epsilon=1e-6):
‘’’
alpha 越大越关注y=1的情况
gamma 越大越关注不确定的情况
‘’’
pred = fluid.layers.clip(pred,epsilon,1-epsilon)
label = fluid.layers.clip(label,epsilon,1-epsilon)
loss = -1 * (alpha * fluid.layers.pow((1 - pred), gamma) * label * fluid.layers.log(pred) + (1 - alpha) * fluid.layers.pow(pred, gamma ) * (1 - label) * fluid.layers.log(1 - pred))
loss = fluid.layers.reduce_mean(loss)
return loss
focal_program = fluid.Program()
with fluid.program_guard(focal_program):
pred = fluid.data(‘pred’, shape=[3,1], dtype=‘float32’)
gt = fluid.data(‘gt’, shape=[3,1], dtype=‘float32’)
loss = focal_loss(pred, gt)
pred_val = np.array([[0.9], [0.1], [1]], dtype=‘float32’)
gt_val = np.array([[1], [0], [1]], dtype=‘float32’)
1
�
�
�
�
(
0
,
1
−
�
�
�
�
)
HingeLoss=
N
1
∑
i=1
N
max(0,1−y
i
p
i
)
Hinge Loss 主要用在支持向量机中,它的标签和之前的0/1不同,正例的标签是1,负例的标签是-1。Hinge Loss的图像如下:
可以看出它不仅惩罚错误的预测,而且惩罚不自信的正确预测。和前面的交叉熵相比,Hinge Loss形式上更简单,运算更快,而且因为一些情况下Loss是0不需要进行反向传递因此训练速度较快。如果不是很关注正确性但是需要作出实时决策Hinge是很合适的。
In [33]
def hinge_loss(pred, label):
‘’’
alpha 越大越关注y=1的情况
gamma 越大越关注不确定的情况
‘’’
zeros = fluid.layers.zeros_like(pred)
loss = fluid.layers.elementwise_max(zeros, 1 - pred * label)
loss = fluid.layers.reduce_sum(loss)
return loss
hinge_program = fluid.Program()
with fluid.program_guard(hinge_program):
pred = fluid.data(‘pred’, shape=[3,1], dtype=‘float32’)
gt = fluid.data(‘gt’, shape=[3,1], dtype=‘float32’)
loss = focal_loss(pred, gt)
pred_val = np.array([[0.9], [-0.1], [1]], dtype=‘float32’)
gt_val = np.array([[1], [-1], [1]], dtype=‘float32’)
loss_value = exe.run(hinge_program, feed={‘pred’: pred_val, ‘gt’: gt_val}, fetch_list= [loss])
print(“Hinge Loss:”,loss_value)
Hinge Loss: [array([9.292055e-05], dtype=float32)]