web-dev-qa-db-fra.com

Algorithme sigmoïde rapide

La fonction sigmoïde est définie comme

enter image description here

J'ai trouvé que l'utilisation de la fonction intégrée C exp() pour calculer la valeur de f(x) est lente. Existe-t-il un algorithme plus rapide pour calculer la valeur de f(x)?

32
user416983

vous n'avez pas à utiliser la fonction sigmoïde réelle et exacte dans un algorithme de réseau de neurones, mais vous pouvez la remplacer par une version approximative qui a des propriétés similaires mais qui est plus rapide à calculer.

Par exemple, vous pouvez utiliser la fonction "sigmoïde rapide"

  f(x) = x / (1 + abs(x))

L'utilisation des premiers termes de l'expansion de la série pour exp (x) n'aidera pas trop si les arguments de f(x) ne sont pas proches de zéro et que vous rencontrez le même problème avec une expansion de série de la fonction sigmoïde si les arguments sont "grands".

Une alternative consiste à utiliser la recherche de table. Autrement dit, vous précalculez les valeurs de la fonction sigmoïde pour un nombre donné de points de données, puis effectuez une interpolation rapide (linéaire) entre eux si vous le souhaitez.

31
Antti Huima

Il est préférable de mesurer d'abord sur votre matériel. Un simple benchmark script montre que sur ma machine 1/(1+|x|) est la plus rapide, et tanh(x) est la seconde proche. La fonction d'erreur erf est également assez rapide.

% 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

Je m'attends à ce que les résultats puissent varier en fonction de l'architecture et du compilateur utilisé, mais erf(x) (depuis C99), tanh(x) et x/(1.0+fabs(x)) sont susceptibles d'être les plus performants .

17
sastanin

Les gens ici sont principalement préoccupés par la vitesse d'une fonction par rapport à une autre et créent un micro-benchmark pour voir si f1(x) s'exécute 0,0001 ms plus vite que f2(x). Le gros problème est que cela n'est généralement pas pertinent, car ce qui importe, c'est la vitesse à laquelle votre réseau apprend avec votre fonction d'activation en essayant de minimiser votre fonction de coût.

Selon la théorie actuelle, fonction redresseur et softplusenter image description here

par rapport à la fonction sigmoïde ou à des fonctions d'activation similaires, permettent un apprentissage plus rapide et efficace des architectures neuronales profondes sur des ensembles de données volumineux et complexes.

Je suggère donc de jeter la micro-optimisation et de voir quelle fonction permet un apprentissage plus rapide (en prenant également en compte diverses autres fonctions de coût).

12
Salvador Dali

Pour rendre le NN plus flexible, vous utilisiez généralement un taux alpha pour modifier l'angle du graphique autour de 0.

La fonction sigmoïde ressemble à:

f(x) = 1 / ( 1+exp(-x*alpha))

La fonction presque équivalente (mais plus rapide) est:

f(x) = 0.5 * (x * alpha / (1 + abs(x*alpha))) + 0.5

Vous pouvez vérifier les graphiques ici

Lorsque j'utilise la fonction abs, le réseau devient plus rapide 100+ fois.

8
Nosyara

Cette réponse n'est probablement pas pertinente dans la plupart des cas, mais je voulais simplement dire que pour l'informatique CUDA, j'ai trouvé que x/sqrt(1+x^2) était de loin la fonction la plus rapide.

Par exemple, fait avec des intrinsèques flottants à simple précision:

__device__ void fooCudaKernel(/* some arguments */) {
    float foo, sigmoid;
    // some code defining foo
    sigmoid = __fmul_rz(rsqrtf(__fmaf_rz(foo,foo,1)),foo);
}
6
pqn

Vous pouvez également utiliser une version approximative de sigmoid (ses différences ne dépassent pas 0,2% de l'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);
    }

Optimisation de la fonction RoughSigmoid avec l'utilisation 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]);
    }

Optimisation de la fonction RoughSigmoid avec l'utilisation d'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]);
    }
5
ErmIg

Vous pouvez utiliser une méthode simple mais efficace en utilisant deux formules:

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

Cela ressemblera à ceci:

Deux graphiques pour un sigmoïde {Bleu: (0,5/(1+ (x ^ 2))), Jaune: (-0,5/(1+ (x ^ 2))) + 1}

3
user9848049

En utilisant Eureqa pour rechercher des approximations de sigmoïde, j'ai trouvé 1/(1 + 0.3678749025^x) l'approximait. C'est assez proche, il suffit de se débarrasser d'une opération avec la négation de x.

Certaines des autres fonctions présentées ici sont intéressantes, mais le fonctionnement électrique est-il vraiment si lent? Je l'ai testé et il a effectivement été plus rapide que l'addition, mais cela pourrait être un coup de chance. Si c'est le cas, cela devrait être aussi rapide ou plus rapide que tous les autres.

EDIT: 0.5 + 0.5*tanh(0.5*x) et moins précis, 0.5 + 0.5*tanh(n) fonctionne également. Et vous pouvez simplement vous débarrasser des constantes si vous ne vous souciez pas de les placer entre la plage [0,1] comme sigmoïde. Mais cela suppose que tanh est plus rapide.

1
Houshalter

La fonction tanh peut être optimisée dans certaines langues, ce qui la rend plus rapide qu'un x/(1 + abs (x)) personnalisé, comme c'est le cas dans Julia.

1
Qni