Математическая оптимизация в C # - PullRequest
53 голосов
/ 05 января 2009

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

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

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

Ответы [ 24 ]

4 голосов
/ 05 января 2009

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

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

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

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

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

4 голосов
/ 05 января 2009

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

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

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

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

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

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

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

2 голосов
/ 05 января 2009

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

2 голосов
/ 06 января 2009

Это немного не по теме, но просто из любопытства я реализовал ту же реализацию, что и в 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());
    }
}
2 голосов
/ 31 декабря 2010

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

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

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

1 голос
/ 29 октября 2016

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

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

И аналогично:

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

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

1 голос
/ 29 ноября 2009

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

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

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

1 голос
/ 05 января 2009

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

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

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

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

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

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

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

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

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

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

1 голос
/ 05 января 2009

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

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 ...

...