Я профилировал приложение весь день и, оптимизировав пару битов кода, остался с этим в моем списке дел. Это функция активации нейронной сети, которая вызывается более 100 миллионов раз. По данным dotTrace, это составляет около 60% от общего времени работы.
Как бы вы это оптимизировали?
public static float Sigmoid(double value) {
return (float) (1.0 / (1.0 + Math.Pow(Math.E, -value)));
}
Кроме того, насколько точны входные значения? Сколько десятичных цифр для вас важно?
На самом деле, чем больше, тем лучше, но я бы сказал, что около 6-7 десятичных цифр.
Какой диапазон ввода значения?
Он варьируется от запуска к запуску, но обычно от -10,00000 до +10,000000. Изменил его на float и работает, за исключением некоторого литья в некоторых классах
Есть ли простой способ убедиться, что метод встроен? Возможно финальный модификатор?
p.s. Я новичок в C#, поэтому только догадываюсь.
+1 за профилирование, прежде чем вы решите, что вам нужно оптимизировать!
@jjnguy: Я не уверен, что есть способ заставить JIT-выполнение встроить метод.





Идея: Возможно, вы можете создать (большую) справочную таблицу с предварительно рассчитанными значениями?
Я попробую. Будем надеяться, что стол не вырастет до титанических размеров.
hb: это может иметь очень неприятные последствия. Если вы не уверены в максимальном размере, вам придется реализовать структуру с ограниченным размером (вроде кеша), и это нетривиальная задача.
Я знаю - провел быструю проверку, получил OutOfMemoryException. функция сопрано помогла кучи
Время поиска будет быстрее, чем время Math.Exp?
Возможно, таблица, объединенная с интерполяцией, = с использованием математики с фиксированной точкой (т.е. масштабированных целых чисел) будет работать. Раньше процессор 88000 DSP поддерживал это в машинном коде.
Большой поиск имеет меньше шансов подобрать кеш процессора
Пытаться:
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);
}
Кроме того, если вы выполняете миллионы вызовов, накладные расходы на вызов функций могут быть проблемой. Попробуйте создать встроенную функцию и посмотрите, поможет ли это.
Знаете ли вы, что они варьируются в пределах возможного значения параметра? Если да, рассмотрите возможность создания таблицы поиска.
Вы изменили знак значения, моя математика ржавая, но я не думаю, что это то же самое ... у вас должен быть Math.Exp (-value) в соответствии с исходным кодом.
@Marcel: Нет, он изменил значение e ^ на 1 / (значение e ^), затем добавил 1.0 и поменял местами числитель / знаменатель.
Пожалуйста, простите меня, зачем конвертировать в float? Разве число с плавающей запятой не является производным от числа с двойной точностью? Кажется, в таком случае лучше было бы использовать double?
Так это 1 / (1+k) или k / (1+k)?
При 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
Попробую это банкомат, я отправлю ответ через минуту
Если это для функции активации, имеет ли значение, если вычисление 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# тоже должен это делать.
Ого. Это еще больше снизило его!
Если вы умножаете или делите на степень двойки, сдвигаете влево или вправо (<< и >>), вместо использования умножения / деления, это будет намного быстрее.
@ nicodemus13 - это будет работать для целочисленных случаев, хотя на современных процессорах не обязательно быстрее, чем просто умножение. Но вы действительно можете позволить компилятору выполнить такую оптимизацию.
Но не думайте, что ваши представления о тайминге процессора и оптимизации 20-летней давности все еще применимы. Вы вполне можете обнаружить, что ваш процессор может выполнять умножение FP одновременно с целочисленным сдвигом ...
Первая мысль: как насчет статистики по переменной значений?
Если нет, вы, вероятно, можете получить импульс, протестировав значения за пределами границ.
if (value < -10) return 0;
if (value > 10) return 1;
Если да, то, вероятно, вы можете получить некоторую выгоду от Мемоизация (возможно, нет, но проверить не помешает ....)
if (sigmoidCache.containsKey(value)) return sigmoidCache.get(value);
Если ни один из них не может быть применен, то, как предлагали некоторые другие, возможно, вам удастся снизить точность вашей сигмовидной кишки ...
Мемоизация в этом случае, вероятно, стоит довольно дорого.
Хенрик: Вполне возможно, да. Это может иметь смысл в зависимости от того, как часто в функцию передается одно и то же значение. Я не уверен, как остальная часть алгоритма использует эту функцию, но он может повторно вызывать ее много раз без необходимости.
Мы имеем дело с поплавками и нейронными сетями, я предполагаю, что значения будут везде :)
(Обновлено с учетом измерений производительности) (Обновлено снова с реальными результатами :)
Я думаю, что решение с помощью таблицы поиска поможет вам очень далеко, когда дело доходит до производительности, с незначительными затратами на память и точность.
Следующий фрагмент - это пример реализации на 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

ДЕЛАТЬ:
Есть вещи, которые нужно улучшить, и способы устранения слабых мест; как это сделать - это упражнение для читателя :)
Вы занимаетесь математикой с фиксированной точкой. Я предпочитаю использовать коэффициент масштабирования 2 ^ n, например 256, 1024, 65536. Идеальные - 2 ^ 8 или 2 ^ 16, тогда вы можете просто взять байты, чтобы получить целую часть. Вероятно, вы получите еще больший прирост, если во всем приложении будут использоваться таблицы с фиксированной точкой.
Ну, я намеренно не использовал математику с фиксированной точкой. Я мог бы, конечно, но я бы предпочел избегать этого дополнительного источника псевдонимов.
Кроме того, я не использовал какие-либо более волшебные оптимизации с использованием бит-тиддлинга, поскольку код должен быть 1. читаемым, 2. легко переводимым на C# (где большая часть его в любом случае будет бесполезна).
Конечно, использование 32-битных целых чисел повсюду, вероятно, будет самым быстрым, но, учитывая характер сигмовидной функции, математика с фиксированной точкой может быть не лучшим выбором. (Мне не нравится лимит комментариев :)
Другой метод, который я использовал, - это создать таблицу, которая представляет собой массив чисел с плавающей запятой, а затем интерполировать между точками. Чтобы избавиться от эффектов квантования. В зависимости от того, как вы его построите, он все равно может быть быстрым. Может хорошо работать для такой функции, которая сжимает диапазон.
Конечно, здесь очень хорошо сработала бы интерполяция, но я не думаю, что она стоит дополнительных сложностей. При использовании только 2 тыс. Точек данных была обнаружена ошибка <0,25% в пределах диапазона таблицы. Сразу за его пределами он подскакивает почти до 0,5%, но это можно исправить, отрегулировав покрытие стола.
У Сопрано было несколько хороших оптимизаций для вашего звонка:
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], что также является диапазоном).
Обновлено:Публикация в справочных таблицах для функций активации ИНС
ОБНОВЛЕНИЕ2: Я удалил точку на LUT, так как я перепутал их с полным хешированием. Спасибо Хенрик Густафссон за то, что вернули меня на трассу. Таким образом, память не является проблемой, хотя пространство поиска все еще немного испорчено из-за локальных экстремумов.
Я уже распараллеливаю части кода, некоторые с помощью Parallel.For, а некоторые с помощью PLINQ. Пока все заработало большой
Да, подозреваю. Но все самое интересное начинается, когда алгоритм обучения (я использую эволюционный алгоритм, который позволяет выбирать структуру сети) запускается на нескольких машинах))
Использование LUT для каждого возможного значения двойного диапазона съело бы всю вашу память, но если используются числа с плавающей запятой и возможна некоторая потеря точности, таблица может уместиться менее чем в 4 КБ памяти. Ваше утверждение 1 неверно.
Если ошибка ~ 0,1% в функции активации может быть принята, C-пример, который я опубликовал ниже, будет работать, если вы немного его исправите (первые два пункта в TODO должны сделать это :)
Я попробую. Я также пытаюсь минимизировать использование памяти (не связанное с вашим сообщением) - очевидно .NET считает, что неплохо передать огромный массив вокруг ... мля ..
Я бы не сказал, что 4k - это много в любом случае, потому что это только один раз для каждого процесса :) Почему я пытался использовать 4k, это, конечно, чтобы не выходить за границы страницы. Это легко гарантировать в C, но, я полагаю, сложнее в C#; ymmv на этом
4к это довольно мягкое занижение)) rabdullin.com/journal/2009/1/5/…
Вы серьезно не пользуетесь хеш-таблицей? Эта версия здесь уже обсуждалась и уже несколько раз отвергалась. Ты хоть мой код читал? Да, и прочтите это, пока вы на нем: en.wikipedia.org/wiki/Lookup_table
Псевдонимы могут просто плохо повлиять на вещи, но могут быть способы исправить это, а выигрыш от LUT потенциально велик, и, как правило, стоит хотя бы изучить. Поскольку он будет часто использоваться, он почти наверняка будет горячим в кеше.
Хм ... моя вина. Я изначально перепутал LUT с полным хешированием. Но, тем не менее, использование такого подхода просто не сработало в моих сценариях и привело к худшим результатам тренировок.
«Худшие результаты тренировки» == требуется больше попыток, чтобы получить достаточно хороший результат тренировки.
Я опубликовал новый ответ с реализацией C#, я также включил несколько ссылок на некоторые документы, касающиеся введенных ошибок. У меня не было времени прочитать их все, но в целом они, кажется, говорят, что квантование имеет хорошие и плохие особенности, но большинством из них можно управлять, используя соответствующие методы.
Если вы можете взаимодействовать с 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 за нестандартное мышление и использование аппаратного ускорения.
Вне всяких сомнений, в этой статье объясняется способ аппроксимации экспоненты путем злоупотребления числами с плавающей запятой. (щелкните ссылку в правом верхнем углу для PDF), но я не знаю, будет ли это полезно для вас в .NET.
Еще один момент: для быстрого обучения больших сетей логистическая сигмоида, которую вы используете, довольно ужасна. См. Раздел 4.4 Эффективное обратное распространение, LeCun et al. и используйте что-нибудь с нулевым центром (на самом деле, прочтите всю статью, это очень полезно).
Ваша бумажная ссылка сейчас не работает.
Мягкая вариация на тему Сопрано:
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 появились числа с плавающей запятой?
Взгляните на эта почта. у него есть приближение для 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 верно, это наверняка оптимизировано компилятором. Я оставил его таким, потому что отчасти формула была разработана таким образом, но вы можете заменить его на 1072632447.
@martinus, другую технику пробовали? Записываем в виде: long tmp = (long) (1512775 * val) + 1072632447;
Использование этого в сообщении Хенрика Sigmoid1 / Sigmoid2 предлагает улучшение примерно на 50% (900 мс против 500 мс). Однако не проверял точность.
Я протестировал эту очень простую функцию на 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).
Если вам нужен гигантский прирост скорости, вы, вероятно, могли бы рассмотреть возможность распараллеливания функции с помощью силы (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 для функций активации - не такая уж редкость, особенно при аппаратной реализации. Есть много хорошо зарекомендовавших себя вариантов этой концепции, если вы хотите включить эти типы таблиц. Однако, как уже указывалось, сглаживание может оказаться проблемой, но есть и способы обойти это. Дальнейшее чтение:
Некоторые проблемы с этим:
Простите за кодирование копипаста ...
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%.
Дополнительная компиляция вашего кода в режиме выпуска и запуск его без подключенного отладчика дала результаты: 1195 мс против 41 мс. Это более чем в 10 раз быстрее))
Но исправление Sigmoid1 снизило преимущество в скорости до 10 раз. Кроме того, можно улучшить Sigmoid2 на 2 мс, сохранив промежуточное значение. См .: rabdullin.com/journal/2009/1/5/…
Я не стал там умничать. Я подумал, что лучше сделать это очень простым и близким к en.wikipedia.org/wiki/Approximation_error. Чтобы хорошо справиться с v1 = 0, потребуется что-то совершенно другое, но это неотъемлемая, хорошо известная и понятная слабость этого измерения.
Что касается «локальных экстремумов», один из способов, который мы использовали, чтобы избавиться от эффектов этого в проекте (совершенно не связанный с машинным обучением), заключался в добавлении шума к сигналу. Я думаю, что добавление этого недетерминизма поможет алгоритму обучения не застрять там.
Повторный шум на тренировках - да, это известный трюк. Я слышал, что он используется для обучения аппаратных ANN (анализ и реакция среды в реальном времени) с некоторыми приличными результатами.
PS: Реализация F# опубликована. Это даже быстрее.
F# имеет лучшую производительность, чем C# в математических алгоритмах .NET. Таким образом, переписывание нейронной сети на F# может улучшить общую производительность.
Если мы повторно реализуем Фрагмент сравнительного анализа LUT (я использовал слегка измененную версию) в F#, то полученный код:
Более подробную информацию можно найти в Сообщение блога. Вот фрагмент кода 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#, а наши машины настолько сильно отличаются, что я думаю, что будет лучше, если вы запустите тест. Следите за обновлениями
Эй, вы делаете 10 ^ 7 итераций, не так ли?
Да, да. Я исправил эту опечатку в сообщении блога и забыл об этом фрагменте. Спасибо. Re C фрагмент, я запустил его на своей машине чуть позже. Просто нужно получить компилятор C.
Это были мои числа в моно: 10 ^ 7 итераций с использованием sigmoid1: 1661,244000 мс 10 ^ 7 итераций с использованием sigmoid2: 732,762000 мс
@ Ринат Абдуллин: Ваш тест неверен. Эффект, который вы наблюдаете, заключается в использовании float в качестве счетчика в цикле for в C#. Если вы используете int в качестве счетчика и делегата в C# для выполнения сигмовидного алгоритма, как в F#, C# будет немного быстрее. thinkfulcode.wordpress.com/2010/12/30/…
Это немного не по теме, но просто из любопытства я сделал ту же реализацию, что и в 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());
}
}
Просто хочу, чтобы оно было полным :)
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/…
Здесь есть много хороших ответов. Я бы посоветовал запустить его через эта техника, просто чтобы убедиться
Кстати, у вас есть функция обратного логита,
или обратная логарифмическая функция отношения шансов log(f/(1-f)).
Я понимаю, что прошел год с тех пор, как этот вопрос возник, но я столкнулся с ним из-за обсуждения производительности F# и C по сравнению с C#. Я поиграл с некоторыми примерами от других респондентов и обнаружил, что делегаты выполняются быстрее, чем вызов обычного метода, но у F# нет очевидного преимущества в производительности перед C#.
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. Помните, как вышеперечисленное влияет на другие аспекты ваших тренировочных результатов.
Какой диапазон входных значений?