algorithm - networks - Algoritmo sigmoide rápido
Es mejor medir primero en su hardware. Solo un script prueba rápido muestra que en mi máquina 1/(1+|x|)
es la más rápida, y tanh(x)
es la segunda más cercana. La función de error erf
es bastante rápida.
% gcc -Wall -O2 -lm -o sigmoid-bench{,.c} -std=c99 && ./sigmoid-bench
atan(pi*x/2)*2/pi 24.1 ns
atan(x) 23.0 ns
1/(1+exp(-x)) 20.4 ns
1/sqrt(1+x^2) 13.4 ns
erf(sqrt(pi)*x/2) 6.7 ns
tanh(x) 5.5 ns
x/(1+|x|) 5.5 ns
Espero que los resultados varíen dependiendo de la arquitectura y el compilador utilizado, pero es probable que erf(x)
(desde C99), tanh(x)
y x/(1.0+fabs(x))
sean los de desempeño rápido.
Esta respuesta probablemente no sea relevante para la mayoría de los casos, pero solo quería decir que para la computación CUDA he encontrado que x/sqrt(1+x^2)
es la función más rápida.
Por ejemplo, hecho con intrínsecos de flotador de precisión simple:
__device__ void fooCudaKernel(/* some arguments */) {
float foo, sigmoid;
// some code defining foo
sigmoid = __fmul_rz(rsqrtf(__fmaf_rz(foo,foo,1)),foo);
}
La función tanh puede optimizarse en algunos idiomas, lo que la hace más rápida que un x / (1 + abs (x) definido de forma personalizada, tal es el caso de Julia.
La gente aquí está más preocupada por la rapidez con la que una función se relaciona con otra y crea un micro benchmark para ver si f1(x)
ejecuta 0.0001 ms más rápido que f2(x)
. El gran problema es que esto es en su mayoría irrelevante, porque lo que importa es qué tan rápido aprende su red con su función de activación, tratando de minimizar su función de costo.
A partir de la teoría actual, función rectificadora y softplus.
en comparación con la función sigmoide o funciones de activación similares, permiten un entrenamiento más rápido y eficaz de arquitecturas neuronales profundas en conjuntos de datos grandes y complejos.
Por lo tanto, sugiero desechar la microoptimización y echar un vistazo a qué función permite un aprendizaje más rápido (también teniendo en cuenta otras funciones de costo).
No creo que puedas hacerlo mejor que el exp () incorporado, pero si quieres otro enfoque, puedes usar la expansión de la serie. WolframAlpha puede calcularlo por ti.
Para hacer el NN más flexible, usualmente se usa cierta tasa alfa para cambiar el ángulo del gráfico alrededor de 0.
La función sigmoide se ve como:
f(x) = 1 / ( 1+exp(-x*alpha))
El casi equivalente, (pero la función más rápida) es:
f(x) = 0.5 * (x * alpha / (1 + abs(x*alpha))) + 0.5
Puedes consultar los gráficos here
Cuando uso la función abs, la red se vuelve más rápida 100 veces.
Puedes usar un método simple pero efectivo usando dos fórmulas:
if x < 0 then f(x) = 1 / (0.5/(1+(x^2)))
if x > 0 then f(x) = 1 / (-0.5/(1+(x^2)))+1
Esto se verá así:
Dos gráficas para un sigmoide {Azul: (0.5 / (1+ (x ^ 2)), Amarillo: (-0.5 / (1+ (x ^ 2))) + 1}
También puedes usar una versión aproximada de sigmoide (no difiere más del 0.2% del original):
inline float RoughSigmoid(float value)
{
float x = ::abs(value);
float x2 = x*x;
float e = 1.0f + x + x2*0.555f + x2*x2*0.143f;
return 1.0f / (1.0f + (value > 0 ? 1.0f / e : e));
}
void RoughSigmoid(const float * src, size_t size, const float * slope, float * dst)
{
float s = slope[0];
for (size_t i = 0; i < size; ++i)
dst[i] = RoughSigmoid(src[i] * s);
}
Optimización de la función RoughSigmoid con el uso de SSE:
#include <xmmintrin.h>
void RoughSigmoid(const float * src, size_t size, const float * slope, float * dst)
{
size_t alignedSize = size/4*4;
__m128 _slope = _mm_set1_ps(*slope);
__m128 _0 = _mm_set1_ps(-0.0f);
__m128 _1 = _mm_set1_ps(1.0f);
__m128 _0555 = _mm_set1_ps(0.555f);
__m128 _0143 = _mm_set1_ps(0.143f);
size_t i = 0;
for (; i < alignedSize; i += 4)
{
__m128 _src = _mm_loadu_ps(src + i);
__m128 x = _mm_andnot_ps(_0, _mm_mul_ps(_src, _slope));
__m128 x2 = _mm_mul_ps(x, x);
__m128 x4 = _mm_mul_ps(x2, x2);
__m128 series = _mm_add_ps(_mm_add_ps(_1, x), _mm_add_ps(_mm_mul_ps(x2, _0555), _mm_mul_ps(x4, _0143)));
__m128 mask = _mm_cmpgt_ps(_src, _0);
__m128 exp = _mm_or_ps(_mm_and_ps(_mm_rcp_ps(series), mask), _mm_andnot_ps(mask, series));
__m128 sigmoid = _mm_rcp_ps(_mm_add_ps(_1, exp));
_mm_storeu_ps(dst + i, sigmoid);
}
for (; i < size; ++i)
dst[i] = RoughSigmoid(src[i] * slope[0]);
}
Optimización de la función RoughSigmoid con el uso de AVX:
#include <immintrin.h>
void RoughSigmoid(const float * src, size_t size, const float * slope, float * dst)
{
size_t alignedSize = size/8*8;
__m256 _slope = _mm256_set1_ps(*slope);
__m256 _0 = _mm256_set1_ps(-0.0f);
__m256 _1 = _mm256_set1_ps(1.0f);
__m256 _0555 = _mm256_set1_ps(0.555f);
__m256 _0143 = _mm256_set1_ps(0.143f);
size_t i = 0;
for (; i < alignedSize; i += 8)
{
__m256 _src = _mm256_loadu_ps(src + i);
__m256 x = _mm256_andnot_ps(_0, _mm256_mul_ps(_src, _slope));
__m256 x2 = _mm256_mul_ps(x, x);
__m256 x4 = _mm256_mul_ps(x2, x2);
__m256 series = _mm256_add_ps(_mm256_add_ps(_1, x), _mm256_add_ps(_mm256_mul_ps(x2, _0555), _mm256_mul_ps(x4, _0143)));
__m256 mask = _mm256_cmp_ps(_src, _0, _CMP_GT_OS);
__m256 exp = _mm256_or_ps(_mm256_and_ps(_mm256_rcp_ps(series), mask), _mm256_andnot_ps(mask, series));
__m256 sigmoid = _mm256_rcp_ps(_mm256_add_ps(_1, exp));
_mm256_storeu_ps(dst + i, sigmoid);
}
for (; i < size; ++i)
dst[i] = RoughSigmoid(src[i] * slope[0]);
}
Usando Eureqa para buscar aproximaciones a sigmoides, encontré que 1/(1 + 0.3678749025^x)
aproxima. Está bastante cerca, simplemente se deshace de una operación con la negación de x.
Algunas de las otras funciones que se muestran aquí son interesantes, pero ¿es realmente lenta la operación de potencia? Lo probé y en realidad lo hice más rápido que la adición, pero eso podría ser una casualidad. Si es así, debería ser tan rápido o más rápido que todos los demás.
EDITAR: 0.5 + 0.5*tanh(0.5*x)
y menos preciso, 0.5 + 0.5*tanh(n)
también funciona. Y podría simplemente deshacerse de las constantes si no le importa ubicarse entre el rango [0,1] como sigmoide. Pero asume que Tanh es más rápido.
no tiene que usar la función sigmoide exacta y real en un algoritmo de red neuronal, pero puede reemplazarla con una versión aproximada que tenga propiedades similares pero que sea más rápida que el cálculo.
Por ejemplo, puedes usar la función "fast sigmoid"
f(x) = x / (1 + abs(x))
El uso de los primeros términos de la expansión de la serie para exp (x) no ayudará demasiado si los argumentos de f (x) no están cerca de cero, y tiene el mismo problema con una expansión de la serie de la función sigmoide si los argumentos son " grande".
Una alternativa es usar la búsqueda de tablas. Es decir, se precalculan los valores de la función sigmoide para un número dado de puntos de datos y luego se realiza una interpolación rápida (lineal) entre ellos, si así lo desea.