机器学习常用的损失函数总结一下,给自己备忘用。

0. 损失函数和代价函数

损失函数(Loss Function)代价函数(Cost Function)。损失函数也叫误差函数(error function),是用与单个训练样本,表示单个样本的预测值和真实值相似性,而代价函数是用于整体数据,是整个训练数据集的平均损失。算法(模型)的优化策略在于最小化代价函数。

公式只写出损失函数的公式,代价函数只是在此基础上多了平均操作。代码包括损失函数和代价函数,主要使用Numpy。


在Keras API中,分为了三类:概率型、回归型、Hinge型,Keras API中,每个损失函数包括多个API,其中大写的表示类实现,小写的是函数实现

概率型损失函数:

  • BinaryCrossentropy
  • CategoricalCrossentropy
  • SparseCategoricalCrossentropy
  • Poisson
  • KLDivergence

回归型损失函数:

  • MeanSquaredError
  • MeanAbsoluteError
  • MeanAbsolutePercentageError
  • MeanSqauredLogarithmicError
  • CosineSimilarity
  • Huber
  • LogCosh

Hinge型损失:

  • Hinge
  • SquareHinge
  • CategoricalHinge

1. 平方损失

也称L2损失,多用于回归任务,其代价函数称为MSE(Mean Squared Error)。损失函数 - 图1表示真实值,损失函数 - 图2表示预测值,下同。

损失函数 - 图3%5E2%0A#card=math&code=L%20%3D%20%5Cfrac%7B1%7D%7B2%7D%28y%20-%20%5Chat%20y%29%5E2%0A)

  1. # 数据张量形式为(xxx, 1)
  2. loss = np.square(y - y_preds)
  3. cost = np.mean(loss)

2. 绝对损失

也称L1损失,其代价函数称之为MAE(Mean Absolute Error)。

损失函数 - 图4%5E2%0A#card=math&code=L%20%3D%20%28y-%5Chat%20y%29%5E2%0A)

loss = np.abs(y - y_preds)
cost = np.mean(loss)

3. Huber损失

结合了MSE和MAE,对于较小误差是二次的,对于略大的误差则是一次的,降低了对异常数据的敏感性。

损失函数 - 图5%5E2%2C%20%5Cquad%20%7Cy%20-%20%5Chat%20y%7C%20%5Cle%20%5Cdelta%5C%5C%5C%5C%0A%5Cdelta%7Cy%20-%20%5Chat%20y%7C%20-%20%5Cfrac%7B1%7D%7B2%7D%5Cdelta%5E2%2C%20%5Cquad%20others%0A%5Cend%7Bcases%7D%0A#card=math&code=L%20%3D%20%5Cbegin%7Bcases%7D%0A%5Cfrac%7B1%7D%7B2%7D%28y%20-%20%5Chat%20y%29%5E2%2C%20%5Cquad%20%7Cy%20-%20%5Chat%20y%7C%20%5Cle%20%5Cdelta%5C%5C%5C%5C%0A%5Cdelta%7Cy%20-%20%5Chat%20y%7C%20-%20%5Cfrac%7B1%7D%7B2%7D%5Cdelta%5E2%2C%20%5Cquad%20others%0A%5Cend%7Bcases%7D%0A)

ae = np.abs(y - y_preds)
huber_1 = np.square(ae) * 0.5
huber_2 = delta * ae - 0.5 * np.square(delta)

loss = np.where(ae <= delta, huber_1, huber_2)
cost = np.mean(loss)

4. 二元交叉熵

损失函数 - 图6%20-%20(1-y)%5Clog(1-%5Chat%20y)%0A#card=math&code=L%20%3D%20-y%5Clog%28%5Chat%20y%29%20-%20%281-y%29%5Clog%281-%5Chat%20y%29%0A)

# 防止y_preds = 0,导致log产生nan,用某一比较小的数代替0
# y_preds = np.where(y_preds > 0, y_preds, 1e-7)
loss = - y * np.log(y_preds) - (1 - y) * np.log(1 - y_preds)
cost = np.mean(loss)

5. Hinge损失

主要用于带有类-11标签的支持向量机(maximum-margin任务),首先得确保数据只取-11

损失函数 - 图7%0A#card=math&code=L%20%3D%20max%280%2C%201%20-%20y%2A%20%5Chat%20y%29%0A)

tmp = 1 - y * y_preds
loss = np.maximum(tmp, 0)
cost = np.mean(loss)

6. 多元交叉熵

在多分类问题中使用交叉熵,是二元交叉熵的拓展。

损失函数 - 图8%20%2B%20(1-y)%5Clog(1%20-%20%5Chat%20y)%5D%0A#card=math&code=L%20%3D%20-%5Csum_%7Bk%3D1%7D%5EK%20%5By%5Clog%28%5Chat%20y%29%20%2B%20%281-y%29%5Clog%281%20-%20%5Chat%20y%29%5D%0A)

有一个问题就是,实际使用中预测值每个样本数据是K维(如果有K类),但是通常真实值是1维。

tensorflow(keras)中如果预测值是概率的表示,标签值是类别表示,使用keras.losses.SparseCategoricalCrossentropy,如果标签值是ont-hot表示,使用keras.losses.CategoricalCrossentropy

Tensorflow中通过One-hot化标签值:

# Tensorflow
# 通过tf.clip_by_value截断值使得不存在为0的值,避免对log操作的影响
loss = - tf.math.log(tf.clip_by_value(preds, 1e-7, 1)) * tf.one_hot(y.ravel(), K)
loss = tf.reduce_sum(loss, axis=1)
cost = tf.reduce_mean(loss)
# 结果与`keras.losses.SparseCategoricalCrossentropy()(labels, preds)`完全相同。

Numpy中通过One-hot化标签值索引操作(fancy indexing)

# Numpy
# one-hot,此处0值处理未做
loss = np.log(y_preds) * np.eye(K)[y.ravel()]
loss = np.sum(loss, axis=1)
cost = np.mean(loss)

# fancy indexing
loss = np.log(y_preds)[np.range(y_preds.shape[0]), y.ravel()]
loss = np.sum(loss, axis=1)
cost = np.mean(loss)

补充:numpy心得

关于切片、索引(掩码)操作,有几个点之前卡了很久。

  1. 二维数组每行取特定的元素

在个人计算多元交叉熵时碰到的,preds(1000,7)是数组,每行元素是该样本对各类别的预测概率,labels(1000,1)的真实数据,要求多元交叉熵。想了两种思路:第一,在preds中按照labels对应的值进行采样,得到数组视图;第二,将labels进行One-Hot化,然后逐元素乘。

其中第一种思路需要的采样规则一时没想到,后来才试出来:preds[[],[]],如果对两个数组进行切片,数组长度需要相同,那么是逐元素进行取点,类似于:zip([]. []),那么代码如下:

cross_entropy = np.mean(np.log(preds(np.arange(preds.shape[0]), labels.ravel())), axis=0)

第二种思路中,One-Hot化查到了一个巧妙的方法,生成单位矩阵后再对其按labels值进行采样:

cross_entropy = np.mean(np.log(preds * np.eye(preds.shape[0])[labels.ravel()]))

其中有个问题,preds中可能有0的情况,进行log操作会产生无穷大,而我们希望使它为0

  1. 对于掩码切片

对numpy数组进行这样的切片:A[b]A, b都为数组,其中b长度和A相同

b形式:[0,1,1,0,...,1],切片结果长度和原数组相同,只是将部门元素变为0

b形式:[True,True, False,...False],切片结果长度与原数组不同,结果中只有b中为True位置对应的A的值。


7. KL散度

衡量两个概率分布之间的区别,KL散度为0表示两个分布相同,值越大。其中损失函数 - 图9#card=math&code=P%28x%29)为

损失函数 - 图10%20%3D%20%5Csum%20P(x)%20%5Clog%5Cfrac%7BP(x)%7D%7BQ(x)%7D%2C%20%5Cquad%20%E7%A6%BB%E6%95%A3%E6%83%85%E5%86%B5%5C%5C%5C%5C%0AKL(P%5CVert%20Q)%20%3D%20%5Cint%20P(x)%20%5Clog%5Cfrac%7BP(x)%7D%7BQ(x)%7Ddx%2C%20%5Cquad%20%E8%BF%9E%E7%BB%AD%E6%83%85%E5%86%B5%0A#card=math&code=KL%28P%5CVert%20Q%29%20%3D%20%5Csum%20P%28x%29%20%5Clog%5Cfrac%7BP%28x%29%7D%7BQ%28x%29%7D%2C%20%5Cquad%20%E7%A6%BB%E6%95%A3%E6%83%85%E5%86%B5%5C%5C%5C%5C%0AKL%28P%5CVert%20Q%29%20%3D%20%5Cint%20P%28x%29%20%5Clog%5Cfrac%7BP%28x%29%7D%7BQ%28x%29%7Ddx%2C%20%5Cquad%20%E8%BF%9E%E7%BB%AD%E6%83%85%E5%86%B5%0A)

keraskullback_leibler_divergencekl_divergence

loss = y * np.log(y / y_preds)
cost = np.sum(loss)

注:公式理解及推导

KL散度也称相对熵,自信息(损失函数 - 图11)在信息编码上表示的是对概率值为损失函数 - 图12的数据进行编码的最优编码长度,熵(损失函数 - 图13#card=math&code=H%28P%29))表示的是分布损失函数 - 图14最优编码的平均编码长度,交叉熵(损失函数 - 图15#card=math&code=H%28P%2C%20Q%29))表示用损失函数 - 图16的最优编码长度对损失函数 - 图17编码需要的平均长度,KL散度则表示:损失函数 - 图18的最优编码长度对损失函数 - 图19编码需要的平均长度损失函数 - 图20最优编码的平均编码长度。即:

损失函数 - 图21%20%3D%20H(P%2C%20Q)%20-%20H(P)%0A#card=math&code=KL%28P%5CVert%20Q%29%20%3D%20H%28P%2C%20Q%29%20-%20H%28P%29%0A)

结果就是KL散度的计算公式。KL散度是不对称的,所以有JS散度,是对称的衡量两分布的指标:

损失函数 - 图22%20%3D%20%5Cfrac%7B1%7D%7B2%7DKL(P%5CVert%20M)%20%2B%20%5Cfrac%7B1%7D%7B2%7DKL(Q%5CVert%20M)%0A#card=math&code=JS%28P%5CVert%20Q%29%20%3D%20%5Cfrac%7B1%7D%7B2%7DKL%28P%5CVert%20M%29%20%2B%20%5Cfrac%7B1%7D%7B2%7DKL%28Q%5CVert%20M%29%0A)

其中损失函数 - 图23是两分布的平均。

8. 余弦相似度

初高中内容

损失函数 - 图24

# numpy
l2_normed_y = y / np.sqrt(np.sum(y ** 2)
l2_normed_y_pred = y_pred / np.sqrt(np.sum(y_pred ** 2))                         
loss = l2_normed_y * l2_normed_y_pred

# tensorflow
y = tf.nn.l2_normalize(y)
y_pred = tf.nn.l2_normalize(y_pred)
loss = tf.reduce_sum(y * y_pred)

# tensorflow API
tf.keras.cosine_similarity(y, y_pred)

9. 绝对百分比损失

损失函数 - 图25

loss = np.abs((y - y_preds) / y_preds)
cost = np.mean(loss) * 100

# tf API
tf.losses.mape(y, y_preds)

10. 平方对数损失

损失函数 - 图26%20-%20log(%5Chat%20y%20%2B%201))%5E2%0A#card=math&code=L%20%3D%20%28%5Clog%28y%2B1%29%20-%20log%28%5Chat%20y%20%2B%201%29%29%5E2%0A)

loss = np.square(np.log(y + 1) - np.log(y_preds + 1))
cost = np.mean(loss)

# tf API
tf.losses.msle(y, y_preds)

11. Hinge损失

对输入值有要求,真实值需要是1-1

损失函数 - 图27%0A#card=math&code=L%20%3D%20%5Cmax%281%20-%20%5Chat%20y%20%2A%20y%2C%200%29%0A)

loss = np.maximum(1 - y_preds * y, 0)
cost = np.mean(loss)

# tf API
tf.losses.hinge(y, y_preds)

12. 平方Hinge损失

对输入值有要求,真实值需要是1-1,就是Hinge加上了一个平方操作。

损失函数 - 图28%5E2%0A#card=math&code=L%20%3D%20%5Cmax%281%20-%20%5Chat%20y%20%2A%20y%2C%200%29%5E2%0A)

loss = np.square(np.maximum(1 - y_preds * y, 0))
cost = np.mean(loss)

# tf API
tf.losses.squared_hinge(y, y_preds)

13. 多元Hinge损失

计算多元的hinge损失,类似于SparseCategoricalCrossentropy

neg = np.maximum((1 - y_preds) * y_preds)
pos = np.sum(y_preds * y)
loss = np.maximum(neg - pos + 1, 0)

# tf API
tf.losses.categorical_hinge(y, y_preds)

14.泊松损失

计算预测值和真实值之间的泊松损失。

损失函数 - 图29

loss = y_preds - y * np.log(y_preds)

# tf API
tf.losses.poisson(y, y_preds)

15. Logcosh

比L2更加平滑,实际用途用L2相同,更加平滑,对异常值敏感性下降。

损失函数 - 图30)%0A#card=math&code=L%20%3D%20%5Clog%20%28%5Ccosh%28%5Chat%20y%20-%20y%29%29%0A)

并且当损失函数 - 图31比较小时,损失函数 - 图32)%20%5Capprox%20x%5E2%2F2#card=math&code=%5Clog%28%5Ccosh%28x%29%29%20%5Capprox%20x%5E2%2F2),当损失函数 - 图33比较大时,损失函数 - 图34)%5Capprox%20%7Cx%7C-%5Clog2#card=math&code=%5Clog%28%5Ccosh%28x%29%29%5Capprox%20%7Cx%7C-%5Clog2),可用其近似计算

loss = np.log(np.cosh(y_preds - y))
'''
np.cosh(x) == (np.exp(x) + np.exp(-x)) / 2
'''
cost = np.mean(loss)

# tf API
tf.losses.log_cosh(y, y_preds)