Вопрос

Я весь день профилировал приложение и, оптимизировав пару фрагментов кода, оставил это в своем списке дел.Это функция активации нейронной сети, которая вызывается более 100 миллионов раз.По данным 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, в моем тестировании Pentium на Java (я предполагаю, что C # по существу компилируется с теми же инструкциями процессора), это примерно в 7-8 раз быстрее, чем e ^ x (Math.exp()), и с точностью до 2 знаков после запятой примерно до x, равного + /-1,5, и в пределах правильного порядка величины в указанном вами диапазоне.(Очевидно, что для увеличения до 256 вы фактически возводите число в квадрат 8 раз - не используйте математику.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 он по-прежнему дает точность около 1,5 знаков после запятой для значений x от -0,5 до 0,5 (и работает в 15 раз быстрее, чем Math.exp()).

P.S.Я забыл упомянуть - вам, очевидно, не следует в самом деле разделите на 256:умножьте на константу 1/256.JIT-компилятор Java выполняет эту оптимизацию автоматически (по крайней мере, Hotspot делает), и я предполагал, что C # тоже должен это делать.

<Ол>
  • Помните, что любые изменения в этой функции активации происходят за счет различного поведения . Это даже включает переключение на float (и, следовательно, снижение точности) или использование заменителей активации. Только эксперимент с вашим вариантом использования покажет правильный путь.
  • В дополнение к простой оптимизации кода, я бы также рекомендовал рассмотреть возможность распараллеливания вычислений (т. е. для использования нескольких ядер вашей машины или даже машин в облаках Windows Azure) и улучшения алгоритмы обучения.
  • ОБНОВЛЕНИЕ: Опубликовать таблицы поиска для функций активации ANN

    ОБНОВЛЕНИЕ2: я удалил точку на LUT, поскольку перепутал ее с полным хэшированием. Благодарим Хенрика Густафссона за то, что вернули меня на трассу. Таким образом, память не является проблемой, хотя пространство поиска все еще немного перепутано с локальными экстремумами.

    При 100 миллионах звонков я бы начал задаваться вопросом, не влияют ли накладные расходы профилировщика на ваши результаты. Замените вычисление неактивным и посмотрите, что все еще сообщает, что оно потребляет 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));
        }
    }
    

    Однако помните, что используемые вами массивы следует размещать с помощью _aligned_malloc (some_size * sizeof (float), 16), поскольку SSE требует памяти, выровненной по границе.

    Используя SSE, я могу вычислить результат для всех 100 миллионов элементов примерно за полсекунды. Однако выделение такого большого количества памяти за один раз обойдется вам почти в две трети гигабайта, поэтому я рекомендую обрабатывать больше, но меньшие массивы за раз. Возможно, вы даже захотите использовать подход двойной буферизации с элементами по 100 КБ или более.

    Кроме того, если количество элементов начинает значительно расти, вы можете захотеть обработать эти вещи на GPU (просто создайте 1D-текстуру float4 и запустите очень простой фрагментный шейдер).

    FWIW, вот мои тесты C # для уже опубликованных ответов. (Empty - это функция, которая просто возвращает 0, чтобы измерить издержки вызова функции)

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

    Вдобавок к моей этой статье объясняется способ аппроксимации экспоненциальное, злоупотребляя плавающей точкой (нажмите на ссылку в правом верхнем углу для просмотра PDF), но я не знаю, будет ли она вам полезна в .NET.

    Кроме того, еще один момент: для быстрой подготовки больших сетей используемая логистическая сигмоида довольно ужасна. См. Раздел 4.4 Эффективный Backprop by LeCun et al. и используйте что-нибудь нольцентрированный (на самом деле, прочитайте всю статью, это очень полезно).

    F # обладает лучшей производительностью, чем C # в математических алгоритмах .NET. Таким образом, переписывание нейронной сети на F # может улучшить общую производительность.

    Если мы повторно внедрим Фрагмент бенчмаркинга LUT (Я использовал слегка измененную версию) в F #, затем результирующий код:

    • выполняет бенчмарк sigmoid1 в 588,8 мс вместо 3899,2 мс
    • выполняет тест sigmoid2 (LUT) в 156,6 мс вместо 411,4 мс

    Более подробную информацию можно найти в запись в блоге.Вот фрагмент 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

    ОБНОВЛЕНИЕ 2: вот результаты работы 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 на 6 быстрее.Кто-то с Windows box может исследовать использование памяти и производительность с помощью MS-stuff :)

    Использование LUT для функций активации не так уж и редко, особенно когда реализовано аппаратно.Существует множество хорошо зарекомендовавших себя вариантов концепции, если вы готовы включить эти типы таблиц.Однако, как уже указывалось, наложение псевдонимов может оказаться проблемой, но и этого можно избежать.Немного дальнейшего чтения:

    Некоторые проблемы с этим:

    • Ошибка увеличивается, когда вы выходите за пределы таблицы (но сходится к 0 в крайних точках);для x приблизительно +-7,0.Это связано с выбранным коэффициентом масштабирования.Большие значения МАСШТАБА дают более высокие ошибки в среднем диапазоне, но меньшие по краям.
    • Обычно это очень глупый тест, и я не знаю C #, это просто простое преобразование моего C-кода :)
    • Ринат Абдуллин очень верно, что сглаживание и потеря точности могут вызвать проблемы, но поскольку я не видел переменных для этого, я могу только посоветовать вам попробовать это.На самом деле, я согласен со всем, что он говорит, за исключением проблемы с таблицами поиска.

    Прошу прощения за копипаст-кодировку...

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

    Первая мысль:Как насчет некоторой статистики по переменной values?

    • Являются ли значения "value" обычно небольшими -10 <= значение <= 10?

    Если нет, вы, вероятно, можете получить повышение, протестировав значения, выходящие за рамки допустимых значений

    if(value < -10)  return 0;
    if(value > 10)  return 1;
    
    • Часто ли эти значения повторяются?

    Если это так, вы, вероятно, сможете извлечь некоторую выгоду из Запоминание (вероятно, нет, но проверить не помешает ....)

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

    Если ни то, ни другое не может быть применено, то, как предполагали некоторые другие, возможно, вам сойдет с рук снижение точности вашей сигмовидной мышцы...

    Сопрано провел несколько приятных оптимизаций вашего звонка:

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

    Если вы попробуете справочную таблицу и обнаружите, что она использует слишком много памяти, вы всегда можете посмотреть на значение вашего параметра для каждого последующего вызова и использовать некоторую технику кэширования.

    Например, попробуйте кэшировать последнее значение и результат. Если следующий вызов имеет то же значение, что и предыдущий, вам не нужно рассчитывать его, так как вы бы кэшировали последний результат. Если текущий вызов был таким же, как и предыдущий, даже 1 из 100 раз, вы потенциально можете сэкономить 1 миллион вычислений.

    Или вы можете обнаружить, что в течение 10 последовательных вызовов параметр value в среднем один и тот же 2 раза, поэтому вы можете попробовать кэшировать последние 10 значений / ответов.

    Идея: возможно, вы можете создать (большую) справочную таблицу с предварительно рассчитанными значениями?

    Это немного не по теме, но просто из любопытства я выполнил ту же реализацию, что и в C, C# и F# на языке Java.Я просто оставлю это здесь на случай, если кому-то еще будет интересно.

    Результат:

    $ 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 # в моем случае связано с тем, что Java лучше оптимизирована, чем Mono для OS X.В аналогичной 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());
        }
    }
    

    Я понимаю, что прошел год с тех пор, как возник этот вопрос, но я наткнулся на него из-за обсуждения производительности F # и C относительно C #.Я поиграл с некоторыми примерами от других респондентов и обнаружил, что делегаты, похоже, выполняются быстрее, чем обычный вызов метода, но у F # нет явного преимущества в производительности по сравнению с C#.

    • C:166 мс
    • C# (делегат):275 мс
    • C# (метод):431мс
    • C# (метод, счетчик с плавающей точкой):2,656 мс
    • F#:404 мс

    C # со счетчиком с плавающей точкой был прямым портом кода C.Гораздо быстрее использовать int в цикле for.

    Вы также можете поэкспериментировать с альтернативными функциями активации, которые дешевле оценить. Например:

    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 вычислять двойное число? Любой калькулятор показателей, который использует итеративное суммирование (см. расширение e x ) будет занимать больше времени для большей точности, каждый раз. И двойной - это в два раза больше работы одного! Таким образом, вы сначала конвертируете в одиночную, а затем затем выполняете экспоненциальную.

    Но функция expf должна быть еще быстрее. Я не вижу необходимости в приведении сопрано (float) при передаче в expf, если только C # не выполняет неявное преобразование типа float-double.

    В противном случае, просто используйте настоящий язык, например FORTRAN ...

    Здесь есть много хороших ответов.Я бы предложил проработать это до конца эта техника, просто чтобы убедиться

    • Ты не будешь повторять это больше, чем тебе нужно.
      (Иногда функции вызываются чаще, чем необходимо, просто потому, что их так легко вызвать.)
    • Вы не вызываете его повторно с одними и теми же аргументами
      (где вы могли бы использовать запоминание)

    Кстати, функция, которая у вас есть, является обратной логит-функцией,
    или обратная функция логарифмического отношения шансов 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;
    }
    

    Предыдущие результаты были получены благодаря тому, что оптимизатор выполнил свою работу и оптимизировал вычисления.Выполнение кода на самом деле приводит к немного другим и гораздо более интересным результатам (на моем пути медленный 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:

    Есть вещи, которые нужно улучшить, и способы устранить слабые места;как это сделать, оставлено в качестве упражнения читателю :)

    • Настройте диапазон действия функции, чтобы избежать скачка между началом и концом таблицы.
    • Добавьте функцию небольшого шума, чтобы скрыть артефакты сглаживания.
    • Как сказал Рекс, интерполяция может повысить точность при довольно низкой производительности.

    Есть гораздо более быстрые функции, которые делают очень похожие вещи:

    x / (1 + abs(x)) – быстрая замена для TAHN

    И точно так же:

    x / (2 + 2 * abs(x)) + 0.5 - быстрая замена СИГМОВИДНОЙ КИШКИ

    Сравните графики с реальной сигмовидной кишкой

    Выполняя поиск в Google, я нашел альтернативную реализацию функции Sigmoid.

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

    Это правильно для ваших нужд? Это быстрее?

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

    1) Вы называете это только из одного места? Если это так, вы можете получить небольшую производительность, переместив код из этой функции и просто поместив его туда, где вы обычно вызывали бы функцию Sigmoid. Мне не нравится эта идея с точки зрения читабельности кода и организации, но когда вам нужно получить каждый последний выигрыш в производительности, это может помочь, потому что я думаю, что вызовы функций требуют проталкивания / выталкивания регистров в стеке, чего можно было бы избежать, если ваш код был весь встроенный.

    2) Я понятия не имею, может ли это помочь, но попробуйте сделать ваш параметр функции параметром ref. Посмотри, быстрее ли это. Я бы предложил сделать его const (что было бы оптимизацией, если бы это было в c ++), но c # не поддерживает параметры const.

    Если вам нужен гигантский прирост скорости, вы, вероятно, могли бы изучить распараллеливание функции с помощью (ge) силы. IOW, используйте DirectX для управления видеокартой, чтобы сделать это для вас. Я понятия не имею, как это сделать, но я видел людей, использующих видеокарты для всех видов вычислений.

    Я видел, что многие люди здесь пытаются использовать приближение, чтобы сделать сигмоид быстрее. Тем не менее, важно знать, что сигмоид также может быть выражен с использованием tanh, а не только exp. Этот способ вычисления сигмоида примерно в 5 раз быстрее, чем с экспоненциальным, и, используя этот метод, вы ничего не приближаете, поэтому исходное поведение сигмоиды сохраняется как есть.

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

    Конечно, пареллизация была бы следующим шагом к повышению производительности, но что касается необработанных вычислений, использование Math.Tanh быстрее, чем Math.Exp.

    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top