Подтвердить что ты не робот

Математическая оптимизация в С#

Я прорабатывал приложение в течение всего дня, и, оптимизировав пару бит кода, я остался с этим в списке задач. Это активационная функция для нейронной сети, которая вызывается более 100 миллионов раз. Согласно dotTrace, это составляет около 60% от общего времени работы.

Как бы вы это оптимизировали?

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

Ответ 1

Try:

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

EDIT: Я сделал быстрый тест. На моей машине приведенный выше код примерно на 43% быстрее, чем ваш метод, и этот математически эквивалентный код является самым младшим бит быстрее (на 46% быстрее оригинала):

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

РЕДАКТИРОВАТЬ 2: Я не уверен, сколько накладных функций С# есть, но если вы #include <math.h> в своем исходном коде, вы должны использовать это, в котором используется float-exp функция. Это может быть немного быстрее.

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

Кроме того, если вы выполняете миллионы вызовов, это может быть проблемой. Попробуйте сделать встроенную функцию и посмотрите, не поможет ли она.

Ответ 2

Если это для функции активации, имеет ли это значение очень сильно, если вычисление e ^ x является полностью точным?

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

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

Ответ 3

Посмотрите этот пост. он имеет аппроксимацию для e ^ x, написанную на Java, это должен быть код С# для него (untested):

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

В моих тестах это больше, чем в 5 раз быстрее, чем Math.exp() (на Java). Аппроксимация основана на статье "" Быстрая, компактная аппроксимация экспоненциальной функции", которая была разработана точно для использования в нейронных сетях. Это в основном то же самое, что и таблица поиска 2048 записей и линейное приближение между элементами, но все это с помощью трюков с плавающей точкой IEEE.

EDIT: В соответствии с Special Sauce это на 3,25 раза быстрее, чем реализация CLR. Спасибо!

Ответ 4

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

UPDATE: Опубликовать таблицы поиска для функций активации ANN

UPDATE2: Я удалил точку на LUT, так как я смутил их с полным хешированием. Благодарим вас за Хенрик Густафссон за то, что вернули меня на трассу. Таким образом, память не является проблемой, хотя пространство поиска по-прежнему немного перепутано с локальными экстремумами.

Ответ 5

При 100 миллионах звонков, я бы начал задаваться вопросом, не наносит ли чрезмерные издержки профилировщика ваши результаты. Замените вычисление на no-op и убедитесь, что он все еще сообщает, что он потребляет 60% времени выполнения...

Или еще лучше создать некоторые тестовые данные и использовать таймер секундомера для профилирования миллионов звонков.

Ответ 6

Если вы можете взаимодействовать с С++, вы можете рассмотреть сохранение всех значений в массиве и их цикл через 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 миллионов элементов за полсекунды. Однако выделение столько памяти за один раз обойдется вам почти в две трети гигабайта, поэтому я предлагаю обрабатывать больше, но меньше массивов одновременно. Возможно, вы даже захотите использовать двойной буферный подход с элементами 100K или более.

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

Ответ 7

FWIW, здесь мои тесты С# для ответов уже отправлены. (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));
}

Ответ 8

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

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

Ответ 9

Примечание: Это продолжение .

Изменить: Обновить, чтобы рассчитать то же самое, что this и this, немного вдохнув из this.

Теперь посмотри, что ты заставлял меня делать! Вы заставили меня установить 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 быстрее. Кто-то, у которого есть окно, позволяет исследовать использование и производительность памяти с использованием MS-материалов:)

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

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

  • Ошибка возрастает, когда вы выходите за пределы таблицы (но сходится к 0 в крайних случаях); для x приблизительно + -7,0. Это обусловлено выбранным коэффициентом масштабирования. Большие значения SCALE дают более высокие ошибки в среднем диапазоне, но меньше по краям.
  • Это, как правило, очень глупый тест, и я не знаю С#, это просто конверсия моего 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());
    }
}

Ответ 10

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

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

  • выполняет тест sigmoid1 в 588.8ms вместо 3899,2ms
  • выполняет тест sigmoid2 (LUT) в 156.6ms вместо 411.4 ms

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

UPDATE: обновленный бенчмаркинг для использования итераций 10 ^ 7, чтобы результаты были сопоставимы с C

UPDATE2: приведены результаты производительности C-реализации на той же машине, что и для сравнения:

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

Ответ 11

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

  • Значения "значение" обычно малы -10 <= значение <= 10?

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

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

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

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

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

Ответ 12

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

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

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

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

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

Ответ 13

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

Ответ 14

Это немного не по теме, но из-за любопытства я сделал ту же реализацию, что и в 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

Я полагаю, что улучшение по сравнению с С# в моем случае связано с тем, что 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());
    }
}

Ответ 15

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

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

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

Ответ 16

(Обновлено с измерением производительности) (Обновлено снова с реальными результатами:)

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

Следующий фрагмент представляет собой пример реализации в 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:

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

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

Ответ 17

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

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

(который может быть учтен как

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

за одно меньшее умножение). Эта функция имеет нечетную симметрию, а ее производная тривиальна. Использование его для нейронной сети требует нормализации суммы входов путем деления на общее количество входов (ограничение домена на [-1.1], которое также находится в диапазоне).

Ответ 18

Мягкая вариация на тему сопрано:

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

Поскольку вы только после получения одного результата точности, почему функция Math.Exp вычисляет двойной? Любой калькулятор экспоненты, который использует итеративное суммирование (см. расширение e x), займет больше времени для большей точности, каждый раз. И удвоение вдвое больше работы сингла! Таким образом, вы сначала конвертируете в один, , затем выполняете свою экспоненту.

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

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

Ответ 19

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

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

Кстати, у вас есть функция обратного логита,
или обратной функции log-odds-ratio log(f/(1-f)).

Ответ 21

Выполняя поиск 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

Ответ 22

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

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

Ответ 23

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

Ответ 24

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

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

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