Wbudowane sieci neuronowe w STM32 (4). Autokoder

Wbudowane sieci neuronowe w STM32 (4). Autokoder

W poprzedniej części cyklu artykułów nauczyliśmy sieć neuronową rozpoznawać gesty. Jako dane wejściowe posłużyły nam przebiegi zebrane za pomocą czujnika odległości. Teraz użyjemy tych danych przy eksperymentach z autokoderami.

Czym jest autokoder?

Autokoder (autoencoder) to specjalny rodzaj sieci neuronowej pozwalający na kompresję wysokowymiarowych przykładów do niskowymiarowego kodu. Jego schemat został pokazany na rysunku 1. Składa się z dwóch części: kodera (encoder) i dekodera (decoder). Zadaniem pierwszego jest kompresja przykładów do kodu. Następnie dekoder stara się wykonać odwrotne zadanie: na podstawie kodu musi odtworzyć przykład. Funkcją celu używaną do treningu jest minimalizacja błędu między przykładem, a rekonstrukcją. Gdyby kod miał taki sam wymiar, jak przykład, to sieć nie tworzyłaby niczego ciekawego. Jednak, gdy jest on mniejszy, w wyniku treningu otrzymamy niskowymiarową reprezentację, która powinna opisywać najważniejsze cechy przykładu. Jest to uczenie nienadzorowane - sieć nie dostaje żadnych dodatkowych informacji poza zbiorem danych. Możemy także użyć samego dekodera, do generowania nowych przykładów podobnych to tych zastosowanych do treningu.

Rysunek 1. Schemat autokodera

Implementacja

Eksperymenty wykonamy na zbiorze danych zebranych w poprzednim odcinku, dotyczących gestów. Cały kod znajdziemy w notatniku [1]. Na początku znajdują się dane. Jedyną różnicą względem poprzedniego odcinka jest znormalizowanie danych z przetwornika ADC poprzez podzielenie ich przez maksymalną możliwą wartość dla 12-bitowego przetwornika czyli 4096.

Implementacje samego autokodera pokazuje listing 1. Składa się on z dwóch sieci neuronowych: encoder i decoder. Są one połączone razem w funkcji call, która będzie używana w fazie treningu. Przyjąłem, że koder składa się z jednej warstwy ukrytej ReLU i wyjściowej warstwy liniowej. Podobnie wygląda dekoder: najpierw warstwa ukryta ReLu, a później wyjściowa warstwa sigmoidalna. Wybór padł na nią, ponieważ jej wartość wyjściowa zawiera się pomiędzy 0, a 1 - czyli w takim samym przedziale jak wartości wejściowe. Do konfiguracji użyto dwóch parametrów - hidden to liczba neuronów w warstwach ukrytych, a latent_dim to długość wektora kodu.

Listing 1. Implementacja autokodera

class Autoencoder(Model):
def __init__(self, latent_dim, hidden):
super(Autoencoder, self).__init__()
self.latent_dim = latent_dim
self.hidden = hidden
self.encoder = tf.keras.Sequential([
layers.Dense(hidden, activation=’relu’),
layers.Dense(latent_dim),
])
self.decoder = tf.keras.Sequential([
layers.Dense(hidden, activation=’relu’),
layers.Dense(N, activation=’sigmoid’),
])

def call(self, x):
encoded = self.encoder(x)
decoded = self.decoder(encoded)
return decoded

Eksperymenty

W pierwszym eksperymencie wymiar kodu to 1. Oznacza to, że cały wektor z zapisanym gestem zostanie zamieniony na pojedynczą liczbę. Program realizujący trening przedstawia listing 2. Najpierw tworzymy instancję klasy, a następnie wywołujemy trening analogicznie jak w poprzednich odcinkach.

Listing 2. Tworzenie i trening autokodera

autoencoder_1d = Autoencoder(latent_dim, hidden)
autoencoder_1d.compile(optimizer=’adam’, loss=losses.MeanSquaredError())
autoencoder_1d.fit(data_t, data_t, epochs=1000, shuffle=True, validation_data=(data_v, data_v), verbose=0)
autoencoder_1d.evaluate(np.array(data_t), np.array(data_t), verbose=2)
autoencoder_1d.evaluate(np.array(data_v), np.array(data_v), verbose=2)
autoencoder_1d.save("autoencoder_1d.pb")

Przydatna jest funkcja save, która pozwala na zapis parametrów modelu. Później można je załadować za pomocą polecenia:

autoencoder_1d = tf.keras.models.load_model("autoencoder_1d.pb")

Aby sprawdzić co osiągnęliśmy, wyliczymy jakie liczby zostały przyporządkowane gęstą z różnych kategorii. Przedstawia je wykres z rysunku 2.

Rysunek 2. Wartości uzyskane dla różnych gestów

Oś Y przedstawia uzyskaną wartość, a oś X oznacza rodzaj gestu:

  • 0 - pusty,
  • 1 - z dołu do góry,
  • 2 - z góry na dół,
  • 3 - koziołkowanie,
  • 4 - machanie.

Widzimy, że gesty tego samego typu tworzą skupiska, ale jednak nachodzą one na siebie. Wyznaczenie granic rozdzielających poszczególne typy wydaje się tutaj trudne. Sprawdźmy więc, co się stanie, gdy zwiększymy wymiar kodu do 2, czyli pojedynczy wektor zostanie opisany parą liczb. Odpowiedni autokoder stworzymy rozkazem:

autoencoder_2d = Autoencoder(2, 20)

Trenowanie przebiega analogicznie jak dla przypadku jednowymiarowego. Tym razem możemy nanieść wyniki na płaszczyznę. Uzyskany wynik pokazuje rysunek 3.

Rysunek 3. Kody uzyskane dla różnych gestów

Tym razem możemy już wydzielić część płaszczyzny odpowiadającą różnym rodzajom gestów. Mimo, że sam koder "nie wiem nic" o znaczeniu poszczególnych wektorów, to jednak w otrzymanym kodzie są one rozdzielone. Można użyć enkodera do wstępnego rozdzielania próbek, a następnie wykorzystać prostszy algorytm do klasyfikacji.

Dekoder

Na razie używaliśmy tylko kodera, sprawdzimy więc jeszcze do czego może nam się przydać druga część, czyli dekoder. Wybrałem 100 różnych punktów, równo rozmieszczonych w kwadracie o skrajnych bokach (-5,-5) i (5,5). Jak widzimy na rysunku 3 w tym obszarze znajdziemy każdy rodzaj gestu. Gdy poszczególne punkty potraktujemy jako wejścia dla dekodera otrzymamy przebiegi takie, jak na rysunku 4. Wygenerowaliśmy w ten sposób nowe przebiegi, jednak podobne do tych pochodzących z danych uczących. Przeglądając je odnajdziemy wzorce wcześniej zarejestrowanych gestów.

Rysunek 4. "Gesty" wygenerowane przez dekoder dla różnych wartości kodu

Jednym z zastosowań tak wygenerowanych danych jest tworzenie nowych przykładów szkoleniowych dla naszej sieci. Można na przykład ręcznie przydzielić kategorię nowo powstałym przykładom i w ten sposób wypełnić miejsca przestrzeni kodu, gdzie mamy mniej punktów.

Podsumowanie

W tym odcinku nie wykonaliśmy żadnego eksperymentu na mikrokontrolerze. Poznaliśmy jednak ciekawą strukturę sieci jaką są autokodery. W dalszych częściach porównamy jak z zadaniem rozpoznawania gestów poradzi sobie sieć rekurencyjna - jaką osiągnie dokładność oraz jak dużych nakładów obliczeniowych będzie wymagała.

Rafał Kozik
rafkozik@gmail.com

Bibliografia

  1. http://bit.ly/3FjJQfn
Artykuł ukazał się w
Elektronika Praktyczna
kwiecień 2023
Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik grudzień 2024

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio listopad - grudzień 2024

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje listopad - grudzień 2024

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna grudzień 2024

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich grudzień 2024

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów