MNIST dataset - sieci neuronowe, część 0 (przygotowanie)
Zaczynamy cykl związany z sieciami neuronowymi. Zaczniemy od przygotowania danych. W kilku kolejnych postach posłużymy się danymi z ręcznie pisanymi liczbami. Zbiór ten nazywa się MNIST i jest bardzo popularny w świecie datalogów. Można go pobrać z tego miejsca. Klasyfikacja ręcznie pisanych liczb to takie hello world w sieciach neuronowych. W kolejnych częściach:
- zbudujemy narzędziownię w R (w oparciu o bibliotekę Keras)
- zbudujemy prostą sieć neuronową i sprawdzimy jakie daje efekty
- zbudujemy sieć konwolucyjną (CNN) i sprawdzimy czy daje lepsze efekty, spróbujemy ją również rozbudować
Pliki jednak nie są w super przyjaznej formie (na przykład w Excelu nie da się ich otworzyć) - na początek więc przetworzymy je na czytelną formę. Zgodnie z opisem zbiór dzieli się na cztery pliki. Mamy więc dwie (dane treningowe i testowe) paczki: plik określający literki oraz dane opisujące ich wygląd. Pliki są binarne, przetworzymy je na pliki CSV.
library(tidyverse)
library(gridExtra)
Ściągamy pliki .GZ i rozpakowujemy je. W dalszej części wpisu zajmiemy się tylko plikiem z danymi treningowymi. Sama konwersja danych testowych jest identyczna.
train_labels_path <- "data/train-labels.idx1-ubyte"
train_images_path <- "data/train-images.idx3-ubyte"
train_labels <- read_file_raw(train_labels_path)
train_images <- read_file_raw(train_images_path)
Pierwszy krok to labelki, czyli jaką liczbę pokazują dane? To prosta sprawa - wystarczy poszczególne bajty zamienić na wartości liczbowe:
labels <- train_labels[9:length(train_labels)] %>% as.numeric()
I już! Kolejne elementy tablicy to po prostu liczby mówiące nam co widzimy na obrazku. Obrazki są nieco trudniejsze w obsłudze. Każda liczba zapisana jest w postaci obrazka 28 na 28 pikseli. Początek pliku to jakieś dane informacyjne (opisane na stronie ze zbiorem), a później mamy 60 tysięcy ciągów 784 bajtów (28*28) opisujących wygląd pisanych liczb (60 tysięcy). Wybieramy zatem po te 784 bajtów, układamy je w wiersz, na początku wiersza dodajemy odpowiedni label i całość upychamy w wielką tablicę:
train_all <- tibble()
for(n_pic in 0:length(labels)) {
pixels <- train_images[(17+n_pic*28*28):(17+(n_pic+1)*28*28-1)] %>% as.numeric()
data_row <- c(labels[n_pic+1], pixels)
train_mat <- matrix(data_row, nrow = 1) %>% as_tibble()
colnames(train_mat) <- c("label", paste0("pixel", 0:783))
train_all <- bind_rows(train_all, train_mat)
}
Proces jest pracochłonny, dlatego od razu zapisujemy wynik do pliku CSV:
write_csv(train_all, path = "data/train_converted.csv", col_names = TRUE)
Podobne zabiegi trzeba dokonać dla danych testowych (odpowiednio modyfikując ścieżkę do plików i przesunięcie - w danych testowych opisujących obraz nie ma kilku bajtów). Zobaczmy ile jest liczb w danych treningowych?
train_all %>%
count(label) %>%
ggplot() +
geom_col(aes(label, n), fill = "lightgreen", color = "grey50") +
scale_x_continuous(breaks = 0:9)
Rozkład jest mniej więcej równomierny.
Na razie mamy do czynienia z danymi w postaci cyferek, ale czy możemy je pokazać? Napiszemy funkcję, która z całych danych treningowych wybierze jeden wiersz i jego zawartość pokaże w formie graficznej.
Jedyne co trzeba zrobić to ciąg 784 liczb (o wartości od 0 do 255 co odpowiada stopniowi zaczernienia punktu) złożyć w macierz 28x28 punktów i ją pokazać. Dodatkowo, aby obraz nie był odbity w pionie (punkt 0,0 jest w lewym dolnym rogu dla wykresów, ale dla obrazków jest to lewy górny róg) musimy go obrócić.
showDigit <- function(dataset=train_all, digit_number=1) {
# wybieramy wskazany wiersz i składamy w macierz 28x28 punktów
matrix1 <- dataset[digit_number, 2:dim(dataset)[2]] %>%
as.numeric() %>%
matrix(ncol = 28, byrow = T)
# miejsce na odwróconą macierz
matrix2 <- matrix(rep(0, 28*28), 28, 28)
# odwrócenie osi Y (punkt 0,0 na górze, a nie na dole)
for(i in 1:28)
for(j in 1:28)
matrix2[28-i, j] <- matrix1[i, j]
# pokazujemy obrazek, w dodatku macierz musimy transponować (zamienić wiersze na kolumny)
image(t(matrix2), col = rev(topo.colors(255)), axes = FALSE)
# dodajemy tytuł, który powie nam co za liczbę oglądamy :)
title(dataset[digit_number,1] %>% as.numeric())
}
Zamiast podwójnego fora można użyć kombinacji apply() z rev() . Pokażmy jakąś losową liczbę:
showDigit(train_all, sample(nrow(train_all), 1))
Działa.
Teraz zróbmy coś podobnego, ale z pomocą ggplot. I żeby nie było tak samo - weźmiemy 12 losowo wybranych szóstek:
# miejsce na kolejne obrazki szóstek
plots <- list()
# wybieramy 12 szóstek
for(i in 1:12) {
# weź losowy wiersz z danych, ale opisujący szóstkę
digits_data <- train_all[train_all$label == 6, ] %>% sample_n(1)
# do listy wykresów dodajemy to co uzyskamy:
plots[[i]] <- digits_data %>%
# dane z szerokich przekształcamy na długie
gather() %>%
# usuwamy informację o labelce - zostanie nam 784 wierszy
filter(key != "label") %>%
# numerujemy je kolejno
mutate(row = row_number()-1) %>%
# zapisujemy numer kolumny i wiersza - czyli części:
# całkowitą dzielenia numeru punktu przez 28 i resztę z tego dzielenia
mutate(col = row %% 28, row = row %/% 28) %>%
# rysujemy całość
ggplot() +
geom_tile(aes(col, 28-row, fill=value), show.legend = FALSE) +
scale_fill_gradient(low = "white", high = "black") +
coord_equal() +
theme_void() +
theme(plot.background = element_rect(fill = "gray80"))
}
# wszystkie wygenerowane obrazy układamy na jednym obrazku
do.call("grid.arrange", c(plots, ncol = 4, nrow = 3))
Jak widać szóstki są różne, proces kategoryzacji danych testowych będzie zatem dość trudny. Ale to w kolejnej części.
Spróbujmy zobaczyć uśrednione wartości dla każdej z liczb. Wykorzystamy w znacznym stopniu powyższy kod. Zamiast rysować pojedyncze szóstki - przygotujemy 10 wierszy (liczby od 0 do 9) z uśrednionymi danymi:
plots <- list()
for(i in 0:9) {
# uśredniamy dane dla kolejnych liczb
digits_data <- train_all[train_all$label == i, ] %>% map_df(mean)
# analogicznie jak dla szóstek je rysujemy
plots[[i+1]] <- digits_data %>%
gather() %>%
filter(key != "label") %>%
mutate(row = row_number()-1) %>%
mutate(col = row %% 28, row = row %/% 28) %>%
ggplot() +
geom_tile(aes(col, 28-row, fill=value), show.legend = FALSE) +
scale_fill_gradient(low = "white", high = "black") +
coord_equal() +
theme_void() +
theme(plot.title = element_text(hjust = 0.5, size = 15),
plot.background = element_rect(fill = "gray80"))
plots[[i+1]] <- plots[[i+1]] + labs(title = paste("Liczba ", i))
}
# pokazujemy całość
do.call("grid.arrange", c(plots, ncol = 5, nrow = 2))
Widać, że niektóre z liczb są w miarę “zwarte”, w pewnym sensie można wyróżnić miejsca, przez które musi przechodzić linia w każdym z wariantów zapisu danej liczby. To cenna obserwacja, zapamiętajcie ją.
A na koniec niespodzianka: zamiast przygotowywać dane w CSV z oryginalnego zbioru MNIST można je po prostu ściągnąć z Kaggle.com. Dalsze części opierać się będą właśnie na danych z Kaggle.