Математическая оптимизация в С#
Я прорабатывал приложение в течение всего дня, и, оптимизировав пару бит кода, я остался с этим в списке задач. Это активационная функция для нейронной сети, которая вызывается более 100 миллионов раз. Согласно dotTrace, это составляет около 60% от общего времени работы.
Как бы вы это оптимизировали?
public static float Sigmoid(double value) {
return (float) (1.0 / (1.0 + Math.Pow(Math.E, -value)));
}
Ответы
Ответ 1
Try:
public static float Sigmoid(double value) {
return 1.0f / (1.0f + (float) Math.Exp(-value));
}
EDIT: Я сделал быстрый тест. На моей машине приведенный выше код примерно на 43% быстрее, чем ваш метод, и этот математически эквивалентный код является самым младшим бит быстрее (на 46% быстрее оригинала):
public static float Sigmoid(double value) {
float k = Math.Exp(value);
return k / (1.0f + k);
}
РЕДАКТИРОВАТЬ 2: Я не уверен, сколько накладных функций С# есть, но если вы #include <math.h>
в своем исходном коде, вы должны использовать это, в котором используется float-exp функция. Это может быть немного быстрее.
public static float Sigmoid(double value) {
float k = expf((float) value);
return k / (1.0f + k);
}
Кроме того, если вы выполняете миллионы вызовов, это может быть проблемой. Попробуйте сделать встроенную функцию и посмотрите, не поможет ли она.
Ответ 2
Если это для функции активации, имеет ли это значение очень сильно, если вычисление e ^ x является полностью точным?
Например, если вы используете аппроксимацию (1 + x/256) ^ 256, на моем тестировании Pentium в Java (я предполагаю, что С# по существу компилируется с теми же инструкциями процессора), это примерно в 7-8 раз быстрее, чем e ^ x (Math.exp()) и с точностью до 2 десятичных знаков до примерно x +/- 1,5 и в правильном порядке величины в указанном диапазоне. (Очевидно, чтобы поднять до 256, вы фактически набираете число 8 раз - не используйте Math.Pow для этого!) В Java:
double eapprox = (1d + x / 256d);
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
Продолжайте удваивать или сокращать вдвое 256 (и добавлять/удалять умножение) в зависимости от того, насколько точна ваша аппроксимация. Даже при n = 4 он все же дает около 1,5 десятичных знаков точности для значений x, равных -0,5 и 0,5 (и кажется хорошим в 15 раз быстрее, чем Math.exp()).
P.S. Я забыл упомянуть - вы должны явно не действительно делить на 256: умножить на константу 1/256. Java JIT-компилятор делает эту оптимизацию автоматически (по крайней мере, Hotspot), и я предполагал, что С# тоже должен сделать.
Ответ 3
Посмотрите этот пост. он имеет аппроксимацию для e ^ x, написанную на Java, это должен быть код С# для него (untested):
public static double Exp(double val) {
long tmp = (long) (1512775 * val + 1072632447);
return BitConverter.Int64BitsToDouble(tmp << 32);
}
В моих тестах это больше, чем в 5 раз быстрее, чем Math.exp() (на Java). Аппроксимация основана на статье "" Быстрая, компактная аппроксимация экспоненциальной функции", которая была разработана точно для использования в нейронных сетях. Это в основном то же самое, что и таблица поиска 2048 записей и линейное приближение между элементами, но все это с помощью трюков с плавающей точкой IEEE.
EDIT: В соответствии с Special Sauce это на 3,25 раза быстрее, чем реализация CLR. Спасибо!
Ответ 4
- Помните, что любые изменения в этой функции активации выходят за рамки разного поведения. Это даже включает в себя переход на float (и, следовательно, снижение точности) или использование заменителей активации. Только экспериментирование с вашим примером использования покажет правильный путь.
- В дополнение к простой оптимизации кода я бы также рекомендовал рассмотреть распараллеливание вычислений (то есть: использовать несколько ядер вашего компьютера или даже машины в облаках Windows Azure) и улучшить обучающих алгоритмов.
UPDATE: Опубликовать таблицы поиска для функций активации ANN
UPDATE2: Я удалил точку на LUT, так как я смутил их с полным хешированием. Благодарим вас за Хенрик Густафссон за то, что вернули меня на трассу. Таким образом, память не является проблемой, хотя пространство поиска по-прежнему немного перепутано с локальными экстремумами.
Ответ 5
При 100 миллионах звонков, я бы начал задаваться вопросом, не наносит ли чрезмерные издержки профилировщика ваши результаты. Замените вычисление на no-op и убедитесь, что он все еще сообщает, что он потребляет 60% времени выполнения...
Или еще лучше создать некоторые тестовые данные и использовать таймер секундомера для профилирования миллионов звонков.
Ответ 6
Если вы можете взаимодействовать с С++, вы можете рассмотреть сохранение всех значений в массиве и их цикл через SSE следующим образом:
void sigmoid_sse(float *a_Values, float *a_Output, size_t a_Size){
__m128* l_Output = (__m128*)a_Output;
__m128* l_Start = (__m128*)a_Values;
__m128* l_End = (__m128*)(a_Values + a_Size);
const __m128 l_One = _mm_set_ps1(1.f);
const __m128 l_Half = _mm_set_ps1(1.f / 2.f);
const __m128 l_OneOver6 = _mm_set_ps1(1.f / 6.f);
const __m128 l_OneOver24 = _mm_set_ps1(1.f / 24.f);
const __m128 l_OneOver120 = _mm_set_ps1(1.f / 120.f);
const __m128 l_OneOver720 = _mm_set_ps1(1.f / 720.f);
const __m128 l_MinOne = _mm_set_ps1(-1.f);
for(__m128 *i = l_Start; i < l_End; i++){
// 1.0 / (1.0 + Math.Pow(Math.E, -value))
// 1.0 / (1.0 + Math.Exp(-value))
// value = *i so we need -value
__m128 value = _mm_mul_ps(l_MinOne, *i);
// exp expressed as inifite series 1 + x + (x ^ 2 / 2!) + (x ^ 3 / 3!) ...
__m128 x = value;
// result in l_Exp
__m128 l_Exp = l_One; // = 1
l_Exp = _mm_add_ps(l_Exp, x); // += x
x = _mm_mul_ps(x, x); // = x ^ 2
l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_Half, x)); // += (x ^ 2 * (1 / 2))
x = _mm_mul_ps(value, x); // = x ^ 3
l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver6, x)); // += (x ^ 3 * (1 / 6))
x = _mm_mul_ps(value, x); // = x ^ 4
l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver24, x)); // += (x ^ 4 * (1 / 24))
#ifdef MORE_ACCURATE
x = _mm_mul_ps(value, x); // = x ^ 5
l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver120, x)); // += (x ^ 5 * (1 / 120))
x = _mm_mul_ps(value, x); // = x ^ 6
l_Exp = _mm_add_ps(l_Exp, _mm_mul_ps(l_OneOver720, x)); // += (x ^ 6 * (1 / 720))
#endif
// we've calculated exp of -i
// now we only need to do the '1.0 / (1.0 + ...' part
*l_Output++ = _mm_rcp_ps(_mm_add_ps(l_One, l_Exp));
}
}
Однако помните, что массивы, которые вы будете использовать, должны быть выделены с помощью _aligned_malloc (some_size * sizeof (float), 16), потому что SSE требует, чтобы память была привязана к границе.
Используя SSE, я могу вычислить результат для всех 100 миллионов элементов за полсекунды. Однако выделение столько памяти за один раз обойдется вам почти в две трети гигабайта, поэтому я предлагаю обрабатывать больше, но меньше массивов одновременно. Возможно, вы даже захотите использовать двойной буферный подход с элементами 100K или более.
Кроме того, если количество элементов начинает значительно расти, вы можете захотеть обработать эти вещи на графическом процессоре (просто создайте 1D-текстуру float4 и запустите очень тривиальный шейдер фрагмента).
Ответ 7
FWIW, здесь мои тесты С# для ответов уже отправлены. (Empty - это функция, которая просто возвращает 0, чтобы измерить служебные данные функции)
Empty Function: 79ms 0
Original: 1576ms 0.7202294
Simplified: (soprano) 681ms 0.7202294
Approximate: (Neil) 441ms 0.7198783
Bit Manip: (martinus) 836ms 0.72318
Taylor: (Rex Logan) 261ms 0.7202305
Lookup: (Henrik) 182ms 0.7204863
public static object[] Time(Func<double, float> f) {
var testvalue = 0.9456;
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 1e7; i++)
f(testvalue);
return new object[] { sw.ElapsedMilliseconds, f(testvalue) };
}
public static void Main(string[] args) {
Console.WriteLine("Empty: {0,10}ms {1}", Time(Empty));
Console.WriteLine("Original: {0,10}ms {1}", Time(Original));
Console.WriteLine("Simplified: {0,10}ms {1}", Time(Simplified));
Console.WriteLine("Approximate: {0,10}ms {1}", Time(ExpApproximation));
Console.WriteLine("Bit Manip: {0,10}ms {1}", Time(BitBashing));
Console.WriteLine("Taylor: {0,10}ms {1}", Time(TaylorExpansion));
Console.WriteLine("Lookup: {0,10}ms {1}", Time(LUT));
}
Ответ 8
В верхней части головы в этом документе объясняется способ приближения экспоненты путем злоупотребления плавающей запятой (щелкните ссылку вверху) право для PDF), но я не знаю, будет ли это очень полезно для вас в .NET.
Кроме того, еще один момент: для быстрого обучения больших сетей логистический сигмоид, который вы используете, довольно ужасен. См. Раздел 4.4 Эффективный Backprop от LeCun и др. и используйте что-то ноль-центрированное (на самом деле, прочитайте всю эту статью, это очень полезно).
Ответ 9
Примечание: Это продолжение .
Изменить: Обновить, чтобы рассчитать то же самое, что this и this, немного вдохнув из this.
Теперь посмотри, что ты заставлял меня делать! Вы заставили меня установить Mono!
$ gmcs -optimize test.cs && mono test.exe
Max deviation is 0.001663983
10^7 iterations using Sigmoid1() took 1646.613 ms
10^7 iterations using Sigmoid2() took 237.352 ms
C вряд ли стоит усилий, мир движется вперед:)
Итак, чуть больше фактора 10 6 быстрее. Кто-то, у которого есть окно, позволяет исследовать использование и производительность памяти с использованием MS-материалов:)
Использование LUT для функций активации не столь необычно, особенно при использовании в оборудовании. Существует много хорошо проверенных вариантов концепции, если вы хотите включить эти типы таблиц. Однако, как уже отмечалось, сглаживание может оказаться проблемой, но есть и способы обойти это. Дальнейшее чтение:
Некоторые проблемы с этим:
- Ошибка возрастает, когда вы выходите за пределы таблицы (но сходится к 0 в крайних случаях); для x приблизительно + -7,0. Это обусловлено выбранным коэффициентом масштабирования. Большие значения SCALE дают более высокие ошибки в среднем диапазоне, но меньше по краям.
- Это, как правило, очень глупый тест, и я не знаю С#, это просто конверсия моего C-кода:)
- Ринат Абдуллин очень верен, что сглаживание и потеря точности могут вызвать проблемы, но поскольку я не видел переменные, я могу посоветовать вам попробовать это, На самом деле, я согласен со всем, что он говорит, за исключением проблем с поисковыми таблицами.
Извините кодирование вставки...
using System;
using System.Diagnostics;
class LUTTest {
private const float SCALE = 320.0f;
private const int RESOLUTION = 2047;
private const float MIN = -RESOLUTION / SCALE;
private const float MAX = RESOLUTION / SCALE;
private static readonly float[] lut = InitLUT();
private static float[] InitLUT() {
var lut = new float[RESOLUTION + 1];
for (int i = 0; i < RESOLUTION + 1; i++) {
lut[i] = (float)(1.0 / (1.0 + Math.Exp(-i / SCALE)));
}
return lut;
}
public static float Sigmoid1(double value) {
return (float) (1.0 / (1.0 + Math.Exp(-value)));
}
public static float Sigmoid2(float value) {
if (value <= MIN) return 0.0f;
if (value >= MAX) return 1.0f;
if (value >= 0) return lut[(int)(value * SCALE + 0.5f)];
return 1.0f - lut[(int)(-value * SCALE + 0.5f)];
}
public static float error(float v0, float v1) {
return Math.Abs(v1 - v0);
}
public static float TestError() {
float emax = 0.0f;
for (float x = -10.0f; x < 10.0f; x+= 0.00001f) {
float v0 = Sigmoid1(x);
float v1 = Sigmoid2(x);
float e = error(v0, v1);
if (e > emax) emax = e;
}
return emax;
}
public static double TestPerformancePlain() {
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 10; i++) {
for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
Sigmoid1(x);
}
}
sw.Stop();
return sw.Elapsed.TotalMilliseconds;
}
public static double TestPerformanceLUT() {
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 10; i++) {
for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
Sigmoid2(x);
}
}
sw.Stop();
return sw.Elapsed.TotalMilliseconds;
}
static void Main() {
Console.WriteLine("Max deviation is {0}", TestError());
Console.WriteLine("10^7 iterations using Sigmoid1() took {0} ms", TestPerformancePlain());
Console.WriteLine("10^7 iterations using Sigmoid2() took {0} ms", TestPerformanceLUT());
}
}
Ответ 10
F # имеет лучшую производительность, чем С# в математических алгоритмах .NET.. Поэтому перезапись нейронной сети в F # может улучшить общую производительность.
Если мы повторно реализуем фрагмент бенчмаркинга LUT (я использовал слегка измененную версию) в F #, то полученный код:
- выполняет тест sigmoid1 в 588.8ms вместо 3899,2ms
- выполняет тест sigmoid2 (LUT) в 156.6ms вместо 411.4 ms
Более подробную информацию можно найти в сообщении в блоге. Здесь фрагмент F # JIC:
#light
let Scale = 320.0f;
let Resolution = 2047;
let Min = -single(Resolution)/Scale;
let Max = single(Resolution)/Scale;
let range step a b =
let count = int((b-a)/step);
seq { for i in 0 .. count -> single(i)*step + a };
let lut = [|
for x in 0 .. Resolution ->
single(1.0/(1.0 + exp(-double(x)/double(Scale))))
|]
let sigmoid1 value = 1.0f/(1.0f + exp(-value));
let sigmoid2 v =
if (v <= Min) then 0.0f;
elif (v>= Max) then 1.0f;
else
let f = v * Scale;
if (v>0.0f) then lut.[int (f + 0.5f)]
else 1.0f - lut.[int(0.5f - f)];
let getError f =
let test = range 0.00001f -10.0f 10.0f;
let errors = seq {
for v in test ->
abs(sigmoid1(single(v)) - f(single(v)))
}
Seq.max errors;
open System.Diagnostics;
let test f =
let sw = Stopwatch.StartNew();
let mutable m = 0.0f;
let result =
for t in 1 .. 10 do
for x in 1 .. 1000000 do
m <- f(single(x)/100000.0f-5.0f);
sw.Elapsed.TotalMilliseconds;
printf "Max deviation is %f\n" (getError sigmoid2)
printf "10^7 iterations using sigmoid1: %f ms\n" (test sigmoid1)
printf "10^7 iterations using sigmoid2: %f ms\n" (test sigmoid2)
let c = System.Console.ReadKey(true);
И вывод (выпустить компиляцию против F # 1.9.6.2 CTP без отладчика):
Max deviation is 0.001664
10^7 iterations using sigmoid1: 588.843700 ms
10^7 iterations using sigmoid2: 156.626700 ms
UPDATE: обновленный бенчмаркинг для использования итераций 10 ^ 7, чтобы результаты были сопоставимы с C
UPDATE2: приведены результаты производительности C-реализации на той же машине, что и для сравнения:
Max deviation is 0.001664
10^7 iterations using sigmoid1: 628 ms
10^7 iterations using sigmoid2: 157 ms
Ответ 11
Первая мысль: как насчет некоторой статистики по переменной значений?
- Значения "значение" обычно малы -10 <= значение <= 10?
Если нет, вы, вероятно, можете получить повышение путем тестирования значений вне границ
if(value < -10) return 0;
if(value > 10) return 1;
- Часто повторяются ли значения?
Если это так, вы можете получить некоторую выгоду от Memoization (вероятно, нет, но не мешает проверить....)
if(sigmoidCache.containsKey(value)) return sigmoidCache.get(value);
Если ни одно из них не может быть применено, то, как предложили некоторые другие, возможно, вы можете уйти с понижением точности вашего сигмоида...
Ответ 12
У Сопрано была хорошая оптимизация вашего звонка:
public static float Sigmoid(double value)
{
float k = Math.Exp(value);
return k / (1.0f + k);
}
Если вы попытаетесь найти таблицу поиска и обнаружите, что она использует слишком много памяти, вы всегда можете посмотреть значение вашего параметра для каждого последующего вызова и использовать некоторую технику кэширования.
Например, попробуйте кэшировать последнее значение и результат. Если следующий вызов имеет то же значение, что и предыдущее, вам не нужно его вычислять, так как вы бы кэшировали последний результат. Если текущий вызов был таким же, как и предыдущий, даже 1 из 100 раз, вы могли бы сэкономить 1 миллион вычислений.
Или вы можете обнаружить, что в течение 10 последовательных вызовов параметр значения в среднем равен 2 раза, поэтому вы можете попробовать кэшировать последние 10 значений/ответов.
Ответ 13
Идея: возможно, вы можете создать (большую) таблицу поиска со значениями, предварительно рассчитанными?
Ответ 14
Это немного не по теме, но из-за любопытства я сделал ту же реализацию, что и в C, С# и F # в Java. Я просто оставлю это здесь, если кому-то интересно.
Результат:
$ javac LUTTest.java && java LUTTest
Max deviation is 0.001664
10^7 iterations using sigmoid1() took 1398 ms
10^7 iterations using sigmoid2() took 177 ms
Я полагаю, что улучшение по сравнению с С# в моем случае связано с тем, что Java лучше оптимизирован, чем Mono для OS X. На аналогичной MS.NET-реализации (по сравнению с Java 6, если кто-то хочет опубликовать сравнительные номера), я полагаю, что результаты будут отличаться.
код:
public class LUTTest {
private static final float SCALE = 320.0f;
private static final int RESOLUTION = 2047;
private static final float MIN = -RESOLUTION / SCALE;
private static final float MAX = RESOLUTION / SCALE;
private static final float[] lut = initLUT();
private static float[] initLUT() {
float[] lut = new float[RESOLUTION + 1];
for (int i = 0; i < RESOLUTION + 1; i++) {
lut[i] = (float)(1.0 / (1.0 + Math.exp(-i / SCALE)));
}
return lut;
}
public static float sigmoid1(double value) {
return (float) (1.0 / (1.0 + Math.exp(-value)));
}
public static float sigmoid2(float value) {
if (value <= MIN) return 0.0f;
if (value >= MAX) return 1.0f;
if (value >= 0) return lut[(int)(value * SCALE + 0.5f)];
return 1.0f - lut[(int)(-value * SCALE + 0.5f)];
}
public static float error(float v0, float v1) {
return Math.abs(v1 - v0);
}
public static float testError() {
float emax = 0.0f;
for (float x = -10.0f; x < 10.0f; x+= 0.00001f) {
float v0 = sigmoid1(x);
float v1 = sigmoid2(x);
float e = error(v0, v1);
if (e > emax) emax = e;
}
return emax;
}
public static long sigmoid1Perf() {
float y = 0.0f;
long t0 = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
y = sigmoid1(x);
}
}
long t1 = System.currentTimeMillis();
System.out.printf("",y);
return t1 - t0;
}
public static long sigmoid2Perf() {
float y = 0.0f;
long t0 = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
for (float x = -5.0f; x < 5.0f; x+= 0.00001f) {
y = sigmoid2(x);
}
}
long t1 = System.currentTimeMillis();
System.out.printf("",y);
return t1 - t0;
}
public static void main(String[] args) {
System.out.printf("Max deviation is %f\n", testError());
System.out.printf("10^7 iterations using sigmoid1() took %d ms\n", sigmoid1Perf());
System.out.printf("10^7 iterations using sigmoid2() took %d ms\n", sigmoid2Perf());
}
}
Ответ 15
Я понимаю, что прошло уже год с момента появления этого вопроса, но я столкнулся с ним из-за обсуждения производительности F # и C относительно С#. Я играл с некоторыми образцами от других респондентов и обнаружил, что делегаты, как представляется, выполняют быстрее обычного вызова метода, но нет никакого очевидного преимущества для F # над С#/a > .
- C: 166ms
- С# (делегат): 275 мс
- С# (метод): 431ms
- С# (метод, счетчик поплавка): 2,656мс
- F #: 404ms
С# с поплавковым счетчиком был прямым портом кода C. Гораздо быстрее использовать int в цикле for.
Ответ 16
(Обновлено с измерением производительности) (Обновлено снова с реальными результатами:)
Я думаю, что решение таблицы поиска позволит вам очень далеко, когда дело доходит до производительности, при незначительной памяти и высокой стоимости.
Следующий фрагмент представляет собой пример реализации в C (я не говорю С# достаточно свободно, чтобы сушить его). Он работает и работает достаточно хорошо, но я уверен, что там ошибка:)
#include <math.h>
#include <stdio.h>
#include <time.h>
#define SCALE 320.0f
#define RESOLUTION 2047
#define MIN -RESOLUTION / SCALE
#define MAX RESOLUTION / SCALE
static float sigmoid_lut[RESOLUTION + 1];
void init_sigmoid_lut(void) {
int i;
for (i = 0; i < RESOLUTION + 1; i++) {
sigmoid_lut[i] = (1.0 / (1.0 + exp(-i / SCALE)));
}
}
static float sigmoid1(const float value) {
return (1.0f / (1.0f + expf(-value)));
}
static float sigmoid2(const float value) {
if (value <= MIN) return 0.0f;
if (value >= MAX) return 1.0f;
if (value >= 0) return sigmoid_lut[(int)(value * SCALE + 0.5f)];
return 1.0f-sigmoid_lut[(int)(-value * SCALE + 0.5f)];
}
float test_error() {
float x;
float emax = 0.0;
for (x = -10.0f; x < 10.0f; x+=0.00001f) {
float v0 = sigmoid1(x);
float v1 = sigmoid2(x);
float error = fabsf(v1 - v0);
if (error > emax) { emax = error; }
}
return emax;
}
int sigmoid1_perf() {
clock_t t0, t1;
int i;
float x, y = 0.0f;
t0 = clock();
for (i = 0; i < 10; i++) {
for (x = -5.0f; x <= 5.0f; x+=0.00001f) {
y = sigmoid1(x);
}
}
t1 = clock();
printf("", y); /* To avoid sigmoidX() calls being optimized away */
return (t1 - t0) / (CLOCKS_PER_SEC / 1000);
}
int sigmoid2_perf() {
clock_t t0, t1;
int i;
float x, y = 0.0f;
t0 = clock();
for (i = 0; i < 10; i++) {
for (x = -5.0f; x <= 5.0f; x+=0.00001f) {
y = sigmoid2(x);
}
}
t1 = clock();
printf("", y); /* To avoid sigmoidX() calls being optimized away */
return (t1 - t0) / (CLOCKS_PER_SEC / 1000);
}
int main(void) {
init_sigmoid_lut();
printf("Max deviation is %0.6f\n", test_error());
printf("10^7 iterations using sigmoid1: %d ms\n", sigmoid1_perf());
printf("10^7 iterations using sigmoid2: %d ms\n", sigmoid2_perf());
return 0;
}
Предыдущие результаты были связаны с тем, что оптимизатор выполнил свою работу и оптимизировал вычисления. Фактически выполнение кода дает немного разные и гораздо более интересные результаты (на моем пути медленный MB Air):
$ gcc -O2 test.c -o test && ./test
Max deviation is 0.001664
10^7 iterations using sigmoid1: 571 ms
10^7 iterations using sigmoid2: 113 ms
![profile]()
TODO:
Есть вещи для улучшения и способы устранения недостатков; как это сделать, остается как упражнение для читателя:)
- Настройте диапазон функции, чтобы избежать перехода, когда таблица начинается и заканчивается.
- Добавьте небольшую функцию шума, чтобы скрыть артефакты сглаживания.
- Как сказал Рекс, интерполяция может стать для вас еще более точной, хотя и относительно дешевой по производительности.
Ответ 17
Вы также можете рассмотреть возможность экспериментов с альтернативными функциями активации, которые дешевле оценить. Например:
f(x) = (3x - x**3)/2
(который может быть учтен как
f(x) = x*(3 - x*x)/2
за одно меньшее умножение). Эта функция имеет нечетную симметрию, а ее производная тривиальна. Использование его для нейронной сети требует нормализации суммы входов путем деления на общее количество входов (ограничение домена на [-1.1], которое также находится в диапазоне).
Ответ 18
Мягкая вариация на тему сопрано:
public static float Sigmoid(double value) {
float v = value;
float k = Math.Exp(v);
return k / (1.0f + k);
}
Поскольку вы только после получения одного результата точности, почему функция Math.Exp вычисляет двойной? Любой калькулятор экспоненты, который использует итеративное суммирование (см. расширение e x), займет больше времени для большей точности, каждый раз. И удвоение вдвое больше работы сингла! Таким образом, вы сначала конвертируете в один, , затем выполняете свою экспоненту.
Но функция expf должна быть быстрее. Я не вижу необходимости в том, чтобы использовать soprano (float) в передаче expf, хотя, если С# не выполняет неявное преобразование с плавающей точкой.
В противном случае просто используйте язык real, например FORTRAN...
Ответ 19
Здесь есть много хороших ответов. Я бы предложил запустить его через эту технику, чтобы убедиться, что
- Вы не называете это больше, чем вам нужно.
(Иногда функции вызываются более чем необходимо, просто потому, что их так легко вызвать.)
- Вы не вызываете его повторно с теми же аргументами
(где вы можете использовать memoization)
Кстати, у вас есть функция обратного логита,
или обратной функции log-odds-ratio log(f/(1-f))
.
Ответ 20
Есть намного более быстрые функции, которые делают очень похожие вещи:
x / (1 + abs(x))
- быстрая замена TAHN
И аналогично:
x / (2 + 2 * abs(x)) + 0.5
- быстрая замена SIGMOID
Сравнить графики с фактическим сигмоидом
Ответ 21
Выполняя поиск Google, я нашел альтернативную реализацию функции Sigmoid.
public double Sigmoid(double x)
{
return 2 / (1 + Math.Exp(-2 * x)) - 1;
}
Правильно ли это для ваших нужд? Это быстрее?
http://dynamicnotions.blogspot.com/2008/09/sigmoid-function-in-c.html
Ответ 22
1) Вы называете это только одним местом? Если это так, вы можете получить небольшую производительность, переместив код из этой функции и просто поместив ее вправо, где вы обычно вызывали функцию Sigmoid. Мне не нравится эта идея с точки зрения удобочитаемости кода и организации, но когда вам нужно получить каждый последний прирост производительности, это может помочь, потому что, по моему мнению, вызовы функций требуют ввода/выгрузки регистров в стеке, чего можно избежать, если код был встроен.
2) Я не знаю, может ли это помочь, но попробуйте сделать параметр функции параметром ref. Смотрите, если это быстрее. Я бы предложил сделать его const (что было бы оптимизацией, если бы это было в С++), но С# не поддерживает константные параметры.
Ответ 23
Если вам требуется гигантское ускорение скорости, вы, вероятно, можете рассмотреть возможность параллелизации функции с помощью силы (ge). IOW, используйте DirectX для управления графической картой, делая это за вас. Я понятия не имею, как это сделать, но я видел, как люди используют графические карты для всех видов вычислений.
Ответ 24
Я видел, что многие люди здесь пытаются использовать аппроксимацию, чтобы сделать Сигмоид быстрее. Однако важно знать, что сигмоид также может быть выражен с помощью tanh, а не только exp.
Вычисление Sigmoid таким образом примерно в 5 раз быстрее, чем с экспоненциальным, и с помощью этого метода вы не приближаетесь ни к чему, поэтому исходное поведение Sigmoid сохраняется как есть.
public static double Sigmoid(double value)
{
return 0.5d + 0.5d * Math.Tanh(value/2);
}
Конечно, parellization станет следующим шагом на пути к улучшению производительности, но по сравнению с исходным вычислением использование Math.Tanh происходит быстрее, чем Math.Exp.