0
点赞
收藏
分享

微信扫一扫

从零实现深度学习框架——逻辑回归中的数值稳定


引言

本着“凡我不能创造的,我就不能理解”的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。

要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽量不适用外部完备的框架前提下,实现我们想要的模型。本系列​文章的宗旨就是通过这样的过程,让大家切实掌握深度学习底层实现,而不是仅做一个调包侠。

微信公众号:JavaNLP

本文我们来探讨一下逻辑回归中的数值稳定问题,所谓数值稳定即不会出现数值溢出(上溢或下溢)的问题。常见的表现是除从零实现深度学习框架——逻辑回归中的数值稳定_人工智能,或返回​​​nan​​。

上溢和下溢

我们在计算机中表示实数时,几乎总会引入一些近似误差。当许多操作复合时,即使是理论上可行的算法,在实践时也可能回导致算法失效。

一种极具毁灭性的舍入误差是下溢(underflow)。当接近零的数被四舍五入为零时发生下溢。我们通常要避免被零除或避免取零的对数。

令一种数值错误形式是上溢(overflow)。当大量级的数被近似为无穷大或负无穷大时发生上溢。

Sigmoid函数稳定性问题

我们知道Sigmoid函数公式为:
从零实现深度学习框架——逻辑回归中的数值稳定_深度学习_02
对应的图像如下:

从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_03

其中包含一个从零实现深度学习框架——逻辑回归中的数值稳定_人工智能_04,我们看一下从零实现深度学习框架——逻辑回归中的数值稳定_git_05的图像:

从零实现深度学习框架——逻辑回归中的数值稳定_人工智能_06

从上图可以看出,如果从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_07很大,从零实现深度学习框架——逻辑回归中的数值稳定_git_05会非常大,而很小就没事(不会溢出),变成无限接近从零实现深度学习框架——逻辑回归中的数值稳定_人工智能

当Sigmoid函数中的从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_07负的特别多,那么从零实现深度学习框架——逻辑回归中的数值稳定_人工智能_04就会变成从零实现深度学习框架——逻辑回归中的数值稳定_深度学习_12,就出现了上溢;

那么如何解决这个问题呢?从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_13可以表示成两种形式:
从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_14
从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_15时,我们根据从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_16的图像,我们取从零实现深度学习框架——逻辑回归中的数值稳定_深度学习_17的形式;

从零实现深度学习框架——逻辑回归中的数值稳定_git_18时,我们取从零实现深度学习框架——逻辑回归中的数值稳定_深度学习_19

# 原来的做法
def sigmoid_naive(x):
return 1 / (1 + math.exp(-x))

# 优化后的做法
def sigmoid(x):
if x < 0:
return math.exp(x) / (1 + math.exp(x))
else:
return 1 / (1 + math.exp(-x))

然后用不同的数值进行测试:

> sigmoid_naive(2000)
1.0
> sigmoid(2000)
1.0
> sigmoid_naive(-2000)
OverflowError: math range error
> sigmoid(-2000)
0.0

如果传入​​-2000​​,普通的实现会出现溢出,而优化后的版本不会。

但是这里的实现包含了​​if​​判断,同时只判断了一个标量而不是向量。

有一种更好的方法是,我们的逻辑回归只计算出logit,然后将logit传入损失函数,这里的logit说的是逻辑回归中线性变换的输出(加权和加上偏置)。

数值稳定的BCE损失

在pytorch的github中,有一段代码 ​​https://github.com/pytorch/pytorch/issues/751​​,

class StableBCELoss(nn.modules.Module):
def __init__(self):
super(StableBCELoss, self).__init__()
def forward(self, input, target):
neg_abs = - input.abs()
loss = input.clamp(min=0) - input * target + (1 + neg_abs.exp()).log()
return loss.mean()

笔者又在一篇文章[^1]中找到了对代码进行的解释,如下图:

从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_20

我们先来分析下为什么是数值稳定的。

import numpy as np

# 先定义一个数值稳定的sigmoid
def sigmoid(x):
if x < 0:
return np.exp(x) / (1 + np.exp(x))
else:
return 1 / (1 + np.exp(-x))

# 逻辑回归损失的常规实现
def bce_loss_naive(y, z):
return -y * np.log(sigmoid(z)) - (1-y) * np.log(1 - sigmoid(z))

# 数值稳定版逻辑回归损失
def bce_loss(y, z):
neg_abs = - np.abs(z)
return np.clip(z, a_min=0,a_max=None) - y * z + np.log(1 + np.exp(neg_abs))

接下来我们进行测试,假设从零实现深度学习框架——逻辑回归中的数值稳定_深度学习_21是一个较大的数,比如从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_22,我们知道从零实现深度学习框架——逻辑回归中的数值稳定_git_23会输出从零实现深度学习框架——逻辑回归中的数值稳定_人工智能_24,那么从零实现深度学习框架——逻辑回归中的数值稳定_数值稳定_25应该为从零实现深度学习框架——逻辑回归中的数值稳定_人工智能

> z = 2000
> y = 1
> sigmoid(z)
1.0
> bce_loss(y, z) # 数值稳定版本的
0.0
> bce_loss_naive(y, z)
/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:2: RuntimeWarning: divide by zero encountered in log
/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:2: RuntimeWarning: invalid value encountered in

数值稳定版本的还是稳!常规实现就会碰到​​nan​​了。

我们在假设从零实现深度学习框架——逻辑回归中的数值稳定_深度学习_21是一个负的较大的数,比如从零实现深度学习框架——逻辑回归中的数值稳定_人工智能_28,那么从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_29,即从零实现深度学习框架——逻辑回归中的数值稳定_深度学习_30也应该为从零实现深度学习框架——逻辑回归中的数值稳定_人工智能

> z = -2000
> y = 0
> sigmoid(z)
0.0
> bce_loss(y, z)
0.0
> bce_loss_naive(y, z)
/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:2: RuntimeWarning: divide by zero encountered in log
/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:2: RuntimeWarning: invalid value encountered in

嗯,看起来不错。下面我们仔细分析下这段代码的正确性,没问题我们才可以放心地实现。

我们把从零实现深度学习框架——逻辑回归中的数值稳定_git_32​带入逻辑回归的损失函数中得:
从零实现深度学习框架——逻辑回归中的数值稳定_git_33
就是说如果logit,也就是从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_34,那么代码实现中的损失就变成了从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_35。我们来验证一下。

这里分为两种情况,分别是真实标签从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_36从零实现深度学习框架——逻辑回归中的数值稳定_数值稳定_37

(1) 当从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_34从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_36时,此时代码计算的是

从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_40

公式从零实现深度学习框架——逻辑回归中的数值稳定_深度学习_41计算为从零实现深度学习框架——逻辑回归中的数值稳定_git_42,结果是一样的。

(2)当从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_34从零实现深度学习框架——逻辑回归中的数值稳定_数值稳定_37时,代码计算的是
从零实现深度学习框架——逻辑回归中的数值稳定_数值稳定_45
公式从零实现深度学习框架——逻辑回归中的数值稳定_深度学习_41计算的是从零实现深度学习框架——逻辑回归中的数值稳定_人工智能_47,结果也是一样的。

如果从零实现深度学习框架——逻辑回归中的数值稳定_人工智能_48,那么代码实现中的损失就变成了从零实现深度学习框架——逻辑回归中的数值稳定_数值稳定_49,我们也来验证一下。

(3)当从零实现深度学习框架——逻辑回归中的数值稳定_人工智能_48从零实现深度学习框架——逻辑回归中的数值稳定_数值稳定_37时,代码计算的是
从零实现深度学习框架——逻辑回归中的数值稳定_git_52
公式从零实现深度学习框架——逻辑回归中的数值稳定_深度学习_41计算的是从零实现深度学习框架——逻辑回归中的数值稳定_人工智能_47,结果也是一样的。

(4)当从零实现深度学习框架——逻辑回归中的数值稳定_git_55从零实现深度学习框架——逻辑回归中的数值稳定_逻辑回归_36时,代码计算的是
从零实现深度学习框架——逻辑回归中的数值稳定_深度学习_57
公式从零实现深度学习框架——逻辑回归中的数值稳定_深度学习_41计算为从零实现深度学习框架——逻辑回归中的数值稳定_git_42,结果也是一样的。

我们证明了这种代码实现的正确性。

下面就可以为我们的metagrad实现数值稳定版的BCE损失了。

等等,还需要先实现两个函数:​​abs​​​和​​clip​​。

实现Clip操作

​clip()​​像个夹子,把Tensor中的值限制在最小值和最大值之间。

class Clip(_Function):
def forward(ctx, x: ndarray, x_min=None, x_max=None) -> ndarray:
if x_min is None:
x_min = np.min(x)
if x_max is None:
x_max = np.max(x)

ctx.save_for_backward(x, x_min, x_max)
return np.clip(x, x_min, x_max)

def backward(ctx, grad: ndarray) -> ndarray:
x, x_min, x_max = ctx.saved_tensors
mask = (x >= x_min) * (x <= x_max)
return grad *

只有在​​[x_min,x_max]​​之间的元素才有梯度。

实现Abs操作

​abs()​​即求绝对值,图像如下:

从零实现深度学习框架——逻辑回归中的数值稳定_git_60

我们知道,按照定义绝对值在从零实现深度学习框架——逻辑回归中的数值稳定_人工智能处是不可导的
从零实现深度学习框架——逻辑回归中的数值稳定_数值稳定_62
因为除从零实现深度学习框架——逻辑回归中的数值稳定_人工智能是无意义的,但是我们和PyTorch的做法一致,当从零实现深度学习框架——逻辑回归中的数值稳定_人工智能_64时,令其导数也为从零实现深度学习框架——逻辑回归中的数值稳定_人工智能

class Abs(_Function):
def forward(ctx, x: ndarray) -> ndarray:
ctx.save_for_backward(x)
return np.abs(x)

def backward(ctx, grad: ndarray) -> ndarray:
x, = ctx.saved_tensors
# x中元素为0的位置,返回0
# 否则返回+1/-1
return grad * np.where(x == 0, 0, x / np.abs(x))

实现稳定版BCE损失

现在实现起来就很顺畅:

def binary_cross_entropy(input: Tensor, target: Tensor, reduction: str = "mean") -> Tensor:
'''

:param input: logits
:param target: 真实标签 0或1
:param reduction: binary cross entropy loss
:return:
'''

neg_abs = - abs(input)
errors = input.clip(x_min=0) - input * target + (1 + neg_abs.exp()).log()

N = len(target)

if reduction == "mean":
loss = errors.sum() / N
elif reduction == "sum":
loss = errors.sum()
else:
loss = errors
return

当然,为了稳妥起见,我们写一个测试用例:

def test_binary_cross_entropy():
N = 10
x = np.random.randn(N)
y = np.random.randint(0, 1, (N,))

mx = Tensor(x, requires_grad=True)

tx = torch.tensor(x, dtype=torch.float32, requires_grad=True)
ty = torch.tensor(y, dtype=torch.float32)

mo = torch.binary_cross_entropy_with_logits(tx, ty).mean()
to = F.binary_cross_entropy(mx, y)

assert np.allclose(mo.data,
to.numpy())

mo.backward()
to.backward()

assert np.allclose(mx.grad.data,
tx.grad.numpy())

确保它是通过的:

============================= test session starts =============================
collecting ... collected 1 item

test_cross_entropy.py::test_binary_cross_entropy PASSED [100%]

======================== 1 passed, 1 warning in 0.48s =========================

BCE损失的实现类实际上我们不需要改:

class BCELoss(_Loss):
def __init__(self, reduction: str = "mean") -> None:
super().__init__(reduction)

def forward(self, input: Tensor, target: Tensor) -> Tensor:
'''

:param input: logits
:param target: 真实标签 0或1
:return:
'''
return F.binary_cross_entropy(input, target, self.reduction)

其实之前的逻辑回归实现偶尔会遇到下面这个问题,其实就是数值稳定问题。

~/metagrad/examples/logistic_regression.py:43: FutureWarning: Support for multi-dimensional indexing (e.g. `obj[:, None]`) is deprecated and will be removed in a future version.  Convert to a numpy array before indexing instead.
y = y[:, np.newaxis]
0%| | 0/200000 [00:00<?, ?it/s]
~/metagrad/metagrad/ops.py:214: RuntimeWarning: divide by zero encountered in log
return np.log(x)
~/metagrad/metagrad/ops.py:119: RuntimeWarning: invalid value encountered in multiply
return x * y
~/metagrad/metagrad/ops.py:218: RuntimeWarning: divide by zero encountered in true_divide
return grad / x
~/metagrad/metagrad/ops.py:218: RuntimeWarning: invalid value encountered in

我们把模型的初始化权重打印出来,以便重现这个错误:

# print(model.linear.weight) Tensor([[-0.29942604  0.78735491]], requires_grad=True) 有问题的权重
# 使用之前有问题的权重
model.linear.weight.assign([[-0.29942604, 0.78735491]])
print(f"using weight: {model.linear.weight}")

然后再次训练:

([[-0.29942605  0.7873549 ]], requires_grad=True)
5%|▌ | 10026/200000 [00:05<01:45, 1796.27it/s]Train - Loss: 0.6216351389884949. Accuracy: 69.6969696969697

10%|█ | 20023/200000 [00:11<01:39, 1810.46it/s]Train - Loss: 0.6169218420982361. Accuracy: 71.71717171717172

15%|█▍ | 29933/200000 [00:16<01:33, 1812.38it/s]Train - Loss: 0.6122889518737793. Accuracy: 74.74747474747475

20%|██ | 40009/200000 [00:22<01:27, 1823.05it/s]Train - Loss: 0.6077350378036499. Accuracy: 79.79797979797979

25%|██▌ | 50028/200000 [00:27<01:23, 1804.55it/s]Train - Loss: 0.6032587885856628. Accuracy: 81.81818181818181

30%|██▉ | 59893/200000 [00:33<01:17, 1804.93it/s]Train - Loss: 0.5988588929176331. Accuracy: 81.81818181818181

35%|███▍ | 69928/200000 [00:38<01:11, 1828.28it/s]Train - Loss: 0.5945340394973755. Accuracy: 82.82828282828282

40%|████ | 80000/200000 [00:44<01:06, 1811.00it/s]Train - Loss: 0.5902827978134155. Accuracy: 83.83838383838383

45%|████▌ | 90037/200000 [00:50<01:01, 1782.85it/s]Train - Loss: 0.5861039757728577. Accuracy: 85.85858585858585

50%|████▉ | 99996/200000 [00:55<00:55, 1789.86it/s]Train - Loss: 0.5819962620735168. Accuracy: 86.86868686868686

55%|█████▍ | 109880/200000 [01:01<00:50, 1772.11it/s]Train - Loss: 0.577958345413208. Accuracy: 86.86868686868686

60%|█████▉ | 119899/200000 [01:06<00:45, 1760.05it/s]Train - Loss: 0.5739889144897461. Accuracy: 87.87878787878788

65%|██████▍ | 129886/200000 [01:12<00:40, 1752.00it/s]Train - Loss: 0.5700867176055908. Accuracy: 87.87878787878788

70%|██████▉ | 139954/200000 [01:18<00:33, 1791.69it/s]Train - Loss: 0.5662506222724915. Accuracy: 88.88888888888889

75%|███████▍ | 149921/200000 [01:24<00:31, 1572.65it/s]Train - Loss: 0.5624792575836182. Accuracy: 89.8989898989899

80%|████████ | 160025/200000 [01:30<00:22, 1805.44it/s]Train - Loss: 0.5587714314460754. Accuracy: 91.91919191919192

85%|████████▍ | 169954/200000 [01:35<00:16, 1778.81it/s]Train - Loss: 0.5551260113716125. Accuracy: 90.9090909090909

90%|████████▉ | 179929/200000 [01:41<00:11, 1777.69it/s]Train - Loss: 0.551541805267334. Accuracy: 91.91919191919192

95%|█████████▍| 189974/200000 [01:47<00:05, 1825.03it/s]Train - Loss: 0.5480176210403442. Accuracy: 90.9090909090909

100%|██████████| 200000/200000 [01:52<00:00, 1774.94it/s]
Train - Loss: 0.544552206993103. Accuracy: 90.9090909090909

这次没有问题了。

总结

本文我们实现了数值稳定的逻辑回归损失,下篇文章我们来实现更常用的数值稳定版Softmax回归损失。

References

  1. How do Tensorflow and Keras implement Binary Classification and the Binary Cross-Entropy function?


举报

相关推荐

0 条评论