fbpx

Jak stworzyć sieć neuronową

Maciej Mazurek , 8 stycznia 2020

Jak stworzyć sieć neuronową

Wstęp


Celem tego artykułu jest przedstawienie koncepcji działania sieci neuronowych, a konkretnie sieci neuronowych typu Feedforward neural network, poprzez skonstruowanie prostego przykładu takiej sieci w języku Python. Do pełnego zrozumienia załączonego do artykułu kodu wymagana jest jedynie umiejętność mnożenia macierzy.

Sieć neuronowa to statystyczny model obliczeniowy stosowany w uczeniu maszynowym. Można o nim myśleć jak o systemie połączonych synapsami neuronów, które przesyłają między sobą impulsy (dane). Sieć neuronowa składa się z trzech warstw: warstwy wejścia (input layer), warstwy ukrytej (hidden layer), oraz warstwy wyjścia (output layer), co ilustruje diagram 1.


Warstwy sieci neuronowej
Diagram 1. Warstwy sieci neuronowej

Warstwa wejścia przyjmuje dane wejściowe do obliczeń, w warstwie ukrytej odbywają się wszystkie obliczenia. Wynik tych obliczeń jest przesyłany do warstwy wyjścia.

Na powyższym diagramie okręgi reprezentują neurony, zaś strzałki - synapsy. Każda synapsa ma przypisaną pewną wagę, tzn liczbę, która (nieco upraszczając) określa, jak silnie przesyłana wartość wpływa na ostateczny wynik obliczeń. Żeby przesłać wartość, synapsa najpierw czyta wartość z neuronu wejściowego, następnie wartość tę mnoży przez wagę, by w końcu przesłać wynik do neuronu wyjściowego. Następnie neuron wyjściowy dokonuje obliczeń na dostarczonych mu przez synapsy wartościach i otrzymany wynik przekazuje do wychodzącej z niego synapsy.

Trenowanie sieci neuronowej jest procesem, którego celem jest (nieco upraszczając) dobór odpowiednich wag dla synaps. Zakładamy, że sposób w jaki dokonywane są obliczenia wewnątrz każdego z neuronów jest niezmienny. Trenowanie jest procesem iteracyjnym. Jedna iteracja składa się z dwóch (wykonywanych w podanej kolejności) kroków: propagacji oraz propagacji wstecznej.

Mówiąc w skrócie - propagacja polega na wykonaniu obliczeń na danych wejściowych stosując wagi przypisane synapsom. Propagacja wsteczna mierzy błąd, jakim jest obarczony rezultat propagacji (przez porównanie go z oczekiwanymi wynikami obliczeń, czyli z danymi treningowymi). W zależności od zmierzonego błędu modyfikowane są wagi synaps (można powiedzieć, że dostosowując wagi sieć "uczy się na swoich błędach").


Przykład


W tej sekcji zostanie zaprezentowane, jak skonstruować prostą sieć neuronową implementującą opisane we wstępie koncepcje.

Rozważmy następujący problem. Dla danej trzybitowej dodatniej liczby binarnej (bez znaku) rozstrzygnąć, czy jest ona parzysta. Poniżej przedstawiamy kilka przykładowych danych wejściowych wraz z oczekiwanymi wartościami na wyjściu.


Dane wejścioweOczekiwany wynik
1010
1101
0101

Powyższa tabelka to (bardzo skromne) zestawienie danych treningowych. Każdej podanej wartości wejścia przyporządkowany jest oczekiwany rezultat obliczeń.


Struktura sieci neuronowej


Na diagramie 2. przedstawiono strukturę naszej sieci neuronowej. Warstwę wejścia tworzą trzy neurony (każdy neuron odpowiada wartości jednego bitu z zapisu binarnego liczby z wejścia). Warstwa ukryta składa się z dwóch neuronów: U1 oraz U2. Neuron U1 sumuje wszystkie liczby, jakie zostały mu przesłane (z odpowiednimi wagami) przez wchodzące w niego synapsy, a następnie tę sumę przekazuje do neuronu U2. Waga synapsy biegnącej od neuronu U1 do neuronu U2 wynosi 1 (jeśli waga jest nieokreślona, domyślna jej wartość wynosi 1). Następnie neuron U2 nakłada na rezultat obliczeń wykonanych w neuronie U1 funkcję aktywacyjną (która zostanie opisana szczegółowo w dalszej części artykułu) i przekazuje wynik (znów z wagą 1) do warstwy wyjścia.


Model obliczeń
Diagram 2. Model obliczeń

Propagacja


Z tego, co zostało powiedziane powyżej, jasne jest, że wagi muszą mieć z góry ustaloną wartość w momencie, gdy po raz pierwszy wykonywana jest propagacja. Każdą z nich zainicjujemy losowo wybraną liczbą z przedziału (-1, 1), z jednym tylko ograniczeniem. Wartość oczekiwana wag (z pewnych powodów teoretycznych, które tu pomijamy) musi wynosić 0.

Propagacja wykonuje się następująco. Najpierw neurony z warstwy wejściowej są inicjalizowane bitami wejściowej liczby. Następnie wartość każdego neuronu z warstwy wejściowej mnożona jest przez odpowiednią wagę i jest przesyłana do neuronu U1. Neuron U1 sumuje wszystkie trzy wartości.

Rezultat wyliczony w neuronie U1 należy jeszcze "zinterpretować". O tej wartości można myśleć, jako o pewnej mierze (dla wtajemniczonych - prawdopodobieństwie) rozrzutu. Przykładowo, jeśli w neuronie U1 dostaniemy liczbę 332482, to nasza sieć neuronowa twierdzi, że z dużym prawdopodobieństwem poprawnym rezultatem dla danych trzech bitów jest 1. Jeśli natomiast neuron U1 wyliczył liczbę -54387, nasza sieć przewiduje, że poprawny wynik to 0. Jeśli natomiast neuron U1 wyliczył wartość 0, nasza sieć neuronowa nie ma bladego pojęcia, jaki wynik jest poprawny.

Interpretacja, o której mowa, odbywa się w neuronie U2 poprzez zastosowanie odpowiedniej funkcji aktywacji. Istnieje wiele różnych modeli, w których stosowane są przeróżne funkcje aktywacji. Dla naszych celów najlepsza jest funkcja sigmoidalna, której wykres przedstawiamy na rysunku poniżej.


Funkcja sigmoidalna

Widać, że ta funkcja "interpretuje" wynik obliczeń z neuronu U1 zgodnie z oczekiwaniami. Im argument jest większy, tym wynik jest bliższy jedynce, im argument jest mniejszy, tym liczba jest bliższa zeru. Zauważmy, że dla argumentu 0 funkcja przyjmuje wartość 0.5.


Propagacja wsteczna

Załóżmy, że wykonujemy jedną iterację procesu trenowania dla pary (110, 1) - pierwsza współrzędna w krotce to dane wejściowe, druga to oczekiwany rezultat. Oznaczmy przez R wartość wyliczoną w neuronie U2 dla opisanych wyżej danych wejściowych i dla wag, jakie w czasie propagacji były przypisane synapsom wychodzącym z neuronów z warstwy wejścia.

Propagację wsteczną zaczniemy od wyliczenia tego, jak bardzo wartość wyliczona podczas propagacji różni się od oczekiwanego wyniku.

Następnie - w zależności od otrzymanego błędu - należy poprawić wagi synaps. W metodzie użytej w tym przykładzie optymalizacja pojedynczej wagi odbywa się następująco.

error = R - expected_result
weight = weight + expected_result * error * d_sigmoid(R)

gdzie d_sigmoid(R) jest pochodną funkcji sigmoid w punkcie x=R. Jeśli czytelnika interesuje geneza powyższego wzoru, odsyłamy do opisu regresji logicznej. Orientacyjnie, funkcja mierząca błąd jest funkcją wypukłą, zatem można ją minimalizować schodząc "wzdłuż jej gradientu" - czyli w kierunku jej globalnego minimum.


Kod


import numpy as np


class SimpleNeuralNetwork:
    """
    Simple neural network that checks if a given binary representation of a positive number is even
    """

    def __init__(self):
        np.random.seed(1)
        self.weights = 2 * np.random.random((3, 1)) - 1

    def sigmoid(self, x):
        """
        Sigmmoid function - smooth function that maps any number to a number from 0 to 1
        """
        return 1 / (1 + np.exp(-x))

    def d_sigmoid(self, x):
        """
        Derivative of sigmoid function
        """
        return x * (1 - x)

    def train(self, train_input, train_output, train_iters):
        for _ in range(train_iters):
            propagation_result = self.propagation(train_input)
            self.backward_propagation(
                propagation_result, train_input, train_output)

    def propagation(self, inputs):
        """
        Propagation process
        """
        return self.sigmoid(np.dot(inputs.astype(float), self.weights))

    def backward_propagation(self, propagation_result, train_input, train_output):
        """
        Backward propagation process 
        """
        error = train_output - propagation_result
        self.weights += np.dot(
            train_input.T, error * self.d_sigmoid(propagation_result)
        )

Wyjaśnienie


Na koniec wyjaśnimy, jak przedstawiona powyżej klasa napisana w języku Python realizuje opisaną koncepcję (zakładamy, że czytelnik zna podstawy Pythona).

W konstruktorze klasy SimpleNeuralNetwork najpierw inicjalizujemy generator liczb losowych (linia 10), a następnie określamy początkowe wartości wag liczbami losowymi (linia 11). Początkowe wagi są zapisane jako współrzędne wektora kolumnowego. Po szczegóły odsyłamy do dokumentacji pakietu numpy.

Funkcja proparation jest odpowiedzialna za wykonanie procesu propagacji. Dane wejściowe (trzy bity liczby binarnej) są przekazywane do tej funkcji w postaci wektora o trzech współrzędnych. Polecenie


np.dot(inputs.astype(float), self.weights)

liczy iloczyn skalarny wektora wag oraz wektora danych wejściowych, zatem jest to wartość, którą liczy neuron U1. Przekazując tę wartość do funkcji sigmoid otrzymujemy rezultat obliczeń z neuronu U2.

Funkcja backward_propagation implementuje proces propagacji wstecznej. Na początku obliczany jest błąd względem oczekiwanego rezultatu (linia 41). Następnie modyfikowane są wagi synaps. Instrukcja


self.weights += np.dot(
    train_input.T, error * self.d_sigmoid(propagation_result)
)

wykonuje opisane wcześniej obliczenia na każdej współrzędnej wektora wag. Funkcja np.dot odpowiada za mnożenie macierzy. Wyrażenie train_input.T to po prostu operacja transpozycji wektora (macierzy) train_input.


Krótki program testowy


Poniżej zamieszczono krótki program testowy, który pokazuje możliwości klasy SimpleNeuralNetwork w akcji.


network = SimpleNeuralNetwork()

print(network.weights)

train_inputs = np.array(
    [[1, 1, 0], [1, 1, 1], [1, 1, 0], [1, 0, 0], [0, 1, 1], [0, 1, 0], ]
)

train_outputs = np.array([[0, 1, 0, 0, 1, 0]]).T

train_iterations = 50000

network.train(train_inputs, train_outputs, train_iterations)

print(network.weights)

print("Testing the data")
test_data = np.array([[1, 1, 1], [1, 0, 0], [0, 1, 1], [0, 1, 0], ])

for data in test_data:
    print(f"Result for {data} is:")
    print(network.propagation(data))

Jeśli chcesz poznać inne zastosowania języka Python, zapraszamy do artykułu, który szerzej omawia to zagadnienie.


  • Zobacz także

  • Polub nas

    Facebook Pagelike Widget
  • Ebook

  • 1