Sådan opbygges et neuralt netværk fra bunden med PyTorch

I denne artikel går vi under hætten på neurale netværk for at lære at bygge en fra bunden.

Den ene ting, der mest begejstrer mig for dyb læring, er at rode med kode for at opbygge noget fra bunden. Det er dog ikke en let opgave, og det er endnu sværere at lære en anden, hvordan man gør det.

Jeg har arbejdet mig igennem Fast.ai kurset, og denne blog er stærkt inspireret af min erfaring.

Uden yderligere forsinkelse, lad os starte vores vidunderlige rejse med at afmystificere neurale netværk.

Hvordan fungerer et neuralt netværk?

Lad os starte med at forstå de høje niveauer i neurale netværk.

Et neuralt netværk tager et datasæt ind og udsender en forudsigelse. Det er så simpelt som det.

Lad mig give dig et eksempel.

Lad os sige, at en af ​​dine venner (som ikke er en stor fodboldfan) peger på et gammelt billede af en berømt fodboldspiller - siger Lionel Messi - og spørger dig om ham.

Du vil være i stand til at identificere fodboldspilleren om et sekund. Årsagen er, at du har set hans billeder tusind gange før. Så du kan identificere ham, selvom billedet er gammelt eller blev taget i svagt lys.

Men hvad sker der, hvis jeg viser dig et billede af en berømt baseballspiller (og du aldrig har set et eneste baseballkamp før)? Du kan ikke genkende den afspiller. I så fald, selvom billedet er klart og lyst, ved du ikke, hvem det er.

Dette er det samme princip, der bruges til neurale netværk. Hvis vores mål er at opbygge et neuralt netværk til genkendelse af katte og hunde, viser vi bare det neurale netværk en masse billeder af hunde og katte.

Mere specifikt viser vi neurale netværksbilleder af hunde og fortæller det derefter, at det er hunde. Og vis det derefter billeder af katte og identificer dem som katte.

Når vi først har trænet vores neurale netværk med billeder af katte og hunde, kan det let klassificere, om et billede indeholder en kat eller en hund. Kort sagt kan den genkende en kat fra en hund.

Men hvis du viser vores neurale netværk et billede af en hest eller en ørn, vil det aldrig identificere det som hest eller ørn. Dette skyldes, at det aldrig har set et billede af en hest eller en ørn før, fordi vi aldrig har vist det de dyr.

Hvis du ønsker at forbedre det neurale netværks kapacitet, er alt, hvad du skal gøre, at vise det billeder af alle de dyr, som du ønsker, at det neurale netværk skal klassificere. Fra nu af er alt, hvad den ved, katte og hunde og intet andet.

Det datasæt, vi bruger til vores træning, afhænger stærkt af problemet i vores hænder. Hvis du ønsker at klassificere, om en tweet har en positiv eller negativ stemning, vil du sandsynligvis have et datasæt, der indeholder mange tweets med deres tilsvarende etiket som enten positiv eller negativ.

Nu hvor du har et højt overblik over datasæt, og hvordan et neuralt netværk lærer af disse data, lad os dykke dybere ned i, hvordan neurale netværk fungerer.

Forståelse af neurale netværk

Vi bygger et neuralt netværk for at klassificere cifrene tre og syv ud fra et billede.

Men inden vi bygger vores neurale netværk, er vi nødt til at gå dybere for at forstå, hvordan de fungerer.

Hvert billede, vi sender til vores neurale netværk, er bare en masse tal. Det vil sige, at hver af vores billeder har en størrelse på 28 × 28, hvilket betyder at de har 28 rækker og 28 kolonner, ligesom en matrix.

Vi ser hvert af cifrene som et komplet billede, men for et neuralt netværk er det bare en masse tal, der spænder fra 0 til 255.

Her er en pixelrepræsentation af cifret fem:

Som du kan se ovenfor, har vi 28 rækker og 28 kolonner (indekset starter fra 0 og slutter ved 27) ligesom en matrix. Neurale netværk ser kun disse 28 × 28 matricer.

For at vise nogle flere detaljer har jeg lige vist skyggen sammen med pixelværdierne. Hvis du ser nærmere på billedet, kan du se, at pixelværdierne tæt på 255 er mørkere, mens værdierne tættere på 0 er lysere i skygge.

I PyTorch bruger vi ikke udtrykket matrix. I stedet bruger vi udtrykket tensor. Hvert tal i PyTorch er repræsenteret som en tensor. Så fra nu af bruger vi udtrykket tensor i stedet for matrix.

Visualisering af et neuralt netværk

Et neuralt netværk kan have et hvilket som helst antal neuroner og lag.

Sådan ser et neuralt netværk ud:

Bliv ikke forvirret af de græske bogstaver på billedet. Jeg nedbryder det for dig:

Tag sagen om at forudsige, om en patient vil overleve eller ikke baseret på et datasæt, der indeholder patientens navn, temperatur, blodtryk, hjertesygdom, månedsløn og alder.

I vores datasæt har kun temperatur, blodtryk, hjertesygdom og alder væsentlig betydning for at forudsige, om patienten vil overleve eller ej. Så vi tildeler disse værdier en højere vægtværdi for at vise højere betydning.

Men funktioner som navnet på patienten og månedsløn har ringe eller ingen indflydelse på patientens overlevelsesrate. Så vi tildeler mindre vægtværdier til disse funktioner for at vise mindre betydning.

I ovenstående figur er x1, x2, x3 ... xn de funktioner i vores datasæt, der kan være pixelværdier i tilfælde af billeddata eller funktioner som blodtryk eller hjertesygdom som i ovenstående eksempel.

Funktionsværdierne ganges med de tilsvarende vægtværdier kaldet w1j, w2j, w3j ... wnj. De multiplicerede værdier summeres sammen og overføres til det næste lag.

De optimale vægtværdier læres under træningen af ​​det neurale netværk. Vægtværdierne opdateres løbende på en sådan måde, at antallet af korrekte forudsigelser maksimeres.

Aktiveringsfunktionen er intet andet end sigmoid-funktionen i vores tilfælde. Enhver værdi, vi overfører til sigmoid, konverteres til en værdi mellem 0 og 1. Vi sætter sigmoid-funktionen oven på vores neurale netværksforudsigelse for at få en værdi mellem 0 og 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.

Da vores model er meget lille, tager det ikke meget tid at træne i 2000 epoker eller iterationer. Efter 2000-epoker har vores neurale netværk givet en tabsværdi på 0,6805, hvilket ikke er dårligt fra en så lille model.

Konklusion

Der er et enormt plads til forbedring i den model, som vi lige har oprettet.

Dette er bare en simpel model, og du kan eksperimentere med den ved at øge antallet af lag, antallet af neuroner i hvert lag eller øge antallet af epoker.

Kort sagt, maskinindlæring er en hel masse magi ved hjælp af matematik. Lær altid de grundlæggende begreber - de kan være kedelige, men til sidst vil du forstå, at disse kedelige matematiske koncepter skabte disse banebrydende teknologier som deepfakes.

Du kan få den komplette kode på GitHub eller spille med koden i Google colab.