Lección 3 de 10 · La red · 14 min
Por qué hace falta no linealidad
ReLU, GELU, sigmoide y tanh, y qué pasa sin ellas
Notebook del episodio
El código de esta lección, para correrlo y modificarlo en Google Colab, gratis y con tu cuenta. Para guardar tus cambios: Archivo → Guardar una copia en Drive.
Deep learning a secas. Episodio tres: por qué hace falta no linealidad. En el episodio dos armamos capas y las pusimos en fila. Hoy sacamos la activación del medio y vemos cómo se derrumba todo. Y después miramos las activaciones que se usan de verdad, y qué hace cada una.
La pregunta de hoy
- Sin activación, dos capas lineales son una sola
- Y una sola capa lineal no puede con XOR
- La activación dobla el espacio
- Sigmoide, tanh, ReLU, GELU: qué cambia entre ellas
El recorrido. Primero, una cuenta de álgebra: dos capas lineales seguidas, sin nada en el medio, equivalen a una sola. Apilar no sirve de nada. Y una sola capa lineal, como vimos, no puede con XOR, ni siquiera la mejor posible. La activación es lo que rompe eso: dobla el espacio. Y las cuatro activaciones más comunes: sigmoide, tanh, ReLU, rectified linear unit, y GELU, Gaussian error linear unit. De cada una, su forma y su pendiente.
Parte 1 · Dos capas lineales son una
Arrancamos con el experimento más simple: dos nn.Linear seguidas, sin ReLU en el medio.
Dos capas sin activación
Una capa de tres a cuatro, con pesos al azar, y sin activación. Otra de cuatro a dos. Ahora fabricamos una sola matriz: W dos por W uno. Cuatro por tres, multiplicada desde la izquierda por dos por cuatro, da dos por tres. Y un solo sesgo: W dos por b uno, más b dos. Pasamos un batch de cinco ejemplos al azar por las dos capas, y por la capa única, y comparamos. La capa única es de dos por tres, como si fuera una sola nn.Linear de tres a dos. Y la comparación da True: las dos capas calculan exactamente lo mismo que una.
capa1 = nn.Linear(3, 4) # 3 -> 4, SIN activación
capa2 = nn.Linear(4, 2) # 4 -> 2
# una sola capa equivalente: W = W2 W1, b = W2 b1 + b2
W = capa2.weight @ capa1.weight # (2,4)@(4,3) = (2,3)
b = capa2.weight @ capa1.bias + capa2.bias
X = torch.randn(5, 3) # un batch de 5
dos_capas = capa2(capa1(X))
una_capa = X @ W.T + b
print("W:", tuple(W.shape), " b:", tuple(b.shape))
print("iguales:", torch.allclose(dos_capas, una_capa, atol=1e-6))
W: (2, 3) b: (2,)
iguales: True
Por qué colapsan
la capa 2 recibe la salida de la capa 1
propiedad distributiva y asociativa
una sola capa lineal: igual de limitada
La cuenta, paso por paso. La segunda capa recibe lo que sale de la primera: W dos multiplica a todo el paréntesis. En el dibujo, la matriz W dos, de dos por cuatro, por el vector de cuatro que sale de la capa uno. Ahora distribuimos: W dos por W uno x, más W dos por b uno, más b dos. Y agrupamos: el producto de matrices se puede hacer primero, porque es asociativo. En el dibujo, W dos por W uno da una matriz de dos por tres, que llamamos W prima. Es fija: no depende de x. Lo mismo con el sesgo, que llamamos b prima. Resultado: una capa lineal. Con cien capas pasa igual: todas se multiplican en una sola matriz.
El mejor modelo lineal, contra XOR
Y una sola capa lineal, ¿qué tan bien puede hacerlo con XOR? No hace falta entrenar para saberlo. Las cuatro entradas y XOR. Le agregamos a X una columna de unos: multiplicada por un peso, hace de sesgo. Y lstsq, de mínimos cuadrados, encuentra los pesos que minimizan el error al cuadrado. Es el mejor modelo lineal que existe para estos datos, calculado de forma exacta. Miramos qué predice. Pesos cero, sesgo cero coma cinco. Predice un medio para las cuatro entradas. El mejor modelo lineal se rinde y contesta el promedio.
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
y = np.array([0, 1, 1, 0], dtype=float) # XOR
# el MEJOR modelo lineal posible (mínimos cuadrados): y ≈ X w + b
A = np.hstack([X, np.ones((4, 1))]) # columna de unos = sesgo
w1, w2, b = np.linalg.lstsq(A, y, rcond=None)[0]
print("w =", np.round([w1, w2], 3) + 0.0, " b =", round(b, 3))
print("predice:", (A @ [w1, w2, b]).round(3))
w = [0. 0.] b = 0.5
predice: [0.5 0.5 0.5 0.5]
Parte 2 · Las activaciones
La activación va entre capa y capa, celda por celda. Hay muchas; cuatro alcanzan para entender todas.
Forma y pendiente de cada una
sigmoide: pendiente máxima 0,25, en z = 0
tanh: centrada en cero, pendiente 1 en z = 0
pendiente 0 o 1: no se achica para z > 0
Φ(z): probabilidad de que una normal estándar quede por debajo de z
La sigmoide, del episodio uno, va de cero a uno. En el dibujo, mirá cómo, al moverse el punto, la tangente se para en el medio y se acuesta en los extremos: pendiente cero coma veinticinco en cero, y casi nada lejos. La tanh, tangente hiperbólica, tiene la misma forma de ese, pero va de menos uno a uno y está centrada en cero. Su pendiente en cero es uno. La ReLU: plana para z negativa, pendiente uno para z positiva. Mirá el punto: al cruzar el cero, la pendiente salta de cero a uno. Y la GELU: z por fi de z. Fi es la probabilidad de que un número sacado de una campana normal estándar quede por debajo de z: para z grande, casi uno; para z muy negativa, casi cero. En el dibujo, con z igual a menos uno, la ReLU daría cero, y la GELU deja pasar menos cero coma dieciséis.
Valores y pendientes, en PyTorch
Los números de esos dibujos salen de acá. Cinco valores de z, de menos cuatro a cuatro. Las cuatro funciones, como las trae PyTorch. La pendiente la medimos a mano: el valor un poquito a la derecha, menos el valor un poquito a la izquierda, dividido la distancia. Imprimimos valor y pendiente. La sigmoide tiene pendiente cero coma veinticinco en el centro y cero coma cero dos en los bordes. La tanh, uno en el centro. La ReLU, cero o uno. En el cero exacto, la ReLU no tiene pendiente definida: la cuenta numérica promedia y da un medio, y PyTorch, por convención, usa cero. La GELU tiene una pendiente negativa chiquita a la izquierda: deja pasar algo de lo negativo.
z = torch.tensor([-4.0, -1.0, 0.0, 1.0, 4.0])
fs = {"sigm": torch.sigmoid, "tanh": torch.tanh,
"ReLU": F.relu, "GELU": F.gelu}
fila = lambda t: " ".join(f"{round(float(v), 2) + 0:5.2f}" for v in t)
h = 1e-3 # pendiente numérica
print("z ", fila(z))
for nombre, f in fs.items():
pend = (f(z + h) - f(z - h)) / (2 * h)
print(f"{nombre:4} ", fila(f(z))) # valor
print(" pend", fila(pend)) # pendiente
z -4.00 -1.00 0.00 1.00 4.00
sigm 0.02 0.27 0.50 0.73 0.98
pend 0.02 0.20 0.25 0.20 0.02
tanh -1.00 -0.76 0.00 0.76 1.00
pend 0.00 0.42 1.00 0.42 0.00
ReLU 0.00 0.00 0.00 1.00 4.00
pend 0.00 0.00 0.50 1.00 1.00
GELU 0.00 -0.16 0.00 0.84 4.00
pend 0.00 -0.08 0.50 1.08 1.00
ReLU y GELU, de cerca
Las dos más usadas hoy, superpuestas. La ReLU, con su codo en cero. Y la GELU: para z grande coincide con la ReLU, pero el codo es suave, y para z un poco negativa baja por debajo de cero antes de volver. Que la curva sea suave significa que la pendiente cambia de a poco, sin saltos. La GELU es la que usan BERT, GPT y muchos transformers; en Alineación, episodio uno, era la no linealidad dentro del MLP, multilayer perceptron, de Gemma.
Por qué importa la pendiente máxima
Un adelanto de por qué la pendiente importa tanto. Cuando entrenemos, en el episodio cinco, vamos a ver que la señal que corrige los pesos atraviesa la red hacia atrás y, en cada capa, queda multiplicada por la pendiente de la activación. Con sigmoides, en el mejor caso, esa pendiente es cero coma veinticinco. Una capa: cero coma veinticinco. Dos: cero coma cero seis. Tres: menos de dos centésimos. Cinco: una milésima. Las primeras capas casi no se enteran del error. La ReLU, con pendiente uno, no tiene ese problema. Lo vemos entero en el episodio diez.
- 1 capa: 0,25
- 2 capas: 0,0625
- 3 capas: 0,016
- 5 capas: 0,001
Hendrycks & Gimpel, 2016 — Gaussian Error Linear Units (GELUs) · arXiv:1606.08415
x · Φ(x) · «The GELU nonlinearity weights inputs by their value, rather than gates inputs by their sign as in ReLUs»
La GELU tiene paper propio. Hendrycks y Gimpel la propusieron en dos mil dieciséis con esta idea: la ReLU deja pasar o corta según el signo, la GELU pesa cada entrada según su valor. La compararon contra ReLU en visión, lenguaje y voz. Dos años después, BERT la adoptó, y desde ahí quedó en muchos transformers. Fijate que no es un teorema: es una elección empírica, que funcionó mejor en sus experimentos.
Parte 3 · Qué hace la activación: doblar
Volvamos a XOR, ahora con ReLU en vez de escalón, y miremos qué le hace la activación al espacio.
XOR con una ReLU en el medio
Una solución clásica, con pesos elegidos a mano. Las dos neuronas ocultas suman las dos entradas. La diferencia está en el sesgo: la segunda resta uno. La salida es h uno menos dos veces h dos. La hidden layer con ReLU. Para el uno, uno, la hidden layer da dos y uno. La salida da cero, uno, uno, cero: XOR. Y fijate que la salida es lineal, sin escalón: la única no linealidad es la ReLU del medio. Sacala, y volvés a la capa única que contesta un medio para todo. Eso es lo que la activación aporta: no es un detalle, es la diferencia entre poder y no poder.
relu = lambda z: np.maximum(0, z)
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
W1 = np.array([[1, 1],
[1, 1]])
b1 = np.array([0, -1]) # la segunda neurona se corre 1
w2 = np.array([1, -2]) # salida: h1 - 2·h2
H = relu(X @ W1.T + b1) # hidden layer con ReLU
y = H @ w2
print("H =", H.tolist())
print("y =", y.tolist(), " (XOR: [0, 1, 1, 0])")
H = [[0, 0], [1, 0], [1, 0], [2, 1]]
y = [0, 1, 1, 0] (XOR: [0, 1, 1, 0])
La cuenta en (1, 1)
la primera neurona solo suma
la segunda se corre 1: se enciende tarde
la salida resta el exceso
El caso difícil, uno, uno. h uno es la ReLU de la suma: dos. h dos es la ReLU de la suma menos uno: uno. Con el cero, uno, en cambio, daría la ReLU de cero, que es cero. Esta neurona se enciende solo cuando la suma pasa de uno: ahí está el doblez. La salida: dos menos dos por uno, cero. En el dibujo, la última neurona recibe h, no x, y no tiene activación: es una resta.
El espacio, doblado
En el plano de la hidden layer, h uno horizontal y h dos vertical. El cero, cero queda en el origen. Los dos verdes caen juntos en uno, cero. Y el uno, uno se fue a dos, uno: la ReLU de la segunda neurona lo levantó, porque solo ahí la suma pasa de uno. Una recta los separa: la de la salida, h uno menos dos h dos igual a un medio. Los verdes, del lado del uno. La ReLU no separa nada por sí misma: dobla el plano por una línea, y después del doblez, una recta alcanza.
Muchos dobleces: imitar una curva
Con más neuronas, más dobleces. La curva a imitar: el seno entre menos tres y tres. Cada neurona ReLU dobla en un punto distinto, repartidos parejo. La hidden layer: una columna por neurona, cero antes de su codo y una rampa después. Los pesos de salida los sacamos con mínimos cuadrados, como antes. Y probamos con dos, cuatro y dieciséis neuronas. Con dos, el error máximo es cero coma ochenta y ocho: casi nada. Con cuatro, cero coma veintinueve. Con dieciséis, cero coma cero uno.
x = np.linspace(-3, 3, 200)
y = np.sin(x) # la curva a imitar
def aproximar(n): # n neuronas ReLU
codos = np.linspace(-3, 3, n) # dónde dobla cada una
H = np.maximum(0, x[:, None] - codos) # hidden layer: (200, n)
H = np.hstack([H, np.ones((200, 1))]) # + sesgo
a = np.linalg.lstsq(H, y, rcond=None)[0] # pesos de salida
return H @ a
for n in (2, 4, 16):
err = np.abs(aproximar(n) - y).max()
print(f"{n:2} neuronas ReLU: error máximo {err:.3f}")
2 neuronas ReLU: error máximo 0.882
4 neuronas ReLU: error máximo 0.288
16 neuronas ReLU: error máximo 0.013
Rectas que se doblan
Lo mismo, dibujado con las curvas del snippet. En punteado, el seno, la curva que queremos imitar. Con cuatro neuronas, cuatro tramos rectos: se nota cada codo. Con dieciséis, los tramos son tan cortos que la curva casi no se distingue del seno. Esta es la intuición de un resultado clásico, el teorema de aproximación universal: con suficientes neuronas en una hidden layer, se puede aproximar cualquier función continua tanto como quieras. Lo que el teorema no dice es cómo encontrar los pesos. Eso es entrenar.
Cuál usar hoy
- ReLU: la opción por defecto en redes densas
- GELU: la de los transformers
- Sigmoide: a la salida, cuando querés una probabilidad
- tanh: dentro de redes recurrentes y compuertas
En la práctica. La ReLU es la elección por defecto: barata, y no achica la pendiente. La GELU, en muchos transformers. La sigmoide casi no se usa entre capas, por lo que vimos de la pendiente, pero es la salida natural cuando la red tiene que dar una probabilidad entre cero y uno. Lo vemos en el próximo episodio. Y la tanh sigue viva dentro de las RNN, recurrent neural network, las redes recurrentes, donde hace falta un valor entre menos uno y uno.
Resumiendo
- Sin activación: , una sola capa
- El mejor modelo lineal contesta 0,5 en XOR
- Sigmoide y tanh: forma de S, pendiente chica en los bordes
- ReLU dobla en un punto; GELU, , es su versión suave
- Muchos dobleces aproximan cualquier curva
- Próximo: pérdidas, el número que mide el error
Papers del episodio
¿Qué tal esta lección?
Sé el primero en calificarla
¿Terminaste la lección?
Márcala para sumar +50 XP y mantener tu racha.
Tutor IA
Contexto: Por qué hace falta no linealidad
Conectando con el tutor…