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

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

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

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

24 ответа

Решение

Пытаться:

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 раз - не используйте для этого 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 ()).

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

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

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

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

РЕДАКТИРОВАТЬ: Согласно Special Sauce это примерно в 3,25 раза быстрее, чем реализация CLR. Спасибо!

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

ОБНОВЛЕНИЕ: Опубликовать таблицы поиска для функций активации ANN

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

Если вы можете взаимодействовать с 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 и запустите очень тривиальный фрагментный шейдер).

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

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

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

Пустая функция:       79мс 0
Оригинал:             1576мс 0,7202294
Упрощенно: (сопрано) 681мс 0,7202294
Приблизительно: (Нил) 441мс 0,7198783
Бит Манип: (мартинус) 836мс 0,72318
Тейлор: (Рекс Логан) 261мс 0,7202305
Поиск: (Хенрик) 182мс 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# имеет лучшую производительность, чем C# в математических алгоритмах.NET. Таким образом, переписывание нейронной сети на F# может улучшить общую производительность.

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

  • выполняет тест sigmoid1 за 588,8 мс вместо 3899,2 мс
  • выполняет тест Sigmoid2 (LUT) за 156,6 мс вместо 411,4 мс

Более подробную информацию можно найти в блоге. Вот фрагмент кода F#:

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

И вывод (компиляция релиза против FTP 1.9.6.2 FTP без отладчика):

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 получает возможность исследовать использование памяти и производительность с помощью MS-материала:)

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

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

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

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

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

Сначала подумал: как насчет статистики по переменной values?

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

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

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

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

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# (метод, счетчик с плавающей запятой): 2656мс
  • F#: 404 мс

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

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

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

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

И так же:

x / (2 + 2 * abs(x)) + 0.5 - быстрая замена для SIGMOID

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

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

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

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

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

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

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

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

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

профиль


СДЕЛАТЬ:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Помните, что результаты сигмовидных ограничений находятся в диапазоне от 0 до 1. Значения меньше примерно -10 возвращают значение, очень, очень близкое к 0,0. Значения больше примерно 10 возвращают значение, очень, очень близкое к 1.

В старые времена, когда компьютеры не могли так хорошо справляться с арифметическим переполнением / потерей значимости, установка условий для ограничения вычислений была обычным делом. Если бы меня действительно беспокоила его производительность (или, в основном, производительность Math), я бы изменил ваш код на старомодный способ (и не обращал внимания на ограничения), чтобы он не вызывал Math без необходимости:

public double Sigmoid(double value)
{
    if (value < -45.0) return 0.0;
    if (value > 45.0) return 1.0;
    return 1.0 / (1.0 + Math.Exp(-value));
}

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

Делая поиск в 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

Другие вопросы по тегам