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.
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.
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.
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:
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.
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:
Trenowanie przebiega analogicznie jak dla przypadku jednowymiarowego. Tym razem możemy nanieść wyniki na płaszczyznę. Uzyskany wynik pokazuje rysunek 3.
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.
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