Comparación de rendimiento: sigmoid manual vs breeze.numerics.sigmoid en Scala

Al implementar la función sigmoide en Scala, existen varias formas de calcularla. Este artículo compara el rendimiento de tres enfoques: la fórmula directa, el uso de la biblioteca breeze.numerics y una función definida por el usuario, tanto sobre valores escalares como sobre vectores.

Se preparó una secuencia de 20000 números aleatorios de tipo Double (generada con scala.util.Random.nextDouble) y se midió el tiempo de ejecución con System.nanoTime. A continuación se muestran los resultados típicos obtenidos.

1. Fórmula directa (inline)

val datos = (0 until 20000).map(_ => scala.util.Random.nextDouble)

val t0 = System.nanoTime
val resultadoDirecto = datos.map(x => 1.0 / (1.0 + Math.exp(-x)))
val t1 = System.nanoTime
println(s"Tiempo directo: ${t1 - t0} ns")  // Aprox. 5 980 031 ns

2. Usando breeze.numerics.sigmoid

val t2 = System.nanoTime
val resultadoBreeze = datos.map(x => breeze.numerics.sigmoid(x))
val t3 = System.nanoTime
println(s"Tiempo breeze: ${t3 - t2} ns")  // Aprox. 7 606 556 ns

3. Función definida por el usuario

def sig(x: Double): Double = 1.0 / (1.0 + Math.exp(-x))

val t4 = System.nanoTime
val resultadoFunc = datos.map(sig)
val t5 = System.nanoTime
println(s"Tiempo función definida: ${t5 - t4} ns")  // Aprox. 9 380 656 ns

Se observa que la fórmula directa es la más rápida, seguida de breeze.numerics.sigmoid y luego de la función definida. La diferencia se debe a la sobrecarga de la llamada a función y a la lógica interna de la biblioteca.

Operaciones sobre vectores con Breeze

Al convertir los datos en un DenseVector y aplicar la función sigmoide directamente sobre el vector, el rendimiento de Breeeze mejora notablemente:

val vectorDatos = new breeze.linalg.DenseVector(datos.toArray)

val t6 = System.nanoTime
breeze.numerics.sigmoid(vectorDatos)
val t7 = System.nanoTime
println(s"Tiempo Breeze sobre vector: ${t7 - t6} ns")  // Aprox. 3 372 600 ns

Incluso considerando el tiempo de creación del vector, el resultado sigue siendo competitivo (~4 293 909 ns). Además, al escalar el número de elementos (20, 200, 2000, 20000), Breeze mantiene una escalabilidad casi lineal, superando ampliamente a la versión map sobre colecciones.

Conclusiones

  • Para operaciones sobre valores escalares o pequeñas colecciones, es preferible escribir la fórmula directamente para evitar la sobrecarga de llamadas a funciones o bibliotecas.
  • Cuando se trabaja con vectores o matrices grandes, utilizar las funciones vectorizadas de Breeze ofrece una aceleración significativa gracias a su implementación optimizada (posiblemente usando BLAS/LAPACK).
  • La elección depende del contexto: si la legibilidad y la mantenibilidad son críticas, la función definida puede ser aceptable; pero para rendimiento en cómputo numérico masivo, se recomienda adoptar Breeze con estructuras de datos adecuadas.

Etiquetas: scala breeze Optimización de Rendimiento sigmoid cálculo numérico

Publicado el 6-18 06:20