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

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

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

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

Какой диапазон входных значений?

Bill the Lizard 05.01.2009 04:06

Кроме того, насколько точны входные значения? Сколько десятичных цифр для вас важно?

Bill the Lizard 05.01.2009 04:07

На самом деле, чем больше, тем лучше, но я бы сказал, что около 6-7 десятичных цифр.

hb. 05.01.2009 04:17

Какой диапазон ввода значения?

Sophie Alpert 05.01.2009 04:18

Он варьируется от запуска к запуску, но обычно от -10,00000 до +10,000000. Изменил его на float и работает, за исключением некоторого литья в некоторых классах

hb. 05.01.2009 04:24

Есть ли простой способ убедиться, что метод встроен? Возможно финальный модификатор?

jjnguy 05.01.2009 04:27

p.s. Я новичок в C#, поэтому только догадываюсь.

jjnguy 05.01.2009 04:31

+1 за профилирование, прежде чем вы решите, что вам нужно оптимизировать!

erikkallen 05.01.2009 19:59

@jjnguy: Я не уверен, что есть способ заставить JIT-выполнение встроить метод.

Jimmy 06.01.2009 21:31
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
60
9
24 042
25
Перейти к ответу Данный вопрос помечен как решенный

Ответы 25

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

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

hb. 05.01.2009 04:05

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

Andrew not the Saint 05.01.2009 04:08

Я знаю - провел быструю проверку, получил OutOfMemoryException. функция сопрано помогла кучи

hb. 05.01.2009 04:23

Время поиска будет быстрее, чем время Math.Exp?

FerranB 05.01.2009 04:24

Возможно, таблица, объединенная с интерполяцией, = с использованием математики с фиксированной точкой (т.е. масштабированных целых чисел) будет работать. Раньше процессор 88000 DSP поддерживал это в машинном коде.

Bdoserror 05.01.2009 04:24

Большой поиск имеет меньше шансов подобрать кеш процессора

Rinat Abdullin 07.01.2009 07:37
Ответ принят как подходящий

Пытаться:

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

Обновлено еще раз: Я не уверен, сколько накладных расходов у функций C#, но если вы используете #include <math.h> в исходном коде, вы сможете использовать это, которое использует функцию float-exp. Это могло быть немного быстрее.

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

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

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

Jeremy 05.01.2009 06:23

Вы изменили знак значения, моя математика ржавая, но я не думаю, что это то же самое ... у вас должен быть Math.Exp (-value) в соответствии с исходным кодом.

Marcel Popescu 05.01.2009 13:51

@Marcel: Нет, он изменил значение e ^ на 1 / (значение e ^), затем добавил 1.0 и поменял местами числитель / знаменатель.

lacop 05.01.2009 16:58

Пожалуйста, простите меня, зачем конвертировать в float? Разве число с плавающей запятой не является производным от числа с двойной точностью? Кажется, в таком случае лучше было бы использовать double?

Rusty Nail 01.07.2016 11:36

Так это 1 / (1+k) или k / (1+k)?

Aaron Franke 06.12.2018 11:24

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

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

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

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

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

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

Попробую это банкомат, я отправлю ответ через минуту

hb. 05.01.2009 04:44

Если это для функции активации, имеет ли значение, если вычисление 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 ()).

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

Ого. Это еще больше снизило его!

hb. 05.01.2009 07:54

Если вы умножаете или делите на степень двойки, сдвигаете влево или вправо (<< и >>), вместо использования умножения / деления, это будет намного быстрее.

nicodemus13 24.05.2012 19:14

@ nicodemus13 - это будет работать для целочисленных случаев, хотя на современных процессорах не обязательно быстрее, чем просто умножение. Но вы действительно можете позволить компилятору выполнить такую ​​оптимизацию.

Neil Coffey 24.05.2012 19:17

Но не думайте, что ваши представления о тайминге процессора и оптимизации 20-летней давности все еще применимы. Вы вполне можете обнаружить, что ваш процессор может выполнять умножение FP одновременно с целочисленным сдвигом ...

Neil Coffey 24.05.2012 19:24

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

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

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

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

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

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

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

Мемоизация в этом случае, вероятно, стоит довольно дорого.

Henrik Gustafsson 05.01.2009 06:27

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

Stobor 05.01.2009 07:38

Мы имеем дело с поплавками и нейронными сетями, я предполагаю, что значения будут везде :)

Henrik Gustafsson 05.01.2009 10:59

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

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

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


ДЕЛАТЬ:

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

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

Вы занимаетесь математикой с фиксированной точкой. Я предпочитаю использовать коэффициент масштабирования 2 ^ n, например 256, 1024, 65536. Идеальные - 2 ^ 8 или 2 ^ 16, тогда вы можете просто взять байты, чтобы получить целую часть. Вероятно, вы получите еще больший прирост, если во всем приложении будут использоваться таблицы с фиксированной точкой.

Rex Logan 05.01.2009 06:22

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

Henrik Gustafsson 05.01.2009 06:31

Кроме того, я не использовал какие-либо более волшебные оптимизации с использованием бит-тиддлинга, поскольку код должен быть 1. читаемым, 2. легко переводимым на C# (где большая часть его в любом случае будет бесполезна).

Henrik Gustafsson 05.01.2009 06:33

Конечно, использование 32-битных целых чисел повсюду, вероятно, будет самым быстрым, но, учитывая характер сигмовидной функции, математика с фиксированной точкой может быть не лучшим выбором. (Мне не нравится лимит комментариев :)

Henrik Gustafsson 05.01.2009 06:34

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

Rex Logan 05.01.2009 07:20

Конечно, здесь очень хорошо сработала бы интерполяция, но я не думаю, что она стоит дополнительных сложностей. При использовании только 2 тыс. Точек данных была обнаружена ошибка <0,25% в пределах диапазона таблицы. Сразу за его пределами он подскакивает почти до 0,5%, но это можно исправить, отрегулировав покрытие стола.

Henrik Gustafsson 05.01.2009 09:30

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

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

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

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

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

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

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

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

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

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

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

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

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

Обновлено:Публикация в справочных таблицах для функций активации ИНС

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

Я уже распараллеливаю части кода, некоторые с помощью Parallel.For, а некоторые с помощью PLINQ. Пока все заработало большой

hb. 05.01.2009 08:51

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

Rinat Abdullin 05.01.2009 09:46

Использование LUT для каждого возможного значения двойного диапазона съело бы всю вашу память, но если используются числа с плавающей запятой и возможна некоторая потеря точности, таблица может уместиться менее чем в 4 КБ памяти. Ваше утверждение 1 неверно.

Henrik Gustafsson 05.01.2009 10:22

Если ошибка ~ 0,1% в функции активации может быть принята, C-пример, который я опубликовал ниже, будет работать, если вы немного его исправите (первые два пункта в TODO должны сделать это :)

Henrik Gustafsson 05.01.2009 10:56

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

hb. 05.01.2009 11:06

Я бы не сказал, что 4k - это много в любом случае, потому что это только один раз для каждого процесса :) Почему я пытался использовать 4k, это, конечно, чтобы не выходить за границы страницы. Это легко гарантировать в C, но, я полагаю, сложнее в C#; ymmv на этом

Henrik Gustafsson 05.01.2009 11:40

4к это довольно мягкое занижение)) rabdullin.com/journal/2009/1/5/…

Rinat Abdullin 05.01.2009 17:58

Вы серьезно не пользуетесь хеш-таблицей? Эта версия здесь уже обсуждалась и уже несколько раз отвергалась. Ты хоть мой код читал? Да, и прочтите это, пока вы на нем: en.wikipedia.org/wiki/Lookup_table

Henrik Gustafsson 05.01.2009 20:15

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

Henrik Gustafsson 05.01.2009 20:18

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

Rinat Abdullin 05.01.2009 21:34

«Худшие результаты тренировки» == требуется больше попыток, чтобы получить достаточно хороший результат тренировки.

Rinat Abdullin 05.01.2009 21:36

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

Henrik Gustafsson 05.01.2009 22:12

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

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

+1 за нестандартное мышление и использование аппаратного ускорения.

nicodemus13 24.05.2012 19:18

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

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

Ваша бумажная ссылка сейчас не работает.

fig 26.02.2014 20:08

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

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

Когда в Math.Exp появились числа с плавающей запятой?

Jimmy 06.01.2009 22:02

Взгляните на эта почта. у него есть приближение для 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 с плавающей запятой.

Обновлено: Согласно Специальный соус это примерно в 3,25 раза быстрее, чем реализация CLR. Спасибо!

Любопытно: Можете ли вы упростить (1072693248-60801) как 1072632447? Кроме того, можете ли вы взять его за пределы дальнего заброса, чтобы он не добавлялся к дублю для скорости? Влияет ли это на точность и / или производительность?

strager 05.01.2009 22:32

(Оглядываясь назад: я понимаю, что вычитание, вероятно, оптимизировано компилятором, но все равно стоит попробовать.)

strager 05.01.2009 22:33

@strager верно, это наверняка оптимизировано компилятором. Я оставил его таким, потому что отчасти формула была разработана таким образом, но вы можете заменить его на 1072632447.

martinus 05.01.2009 22:53

@martinus, другую технику пробовали? Записываем в виде: long tmp = (long) (1512775 * val) + 1072632447;

strager 05.01.2009 23:10

Использование этого в сообщении Хенрика Sigmoid1 / Sigmoid2 предлагает улучшение примерно на 50% (900 мс против 500 мс). Однако не проверял точность.

hb. 06.01.2009 09:24

Я протестировал эту очень простую функцию на C# и обнаружил, что она ~ В 3,25 раза быстрее, а не реализация CLR. Чтобы получить представление об уровне ошибки, вот пять случайных пар примеров (результат CLR, приблизительный результат) с разными порядками величины: (0.0007242, 0.0007376), (1.55306, 1.57713), (307.78015, 309.18896), (1093286.54660, 1050935.0), (9.76825E+30, 9.57295E+30).

Special Sauce 22.01.2017 08:06

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

Примечание: Это продолжение сообщения это.

Редактировать: Обновите, чтобы вычислить то же, что и это и это, взяв за основу это.

А теперь посмотри, что ты заставил меня сделать! Вы заставили меня установить 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

С вряд ли стоит затраченных усилий, мир движется вперед :)

Итак, чуть более 10 раз в 6 быстрее. Кто-то с коробкой Windows может исследовать использование памяти и производительность с помощью MS-вещей :)

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

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

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

Спасибо за обновления. Обратите внимание, что вы можете выбрать другое измерение ошибки, иначе измените -6.0f; x <6.0f до 7f увеличивает ошибку до 100%.

Rinat Abdullin 06.01.2009 07:59

Дополнительная компиляция вашего кода в режиме выпуска и запуск его без подключенного отладчика дала результаты: 1195 мс против 41 мс. Это более чем в 10 раз быстрее))

Rinat Abdullin 06.01.2009 08:00

Но исправление Sigmoid1 снизило преимущество в скорости до 10 раз. Кроме того, можно улучшить Sigmoid2 на 2 мс, сохранив промежуточное значение. См .: rabdullin.com/journal/2009/1/5/…

Rinat Abdullin 06.01.2009 09:06

Я не стал там умничать. Я подумал, что лучше сделать это очень простым и близким к en.wikipedia.org/wiki/Approximation_error. Чтобы хорошо справиться с v1 = 0, потребуется что-то совершенно другое, но это неотъемлемая, хорошо известная и понятная слабость этого измерения.

Henrik Gustafsson 06.01.2009 14:18

Что касается «локальных экстремумов», один из способов, который мы использовали, чтобы избавиться от эффектов этого в проекте (совершенно не связанный с машинным обучением), заключался в добавлении шума к сигналу. Я думаю, что добавление этого недетерминизма поможет алгоритму обучения не застрять там.

Henrik Gustafsson 06.01.2009 14:19

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

Rinat Abdullin 06.01.2009 14:38

PS: Реализация F# опубликована. Это даже быстрее.

Rinat Abdullin 06.01.2009 14:58

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

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

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

Более подробную информацию можно найти в Сообщение блога. Вот фрагмент кода JIC на 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);

И вывод (компиляция релиза для 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

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

Henrik Gustafsson 06.01.2009 15:07

Эй, вы делаете 10 ^ 7 итераций, не так ли?

Henrik Gustafsson 06.01.2009 19:36

Да, да. Я исправил эту опечатку в сообщении блога и забыл об этом фрагменте. Спасибо. Re C фрагмент, я запустил его на своей машине чуть позже. Просто нужно получить компилятор C.

Rinat Abdullin 06.01.2009 19:57

Это были мои числа в моно: 10 ^ 7 итераций с использованием sigmoid1: 1661,244000 мс 10 ^ 7 итераций с использованием sigmoid2: 732,762000 мс

Henrik Gustafsson 07.01.2009 00:42

@ Ринат Абдуллин: Ваш тест неверен. Эффект, который вы наблюдаете, заключается в использовании float в качестве счетчика в цикле for в C#. Если вы используете int в качестве счетчика и делегата в C# для выполнения сигмовидного алгоритма, как в F#, C# будет немного быстрее. thinkfulcode.wordpress.com/2010/12/30/…

Brian Reiter 31.12.2010 03:00

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

Просто хочу, чтобы оно было полным :)

Henrik Gustafsson 08.01.2009 19:03

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

Хорошая вещь! Поскольку F# - это .NET, вы думаете, что можно было бы включить и его? research.microsoft.com/en-us/downloads/…

Henrik Gustafsson 06.01.2009 23:26

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

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

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

Я понимаю, что прошел год с тех пор, как этот вопрос возник, но я столкнулся с ним из-за обсуждения производительности 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

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

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

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