15

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

def sigmoid(x):
    if x >= 0:
        return 1. / ( 1. + np.exp(-x) )
    else:
        return exp(x) / ( 1. + np.exp(x) )

对于一个标量列表,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

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

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

4

6 回答 6

10

np.where你是对的,你可以通过使用numpy来做的更好if

def sigmoid(x):
    return np.where(x >= 0, 
                    1 / (1 + np.exp(-x)), 
                    np.exp(x) / (1 + np.exp(x)))

这个函数接受一个 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])
于 2018-08-22T23:30:15.010 回答
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)

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

于 2019-07-24T08:29:42.733 回答
4

@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
    )

当两个分支都被评估时(它们是参数,它们必须是),第一个分支会给我们一个负值警告,第二个是正值。

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

缺点

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

@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

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

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

缺点

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

改进了@hao peng答案

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

乙状结肠

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

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

因此,我们必须在x等于零时进行分支。使用numpy's masking 我们可以仅转换数组中特定的 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
    result = np.empty_like(x)
    result[positive] = _positive_sigmoid(x[positive])
    result[negative] = _negative_sigmoid(x[negative])

    return result

时间测量

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

289.5070939064026 #DYZ
222.49267292022705 #ynn
230.81086134910583 #this

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

使用了以下代码:

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)
于 2020-11-06T15:55:01.430 回答
1

接受的答案是正确的,但正如该评论所指出的,它计算了两个分支,因此是有问题的。

相反,您可能想要使用np.piecewise(). 这更快,更有意义(np.where不是为了定义分段函数)并且没有由于进入两个分支而引起的误导性警告。

基准

源代码

import numpy as np
import time

N: int = int(1e+4)

np.random.seed(0)

x: np.ndarray = np.random.random((N, N))
x *= 1e+3

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

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

assert (np.array_equal(y1, y2))

结果

np.piecewise()是无声的,速度快了两倍!

test.py:12: RuntimeWarning: overflow encountered in exp
  y1 = np.where(x > 0, 1 / (1 + np.exp(-x)), np.exp(x) / (1 + np.exp(x)))
test.py:12: RuntimeWarning: invalid value encountered in true_divide
  y1 = np.where(x > 0, 1 / (1 + np.exp(-x)), np.exp(x) / (1 + np.exp(x)))

6.32736349105835
3.138420343399048
于 2020-07-12T10:54:29.197 回答
0

您的代码的另一种替代方法如下:

def sigmoid(z):
    return [(1. / (1. + np.exp(-x)) if x >= 0 else (np.exp(x) / (1. + np.exp(x))) for x in z]
于 2018-08-22T23:44:35.817 回答
0

我写了一个技巧,我猜 np.where 或 torch.where 以相同的方式实现以处理二进制条件:

def sigmoid(x, max_v=1.0):    
    sign = (torch.sign(x) + 3)//3
    x = torch.abs(x)
    res = max_v/(1 + torch.exp(-x))
    res = res * sign + (1 - sign) * (max_v - res)
    return res
于 2020-07-16T14:19:01.007 回答