23 listopada 2018

SSN - Perceptron w asm x64 - część I - bramki logiczne AND i OR

W krótkim cyklu zaprezentuję przykłady prostych sieci neuronowych, zaimplementowanych w asemblerze. Wybrałem architekturę x64 ze względu na ilość dostępnych rejestrów oraz ich rozmiar. W pierwszej części, przedstawię kod prostego perceptronu, którego zadaniem będzie symulowanie bramek logicznych AND, OR, bez użycia w programie instrukcji and, or.
Opisywany perceptron nie posiada trwałej pamięci i składa się z dwóch niezależnych od siebie neuronów, z których każdy symuluje jedną bramkę. Bramki posiadają po dwa wejścia (A i B) i na każde z nich, można wprowadzić po jednej dowolnej wartości, ze zbioru liczb całkowitych {0,1}. Korzystając z wariacji z powtórzeniami, otrzymujemy 4 różne warunki wejściowe.


bramki logiczne

Dlaczego nie XOR ? Nie ma możliwości opisania jednym neuronem bramki XOR, ponieważ pojedynczy neuron nie potrafi odróżniać zbiorów nieseparowalnych liniowo.
Neuron perceptronu, jako bramka logiczna, musi posiadać dwa wejścia. Sygnał na każde z wejść, jest wyrażony wartością (s=xw+b) gdzie:
s - sygnał,
x - liczba całkowita z przedziału {0,1},
w - waga wejścia, jak ważne jest to co wchodzi na dane wejście, należy pamiętać, że w > 0,
b - odchylenie (bias), jest to wartość odpowiadająca za nieliniowe przekształcenie wejść w wyjście

W opisywanym perceptronie wartość x = {0,1}, waga poszczególnych wejść = 1, a odchylenie = 0.
Ostatnią wartością jaką musimy określić jest poziom aktywacji neuronu, który w rzeczywistości, jest sumatorem wartości sygnałów wejściowych. I tak dla:
- bramki AND: poziom aktywacji >= 2,
- bramki OR: poziom aktywacji >= 1,
- bramki XOR: 2 > poziom aktywacji >= 1

Poziom aktywacji to wartość graniczna, po której neuron zaczyna zwracać dane na wyjście.
Dlaczego Asembler ? Złożone sieci neuronowe będą wymagały szybkich urządzeń, a pisanie w języku niskiego poziomu zmniejsza ilość kodu wynikowego oraz daje możliwość optymalizowania procedur już na poziomie wykorzystania rejestrów - o czym przekonamy się w prezentowanym przykładzie.
OPTION DOTNAME
option casemap:none

.DATA
weight_A  db  1                   ; waga dla operacji AND i OR ustalona dla wejścia A = 1
weight_B  db  1                   ; waga dla operacji AND i OR ustalona dla wejścia B = 1
bias_A    db  0                   ; odchylenie dla operacji AND i OR ustalone dla wejścia A = 0
bias_B    db  0                   ; odchylenie dla operacji AND i OR ustalone dla wejścia B = 0

.CODE
start:
main  PROC
  xor  rax, rax                   ; zerujemu rejestry: RAX, RCX, R9, R10
  xor  rcx, rcx
  xor  r9, r9
  xor  r10, r10
  mov  rbx, 0101000101000000h    ; dane wejściowe - w opisywanym przypadku mamy 4 możliwości
                                 ; dla wejść AB - 11, 01, 10, 00. Aby zaoszczędzić pamięć 
                                 ; posłużyłem się rejestrem 64 bitowym, ponieważ wszystkie
                                 ; próbki (przy założeniu, że każda z nich zajmuje 16
                                 ; bitów, a każde wejście 8) właśnie tyle miejsca wymagają.
                                 ; każda próbka składa się z 2 bajtów, po jednym dla wejść A i B
@run_neuron:
  xor  r8, r8                    ; wejście A - oba neurony obsługujemy jedną parą wejść AB
  mov  cl, BYTE PTR [weight_A]   ; waga dla wejścia A neuronów AND i OR
  mov  al, bl                    ; pobieramy pierwszy bajt z próbki dla wejścia A,
  imul cl                        ; mnożenie wagi z bajtem wejściowym
  add  al, BYTE PTR [bias_A]     ; dodajemy odchylenie
  add  r8, rax                   ; wynik zapisujemy w R8 - rejestrze wynikowym dla neuronów

  shr  rbx, 08h                  ; przechodzimy do następnego bajtu z próbki

  mov  cl, BYTE PTR [weight_B]   ; waga dla wejścia B neuronów AND i OR
  mov  al, bl                    ; pobieramy drugi bajt z próbki dla wejścia B,
  imul cl                        ; mnożenie wagi z bajtem wejściowym
  add  al, BYTE PTR [bias_B]     ; dodajemy odchylenie
  add  r8, rax                   ; wynik dodajemy do wyniku z wejścia A w R8
 
  shr  rbx, 08h                  ; przechodzimy do następnej próbki
 
@check_and: 
  cmp  r8, 2                     ; w R8 trzymamy wynik, aby porównać go z poziomem aktywacji
                                 ; neuronu. Dla bramki AND poziom ten wynosi 2
  jge  @and_active               ; jeśli w R8 jest wartość >= 2 to aktywujemy neuron AND
@check_or:
  cmp  r8, 1                     ; w R8 trzymamy wynik, aby porównać go z poziomem aktywacji
                                 ; neuronu. Dla bramki OR poziom ten wynosi 1
  jge  @or_active                ; jeśli w R8 jest wartość >= 1 to aktywujemy neuron OR
  jmp  @next                     ; po zakończeniu analizy sprawdzamy czy jest kolejna próbka
 
@and_active: 
  inc  r9                        ; aktywacja neuronu AND w naszym przypadku to zwiększenie R9 o 1
                                 ; w ten sposób policzymy ile razy aktywował się neuron, a dla
                                 ; operacji AND powinien zrobić to tylko jeden raz - tabelka
  jmp  @check_or                 ; tutaj przechodzimy do sprawdzenia czy aktywować neuron OR
                                 ; dla takich samych parametrów wejściowych
@or_active:
  inc  r10                       ; aktywacja neuronu OR w naszym przypadku to zwiększenie R10 o 1
                                 ; w ten sposób policzymy ile razy aktywował się neuron, a dla
                                 ; operacji OR powinien zrobić to trzy razy - tabelka
@next:
  cmp  rbx, 0                    ; przesuwanie RBX o 8 bitów powoduje przechodzenie do kolejnej
                                 ; wartości wejść AB i po 4 próbkach rejestr się wyzeruje. Dzieje
                                 ; się tak dlatego, że dane wejściowe zostały zapisane w
                                 ; kolejności 11, 01, 10, 00 - inny zapis spowodowałby błędne
                                 ; działanie programu - tzn. nie wszystkie próbki zostały by 
                                 ; sprawdzone
  jnz  @run_neuron               ; jeśli RBX > 0 to przechodzimy do następnej próbki
  ret
main ENDP
END

W prezentowanym przykładzie jest sporo niepotrzebnego kodu, bo waga = 1, a odchylenie = 0. Operacje odpowiedzialne za ich obsługę można śmiało usunąć. Etykiety również można poukładać w taki sposób. aby zoptymalizować działanie programu. Kod został napisany tak, aby pokazać przebieg procedury. Ostatecznie śledząc program powinniśmy otrzymać, przed wyjściem z niego, wartości R9 = 1, a R10 = 3. Analizując kod zobaczymy, które próbki aktywowały neurony AND i OR. Wszystko wyszło zgodnie z tabelką działania bramek logicznych bez użycia instrukcji and i or w kodzie.

Brak komentarzy:

Prześlij komentarz