Kaip sukurti neuroninį tinklą nuo nulio

Neuroniniai tinklai yra tarsi gilaus mokymosi darbiniai arkliai. Turėdami pakankamai duomenų ir skaičiavimo galios, juos galima panaudoti sprendžiant daugumą gilaus mokymosi problemų. Labai lengva naudoti „Python“ ar „R“ biblioteką, norint sukurti neuroninį tinklą, mokyti jį bet kuriame duomenų rinkinyje ir gauti didelį tikslumą.

Mes galime traktuoti neuroninius tinklus tik kaip tam tikrą juodąją dėžę ir be jokių sunkumų juos naudoti. Nors atrodo, kad tokiu keliu eiti labai lengva, daug įdomiau sužinoti, kas slypi už šių algoritmų ir kaip jie veikia.

Šiame straipsnyje mes aptarsime kai kurias neuroninio tinklo kūrimo detales. Tinklo kodui rašyti ketinu naudoti „Python“. Skaitmeniniams skaičiavimams taip pat naudosiu „Python“ numpy biblioteką. Pabandysiu išvengti kai kurių sudėtingų matematinių detalių, tačiau galų gale remsiuosi keletu puikių šaltinių, jei norite apie tai sužinoti daugiau.

Taigi pradėkime.

Idėja

Prieš pradėdami rašyti savo neuroninio tinklo kodą, tiesiog palaukite ir supraskime, kas iš tikrųjų yra neuroninis tinklas.

Aukščiau esančiame paveikslėlyje galite pamatyti labai atsitiktinę nervų tinklo schemą. Jame yra keli spalvoti apskritimai, sujungti vienas su kitu rodyklėmis, nukreiptomis į tam tikrą kryptį. Šie spalvoti apskritimai kartais vadinami neuronais .

Šie neuronai yra ne kas kita, kaip matematinės funkcijos, kurios, įvedus tam tikrą įvestį, sukuria išėjimą . Išėjimoneuronų priklauso nuo įvesties ir parametrųneuronų . Mes galime atnaujinti šiuos parametrus, kad iš tinklo gautume norimą vertę.

Kiekvienas iš šių neuronų yra apibrėžtas naudojant sigmoidinę funkciją . Riestinės funkcija suteikia išvestį tarp nulio iki vieno kiekvienam įvesties ji pasireiškia. Šie sigmoidiniai vienetai yra sujungti vienas su kitu, kad suformuotų neuronų tinklą.

Čia jungdami turime omenyje, kad vieno sigmoidinių vienetų sluoksnio išvestis pateikiama kaip įvestis kiekvienam kito sluoksnio sigmoidiniam vienetui. Tokiu būdu mūsų neuroninis tinklas sukuria išėjimą bet kuriam įėjimui. Procesas tęsiasi tol, kol pasieksime galutinį sluoksnį. Galutinis sluoksnis sukuria savo produkciją.

Šis neuroninio tinklo, generuojančio išvestį duotam įėjimui, procesas yra „ Pirmyn sklidimas“ . Galutinio sluoksnio išvestis taip pat vadinama neuroninio tinklo prognozavimu . Vėliau šiame straipsnyje aptarsime, kaip mes vertiname prognozes . Šiuos vertinimus galima naudoti norint pasakyti, ar mūsų nervų tinklą reikia tobulinti, ar ne.

Iš karto po to, kai galutinis sluoksnis sugeneruoja savo produkciją, mes apskaičiuojame sąnaudų funkciją . Sąnaudų funkcija apskaičiuoja, kiek mūsų neuroninis tinklas yra nuo norimų prognozių. Sąnaudų funkcijos vertė parodo skirtumą tarp numatomos vertės ir tiesos vertės .

Mūsų tikslas yra sumažinti sąnaudų funkcijos vertę . Sąnaudų funkcijos sumažinimo procesui reikalingas algoritmas, kuris gali atnaujinti tinklo parametrų vertes taip, kad sąnaudų funkcija pasiektų mažiausią vertę .

Neuroninio tinklo parametrams atnaujinti naudojami tokie algoritmai kaip gradientinis nusileidimas ir stochastinis gradiento nusileidimas . Šie algoritmai atnaujina kiekvieno tinklo sluoksnio svorio ir šališkumo vertes, priklausomai nuo to, kaip tai paveiks išlaidų sumažinimo funkciją. Poveikis sąnaudų funkcijos minimizavimui, atsižvelgiant į kiekvieno iš tinklo įvesties neuronų svorį ir poslinkius, apskaičiuojamas proporcijos būdu .

Kodas

Taigi, dabar mes žinome pagrindines idėjas, susijusias su neuroniniais tinklais. Pradėkime įgyvendinti šias idėjas į kodą. Pradėsime nuo visų reikalingų bibliotekų importavimo.

import numpy as np import matplotlib.pyplot as plt

Kaip jau minėjau, nesinaudosime nė viena iš giliai besimokančių bibliotekų. Taigi, daugiausiai naudosime numerį, kad efektyviai atliktume matematinius skaičiavimus.

Pirmasis žingsnis kuriant mūsų neuroninį tinklą bus inicijuoti parametrus. Turime inicijuoti du kiekvieno sluoksnio neuronų parametrus: 1) svoris ir 2) šališkumas .

Šie svoriai ir poslinkiai deklaruojami vektorizuota forma. Tai reiškia, kad užuot inicijavę kiekvieno atskiro neurono svorius ir poslinkius kiekviename sluoksnyje, mes sukursime vektorių (arba matricą) svoriams ir kitą - šališkumui - kiekvienam sluoksniui.

Šie svoriai ir šališkumo vektoriai bus derinami su įvestimi į sluoksnį. Tada mes pritaikysime sigmoidinę funkciją šiam deriniui ir išsiųsime tai kaip įvestį į kitą sluoksnį.

sluoksnio matmenysišlaiko kiekvieno sluoksnio matmenis. Šiuos sluoksnių matmenis perduosime init_parmsfunkcija, kuri juos naudos parametrams inicializuoti. Šie parametrai bus saugomi žodyne, vadinamame parametrais . Taigi „params“ žodyne params ['W1']atspindės 1 sluoksnio svorio matricą.

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

Puiku! Mes inicializavome svorius ir poslinkius ir dabar apibrėžsime sigmoidinę funkciją . Jis apskaičiuos bet kurios nurodytos Z vertės sigmoidinės funkcijos vertę ir taip pat išsaugos šią vertę kaip talpyklą. Saugosime talpyklos vertes, nes jos reikalingos įgyvendinant atkūrimą. Z čia yra tiesinė hipotezė .

Atkreipkite dėmesį, kad neuroidinio tinklo terminologijoje sigmoidinė funkcija patenka į aktyvinimo funkcijų klasę . Sukūrus darbo aktyvinimo funkcija yra formuoti neurono išėjimo.

Pavyzdžiui, sigmoidinė funkcija ima įvestį su atskiromis reikšmėmis ir suteikia vertę, kuri yra tarp nulio ir vieno. Jo paskirtis yra konvertuoti tiesines išvestis į netiesines išvestis. Yra keli aktyvinimo funkcijų tipai , kuriuos galima naudoti siekiant geresnio veikimo, tačiau paprastumo sumetimais laikysimės sigmoidų.

# 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

Dabar pradėkime rašyti kodą į priekį. Anksčiau aptarėme, kad skleidimas į priekį ims ankstesnio sluoksnio reikšmes ir suteiks jį kaip įvestį kitam sluoksniui. Žemiau esančioje funkcijoje treniruotės duomenys ir parametrai bus naudojami kaip įvestis ir bus sukurta vieno sluoksnio išvestis, tada ji bus tiekiama kitam sluoksniui ir pan.

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 i įvestis į pirmąjį sluoksnį. Mes apžvelgsime visus tinklo sluoksnius ir apskaičiuosime tiesinę hipotezę. Po to ji paims Z reikšmę (tiesinė hipotezė) ir suteiks ją sigmoidinės aktyvacijos funkcijai. Talpyklos reikšmės saugomos pakeliui ir kaupiamos talpyklose . Galiausiai funkcija grąžins sukurtą vertę ir saugomą talpyklą.

Dabar apibrėžkime savo išlaidų funkciją.

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

Mažėjant kaštų funkcijos vertei, mūsų modelio našumas tampa geresnis. Sąnaudų funkcijos reikšmę galima sumažinti atnaujinant kiekvieno neuroninio tinklo sluoksnio parametrų vertes. Tokie algoritmai kaip Gradient Descent naudojami atnaujinant šias vertes taip, kad būtų kuo labiau sumažinta sąnaudų funkcija.

„Gradient Descent“ atnaujina vertes naudodama kai kuriuos atnaujinimo terminus. Šie atnaujinimo terminai, vadinami gradientais , apskaičiuojami naudojant atgalinį sklaidą. Gradiento vertės apskaičiuojamos kiekvienam tinklo neuronui ir tai atspindi galutinės išeigos pokytį, atsižvelgiant į konkretaus neurono parametrų pokytį.

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

Jei vis dar skaitote, ačiū! Šis straipsnis buvo šiek tiek sudėtingas, todėl siūlau jums pabandyti žaisti su kodu. Galite iš to gauti daugiau įžvalgų ir galbūt aptiksite klaidų kode. Jei taip yra, jei turite klausimų arba abu, nedvejodami pataikykite į „Twitter“. Aš padarysiu viską, kad tau padėčiau.

Ištekliai

  • „Neural Networks“ grojaraštis - „3Blue1Brown“
  • Neuroniniai tinklai ir gilus mokymasis - Michaelas A. Nielsenas
  • Gradiento nusileidimas ir stochastinis gradiento nusileidimas