Wnioskowanie w sieciach neuronowych z Leo

post image

Ten wpis został napisany przez członka społeczności Aleo zk_tutorials.

Kopiowanie kodu W całym tym blogu będziemy odwoływać się do fragmentów kodu. Możesz skopiować te fragmenty klikając w prawym górnym rogu pola. Pełny kod źródłowy tego artykułu można znaleźć na GitHubie tutaj.

post image

Wstęp Sztuczna inteligencja (AI) może rozwiązać wiele zadań, które wcześniej wymagały ludzkiej inteligencji, zwiększając możliwości systemów oprogramowania. Większość systemów AI jest obecnie zbudowana na bazie sieci neuronowych, które okazały się zdolne do wykonywania złożonych zadań i utorowały drogę wielu ostatnim przełomom w dziedzinie AI.

Typowy przepływ pracy AI składa się z dwóch faz: szkolenia i wnioskowania. W trakcie tego przepływu pracy, intensywny charakter danych współczesnych systemów AI wzbudził obawy dotyczące prywatności. Podczas szkolenia, szczególnie dane szkoleniowe powinny być często chronione. Następnie, parametry modelu AI, jak również dane wejściowe i wyjściowe modelu mogą być chronione. Porównanie obu faz i danych, które należy chronić, przedstawiono w poniższej tabeli.

post image

W tym artykule skupiamy się na prowadzeniu wnioskowania wielowarstwowych sieci neuronowych typu perceptron w zkSnark. Oznacza to, że obliczamy wyjście sieci neuronowej w zkSnark, biorąc pod uwagę cechy wejściowe. Jak podkreśla tabela, istnieje szeroki zakres danych, które możemy chcieć chronić w tym obliczeniu, takich jak cechy wejściowe, model wejściowy, a nawet predykcja wyjścia. Do tego celu używamy języka programowania Leo.

**Obliczanie wnioskowania w sieciach neuronowych ** Sieć neuronowa jest funkcją matematyczną, która w sposób deterministyczny przekształca wartości wejściowe w wartości wyjściowe. Sieć neuronowa składa się z wielu połączonych ze sobą neuronów, które same są funkcjami matematycznymi przekształcającymi wartości wejściowe na wartości wyjściowe.

Aby zrozumieć, jak sieć neuronowa oblicza wartości wyjściowe, najpierw przyjrzymy się, jak oblicza się wyjście poszczególnych neuronów. Neurony składają się z funkcji aktywacji i dwóch parametrów - parametru wagi i parametru skosu.

Popularną funkcją aktywacji a(x) dla NN jest funkcja aktywacji ReLU (Rectified Linear Unit), która jest zdefiniowana przez:

a(x) = max(0,x)

Przed obliczeniem funkcji aktywacji obliczana jest suma wejść z wagami oraz bias. Przy wadze w=1.5 i biasie b=.5 neuron wyprowadza następującą funkcję:

max(0,1.5*x+.5)

Wejście o wartości x=1 tworzy wyjście o wartości 2.

Możemy teraz połączyć różne neurony, aby stworzyć sieć neuronową. Ten przykład demonstruje sieć neuronową z dwoma wejściami, dwoma neuronami w warstwie środkowej (zwanej warstwą ukrytą) i jednym wyjściem. Architekturę sieci neuronowej przedstawiamy na poniższym rysunku.

post image

Podczas gdy warstwa ukryta i warstwa wyjściowa mają funkcję aktywacji ReLU, warstwa wejściowa ma zazwyczaj liniową funkcję aktywacji a(x)=x. Taka architektura przekłada się na funkcję matematyczną:

f(x)=y0=max(0,max(0,(1.5*x0+0.5))+max(0,(0.5*x0+0.5*x1+1.5))+0.25)

Funkcję tę otrzymujemy patrząc najpierw na ostatni neuron wyjściowy, a następnie podstawiając do warstw wcześniejszych. Dla wartości wejściowych x0 = 0,5 i x1 = 1, prawidłowe wyjście to 1,25+2,25+0,25=3,75.

Generalnie NN mogą aproksymować szeroki wachlarz funkcji, co określa się mianem uniwersalnego twierdzenia o aproksymacji.

**Liczby stałoprzecinkowe dla sieci neuronowych ** Jak w powyższym przykładzie, często mamy wartości niecałkowite, które chcemy reprezentować i obliczać, w przeciwnym razie wyniki mogą być błędne. Jest to szczególnie ważne dla głębszych sieci neuronowych, gdzie błędy mogą się potęgować przez wiele warstw. Języki programowania oparte na zk-SNARK, takie jak Leo, nie obsługują domyślnie liczb niecałkowitych, ale możemy to obejść. Jednym z wygodnych sposobów na to jest użycie liczb stałoprzecinkowych, które omówiliśmy w poprzednim artykule. Używając liczb stałoprzecinkowych, możemy reprezentować i obliczać ułamkowe części liczb.

Implementacja sieci neuronowych w Leo

Aby zaimplementować sieć neuronową w Leo, ustawiamy wagi sieci neuronowej, biasy oraz wejście funkcji x jako parametry wejściowe programu. Architektura sieci neuronowej jest zakodowana na sztywno, a program oblicza i wyprowadza dane wyjściowe. W ten sposób neurony w warstwie ukrytej i wyjściowej używają rektyfikowanej liniowej funkcji aktywacji.

Poniższy kod dla układu LEO oblicza wyjście sieci neuronowej. W ten sposób, obliczamy wyjście od lewej do prawej w sieci, co oznacza, że najpierw obliczamy wyjścia dwóch neuronów w pierwszej warstwie. Następnie obliczana jest warstwa ukryta, a po niej warstwa wyjściowa. Obliczenia opierają się na liczbach stałoprzecinkowych. Po operacjach mnożenia z liczbami stałoprzecinkowymi musimy skorygować wynik, co zostało opisane w artykule o stałych punktach.

main.leo

post image

Ocena Wprowadzamy liczby opisane na powyższej grafice, w reprezentacji stałoprzecinkowej (liczba pomnożona przez 100).

project.in

post image

Wyjście układu jest wtedy następujące:

project.out

post image

Dzieląc przez 100, aby uwzględnić format liczb stałoprzecinkowych, otrzymujemy wynik 3,75, jak wyżej.

Pod względem złożoności, obwód urósł do stosunkowo dużej liczby 176 189 ograniczeń - szczególnie mnożenie i dzielenie dodaje duże ilości ograniczeń.

Skrypt Pythona do automatycznego generowania kodu Leo sieci neuronowej Teraz przeanalizujmy głębsze sieci neuronowe z większą ilością warstw. Te głębokie sieci neuronowe stoją za ostatnimi zadziwiającymi postępami w AI. W powyższym przykładzie mamy wagi i bias, które możemy dostosować. To, czego nie możemy jednak dostosować, to liczba wag i biasów, czyli architektura sieci neuronowej. Nie możemy też przekazać dowolnej liczby wejść, ponieważ Leo nie jest turing complete i nie pozwala np. na pętle z dynamiczną ilością wykonań. Możemy jednak zakodować program, który wygeneruje kod Leo dla dowolnych architektur sieci neuronowych, dzięki czemu nie będziemy musieli ręcznie kodować Leo NN. Python jest dobrym wyborem dla takiego programu.

Wszystko, czego potrzebujemy jako dane wejściowe do takiego generatora NN, to liczba warstw i neuronów na warstwę, współczynnik skalowania dla liczb stałoprzecinkowych oraz użyty typ liczby całkowitej. W pliku leo_neural_network_generator.py przedstawiono program potrafiący to zrobić. Możesz modyfikować liczbę neuronów na warstwę, współczynnik skalowania dla obliczeń stałoprzecinkowych, a także typ liczby całkowitej.

‍leo_neural_network_generator.py

post image
post image
post image
post image

Wyjście programu

Uruchommy ten plik python na najnowszej wersji python 3 i sprawdźmy wyjście. Generuje dwa pliki, plik kodu LEO main.leo i plik wejść project.in. Sieć neuronowa ma ten sam rozmiar co powyższa i jest bardzo podobna z dwoma drobnymi zmianami. Po pierwsze, automatycznie generowana sieć neuronowa zwraca tablicę, w tym konkretnym przypadku o rozmiarze 1, ponieważ mamy jeden neuron wyjściowy. Po drugie, automatycznie generowana sieć neuronowa jest bardziej uogólniona i pozwala na wszystkie możliwe połączenia neuronów warstwy wejściowej z neuronami warstwy środkowej.

main.leo

post image

project.in

post image

Możemy teraz umieścić te dwa pliki w projekcie Leo, na przykład w Aleo Studio, i użyć polecenia "leo run". Program kompiluje się pomyślnie z 176091 ograniczeniami, bardzo podobnie jak w powyższym przykładzie. Zasadniczo odtworzyliśmy sieć neuronową automatycznie za pomocą programu Pythona, który najpierw zaimplementowaliśmy ręcznie.

**Skalowalność ** Teraz możemy generować głębsze sieci neuronowe. Warstwa pomiędzy warstwą wejściową a warstwą wyjściową jest określana jako warstwy ukryte. W powyższym przykładzie mamy trójwarstwową sieć neuronową z jedną warstwą ukrytą. Dodajmy kolejną warstwę ukrytą z dwoma neuronami, czyli zmieniamy kod wejściowy na następujący wiersz:

post image

Teraz uruchamiamy program Python i uruchamiamy wygenerowany kod Leo poprzez "leo run". Mamy teraz 236997 ograniczeń.

Wypróbujmy trzy ukryte warstwy:

post image

Otrzymujemy teraz obwód z 351863 ograniczeniami.

post image

Cztery warstwy ukryte dają nam 439749 ograniczeń, pięć warstw ukrytych 527635 ograniczeń, sześć warstw ukrytych 615521 ograniczeń itd. Nanieśmy to na wykres.

post image