在python中为列表定义数值稳定的sigmoid函数的最佳方法

Ran*_*ker 12 python sigmoid

对于标量变量x,我们知道如何在python中写出数值稳定的sigmoid函数:

def sigmoid(x):
    if x >= 0:
        return 1. / ( 1. + np.exp(-x) )
    else:
        return exp(x) / ( 1. + np.exp(x) )
Run Code Online (Sandbox Code Playgroud)

对于标量列表z = [x_1, x_2, x_3, ...],假设我们x_i事先不知道每个标量的符号,我们可以概括上面的定义并尝试:

def sigmoid(z):
    result = []
    for x in z:
        if x >= 0:
            result.append(1. / ( 1. + np.exp(-x) ) )
        else:
            result.append( exp(x) / ( 1. + np.exp(x) ) )
    return result
Run Code Online (Sandbox Code Playgroud)

这似乎有效。但是,我觉得这可能不是最 Pythonic 的方式。我应该如何改进“清洁”方面的定义?说,有没有办法使用理解来缩短函数定义?

如果有人问过这个,我很抱歉,因为我在 SO 上找不到类似的问题。非常感谢您的时间和帮助!

Szy*_*zke 11

@hao peng提供了完全正确的答案(没有警告),但解决方案没有清楚地解释。对于评论来说这太长了,所以我会去找答案。

我们先来分析几个答案(numpy仅限纯粹答案):

@DYZ 接受答案

这在数学上是正确的,但仍然给我们一个警告。我们看一下代码:

def sigmoid(x):
    return np.where(
            x >= 0, # condition
            1 / (1 + np.exp(-x)), # For positive values
            np.exp(x) / (1 + np.exp(x)) # For negative values
    )
Run Code Online (Sandbox Code Playgroud)

由于两个分支都被评估(它们是参数,它们必须是),第一个分支将向我们发出负值警告,第二个分支将向我们发出正值警告。

尽管会发出警告,但不会合并溢出结果,因此结果是正确的。

缺点

  • 对两个分支进行不必要的评估(所需操作数量的两倍)
  • 抛出警告

@ynn 回答

这几乎是正确的,仅适用于浮点值,请参见下文:

def sigmoid(x):
    return np.piecewise(
        x,
        [x > 0],
        [lambda i: 1 / (1 + np.exp(-i)), lambda i: np.exp(i) / (1 + np.exp(i))],
    )


sigmoid(np.array([0.0, 1.0]))  # [0.5 0.73105858] correct
sigmoid(np.array([0, 1]))  # [0, 0] incorrect
Run Code Online (Sandbox Code Playgroud)

为什么? @mhawke在另一个线程中提供了更长的答案,但要点是:

看来,piecewise() 将返回值转换为与输入相同的类型,因此,当输入整数时,会对结果执行整数转换,然后返回结果。

缺点

  • 由于分段函数的奇怪行为,没有自动转换

改进了@hao peng的答案

稳定 sigmoid 的想法来自于以下事实:

乙状结肠

exp如果编码正确(一次评估就足够了),这两个版本在操作方面同样有效。现在:

  • e^xx当为正数时会溢出
  • e^-xx为负数时会溢出

因此我们必须在x等于 0 的地方分支。使用numpy的掩码,我们可以通过特定的 sigmoid 实现仅转换数组的正数或负数部分。

其他要点请参阅代码注释:

def _positive_sigmoid(x):
    return 1 / (1 + np.exp(-x))


def _negative_sigmoid(x):
    # Cache exp so you won't have to calculate it twice
    exp = np.exp(x)
    return exp / (exp + 1)


def sigmoid(x):
    positive = x >= 0
    # Boolean array inversion is faster than another comparison
    negative = ~positive

    # empty contains junk hence will be faster to allocate
    # Zeros has to zero-out the array after allocation, no need for that
    # See comment to the answer when it comes to dtype
    result = np.empty_like(x, dtype=np.float)
    result[positive] = _positive_sigmoid(x[positive])
    result[negative] = _negative_sigmoid(x[negative])

    return result
Run Code Online (Sandbox Code Playgroud)

时间测量

结果(50次案例测试来自ynn):

289.5070939064026 #DYZ
222.49267292022705 #ynn
230.81086134910583 #this
Run Code Online (Sandbox Code Playgroud)

事实上,分段似乎更快(不确定原因,也许屏蔽和额外的屏蔽操作使它更慢)。

使用以下代码:

import time

import numpy as np


def _positive_sigmoid(x):
    return 1 / (1 + np.exp(-x))


def _negative_sigmoid(x):
    # Cache exp so you won't have to calculate it twice
    exp = np.exp(x)
    return exp / (exp + 1)


def sigmoid(x):
    positive = x >= 0
    # Boolean array inversion is faster than another comparison
    negative = ~positive

    # empty contains juke hence will be faster to allocate than zeros
    result = np.empty_like(x)
    result[positive] = _positive_sigmoid(x[positive])
    result[negative] = _negative_sigmoid(x[negative])

    return result


N = int(1e4)
x = np.random.uniform(size=(N, N))

start: float = time.time()
for _ in range(50):
    y1 = np.where(x > 0, 1 / (1 + np.exp(-x)), np.exp(x) / (1 + np.exp(x)))
    y1 += 1
end: float = time.time()
print(end - start)

start: float = time.time()
for _ in range(50):
    y2 = np.piecewise(
        x,
        [x > 0],
        [lambda i: 1 / (1 + np.exp(-i)), lambda i: np.exp(i) / (1 + np.exp(i))],
    )
    y2 += 1
end: float = time.time()
print(end - start)

start: float = time.time()
for _ in range(50):
    y2 = sigmoid(x)
    y2 += 1
end: float = time.time()
print(end - start)
Run Code Online (Sandbox Code Playgroud)


DYZ*_*DYZ 10

你是对的,你可以通过使用np.wherenumpy 等价物做得更好if

def sigmoid(x):
    return np.where(x >= 0, 
                    1 / (1 + np.exp(-x)), 
                    np.exp(x) / (1 + np.exp(x)))
Run Code Online (Sandbox Code Playgroud)

这个函数接受一个 numpy 数组x并返回一个 numpy 数组:

data = np.arange(-5,5)
sigmoid(data)
#array([0.00669285, 0.01798621, 0.04742587, 0.11920292, 0.26894142,
#       0.5       , 0.73105858, 0.88079708, 0.95257413, 0.98201379])
Run Code Online (Sandbox Code Playgroud)

  • 似乎“np.where”评估两个分支,然后选择它需要的一个,这会导致误导性的溢出警告。类似于“sigmoid(np.array(-300, np.float32))” (2认同)

小智 5

def sigmoid(x):
    """
    A numerically stable version of the logistic sigmoid function.
    """
    pos_mask = (x >= 0)
    neg_mask = (x < 0)
    z = np.zeros_like(x)
    z[pos_mask] = np.exp(-x[pos_mask])
    z[neg_mask] = np.exp(x[neg_mask])
    top = np.ones_like(x)
    top[neg_mask] = z[neg_mask]
    return top / (1 + z)
Run Code Online (Sandbox Code Playgroud)

这段代码来自cs231n的assignment3,我不太明白为什么要这样计算,但我知道这可能就是你要找的代码。希望有所帮助。