Sådan opbygges et neuralt netværk fra bunden

Neurale netværk er som arbejdsheste ved dyb læring. Med nok data og beregningskraft kan de bruges til at løse de fleste af problemerne i dyb læring. Det er meget let at bruge et Python- eller R-bibliotek til at oprette et neuralt netværk og træne det i ethvert datasæt og få en stor nøjagtighed.

Vi kan behandle neurale netværk som bare en sort boks og bruge dem uden problemer. Men selvom det virker meget let at gå den vej, er det meget mere spændende at lære, hvad der ligger bag disse algoritmer, og hvordan de fungerer.

I denne artikel vil vi komme ind på nogle af detaljerne i opbygningen af ​​et neuralt netværk. Jeg skal bruge Python til at skrive kode til netværket. Jeg vil også bruge Pythons dumme bibliotek til at udføre numeriske beregninger. Jeg vil forsøge at undgå nogle komplicerede matematiske detaljer, men jeg vil henvise til nogle geniale ressourcer i sidste ende, hvis du vil vide mere om det.

Så lad os komme i gang.

Ide

Før vi begynder at skrive kode til vores neurale netværk, lad os bare vente og forstå, hvad der præcist er et neuralt netværk.

På billedet ovenfor kan du se et meget afslappet diagram over et neuralt netværk. Det har nogle farvede cirkler forbundet med hinanden med pile, der peger på en bestemt retning. Disse farvede cirkler kaldes undertiden neuroner .

Disse neuroner er intet andet end matematiske funktioner, som, når de får noget input, genererer en output . Den udgang af neuroner afhænger af input og parametre af neuroner . Vi kan opdatere disse parametre for at få en ønsket værdi ud af netværket.

Hver af disse neuroner defineres ved hjælp af sigmoid-funktion . En sigmoid-funktion giver en output mellem nul og en for hver input, den får. Disse sigmoid-enheder er forbundet til hinanden for at danne et neuralt netværk.

Ved forbindelse her mener vi, at output fra et lag af sigmoid-enheder gives som input til hver sigmoid-enhed i det næste lag. På denne måde producerer vores neurale netværk et output for et givet input. Processen fortsætter, indtil vi har nået det sidste lag. Det endelige lag genererer dets output.

Denne proces med et neuralt netværk, der genererer en output til en given input, er Forplantning . Output af det endelige lag kaldes også forudsigelse af det neurale netværk. Senere i denne artikel vil vi diskutere, hvordan vi vurderer forudsigelserne . Disse evalueringer kan bruges til at fortælle, om vores neurale netværk skal forbedres eller ej.

Lige efter at det endelige lag genererer dets output, beregner vi omkostningsfunktionen . Omkostningsfunktionen beregner, hvor langt vores neurale netværk er fra at forudsige de ønskede forudsigelser. Værdien af ​​omkostningsfunktionen viser forskellen mellem den forudsagte værdi og sandhedsværdien .

Vores mål her er at minimere værdien af omkostningsfunktionen . Processen med minimering af omkostningsfunktionen kræver en algoritme, der kan opdatere værdierne for parametrene i netværket på en sådan måde, at omkostningsfunktionen opnår sin minimumsværdi .

Algoritmer som gradientnedstigning og stokastisk gradientnedstigning bruges til at opdatere parametrene i det neurale netværk. Disse algoritmer opdaterer værdierne for vægte og forspændinger for hvert lag i netværket afhængigt af, hvordan det vil påvirke minimeringen af ​​omkostningsfunktionen. Effekten på minimeringen af ​​omkostningsfunktionen med hensyn til hver af vægtene og forspændingerne for hver af indgangsneuronerne i netværket beregnes ved hjælp af backpropagation .

Kode

Så vi kender nu hovedideerne bag de neurale netværk. Lad os begynde at implementere disse ideer i kode. Vi starter med at importere alle de krævede biblioteker.

import numpy as np import matplotlib.pyplot as plt

Som jeg nævnte, vil vi ikke bruge nogen af ​​dyb læringsbibliotekerne. Så vi bruger for det meste følelsesløs til at udføre matematiske beregninger effektivt.

Det første trin i opbygningen af ​​vores neurale netværk vil være at initialisere parametrene. Vi har brug for at initialisere to parametre for hver af neuronerne i hvert lag: 1) Vægt og 2) Bias .

Disse vægte og forspændinger er deklareret i vektoriseret form. Det betyder, at i stedet for at initialisere vægte og forspændinger for hver enkelt neuron i hvert enkelt lag, opretter vi en vektor (eller en matrix) for vægte og en anden for forspændinger for hvert lag.

Disse vægte og biasvektorer kombineres med input til laget. Derefter anvender vi sigmoid-funktionen over den kombination og sender den som input til det næste lag.

lagdæmpningholder dimensionerne for hvert lag. Vi sender disse dimensioner af lag til init_parmsfunktion, der bruger dem til at initialisere parametre. Disse parametre gemmes i en ordbog kaldet params . Så i params ordbogen params ['W1']repræsenterer vægtmatrixen for lag 1.

def init_params(layer_dims): np.random.seed(3) params = {} L = len(layer_dims) for l in range(1, L): params['W'+str(l)] = np.random.randn(layer_dims[l], layer_dims[l-1])*0.01 params['b'+str(l)] = np.zeros((layer_dims[l], 1)) return params

Store! Vi har initialiseret vægten og forspændingen, og nu definerer vi sigmoid-funktionen . Det beregner værdien af ​​sigmoid-funktionen for en given værdi af Z og gemmer også denne værdi som en cache. Vi gemmer cache-værdier, fordi vi har brug for dem til implementering af backpropagation. Den Z her er den lineære hypotese .

Bemærk, at sigmoid-funktionen falder ind under klassen af aktiveringsfunktioner i det neurale netværksterminologi. Jobbet med en aktiveringsfunktion er at forme output fra en neuron.

For eksempel tager sigmoid-funktionen input med diskrete værdier og giver en værdi, der ligger mellem nul og en. Dens formål er at konvertere de lineære output til ikke-lineære output. Der er forskellige typer aktiveringsfunktioner, der kan bruges til bedre ydeevne, men vi holder os til sigmoid for enkelheds skyld.

# Z (linear hypothesis) - Z = W*X + b , # W - weight matrix, b- bias vector, X- Input def sigmoid(Z): A = 1/(1+np.exp(np.dot(-1, Z))) cache = (Z) return A, cache

Lad os nu begynde at skrive kode til videreformidling. Vi har diskuteret tidligere, at frem formering vil tage værdierne fra det foregående lag og give det som input til det næste lag. Funktionen nedenfor tager træningsdataene og parametrene som input og genererer output for et lag, og derefter føder det output til det næste lag osv.

def forward_prop(X, params): A = X # input to first layer i.e. training data caches = [] L = len(params)//2 for l in range(1, L+1): A_prev = A # Linear Hypothesis Z = np.dot(params['W'+str(l)], A_prev) + params['b'+str(l)] # Storing the linear cache linear_cache = (A_prev, params['W'+str(l)], params['b'+str(l)]) # Applying sigmoid on linear hypothesis A, activation_cache = sigmoid(Z) # storing the both linear and activation cache cache = (linear_cache, activation_cache) caches.append(cache) return A, caches

A_prev jeg s input til det første lag. Vi vil løbe gennem alle lagene i netværket og beregne den lineære hypotese. Derefter tager det værdien af Z (lineær hypotese) og giver den til sigmoid-aktiveringsfunktionen. Cache-værdier gemmes undervejs og akkumuleres i cacher . Endelig returnerer funktionen den genererede værdi og den gemte cache.

Lad os nu definere vores omkostningsfunktion.

def cost_function(A, Y): m = Y.shape[1] cost = (-1/m)*(np.dot(np.log(A), Y.T) + np.dot(log(1-A), 1-Y.T)) return cost

Efterhånden som værdien af ​​omkostningsfunktionen falder, bliver vores model ydeevne bedre. Værdien af ​​omkostningsfunktionen kan minimeres ved at opdatere værdierne for parametrene for hvert af lagene i det neurale netværk. Algoritmer som Gradient Descent bruges til at opdatere disse værdier på en sådan måde, at omkostningsfunktionen minimeres.

Gradient Descent opdaterer værdierne ved hjælp af nogle opdateringsudtryk. Disse opdateringsudtryk kaldet gradienter beregnes ved hjælp af backpropagation. Gradientværdier beregnes for hver neuron i netværket, og det repræsenterer ændringen i den endelige output i forhold til ændringen i parametrene for den pågældende neuron.

def one_layer_backward(dA, cache): linear_cache, activation_cache = cache Z = activation_cache dZ = dA*sigmoid(Z)*(1-sigmoid(Z)) # The derivative of the sigmoid function A_prev, W, b = linear_cache m = A_prev.shape[1] dW = (1/m)*np.dot(dZ, A_prev.T) db = (1/m)*np.sum(dZ, axis=1, keepdims=True) dA_prev = np.dot(W.T, dZ) return dA_prev, dW, db

The code above runs the backpropagation step for one single layer. It calculates the gradient values for sigmoid units of one layer using the cache values we stored previously. In the activation cache we have stored the value of Z for that layer. Using this value we will calculate the dZ, which is the derivative of the cost function with respect to the linear output of the given neuron.

Once we have calculated all of that, we can calculate dW, db and dA_prev, which are the derivatives of cost function with respect the weights, biases and previous activation respectively. I have directly used the formulae in the code. If you are not familiar with calculus then it might seem too complicated at first. But for now think about it as any other math formula.

After that we will use this code to implement backpropagation for the entire neural network. The function backprop implements the code for that. Here, we have created a dictionary for mapping gradients to each layer. We will loop through the model in a backwards direction and compute the gradient.

def backprop(AL, Y, caches): grads = {} L = len(caches) m = AL.shape[1] Y = Y.reshape(AL.shape) dAL = -(np.divide(Y, AL) - np.divide(1-Y, 1-AL)) current_cache = caches[L-1] grads['dA'+str(L-1)], grads['dW'+str(L-1)], grads['db'+str(L-1)] = one_layer_backward(dAL, current_cache) for l in reversed(range(L-1)): current_cache = caches[l] dA_prev_temp, dW_temp, db_temp = one_layer_backward(grads["dA" + str(l+1)], current_cache) grads["dA" + str(l)] = dA_prev_temp grads["dW" + str(l + 1)] = dW_temp grads["db" + str(l + 1)] = db_temp return grads

Once, we have looped through all the layers and computed the gradients, we will store those values in the grads dictionary and return it.

Finally, using these gradient values we will update the parameters for each layer. The function update_parameters goes through all the layers and updates the parameters and returns them.

def update_parameters(parameters, grads, learning_rate): L = len(parameters) // 2 for l in range(L): parameters['W'+str(l+1)] = parameters['W'+str(l+1)] -learning_rate*grads['W'+str(l+1)] parameters['b'+str(l+1)] = parameters['b'+str(l+1)] - learning_rate*grads['b'+str(l+1)] return parameters

Finally, it's time to put it all together. We will create a function called train for training our neural network.

def train(X, Y, layer_dims, epochs, lr): params = init_params(layer_dims) cost_history = [] for i in range(epochs): Y_hat, caches = forward_prop(X, params) cost = cost_function(Y_hat, Y) cost_history.append(cost) grads = backprop(Y_hat, Y, caches) params = update_parameters(params, grads, lr) return params, cost_history

This function will go through all the functions step by step for a given number of epochs. After finishing that, it will return the final updated parameters and the cost history. Cost history can be used to evaluate the performance of your network architecture.

Conclusion

Hvis du stadig læser dette, tak! Denne artikel var lidt kompliceret, så hvad jeg foreslår, at du er at prøve at lege med koden. Du får muligvis nogle flere indsigter ud af det, og måske finder du muligvis også nogle fejl i koden. Hvis det er tilfældet, eller hvis du har nogle spørgsmål eller begge dele, er du velkommen til at slå mig op på twitter. Jeg vil gøre mit bedste for at hjælpe dig.

Ressourcer

  • Neural Networks Playlist - af 3Blue1Brown
  • Neurale netværk og dyb læring - af Michael A. Nielsen
  • Gradient Descent og Stochastic Gradient Descent