Rețelele neuronale sunt ca și calele de lucru ale învățării profunde. Cu suficiente date și putere de calcul, acestea pot fi utilizate pentru a rezolva majoritatea problemelor din învățarea profundă. Este foarte ușor să utilizați o bibliotecă Python sau R pentru a crea o rețea neuronală și a o antrena pe orice set de date și pentru a obține o precizie excelentă.

Putem trata rețelele neuronale ca doar niște cutii negre și le putem folosi fără nicio dificultate. Dar, deși pare foarte ușor să mergeți în acest fel, este mult mai interesant să aflați ce se află în spatele acestor algoritmi și cum funcționează.

În acest articol vom intra în unele dintre detaliile construirii unei rețele neuronale. Voi folosi Python pentru a scrie cod pentru rețea. De asemenea, voi folosi biblioteca numpy a lui Python pentru a efectua calcule numerice. Voi încerca să evit câteva detalii matematice complicate, dar mă voi referi la câteva resurse strălucitoare în cele din urmă, dacă doriți să aflați mai multe despre asta.

Deci sa începem.

Idee

Înainte de a începe să scriem cod pentru rețeaua noastră neuronală, să așteptăm și să înțelegem ce este exact o rețea neuronală.

Cum se construieste o retea neuronala de la zero
Sursă

În imaginea de mai sus puteți vedea o diagramă foarte casuală a unei rețele neuronale. Are câteva cercuri colorate conectate între ele cu săgeți îndreptate către o anumită direcție. Aceste cercuri colorate sunt uneori denumite neuroni.

Aceste neuroni nu sunt altceva decât funcții matematice care, atunci când sunt date unele intrare, genera un ieșire. ieșire de neuroni depinde de intrare si parametrii din neuroni. Le putem actualiza parametrii pentru a obține o valoare dorită din rețea.

Fiecare dintre acestea neuroni sunt definite folosind funcția sigmoidă. A funcția sigmoidă dă o ieșire între zero la unu pentru fiecare intrare pe care o primește. Aceste unități sigmoide sunt conectate între ele pentru a forma o rețea neuronală.

Prin conexiune aici înțelegem că ieșirea unui strat de unități sigmoide este dată ca intrare pentru fiecare unitate sigmoidă a stratului următor. În acest fel, rețeaua noastră neuronală produce o ieșire pentru orice intrare dată. Procesul continuă până când am ajuns la stratul final. Stratul final își generează ieșirea.

Acest proces al unei rețele neuronale generatoare de ieșire pentru un dat intrare este Propagare înainte. Ieșirea stratului final se mai numește predicție a rețelei neuronale. Mai târziu în acest articol vom discuta despre modul în care noi evaluați predicțiile. Aceste evaluări pot fi folosite pentru a spune dacă rețeaua noastră neuronală are nevoie de îmbunătățiri sau nu.

Imediat după ce stratul final își generează ieșirea, calculăm funcția de cost. Funcția de cost calculează cât de departe este rețeaua noastră neuronală de a face predicțiile dorite. Valoarea funcției de cost arată diferența dintre valoarea prezisă si valoarea adevărului.

Obiectivul nostru aici este de a minimiza valoarea funcția de cost. Procesul de minimizare a funcției cost necesită un algoritm care poate actualiza valorile parametrii în rețea în așa fel încât funcția de cost să o atingă valoare minimă.

Algoritmi precum coborâre în gradient și coborâre gradient stochastic sunt utilizate pentru actualizarea fișierului parametrii a rețelei neuronale. Acești algoritmi actualizează valorile greutăților și părtinirilor fiecărui strat din rețea în funcție de modul în care va afecta funcția de minimizare a costurilor. Efectul asupra minimizării funcției de cost în raport cu fiecare dintre greutățile și părtinirile fiecărui neuron de intrare din rețea este calculat de propagarea înapoi.

Cod

Deci, acum cunoaștem ideile principale din spatele rețelelor neuronale. Să începem să punem în aplicare aceste idei în cod. Vom începe prin importul tuturor bibliotecilor necesare.

import numpy as np
import matplotlib.pyplot as plt

După cum am menționat, nu vom folosi niciuna dintre bibliotecile de învățare profundă. Deci, vom folosi mai ales numpy pentru efectuarea eficientă a calculelor matematice.

Primul pas în construirea rețelei noastre neuronale va fi inițializarea parametrilor. Trebuie să inițializăm doi parametri pentru fiecare dintre neuronii din fiecare strat: 1) Greutate și 2) Părtinire.

Aceste greutăți și prejudecăți sunt declarate în vectorizat formă. Asta înseamnă că, în loc să inițializăm greutățile și părtinirile pentru fiecare neuron individual din fiecare strat, vom crea un vector (sau o matrice) pentru greutăți și altul pentru părtiniri, pentru fiecare strat.

Aceste greutăți și părtinire vectorii vor fi combinați cu intrarea în strat. Apoi vom aplica funcția sigmoidă asupra acelei combinații și o vom trimite ca intrare la următorul strat.

layer_dims deține dimensiunile fiecărui strat. Vom trece aceste dimensiuni ale straturilor către init_parms funcție care le va folosi pentru a inițializa parametrii. Acești parametri vor fi stocați într-un dicționar numit params. Deci, în dicționarul params params[‘W1’] va reprezenta matricea de greutate pentru stratul 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

Grozav! Am inițializat greutățile și părtinirile și acum vom defini funcția sigmoidă. Acesta va calcula valoarea funcției sigmoide pentru orice valoare dată de Z și va stoca, de asemenea, această valoare ca un cache. Vom stoca valorile cache deoarece avem nevoie de ele pentru implementarea propagării înapoi. Z aici este ipoteză liniară.

Rețineți că funcția sigmoidă se încadrează în clasa funcții de activare în terminologia rețelei neuronale. Slujba unui funcția de activare este de a modela ieșirea unui neuron.

De exemplu, funcția sigmoidă intră cu valori discrete și dă o valoare care se află între zero și una. Scopul său este de a converti ieșirile liniare în ieșiri neliniare. Există diferite tipuri de funcții de activare care poate fi folosit pentru performanțe mai bune, dar vom rămâne sigmoid din motive de simplitate.

# 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

Acum, să începem să scriem cod pentru propagarea înainte. Am discutat mai devreme că redirecţiona propagare va prelua valorile din stratul anterior și le va da ca intrare în stratul următor. Funcția de mai jos va prelua date de instruire și parametrii ca intrări și va genera ieșire pentru un strat și apoi va alimenta ieșirea către următorul strat și așa mai departe.

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 euIntroducere în primul strat. Vom parcurge toate straturile rețelei și vom calcula ipoteza liniară. După aceea va lua valoarea Z (ipoteză liniară) și o va da funcției de activare sigmoidă. Valorile cache sunt stocate pe parcurs și sunt acumulate în cache-uri. În cele din urmă, funcția va returna valoarea generată și memoria cache stocată.

Să definim acum funcția noastră de cost.

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

Pe măsură ce valoarea funcției de cost scade, performanța modelului nostru devine mai bună. Valoarea funcției de cost poate fi minimizată prin actualizarea valorilor parametrilor fiecăruia dintre straturile din rețeaua neuronală. Algoritmi precum Coborâre în gradient sunt utilizate pentru a actualiza aceste valori în așa fel încât funcția de cost să fie redusă la minimum.

Gradient Descent actualizează valorile cu ajutorul unor termeni de actualizare. Acești termeni de actualizare numiți gradienți sunt calculate utilizând propagarea înapoi. Valorile gradientului sunt calculate pentru fiecare neuron din rețea și reprezintă schimbarea rezultatului final în raport cu modificarea parametrilor acelui 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

Codul de mai sus execută pasul de propagare înapoi pentru un singur strat. Calculează valorile gradientului pentru unitățile sigmoide ale unui singur nivel folosind valorile cache stocate anterior. În memoria cache de activare am stocat valoarea lui Z pentru acel strat. Folosind această valoare vom calcula dZ, care este derivatul funcției de cost în raport cu ieșirea liniară a neuronului dat.

Odată ce am calculat toate acestea, putem calcula dW, db și dA_prev, care sunt derivații funcției de cost în ceea ce privește ponderile, părtinirile și respectiv activarea anterioară. Am folosit direct formulele din cod. Dacă nu sunteți familiarizați cu calculul, atunci ar putea părea prea complicat la început. Dar, deocamdată, gândiți-vă la asta ca la orice altă formulă matematică.

După aceea, vom folosi acest cod pentru a implementa propagarea înapoi pentru întreaga rețea neuronală. Functia backprop implementează codul pentru asta. Aici, am creat un dicționar pentru maparea gradienților pe fiecare strat. Vom parcurge modelul într-o direcție inversă și vom calcula gradientul.

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

Odată ce am parcurs toate straturile și am calculat gradienții, vom stoca aceste valori în absolvenți dicționar și returnează-l.

În cele din urmă, folosind aceste valori de gradient vom actualiza parametrii pentru fiecare strat. Functia update_parameters parcurge toate straturile și actualizează parametrii și îi returnează.

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

În cele din urmă, este timpul să punem totul împreună. Vom crea o funcție numită tren pentru instruirea rețelei noastre neuronale.

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

Această funcție va parcurge toate funcțiile pas cu pas pentru un număr dat de epoci. După ce ați terminat, va returna parametrii actualizați finali și istoricul costurilor. Istoricul costurilor poate fi utilizat pentru a evalua performanța arhitecturii rețelei.

Concluzie

Dacă tot citești asta, mulțumesc! Acest articol a fost puțin complicat, așa că ceea ce vă sugerez să faceți este să încercați să vă jucați cu codul. S-ar putea să obțineți mai multe informații din acesta și poate că veți găsi și unele erori în cod. Dacă acesta este cazul sau dacă aveți câteva întrebări sau ambele, nu ezitați să mă loviți stare de nervozitate. Voi face tot posibilul să te ajut.

Resurse