[技术干货] 技术干货 | 换个损失函数可较快达到收敛,基于MindSpore精准实现!

image.png

本文来源于:知乎

作者:李嘉琪

Error系列的指标及loss损失函数,该系列有:

  • 均方误差(Mean Square Error,MSE)
  • 平均绝对误差(Mean Absolute Error,MAE)
  • 均方根误差(Root Mean Square Error,RMSE)
  • 均方对数误差(Mean Squared Log Error)
  • 平均相对误差(Mean Relative Error,MAE)

今天为大家打包整理均方误差(Mean Square Error,MSE)、平均绝对误差(Mean Absolute Error,MAE)、均方根误差(Root Mean Square Error,RMSE)的原理介绍及MindSpore的实现代码。

image.png

均方误差指的就是模型预测值image.png与样本真实值 y 之间距离平方的平均值。其公式如下所示:image.png

其中,image.pngimage.png分别表示第image.png个样本的真实值和预测值,image.png为样本个数。

为了简化讨论,忽略下标image.pngimage.png,以image.png为横坐标,image.png为纵坐标,绘制其损失函数的图形:

image.png

image.png曲线的特点是光滑连续、可导,便于使用梯度下降算法,是比较常用的一种损失函数。而且,image.png随着误差的减小,梯度也在减小,这有利于函数的收敛,即使固定学习因子,函数也能较快取得最小值。


平方误差有个特性,就是当image.pngimage.png的差值大于 1 时,会增大其误差;当image.pngimage.png的差值小于 1 时,会减小其误差。这是由平方的特性决定的。也就是说,image.png会对误差较大(>1)的情况给予更大的惩罚,对误差较小(<1)的情况给予更小的惩罚。从训练的角度来看,模型会更加偏向于惩罚较大的点,赋予其更大的权重。
如果样本中存在离群点,image.png会给离群点赋予更高的权重,但是却是以牺牲其他正常数据点的预测效果为代价,这最终会降低模型的整体性能。我们来看一下使用image.png解决含有离群点的回归模型。(我们先用numpy来举一个例子,matplotlib画一下图来说明一下)


import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(1, 20, 40)
y = x + [np.random.choice(4) for _ in range(40)]
y[-5:] -= 8
X = np.vstack((np.ones_like(x),x))    # 引入常数项 1
m = X.shape[1]
# 参数初始化
W = np.zeros((1,2))
# 迭代训练
num_iter = 20
lr = 0.01
J = []
for i in range(num_iter):
   y_pred = W.dot(X)
   loss = 1/(2*m) * np.sum((y-y_pred)**2)
   J.append(loss)
   W = W + lr * 1/m * (y-y_pred).dot(X.T)
# 作图
y1 = W[0,0] + W[0,1]*1
y2 = W[0,0] + W[0,1]*20
plt.scatter(x, y)
plt.plot([1,20],[y1,y2])
plt.show()

拟合结果如下图所示:

image.png

可见,使用image.png损失函数,受离群点的影响较大,虽然样本中只有 5 个离群点,但是拟合的直线还是比较偏向于离群点。这往往是我们不希望看到的。

image.png


好了,原理已经讲完,话不多说,我们开始上代码。使用的是MindSpore框架实现的代码。
  • Mean Squared Error的Metric代码实现

"""Error."""
import numpy as np
from .metric import Metric
class MSE(Metric):
   
    def __init__(self):
        super(MSE, self).__init__()
        self.clear()
    def clear(self):
        """清除历史数据"""
        self._squared_error_sum = 0
        self._samples_num = 0
    def update(self, *inputs):
        # 校验输入的个数
        if len(inputs) != 2:
            raise ValueError('Mean squared error need 2 inputs (y_pred, y), but got {}'.format(len(inputs)))
        # 将输入统一转换为numpy
        y_pred = self._convert_data(inputs[0])
        y = self._convert_data(inputs[1])
        # 复现公式
        squared_error_sum = np.power(y.reshape(y_pred.shape) - y_pred, 2)
        # 多组数据进行累加
        self._squared_error_sum += squared_error_sum.sum()
        # 统计数据的个数,方便后面求均值
        self._samples_num += y.shape[0]
    def (self):
        """
        返回值是一个float的标量。
        """
        if self._samples_num == 0:
            raise RuntimeError('The number of input samples must not be 0.')
        return self._squared_error_sum / self._samples_num

使用方法如下:

import math
import numpy as np
from mindspore import Tensor
from mindspore.nn.metrics import MSE
def test_MSE():
    x = Tensor(np.array([0.1, 0.2, 0.6, 0.9]))
    y = Tensor(np.array([0.1, 0.25, 0.5, 0.9]))
    error = MSE()
    error.clear()
    error.update(x, y)
    result = error.()
    print()
test_MSE()
0.0125 / 4

Mean Squared Error的Loss代码实现

import mindspore
import mindspore.common.dtype as mstype
from mindspore.common.tensor import Tensor
from mindspore.common.parameter import Parameter
from mindspore.ops import operations as P
from mindspore.ops import functional as F
from mindspore import nn
class MSELoss(_Loss):
    def construct(self, base, target):
        x = F.square(base - target)
        return self.get_loss(x)

使用方法如下:


import numpy as np
from mindspore import nn
import mindspore
from mindspore import Tensor
loss = nn.MSELoss()
input_data = Tensor(np.array([1, 2, 3]), mindspore.float32)
target_data = Tensor(np.array([1, 2, 2]), mindspore.float32)
output = loss(input_data, target_data)
print(output)
0.33333334

image.png


平均绝对误差指的就是模型预测值 f(x) 与样本真实值 y 之间距离的平均值。其公式如下所示:


  image.png
为了简化讨论,忽略下标image.pngimage.png,以image.png为横坐标,MAE 为纵坐标,绘制其损失函数的图形:


image.png


直观上来看,MAE 的曲线呈 V 字型,连续但在 y-f(x)=0 处不可导,计算机求解导数比较困难。而且 MAE 大部分情况下梯度都是相等的,这意味着即使对于小的损失值,其梯度也是大的。这不利于函数的收敛和模型的学习。


值得一提的是,MAE 相比 MSE 有个优点就是 MAE 对离群点不那么敏感,更有包容性。因为 MAE 计算的是误差 y-f(x) 的绝对值,无论是 y-f(x)>1 还是 y-f(x)<1,没有平方项的作用,惩罚力度都是一样的,所占权重一样。针对 MSE 中的例子,我们来使用 MAE 进行求解,看下拟合直线有什么不同。(我们先用numpy举一下例子。)


X = np.vstack((np.ones_like(x),x))    # 引常数项 1
m = X.shape[1]
# 参数初始化
W = np.zeros((1,2))
# 迭代训练
num_iter = 20
lr = 0.01
J = []
for i in range(num_iter):
   y_pred = W.dot(X)
   loss = 1/m * np.sum(np.abs(y-y_pred))
   J.append(loss)
   mask = (y-y_pred).copy()
   mask[y-y_pred > 0] = 1
   mask[mask <= 0] = -1
   W = W + lr * 1/m * mask.dot(X.T)
# 作图
y1 = W[0,0] + W[0,1]*1
y2 = W[0,0] + W[0,1]*20
plt.scatter(x, y)
plt.plot([1,20],[y1,y2],'r--')
plt.xlabel('x')
plt.ylabel('y')
plt.('MAE')
plt.show()

注意上述代码中对 MAE 计算梯度的部分。拟合结果如下图所示:

image.png

显然,使用 MAE 损失函数,受离群点的影响较小,拟合直线能够较好地表征正常数据的分布情况。这一点,MAE 要优于 MSE。二者的对比图如下:

image.png


选择 MSE 还是 MAE 呢?


实际应用中,我们应该选择 MSE 还是 MAE 呢?从计算机求解梯度的复杂度来说,MSE 要优于 MAE,而且梯度也是动态变化的,能较快准确达到收敛。但是从离群点角度来看,如果离群点是实际数据或重要数据,而且是应该被检测到的异常值,那么我们应该使用MSE。另一方面,离群点仅仅代表数据损坏或者错误采样,无须给予过多关注,那么我们应该选择MAE作为损失


image.png


好了,原理已经讲完,话不多说,我们开始上代码。使用的是MindSpore框架实现的代码。
  • Mean Absolute Error的Metric代码实现

"""Error."""
import numpy as np
from .metric import Metric
class MAE(Metric):
    
    def __init__(self):
        super(MAE, self).__init__()
        self.clear()
    def clear(self):
        """清除历史数据"""
        self._abs_error_sum = 0
        self._samples_num = 0
    def update(self, *inputs):
        # 检验输入个数
        if len(inputs) != 2:
            raise ValueError('Mean absolute error need 2 inputs (y_pred, y), but got {}'.format(len(inputs)))
        y_pred = self._convert_data(inputs[0])
        y = self._convert_data(inputs[1])
        # 复现公式计算
        abs_error_sum = np.abs(y.reshape(y_pred.shape) - y_pred)
        # 多组数据进行累加
        self._abs_error_sum += abs_error_sum.sum()
        # 统计数据的个数,方便后面求均值
        self._samples_num += y.shape[0]
    def (self):
        """
        返回值是一个float的标量。
        """
        if self._samples_num == 0:
            raise RuntimeError('Total samples num must not be 0.')
        return self._abs_error_sum / self._samples_num

使用方法如下:


import numpy as np
import mindspore
from mindspore import Tensor
from mindspore.nn.metrics import MAE
x = Tensor(np.array([0.1, 0.2, 0.6, 0.9]), mindspore.float32)
y = Tensor(np.array([0.1, 0.25, 0.7, 0.9]), mindspore.float32)
error = MAE()
error.clear()
error.update(x, y)
result = error.()
print(result)
0.037499990314245224

每个batch(比如两组数据)进行计算的时候如下:


import numpy as np
from mindspore import Tensor
from mindspore.nn.metrics import MAE
error = MAE()
error.clear()
x = Tensor(np.array([0.1, 0.2, 0.6, 0.9]))
y = Tensor(np.array([0.1, 0.25, 0.7, 0.9]))
error.update(x, y)
x1 = Tensor(np.array([0.1, 0.2, 0.6, 0.9]))
y1 = Tensor(np.array([0.1, 0.25, 0.7, 0.9]))
error.update(x1, y1)
result = metric.()
print(result)
Mean Squared Error的Loss代码实现
import mindspore
import mindspore.common.dtype as mstype
from mindspore.common.tensor import Tensor
from mindspore.common.parameter import Parameter
from mindspore.ops import operations as P
from mindspore.ops import functional as F
from mindspore import nn
class MAELoss(_Loss):
    def construct(self, logits, label):
        _check_shape(logits.shape, label.shape)
        x = F.absolute(logits - label)
        return self.get_loss(x)

使用方法如下:


import numpy as np
from mindspore import nn
import mindspore
from mindspore import Tensor
loss = nn.MAELoss()
input_data = Tensor(np.array([1, 2, 3]), mindspore.float32)
target_data = Tensor(np.array([1, 2, 2]), mindspore.float32)
output = loss(input_data, target_data)
print(output)
0.33333334

image.png

均方根误差指的就是模型预测值 f(x) 与样本真实值 y 之间距离平方的平均值,取结果后再开方。其公式如下所示:


  image.png
其中,image.pngimage.png分别表示第image.png个样本的真实值和预测值,image.png为样本个数。从公式中看出,RMSE的结果是基于MSE的(可以参考上方对MSE的介绍)。


image.png

我们来看一下代码,代码比较简单,在此我们没有metric的实现,实现的是loss的代码。
  • Root Mean Squared Error的Loss代码实现

import mindspore
import mindspore.common.dtype as mstype
from mindspore.common.tensor import Tensor
from mindspore.common.parameter import Parameter
from mindspore.ops import operations as P
from mindspore.ops import functional as F
from mindspore import nn
class RMSELoss(_Loss):
    def __init__(self):
        super(RMSELoss, self).__init__()
        # 结果是基于MSE的
        self.MSELoss = MSELoss()
    def construct(self, logits, label):
        # 校验输入的维度
        _check_shape(logits.shape, label.shape)
        # 基于MSE的结果进行开方
        rmse_loss = F.sqrt(self.MSELoss(logits, label))
        return rmse_loss

使用方法如下:

import numpy as np
from mindspore import nn
import mindspore
from mindspore import Tensor
loss = nn.RMSELoss()
input_data = Tensor(np.array([1, 2, 3]), mindspore.float32)
target_data = Tensor(np.array([1, 2, 2]), mindspore.float32)
output = loss(input_data, target_data)
print(output)
0.57735026

image.png

image.png


MindSpore官方资料
GitHub : https://github.com/mindspore-ai/mindspore
Gitee:https : //gitee.com/mindspore/mindspore
官方QQ群 : 871543426


长按下方二维码加入MindSpore项目↓


image.png