Pergunta

Eu tenho perfil de uma aplicação durante todo o dia e, depois de ter otimizado um par de pedaços de código, eu estou à esquerda com isso na minha lista de afazeres. É a função de ativação para uma rede neural, que é chamado mais de 100 milhões de vezes. De acordo com dotTrace, isso equivale a cerca de 60% do tempo de funcionamento geral.

Como você otimizar isso?

public static float Sigmoid(double value) {
    return (float) (1.0 / (1.0 + Math.Pow(Math.E, -value)));
}
Foi útil?

Solução

Tente:

public static float Sigmoid(double value) {
    return 1.0f / (1.0f + (float) Math.Exp(-value));
}

EDIT: Eu fiz uma referência rápida. Na minha máquina, o código acima é de cerca de 43% mais rápido do que o seu método, e este código matematicamente equivalente é o teeniest pouco mais rápido (46% mais rápido do que o original):

public static float Sigmoid(double value) {
    float k = Math.Exp(value);
    return k / (1.0f + k);
}

EDIT 2: Eu não tenho certeza quanto C # funções gerais têm, mas se você #include <math.h> em seu código-fonte, você deve ser capaz de usar este, que usa uma função de flutuação-exp. Pode ser um pouco mais rápido.

public static float Sigmoid(double value) {
    float k = expf((float) value);
    return k / (1.0f + k);
}

Além disso, se você está fazendo milhões de chamadas, a sobrecarga de chamada de função pode ser um problema. Tente fazer uma função inline e ver se isso é de alguma ajuda.

Outras dicas

Se é para uma função de ativação, que importa terrivelmente muito se o cálculo de e ^ x é totalmente preciso?

Por exemplo, se você usar a aproximação (1 + x / 256) ^ 256, em meus testes Pentium em Java (estou assumindo C # essencialmente compila as mesmas instruções do processador), este é de cerca de 7-8 vezes mais rápido do que e ^ x (Math.exp ()), e tem uma precisão de 2 casas decimais até cerca x de +/- 1,5, e dentro da ordem correcta de magnitude em toda a gama você indicou. (! Obviamente, para elevar ao 256, você realmente quadrada do número 8 vezes - não use Math.pow para isso) Em Java:

double eapprox = (1d + x / 256d);
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;

Mantenha duplicar ou reduzir para metade a 256 (e adicionar / remover uma multiplicação) dependendo de quão preciso você quer a aproximação de ser. Mesmo com n = 4, ainda dá cerca de 1,5 casas decimais de precisão para valores de x beween -0,5 e 0,5 (e parece uma boa 15 vezes mais rápido do que Math.exp ()).

P.S. Eu esqueci de mencionar - você não deve, obviamente, realmente dividir por 256: multiplicar por uma constante 1/256. compilador JIT Java faz essa otimização automaticamente (pelo menos, Hotspot faz), e eu estava assumindo que C # deve fazer também.

Tenha um olhar em este post . ele tem uma aproximação para e ^ x escrito em Java, este deve ser o código C # para ele (não testado):

public static double Exp(double val) {  
    long tmp = (long) (1512775 * val + 1072632447);  
    return BitConverter.Int64BitsToDouble(tmp << 32);  
}

Em meus benchmarks isso é mais do que 5 vezes mais rápido do que Math.exp () (em Java). A aproximação é baseada no papel " A Fast, Compact aproximação da função exponencial " que foi desenvolvido exatamente para ser usado em redes neurais. É basicamente o mesmo que uma tabela de referência de 2048 entradas e aproximação linear entre as entradas, mas tudo isso com IEEE truques ponto flutuante.

EDIT: De acordo com a Special Sauce este é ~ 3.25x mais rápido do que o implementação CLR. Obrigado!

  1. Lembre-se que quaisquer alterações nesta função de ativação vir ao custo de comportamento diferente . Este inclui ainda a mudança para flutuador (e, portanto, reduzindo a precisão) ou usando substitutos de activação. Só experimentando com seu caso de uso irá mostrar o caminho certo.
  2. Além das otimizações de código simples, eu recomendaria também a considerar paralelização dos cálculos (ou seja: a alavancagem múltiplos núcleos de sua máquina ou até mesmo máquinas no Windows Azure Nuvens) e melhorar a algoritmos de treinamento.

UPDATE: Post em tabelas de referência para funções de ativação ANN

UPDATE2: eu removi o ponto em LUTs desde que eu tenha confundido estes com o hashing completa. Os agradecimentos vão para Henrik Gustafsson para me colocar de volta na pista. Assim, a memória não é um problema, embora o espaço de busca ainda fica confuso com extremos locais um pouco.

A 100 milhões de chamadas, eu ia começar a se perguntar se profiler sobrecarga não está distorcendo os resultados. Substitua o cálculo com uma-op não e ver se é ainda informou a consumir 60% do tempo de execução ...

Ou melhor ainda, criar alguns dados de teste e usar um cronômetro ao perfil de um milhão ou mais chamadas.

Se você é capaz de interoperabilidade com C ++, você poderia considerar o armazenamento de todos os valores em uma matriz e loop sobre eles usando SSE como esta:

void sigmoid_sse(float *a_Values, float *a_Output, size_t a_Size){
    __m128* l_Output = (__m128*)a_Output;
    __m128* l_Start  = (__m128*)a_Values;
    __m128* l_End    = (__m128*)(a_Values + a_Size);

    const __m128 l_One        = _mm_set_ps1(1.f);
    const __m128 l_Half       = _mm_set_ps1(1.f / 2.f);
    const __m128 l_OneOver6   = _mm_set_ps1(1.f / 6.f);
    const __m128 l_OneOver24  = _mm_set_ps1(1.f / 24.f);
    const __m128 l_OneOver120 = _mm_set_ps1(1.f / 120.f);
    const __m128 l_OneOver720 = _mm_set_ps1(1.f / 720.f);
    const __m128 l_MinOne     = _mm_set_ps1(-1.f);

    for(__m128 *i = l_Start; i < l_End; i++){
        // 1.0 / (1.0 + Math.Pow(Math.E, -value))
        // 1.0 / (1.0 + Math.Exp(-value))

        // value = *i so we need -value
        __m128 value = _mm_mul_ps(l_MinOne, *i);

        // exp expressed as inifite series 1 + x + (x ^ 2 / 2!) + (x ^ 3 / 3!) ...
        __m128 x = value;

        // result in l_Exp
        __m128 l_Exp = l_One; // = 1

        l_Exp = _mm_add_ps(l_Exp, x); // += x

        x = _mm_mul_ps(x, x); // = x ^ 2
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_Half, x)); // += (x ^ 2 * (1 / 2))

        x = _mm_mul_ps(value, x); // = x ^ 3
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver6, x)); // += (x ^ 3 * (1 / 6))

        x = _mm_mul_ps(value, x); // = x ^ 4
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver24, x)); // += (x ^ 4 * (1 / 24))

#ifdef MORE_ACCURATE

        x = _mm_mul_ps(value, x); // = x ^ 5
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver120, x)); // += (x ^ 5 * (1 / 120))

        x = _mm_mul_ps(value, x); // = x ^ 6
        l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver720, x)); // += (x ^ 6 * (1 / 720))

#endif

        // we've calculated exp of -i
        // now we only need to do the '1.0 / (1.0 + ...' part
        *l_Output++ = _mm_rcp_ps(_mm_add_ps(l_One,  l_Exp));
    }
}

No entanto, lembre-se que as matrizes que você vai usar deve ser alocado usando _aligned_malloc (some_size * sizeof (float), 16), porque SSE requer memória alinhado a um limite.

Usando SSE, eu posso calcular o resultado para todos os 100 milhões de elementos em cerca de meio segundo. No entanto, alocando essa quantidade de memória em um momento vai custar-lhe cerca de dois terços de um gigabyte, então eu sugiro processar mais, mas menores matrizes de cada vez. Você pode até querer considerar o uso de uma abordagem buffer duplo com 100K elementos ou mais.

Além disso, se o número de elementos começa a crescer consideravelmente você pode querer optar por processar essas coisas na GPU (basta criar uma 1D float4 textura e executar um shader de fragmento muito trivial).

FWIW, aqui está o meu C # referência para as respostas já publicadas. (Vazio é uma função que retorna apenas 0, para medir a sobrecarga chamada de função)

Empty Function:       79ms   0
Original:             1576ms 0.7202294
Simplified: (soprano) 681ms  0.7202294
Approximate: (Neil)   441ms  0.7198783
Bit Manip: (martinus) 836ms  0.72318
Taylor: (Rex Logan)   261ms  0.7202305
Lookup: (Henrik)      182ms  0.7204863
public static object[] Time(Func<double, float> f) {
    var testvalue = 0.9456;
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < 1e7; i++)
        f(testvalue);
    return new object[] { sw.ElapsedMilliseconds, f(testvalue) };
}
public static void Main(string[] args) {
    Console.WriteLine("Empty:       {0,10}ms {1}", Time(Empty));
    Console.WriteLine("Original:    {0,10}ms {1}", Time(Original));
    Console.WriteLine("Simplified:  {0,10}ms {1}", Time(Simplified));
    Console.WriteLine("Approximate: {0,10}ms {1}", Time(ExpApproximation));
    Console.WriteLine("Bit Manip:   {0,10}ms {1}", Time(BitBashing));
    Console.WriteLine("Taylor:      {0,10}ms {1}", Time(TaylorExpansion));
    Console.WriteLine("Lookup:      {0,10}ms {1}", Time(LUT));
}

Em cima da minha cabeça, este documento explica uma maneira de aproximar exponencial por abusar de ponto flutuante , (clique no link no canto superior direito para PDF), mas eu não sei se ele vai ser de muito uso para você em .NET.

Além disso, outro ponto: com a finalidade de treinar grandes redes rapidamente, a logística sigmóide que você está usando é muito terrível. Ver secção 4.4 da eficiente Retropropagação por LeCun et al e utilização algo zero-centrada (na verdade, ler todo esse papel, é imensamente útil).

F # tem um desempenho melhor do que o C #, em algoritmos matemáticos NET. rede neural Então reescrever em F # pode melhorar o desempenho geral.

Se nós re-implementar LUT de benchmarking trecho (Estou usando versão ligeiramente beliscada) em F #, em seguida, o código resultante:

  • executa sigmoid1 referência no 588.8ms vez de 3899,2ms
  • executa sigmoid2 (LUT) referência no 156.6ms em vez de 411,4 ms

Mais detalhes podem ser encontrados no rel Blog Post . Aqui está o F # trecho JIC:

#light

let Scale = 320.0f;
let Resolution = 2047;

let Min = -single(Resolution)/Scale;
let Max = single(Resolution)/Scale;

let range step a b =
  let count = int((b-a)/step);
  seq { for i in 0 .. count -> single(i)*step + a };

let lut = [| 
  for x in 0 .. Resolution ->
    single(1.0/(1.0 +  exp(-double(x)/double(Scale))))
  |]

let sigmoid1 value = 1.0f/(1.0f + exp(-value));

let sigmoid2 v = 
  if (v <= Min) then 0.0f;
  elif (v>= Max) then 1.0f;
  else
    let f = v * Scale;
    if (v>0.0f) then lut.[int (f + 0.5f)]
    else 1.0f - lut.[int(0.5f - f)];

let getError f = 
  let test = range 0.00001f -10.0f 10.0f;
  let errors = seq { 
    for v in test -> 
      abs(sigmoid1(single(v)) - f(single(v)))
  }
  Seq.max errors;

open System.Diagnostics;

let test f = 
  let sw = Stopwatch.StartNew(); 
  let mutable m = 0.0f;
  let result = 
    for t in 1 .. 10 do
      for x in 1 .. 1000000 do
        m <- f(single(x)/100000.0f-5.0f);
  sw.Elapsed.TotalMilliseconds;

printf "Max deviation is %f\n" (getError sigmoid2)
printf "10^7 iterations using sigmoid1: %f ms\n" (test sigmoid1)
printf "10^7 iterations using sigmoid2: %f ms\n" (test sigmoid2)

let c = System.Console.ReadKey(true);

E a saída (compilação lançamento contra F # 1.9.6.2 CTP sem depurador):

Max deviation is 0.001664
10^7 iterations using sigmoid1: 588.843700 ms
10^7 iterations using sigmoid2: 156.626700 ms

UPDATE: aferição atualizado para usar 10 ^ 7 iterações para tornar os resultados comparáveis ??com C

UPDATE2: aqui estão os resultados de desempenho do C implementação de a mesma máquina para comparar com:

Max deviation is 0.001664
10^7 iterations using sigmoid1: 628 ms
10^7 iterations using sigmoid2: 157 ms

Nota: Este é um seguimento este post .

Editar: Update para calcular a mesma coisa que este e < a href = "https://stackoverflow.com/questions/412019/code-optimizations#416180"> este , tendo alguma inspiração de este .

Agora, olhe o que você me fez fazer! Você me fez instalar Mono!

$ gmcs -optimize test.cs && mono test.exe
Max deviation is 0.001663983
10^7 iterations using Sigmoid1() took 1646.613 ms
10^7 iterations using Sigmoid2() took 237.352 ms

C é não vale a pena o esforço mais, o mundo está a avançar:)

Assim, pouco mais de um fator 10 6 mais rápido. Alguém com uma caixa de Windows começa a investigar o uso de memória e desempenho usando MS-stuff:)

Usando LUTs para funções de ativação não é tão incomum, especielly quando implementada em hardware. Existem muitas variantes bem comprovados do conceito lá fora, se você estiver disposto a incluir esses tipos de tabelas. No entanto, como já foi referido, aliasing pode vir a ser um problema, mas existem maneiras de contornar isso também. Alguns leitura adicional:

Algumas dicas com este:

  • O erro sobe quando você chegar fora da tabela (mas converge para 0 nos extremos); para x cerca de + -7,0. Isto é devido ao fator de escala escolhida. valores maiores de ESCALA dar erros maiores na faixa intermediária, mas menor nas bordas.
  • Este é geralmente um teste muito estúpido, e eu não sei C #, é apenas uma conversão simples da minha C-código:)
  • Rinat Abdullin é muito correto que aliasing e perda de precisão pode causar problemas, mas desde que eu tenho não se vêem as variáveis ??para que eu só posso aconselhá-lo a tentar isso. Na verdade, eu concordo com tudo o que ele diz, exceto para a emissão de tabelas de pesquisa.

Pardon o copy-paste de codificação ...

using System;
using System.Diagnostics;

class LUTTest {
    private const float SCALE = 320.0f;
    private const int RESOLUTION = 2047;
    private const float MIN = -RESOLUTION / SCALE;
    private const float MAX = RESOLUTION / SCALE;

    private static readonly float[] lut = InitLUT();

    private static float[] InitLUT() {
      var lut = new float[RESOLUTION + 1];

      for (int i = 0; i < RESOLUTION + 1; i++) {
        lut[i] = (float)(1.0 / (1.0 + Math.Exp(-i / SCALE)));
      }
      return lut;
    }

    public static float Sigmoid1(double value) {
        return (float) (1.0 / (1.0 + Math.Exp(-value)));
    }

    public static float Sigmoid2(float value) {
      if (value <= MIN) return 0.0f;
      if (value >= MAX) return 1.0f;
      if (value >= 0) return lut[(int)(value * SCALE + 0.5f)];
      return 1.0f - lut[(int)(-value * SCALE + 0.5f)];
    }

    public static float error(float v0, float v1) {
      return Math.Abs(v1 - v0);
    }

    public static float TestError() {
        float emax = 0.0f;
        for (float x = -10.0f; x < 10.0f; x+= 0.00001f) {
          float v0 = Sigmoid1(x);
          float v1 = Sigmoid2(x);
          float e = error(v0, v1);
          if (e > emax) emax = e;
        }
        return emax;
    }

    public static double TestPerformancePlain() {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < 10; i++) {
            for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
                Sigmoid1(x);
            }
        }
        sw.Stop();
        return sw.Elapsed.TotalMilliseconds;
    }    

    public static double TestPerformanceLUT() {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < 10; i++) {
            for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
                Sigmoid2(x);
            }
        }
        sw.Stop();
        return sw.Elapsed.TotalMilliseconds;
    }    

    static void Main() {
        Console.WriteLine("Max deviation is {0}", TestError());
        Console.WriteLine("10^7 iterations using Sigmoid1() took {0} ms", TestPerformancePlain());
        Console.WriteLine("10^7 iterations using Sigmoid2() took {0} ms", TestPerformanceLUT());
    }
}

Primeiro pensei:? Que tal algumas estatísticas sobre a variável valores

  • são os valores de "valor" tipicamente pequenos -10 <= valor <= 10?

Se não, você provavelmente pode obter um impulso testando para fora dos valores Bounds

if(value < -10)  return 0;
if(value > 10)  return 1;
  • são os valores repetido muitas vezes?

Se assim for, você provavelmente pode obter algum benefício com Memoização (provavelmente não, mas não ferir a verificação ....)

if(sigmoidCache.containsKey(value)) return sigmoidCache.get(value);

Se nenhum destes pode ser aplicado, em seguida, como alguns sugeriram, talvez você pode ir longe com a redução da precisão da sua sigmóide ...

Soprano tinha algumas nice otimizações sua chamada:

public static float Sigmoid(double value) 
{
    float k = Math.Exp(value);
    return k / (1.0f + k);
}

Se você tentar uma tabela de pesquisa e encontre ele usa muita memória você pode sempre olhar para o valor de seu parâmetro para cada sucessivas chamadas e empregando alguma técnica de cache.

Por exemplo, tentar cache o último valor e resultado. Se a próxima chamada tem o mesmo valor que o anterior, você não precisa calcular-lo como você teria em cache o último resultado. Se a chamada atual era o mesmo que a chamada anterior até 1 de 100 vezes, você poderia salvar-se 1 milhão de cálculos.

Ou, você pode achar que dentro de 10 chamadas sucessivas, o parâmetro valor é, em média, os mesmos 2 vezes, para que você possa, em seguida, tentar cache os últimos 10 valores / respostas.

Idea:? Talvez você pode fazer uma (grande) tabela de pesquisa com os valores pré-calculados

Este é um pouco fora do tópico, mas só por curiosidade, eu fiz a mesma implementação como o de C , C # e F # em Java. Eu só vou deixar isso aqui no caso de alguém é curioso.

Resultado:

$ javac LUTTest.java && java LUTTest
Max deviation is 0.001664
10^7 iterations using sigmoid1() took 1398 ms
10^7 iterations using sigmoid2() took 177 ms

Suponho que a melhoria sobre C # no meu caso é devido ao Java ser melhor otimizado de Mono para OS X. Em um MS semelhantes .NET-implementação (vs. Java 6, se alguém quiser postar números comparativos) Suponho que os resultados seria diferente.

Código:

public class LUTTest {
    private static final float SCALE = 320.0f;
    private static final  int RESOLUTION = 2047;
    private static final  float MIN = -RESOLUTION / SCALE;
    private static final  float MAX = RESOLUTION / SCALE;

    private static final float[] lut = initLUT();

    private static float[] initLUT() {
        float[] lut = new float[RESOLUTION + 1];

        for (int i = 0; i < RESOLUTION + 1; i++) {
            lut[i] = (float)(1.0 / (1.0 + Math.exp(-i / SCALE)));
        }
        return lut;
    }

    public static float sigmoid1(double value) {
        return (float) (1.0 / (1.0 + Math.exp(-value)));
    }

    public static float sigmoid2(float value) {
        if (value <= MIN) return 0.0f;
        if (value >= MAX) return 1.0f;
        if (value >= 0) return lut[(int)(value * SCALE + 0.5f)];
        return 1.0f - lut[(int)(-value * SCALE + 0.5f)];
    }

    public static float error(float v0, float v1) {
        return Math.abs(v1 - v0);
    }

    public static float testError() {
        float emax = 0.0f;
        for (float x = -10.0f; x < 10.0f; x+= 0.00001f) {
            float v0 = sigmoid1(x);
            float v1 = sigmoid2(x);
            float e = error(v0, v1);
            if (e > emax) emax = e;
        }
        return emax;
    }

    public static long sigmoid1Perf() {
        float y = 0.0f;
        long t0 = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
                y = sigmoid1(x);
            }
        }
        long t1 = System.currentTimeMillis();
        System.out.printf("",y);
        return t1 - t0;
    }    

    public static long sigmoid2Perf() {
        float y = 0.0f;
        long t0 = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
                y = sigmoid2(x);
            }
        }
        long t1 = System.currentTimeMillis();
        System.out.printf("",y);
        return t1 - t0;
    }    

    public static void main(String[] args) {

        System.out.printf("Max deviation is %f\n", testError());
        System.out.printf("10^7 iterations using sigmoid1() took %d ms\n", sigmoid1Perf());
        System.out.printf("10^7 iterations using sigmoid2() took %d ms\n", sigmoid2Perf());
    }
}

Eu percebo que ele tem sido um ano desde que esta pergunta apareceu, mas eu corri em toda ela por causa da discussão de F # e C desempenho em relação ao C #. Eu joguei com algumas das amostras de outros respondedores e descobriu que os delegados aparecem para executar mais rápido do que uma chamada de método regular, mas não há nenhuma vantagem evidente para peformance F # C # sobre .

  • C: 166ms
  • C # (delegado): 275ms
  • C # (método): 431ms
  • C # (método, contador de flutuador): 2,656ms
  • F #: 404ms

A C # com um contador flutuador era uma porta em frente do código C. É muito mais rápido para usar um int no loop for.

Você também pode considerar a experimentar com funções de ativação alternativas que são mais baratos para avaliar. Por exemplo:

f(x) = (3x - x**3)/2

(que poderia ser tomada como

f(x) = x*(3 - x*x)/2

para um a menos multiplicação). Esta função tem uma simetria ímpar, e o seu derivado é trivial. Usá-lo para uma rede neural requer normalizar as entradas de soma-dividindo-se pelo número total de entradas (que limita o domínio de [-1..1], que é também variam).

Uma variação leve no tema da Soprano:

public static float Sigmoid(double value) {
    float v = value;
    float k = Math.Exp(v);
    return k / (1.0f + k);
}

Desde que você está apenas depois de um único resultado de precisão, por que fazer a função de calcular Math.Exp um duplo? Qualquer calculadora expoente que utiliza um somatório iterativa (ver a expansão do e x ) levará mais tempo para maior precisão, cada um e cada vez. E dupla é o dobro do trabalho do single! Dessa forma, você converter para primeiro single, , em seguida, do seu exponencial.

Mas a função expf deve ser mais rápido ainda. Eu não vejo a necessidade de (float) elenco de soprano de passagem para expf embora, a menos que C # não faz conversão implícita bóia-double.

Caso contrário, basta usar um real linguagem, como Fortran ...

Há um monte de boas respostas aqui. Gostaria de sugerir executá-lo através esta técnica , só para ter certeza

  • Você não está chamando-o de quaisquer mais vezes do que você precisa.
    (Às vezes, as funções são chamados mais do que o necessário, apenas porque eles são tão fáceis de chamada.)
  • Você não está chamando-o repetidamente com os mesmos argumentos
    (Onde você poderia usar memoization)

BTW a função que você tem é a função logit inversa,
ou o inverso da função de log-log(f/(1-f)) odds ratio.

(Atualizado com medições de desempenho) (Atualizado novamente com resultados reais:)

Eu acho que uma solução tabela de pesquisa iria chegar muito longe quando se trata de desempenho, a um custo de memória e precisão insignificante.

O trecho a seguir é um exemplo de implementação em C (eu não falo c # fluente o suficiente para secar-code-lo). Ele roda e executa bem o suficiente, mas eu tenho certeza que há um bug nele:)

#include <math.h>
#include <stdio.h>
#include <time.h>

#define SCALE 320.0f
#define RESOLUTION 2047
#define MIN -RESOLUTION / SCALE
#define MAX RESOLUTION / SCALE

static float sigmoid_lut[RESOLUTION + 1];

void init_sigmoid_lut(void) {
    int i;    
    for (i = 0; i < RESOLUTION + 1; i++) {
        sigmoid_lut[i] =  (1.0 / (1.0 + exp(-i / SCALE)));
    }
}

static float sigmoid1(const float value) {
    return (1.0f / (1.0f + expf(-value)));
}

static float sigmoid2(const float value) {
    if (value <= MIN) return 0.0f;
    if (value >= MAX) return 1.0f;
    if (value >= 0) return sigmoid_lut[(int)(value * SCALE + 0.5f)];
    return 1.0f-sigmoid_lut[(int)(-value * SCALE + 0.5f)];
}

float test_error() {
    float x;
    float emax = 0.0;

    for (x = -10.0f; x < 10.0f; x+=0.00001f) {
        float v0 = sigmoid1(x);
        float v1 = sigmoid2(x);
        float error = fabsf(v1 - v0);
        if (error > emax) { emax = error; }
    } 
    return emax;
}

int sigmoid1_perf() {
    clock_t t0, t1;
    int i;
    float x, y = 0.0f;

    t0 = clock();
    for (i = 0; i < 10; i++) {
        for (x = -5.0f; x <= 5.0f; x+=0.00001f) {
            y = sigmoid1(x);
        }
    }
    t1 = clock();
    printf("", y); /* To avoid sigmoidX() calls being optimized away */
    return (t1 - t0) / (CLOCKS_PER_SEC / 1000);
}

int sigmoid2_perf() {
    clock_t t0, t1;
    int i;
    float x, y = 0.0f;
    t0 = clock();
    for (i = 0; i < 10; i++) {
        for (x = -5.0f; x <= 5.0f; x+=0.00001f) {
            y = sigmoid2(x);
        }
    }
    t1 = clock();
    printf("", y); /* To avoid sigmoidX() calls being optimized away */
    return (t1 - t0) / (CLOCKS_PER_SEC / 1000);
}

int main(void) {
    init_sigmoid_lut();
    printf("Max deviation is %0.6f\n", test_error());
    printf("10^7 iterations using sigmoid1: %d ms\n", sigmoid1_perf());
    printf("10^7 iterations using sigmoid2: %d ms\n", sigmoid2_perf());

    return 0;
}

Os resultados anteriores foram devido ao otimizador de fazer o seu trabalho e otimizado afastado os cálculos. Tornando-se realmente executar os rendimentos de código ligeiramente diferentes e resultados muito mais interessantes (no meu caminho lento MB Air):

$ gcc -O2 test.c -o test && ./test
Max deviation is 0.001664
10^7 iterations using sigmoid1: 571 ms
10^7 iterations using sigmoid2: 113 ms

perfil


TODO:

Há coisas para melhorar e maneiras para remover fraquezas; como fazer é é deixado como um exercício para o leitor:)

  • Tune a faixa da função para evitar o salto onde a tabela começa e termina.
  • Adicione uma função de ruído leve para esconder os serrilhamentos.
  • Como Rex disse, interpolação poderia conseguir-lhe um pouco mais de precisão-wise enquanto ser em termos de performance em vez barato.

Há muito mais rápido funções que fazem coisas muito semelhantes:

x / (1 + abs(x)) - substituição rápida para Tahn

E da mesma forma:

x / (2 + 2 * abs(x)) + 0.5 - substituição rápida para sigmóide

Compare parcelas com real sigmóide

Fazendo uma pesquisa no Google, encontrei uma implementação alternativa da função sigmóide.

public double Sigmoid(double x)
{
   return 2 / (1 + Math.Exp(-2 * x)) - 1;
}

Isso é correto para suas necessidades? É mais rápido?

http://dynamicnotions.blogspot.com/2008 /09/sigmoid-function-in-c.html

1) Você chama isto de apenas um lugar? Se assim for, você pode ganhar uma pequena quantidade de desempenho movendo o código de fora dessa função e apenas colocá-lo exatamente onde você normalmente teria chamado a função sigmóide. Eu não gosto dessa ideia, em termos de legibilidade do código e organização, mas quando você precisa para obter cada último ganho de desempenho, isso pode ajudar, porque eu acho chamadas de funções requerem um push / pop de registros na pilha, que poderiam ser evitados se o seu código era tudo em linha.

2) Eu não tenho idéia se isso pode ajudar, mas tentar fazer a sua função de parâmetro um parâmetro ref. Veja se é mais rápido. Eu teria sugerido tornando-const (o que teria sido uma otimização se este estivesse em C ++), mas c # não suporta parâmetros const.

Se você precisa de um impulso de velocidade gigante, você poderia provavelmente olhar para paralelização a função usando a força (GE). IOW, use DirectX para controlar a placa gráfica em fazê-lo para você. Eu não tenho nenhuma idéia de como fazer isso, mas eu vi pessoas usam placas gráficas para todos os tipos de cálculos.

Eu vi que um monte de pessoas por aqui estão tentando usar a aproximação para fazer Sigmoid mais rápido. No entanto, é importante saber que sigmóide também pode ser expresso utilizando tanh, não só de exp. Calculando Sigmoid dessa forma é cerca de 5 vezes mais rápido do que com exponencial, e usando este método você não está aproximando nada, portanto, o comportamento original da sigmóide é mantido como está.

    public static double Sigmoid(double value)
    {
        return 0.5d + 0.5d * Math.Tanh(value/2);
    }

Claro, parellization seria o próximo passo para a melhoria do desempenho, mas, tanto quanto o cálculo cru está em causa, utilizando Math.Tanh é mais rápido que Math.Exp.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top