Како изградити неуронску мрежу од огреботина помоћу ПиТорцх-а

У овом чланку ћемо ићи испод хаубе неуронских мрежа да бисмо научили како да га направимо од темеља.

Једино што ме највише одушевљава у дубоком учењу је петљање са кодом да бих створио нешто од нуле. Ипак то није лак задатак, а још некога је научити како то учинити још је теже.

Радио сам кроз курс Фаст.аи и овај блог је надахнут мојим искуством.

Без даљег одлагања започнимо наше дивно путовање до демистификације неуронских мрежа.

Како функционише неуронска мрежа?

Почнимо са разумевањем рада неуронских мрежа на високом нивоу.

Неуронска мрежа узима скуп података и даје предвиђање. То је тако једноставно.

Даћу вам пример.

Рецимо да један од ваших пријатеља (који није велики љубитељ фудбала) покаже на стару слику славног фудбалера - рецимо Лионел Месси - и пита вас за њега.

Фудбалера ћете моћи да препознате у секунди. Разлог је тај што сте његове слике видели хиљаду пута раније. Тако да га можете препознати чак и ако је слика стара или је снимљена у пригушеном светлу.

Али шта ће се догодити ако вам покажем слику познатог бејзбол играча (а никада пре нисте видели ниједну бејзбол утакмицу)? Нећете моћи да препознате тог играча. У том случају, чак и ако је слика јасна и светла, нећете знати ко је то.

Ово је исти принцип који се користи за неуронске мреже. Ако је наш циљ да изградимо неуронску мрежу за препознавање мачака и паса, само ћемо показати неуронској мрежи гомилу слика паса и мачака.

Прецизније, приказујемо слике паса са неуронске мреже, а затим им кажемо да су то пси. А онда покажите слике мачака и идентификујте их као мачке.

Једном када обучимо своју неуронску мрежу са сликама мачака и паса, она лако може класификовати да ли слика садржи мачку или пса. Укратко, може препознати мачку од пса.

Али ако покажете нашој неуронској мрежи слику коња или орла, она је никада неће идентификовати као коња или орла. То је зато што никада раније није видео слику коња или орла, јер им никада нисмо показали те животиње.

Ако желите да побољшате способност неуронске мреже, онда све што треба да урадите је да јој покажете слике свих животиња које желите да неуронска мрежа класификује. Од сада све што знају су мачке и пси и ништа друго.

Скуп података који користимо за тренинг увелико зависи од проблема који имамо у рукама. Ако желите да класификујете да ли твит има позитиван или негативан осећај, вероватно ћете желети да скуп података који садржи пуно твитова са одговарајућом ознаком буде позитиван или негативан.

Сад кад имате преглед скупова података на високом нивоу и како неуронска мрежа учи из тих података, заронимо дубље у то како функционишу неуронске мреже.

Разумевање неуронских мрежа

Изградићемо неуронску мрежу како бисмо класификовали цифре три и седам са слике.

Али пре него што изградимо своју неуронску мрежу, морамо дубље да схватимо како оне функционишу.

Свака слика коју проследимо нашој неуронској мрежи је само гомила бројева. Односно, свака наша слика има величину 28 × 28, што значи да има 28 редова и 28 колона, баш попут матрице.

Сваку цифру видимо као комплетну слику, али за неуронску мрежу то је само гомила бројева у распону од 0 до 255.

Ево представљања цифре пет у пикселима:

Као што видите горе, имамо 28 редова и 28 колона (индекс почиње од 0 и завршава се на 27) баш као матрица. Неуронске мреже виде само ове матрице 28 × 28.

Да бих приказао још неких детаља, управо сам показао нијансу заједно са вредностима пиксела. Ако погледате ближе слику, можете видети да су вредности пиксела близу 255 тамније док су вредности ближе 0 светлије у сенци.

У ПиТорцх-у не користимо термин матрица. Уместо тога, користимо израз тензор. Сваки број у ПиТорцх-у представљен је као тензор. Дакле, од сада ћемо уместо матрице користити израз тензор.

Визуелизација неуронске мреже

Неуронска мрежа може имати било који број неурона и слојева.

Ево како изгледа неуронска мрежа:

Нека вас не збуне грчка слова на слици. Раздвојићу за вас:

Узмимо случај предвиђања да ли ће пацијент преживети или не на основу скупа података који садржи име пацијента, температуру, крвни притисак, стање срца, месечну плату и старост.

У нашем скупу података, само температура, крвни притисак, стање срца и старост имају значајну важност за предвиђање да ли ће пацијент преживети или не. Тако ћемо тим вредностима доделити већу вредност тежине како бисмо показали већу важност.

Али карактеристике попут имена пацијента и месечне зараде имају мало или нимало утицаја на стопу преживљавања пацијента. Дакле, овим карактеристикама додељујемо мање вредности тежине како би показале мању важност.

На горњој слици, к1, к2, к3 ... кн су карактеристике у нашем скупу података које могу бити вредности пиксела у случају података са слике или функције попут крвног притиска или стања срца као у горњем примеру.

Вредности обележја множе се одговарајућим вредностима тежине означеним као в1ј, в2ј, в3ј ... вњ. Помножене вредности се збрајају и преносе на следећи слој.

Оптималне вредности тежине се науче током тренинга неуронске мреже. Вредности тежине се континуирано ажурирају на такав начин да се максимализује број тачних предвиђања.

Функција активације у нашем случају није ништа друго до сигмоидна функција. Било која вредност коју проследимо сигмоиду претвара се у вредност између 0 и 1. Ставили смо сигмоидну функцију на врх нашег предвиђања неуронске мреже да бисмо добили вредност између 0 и 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.

Будући да је наш модел врло мали, не треба пуно времена да се обучи за 2000 епоха или итерација. После 2000 епоха, наш неуронски мрежни систем дао је вредност губитка од 0,6805, што није лоше од тако малог модела.

Закључак

Постоји огроман простор за побољшање модела који смо управо креирали.

Ово је само једноставан модел и на њему можете експериментисати повећавањем броја слојева, броја неурона у сваком слоју или повећавањем броја епоха.

Укратко, машинско учење је пуно чаролије помоћу математике. Увек научите темељне концепте - они могу бити досадни, али на крају ћете схватити да су ти досадни математички концепти створили ове најсавременије технологије попут деепфакеова.

Комплетни код можете добити на ГитХуб-у или се играти с њим у Гоогле цолаб-у.