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

时间:2018-08-22 23:26:32

标签: 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) )

对于一个标量列表,假设z = [x_1,x_2,x_3,...],并且假设我们事先不知道每个$ x_i $的符号,我们可以将上面的定义归纳并尝试: / p>

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

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

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

6 个答案:

答案 0 :(得分:3)

您是对的,可以使用np.where(相当于if的numpy来做得更好:

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

答案 1 :(得分: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]

答案 2 :(得分:0)

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,我不太明白为什么要用这种方式计算它,但是我知道这可能是您正在寻找的代码。希望能有所帮助。

答案 3 :(得分:0)

The accepted answer是正确的,但正如this comment所指出的那样,它计算两个分支,因此存在问题。

相反,您可能要使用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

答案 4 :(得分:0)

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

std::vector<std::pair<std::string, Team>> orderTeams2( const std::map<std::string, Team>& map) {
    std::vector<std::pair<std::string, Team>> vect = { map.begin(), map.end() };
    std::sort( vect.begin(), vect.end(), []( auto &left, auto &right ) {
        return left.second.points > right.second.points;
    });
    return vect;
}

答案 5 :(得分:0)

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

让我们从分析一些答案开始(仅限纯numpy答案):

@DYZ accepted answer

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

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 answer

这几乎是正确的,但是仅适用于浮点值,如下所示:

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^x将在x为正值时溢出
  • e^-xx为负值时将溢出

因此,当x等于零时,我们必须分支。使用numpy的遮罩,我们只能通过特定的S形变换来变换正或负数组的一部分。

请参见代码注释以获取其他要点:

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

时间测量

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

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)