una - Optimización matemática en C#
suma resta multiplicacion y division en visual basic (24)
- Recuerde que cualquier cambio en esta función de activación tiene un costo de comportamiento diferente . Esto incluso incluye cambiar a flotación (y por lo tanto reducir la precisión) o usar sustitutos de activación. Solo experimentar con su caso de uso mostrará el camino correcto.
- Además de las simples optimizaciones de código, también recomendaría considerar la paralelización de los cálculos (es decir, aprovechar los múltiples núcleos de su máquina o incluso las máquinas en las nubes de Windows Azure) y mejorar los algoritmos de entrenamiento.
ACTUALIZACIÓN: Publicar en tablas de búsqueda para funciones de activación ANN
ACTUALIZACIÓN2: Eliminé el punto en LUT ya que confundí estos con el hash completo. Gracias a Henrik Gustafsson por ponerme de nuevo en la pista. Entonces, la memoria no es un problema, aunque el espacio de búsqueda todavía se descompone con los extremos locales un poco.
He estado perfilando una aplicación todo el día y, habiendo optimizado un par de bits de código, me quedé con esto en mi lista de tareas pendientes. Es la función de activación de una red neuronal, que se llama más de 100 millones de veces. Según dotTrace, representa aproximadamente el 60% del tiempo total de la función.
¿Cómo optimizarías esto?
public static float Sigmoid(double value) {
return (float) (1.0 / (1.0 + Math.Pow(Math.E, -value)));
}
(Actualizado con medidas de rendimiento) (Actualizado de nuevo con resultados reales :)
Creo que una solución de tabla de búsqueda lo llevaría muy lejos cuando se trata de rendimiento, con una memoria despreciable y un costo de precisión.
El siguiente fragmento es una implementación de ejemplo en C (no hablo c # con la suficiente fluidez como para codificarlo en seco). Funciona y funciona bastante bien, pero estoy seguro de que hay un error :)
#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;
}
Previous results were due to the optimizer doing its job and optimized away the calculations. Making it actually execute the code yields slightly different and much more interesting results (on my way slow 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
TODO:
There are things to improve and ways to remove weaknesses; how to do is is left as an exercise to the reader :)
- Tune the range of the function to avoid the jump where the table starts and ends.
- Add a slight noise function to hide the aliasing artifacts.
- As Rex said, interpolation could get you quite a bit further precision-wise while being rather cheap performance-wise.
Aquí hay muchas buenas respuestas. Sugeriría ejecutarlo a través de esta técnica , solo para estar seguro
- No lo está llamando más veces de las necesarias.
(A veces las funciones se llaman más de lo necesario, simplemente porque son tan fáciles de llamar). - No lo llamas repetidamente con los mismos argumentos
(donde podría usar la memorización)
Por cierto, la función que tiene es la función de logit inversa,
o el inverso del log de la función log-odds-ratio log(f/(1-f))
.
Con 100 millones de llamadas, comenzaría a preguntarme si la sobrecarga de Profiler no está sesgando los resultados. Reemplace el cálculo con no-operativa y vea si todavía se informa que consume el 60% del tiempo de ejecución ...
O mejor aún, cree algunos datos de prueba y use un cronómetro para programar un millón de llamadas.
Eche un vistazo a esta publicación . tiene una aproximación para e ^ x escrita en Java, este debería ser el código de C # para ella (no probado):
public static double Exp(double val) {
long tmp = (long) (1512775 * val + 1072632447);
return BitConverter.Int64BitsToDouble(tmp << 32);
}
En mis puntos de referencia, esto es más de 5 veces más rápido que Math.exp () (en Java). La aproximación se basa en el documento " Aproximación rápida y compacta de la función exponencial " que se desarrolló exactamente para ser utilizado en redes neuronales. Es básicamente lo mismo que una tabla de búsqueda de 2048 entradas y una aproximación lineal entre las entradas, pero todo esto con trucos de punto flotante IEEE.
EDITAR: según Special Sauce esto es ~ 3.25x más rápido que la implementación de CLR. ¡Gracias!
En la parte superior de mi cabeza, este documento explica una forma de aproximar la exponencial al abusar del punto flotante (haga clic en el enlace de la esquina superior derecha para ver el PDF) pero no sé si será de mucha utilidad para usted. RED.
Además, otro punto: con el propósito de entrenar grandes redes rápidamente, el sigmoide logístico que está utilizando es bastante terrible. Consulte la sección 4.4 de Efficient Backprop por LeCun et al y use algo centrado en cero (en realidad, lea todo el documento, es inmensamente útil).
Esto está un poco fuera de tema, pero solo por curiosidad, hice la misma implementación que la de C , C # y this en Java. Dejaré esto aquí en caso de que alguien más tenga curiosidad.
Resultado:
$ 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
Supongo que la mejora sobre C # en mi caso se debe a que Java está mejor optimizado que Mono para OS X. En una implementación de MS .NET similar (frente a Java 6 si alguien quiere publicar números comparativos), supongo que los resultados serían diferentes .
Código:
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, aquí están mis puntos de referencia de C # para las respuestas ya publicadas. (Empty es una función que simplemente devuelve 0, para medir la sobrecarga de llamada a la función)
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));
}
Idea: ¿Tal vez pueda hacer una tabla de búsqueda (grande) con los valores precalculados?
Me doy cuenta de que ha pasado un año desde que surgió esta pregunta, pero la encontré debido a la discusión sobre el rendimiento de F # y C en relación con C #. Jugué con algunas de las muestras de otros respondedores y descubrí que los delegados parecen ejecutar más rápido que una invocación de método normal, pero thoughtfulcode.wordpress.com/2010/12/30/… .
- C: 166 ms
- C # (delegado): 275 ms
- C # (método): 431ms
- C # (método, contador de flotación): 2,656ms
- F #: 404ms
El C # con un contador de flotación era un puerto directo del código C. Es mucho más rápido usar un int en el ciclo for.
Primero pensé: ¿Qué tal algunas estadísticas sobre la variable de valores?
- ¿Son los valores de "valor" típicamente pequeños -10 <= valor <= 10?
Si no, probablemente puedas obtener un impulso probando valores fuera de límites
if(value < -10) return 0;
if(value > 10) return 1;
- ¿Los valores se repiten a menudo?
Si es así, probablemente puedas obtener algún beneficio de Memoization (probablemente no, pero no está de más comprobar ...)
if(sigmoidCache.containsKey(value)) return sigmoidCache.get(value);
Si ninguno de estos se puede aplicar, entonces, como algunos otros han sugerido, tal vez pueda salirse con la suya reduciendo la precisión de su sigmoide ...
Si puede interoperar con C ++, podría considerar almacenar todos los valores en una matriz y recorrerlos usando SSE de la siguiente manera:
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));
}
}
Sin embargo, recuerde que las matrices que utilizará se deben asignar usando _aligned_malloc (some_size * sizeof (float), 16) porque SSE requiere memoria alineada con un límite.
Usando SSE, puedo calcular el resultado para todos los 100 millones de elementos en alrededor de medio segundo. Sin embargo, asignar esa cantidad de memoria a la vez le costará casi dos tercios de un gigabyte, por lo que le sugiero que procese más, pero arreglos más pequeños a la vez. Incluso podría considerar usar un enfoque de doble buffer con 100K elementos o más.
Además, si la cantidad de elementos comienza a crecer considerablemente, es posible que desee elegir procesar estos elementos en la GPU (solo cree una textura 1D float4 y ejecute un sombreador de fragmentos muy trivial).
Si se trata de una función de activación, ¿importa mucho si el cálculo de e ^ x es completamente preciso?
Por ejemplo, si usa la aproximación (1 + x / 256) ^ 256, en mi prueba Pentium en Java (supongo que C # se compila esencialmente con las mismas instrucciones del procesador), esto es aproximadamente 7-8 veces más rápido que e ^ x (Math.exp ()), y tiene una precisión de 2 lugares decimales hasta aproximadamente x de +/- 1.5, y dentro del orden correcto de magnitud en todo el rango que indicó. (Obviamente, para elevar al 256, en realidad cuadras el número 8 veces - ¡no uses Math.Pow para esto!) En Java:
double eapprox = (1d + x / 256d);
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
eapprox *= eapprox;
Continúa doblando o reduciendo a la mitad 256 (y agregando / eliminando una multiplicación) dependiendo de qué tan precisa quieras que sea la aproximación. Incluso con n = 4, todavía da aproximadamente 1.5 decimales de precisión para valores de x entre -0.5 y 0.5 (y aparece un buen 15 veces más rápido que Math.exp ()).
PD. Olvidé mencionar que, obviamente, no debes dividir por 256: multiplica por una constante de 1/256. El compilador JIT de Java hace esta optimización automáticamente (al menos, Hotspot lo hace), y asumí que C # debe hacerlo también.
Soprano tuvo algunas buenas optimizaciones a su llamada:
public static float Sigmoid(double value)
{
float k = Math.Exp(value);
return k / (1.0f + k);
}
Si prueba con una tabla de búsqueda y encuentra que usa demasiada memoria, siempre podría mirar el valor de su parámetro para cada llamada sucesiva y emplear alguna técnica de almacenamiento en caché.
Por ejemplo, intente almacenar en caché el último valor y resultado. Si la próxima llamada tiene el mismo valor que la anterior, no es necesario que la calcule como lo haría en la memoria caché del último resultado. Si la llamada actual fue la misma que la llamada anterior, incluso 1 de cada 100 veces, podría ahorrarse 1 millón de cálculos.
O bien, puede encontrar que dentro de las 10 llamadas sucesivas, el parámetro de valor es, en promedio, el mismo 2 veces, por lo que podría intentar almacenar en caché los últimos 10 valores / respuestas.
También podría considerar experimentar con funciones de activación alternativas que son más baratas de evaluar. Por ejemplo:
f(x) = (3x - x**3)/2
(que podría tenerse en cuenta como
f(x) = x*(3 - x*x)/2
por una multiplicación menos). Esta función tiene una simetría impar, y su derivada es trivial. Usarlo para una red neuronal requiere normalizar la suma de las entradas dividiendo por el número total de entradas (limitando el dominio a [-1..1], que también es el rango).
Tratar:
public static float Sigmoid(double value) {
return 1.0f / (1.0f + (float) Math.Exp(-value));
}
EDITAR: Hice un punto de referencia rápido. En mi máquina, el código anterior es aproximadamente un 43% más rápido que su método, y este código matemáticamente equivalente es el más rápido más rápido (46% más rápido que el original):
public static float Sigmoid(double value) {
float k = Math.Exp(value);
return k / (1.0f + k);
}
EDIT 2: No estoy seguro de cuántas funciones generales de C # tienen, pero si #include <math.h>
en tu código fuente, deberías poder usar esto, que usa una función float-exp. Puede ser un poco más rápido.
public static float Sigmoid(double value) {
float k = expf((float) value);
return k / (1.0f + k);
}
Además, si está haciendo millones de llamadas, la sobrecarga de llamadas a funciones podría ser un problema. Intente hacer una función en línea y vea si eso es de alguna ayuda.
Una leve variación en el tema de Soprano:
public static float Sigmoid(double value) {
float v = value;
float k = Math.Exp(v);
return k / (1.0f + k);
}
Ya que solo busca un único resultado de precisión, ¿por qué hacer que la función Math.Exp calcule un doble? Cualquier calculadora de exponente que use una suma iterativa (vea la expansión de la e x ) tomará más tiempo para obtener más precisión, todas y cada una de las veces. ¡Y el doble es el doble del trabajo de soltero! De esta forma, primero se convierte en soltero y luego se hace exponencial.
Pero la función expf debería ser aún más rápida. Sin embargo, no veo la necesidad de lanzar soprano (flotante) al pasar a expf, a menos que C # no haga una conversión flotante doble implícita.
De lo contrario, solo usa un lenguaje real , como FORTRAN ...
1) Do you call this from only one place? If so, you may gain a small amount of performance by moving the code out of that function and just putting it right where you would normally have called the Sigmoid function. I don''t like this idea in terms of code readability and organization but when you need to get every last performance gain, this might help because I think function calls require a push/pop of registers on the stack, which could be avoided if your code was all inline.
2) I have no idea if this might help but try making your function parameter a ref parameter. See if it''s faster. I would have suggested making it const (which would have been an optimization if this were in c++) but c# doesn''t support const parameters.
F # tiene un mejor rendimiento que C # en los algoritmos matemáticos .NET. Por lo tanto, la reescritura de la red neuronal en F # podría mejorar el rendimiento general.
Si volvemos a implementar el fragmento de referencia de LUT (he estado usando una versión ligeramente modificada) en F #, entonces el código resultante:
- ejecuta sigmoid1 punto de referencia en 588.8ms en lugar de 3899,2ms
- ejecuta sigmoid2 (LUT) benchmark en 156.6ms en lugar de 411.4 ms
Se pueden encontrar más detalles en la publicación del blog . Aquí está el fragmento de 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);
Y el resultado (compilación de versión contra F # 1.9.6.2 CTP sin depurador):
Max deviation is 0.001664
10^7 iterations using sigmoid1: 588.843700 ms
10^7 iterations using sigmoid2: 156.626700 ms
ACTUALIZACIÓN: benchmarking actualizado para usar 10 ^ 7 iteraciones para hacer que los resultados sean comparables con C
ACTUALIZACIÓN2: aquí están los resultados de rendimiento de la implementación C de la misma máquina para comparar:
Max deviation is 0.001664
10^7 iterations using sigmoid1: 628 ms
10^7 iterations using sigmoid2: 157 ms
Nota: esta es una continuación de esta publicación.
Editar: actualiza para calcular lo mismo que esto y this , inspirándose en esto .
¡Ahora mira lo que me hiciste hacer! ¡Me hiciste instalar 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 ya no vale la pena el esfuerzo, el mundo avanza :)
Entonces, un poco más de 10 6 más rápido. Alguien con un cuadro de Windows puede investigar el uso de la memoria y el rendimiento usando MS-stuff :)
El uso de LUT para las funciones de activación no es tan poco común, especialmente cuando se implementa en hardware. Hay muchas variantes bien probadas del concepto por ahí si está dispuesto a incluir ese tipo de tablas. Sin embargo, como ya se ha señalado, el aliasing podría ser un problema, pero también hay formas de evitarlo. Algunas lecturas adicionales:
- NEURObjects por Giorgio Valentini (también hay un artículo sobre esto)
- Redes neuronales con funciones de activación LUT digitales
- Impulso de la extracción de funciones de red neuronal mediante funciones de activación de precisión reducida
- Un nuevo algoritmo de aprendizaje para redes neuronales con pesos enteros y funciones cuantificadas de activación no lineal
- Los efectos de la cuantificación en las redes neuronales de función de alto orden
Algunos problemas con esto:
- El error aumenta cuando alcanzas fuera de la tabla (pero converge a 0 en los extremos); para x aproximadamente + -7.0. Esto se debe al factor de escala elegido. Los valores más grandes de SCALE dan errores más altos en el rango medio, pero más pequeños en los bordes.
- Esta es generalmente una prueba muy estúpida, y no sé C #, es solo una conversión simple de mi código C :)
- Rinat Abdullin está en lo cierto al decir que el aliasing y la pérdida de precisión pueden causar problemas, pero como no he visto las variables para eso, solo puedo aconsejarle que pruebe esto. De hecho, estoy de acuerdo con todo lo que dice, excepto por el tema de las tablas de búsqueda.
Perdonen la codificación de copiar y pegar ...
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());
}
}
Doing a Google search, I found an alternative implementation of the Sigmoid function.
public double Sigmoid(double x)
{
return 2 / (1 + Math.Exp(-2 * x)) - 1;
}
Is that correct for your needs? Is it faster?
http://dynamicnotions.blogspot.com/2008/09/sigmoid-function-in-c.html
If you need a giant speed boost, you could probably look into parallelizing the function using the (ge)force. IOW, use DirectX to control the graphics card into doing it for you. I have no idea how to do this, but I''ve seen people use graphics cards for all kinds of calculations.
There are a much faster functions that do very similar things:
x / (1 + abs(x))
– fast replacement for TAHN
And similarly:
x / (2 + 2 * abs(x)) + 0.5
- fast replacement for SIGMOID
He visto que mucha gente por aquí está tratando de usar la aproximación para hacer que Sigmoid sea más rápido. Sin embargo, es importante saber que Sigmoid también se puede expresar usando tanh, no solo exp. Calcular Sigmoid de esta manera es alrededor de 5 veces más rápido que con exponencial, y al usar este método no se aproxima a nada, por lo que el comportamiento original de Sigmoid se mantiene tal como está.
public static double Sigmoid(double value)
{
return 0.5d + 0.5d * Math.Tanh(value/2);
}
Por supuesto, la paginación sería el siguiente paso para la mejora del rendimiento, pero en lo que se refiere al cálculo sin procesar, usar Math.Tanh es más rápido que Math.Exp.