<jupyter_start><jupyter_text>Los bloques de construcción matemáticos del aprendizaje profundo (parte II) En este cuaderno vamos a ver: * Un ejemplo de red neuronal* El concepto de tensor y operaciones con tensores* Cómo las redes neuronales aprenden a través del algoritmo de retropropagación y el descenso por gradiente Este cuaderno está basado en los cuadernos del libro [Deep Learning with Python, Second Edition](https://www.manning.com/books/deep-learning-with-python-second-edition?a_aid=keras&a_bid=76564dff).> This is a companion notebook for the book [Deep Learning with Python, Second Edition](https://www.manning.com/books/deep-learning-with-python-second-edition?a_aid=keras&a_bid=76564dff). For readability, it only contains runnable code blocks and section titles, and omits everything else in the book: text paragraphs, figures, and pseudocode.> **If you want to be able to follow what's going on, I recommend reading the notebook side by side with your copy of the book.**> This notebook was generated for TensorFlow 2.6.  Reimplementando nuestro ejemplo desde cero en TensorFlow  Clase DenseLa clase `Dense` implementa la siguiente transformación de los datos de entrada donde `W` y `b` son los datos del modelo:```output = activation(dot(W, input) + b)```Donde la función de actividad suele ser `relu` para capas ocultas o `softmax` en la última capa.Nota de orientación a objetos en Python: * El método `__init__` es el constructor de la clase y se llama al crear una instancia de la clase para asignar valores por defecto a los atributos de la clase. * El método `__call__` es el método que se lanza cuando se llama a la instancia de la clase como si fuera una función.<jupyter_code>import tensorflow as tf

class NaiveDense:
    def __init__(self, input_size, output_size, activation):
        self.activation = activation

        w_shape = (input_size, output_size)
        w_initial_value = tf.random.uniform(w_shape, minval=0, maxval=1e-1)
        self.W = tf.Variable(w_initial_value)

        b_shape = (output_size,)
        b_initial_value = tf.zeros(b_shape)
        self.b = tf.Variable(b_initial_value)

    def __call__(self, inputs):
        return self.activation(tf.matmul(inputs, self.W) + self.b)

    @property
    def weights(self):
        return [self.W, self.b]<jupyter_output><empty_output><jupyter_text>❓❓❓ Intenta cambiar la forma de inicialización de los pesos y observa el efecto. ❓❓❓  Clase SequentialEsta clase se va a encargar de encadenar las capas. Va a empaquetar una lista de capas y expone un método `__call__()` que simplemente llama en orden a las capas subyacentes.<jupyter_code>class NaiveSequential:
    def __init__(self, layers):
        self.layers = layers

    def __call__(self, inputs):
        x = inputs
        for layer in self.layers:
           x = layer(x)
        return x

    @property
    def weights(self):
       weights = []
       for layer in self.layers:
           weights += layer.weights
       return weights<jupyter_output><empty_output><jupyter_text>Usando las clases `NaiveDense` y `NaiveSequential` podemos crear algo similar a un modelo de Keras:<jupyter_code>model = NaiveSequential([
    NaiveDense(input_size=28 * 28, output_size=512, activation=tf.nn.relu),
    NaiveDense(input_size=512, output_size=10, activation=tf.nn.softmax)
])
assert len(model.weights) == 4<jupyter_output><empty_output><jupyter_text>❓❓❓Prueba a cambiar: la función de activación de la capa oculta, el tamaño de salida, etc.❓❓❓  Generador de lotes o *batches* Esta clase nos permitirá iterar sobre las imágenes de entrenamiento en *mini-batches*.<jupyter_code>import math

class BatchGenerator:
    def __init__(self, images, labels, batch_size=128):
        assert len(images) == len(labels)
        self.index = 0
        self.images = images
        self.labels = labels
        self.batch_size = batch_size
        self.num_batches = math.ceil(len(images) / batch_size)

    def next(self):
        images = self.images[self.index : self.index + self.batch_size]
        labels = self.labels[self.index : self.index + self.batch_size]
        self.index += self.batch_size
        return images, labels<jupyter_output><empty_output><jupyter_text>Ejecutar un paso de entrenamientoLa parte más difícil del proceso es el "paso de entrenamiento": actualizar los pesos del modelo después de procesar un lote de datos. Necesitamos: 1. Calcular las predicciones del modelo para las imágenes del lote.1. Calcular el valor de la pérdida para estas predicciones, dadas las etiquetas reales.1. Calcular el gradiente de la pérdida con respecto a los pesos del modelo.1. Mover los pesos una pequeña cantidad en la dirección opuesta al gradiente. El gradiente lo vamos a calcular con `GradientTape` de TensorFlow.<jupyter_code>def one_training_step(model, images_batch, labels_batch):
    with tf.GradientTape() as tape:
        predictions = model(images_batch) # llama a __call__()
        per_sample_losses = tf.keras.losses.sparse_categorical_crossentropy(
            labels_batch, predictions)
        # media de elementos a través de las dimensiones del tensor
        average_loss = tf.reduce_mean(per_sample_losses)
    # gradiente del la función de coste/pérdida respecto a los pesos del modelo
    # de todas las capas
    gradients = tape.gradient(average_loss, model.weights)
    update_weights(gradients, model.weights)
    return average_loss<jupyter_output><empty_output><jupyter_text>La función `update_weights` moverá los pesos "un poco" en la dirección que reduzca la pérdida para este lote. La magnitud de este movimiento dependerá del `learning_rate` y será en dirección contaria al gradiente, por tanto basta con restar el gradiente multiplicado por `learning_rate` a los pesos para actualizarlos. Nota Python: [cómo funciona zip](https://www.freecodecamp.org/news/the-zip-function-in-python-explained-with-examples/).```learning_rate = 1e-3def update_weights(gradients, weights):    for g, w in zip(gradients, weights):        w.assign_sub(g * learning_rate)``` En la práctica será muy raro que implementemos la actualización de los pesos a mano y utilizaremos uno de los optimizadores de Keras:<jupyter_code>from tensorflow.keras import optimizers

optimizer = optimizers.SGD(learning_rate=1e-3)

def update_weights(gradients, weights):
    optimizer.apply_gradients(zip(gradients, weights))<jupyter_output><empty_output><jupyter_text>El bucle de aprendizaje completoUna época o *epoch* de aprendizaje consiste en repetir un paso de aprendizaje para cada lote del conjunto de entrenamiento. El bucle completo de aprendizaje es simplemente la repetición de una época.<jupyter_code>def fit(model, images, labels, epochs, batch_size=128):
    for epoch_counter in range(epochs):
        print(f"Epoch {epoch_counter}")
        batch_generator = BatchGenerator(images, labels)
        for batch_counter in range(batch_generator.num_batches):
            images_batch, labels_batch = batch_generator.next()
            loss = one_training_step(model, images_batch, labels_batch)
            if batch_counter % 100 == 0:
                print(f"loss at batch {batch_counter}: {loss:.2f}")<jupyter_output><empty_output><jupyter_text>Probemos nuestro modelo:<jupyter_code>from tensorflow.keras.datasets import mnist
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype("float32") / 255
test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype("float32") / 255

fit(model, train_images, train_labels, epochs=10, batch_size=128)<jupyter_output><empty_output><jupyter_text>❓❓❓ Modifica el código anterior para guardar el histórico de la función de pérdida y dibuja esta en una gráfica para poder estudiar la convergencia del algoritmo. Analiza la salida de la evolución de la función de error. ¿Ha sido siempre descendente?¿Qué podríamos hacer para mejorar la precisión?Prueba a reemplazar el optimizador por [otro de Keras](https://keras.io/api/optimizers/).Prueba a cambiar la tasa de aprendizaje y a activar el momento.❓❓❓  Evaluación del modeloPodemos evaluar el modelo eligiendo la neurona de salida con máximo valor en la predicción del conjunto de test y comparándola con la etiqueta esperada:<jupyter_code>import numpy as np
predictions = model(test_images)
predictions = predictions.numpy()
predicted_labels = np.argmax(predictions, axis=1)
matches = predicted_labels == test_labels
print(f"accuracy: {matches.mean():.2f}")<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0789044289044289, 'fraction_numerical': 0.009673659673659674, 'mean_word_length': 4.51478149100257, 'pattern_counts': {'":': 1, '<': 37, '<?xml version=': 0, '>': 40, 'https://': 4, 'lorem ipsum': 0, 'www.': 3, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '105423', 'license': 'MIT', 'n_tokens_mistral': 2834, 'n_tokens_neox': 2755, 'n_words': 886, 'path': 'chapter02_mathematical-building-blocks-network-from-scratch.ipynb', 'repo_name': 'ayrna/deep-learning-with-python-notebooks', 'size': '8580'}