Lección 1 de 10 · La red · 16 min
Una neurona
Suma pesada, sesgo, activación y el perceptrón
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 uno: una neurona. En esta serie armamos una red neuronal desde la pieza más chica, con código que corre y cada fórmula dibujada. Sin transformers: las bases que todo lo demás da por sabidas. Hoy, la pieza más chica.
El mapa de la serie
El recorrido tiene diez episodios. Los tres primeros son la pieza: una neurona, muchas neuronas juntas formando una capa, y por qué hace falta algo no lineal entre capa y capa. Después, cómo aprende: la pérdida, los gradientes a mano con backpropagation, y un autograd desde cero, como el de PyTorch. Luego, entrenar bien: el training loop completo y cómo evitar que la red memorice. Y al final, las herramientas que hacen posible apilar muchas capas.
Una neurona → Neurona → capa → No linealidad → Pérdidas → Backprop a mano → Autograd → Training loop → Regularización → Normalización → Profundidad
Parte 1 · La suma pesada
Una neurona artificial hace una cuenta de escuela primaria: multiplica y suma. Arranquemos con números.
Una neurona en NumPy, a mano
Tres números entran a la neurona. Les decimos x: un vector de tres entradas, uno, dos y tres. Cada entrada tiene su peso, y los pesos forman otro vector, w, también de tres números: cero coma cinco, menos cero coma cinco y cero coma dos. Un peso grande hace que esa entrada importe mucho; uno negativo, que empuje en contra. Y hay un número suelto, b, el sesgo. La cuenta: cada entrada por su peso, y lo vamos sumando en z. Al final, lo mismo en una línea, con el arroba de NumPy, que es el producto punto. Los tres productos dan cero coma cinco, menos uno y cero coma seis. Suman cero coma uno. Con el sesgo, cero coma seis.
x = np.array([1.0, 2.0, 3.0]) # 3 entradas
w = np.array([0.5, -0.5, 0.2]) # un peso por entrada
b = 0.5 # el sesgo
# a mano: cada entrada por su peso, y sumar
z = 0.0
for i in range(3):
print(f"x{i+1} * w{i+1} = {x[i]} * {w[i]:+} = {x[i]*w[i]:+.1f}")
z += x[i] * w[i]
print(f"suma = {z:.1f} + b = {z + b:.1f}")
# lo mismo en una línea: producto punto
z = w @ x + b
print("w @ x + b =", round(z, 2))
x1 * w1 = 1.0 * +0.5 = +0.5
x2 * w2 = 2.0 * -0.5 = -1.0
x3 * w3 = 3.0 * +0.2 = +0.6
suma = 0.1 + b = 0.6
w @ x + b = 0.6
Lo que calculó el código
cada entrada por su peso, y sumar
el sesgo se suma al final
lo mismo para n entradas: producto punto
Lo mismo, dibujado. En el dibujo, cada entrada se multiplica por su peso y llega al círculo, que suma. El grosor es el tamaño del peso; el rojo, el peso negativo. Sin sesgo, da cero coma uno. Ahora le sumamos el sesgo, cero coma cinco: z da cero coma seis. El sesgo es un empujón fijo, que no depende de las entradas. La forma general. La sigma mayúscula significa sumar: para cada i de uno a n, el peso i por la entrada i. Y w transpuesta por x, la w con una te arriba, es otra forma de escribir el producto punto. En el dibujo, las tres barras son los tres productos, y suman cero coma uno.
Parte 2 · La activación
Con la suma sola no alcanza. La neurona tiene que decidir algo, y para eso z pasa por una función más.
Tres activaciones
La activación es una función que recibe z y devuelve la salida de la neurona. El escalón: si z es mayor que cero, uno; si no, cero. Es un sí o un no, sin matices. La sigmoide: una ese suave que va de cero a uno. Cuanto más grande z, más cerca de uno, pero nunca salta. Y la ReLU, rectified linear unit: si z es negativa, cero; si es positiva, la deja pasar igual. Es la más usada hoy en redes profundas. Cuál conviene y por qué es el tema del episodio tres.
Las tres, en NumPy
En código son tres líneas. El escalón compara con cero y convierte el verdadero o falso en uno o cero. La sigmoide es uno sobre uno más e a la menos z. La ReLU es el máximo entre cero y z. Las probamos con cuatro valores de z, entre ellos el cero coma seis de nuestra neurona. Mirá la columna del cero coma seis: el escalón dice uno, la sigmoide dice cero coma sesenta y cinco, y la ReLU lo deja en cero coma seis. En cero, el escalón dice cero, porque pide mayor estricto.
def escalon(z): return (z > 0).astype(float)
def sigmoide(z): return 1 / (1 + np.exp(-z))
def relu(z): return np.maximum(0, z)
z = np.array([-2.0, 0.0, 0.6, 3.0]) # 0.6 es nuestra z
print("z ", z)
print("escalón ", escalon(z))
print("sigmoide ", sigmoide(z).round(2))
print("ReLU ", relu(z))
z [-2. 0. 0.6 3. ]
escalón [0. 0. 1. 1.]
sigmoide [0.12 0.5 0.65 0.95]
ReLU [0. 0. 0.6 3. ]
La activación, dibujada
sigmoide: aplasta cualquier z entre 0 y 1
negativo → 0; positivo → pasa igual
el escalón tiene pendiente 0 en casi todos lados
Sigma de z, con la sigma minúscula griega, es la sigmoide. La e es el número de Euler, dos coma siete uno ocho. En el dibujo: z igual a menos dos da cero coma doce; nuestra z, cero coma seis, da cero coma sesenta y cinco. La ReLU es el máximo entre cero y z. En el dibujo, la neurona completa: suma, sesgo y ReLU, y sale cero coma seis. Una pista para más adelante. En el dibujo, la recta que toca la curva: su inclinación es la pendiente, la derivada. La sigmoide tiene pendiente en todos lados; el escalón es plano salvo en el salto. Aprender con gradientes necesita pendiente: episodio cinco.
Lo mismo con torch.nn.Linear
En PyTorch, una neurona es una capa lineal con una sola salida. nn punto Linear de tres a uno crea una matriz de pesos W de una fila por tres columnas, y un sesgo. Una matriz es una tabla de números; esta tiene una sola fila, que es nuestro vector w. Copiamos nuestros números adentro, sin registrar gradientes. Llamar a la capa calcula x por W transpuesta más b. Transponer es dar vuelta la tabla: la fila pasa a ser columna, y queda el producto punto de antes. La activación no está adentro de Linear: se aplica aparte. Las formas: weight de uno por tres, bias de uno. PyTorch da z igual a cero coma seis, igual que NumPy, y la sigmoide da cero coma seis cuatro cinco siete.
Cada línea, su término:
nn.Linear(3, 1)→ — una fila de pesos, un sesgocapa.weight.copy_→ — nuestros númerosz = capa(x)→ — lo que calcula nn.Lineartorch.sigmoid(z)→ — la activación va aparte
capa = nn.Linear(3, 1) # 3 entradas, 1 salida
print("weight:", tuple(capa.weight.shape), " bias:", tuple(capa.bias.shape))
with torch.no_grad(): # copiamos nuestros números
capa.weight.copy_(torch.tensor([[0.5, -0.5, 0.2]]))
capa.bias.fill_(0.5)
x = torch.tensor([1.0, 2.0, 3.0])
z = capa(x) # = x @ weight.T + bias
y = torch.sigmoid(z)
print("torch: z =", round(z.item(), 4), " y =", round(y.item(), 4))
z_np = w_np @ x_np + b_np # la versión NumPy de antes
print("numpy: z =", round(z_np, 4))
print("iguales:", np.allclose(z.item(), z_np))
weight: (1, 3) bias: (1,)
torch: z = 0.6 y = 0.6457
numpy: z = 0.6
iguales: True
Una neurona, como la dibuja todo el mundo
El dibujo de todos los libros. Tres círculos a la izquierda: no calculan nada, son las entradas. Un círculo a la derecha: la neurona. Cada línea es un peso, y adentro del círculo pasan la suma, el sesgo y la activación. Acá los colores son esquemáticos; los pesos de verdad son los de la escena anterior. Cuando en el episodio dos pongamos varias neuronas en columna, este dibujo se convierte en una capa.
Parte 3 · El perceptrón aprende
Hasta ahora los pesos los elegimos nosotros. La gracia es que la neurona los encuentre sola, mirando ejemplos.
La regla del perceptrón
- Mirar un ejemplo y predecir con el escalón
- Si acertó: no tocar nada
- Si dijo 0 y era 1: sumarle la entrada a los pesos
- Si dijo 1 y era 0: restarle la entrada
El perceptrón es una neurona con escalón, más una regla para corregir los pesos. Así se enseña hoy. Tomás un ejemplo con su respuesta correcta, y la neurona predice cero o uno. Si acertó, no se toca nada. Si dijo cero y era uno, z quedó corta: le sumás a los pesos un pedacito de la entrada, así la próxima vez z da más grande para ese ejemplo. Y si dijo uno y era cero, al revés: le restás un pedacito. Repetís con todos los ejemplos, una y otra vez, hasta que no haya errores.
La regla, con números
el escalón: 1 si se cumple, 0 si no
y: la respuesta correcta
η: learning rate (0,1 en el lab)
y sombrero, la y con techito, es la predicción. Los corchetes valen uno si lo de adentro se cumple y cero si no: es el escalón. En el dibujo, con los pesos que aprende el lab y la entrada uno, uno: z da cero coma uno, positivo, así que y sombrero es uno. El error es la respuesta correcta, y, menos la predicción. En el dibujo, la primera pasada del lab: tres aciertos y un error. Y la corrección: a w le sumamos eta por el error por x. Eta, la letra griega, es el learning rate: qué tan grande es cada paso. Al sesgo, eta por el error. Si el error es cero, nada cambia. En el dibujo, ese error en uno, uno: w y b pasan de cero a cero coma uno, y z sube de cero a cero coma tres.
Un perceptrón aprende AND
En código, con la compuerta lógica AND: dos entradas que valen cero o uno, y la salida es uno solo si las dos son uno. Cuatro ejemplos, y sus respuestas. Pesos y sesgo arrancan en cero; learning rate, cero coma uno. Para cada ejemplo, predecimos con el escalón. Calculamos el error y corregimos. Una epoch es una pasada por todos los ejemplos; frenamos cuando una epoch entera sale sin errores. En la primera epoch, un error. Después tres, y en la epoch seis, cero. Pesos finales: cero coma dos y cero coma uno, sesgo menos cero coma dos. Y predice cero, cero, cero, uno: AND.
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
y = np.array([0, 0, 0, 1]) # AND: 1 solo si las dos son 1
w, b, eta = np.zeros(2), 0.0, 0.1 # eta: learning rate
for epoch in range(1, 11):
errores = 0
for xi, yi in zip(X, y):
y_hat = float(w @ xi + b > 0) # escalón
error = yi - y_hat # +1, -1 o 0
w += eta * error * xi # corregir hacia xi
b += eta * error
errores += int(error != 0)
print(f"epoch {epoch}: errores={errores} w={w.round(2)} b={b:.1f}")
if errores == 0:
break
print("predice:", (X @ w + b > 0).astype(int))
epoch 1: errores=1 w=[0.1 0.1] b=0.1
epoch 2: errores=3 w=[0.2 0.1] b=0.0
epoch 3: errores=3 w=[0.2 0.1] b=-0.1
epoch 4: errores=2 w=[0.2 0.2] b=-0.1
epoch 5: errores=1 w=[0.2 0.1] b=-0.2
epoch 6: errores=0 w=[0.2 0.1] b=-0.2
predice: [0 0 0 1]
Lo que aprendió es una recta
En el plano, cada ejemplo es un punto: verde el uno, uno; rojos los otros. La neurona dice uno de un lado de la recta donde z vale cero. Después de la primera epoch, la recta queda lejos, abajo a la izquierda: todos los puntos le dan uno. En la epoch cuatro ya pasa entre los puntos, pero todavía mal. En la epoch seis separa al verde de los rojos. Ojo con el uno, cero: queda justo sobre la recta, z da cero, y el escalón dice cero. Acierta sin margen. Y el vector w es perpendicular a la recta y apunta hacia el lado del uno.
Rosenblatt, 1958 — The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain · fuente
1958 · «The theory has been developed for a hypothetical nervous system, or machine, called a perceptron.»
La idea tiene nombre y fecha. En mil novecientos cincuenta y ocho, Frank Rosenblatt, en el Cornell Aeronautical Laboratory, publicó en Psychological Review la teoría de un sistema nervioso hipotético, o una máquina, al que llamó perceptron. Tenía una retina de sensores, unidades de asociación conectadas al azar y unidades de respuesta. Cada unidad de asociación se dispara si la suma de impulsos llega a un umbral: el escalón. La regla con eta es la forma que se enseña hoy.
Parte 4 · Lo que una neurona no puede
Una neurona separa con una recta. Hay problemas que ninguna recta separa.
El mismo perceptrón contra XOR
La compuerta XOR, o exclusivo: uno si las entradas son distintas, cero si son iguales. Solo cambian las respuestas. La regla es exactamente la misma. Corremos cien epochs e imprimimos algunas. Y comparamos lo que predice con lo que queríamos. Dos errores, tres, y después cuatro errores en cada epoch, de cuatro ejemplos, para siempre. Al final predice uno, uno, cero, cero, cuando queríamos cero, uno, uno, cero.
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
y = np.array([0, 1, 1, 0]) # XOR: 1 si son distintas
w, b, eta = np.zeros(2), 0.0, 0.1
for epoch in range(1, 101):
errores = 0
for xi, yi in zip(X, y):
error = yi - float(w @ xi + b > 0)
w += eta * error * xi
b += eta * error
errores += int(error != 0)
if epoch in (1, 2, 3, 10, 50, 100):
print(f"epoch {epoch:3}: errores={errores}")
print("predice:", (X @ w + b > 0).astype(int), " quería:", y)
epoch 1: errores=2
epoch 2: errores=3
epoch 3: errores=4
epoch 10: errores=4
epoch 50: errores=4
epoch 100: errores=4
predice: [1 1 0 0] quería: [0 1 1 0]
XOR: ninguna recta alcanza
En el plano. Los verdes están en una diagonal y los rojos en la otra. Probá una recta: deja un verde y un rojo del mismo lado. Probá otra: lo mismo. Pasa con cualquiera: ninguna recta deja a los dos verdes de un lado y a los dos rojos del otro. Ahora, con dos rectas. La primera deja abajo solo al cero, cero. La segunda deja arriba solo al uno, uno. Los verdes quedan en la franja del medio. Dos rectas son dos neuronas, y alguien tiene que combinar sus respuestas.
Adelanto: una red chica
Eso es una red. Las dos entradas. Una hidden layer: se llama así porque no es ni entrada ni salida, no se ve desde afuera. Tiene dos neuronas: cada una dibuja una de las dos rectas. Y una neurona final que combina sus dos respuestas. Así se resuelve XOR. Apilar capas de perceptrones es lo que le da nombre al MLP, multilayer perceptron: el bloque que en Alineación, episodio uno, vimos adentro de cada capa de Gemma. Calcular una capa entera de una vez, con matrices, es el episodio dos.
Resumiendo
- Neurona: , cada entrada por su peso, más el sesgo
- Activación: escalón, sigmoide o ReLU, aplicada a
- nn.Linear(3, 1) calcula : lo mismo que NumPy
- Perceptrón: hasta cero errores
- Una neurona separa con una recta: AND sí, XOR no
- Próximo: de neurona a capa, con matrices y batch
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: Una neurona
Conectando con el tutor…