Cómo construir una red neuronal desde cero con PyTorch

En este artículo, profundizaremos en las redes neuronales para aprender a construir una desde cero.

Lo único que más me emociona en el aprendizaje profundo es jugar con el código para construir algo desde cero. Sin embargo, no es una tarea fácil y enseñarle a otra persona cómo hacerlo es aún más difícil.

He estado trabajando en el curso Fast.ai y este blog está muy inspirado en mi experiencia.

Sin más demora, comencemos nuestro maravilloso viaje para desmitificar las redes neuronales.

¿Cómo funciona una red neuronal?

Comencemos por comprender el funcionamiento de alto nivel de las redes neuronales.

Una red neuronal toma un conjunto de datos y genera una predicción. Es tan simple como eso.

Dejame darte un ejemplo.

Digamos que uno de tus amigos (que no es un gran aficionado al fútbol) señala una vieja foto de un futbolista famoso, digamos Lionel Messi, y te pregunta por él.

Podrás identificar al futbolista en un segundo. La razón es que has visto sus fotos miles de veces antes. Para que pueda identificarlo incluso si la foto es antigua o se tomó con poca luz.

Pero, ¿qué pasa si te muestro una foto de un jugador de béisbol famoso (y nunca antes has visto un solo juego de béisbol)? No podrá reconocer a ese jugador. En ese caso, incluso si la imagen es clara y brillante, no sabrá quién es.

Este es el mismo principio que se utiliza para las redes neuronales. Si nuestro objetivo es construir una red neuronal para reconocer gatos y perros, simplemente mostramos a la red neuronal un montón de imágenes de perros y gatos.

Más específicamente, mostramos las imágenes de la red neuronal de perros y luego le decimos que son perros. Y luego muéstrele imágenes de gatos e identifíquelos como gatos.

Una vez que entrenamos nuestra red neuronal con imágenes de perros y gatos, esta puede clasificar fácilmente si una imagen contiene un gato o un perro. En resumen, puede reconocer un gato de un perro.

Pero si muestra a nuestra red neuronal una imagen de un caballo o un águila, nunca lo identificará como un caballo o un águila. Esto se debe a que nunca antes había visto una imagen de un caballo o un águila porque nunca le hemos mostrado esos animales.

Si desea mejorar la capacidad de la red neuronal, todo lo que tiene que hacer es mostrarle imágenes de todos los animales que desea que la red neuronal clasifique. A partir de ahora, todo lo que sabe son gatos y perros y nada más.

El conjunto de datos que utilizamos para nuestro entrenamiento depende en gran medida del problema en nuestras manos. Si desea clasificar si un tweet tiene un sentimiento positivo o negativo, entonces probablemente querrá un conjunto de datos que contenga muchos tweets con su etiqueta correspondiente como positivo o negativo.

Ahora que tiene una descripción general de alto nivel de los conjuntos de datos y cómo una red neuronal aprende de esos datos, profundicemos en cómo funcionan las redes neuronales.

Comprender las redes neuronales

Construiremos una red neuronal para clasificar los dígitos tres y siete de una imagen.

Pero antes de construir nuestra red neuronal, necesitamos profundizar para comprender cómo funcionan.

Cada imagen que pasamos a nuestra red neuronal es solo un montón de números. Es decir, cada una de nuestras imágenes tiene un tamaño de 28 × 28 lo que significa que tiene 28 filas y 28 columnas, al igual que una matriz.

Vemos cada uno de los dígitos como una imagen completa, pero para una red neuronal, es solo un grupo de números que van del 0 al 255.

Aquí hay una representación de píxeles del dígito cinco:

Como puede ver arriba, tenemos 28 filas y 28 columnas (el índice comienza en 0 y termina en 27) como una matriz. Las redes neuronales solo ven estas matrices de 28 × 28.

Para mostrar más detalles, acabo de mostrar la sombra junto con los valores de los píxeles. Si observa más de cerca la imagen, puede ver que los valores de píxeles cercanos a 255 son más oscuros, mientras que los valores más cercanos a 0 son más claros en la sombra.

En PyTorch no usamos el término matriz. En cambio, usamos el término tensor. Cada número en PyTorch se representa como un tensor. Entonces, de ahora en adelante, usaremos el término tensor en lugar de matriz.

Visualizando una red neuronal

Una red neuronal puede tener cualquier cantidad de neuronas y capas.

Así es como se ve una red neuronal:

No se confunda con las letras griegas de la imagen. Lo desglosaré por ti:

Tomemos el caso de predecir si un paciente sobrevivirá o no basándose en un conjunto de datos que contiene el nombre del paciente, la temperatura, la presión arterial, la condición cardíaca, el salario mensual y la edad.

En nuestro conjunto de datos, solo la temperatura, la presión arterial, la condición cardíaca y la edad tienen una importancia significativa para predecir si el paciente sobrevivirá o no. Por lo tanto, asignaremos un valor de peso más alto a estos valores para mostrar una mayor importancia.

Pero características como el nombre del paciente y el salario mensual tienen poca o ninguna influencia en la tasa de supervivencia del paciente. Así que asignamos valores de peso más pequeños a estas características para mostrar menos importancia.

En la figura anterior, x1, x2, x3 ... xn son las características de nuestro conjunto de datos que pueden ser valores de píxeles en el caso de datos de imagen o características como la presión arterial o la condición cardíaca como en el ejemplo anterior.

Los valores de las características se multiplican por los valores de peso correspondientes denominados w1j, w2j, w3j ... wnj. Los valores multiplicados se suman y pasan a la siguiente capa.

Los valores de peso óptimos se aprenden durante el entrenamiento de la red neuronal. Los valores de peso se actualizan continuamente de tal manera que se maximiza el número de predicciones correctas.

La función de activación no es más que la función sigmoidea en nuestro caso. Cualquier valor que pasamos al sigmoide se convierte en un valor entre 0 y 1. Simplemente colocamos la función sigmoidea encima de nuestra predicción de red neuronal para obtener un valor entre 0 y 1.

You will understand the importance of the sigmoid layer once we start building our neural network model.

There are a lot of other activation functions that are even simpler to learn than sigmoid.

This is the equation for a sigmoid function:

The circular-shaped nodes in the diagram are called neurons. At each layer of the neural network, the weights are multiplied with the input data.

We can increase the depth of the neural network by increasing the number of layers. We can improve the capacity of a layer by increasing the number of neurons in that layer.

Understanding our data set

The first thing we need in order to train our neural network is the data set.

Since the goal of our neural network is to classify whether an image contains the number three or seven, we need to train our neural network with images of threes and sevens. So, let's build our data set.

Luckily, we don't have to create the data set from scratch. Our data set is already present in PyTorch. All we have to do is just download it and do some basic operations on it.

We need to download a data set called MNIST(Modified National Institute of Standards and Technology) from the torchvision library of PyTorch.

Now let's dig deeper into our data set.

What is the MNIST data set?

The MNIST data set contains handwritten digits from zero to nine with their corresponding labels as shown below:

So, what we do is simply feed the neural network the images of the digits and their corresponding labels which tell the neural network that this is a three or seven.

How to prepare our data set

The downloaded MNIST data set has images and their corresponding labels.

We just write the code to index out only the images with a label of three or seven. Thus, we get a data set of threes and sevens.

First, let's import all the necessary libraries.

import torch from torchvision import datasets import matplotlib.pyplot as plt

We import the PyTorch library for building our neural network and the torchvision library for downloading the MNIST data set, as discussed before. The Matplotlib library is used for displaying images from our data set.

Now, let's prepare our data set.

mnist = datasets.MNIST('./data', download=True) threes = mnist.data[(mnist.targets == 3)]/255.0 sevens = mnist.data[(mnist.targets == 7)]/255.0 len(threes), len(sevens)

As we learned above, everything in PyTorch is represented as tensors. So our data set is also in the form of tensors.

We download the data set in the first line. We index out only the images whose target value is equal to 3 or 7 and normalize them by dividing with 255 and store them separately.

We can check whether our indexing was done properly by running the code in the last line which gives the number of images in the threes and sevens tensor.

Now let's check whether we've prepared our data set correctly.

def show_image(img): plt.imshow(img) plt.xticks([]) plt.yticks([]) plt.show() show_image(threes[3]) show_image(sevens[8])

Using the Matplotlib library, we create a function to display the images.

Let's do a quick sanity check by printing the shape of our tensors.

print(threes.shape, sevens.shape)

If everything went right, you will get the size of threes and sevens as ([6131, 28, 28]) and ([6265, 28, 28]) respectively. This means that we have 6131 28×28 sized images for threes and 6265 28×28 sized images for sevens.

We've created two tensors with images of threes and sevens. Now we need to combine them into a single data set to feed into our neural network.

combined_data = torch.cat([threes, sevens]) combined_data.shape

We will concatenate the two tensors using PyTorch and check the shape of the combined data set.

Now we will flatten the images in the data set.

flat_imgs = combined_data.view((-1, 28*28)) flat_imgs.shape

We will flatten the images in such a way that each of the 28×28 sized images becomes a single row with 784 columns (28×28=784). Thus the shape gets converted to ([12396, 784]).

We need to create labels corresponding to the images in the combined data set.

target = torch.tensor([1]*len(threes)+[2]*len(sevens)) target.shape

We assign the label 1 for images containing a three, and the label 0 for images containing a seven.

How to train your Neural Network

To train your neural network, follow these steps.

Step 1: Building the model

Below you can see the simplest equation that shows how neural networks work:

                                y = Wx + b

Here, the term 'y' refers to our prediction, that is, three or seven. 'W' refers to our weight values, 'x' refers to our input image, and 'b' is the bias (which, along with weights, help in making predictions).

In short, we multiply each pixel value with the weight values and add them to the bias value.

The weights and bias value decide the importance of each pixel value while making predictions.  

We are classifying three and seven, so we have only two classes to predict.

So, we can predict 1 if the image is three and 0 if the image is seven. The prediction we get from that step may be any real number, but we need to make our model (neural network) predict a value between 0 and 1.

This allows us to create a threshold of 0.5. That is, if the predicted value is less than 0.5 then it is a seven. Otherwise it is a three.

We use a sigmoid function to get a value between 0 and 1.

We will create a function for sigmoid using the same equation shown earlier. Then we pass in the values from the neural network into the sigmoid.

We will create a single layer neural network.

We cannot create a lot of loops to multiply each weight value with each pixel in the image, as it is very expensive. So we can use a magic trick to do the whole multiplication in one go by using matrix multiplication.

def sigmoid(x): return 1/(1+torch.exp(-x)) def simple_nn(data, weights, bias): return sigmoid(([email protected]) + bias)

Step 2: Defining the loss

Now, we need a loss function to calculate by how much our predicted value is different from that of the ground truth.

For example, if the predicted value is 0.3 but the ground truth is 1, then our loss is very high. So our model will try to reduce this loss by updating the weights and bias so that our predictions become close to the ground truth.

We will be using mean squared error to check the loss value. Mean squared error finds the mean of the square of the difference between the predicted value and the ground truth.

def error(pred, target): return ((pred-target)**2).mean()

Step 3: Initialize the weight values

We just randomly initialize the weights and bias. Later, we will see how these values are updated to get the best predictions.

w = torch.randn((flat_imgs.shape[1], 1), requires_grad=True) b = torch.randn((1, 1), requires_grad=True)

The shape of the weight values should be in the following form:

(Number of neurons in the previous layer, number of neurons in the next layer)

We use a method called gradient descent to update our weights and bias to make the maximum number of correct predictions.

Our goal is to optimize or decrease our loss, so the best method is to calculate gradients.

We need to take the derivative of each and every weight and bias with respect to the loss function. Then we have to subtract this value from our weights and bias.

In this way, our weights and bias values are updated in such a way that our model makes a good prediction.

Updating a parameter for optimizing a function is not a new thing – you can optimize any arbitrary function using gradients.

We've set a special parameter (called requires_grad) to true to calculate the gradient of weights and bias.

Step 4: Update the weights

If our prediction does not come close to the ground truth, that means that we've made an incorrect prediction. This means that our weights are not correct. So we need to update our weights until we get good predictions.

For this purpose, we put all of the above steps inside a for loop and allow it to iterate any number of times we wish.

At each iteration, the loss is calculated and the weights and biases are updated to get a better prediction on the next iteration.

Thus our model becomes better after each iteration by finding the optimal weight value suitable for our task in hand.

Each task requires a different set of weight values, so we can't expect our neural network trained for classifying animals to perform well on musical instrument classification.

This is how our model training looks like:

for i in range(2000): pred = simple_nn(flat_imgs, w, b) loss = error(pred, target.unsqueeze(1)) loss.backward() w.data -= 0.001*w.grad.data b.data -= 0.001*b.grad.data w.grad.zero_() b.grad.zero_() print("Loss: ", loss.item())

We will calculate the predictions and store it in the 'pred' variable by calling the function that we've created earlier. Then we calculate the mean squared error loss.

Then, we will calculate all the gradients for our weights and bias and update the value using those gradients.

We've multiplied the gradients by 0.001, and this is called learning rate. This value decides the rate at which our model will learn, if it is too low, then the model will learn slowly, or in other words, the loss will be reduced slowly.

If the learning rate is too high, our model will not be stable, jumping between a wide range of loss values. This means it will fail to converge.

We do the above steps for 2000 times, and each time our model tries to reduce the loss by updating the weights and bias values.

We should zero out the gradients at the end of each loop or epoch so that there is no accumulation of unwanted gradients in the memory which will affect our model's learning.

Since our model is very small, it doesn't take much time to train for 2000 epochs or iterations. After 2000 epochs, our neural netwok has given a loss value of 0.6805 which is not bad from such a small model.

Conclusion

There is a huge space for improvement in the model that we've just created.

This is just a simple model, and you can experiment on it by increasing the number of layers, number of neurons in each layer, or increasing the number of epochs.

In short, machine learning is a whole lot of magic using math. Always learn the foundational concepts – they may be boring, but eventually you will understand that those boring math concepts created these cutting edge technologies like deepfakes.

You can get the complete code on GitHub or play with the code in Google colab.