Números Pseudoaleatorios · Tema 42

Acumulación de errores numéricos

Medio ulp no es nada una vez; sumado un millón de veces y en mal orden, se come tu simulación.

01 · Punto de partida

Medio ulp × un millón

En los Temas 39–41 viste el error de una operación: como mucho ½ ulp si redondeás (Tema 40), hasta 1 unidad sesgada si truncás (Tema 41). El problema es que una simulación no hace una operación: avanza un reloj con t += dt millones de veces, acumula energía, promedia réplicas, suma histogramas. Cada paso agrega su redondeo al pozo común.

Y el pozo tiene formas de crecer: si los redondeos tiran siempre del mismo lado (truncamiento, pasos todos positivos), el error crece lineal con N; si tiran al azar, crece como √N; y si sumás chicos sobre un gigante, el error es todo: los chicos desaparecen por absorción (Tema 39).

  • ¿Cuánto error junto al sumar N veces el mismo paso?
  • ¿Por qué 1e16 + 1 + 1 + … queda clavado en 1e16?
  • ¿Por qué sumar chicos-primero cambia el resultado?
  • ¿Cómo sumo largo sin pagar el sesgo: bloques, Kahan, fsum?

02 · Definición

Tres velocidades del desastre

◈

Lineal (sesgo)

Pasos todos del mismo signo o truncados: cada redondeo suma. Error ≈ N × ½ ulp. Es el reloj t += 0,1.

⇄

Raíz (azar)

Redondeos que a veces suman y a veces restan: se compensan a medias. Error ≈ √N × ½ ulp. Mejor, pero igual crece.

▣

Absorción (todo)

Chico frente a gigante: grande + chico == grande. Error = 100 % del aporte chico, desde el primer paso.

Cuánto se pierde según cómo sumes.
Suma de N términosError típicoEjemplo
N pasos +0,1 ingenuosLineal, deriva visibleReloj que se atrasa/adelanta
N valores mixtos ±√N, paseo aleatorioPromedio con signos mezclados
1e16 + N unosTotal: N perdidosContador gigante + eventos chicos

03 · Anatomía

Por qué el orden cambia la suma

La suma matemática es asociativa; la suma en float no: (a+b)+c ≠ a+(b+c) en general. Cada paréntesis redondea, y redondear temprano sobre un gigante mata a los chicos antes de que se junten entre ellos.

Grande-primero (pierde)
((G + 1) + 1) + …: cada +1 muere solo contra G por absorción. Al final queda G, aunque N valga miles.
Chicos-primero (salva)
G + (1 + 1 + …): los chicos se juntan entre ellos hasta ser grandes frente a G y recién ahí se suman. Se pierde solo el redondeo final.
Suma compensada (Kahan / fsum)
Guarda aparte “lo que se perdió” en cada paso y lo reinyecta en el siguiente: error casi independiente de N. math.fsum es la versión exacta-y-lenta; Kahan, la rápida.

Bien acumulado

Chicos primero o Kahan

Ordenar ascendente, sumar por bloques o usar fsum/Kahan: el error queda en 1–2 ulps aunque N sea enorme.

Mal acumulado

Naive sobre el gigante

total = 1e16; for v in chicos: total += v: cada aporte muere en ½ ulp contra el gigante. Más iteraciones, cero progreso.

04 · Ejemplos

Tres acumulaciones reales

Pasos chicos→Orden de suma→Total honesto o perdido
  1. 1
    El reloj que se corre.

    t = 0; for _ in range(10_000): t += 0,1 termina corrido ~1e−12: cada += 0,1 redondea y el sesgo camina. Con enteros (t = i*dt) no deriva.

  2. 2
    El contador clavado.

    1e16 + 1 + 1 + … (1000 unos) queda en 1e16: el paso local es 2 y cada uno muere solo. Sumando los unos primero da 10000000000001000.

  3. 3
    El promedio mentiroso.

    Promediar 1 M de energías con un outlier gigante ingenuo arrastra el error de la suma al cociente. Con fsum o promedio incremental (Welford, Tema 43) el error no escala con N.

05 · Demostración en Python

Perder mil unos en una línea

El experimento mínimo: un gigante y mil unos. La suma ingenua los pierde todos; fsum y Kahan los recuperan.

Python en tu navegador. Subí la lista a 5000 unos y mirá cómo la ingenua sigue clavada mientras las honestas avanzan.

import math

vals = [1e16] + [1.0] * 1000
print("naive:", sum(vals))
print("fsum: ", math.fsum(vals))
print("al revés fsum:", math.fsum(reversed(vals)))

Naive ≈ 1e16 clavado; fsum ≈ 10000000000001000: los mil unos reaparecen.

Kahan en 6 líneas

def kahan(vals):
    total = comp = 0.0
    for v in vals:
        y = v - comp
        t = total + y
        comp = (t - total) - y
        total = t
    return total


print(kahan([1e16] + [1.0] * 1000))

06 · Exploración

Laboratorio: el gigante que se come los unos

Gigante fijo G = 1e16 más N unos. La curva naranja suma ingenuo de izquierda a derecha (((G+1)+1)…) y queda clavada; la verde junta los unos primero (G + N) y crece. Movelo y contá cuántos unos se pierden.

EXPERIMENTO 42

G + N unos, dos órdenes

perdidos = N − (naive − G)

Los resultados numéricos aparecen debajo.
Naive—
Chicos-primero—
Unos perdidos—
% perdido—

Con N = 1000 la ingenua pierde los 1000 unos.

El eje x es cuántos unos ya se sumaron; el eje y es cuánto creció el total sobre G. La ingenua queda en 0; la honesta sube en diagonal.

Preguntas para explorar

  1. Con N = 1000: ¿cuánto vale la ingenua menos G? ¿Y la chicos-primero menos G?
  2. Subí a N = 5000: ¿la ingenua recupera algo? ¿Por qué el paso local (ulp = 2) lo impide?
  3. Si G fuera tu energía acumulada y los unos tus eventos, ¿qué orden elegirías para el total?
Ver respuestas sugeridas
  1. Ingenua: 0 (los 1000 mueren uno por uno por absorción); chicos-primero: 1000 (los unos se juntan hasta 1000 y recién ahí enfrentan a G).
  2. No: cada +1 aislado es menor que G·ε/2 = 1,0… redondeado al par de paso 2. Solo un bloque ≥ 2 sobrevive solo; el resto necesita juntarse primero.
  3. Chicos-primero, por bloques o Kahan: acumular parciales chicos y sumarlos al gigante al final. Es el patrón de fsum y del Tema 43.

07 · Comprensión

Confusiones frecuentes

«Sumar es asociativo, el orden da igual»

En matemática sí; en float cada paréntesis redondea. (G+1)+1 pierde y G+(1+1) salva: el orden es algoritmo.

«Con N grande el error se promedia a cero»

Solo si los redondeos son simétricos. Con pasos todos positivos o truncados el sesgo es lineal con N: crece, no se cancela (Tema 41).

«fsum/Kahan son para obsesivos»

Son para totales largos o dispares: pasar de error ×N a error ×1 con unas líneas. fsum es la verdad de referencia para medir.

«Si cada paso tiene ½ ulp, el total está bien»

½ ulp × 10⁶ pasos = 5×10⁵ ulps de cota: en 1,0 eso es ~1e−10, y con absorción es el 100 %. La cota por operación no es cota del total.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: el reloj que deriva

Compará t += 0,1 N veces contra i*0,1 y medí la deriva.

N = 10000
t = 0.0
for _ in range(N):
    t += 0.1
print(repr(t), repr(N * 0.1), repr(t - N * 0.1))
Ver solución razonada

Deriva ≈ 1e−12 creciendo con N: cada += redondea con sesgo. El reloj honesto es entero (i) escalado al final (i*dt).

Ejercicio 2: contar los perdidos

Medí cuántos unos pierde la suma ingenua sobre 1e16 con N = 2000.

import math

G, N = 1e16, 2000
naive = G
for _ in range(N):
    naive += 1.0
print("naive - G:", naive - G)
print("fsum - G:", math.fsum([G] + [1.0] * N) - G)
Ver solución

0,0 vs 2000,0: la ingenua pierde los 2000 por absorción (ulp = 2 en G); fsum los recupera juntando exacto por dentro.

Ejercicio 3: ordenar salva

Escribí suma_ordenada (ascendente por |x|) y comparala con sum sobre valores dispares.

Ver una posible respuesta
import math

def suma_ordenada(vals):
    return math.fsum(sorted(vals, key=abs))


vals = [1e16, 1.0, 1.0, 1.0, -1e16]
print(sum(vals), suma_ordenada(vals), math.fsum(vals))

La ingenua puede dar 2,0 o 0,0 según el orden de llegada; la ordenada y fsum dan 3,0 estable: chicos con chicos primero.

09 · Síntesis

Ideas para recordar

  • N sumas ingenuas: cota N × ½ ulp; con sesgo crece lineal, al azar como √N.
  • Absorción: chico + gigante = gigante; el aporte se pierde entero.
  • La suma float no es asociativa: chicos-primero, bloques o Kahan.
  • Relojes con enteros (i*dt); totales críticos con fsum.
  • El promedio hereda el error de la suma: primero sumar bien, después dividir.

En el próximo tema convertimos estas curas en método: la estabilidad numérica, o cómo elegir algoritmos que no amplifiquen el error.