Capítulo 3 · El mecanismo

Un paso, célula por célula

Ya conoces las piezas: la cuadrícula, la percepción y la red. Aquí se juntan en un paso de tiempo completo, y lo miras en cámara lenta con la red ya entrenada. Al final puedes hacer clic en cualquier célula y ver cada número que calcula: lo que percibe, qué neuronas se le encienden y qué decide cambiar.

1

El estado: 82 944 números

La cuadrícula tiene 72 × 72 = 5 184 células, y cada una guarda 16 números, sus canales. En total, el mundo completo en un instante son 82 944 números.

CanalNombreQué es
0, 1, 2R, G, BEl color que se ve, premultiplicado por alfa (abajo se explica).
3alfaQué tan "presente" está la célula. Sobre 0,1 la célula está viva.
4 a 15ocultos12 números que no se ven ni se comparan con nada. Las células los usan para mandarse señales: son su química inventada.

Color premultiplicado

En vez de guardar "verde, con alfa 0,5", se guarda "verde × 0,5". Una célula con alfa 0,5 y color verde puro (0, 1, 0) guarda (0, 0,5, 0, 0,5). Para dibujarla sobre un fondo blanco:

pantalla = rgb + (1 − alfa) · blanco

La ventaja es que una célula vacía, con alfa 0, tiene también color 0 automáticamente, y que mezclar o promediar células premultiplicadas da el resultado correcto. Así lo hace el artículo, y así está escrito el objetivo: la lagartija se convierte a color premultiplicado antes de entrenar.

2

La semilla: una célula madre

Todo empieza con la cuadrícula en ceros salvo la célula del centro, que tiene alfa = 1 y los 12 canales ocultos = 1. Su color es 0, o sea que no se ve nada, pero está viva y "grita" en todos sus canales químicos.

# nca.py
def semilla(lado, lote=1):
    x = torch.zeros(lote, CANALES, lado, lado)
    x[:, 3:, lado // 2, lado // 2] = 1.0       # canales 3 a 15 de la célula del centro
    return x

La semilla es la única asimetría del mundo. Cada célula aplica la misma regla, y la única razón por la que la del centro hace algo distinto a la de la esquina es que tiene a la semilla cerca. A medida que crece, la lagartija entera "sabe" dónde va cada parte por las señales químicas que salieron de ese punto.

3

Los cuatro tiempos de un paso

  1. Percibir. Cada célula calcula sus 48 números: valor, gradiente x y gradiente y de sus 16 canales, mirando su bloque de 3×3 (capítulo 2, sección 6).
  2. Pensar. La red convierte esos 48 números en un cambio de 16: dx = W₂ · ReLU(W₁ · p + b₁).
  3. Disparar. Cada célula lanza una moneda. Con probabilidad ½ aplica su cambio (x = x + dx); si no, se queda igual en este paso.
  4. Vivir. Se revisa quién tiene vecinas vivas. Una célula que no tenía ninguna vecina viva (ella incluida) antes o después del cambio se pone entera en cero.

Ojo con algo sutil: todas las células perciben el estado anterior. Si la célula A cambia en este paso, su vecina B no ve ese cambio hasta el paso siguiente. Por eso el código lee de x y escribe en una copia nueva.

4

Un paso en cámara lenta

Elige hasta qué paso crecer y después avanza de a uno. Cada panel es uno de los cuatro tiempos, con la red entrenada.

Un paso de tiempo, separado en sus tiempos

Crecer hasta
1 · estado en el paso 0
2 · pensar: tamaño del cambio que propone cada célula
3 · disparar: quién aplica su cambio
4 · vivir: vivas, creciendo y borradas
estado en el paso 1
disparó / viva no disparó creciendo (alfa ≤ 0,1, con vecina viva) borrada en este paso

Lo que conviene mirar:

  • En el panel 2, con la lagartija ya crecida (paso 200), todas las células proponen cambios minúsculos: la forma está en equilibrio y la regla dice "quédate así". Medido con el modelo final, el color de una célula cambia en promedio 0,003 por paso. Ahora pulsa "Herir el centro": las células del borde de la herida se encienden, con cambios unas 8 veces más grandes que antes.
  • En el panel 3, la mitad de las células calculadas no aplica su cambio: es ruido puro, distinto en cada paso. Avanza varias veces y compara.
  • En el panel 4, la franja ámbar alrededor de la lagartija es la zona donde puede aparecer vida nueva. Fuera de ella no se calcula nada.
5

Vivas, creciendo y muertas

Cada célula está en uno de tres estados, según su alfa y el de sus vecinas:

EstadoCondiciónQué puede hacer
Vivaalfa > 0,1Todo: percibe, piensa y cambia. Es parte de la forma.
Creciendoalfa ≤ 0,1, pero alguna vecina tiene alfa > 0,1También calcula y cambia. Es el borde por donde la forma se expande.
Muertani ella ni sus 8 vecinas tienen alfa > 0,1Nada. Todos sus canales se fuerzan a 0.

"Alguna vecina viva" se calcula con un max pooling de 3×3 sobre el canal alfa: el máximo del bloque. Si el máximo supera 0,1, hay vida cerca.

# nca.py
def vivas(x):
    return F.max_pool2d(x[:, 3:4], 3, stride=1, padding=1) > UMBRAL_VIDA

¿Para qué sirve? Para dos cosas. Primero, impide que aparezca vida de la nada en una zona vacía: sin esta regla, el sesgo b₁ haría que cada célula vacía del mundo calculara un cambio y el fondo se llenara de basura. Segundo, la máscara se aplica antes y después del cambio: una célula sobrevive al paso solo si tenía vida cerca al empezar y la sigue teniendo al terminar. Si una célula baja su alfa y deja a sus vecinas sin apoyo, ellas mueren en ese mismo paso. Así la forma se puede encoger, no solo crecer.

6

Por qué solo la mitad, al azar

En el Juego de la Vida todas las células cambian a la vez, como si un reloj global marcara el compás. En un tejido de verdad no existe tal reloj: cada célula hace lo suyo a su ritmo. El artículo imita esto con la actualización estocástica: en cada paso, cada célula aplica su cambio con probabilidad 0,5.

  • Rompe la simetría. La semilla es perfectamente simétrica. Si todas las células actualizaran a la vez, la forma tendería a crecer simétrica, como un diamante. El azar permite que una lagartija, que no es simétrica, salga de un punto.
  • Obliga a ser robusta. La regla tiene que funcionar sin importar el orden en que actúen las células. No puede depender de "en el paso 37 mi vecina hace tal cosa". Esta robustez es la base para que después aprenda a mantenerse y a regenerarse.
  • Es una forma de ruido para entrenar. Cada vez que el autómata crece, crece distinto. La red ve muchas variantes y aprende una regla general, no una coreografía exacta.
# nca.py · solo calcula la red donde dispara y hay vida cerca: en el resto el cambio sería 0 igual
disparo = torch.rand_like(x[:, :1]) < prob_disparo
calcula = (antes & disparo)[:, 0]
7

Lo que piensa una célula

Aquí la lagartija ya creció y está quieta. Haz clic en cualquier célula y verás su cálculo completo con los pesos reales. Prueba una del centro del cuerpo, una del borde, una de la cola y una vacía junto a la lagartija. Después hiere el centro y mira las células del borde de la herida.

Estado 16 canales: R, G, B, α y ocultos 1 a 12

Percepción 48 = 16 canales × (valor, grad x, grad y)

Capa oculta 128 neuronas tras ReLU

Cambio propuesto dx, 16 canales

Lo que vas a notar:

  • Las células del interior proponen cambios casi nulos. Ya están donde deben, y la regla aprendió que en esa situación hay que quedarse quieta. Que sea tan cercano a cero, sin serlo del todo, es lo que hace que la forma persista.
  • Una célula vacía junto a la lagartija tiene estado casi cero, pero su percepción no: los gradientes le dicen que hay vida a un lado. Con eso le basta a la red para decidir si nace o no.
  • Cada célula enciende una combinación distinta de neuronas, aunque todas usen la misma red. La red es una sola. Lo que cambia es la situación.
  • Los canales ocultos tienen valores grandes y variados dentro de la lagartija, mucho más que el color. Nadie le dijo a la red qué guardar en ellos, y no hay una forma simple de saber qué significa cada uno. Lo que sí se ve en los rayos X del laboratorio es que cada canal marca una zona distinta del cuerpo, como si la red se hubiera inventado sus propias coordenadas para saber dónde está cada célula.
8

El mismo paso en Python y en JavaScript

El entrenamiento corre en PyTorch (nca.py) y la página corre en tu navegador (web/nca.js). Son dos implementaciones del mismo cálculo. PyTorch trabaja con la cuadrícula entera de una vez, como tensores. JavaScript va célula por célula, con bucles, que es más lento pero se lee como la explicación.

# nca.py: toda la cuadrícula (y un lote) de una vez
def forward(self, x, prob_disparo=0.5):
    antes = vivas(x)
    disparo = torch.rand_like(x[:, :1]) < prob_disparo
    calcula = (antes & disparo)[:, 0]
    p = self.percibir(x).permute(0, 2, 3, 1)[calcula]
    cambio = self.capa2(torch.relu(self.capa1(p)))
    dx = x.new_zeros(...)
    dx[calcula] = cambio
    x = x + dx.permute(0, 3, 1, 2)
    return x * (antes & vivas(x))
// nca.js: célula por célula
function paso(x, lado, red, { prob = 0.5 } = {}) {
  const antes = vivas(x, lado);
  const y = x.slice();
  for (cada célula n en (i, j)) {
    if (!antes[n]) continue;
    if (Math.random() >= prob) continue;
    percibir(x, lado, i, j, p);    // 48
    pensar(red, p, h, dx);         // 128 → 16
    for (let c = 0; c < 16; c++) y[n * 16 + c] += dx[c];
  }
  const despues = vivas(y, lado);
  // sin vida antes y después: todo a cero
}
Verificado

¿Cómo sabemos que calculan lo mismo? verificar_js.py hace crecer la lagartija en PyTorch, da un paso sin azar en los dos lenguajes con los mismos pesos y compara los 82 944 números. La mayor diferencia es del orden de 10⁻⁷: solo redondeo de float32.

9

Cuánto cuesta un paso

Por cada célula que calcula, la red hace 48 × 128 + 128 × 16 = 8 192 multiplicaciones. Si se calculara en las 5 184 células, serían unos 42 millones por paso. Pero casi ninguna lo necesita:

  • Las células muertas terminan en cero igual, así que no tiene sentido calcularlas.
  • De las vivas y creciendo, solo la mitad dispara, y el cambio de las que no disparan se descarta.

Con la lagartija crecida, eso deja unas pocas centenas de células por paso.

Las dos implementaciones calculan solo donde hace falta. En el entrenamiento, saltarse esas células hizo cada paso entre 2 y 4 veces más rápido, con resultados idénticos. La historia está en las decisiones técnicas.