your programing

C #의 수학 최적화

lovepro 2020. 12. 26. 16:16
반응형

C #의 수학 최적화


저는 하루 종일 애플리케이션을 프로파일 링 해 왔으며, 코드 몇 개를 최적화 한 후이 작업을 할 일 목록에 남겼습니다. 1 억 번 이상 호출되는 신경망의 활성화 함수입니다. dotTrace에 따르면 전체 기능 시간의 약 60 %에 해당합니다.

이것을 어떻게 최적화 하시겠습니까?

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

시험:

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

편집 : 빠른 벤치 마크를 수행했습니다. 내 컴퓨터에서 위의 코드는 당신의 방법보다 약 43 % 더 빠르며, 수학적으로 동등한이 코드는 아주 조금 더 빠릅니다 (원본보다 46 % 더 빠름).

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

편집 2 : 오버 헤드 C # 함수가 얼마나 많은지 잘 모르겠지만 #include <math.h>소스 코드에서 float-exp 함수를 사용하는 이것을 사용할 수 있어야합니다. 조금 더 빠를 수 있습니다.

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

또한 수백만 건의 호출을 수행하는 경우 함수 호출 오버 헤드가 문제가 될 수 있습니다. 인라인 함수를 만들어서 도움이되는지 확인하십시오.


활성화 함수에 대한 것이라면 e ^ x의 계산이 완전히 정확하면 끔찍한 문제입니까?

예를 들어, 근사값 (1 + x / 256) ^ 256을 사용하면 Java에서 펜티엄 테스트를 수행 할 때 (C #이 기본적으로 동일한 프로세서 명령으로 컴파일된다고 가정합니다) e ^ x보다 약 7-8 배 빠릅니다. (Math.exp ())이며 소수점 이하 두 자리까지 정확하며 +/- 1.5의 약 x까지, 귀하가 언급 한 범위 전체에서 정확한 크기 내입니다. (분명히 256으로 올리려면 실제로 숫자를 8 배로 제곱하십시오.이를 위해 Math.Pow를 사용하지 마십시오!) Java에서 :

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

근사치가 얼마나 정확하길 원하는지에 따라 256을 계속 두 배 또는 절반으로 줄이십시오 (그리고 곱하기 더하기 / 제거하기). n = 4 인 경우에도 -0.5에서 0.5 사이의 x 값에 대해 소수점 이하 1.5 자리 정도의 정확도를 제공합니다 (그리고 Math.exp ()보다 15 배 더 빠릅니다).

PS 나는 얘기를 깜빡 했네요 - 당신은 분명하지 말아야 정말 256에 의해 분할 : 곱 상수 1/256로. Java의 JIT 컴파일러는이 최적화를 자동으로 수행하며 (적어도 Hotspot은 수행함) C #도 수행해야한다고 가정했습니다.


한 번 봐 가지고 이 게시물을 . Java로 작성된 e ^ x에 대한 근사치가 있습니다. 이것은 C # 코드 여야합니다 (예상되지 않음).

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

내 벤치 마크에서 이것은 Math.exp () (Java에서) 보다 5 배 이상 빠릅니다 . 근사치는 신경망에서 사용하기 위해 정확히 개발 된 " 지수 함수의 빠르고 컴팩트 한 근사 "논문을 기반으로합니다 . 기본적으로 2048 개의 항목과 항목 간의 선형 근사값의 조회 테이블과 동일하지만이 모든 것은 IEEE 부동 소수점 트릭을 사용합니다.

편집 : Special Sauce 에 따르면 이것은 CLR 구현보다 ~ 3.25 배 빠릅니다. 감사!


  1. 이 활성화 기능의 모든 변경은 다른 동작의 대가로 발생한다는 것을 기억하십시오 . 여기에는 부동으로 전환 (따라서 정밀도 저하) 또는 활성화 대체 사용이 포함됩니다. 사용 사례를 실험하는 것만으로 올바른 방법을 알 수 있습니다.
  2. 간단한 코드 최적화 외에도 계산 병렬화 (예 : Windows Azure 클라우드에서 컴퓨터 또는 컴퓨터의 여러 코어 활용) 를 고려 하고 훈련 알고리즘을 개선하는 것도 좋습니다.

업데이트 : ANN 활성화 기능에 대한 조회 테이블에 게시

UPDATE2 : 나는 이것들을 완전한 해싱과 혼동했기 때문에 LUT에 대한 요점을 제거했습니다. 나를 다시 트랙에 올려 준 Henrik Gustafsson 에게 감사드립니다 . 따라서 검색 공간이 여전히 로컬 극한으로 엉망이되지만 메모리는 문제가되지 않습니다.


1 억 번의 호출에서 프로파일 러 오버 헤드가 결과를 왜곡하지 않는지 궁금해지기 시작합니다. 계산을 no-op으로 바꾸고 여전히 실행 시간의 60 %를 소비하는 것으로보고 되는지 확인합니다 .

또는 더 좋은 방법은 테스트 데이터를 만들고 스톱워치 타이머를 사용하여 백만 건 정도의 통화를 프로파일 링하는 것입니다.


C ++와 상호 운용 할 수 있다면 모든 값을 배열에 저장하고 다음과 같이 SSE를 사용하여 반복하는 것을 고려할 수 있습니다.

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));
    }
}

그러나 SSE는 메모리를 경계에 맞춰 정렬해야하므로 _aligned_malloc (some_size * sizeof (float), 16)을 사용하여 사용할 배열을 할당해야합니다.

SSE를 사용하면 약 0.5 초 만에 모든 1 억 요소의 결과를 계산할 수 있습니다. 그러나 한 번에 많은 메모리를 할당하면 거의 2/3 기가 바이트의 비용이 발생하므로 한 번에 더 많지만 더 작은 배열을 처리하는 것이 좋습니다. 10 만 개 이상의 요소가있는 이중 버퍼링 방식을 사용하는 것을 고려할 수도 있습니다.

또한 요소 수가 상당히 증가하기 시작하면 GPU에서 이러한 작업을 처리하도록 선택할 수 있습니다 (1D float4 텍스처를 만들고 매우 간단한 조각 셰이더를 실행하기 만하면됩니다).


FWIW, 이미 게시 된 답변에 대한 C # 벤치 마크가 있습니다. (Empty는 함수 호출 오버 헤드를 측정하기 위해 0을 반환하는 함수입니다)

빈 기능 : 79ms 0
원본 : 1576ms 0.7202294
단순화 : (소프라노) 681ms 0.7202294
근사치 : (닐) 441ms 0.7198783
비트 조작 : (martinus) 836ms 0.72318
테일러 : (Rex Logan) 261ms 0.7202305
조회 : (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));
}

F #은 .NET 수학 알고리즘에서 C #보다 성능이 뛰어납니다. 따라서 F #으로 신경망을 다시 작성하면 전반적인 성능이 향상 될 수 있습니다.

F #에서 LUT 벤치마킹 스 니펫 (약간 조정 된 버전을 사용했습니다)을 다시 구현 하면 결과 코드가

  • 3899,2ms 대신 588.8ms 에서 sigmoid1 벤치 마크를 실행합니다.
  • 411.4ms 대신 156.6ms 에서 sigmoid2 (LUT) 벤치 마크를 실행 합니다.

자세한 내용은 블로그 게시물 에서 확인할 수 있습니다 . 다음은 F # 스 니펫 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);

출력 (디버거없이 F # 1.9.6.2 CTP에 대한 컴파일 릴리스) :

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

업데이트 : 10 ^ 7 반복을 사용하여 C와 비슷한 결과를 내도록 벤치마킹을 업데이트했습니다.

UPDATE2 : 다음은 비교할 동일한 시스템에서 C 구현 의 성능 결과입니다 .

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

참고 : 게시물 의 후속 작업입니다.

편집 : 이것 에서 영감을 받아 이것이것같은 것을 계산하도록 업데이트 하십시오 .

이제 당신이 뭘 시켰는지보세요! 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는 더 이상 노력할 가치가 거의 없습니다. 세상은 앞으로 나아갑니다. :)

따라서 10 배 이상 더 빠릅니다. Windows 상자를 가진 사람은 MS-stuff를 사용하여 메모리 사용량과 성능을 조사하게됩니다. :)

활성화 기능에 LUT를 사용하는 것은 드문 일이 아니며 특히 하드웨어에서 구현할 때 특히 그렇습니다. 이러한 유형의 테이블을 포함하려는 경우 개념의 잘 입증 된 변형이 많이 있습니다. 그러나 이미 지적했듯이 앨리어싱이 문제가 될 수 있지만 그에 대한 방법도 있습니다. 더 읽을 거리 :

이것에 대한 몇 가지 문제 :

  • 테이블 외부에 도달하면 오류가 발생하지만 극단적 인 경우 0으로 수렴합니다. x의 경우 약 + -7.0. 이는 선택한 배율 인수 때문입니다. SCALE 값이 클수록 중간 범위에서 더 높은 오류가 발생하지만 가장자리에서는 더 작습니다.
  • 이것은 일반적으로 매우 어리석은 테스트이며 C #을 모릅니다. 내 C 코드의 단순한 변환 일뿐입니다. :)
  • Rinat Abdullin 은 앨리어싱과 정밀도 손실이 문제를 일으킬 수 있다는 매우 정확하지만, 이에 대한 변수를 보지 못했기 때문에 시도해 보도록 조언 할 수 있습니다. 사실, 나는 조회 테이블 문제를 제외하고 그가 말하는 모든 것에 동의합니다.

복사-붙여 넣기 코딩을 용서하십시오 ...

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());
    }
}

첫 번째 생각 : 값 변수에 대한 통계는 어떻습니까?

  • "값"의 값은 일반적으로 작은 -10 <= 값 <= 10입니까?

그렇지 않은 경우 범위를 벗어난 값을 테스트하여 성능을 높일 수 있습니다.

if(value < -10)  return 0;
if(value > 10)  return 1;
  • 값이 자주 반복됩니까?

그렇다면 아마도 Memoization의 이점을 얻을 수있을 것입니다 (아마도 그렇지 않을 수도 있지만 확인하는 것이 아프지 않습니다 ....).

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

이들 중 어느 것도 적용 할 수 없다면 다른 사람들이 제안한 것처럼 시그 모이 드의 정확도를 낮추는 것으로 벗어날 수 있습니다.


Soprano는 귀하의 요청에 대해 몇 가지 멋진 최적화를 제공했습니다.

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

조회 테이블을 시도하고 너무 많은 메모리를 사용하는 것을 발견하면 항상 각 연속 호출에 대한 매개 변수 값을보고 일부 캐싱 기술을 사용할 수 있습니다.

예를 들어 마지막 값과 결과를 캐싱 해보십시오. 다음 호출이 이전 호출과 동일한 값을 갖는 경우 마지막 결과를 캐시했기 때문에 계산할 필요가 없습니다. 현재 통화가 100 번 중 1 번이라도 이전 통화와 같으면 잠재적으로 1 백만 번의 계산을 절약 할 수 있습니다.

또는 10 번의 연속 호출 내에서 value 매개 변수가 평균적으로 2 번 같으므로 마지막 10 개 값 / 답변을 캐싱 할 수 있습니다.


머릿속 에서이 문서는 부동 소수점을 남용하여 지수를 근사하는 방법을 설명 하지만 (PDF의 경우 오른쪽 상단에있는 링크를 클릭)에서 많이 유용할지는 모르겠습니다. 그물.

또한 또 다른 요점은 대규모 네트워크를 빠르게 훈련시키기 위해 사용하는 로지스틱 시그 모이 드가 매우 끔찍합니다. LeCun et alEfficient Backprop 의 섹션 4.4를 참조 하고 0 중심의 것을 사용하십시오 (실제로 전체 논문을 읽으십시오. 매우 유용합니다).


아이디어 : 아마도 미리 계산 된 값으로 (대형) 조회 테이블을 만들 수 있습니까?


이것은 주제에서 약간 벗어 났지만 호기심 에 따라 Java의 C , C #F # 에서 동일한 구현을 수행했습니다 . 다른 사람이 궁금해 할 경우를 대비하여 여기에 남겨 두겠습니다.

결과:

$ 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

필자의 경우 C #에 대한 개선은 OS X 용 Mono보다 Java가 더 잘 최적화 되었기 때문이라고 생각합니다. 유사한 MS .NET 구현 (비교 숫자를 게시하려는 경우 Java 6과 비교)에서 결과가 다를 것이라고 가정합니다. .

암호:

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());
    }
}

이 질문이 나온 지 1 년이 지났지 만 C #과 관련된 F # 및 C 성능에 대한 논의 때문에 문제를 발견했습니다. 다른 응답자의 일부 샘플을 가지고 놀았는데 대리자가 일반 메서드 호출보다 빠르게 실행되는 것처럼 보이지만 C #보다 F #에 대한 명백한 성능 이점이 없다는 것을 발견했습니다 .

  • C : 166ms
  • C # (대리자) : 275ms
  • C # (방법) : 431ms
  • C # (방법, 부동 카운터) : 2,656ms
  • F # : 404ms

부동 카운터가있는 C #은 C 코드의 직선 포트였습니다. for 루프에서 int를 사용하는 것이 훨씬 빠릅니다.


평가하기 더 저렴한 대체 활성화 함수를 실험 해 보는 것도 고려할 수 있습니다. 예를 들면 :

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

(이것은

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

더 적은 곱셈). 이 함수는 대칭이 이상하고 그 미분은 사소합니다. 신경망에 사용하려면 총 입력 수로 나누어 입력 합계를 정규화해야합니다 (도메인을 범위 인 [-1..1]로 제한).


소프라노의 주제에 대한 약간의 변형 :

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

단 정밀도 결과가 나오기 때문에 Math.Exp 함수가 double을 계산하는 이유는 무엇입니까? 반복적 합산 ( e x 의 확장 참조)을 사용하는 지수 계산기는 매번 더 정확하기 위해 더 오래 걸립니다. 그리고 double은 싱글의 두 배입니다! 이렇게하면 먼저 단일로 변환 한 다음 지수 수행합니다.

그러나 expf 함수는 여전히 더 빠릅니다. C #이 암시 적 float-double 변환을 수행하지 않는 한, expf에 전달할 때 soprano의 (float) 캐스트가 필요하지 않습니다.

그렇지 않으면 FORTRAN과 같은 실제 언어를 사용하십시오 .


여기에 좋은 답변이 많이 있습니다. 나는 통해 실행 제안 이 기술 단지 확인하기 위해,

  • 필요한 것보다 더 많이 전화하지 않습니다.
    (때때로 함수는 호출하기가 쉽기 때문에 필요 이상으로 호출됩니다.)
  • 동일한 인수를 사용하여 반복적으로 호출하지 않습니다
    (암기 화를 사용할 수 있음).

BTW 당신이 가진 함수는 역 로짓 함수
또는 log-odds-ratio 함수의 역입니다 log(f/(1-f)).


(성능 측정으로 업데이트 됨) (실제 결과로 다시 업데이트 됨 :)

나는 룩업 테이블 솔루션이 성능에 관해서는 무시할만한 메모리와 정밀 비용으로 당신을 멀리 할 것이라고 생각합니다.

다음 스 니펫은 C로 구현 한 예제입니다 (C #를 드라이 코딩 할만큼 유창하게 말하지 않습니다). 충분히 실행되고 수행되지만 버그가 있다고 확신합니다. :)

#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;
}

Previous results were due to the optimizer doing its job and optimized away the calculations. Making it actually execute the code yields slightly different and much more interesting results (on my way slow 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

profile


TODO:

There are things to improve and ways to remove weaknesses; how to do is is left as an exercise to the reader :)

  • Tune the range of the function to avoid the jump where the table starts and ends.
  • Add a slight noise function to hide the aliasing artifacts.
  • As Rex said, interpolation could get you quite a bit further precision-wise while being rather cheap performance-wise.

There are a much faster functions that do very similar things:

x / (1 + abs(x)) – fast replacement for TAHN

And similarly:

x / (2 + 2 * abs(x)) + 0.5 - fast replacement for SIGMOID

Compare plots with actual sigmoid


Doing a Google search, I found an alternative implementation of the Sigmoid function.

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

Is that correct for your needs? Is it faster?

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


1) Do you call this from only one place? If so, you may gain a small amount of performance by moving the code out of that function and just putting it right where you would normally have called the Sigmoid function. I don't like this idea in terms of code readability and organization but when you need to get every last performance gain, this might help because I think function calls require a push/pop of registers on the stack, which could be avoided if your code was all inline.

2) I have no idea if this might help but try making your function parameter a ref parameter. See if it's faster. I would have suggested making it const (which would have been an optimization if this were in c++) but c# doesn't support const parameters.


If you need a giant speed boost, you could probably look into parallelizing the function using the (ge)force. IOW, use DirectX to control the graphics card into doing it for you. I have no idea how to do this, but I've seen people use graphics cards for all kinds of calculations.


I've seen that a lot of people around here are trying to use approximation to make Sigmoid faster. However, it is important to know that Sigmoid can also be expressed using tanh, not only exp. Calculating Sigmoid this way is around 5 times faster than with exponential, and by using this method you are not approximating anything, thus the original behaviour of Sigmoid is kept as-is.

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

Of course, parellization would be the next step to performance improvement, but as far as the raw calculation is concerned, using Math.Tanh is faster than Math.Exp.

ReferenceURL : https://stackoverflow.com/questions/412019/math-optimization-in-c-sharp

반응형