Wbudowane sieci neuronowe w STM32 (2). Rozpoznawanie kształtów

Wbudowane sieci neuronowe w STM32 (2). Rozpoznawanie kształtów

Tym razem spróbujemy uruchomić sieć realizującą ciekawsze zadanie. Nauczymy ją rozróżniać trzy kształty. Czujnikiem będzie matryca 36 fototranzystorów. Będziemy musieli przygotować dane do nauki, wytrenować sieć i na końcu zainstalować ją w mikrokontrolerze.

Układ elektroniczny

Czujnikiem, którym się posłużymy, będzie matryca trzydziestu sześciu fototranzystorów. Jej schemat wraz z połączeniami do płytki Nucleo L476RG prezentuje rysunek 1. Wszystkie kolektory fototranzystorów z pojedynczego wiersza są podłączone do wejścia przetwornika analogowo-cyfrowego oraz podciągnięte do 3,3 V za pomocą rezystora o rezystancji 100 kΩ. Emitery są połączone w kolumnach. Każda z nich jest podłączona do pinu mikrokontrolera pracującego jako wyjście. Kolumna jest aktywna, gdy na wyjściu panuje stan niski.

Rysunek 1. Połączenie fototranzystorów do płytki Nucleo

Do płytki zostały także podłączone trzy diody LED, które służą do pokazania jaki kształt został wykryty. Rysunek 2 pokazuje opis wyjść fototranzystora. Matryca została zlutowana na uniwersalnej płytce PCB. Gotowy model prezentuje fotografia tytułowa

Rysunek 2. Opis wejść fototranzystora

Zbieramy dane

Zanim przystąpimy do trenowania sieci musimy zebrać dane. W repozytorium [1] znajdziemy program, który w pętli odczytuje stan fototranzystorów i wysyła je poprzez port szeregowy jako tablicę liczb w formacie JSON. Potrzebne są nam jeszcze kształty, które będziemy rozpoznawać. Ja zdecydowałem się na kwadrat o boku 4 cm, koło o średnicy 4 cm oraz trójkąt równoboczny o boku 4 cm. Kształty zostały wycięte z grubego kartonu tak, jak pokazano na fotografii 1.

Fotografia 1. Kształty wycięte z kartonu, które posłużą do trenowania sieci

Do odczytu danych została przygotowana aplikacja w formie strony WWW, której wygląd pokazuje rysunek 3.

Rysunek 3. Aplikacja do odczytu danych

Znajdziemy ją w repozytorium [1] w folderze gui oraz pod adresem [2]. Jej działanie zostało sprawdzone w przeglądarce Chrome. Po jej uruchomieniu klikamy przycisk Connect i z listy wybieramy port COM naszego projektu. Gdy port zostanie prawidłowo otwarty, zostanie pokazany aktualny stan czujników. Aby zapisać przykład szkoleniowy wybieramy odpowiednią etykietę: Pusty, Kwadrat, Koło, albo Trójkąt i klikamy Add. Struktura danych z oznaczonymi przykładami będzie pojawiać się na dole strony. Jest to słownik przechowujący dwie tablice: labels zawiera etykiety, a data odpowiadające im stany matrycy.

Ja do szkolenia przygotowałem po 50 przykładów dla każdej kategorii. Wbrew pozorom pusta matryca też jest osobnym przypadkiem, który chcemy wykryć. Bez niego sieć próbowałaby zawsze dopasować którąś z pozostałych trzech możliwości. Postarajmy się, aby były one jak najbardziej różnorodne. Chcemy uwzględnić położenie figury w różnych częściach matrycy i pod różnym kątem (fotografia 2).

Fotografia 2. Trenowanie sieci

Jeżeli chcemy aby sieć pracowała dobrze przy różnych natężeniach oświetlenia, także powinniśmy to uwzględnić w danych uczących.

Należy także uważać, aby w czasie zapisywania przykładów nie zasłaniać matrycy ręką. Strukturę z danymi kopiujemy i tymczasowo zapisujemy w pliku tekstowym.

Trenujemy sieć

Szkicownik z kodem użytym do trenowania sieci znajdziemy w [3]. Na początku przypisujemy zebrane dane do zmiennej data. Jeżeli nie chcemy wklejać danych bezpośrednio do kodu możemy stworzyć osobny plik .py i dołączyć go do projektu. Ja jednak pozostałem przy pierwszej opcji. Aby przekonać się, co zawierają nasze dane wyrysujemy kilka przykładów za pomocą kodu z listingu 1.

Listing 1. Kod wyświetlający kilka z zebranych przez nas danych.

fig = plt.figure(figsize=(10, 40))

n = [0, 71, 121, 171]

for i in range(4):
fig.add_subplot(1, 4, i+1)
plt.imshow(data[“data”][n[i]],
vmin=0, vmax=255, cmap=matplotlib.cm.hot)
plt.grid(False)
plt.axis(‘off’)
plt.title(data[“labels”][n[i]])
plt.show()

Do zmiennej n przypisujemy numery elementów z tablicy, które chcemy zobaczyć. Ja wybrałem je tak, aby pokazać po jednym obiekcie każdego rodzaju. Przykładowy wynik widzimy na rysunku 4.

Rysunek 4. Przykładowe dane dla każdej z kategorii

Przetwornik analogowo-cyfrowy zwróci nam wyniki z przedziału od 0 do 255. Skalujemy je, aby do treningu użyć wartości zmiennoprzecinkowych z zakresu od 0 do 1. Następnie dzielimy nasze dane na dwa zbiory: większy treningowy oraz mniejszy testowy. Pierwszy zostanie użyty do nauki, a drugi do oceny uzyskanej sieci.

Struktura sieci jest pokazana na listingu 2.

Listing 2. Struktura sieci

model = tf.keras.models.Sequential([
tf.keras.layers.Flatten(input_shape=(6, 6)),
tf.keras.layers.Dense(64, activation=’relu’), #64
tf.keras.layers.Dropout(0.2),
tf.keras.layers.Dense(4)
])

model.fit(x_train, y_train, epochs=400)

model.evaluate(np.array(x_test), np.array(y_test), verbose=2)

1/1 - 0s - loss: 0.0960 - accuracy: 0.9500 - 19ms/epoch - 19ms/step
[0.09596162289381027, 0.949999988079071]

Wejściem jest wektor długości 36: po jednej liczbie z każdego czujnika. Następnie mamy warstwę ukrytą złożoną z 64 neuronów z funkcją aktywacją ReLU. Warstwa wyjściowa składa się z 4 neuronów, odpowiadających czterem możliwym stanom: pusty, kwadrat, trójkąt i koło. Wykonanych zostało 400 iteracji algorytmu szkolenia:

model.fit(x_train, y_train, epochs=400)

Na końcu sprawdzamy uzyskane dopasowanie na zbiorze testowym:

model.evaluate(np.array(x_test), np.array(y_test), verbose=2)

Ja uzyskałem wynik:

1/1 - 0s - loss: 0.0960 - accuracy: 0.9500 - 19ms/epoch - 19ms/step[0.09596162289381027, 0.949999988079071]

Warto przeprowadzić eksperymenty dla innej konfiguracji sieci oraz czasu szkolenia. Możemy także trochę podejrzeć na jakie kształty reagują poszczególne neurony wyrysowując wagi dla każdego z neuronów warstwy pośredniczącej. W przybliżeniu odpowiadają one kształtowy na jaki czuły jest poszczególny neuron. Warstwa wyjściowa otrzymuje wyniki tych częściowych dopasowań i na ich podstawie “podejmuje decyzję”.

Ostatni element kodu odpowiada za konwersję modelu to TFLite i zapisaniu go do pliku: analogicznie jak w przykładzie z funkcją XOR.

Mikrokontroler

Projekt dla mikrokontrolera tworzymy w środowisku CubeMX analogiczne jak w pierwszym eksperymencie. Tym razem konfiguracja jest bardziej rozbudowana, gdyż obejmuje też przetwornik analogowo cyfrowy. Znajdziemy ją w repozytorium [4].

W pętli głównej najpierw wykonujemy odczyt wartości z kolejnych fototranzystorów, skalujemy je dzieląc przez 255.0f i zapisujemy w dwuwymiarowej tablicy float. Następnie wywołujemy funkcję n = MX_X_CUBE_AI_Process(a). Zwraca ona liczbę odpowiadającą wykrytemu kształtowi, na podstawie której zaświecona zostaje odpowiednia dioda. Samą implementację wywołanej funkcji znajdziemy w pliku X-CUBE-AI/App/app_x-cube-ai.c oraz na listingu 3.

Listing 3. Implementacja funkcji MX_X_CUBE_AI_Process

uint8_t MX_X_CUBE_AI_Process(float a[6][6]){
/* USER CODE BEGIN 6 */
ai_i32 batch;
uint8_t n;
float max;
float nn_output[AI_TF_MATRIX_OUT_1_SIZE];
ai_input->data = a;
ai_output->data = nn_output;

batch = ai_tf_matrix_run(tf_matrix, ai_input, ai_output);

n = 0;
for(uint8_t i=1; i < 4; i++) {
if (nn_output[i] > nn_output[n]) {
n = i;
}
}

printf("%d, %f, %f, %f, %f\r\n", n, nn_output[0],
nn_output[1], nn_output[2], nn_output[3]);

return n;
/* USER CODE END 6 */
}

Najpierw wypełniamy struktury ai_input i ai_output wskaźnikami do zmiennych przechowujących dane. Następnie wywołujemy funkcję ai_tf_matrix_run, która przeprowadza obliczenia sieci neuronowej. Na końcu musimy jeszcze znaleźć, dla którego wyjścia została zwrócona najwyższa wartość. W ten sposób rozstrzygamy jaki kształt został wykryty. Informacja ta zostanie zwrócona do pętli głównej. Dodatkowo na port szeregowy są wysyłane wszystkie informacje. Po zaprogramowaniu mikrokontrolera możemy przetestować działanie sieci neuronowej na danych zbieranych z czujników na żywo. Działanie gotowego układu pokazuje film [5].

Podsumowanie

W artykule zostały pokazane kolejne etapy, które są charakterystyczne dla implementacji sieci neuronowych:

  • zbieranie danych,
  • trening sieci,
  • testowanie gotowej sieci.

Najtrudniejszy i najbardziej pracochłonny jest pierwszy z nich. Aby zapewnić prawidłowe działanie musimy dostarczyć dane podobne do tych, z którymi sieć spotka się w swoje pracy. W naszym przypadku będzie to zwrócenie uwagi na różne położenie kształtów oraz różne warunki oświetlenia. Mam nadzieje, że ten projekt będzie inspiracją dla czytelników do używania sieci w własnych rozwiązaniach dla różnych rodzajów danych wejściowych.

Rafał Kozik
rafkozik@gmail.com

Bibliografia:

  1. https://gitlab.com/rysino_ai/ft_matrix_reader
  2. https://rysino.com/ft_matrix/
  3. http://bit.ly/3XzJpo9
  4. https://gitlab.com/rysino_ai/ft_matrix_net
  5. https://youtu.be/eo8upI5uEN4
Artykuł ukazał się w
Elektronika Praktyczna
luty 2023

Elektronika Praktyczna Plus lipiec - grudzień 2012

Elektronika Praktyczna Plus

Monograficzne wydania specjalne

Elektronik kwiecień 2024

Elektronik

Magazyn elektroniki profesjonalnej

Raspberry Pi 2015

Raspberry Pi

Wykorzystaj wszystkie możliwości wyjątkowego minikomputera

Świat Radio maj - czerwiec 2024

Świat Radio

Magazyn krótkofalowców i amatorów CB

Automatyka, Podzespoły, Aplikacje kwiecień 2024

Automatyka, Podzespoły, Aplikacje

Technika i rynek systemów automatyki

Elektronika Praktyczna kwiecień 2024

Elektronika Praktyczna

Międzynarodowy magazyn elektroników konstruktorów

Elektronika dla Wszystkich maj 2024

Elektronika dla Wszystkich

Interesująca elektronika dla pasjonatów