Curso de ML EN

Capítulo 34 de 37 · avanzado

El perceptrón

Qué cubre este capítulo

Esta es la primera neurona del libro. Todo lo anterior fue estadística disfrazada de machine learning: un umbral sobre un feature, una probabilidad salida de una sigmoide, una recta ajustada bajando una función de pérdida. Al perceptrón le dijeron "célula cerebral" en 1958 y con eso arrancaron sesenta años de redes neuronales. Es una sola unidad lineal — una suma ponderada, un umbral duro, una salida de dispara-o-no — entrenada con una regla tan simple que la puedes correr en tu cabeza: le muestras un punto y, si se equivoca, empujas los pesos hacia la respuesta correcta. Ese es todo el algoritmo.

Lo construimos desde cero en NumPy: la entrada neta, la activación escalón, la regla de actualización y un loop de épocas que sigue barriendo los datos hasta que una pasada completa no comete ni un error. Después dejamos que el Perceptron de scikit-learn ajuste lo mismo y verificamos que los números cuadren. El plato fuerte es una animación de la propia regla de aprendizaje: la recta de decisión girando y desplazándose sobre un scatter en 2-D, un frame por corrección, con un contador de errores vaciándose hasta cero abajo.

Y luego viene el pero, porque este capítulo son en realidad dos historias. El perceptrón converge en un número finito de pasos si los datos son linealmente separables — un teorema de verdad, demostrable, y lo vamos a ver pasar. Si los datos no son separables, nunca se acomoda; oscila para siempre, corrigiendo los mismos puntos una y otra vez. Vamos a demostrar las dos cosas. Esa segunda falla no es una nota al pie. Es el muro que frenó la investigación en redes neuronales por más de una década, y es exactamente la razón por la que existe el siguiente capítulo.

Un poco de historia

La idea empieza con dos personas tratando de modelar un cerebro con matemáticas. En 1943 Warren McCulloch, neurofisiólogo, y Walter Pitts, lógico, publicaron un paper que mostraba que una red de unidades de umbral simples — suma tus entradas, dispara si cruzas un umbral — podía calcular cualquier función lógica que quisieras. Todavía sin aprendizaje; los pesos los cableabas a mano. Pero fue la primera afirmación de que el pensamiento podría ser aritmética, de que una neurona podría ser una compuerta lógica, y puso la unidad de umbral sobre la mesa como algo que valía la pena construir.

Frank Rosenblatt la construyó. En 1958, siendo psicólogo en Cornell, tomó la unidad de McCulloch-Pitts y le dio una regla de aprendizaje — una forma de fijar los pesos a partir de ejemplos en lugar de a mano — y al resultado lo llamó el perceptrón. No fue nada modesto al respecto. El Mark I Perceptron era hardware real, una máquina con una rejilla de fotoceldas que aprendió a distinguir formas, y el New York Times lo reseñó en 1958 diciendo que la Marina esperaba máquinas que caminaran, hablaran, vieran y se reprodujeran solas. Ese es el origen del ciclo de hype de las redes neuronales, y se lee igualito que todos los que vinieron después.

Luego, en 1969, Marvin Minsky y Seymour Papert escribieron un libro, "Perceptrons", que hizo las matemáticas con cuidado y encontró el piso. Un solo perceptrón nada más puede trazar una frontera recta, y hay problemas ridículamente simples que ninguna frontera recta resuelve — el or exclusivo, XOR, siendo el famoso. Dos entradas, salida verdadera cuando exactamente una está encendida. Cuatro puntos, y ninguna recta los separa. La máquina de Rosenblatt no podía aprenderlo y nunca iba a poder. El libro se leyó, con justicia o sin ella, como un veredicto sobre todo el enfoque; el financiamiento se secó y el campo se quedó callado por años, el tramo que hoy llaman el primer invierno de la IA. La ironía es que el arreglo — apila las unidades en capas y el problema de XOR se cae solito — ya era imaginable en 1969. Lo que faltaba era una forma de entrenar la pila, y eso tardó hasta los ochenta. Este capítulo es la unidad que Minsky y Papert acorralaron; el siguiente es la salida.

La intuición

Olvídate un momento de la metáfora del cerebro, hace más daño que bien. Un perceptrón es una recta, más una regla de qué lado es cuál. En dos dimensiones tienes un scatter de puntos de dos clases y quieres una frontera recta con una clase de cada lado. El perceptrón sostiene una frontera candidata, revisa cada punto y, cuando un punto está del lado equivocado, inclina y desplaza la recta para jalar ese punto al lado correcto. Haz eso suficientes veces y, si existe una recta perfecta, vas a caer en una.

Estos son los datos con los que lo vamos a enseñar: 80 puntos, dos clases, y los dos blobs quedan separados con espacio libre entre ellos. Armé este conjunto a propósito para que una recta lo separe limpiamente — ese hueco limpio es toda la precondición de lo que sigue.

Vale la pena sentarse un rato con el mecanismo que hace que esto funcione, porque es el germen de todo lo que viene después. La unidad no ajusta nada de un jalón y no minimiza ninguna pérdida. Reacciona, un error a la vez. Un punto mal clasificado es un empujón en una dirección; el vector de pesos acumula esos empujones; la recta queda donde apunten los empujones acumulados. Es guiado por error, es online y es local — las mismas tres propiedades que, generalizadas a muchas capas y a una activación suave, se convierten en backpropagation. Si entiendes esto, ya tienes la forma de todo el campo.

Las matemáticas

Tres ecuaciones, y la tercera es la única que importa. Empecemos con la entrada neta. Para un punto xix_i con DD features, un vector de pesos ww y un sesgo bb:

zi=wxi+b=j=1Dwjxij+bz_i = w^{\top} x_i + b = \sum_{j=1}^{D} w_j\, x_{ij} + b

Esa es la misma suma ponderada de todos los modelos lineales hasta ahora. La diferencia es qué hacemos con ella. La regresión logística aplastaba ziz_i para convertirlo en probabilidad. El perceptrón hace la cosa más burda posible: mira el signo. La activación es un escalón duro, y la predicción es de qué lado del cero cae el score:

y^i=sign(zi)={+1zi01zi<0\hat{y}_i = \operatorname{sign}(z_i) = \begin{cases} +1 & z_i \ge 0 \\ -1 & z_i < 0 \end{cases}

Por eso las etiquetas viven en {1,+1}\{-1, +1\} en vez de {0,1}\{0, 1\}. No es una decisión cosmética; es lo que hace que la regla de aprendizaje se colapse en una sola línea. La frontera es la superficie plana donde el score vale exactamente cero, wx+b=0w^{\top} x + b = 0, y todo lo que está de un lado dispara +1+1 y todo lo del otro dispara 1-1.

Ahora la regla. Toma un punto que la unidad falló — su predicción y^i\hat{y}_i no coincide con la verdad yiy_i. Actualiza los pesos y el sesgo así, con una learning rate η\eta:

ww+ηyixibb+ηyiw \leftarrow w + \eta\, y_i\, x_i \qquad b \leftarrow b + \eta\, y_i

Lee lo que hace eso. Si la etiqueta verdadera es +1+1 y la fallamos, sumamos el punto al vector de pesos, lo cual aumenta wxiw^{\top}x_i la próxima vez y empuja el score hacia lo positivo. Si la verdad es 1-1, restamos el punto y empujamos el score a lo negativo. Como yiy_i es ±1\pm 1, los dos casos son la misma expresión de arriba. Los puntos correctos no reciben actualización alguna — el perceptrón solo aprende de sus errores, lo cual es elegante o flojo según tu humor del día.

De aquí sale un hecho limpio que conviene guardarse. Como arrancamos los pesos en cero, la learning rate η\eta nada más reescala ww y bb de manera uniforme; nunca cambia de qué lado del cero cae un score, así que nunca cambia una sola predicción ni la secuencia de actualizaciones. En el perceptrón simple, tunear la learning rate es trabajo perdido. Yo la pongo en 1 y sigo adelante.

Y el teorema, porque es la razón por la que algo de esto está garantizado. Si las dos clases se pueden separar con algún hiperplano con un margen — una franja de ancho γ\gamma alrededor de la frontera sin puntos adentro — entonces el número de actualizaciones que hace el perceptrón antes de detenerse es a lo más (R/γ)2(R/\gamma)^2, donde RR es el radio de la bola más chica que contiene los datos. Finito. Acotado. No depende de cuántos puntos tengas ni de cuántos features. La regla de Rosenblatt, por tonta que se vea, es demostrablemente correcta con datos separables. La demostración es corta y la cota es toda la historia: un margen más grande significa menos correcciones, y sin margen —datos no separables— la cota es infinita, que es la manera educada de decir que nunca se detiene.

En qué es bueno y en qué no

Voy a ser honesto sobre dónde encaja esto, porque en 2026 no vas a poner un perceptrón crudo en producción. Sus virtudes son pedagógicas e históricas, no prácticas. En lo que sí es genuinamente bueno es en ser la cosa más simple que aprende: online, un punto a la vez, memoria constante, sin matrices que invertir ni pérdidas que derivar. Con datos que de verdad son linealmente separables encuentra una frontera separadora y la encuentra rápido, y la garantía de convergencia es de ese tipo de resultados limpios que el resto del ML casi nunca te da. Como el átomo del que está hecho el estilo de aprendizaje guiado por errores, es el lugar correcto para empezar.

Los problemas son justo la razón por la que es una lección de historia. Se detiene en cualquier separador, no en el mejor — en cuanto una pasada sale limpia se retira, así que la frontera donde cae puede quedar pegadita a una clase en lugar de partir el hueco por la mitad, que es exactamente el defecto que las máquinas de soporte vectorial se inventaron para arreglar. Con datos que no son separables no se degrada con gracia; nunca converge, brincando entre vectores de pesos para siempre sin ninguna señal de que se rindió. Y el límite profundo es el que Minsky y Papert nombraron: la frontera es una sola recta, así que cualquier problema cuyas clases no sean linealmente separables simplemente queda fuera de su alcance. XOR es el ejemplo de dos bits, y la mayoría de los problemas reales son XOR con más dimensiones y peor letra. A este modelo no le echas mano en producción. Le echas mano a aquello en lo que se convirtió.

Los datos

Dos datasets, porque este capítulo tiene que mostrar un éxito y una falla.

El primero es el conjunto separable de arriba: 80 puntos, 31 en la clase positiva y 49 en la negativa, dibujados a cada lado de una recta diagonal fija con un hueco de 0.35 tallado alrededor para que ningún punto quede dentro del margen. Ese hueco no es decoración. Es lo que hace que los datos sean linealmente separables por construcción, que es lo que hace que aplique el teorema de convergencia. Si dejara que los puntos se metieran al margen, la garantía se evaporaría, así que los rechazo cuando genero los datos.

El segundo es XOR, el problema que rompió al perceptrón. Cuatro blobs gaussianos en las esquinas de un cuadrado, etiquetados como tablero de ajedrez: las dos esquinas diagonales comparten una clase, las otras dos comparten la otra. Aquí está.

Míralo e intenta trazar una sola recta con el cian de un lado y el morado del otro. No se puede, y no es cuestión de echarle más ganas — las dos clases quedan en diagonales opuestas, así que cualquier recta que acierte una diagonal le entrega la otra al lado equivocado. Este es todo el argumento de Minsky y Papert en una imagen. El perceptrón va a perseguir estos datos para siempre.

Constrúyelo, una función a la vez

Cuatro piezas chiquitas, de abajo hacia arriba, en el orden en que las escribirías. Empieza con la entrada neta — la suma ponderada, la única aritmética del modelo.

def net_input(x, w, b):
    """The weighted sum plus bias for one sample: z = w·x + b.

    x and w are length-D vectors, b is a scalar. This is the whole linear
    part of the neuron — everything else is just deciding what to do with
    its sign.
    """
    return float(np.dot(w, x) + b)

Luego la activación. Esta es la línea que separa a un perceptrón de todo lo anterior en el libro: no una probabilidad, no un número real, solo un signo.

def step(z):
    """Step activation: fire +1 when the net input is non-negative, else -1.

    This hard threshold is what makes the perceptron a classifier rather
    than a regressor, and also what makes it non-differentiable — the
    reason later networks swap it out for a smooth activation.
    """
    return 1 if z >= 0.0 else -1


def predict(x, w, b):
    """Classify one sample by the sign of its net input."""
    return step(net_input(x, w, b))

Separé step de predict a propósito. La función escalón es la activación — lo que las redes posteriores reemplazan por algo suave — y mantenerla como una pieza con nombre propio hace que ese cambio se lea claro cuando lleguemos ahí. Fíjate en el desempate: un score de exactamente cero dispara +1+1. Es una decisión arbitraria sobre un evento de medida cero, pero escoge una y sé consistente.

Ahora la regla, la razón entera de que el perceptrón aprenda. Todo lo de arriba es andamio para estas dos líneas.

def update(x, y, w, b, lr):
    """The perceptron correction for one misclassified sample.

    Push the boundary toward getting this point right: add the point to the
    weights if its true label is +1, subtract it if -1. Because y is ±1 the
    two cases fold into one line.

        w ← w + lr · y · x
        b ← b + lr · y

    Returns the updated (w, b). This is the entire learning rule.
    """
    w = w + lr * y * x
    b = b + lr * y
    return w, b

Eso es todo. Ese es el algoritmo que el New York Times creyó que iba a caminar y hablar. Suma el punto a los pesos, o réstalo, según su etiqueta. El sesgo se mueve solo con la etiqueta, que es la misma regla con un feature constante de 1 incorporado. Si esto se ve demasiado simple para funcionar, esa es la reacción correcta, y sí funciona — con los datos adecuados.

Última pieza, el loop que amarra todo: barre los datos, actualiza en cada error y detente cuando una pasada completa salga limpia.

def fit(X, y, lr=1.0, max_epochs=50, seed=0, record=False):
    """Online perceptron learning: sweep the data, correct on every mistake.

    Start the weights at zero. One epoch is a full pass over the samples in
    a seeded random order. For each sample, if the unit's prediction
    disagrees with the truth, apply the perceptron update. Count the
    mistakes in the pass; if a whole epoch goes by with zero of them, the
    data is separated and we stop — that clean pass is the convergence
    check. With `record=True` we also return the per-update history and the
    mistakes-per-epoch list so the chapter can replay the learning.
    """
    rng = np.random.default_rng(seed)
    N, D = X.shape
    w = np.zeros(D)
    b = 0.0
    history = []
    mistakes_per_epoch = []
    for epoch in range(max_epochs):
        order = rng.permutation(N)
        mistakes = 0
        for i in order:
            xi, yi = X[i], y[i]
            if predict(xi, w, b) != yi:
                w, b = update(xi, yi, w, b, lr)
                mistakes += 1
                if record:
                    history.append((epoch, int(i), w.copy(), b, mistakes))
        mistakes_per_epoch.append(mistakes)
        if mistakes == 0:
            break  # a full pass with no corrections: the data is separated
    if record:
        return w, b, history, mistakes_per_epoch
    return w, b, mistakes_per_epoch

La estructura carga las dos ideas que importan. Cada época baraja los puntos con un generador con semilla para que la corrida sea repetible pero el orden no sea degenerado. Y la condición de paro es la verificación de convergencia hecha literal: cuenta las correcciones de la pasada y, si no hubo ninguna, todos los puntos están del lado correcto — los datos quedaron separados y no queda nada por hacer, así que break. Con XOR esa condición nunca se cumple, y por eso existe max_epochs como acto de piedad. La bandera record guarda cada corrección para poder repetir el aprendizaje frame por frame, que es la siguiente sección.

Míralo funcionar

Este es el perceptrón aprendiendo, sobre el conjunto separable para que cada punto sea visible. El panel de arriba son los datos; la línea naranja es la frontera de decisión actual, el lugar donde el score vale cero. Un anillo rojo marca el punto que disparó la corrección en ese frame — el que la unidad acaba de fallar y está arreglando. El panel de abajo cuenta los errores de cada época; la barra de la pasada en la que vamos crece conforme ocurren las correcciones.

Dale play. Cada frame es una actualización real salida de fit — los pesos exactos que produjo el código, sin suavizado ni reordenamiento. Reinícialo y córrelo las veces que quieras; es la misma secuencia siempre.

Fíjate en el arranque. Los pesos empiezan en cero, así que no hay línea — el score es cero en todas partes, la unidad dispara +1+1 para todo, y cada punto negativo es un error esperando corrección. La primera actualización crea una frontera de la nada, burda y mal colocada. Luego las correcciones empiezan a moverla. Cada anillo rojo es un punto del lado equivocado, y cada actualización gira y desliza la línea para recoger ese punto, a veces sacando otro punto de su lugar en el proceso. La línea da tumbos en vez de deslizarse, que es el carácter honesto de esta regla — arregla un punto a la vez y no ve hacia adelante.

El panel de abajo cuenta la misma historia como una cuenta regresiva. Doce errores en la primera pasada, luego cinco, luego seis, luego tres, luego cero. Nota que no es monótono — la tercera pasada comete más errores que la segunda. Eso es real, y está bien; el perceptrón no garantiza que una época dada mejore sobre la anterior, solo que con datos separables los errores terminan deteniéndose. Y así pasa: todo converge después de 5 pasadas y 26 correcciones en total. La última pasada es la recompensa — un barrido limpio, sin anillos, sin actualizaciones, cada punto de su lado correcto. Esa pasada limpia es el algoritmo demostrándose a sí mismo que ya terminó.

Lo único que te tienes que llevar de esta animación: aquí nada minimizó una pérdida. No hubo gradiente, no hubo cuesta abajo. Hubo una línea reaccionando a sus propios errores hasta que se le acabaron. Es una idea de aprendizaje distinta a la de los últimos capítulos, y es la más vieja de las dos.

Ahora pon los dos datasets lado a lado y cuenta los errores por pasada. Este es el capítulo entero en una gráfica. El conjunto separable cae a cero y se detiene — la línea termina donde entró la convergencia y el loop hizo break. XOR nunca llega; el conteo rebota pasada tras pasada, sin barrido limpio, sin final, porque no hay línea que encontrar.

Ese es el teorema de convergencia y su letra chiquita en una sola imagen. Datos separables: los errores llegan a cero en un puñado de pasadas y el algoritmo se detiene porque, demostrablemente, ya terminó. Datos no separables: la misma regla, el mismo código, dando vueltas para siempre sin manera de decirle que pare salvo un tope duro de épocas. El perceptrón no puede distinguir entre "sigo trabajando" y "nunca voy a terminar", y esa incapacidad es la cara práctica de todo lo que Minsky y Papert demostraron.

La implementación completa

El archivo entero, de arriba a abajo — entrada neta, escalón, actualización, el loop de entrenamiento y el helper de accuracy que usan los traces. Este es el código que la animación corrió de verdad:

"""The perceptron, built from scratch.

A single linear threshold unit — the original artificial neuron. Take a
weighted sum of the inputs plus a bias, and fire +1 or -1 depending on its
sign. Learning is the classic online perceptron rule: sweep the data one
point at a time, and every time the unit gets a point wrong, nudge the
weights toward the right answer. Repeat until a whole pass makes no
mistakes. On linearly separable data that pass is guaranteed to arrive
(the perceptron convergence theorem); on data that isn't, it never does.

Labels live in {-1, +1}, which is what makes the update rule so tidy.

Pure NumPy. Every function below appears in the chapter one step at a time;
the `# region:` markers are what the book's include directives pull in.
"""

import numpy as np


# region: net_input
def net_input(x, w, b):
    """The weighted sum plus bias for one sample: z = w·x + b.

    x and w are length-D vectors, b is a scalar. This is the whole linear
    part of the neuron — everything else is just deciding what to do with
    its sign.
    """
    return float(np.dot(w, x) + b)
# endregion


# region: step
def step(z):
    """Step activation: fire +1 when the net input is non-negative, else -1.

    This hard threshold is what makes the perceptron a classifier rather
    than a regressor, and also what makes it non-differentiable — the
    reason later networks swap it out for a smooth activation.
    """
    return 1 if z >= 0.0 else -1


def predict(x, w, b):
    """Classify one sample by the sign of its net input."""
    return step(net_input(x, w, b))
# endregion


# region: update
def update(x, y, w, b, lr):
    """The perceptron correction for one misclassified sample.

    Push the boundary toward getting this point right: add the point to the
    weights if its true label is +1, subtract it if -1. Because y is ±1 the
    two cases fold into one line.

        w ← w + lr · y · x
        b ← b + lr · y

    Returns the updated (w, b). This is the entire learning rule.
    """
    w = w + lr * y * x
    b = b + lr * y
    return w, b
# endregion


# region: fit
def fit(X, y, lr=1.0, max_epochs=50, seed=0, record=False):
    """Online perceptron learning: sweep the data, correct on every mistake.

    Start the weights at zero. One epoch is a full pass over the samples in
    a seeded random order. For each sample, if the unit's prediction
    disagrees with the truth, apply the perceptron update. Count the
    mistakes in the pass; if a whole epoch goes by with zero of them, the
    data is separated and we stop — that clean pass is the convergence
    check. With `record=True` we also return the per-update history and the
    mistakes-per-epoch list so the chapter can replay the learning.
    """
    rng = np.random.default_rng(seed)
    N, D = X.shape
    w = np.zeros(D)
    b = 0.0
    history = []
    mistakes_per_epoch = []
    for epoch in range(max_epochs):
        order = rng.permutation(N)
        mistakes = 0
        for i in order:
            xi, yi = X[i], y[i]
            if predict(xi, w, b) != yi:
                w, b = update(xi, yi, w, b, lr)
                mistakes += 1
                if record:
                    history.append((epoch, int(i), w.copy(), b, mistakes))
        mistakes_per_epoch.append(mistakes)
        if mistakes == 0:
            break  # a full pass with no corrections: the data is separated
    if record:
        return w, b, history, mistakes_per_epoch
    return w, b, mistakes_per_epoch
# endregion


def accuracy(X, y, w, b):
    """Fraction of samples the unit labels correctly."""
    preds = np.array([predict(x, w, b) for x in X])
    return float((preds == y).mean())

La versión de librería

No lo escribirías a mano, y scikit-learn lo tiene como Perceptron. Es la misma unidad y la misma regla; las diferencias son de ergonomía. Recibe tus etiquetas en la forma que sea y maneja la convención de signos por dentro, baraja en cada época y envuelve un par de perillas que la regla clásica no tiene — una tolerancia de early stopping, una penalización de regularización opcional, una learning rate ajustable. Apaga la tolerancia y corre la regla simple guiada por errores hasta la convergencia, que es lo que queremos para una comparación honesta:

def sklearn_perceptron(Xtr, ytr, Xte, yte, seed=0):
    """Fit sklearn's Perceptron and report its test accuracy.

    tol=None disables the early-stopping heuristic so it runs the classic
    rule to convergence, matching our from-scratch loop. Returns the
    learned weight vector, bias, test accuracy, and epochs taken (n_iter_).
    """
    clf = Perceptron(tol=None, max_iter=1000, shuffle=True, random_state=seed)
    clf.fit(Xtr, ytr)
    w = clf.coef_[0]
    b = float(clf.intercept_[0])
    acc = float(clf.score(Xte, yte))
    return w, b, acc, int(clf.n_iter_)

Lo único que vale la pena saber es que Perceptron es una fachada delgada sobre la misma maquinaria de SGDClassifier — es literalmente descenso de gradiente estocástico sobre una pérdida particular (el criterio del perceptrón, parecido a hinge) con la activación escalón. Y eso es un puente bonito: la regla reactiva y guiada por errores que escribimos a mano también se puede expresar como descenso sobre una pérdida, y esa segunda visión es la que escala a redes. El mismo algoritmo, dos maneras de verlo.

Desde cero contra librería

El duelo aquí no es por un decimal de accuracy, es por una capacidad. La promesa del perceptrón es que separa los datos que puede separar, así que ajusté los dos modelos sobre cada dataset completo y pregunté si le atinaron. En el conjunto separable, los dos deberían llegar al 100%; en XOR, los dos deberían fallar, y esa falla es justo el punto.

Con los datos linealmente separables nuestro perceptrón desde cero saca 1.0 y el de sklearn saca 1.0 — los dos encuentran un separador perfecto, que es el teorema de convergencia cobrado en forma de número. No encuentran el mismo separador; nuestros pesos salieron alrededor de [5.7,8.0][5.7, 8.0] con un sesgo de 12-12, los de sklearn alrededor de [3.3,3.8][3.3, 3.8] con un sesgo de 6-6, rectas distintas por el mismo hueco. Ahí se ve la indiferencia del perceptrón: cualquier separador lo satisface, así que dos corridas caen en dos fronteras distintas y las dos son, según el estándar del propio modelo, perfectas. También es la razón exacta para preferir un método de margen máximo cuando la generalización importa — el perceptrón se detiene en la primera recta que funciona, no en la mejor.

Con XOR se le caen las ruedas, como tenía que pasar. Desde cero saca 0.525, sklearn saca 0.5125 — volados, los dos, en un problema balanceado de dos clases. Ninguna implementación está rota; una recta genuinamente no puede hacerlo mejor aquí, así que la mejor ingeniería de sklearn no compra nada. Si quieres la única gráfica que justifica todo el resto de un curso de deep learning, es esta: dos barras idénticas al nivel del azar en un problema que un niño resuelve dibujando dos rectas en vez de una.

Voy a marcar la advertencia honesta. Estas son accuracies sobre el conjunto de ajuste, no sobre datos apartados — estoy midiendo si cada modelo puede separar los datos con los que entrenó, que es la pregunta correcta para una demostración de capacidad y la equivocada para generalización. En el conjunto separable, un split apartado caería por debajo de 1.0, precisamente porque el perceptrón escoge un separador arbitrario que puede quedar pegado a una clase. Esa brecha entre "encontré una recta" y "encontré una buena recta" es real, y es buena parte de la razón por la que el campo siguió avanzando.

Conclusiones

El perceptrón es donde arrancan las redes neuronales, y esa es la razón para conocerlo al derecho y al revés aunque nunca vayas a poner uno en producción. Reduce una red moderna a una sola unidad y esto es lo que queda: una suma ponderada, una activación y una regla que corrige los pesos a partir de errores. Todo lo que vino después es esa idea escalada — más unidades, más capas, una activación más suave, una actualización más lista — pero el esqueleto es el de Rosenblatt, y leer una derivación de backprop es mucho más fácil una vez que viste esta recta moverse sobre un scatter.

Tres cosas para llevarte. Primera, el teorema de convergencia es una garantía de verdad, y las garantías de verdad escasean en este campo — pero está garantizado para un mundo angosto, datos linealmente separables, y no dice nada sobre la calidad de la frontera ni sobre generalización. Una garantía con letra chiquita sigue valiendo la pena, siempre que leas la letra chiquita. Segunda, la limitación es la lección. El perceptrón solo puede trazar una recta, XOR no es un problema de una recta, y ninguna cantidad de entrenamiento arregla eso porque es un muro de representación, no de esfuerzo. Cuando un modelo no puede expresar la respuesta, cambias el modelo, no el optimizador. Tercera, las dos cosas que lo frenan apuntan directo al arreglo. La activación escalón dura tiene una derivada plana e inútil, así que no puedes encadenar la regla a través de unidades apiladas, y una sola recta no se dobla. Cambia el escalón por una activación diferenciable y apila las unidades en capas, y los dos problemas caen al mismo tiempo — la frontera puede curvarse y los errores pueden fluir hacia atrás por la pila para entrenar cada capa. Eso es el perceptrón multicapa, y es el siguiente capítulo. XOR es donde termina este y donde empieza aquel.