Ir al contenido
wannacode

El entrenamiento de principio a fin

En resumen: Entrenar una red neuronal es un ciclo sobre épocas: mezclar, lotes, una pasada hacia adelante, retropropagación y un paso, hasta que el error de validación deja de bajar.

Problema

La red dice −0,23 días

La página de cobro debería decir «llega en 2 días». El tiempo de entrega depende de la distancia, de si el pedido se hace después de las 16:00 (entonces sale al día siguiente) y un poco del peso. Una recta de la regresión lineal no sigue esta curva: en la ciudad es más o menos un día, hasta 200 km crece rápido, luego cada vez más despacio.

La red de la lección de retropropagación puede seguir una curva. Aquí está, con 4 neuronas ocultas y pesos aleatorios, antes de entrenar:

Rng rng = new Rng(42);
Net net = new Net(4, 3);
net.randomize(rng, 1.0);
System.out.printf("error (MSE): %.4f%n", net.loss(trainX, trainY));
for (int k : new int[] {0, 12, 23})
System.out.printf("%.0f km: la red dice %.2f, real %.1f días%n",
TRAIN[k][0], net.predict(trainX[k]), trainY[k]);
Salida
error (MSE): 7.1855
5 km: la red dice -0.23, real 1.0 días
150 km: la red dice -0.17, real 2.3 días
520 km: la red dice 0.18, real 3.0 días

Un pedido a 5 km llega en −0,23 días. En la lección de retropropagación viste un paso para un ejemplo. ¿Cuántos pasos necesitas, con qué ejemplos, en qué orden, y cuándo paras?

La idea

El ciclo de entrenamiento

Entrenar es un ciclo que sigue girando hasta que la red deja de mejorar:

  1. Pesos iniciales: números aleatorios pequeños de un generador con semilla.
  2. Época: una pasada por todos los ejemplos de entrenamiento. Antes de cada época los ejemplos se mezclan, para que la red no aprenda su orden.
  3. Lote: unos cuantos ejemplos a la vez. Cada uno recibe una pasada hacia adelante y un error, luego una pasada hacia atrás. Las pendientes se promedian y la red da un paso.
  4. Comprobación: cada 100 épocas el programa imprime el error en el entrenamiento y en la validación.

Este es el ciclo de entrenamiento. Los números que imprime, dibujados como un gráfico, son la curva del error (loss curve). Con ella puedes saber si la red está aprendiendo, si la tasa es buena y cuándo parar.

Los datos son 32 pedidos de la tienda: 24 para entrenar y 8 para la validación. Con los pedidos de validación la red nunca da un paso. Son la medida para pedidos nuevos.

Míralo

Los datos y la red

Los pedidos, escritos en el código:

// pedido: km, después de las 16:00 (1 o 0), kg, días hasta la entrega
static final double[][] TRAIN = {
{5, 0, 1.2, 1.0}, {12, 1, 3.5, 1.6}, {18, 0, 0.8, 1.0},
{25, 0, 6.0, 1.2}, {28, 1, 2.1, 1.8}, {35, 0, 4.4, 1.3},
{48, 1, 0.5, 2.2}, {60, 0, 9.0, 2.5}, {75, 0, 1.7, 1.6},
{90, 1, 2.9, 2.6}, {110, 0, 5.5, 1.9}, {130, 1, 0.9, 2.7},
{150, 0, 3.1, 2.3}, {170, 0, 8.2, 3.3}, {185, 1, 1.4, 3.1},
{195, 0, 2.6, 2.3}, {210, 0, 4.8, 2.6}, {240, 1, 0.6, 3.3},
{280, 0, 8.6, 3.6}, {320, 0, 1.9, 2.7}, {360, 1, 3.3, 3.6},
{410, 0, 5.0, 2.8}, {460, 1, 2.2, 3.6}, {520, 0, 0.7, 3.0}
};
// validación: pedidos que la red no ve durante el entrenamiento
static final double[][] VALID = {
{8, 1, 2.4, 1.6}, {22, 0, 8.8, 1.0}, {40, 0, 1.1, 1.4},
{95, 1, 7.9, 2.5}, {160, 0, 2.0, 2.3}, {205, 1, 3.9, 3.1},
{300, 0, 9.4, 2.6}, {480, 1, 4.6, 3.6}
};

Las características se llevan a escalas parecidas: kilómetros en cientos, peso en decenas de kilogramos. Si no, 520 km taparían todo lo demás:

// características en escalas parecidas: cientos de km, 0 o 1, decenas de kg
static double[][] inputs(double[][] orders) {
double[][] xs = new double[orders.length][];
for (int k = 0; k < orders.length; k++)
xs[k] = new double[] {orders[k][0] / 100, orders[k][1], orders[k][2] / 10};
return xs;
}
static double[] targets(double[][] orders) {
double[] ys = new double[orders.length];
for (int k = 0; k < orders.length; k++)
ys[k] = orders[k][3];
return ys;
}

El azar viene de nuestro propio generador con semilla. Una misma semilla da los mismos números en cualquier computadora y en los cuatro lenguajes, así que la lección imprime lo mismo en todas partes. El número 48271 y la división por 2³¹ − 1 son un generador antiguo y bien probado (Park–Miller). El producto se queda por debajo de 2⁵³, así que es exacto incluso en JavaScript:

class Rng {
private long state;
Rng(long seed) {
state = seed;
}
// el siguiente número entre 0 y 1; una misma semilla da los mismos números
double next() {
state = state * 48271 % 2147483647;
return (double) state / 2147483647;
}
}
new Random(42) en Java también tiene semilla. Aquí el generador es el nuestro, así que es el mismo que en los otros tres lenguajes.

La red es la de la lección de retropropagación, metida en una clase: 3 entradas, 4 neuronas ocultas con sigmoide y una salida sin activación, porque los días no son una probabilidad. step hace una pasada hacia adelante y otra hacia atrás por cada pedido del lote, suma las pendientes y mueve cada peso una vez, según la pendiente media:

class Net {
final double[][] w1; // entrada → capa oculta
final double[] b1;
final double[] w2; // capa oculta → salida
double b2;
final double[] h; // las salidas de la capa oculta del último predict
Net(int hidden, int inputs) {
w1 = new double[hidden][inputs];
b1 = new double[hidden];
w2 = new double[hidden];
h = new double[hidden];
}
// pesos aleatorios de -scale a scale; los sesgos siguen en 0
void randomize(Rng rng, double scale) {
for (double[] row : w1)
for (int i = 0; i < row.length; i++)
row[i] = (rng.next() * 2 - 1) * scale;
for (int j = 0; j < w2.length; j++)
w2[j] = (rng.next() * 2 - 1) * scale;
}
static double sigmoid(double z) {
return 1 / (1 + Math.exp(-z));
}
double predict(double[] x) {
double y = b2;
for (int j = 0; j < w2.length; j++) {
double z = b1[j];
for (int i = 0; i < x.length; i++)
z += w1[j][i] * x[i];
h[j] = sigmoid(z);
y += w2[j] * h[j];
}
return y;
}
double loss(double[][] xs, double[] ys) {
double total = 0;
for (int k = 0; k < xs.length; k++) {
double e = predict(xs[k]) - ys[k];
total += e * e;
}
return total / xs.length;
}
// un paso: la pendiente media sobre los ejemplos del lote
void step(double[][] xs, double[] ys, int[] batch, double rate) {
int hidden = w2.length, inputs = xs[0].length;
double[][] gw1 = new double[hidden][inputs];
double[] gb1 = new double[hidden], gw2 = new double[hidden];
double gb2 = 0;
for (int k : batch) {
double d = 2 * (predict(xs[k]) - ys[k]);
gb2 += d;
for (int j = 0; j < hidden; j++) {
gw2[j] += d * h[j];
double dz = d * w2[j] * h[j] * (1 - h[j]);
gb1[j] += dz;
for (int i = 0; i < inputs; i++)
gw1[j][i] += dz * xs[k][i];
}
}
int n = batch.length;
b2 -= rate * gb2 / n;
for (int j = 0; j < hidden; j++) {
w2[j] -= rate * gw2[j] / n;
b1[j] -= rate * gb1[j] / n;
for (int i = 0; i < inputs; i++)
w1[j][i] -= rate * gw1[j][i] / n;
}
}
}

La mezcla es Fisher–Yates: de atrás hacia adelante cada lugar se intercambia con un lugar al azar anterior a él.

// Fisher–Yates: todos los órdenes son igual de probables
static void shuffle(int[] order, Rng rng) {
for (int i = order.length - 1; i > 0; i--) {
int k = (int) (rng.next() * (i + 1));
int tmp = order[i];
order[i] = order[k];
order[k] = tmp;
}
}

Míralo

El ciclo y la curva del error

Rng rng = new Rng(42);
Net net = new Net(4, 3);
net.randomize(rng, 1.0);
int size = 8;
double rate = 0.05;
int[] order = IntStream.range(0, trainX.length).toArray();
System.out.println("época entrenamiento validación");
for (int epoch = 1; epoch <= 1000; epoch++) {
shuffle(order, rng);
for (int start = 0; start < order.length; start += size) {
int end = Math.min(start + size, order.length);
int[] batch = Arrays.copyOfRange(order, start, end);
net.step(trainX, trainY, batch, rate);
}
if (epoch % 100 == 0) {
double t = net.loss(trainX, trainY);
double v = net.loss(validX, validY);
System.out.printf("%5d%15.4f%12.4f%n", epoch, t, v);
}
}
Paso a paso, en palabras
  1. Semilla 42: cada ejecución empieza con los mismos pesos aleatorios. Antes del entrenamiento el error es 7,19, muy por encima del gráfico.
  2. Una época es una pasada por los 24 pedidos de entrenamiento. Primero se mezclan, con el mismo generador.
  3. Un lote de 8 pedidos: hacia adelante, error, hacia atrás, un paso. Tres pasos por época, 3000 en todo el entrenamiento.
  4. Época 100: entrenamiento 0,1365, validación 0,0756. Con los pedidos de validación nunca se dio un paso.
  5. La validación es más baja en la época 200: 0,0705. Después sube, mientras el entrenamiento sigue bajando.
  6. Época 1000: entrenamiento 0,0381, validación 0,2371. La red se aprende de memoria tres paquetes pesados que llegaron tarde por casualidad.
  7. La mejor red está cerca de la época 200, no de la 1000. Se para cuando la validación deja de bajar.

El ciclo completo: 1000 épocas, lotes de 8, tasa 0,05:

Rng rng = new Rng(42);
Net net = new Net(4, 3);
net.randomize(rng, 1.0);
int size = 8;
double rate = 0.05;
int[] order = IntStream.range(0, trainX.length).toArray();
System.out.println("época entrenamiento validación");
for (int epoch = 1; epoch <= 1000; epoch++) {
shuffle(order, rng);
for (int start = 0; start < order.length; start += size) {
int end = Math.min(start + size, order.length);
int[] batch = Arrays.copyOfRange(order, start, end);
net.step(trainX, trainY, batch, rate);
}
if (epoch % 100 == 0) {
double t = net.loss(trainX, trainY);
double v = net.loss(validX, validY);
System.out.printf("%5d%15.4f%12.4f%n", epoch, t, v);
}
}
Salida
época entrenamiento validación
100 0.1365 0.0756
200 0.1041 0.0705
300 0.0826 0.0776
400 0.0667 0.1048
500 0.0556 0.1350
600 0.0486 0.1718
700 0.0443 0.1967
800 0.0417 0.2202
900 0.0396 0.2243
1000 0.0381 0.2371

El entrenamiento baja de 7,19 a 0,0381. La validación baja hasta la época 200 y luego sube: en la época 1000 es 0,2371, más de tres veces peor que la mejor. La red sigue ajustándose a los pedidos de entrenamiento, incluido lo que hay en ellos de casualidad. Volveremos a eso al final de la lección.

Pruébalo

Predice la salida

Si el lote es de 10 y hay 24 ejemplos, ¿cuántos pasos da el entrenamiento en 1000 épocas?

int examples = 24, size = 10, steps = 0;
for (int epoch = 1; epoch <= 1000; epoch++)
for (int start = 0; start < examples; start += size)
steps++;
System.out.println("pasos: " + steps);
Respuesta

La idea

Lotes: todos, uno por uno o 8

¿Cuántos ejemplos deben entrar en un paso? Aquí está la misma red durante 40 épocas con un lote de 24 (todos a la vez), 8 y 1. Después de cada paso el programa mide el error sobre los 24 pedidos y cuenta cuántas veces el paso lo subió:

for (int size : new int[] {24, 8, 1}) {
Rng rng = new Rng(42);
Net net = new Net(4, 3);
net.randomize(rng, 1.0);
int[] order = IntStream.range(0, trainX.length).toArray();
int steps = 0, up = 0;
double before = net.loss(trainX, trainY);
for (int epoch = 1; epoch <= 40; epoch++) {
shuffle(order, rng);
for (int start = 0; start < order.length; start += size) {
int end = Math.min(start + size, order.length);
net.step(trainX, trainY, Arrays.copyOfRange(order, start, end), 0.05);
double after = net.loss(trainX, trainY);
steps++;
if (after > before) up++;
before = after;
}
}
System.out.printf("lote %2d: %3d pasos, %3d arriba, error %.4f%n",
size, steps, up, before);
}
Salida
lote 24: 40 pasos, 0 arriba, error 0.2913
lote 8: 120 pasos, 21 arriba, error 0.1840
lote 1: 960 pasos, 495 arriba, error 0.1238
  • Todos a la vez: la pendiente es la pendiente exacta de todo el error, así que ningún paso lo sube. Pero hay un solo paso por época, y tras 40 épocas la red es la que está más lejos.
  • Un ejemplo por vez: 24 pasos por época, así que en 40 épocas llega más abajo. Pero la pendiente de un ejemplo es una estimación burda de la pendiente de todos: 495 de 960 pasos suben el error.
  • Un lote de 8: tres pasos por época y solo 21 de 120 hacia arriba.

Esto se llama minilote (mini-batch): un paso según la pendiente media de unos cuantos ejemplos. El lote es el compromiso que usa casi todo el mundo. En una tarjeta gráfica los ejemplos de un lote se calculan a la vez, así que en proyectos reales los lotes son de decenas o cientos de ejemplos.

Pruébalo

Completa

Las pendientes del lote se suman, y el paso va según la pendiente media. ¿Entre qué divides?

int n = ;
b2 -= rate * gb2 / n;

La idea

Una mala tasa, un mal comienzo

La curva te dice qué anda mal. Las tres tasas de la lección del descenso del gradiente, pero en una red. El error en las épocas 50, 100 y 200:

for (double rate : new double[] {0.005, 0.05, 2.0}) {
Rng rng = new Rng(42);
Net net = new Net(4, 3);
net.randomize(rng, 1.0);
int[] order = IntStream.range(0, trainX.length).toArray();
System.out.printf("tasa %.3f:", rate);
for (int epoch = 1; epoch <= 200; epoch++) {
trainEpoch(net, trainX, trainY, order, 8, rate, rng);
double loss = net.loss(trainX, trainY);
if (Double.isNaN(loss) || loss > 1e6) {
System.out.print(" se disparó en la época " + epoch);
break;
}
if (epoch == 50 || epoch == 100 || epoch == 200)
System.out.printf(" %.4f", loss);
}
System.out.println();
}
Salida
tasa 0.005: 0.3892 0.3201 0.2472
tasa 0.050: 0.1698 0.1365 0.1041
tasa 2.000: se disparó en la época 2

Con 0,005 la red aprende, pero despacio: en la época 200 (0,2472) está peor que 0,05 en la época 50 (0,1698). La curva baja con suavidad y no deja de bajar, lo que significa que puedes subir la tasa. Con 2 cada paso se pasa del fondo y la siguiente pendiente es aún más empinada. El error supera el millón ya en la segunda época, y después llegan infinito y NaN. Por eso el programa lo comprueba y se detiene.

El comienzo también importa:

Rng rng = new Rng(42);
Net zeros = new Net(4, 3); // sin randomize: todos los pesos son 0
int[] order = IntStream.range(0, trainX.length).toArray();
for (int epoch = 1; epoch <= 200; epoch++)
trainEpoch(zeros, trainX, trainY, order, 8, 0.05, rng);
System.out.printf("ceros: error %.4f%n", zeros.loss(trainX, trainY));
for (int j = 0; j < zeros.w1.length; j++)
System.out.printf(" neurona %d: %.2f %.2f %.2f%n", j + 1,
zeros.w1[j][0], zeros.w1[j][1], zeros.w1[j][2]);
for (double scale : new double[] {8, 1}) {
rng = new Rng(42);
Net net = new Net(4, 3);
net.randomize(rng, scale);
double first = net.loss(trainX, trainY);
order = IntStream.range(0, trainX.length).toArray();
for (int epoch = 1; epoch <= 200; epoch++)
trainEpoch(net, trainX, trainY, order, 8, 0.05, rng);
System.out.printf("pesos ×%.0f: al principio %.4f, tras 200 épocas %.4f%n",
scale, first, net.loss(trainX, trainY));
}
Salida
ceros: error 0.1127
neurona 1: 0.99 0.47 0.19
neurona 2: 0.99 0.47 0.19
neurona 3: 0.99 0.47 0.19
neurona 4: 0.99 0.47 0.19
pesos ×8: al principio 33.9817, tras 200 épocas 0.2971
pesos ×1: al principio 7.1855, tras 200 épocas 0.1041
  • Ceros. Las cuatro neuronas ocultas empiezan idénticas, reciben pendientes idénticas y dan pasos idénticos. Tras 200 épocas sus pesos son iguales hasta el último dígito: la red tiene una neurona oculta, repetida cuatro veces. Los pesos iniciales aleatorios existen para que las neuronas se vuelvan distintas.
  • Pesos ×8. Las sumas en la sigmoide son grandes, así que para la mayoría de los pedidos vale casi 0 o casi 1, donde es plana. Una curva plana significa una pendiente casi nula y pasos diminutos: tras 200 épocas el error es casi tres veces mayor que con pesos ×1.

Pruébalo

Encuentra el error

Un colega escribe el ciclo a su manera y mezcla él mismo los arreglos en lugar de los índices. El error se queda por encima de 0,6 y no baja, por muchas épocas que ejecute. ¿En qué línea está el error?

Elige la línea con el error.

La idea

Cuándo parar

¿Qué se aprende exactamente de memoria la red? Entre los pedidos de entrenamiento hay tres paquetes pesados (9,0 kg, 8,2 kg y 8,6 kg) que llegaron un día entero tarde por casualidad. Aquí están los tres paquetes pesados de la validación, que no llegaron tarde:

Rng rng = new Rng(42);
Net net = new Net(4, 3);
net.randomize(rng, 1.0);
int[] order = IntStream.range(0, trainX.length).toArray();
for (int epoch = 1; epoch <= 1000; epoch++) {
trainEpoch(net, trainX, trainY, order, 8, 0.05, rng);
if (epoch == 200 || epoch == 1000) {
System.out.println("época " + epoch + ":");
for (int k : new int[] {1, 3, 6}) // los paquetes pesados de la validación
System.out.printf(" %.0f km, %.1f kg: %.2f (real %.1f)%n",
VALID[k][0], VALID[k][2], net.predict(validX[k]), validY[k]);
}
}
Salida
época 200:
22 km, 8.8 kg: 1.31 (real 1.0)
95 km, 7.9 kg: 2.53 (real 2.5)
300 km, 9.4 kg: 3.13 (real 2.6)
época 1000:
22 km, 8.8 kg: 1.84 (real 1.0)
95 km, 7.9 kg: 3.09 (real 2.5)
300 km, 9.4 kg: 3.38 (real 2.6)

En la época 200 las predicciones están cerca de la realidad. En la época 1000 la red ha aprendido «lo pesado viaja más lento»: el paquete a 22 km recibe 1,84 días en lugar de 1,0. Una regla que en los datos es solo casualidad es sobreajuste: el entrenamiento mejora, y los pedidos nuevos empeoran.

Por eso paras cuando la validación deja de bajar. Esto es la parada temprana (early stopping). El programa comprueba la validación cada 10 épocas y guarda una instantánea (una copia) de la mejor red. Si no hay mejora durante 100 épocas, se detiene y devuelve la instantánea:

// una copia de los pesos: una instantánea de la red en este momento
Net copy() {
Net c = new Net(w2.length, w1[0].length);
for (int j = 0; j < w2.length; j++) {
c.w1[j] = w1[j].clone();
c.b1[j] = b1[j];
c.w2[j] = w2[j];
}
c.b2 = b2;
return c;
}
Rng rng = new Rng(42);
Net net = new Net(4, 3);
net.randomize(rng, 1.0);
int[] order = IntStream.range(0, trainX.length).toArray();
// revisar cada 10 épocas; parar tras 100 sin mejora
double best = Double.MAX_VALUE;
int bestEpoch = 0, epoch = 0;
Net bestNet = net.copy();
while (epoch < 1000 && epoch - bestEpoch < 100) {
epoch++;
trainEpoch(net, trainX, trainY, order, 8, 0.05, rng);
if (epoch % 10 == 0) {
double valid = net.loss(validX, validY);
if (valid < best) {
best = valid;
bestEpoch = epoch;
bestNet = net.copy();
}
}
}
System.out.printf("validación más baja: %.4f en la época %d%n", best, bestEpoch);
System.out.println("me detuve en la época " + epoch);
System.out.printf("conservo la red de la época %d: %.4f%n", bestEpoch, bestNet.loss(validX, validY));
Salida
validación más baja: 0.0667 en la época 230
me detuve en la época 330
conservo la red de la época 230: 0.0667

La mejor validación es 0,0667 en la época 230, mejor que cualquier punto de la curva de arriba, porque la comprobación es más frecuente. El entrenamiento se detiene en la época 330 en lugar de la 1000, y la red conservada es la de la época 230.

La validación decidió cuándo parar, así que ya no es una medida honesta para pedidos nuevos. La nota final se toma sobre una tercera parte de los datos, el conjunto de pruebas, que no participó en ninguna decisión.

La idea

Cuándo perjudica

  • Una red para todo. 32 pedidos y tres características son pocos datos: una recta con unas cuantas características o una fórmula más simple suele bastar, es más fácil de explicar y no aprende de memoria. Una red compensa cuando las reglas son demasiadas para escribirlas y hay suficientes ejemplos.
  • Una sola ejecución. Otra semilla da otros pesos iniciales y otra curva. Una ejecución puede ser suerte. Compara los cambios con varias semillas y mira el promedio.
  • Un conjunto de validación pequeño. Ocho pedidos son una medida ruidosa: un pedido raro mueve la curva. Cuantos menos datos tienes, con más cuidado lees un solo punto.
  • La parada temprana no arregla los datos. Los tres retrasos por casualidad siguen ahí. Es mejor encontrarlos y quitarlos o corregirlos, y si el peso de verdad no influye, no darlo como característica.
  • El costo. Aquí el entrenamiento tarda menos de un segundo. Las redes reales entrenan durante horas o semanas en muchas tarjetas gráficas. Los clústeres de entrenamiento y poner un modelo en producción son infraestructura y se enseñan en el curso de DevOps ZeroToOps.
Más precisamente, para curiosos

El paso como fórmula. El lote tiene B ejemplos, y η es la tasa. Para cada peso w:

w ← w − η · (1/B) · Σ ∂L/∂w

En palabras: suma la pendiente del error respecto a w para cada ejemplo del lote, divide entre B, multiplica por la tasa y resta. ∂L/∂w es la pendiente que calcula la retropropagación para un peso. Cuando B es todos los ejemplos, es el descenso del gradiente corriente. Con B = 1 se llama estocástico (SGD), y con lotes es SGD por minilotes.

Por qué 2 · (y − t). El error de un ejemplo es (y − t)². Su pendiente respecto a y es 2 · (y − t): si la salida está 0,5 por encima de la realidad, la pendiente es 1, y si está 0,5 por debajo, es −1. Ahí empieza la retropropagación en step.

Las herramientas reales. La práctica del aprendizaje automático es sobre todo en Python. En PyTorch el mismo ciclo es: DataLoader(..., batch_size=8, shuffle=True) da los lotes, optimizer.zero_grad() reinicia las pendientes (nuestros arreglos nuevos al inicio del paso), loss.backward() es la retropropagación, y optimizer.step() mueve los pesos. La semilla es torch.manual_seed(42). En C# lo mismo lo hacen ML.NET o TorchSharp, en Java DJL o Tribuo, y en JavaScript TensorFlow.js, mientras que ONNX Runtime ejecuta un modelo ya entrenado. El ciclo es el mismo, solo que las matrices se calculan en una tarjeta gráfica.

Pasos más inteligentes. Casi nadie usa el paso básico de la lección. El momentum recuerda la dirección de los últimos pasos y atraviesa con más suavidad el ruido de los lotes. Adam lleva una tasa propia para cada peso. A menudo la tasa se baja con el tiempo (un learning rate schedule): pasos grandes al principio, pequeños al final.

Los pesos iniciales. Aquí los pesos aleatorios van de −1 a 1. En redes más grandes el rango se calcula a partir del número de entradas de la neurona, por ejemplo ±1/√n (Xavier, He), para que las sumas no sean demasiado grandes ya en la primera capa.

Los mismos números en cuatro lenguajes. Las sumas y multiplicaciones van aquí en el mismo orden, así que el resultado es idéntico bit a bit. No se promete que exp sea idéntica hasta el último bit en cada lenguaje, por eso los programas imprimen 4 decimales. La salida está comprobada y es la misma en Java, C#, Python y JavaScript.

System.out.printf escribe el número según el Locale por defecto: en una computadora con configuración regional española verás 0,1365. Con Locale.ROOT como primer argumento siempre lleva punto.

Ejercicio

Parada temprana con tres tasas

Resuelve la tarea en tu propio editor.

Toma la parada temprana de la lección y ejecútala para las tasas 0,02, 0,05 y 0,2, cada vez con una red nueva de la semilla 42. Para cada tasa imprime la validación más baja, su época y la época en que se detiene el entrenamiento. No hace falta guardar una copia de la red.

Salida
tasa 0.02: validación más baja 0.0683 en la época 470, me detuve en la época 570
tasa 0.05: validación más baja 0.0667 en la época 230, me detuve en la época 330
tasa 0.20: validación más baja 0.0628 en la época 50, me detuve en la época 150

Con 0,2 la red alcanza su validación más baja en 50 épocas y se detiene en la 150. Con 0,02 necesita 470. La tasa mayor llega antes al fondo, pero también empieza a subir antes. Por eso la validación se comprueba a menudo, no solo al final.

Más tareas

  • Seis neuronas. Ejecuta el ciclo principal con 6 neuronas ocultas. ¿Sube la validación antes?
  • Sin el peso. Quita el peso de las características (2 entradas en lugar de 3) y ejecuta el ciclo principal. ¿Qué pasa con la validación en la época 1000?

Test

Ponte a prueba

1. ¿Qué es una época?
2. El error de entrenamiento baja todo el tiempo, mientras que la validación es más baja en la época 200 y luego crece. ¿Qué red conservas?
3. Después de la segunda época el error supera el millón, y luego se vuelve NaN. ¿Qué es lo más probable?
4. Todos los pesos son 0 al principio. ¿Qué pasa con las cuatro neuronas ocultas?
5. ¿Por qué normalmente se entrena con lotes, y no con todos los ejemplos a la vez o con uno por uno?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·