Neurális hálózat kiépítése a Scratch-ból a PyTorch segítségével

Ebben a cikkben az ideghálózatok burkolata alá kerülünk, és megtanuljuk, hogyan építsünk egyet az alapoktól fogva.

Az egyik dolog, ami engem a legjobban izgat a mély tanulásban, az a kódverés, hogy valamit építsek a semmiből. Ez azonban nem könnyű feladat, és mást megtanítani ennek megtételére még nehezebb.

Végigjártam a Fast.ai tanfolyamot, és ezt a blogot nagyban inspirálta tapasztalatom.

Minden további késedelem nélkül kezdjük el csodálatos utunkat a neurális hálózatok demisztifikálásában.

Hogyan működik az ideghálózat?

Kezdjük azzal, hogy megértsük a neurális hálózatok magas szintű működését.

Az ideghálózat befogad egy adatkészletet, és előrejelzést ad ki. Ennyire egyszerű.

Hadd mondjak egy példát.

Tegyük fel, hogy az egyik barátod (aki nem nagy futballrajongó) rámutat egy híres futballista - mondjuk Lionel Messi - régi képére, és rólad kérdez.

Egy másodperc múlva azonosítani tudja a futballistát. Ennek oka az, hogy már ezerszer látta a képeit. Tehát akkor is azonosíthatja őt, ha a kép régi, vagy gyenge fényben készült.

De mi történik, ha megmutatok egy híres baseball játékos képét (és még soha nem látott egyetlen baseball játékot sem)? Nem fogja tudni felismerni azt a játékost. Ebben az esetben még akkor is, ha a kép tiszta és világos, nem fogja tudni, ki az.

Ugyanezt az elvet használják a neurális hálózatoknál is. Ha célunk egy neurális hálózat kiépítése a macskák és kutyák felismerésére, akkor csak egy csomó képet mutatunk be az ideghálózatnak kutyákról és macskákról.

Pontosabban megmutatjuk a kutyák ideghálózatának képeit, majd elmondjuk, hogy ezek kutyák. Ezután mutassa meg a macskák képeit, és azonosítsa azokat macskaként.

Miután macskák és kutyák képeivel képezzük ideghálózatunkat, könnyen osztályozható, hogy egy kép macskát vagy kutyát tartalmaz-e. Röviden: képes felismerni a macskát a kutyából.

De ha ideghálózatunknak képet mutat egy lóról vagy egy sasról, az soha nem fogja azonosítani lónak vagy sasnak. Ez azért van, mert még soha nem látott képet lóról vagy sasról, mert soha nem mutattuk meg neki ezeket az állatokat.

Ha javítani kívánja az ideghálózat képességét, akkor csak annyit kell tennie, hogy megmutatja neki az összes olyan állat képét, amelyet az ideghálózatnak osztályozni szeretne. Mostantól csak macskákat és kutyákat tud, és semmi mást.

Az edzéshez használt adatkészlet nagymértékben függ a kezünkben lévő problémától. Ha azt szeretné besorolni, hogy egy tweetnek pozitív vagy negatív megítélése van-e, akkor valószínűleg olyan adatkészletre lesz szüksége, amely sok tweetet tartalmaz a megfelelő címkével, akár pozitívként, akár negatívként.

Most, hogy magas szintű áttekintése van az adatsorokról és arról, hogy az idegháló hogyan tanul ezekből az adatokból, merüljünk el mélyebben az ideghálózatok működésében.

Az ideghálózatok megértése

Ideghálózatot fogunk építeni a kép három és hét számjegyének osztályozására.

De mielőtt felépítenénk idegi hálózatunkat, mélyebbre kell mennünk, hogy megértsük működésüket.

Minden kép, amelyet átadunk ideghálózatunknak, csak egy csomó szám. Vagyis mindegyik képünk mérete 28 × 28, ami azt jelenti, hogy 28 sor és 28 oszlop van, akárcsak egy mátrix.

A számjegyek mindegyikét teljes képnek tekintjük, de egy neurális hálózat számára ez csak egy csomó szám, 0 és 255 között.

Itt van az ötös szám pixelábrázolása:

Amint a fentiekből látható, 28 sorunk és 28 oszlopunk van (az index 0-tól kezdődik és 27-nél végződik), csakúgy, mint egy mátrix. Az ideghálózatok csak ezeket a 28 × 28 mátrixokat látják.

Néhány részlet bemutatásához csak az árnyékot mutattam a pixelértékekkel együtt. Ha jobban megnézi a képet, láthatja, hogy a 255-höz közeli pixelértékek sötétebbek, míg a 0-hoz közelebb eső értékek világosabbak.

A PyTorch-ban nem használjuk a mátrix kifejezést. Ehelyett a tenzor kifejezést használjuk. A PyTorch minden száma tenzorként jelenik meg. Tehát ezentúl a mátrix helyett a tenzor kifejezést fogjuk használni.

Idegháló vizualizálása

A neurális hálózat tetszőleges számú idegsejtet és réteget tartalmazhat.

Így néz ki egy neurális hálózat:

Ne zavarjanak össze a képen látható görög betűk. Lebontom neked:

Vegyük azt az esetet, amikor a beteg nevét, hőmérsékletét, vérnyomását, szívbetegségét, havi fizetését és életkorát tartalmazó adatkészlet alapján megjósoljuk, hogy a beteg életben marad-e vagy sem.

Adatkészletünkben csak a hőmérsékletnek, a vérnyomásnak, a szív állapotának és az életkornak van jelentős jelentősége annak megjóslásában, hogy a beteg életben marad-e vagy sem. Tehát ezekhez az értékekhez nagyobb súlyértéket rendelünk annak érdekében, hogy nagyobb jelentőséget mutassunk.

De olyan jellemzők, mint a beteg neve és a havi fizetés, alig vagy egyáltalán nem befolyásolják a beteg túlélési arányát. Tehát kisebb súlyértékeket rendelünk ezekhez a jellemzőkhöz, hogy kevésbé fontosak legyenek.

A fenti ábrán x1, x2, x3 ... xn azok az adathalmazunk jellemzői, amelyek képpontértékek lehetnek képadatok vagy olyan jellemzők esetén, mint a vérnyomás vagy a szívbetegség, mint a fenti példában.

A jellemző értékeket megszorozzuk a megfelelő súlyértékekkel, amelyek w1j, w2j, w3j ... wnj. A megszorzott értékeket összesítik és átadják a következő rétegnek.

Az optimális súlyértékeket az ideghálózat edzése során tanulják meg. A súlyértékeket folyamatosan frissítik oly módon, hogy maximalizálják a helyes előrejelzések számát.

Az aktivációs függvény nem más, mint esetünkben a sigmoid függvény. Bármely érték, amelyet átadunk a sigmoidnak, 0 és 1 közötti értékre konvertálódik. Csak a sigmoid függvényt tesszük a neurális hálózati előrejelzésünk tetejére, hogy 0 és 1 közötti értéket kapjunk.

Amint elkezdjük felépíteni az idegi hálózati modellünket, meg fogja érteni a sigmoid réteg fontosságát.

Sok más aktiválási funkció létezik, amelyeket még egyszerűbb megtanulni, mint a sigmoidot.

Ez a szigmoid függvény egyenlete:

A diagram kör alakú csomópontjait neuronoknak nevezzük. A neurális hálózat minden rétegénél a súlyokat megszorozzuk a bemeneti adatokkal.

A rétegek számának növelésével növelhetjük az ideghálózat mélységét. Javíthatjuk egy réteg kapacitását azáltal, hogy növeljük az adott réteg neuronjainak számát.

Adathalmazunk megértése

Az első dolog, amire az ideghálózatunk képzéséhez szükségünk van, az az adatkészlet.

Mivel ideghálózatunk célja annak osztályozása, hogy egy kép tartalmazza-e a három vagy a hét számot, ideghálózatunkat hármas és hetes képekkel kell képeznünk. Tehát készítsük el az adatkészletünket.

Szerencsére nem kell a nulláról létrehoznunk az adatsort. Adatkészletünk már jelen van a PyTorch-ban. Csak annyit kell tennünk, hogy csak letöltjük és néhány alapvető műveletet végrehajtunk rajta.

Le kell töltenünk egy MNIST nevű adatsort(Modified National Institute of Standards and Technology) a PyTorch fáklyás könyvtárából.

Most mélyedjünk el az adatkészletünkben.

Mi az MNIST adatkészlet?

Az MNIST adatkészlet kézzel írt számjegyeket tartalmaz, nullától kilencig, a megfelelő címkékkel együtt, az alábbiak szerint:

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.