Często słyszy się, że modele uczenia głębokiego są jak „czarne skrzynki”: uczą się reprezentacji, których ekstrakcja jest trudna do wykonania, i trudno jest je przedstawić w formie czytelnej z punktu widzenia człowieka. Stwierdzenie to dla niektórych modeli uczenia głębokiego można uznać za częściowo prawdziwe, ale z pewnością nie jest ono prawdziwe w przypadku pracy z konwolucyjnymi sieciami neuronowymi. Reprezentacje uczone przez konwolucyjne sieci neuronowe można z łatwością przedstawić w sposób graficzny. Wynika to w dużej mierze z tego, że są to reprezentacje koncepcji graficznych. Od 2013 r. rozwinięto wiele technik wizualizacji i interpretowania takich reprezentacji. Nie będę opisywał ich wszystkich, ale przedstawię Ci trzy najprostsze rozwiązania tego typu:
Działanie pierwszej techniki — wizualizacji aktywacji — sprawdzimy na przykładzie małej sieci konwolucyjnej wytrenowanej od podstaw podczas podejmowania próby rozwiązania klasyfikacji obrazów psów i kotów w podrozdziale 5.2. Działanie dwóch kolejnych technik sprawdzimy na modelu VGG16 z podrozdziału 5.3.
Wizualizacja pośrednich aktywacji polega na wyświetlaniu map cech, które są generowane przez różne warstwy konwolucyjne i łączące sieci na podstawie określonych danych wejściowych (dane wyjściowe warstwy są często określane mianem aktywacji, wynikiem zwracanym przez funkcję aktywacji). Umożliwia ona podgląd tego, jak dane wejściowe są rozkładane przez różne filtry wyuczone przez sieć. Chcemy dokonać wizualizacji map cech w przestrzeni trzech wymiarów: wysokości, szerokości i głębi (kanały). Każdy kanał koduje względnie niezależne cechy, a więc w celu wykonania poprawnej wizualizacji map tych cech należy przedstawić zawartość każdego kanału w formie niezależnego dwuwymiarowego obrazu. Zacznijmy od załadowania modelu zapisanego podczas rozwiązywania problemu z podrozdziału 5.2.
r
r library(keras) model <- load_model_hdf5(_and_dogs_small_2.h5) summary(model) # As a reminder.
______________________________________________________________________________________
Layer (type) Output Shape Param #
======================================================================================
conv2d_21 (Conv2D) (None, 148, 148, 32) 896
______________________________________________________________________________________
max_pooling2d_21 (MaxPooling2D) (None, 74, 74, 32) 0
______________________________________________________________________________________
conv2d_22 (Conv2D) (None, 72, 72, 64) 18496
______________________________________________________________________________________
max_pooling2d_22 (MaxPooling2D) (None, 36, 36, 64) 0
______________________________________________________________________________________
conv2d_23 (Conv2D) (None, 34, 34, 128) 73856
______________________________________________________________________________________
max_pooling2d_23 (MaxPooling2D) (None, 17, 17, 128) 0
______________________________________________________________________________________
conv2d_24 (Conv2D) (None, 15, 15, 128) 147584
______________________________________________________________________________________
max_pooling2d_24 (MaxPooling2D) (None, 7, 7, 128) 0
______________________________________________________________________________________
flatten_6 (Flatten) (None, 6272) 0
______________________________________________________________________________________
dropout_3 (Dropout) (None, 6272) 0
______________________________________________________________________________________
dense_11 (Dense) (None, 512) 3211776
______________________________________________________________________________________
dense_12 (Dense) (None, 1) 513
======================================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
______________________________________________________________________________________
Teraz uzyskamy obraz wejściowy — zdjęcie kota niewchodzące w skład treningowego zbioru danych.
r
r img_path <- ~/Downloads/cats_and_dogs_small/test/cats/cat.1700.jpg
# We preprocess the image into a 4D tensor img <- image_load(img_path, target_size = c(150, 150)) img_tensor <- image_to_array(img) img_tensor <- array_reshape(img_tensor, c(1, 150, 150, 3)) # Remember that the model was trained on inputs # that were preprocessed in the following way: img_tensor <- img_tensor / 255 dim(img_tensor)
[1] 1 150 150 3
Czas wyświetlić zdjęcie:
r
r plot(as.raster(img_tensor[1,,,]))
W celu wyciągnięcia map cech, którym chcemy się przyjrzeć, utworzymy model Keras, który będzie przyjmował na wejściu wsady składające się z obrazów i będzie generował na swoim wyjściu aktywacje wszystkich warstw konwolucyjnych i warstw łączących. W tym celu skorzystamy z funkcji keras_model przyjmującej dwa argumenty: tensor wejściowy (lub listę tensorów wejściowych) i tensor wyjściowy (lub listę tensorów wyjściowych). Utworzona w ten sposób klasa jest modelem Keras, który podobnie do opisanych wcześniej modeli sekwencyjnych (funkcja keras_sequential_model()) mapuje określone dane wejściowe na określone dane wyjściowe. Model ten od modelu keras_sequential_model odróżnia to, że umożliwia on obsługę wielu wyjść. Więcej informacji na temat funkcji keras_model znajdziesz w podrozdziale 7.1.
r
r # Extracts the outputs of the top 8 layers: layer_outputs <- lapply(model\(layers[1:8], function(layer) layer\)output) # Creates a model that will return these outputs, given the model input: activation_model <- keras_model(inputs = model$input, outputs = layer_outputs)
Model ten po skierowaniu do jego wejścia obrazu zwraca wartości aktywacji warstwy oryginalnego modelu. To pierwszy przykład modelu zwracającego wiele danych wyjściowych, który został zaprezentowany w tej książce. Dotychczas przedstawiałem modele, które przyjmowały dokładnie jeden obiekt wejściowy i zwracały jeden obiekt wyjściowy, ale ogólnie rzecz biorąc, model może przyjmować dowolną liczbę obiektów wejściowych i generować dowolną liczbę obiektów wyjściowych. Opisywany obecnie model przyjmuje jeden obiekt wejściowy i generuje osiem obiektów wyjściowych (po jednym dla każdej warstwy aktywacji).
r
r # Returns a list of five arrays: one array per layer activation activations <- activation_model %>% predict(img_tensor)
Oto przykład aktywacji pierwszej warstwy konwolucyjnej wygenerowanej na podstawie wybranego przez nas zdjęcia kota:
r
r first_layer_activation <- activations[[1]] dim(first_layer_activation)
[1] 1 148 148 32
Jest to mapa cech o rozmiarach 148148 składająca się z 32 kanałów. Przedstawmy wybrane kanały w formie graficznej. Zacznijmy od zdefiniowania funkcji R generującej wykres danych kanału:
r
r plot_channel <- function(channel) { rotate <- function(x) t(apply(x, 2, rev)) image(rotate(channel), axes = FALSE, asp = 1, col = terrain.colors(12)) }
Wizualizacja piątego kanału:
r
r plot_channel(first_layer_activation[1,,,5])
Kanał ten wydaje się zawierać informacje o wykryciu krawędzi. W przypadku Twojej sieci krawędzie mogą zostać wykryte przez inny kanał.
r
r plot_channel(first_layer_activation[1,,,7])
Kanał ten zawiera zupełnie inne informacje — wydaje się, że wykrywa kocie oczy. Za chwilę wygenerujemy kompletną wizualizację wszystkich warstw aktywacji naszej sieci. Wyciągniemy dane każdego kanału ośmiu map aktywacji i przedstawimy je na wykresie mającym formę jednego dużego zbioru obrazów.
r
r dir.create(_activations) image_size <- 58 images_per_row <- 16 for (i in 1:8) {
layer_activation <- activations[[i]] layer_name <- model\(layers[[i]]\)name
n_features <- dim(layer_activation)[[4]] n_cols <- n_features %/% images_per_row
png(paste0(_activations/, i, _, layer_name, .png), width = image_size * images_per_row, height = image_size * n_cols) op <- par(mfrow = c(n_cols, images_per_row), mai = rep_len(0.02, 4))
for (col in 0:(n_cols-1)) { for (row in 0:(images_per_row-1)) { channel_image <- layer_activation[1,,,(col*images_per_row) + row + 1] plot_channel(channel_image) } }
par(op) dev.off() }
Warto zauważyć tu kilka rzeczy:
Właśnie odkryliśmy ważną cechę reprezentacji uczonych przez głębokie sieci neuronowe: wraz ze wzrostem głębokości warstwy rozpoznawane przez nią cechy stają się coraz bardziej abstrakcyjne. Aktywacje wyższych warstw zawierają coraz mniej informacji o obserwacji wejściowej, jednocześnie zawierają coraz więcej informacji o celu (klasie, do której należy obraz). Głęboka sieć neuronowa pełni funkcję potoku oczyszczającego informację — na wejściu tego potoku podawany jest obraz (w tym przypadku jest to obraz w formacie RGB), który w wyniku powtarzania przekształceń jest odzierany ze zbędnych informacji (np. wizualnych parametrów danego zdjęcia), co prowadzi do wzmocnienia przydatnych informacji, na podstawie których można ustalić klasę, do której należy obraz.
W taki sam sposób świat postrzegają ludzie i zwierzęta: w wyniku obserwowania jakiejś sceny przez kilka sekund człowiek zapamiętuje abstrakcyjne obiekty, które były w niej obecne (takie jak rower lub drzewo), ale nie pamięta konkretnego wyglądu tych obiektów. Spróbuj narysować rower z pamięci. Najprawdopodobniej będzie to problematyczne pomimo tego, że z pewnością zdarzyło Ci się w życiu widzieć rowery wielokrotnie (patrz rysunek 5.24). Spróbuj to zrobić teraz. Nasz mózg naprawdę działa, ucząc się abstrakcji bodźców wizualnych i odzierając je ze zbędnych szczegółów. Przez to zjawisko dość trudno jest zapamiętać detale wyglądu otaczających nas rzeczy.
Innym łatwym sposobem na przyjrzenie się działaniu filtrów konwolucyjnej sieci neuronowej jest wyświetlenie graficznego wzorca, na który reaguje każdy z filtrów. Można to zrobić poprzez zwiększanie gradientu w przestrzeni wejściowej: zastosowanie algorytmu spadku gradientowego na wartości obrazu wejściowego konwolucyjnej sieci neuronowej w celu zmaksymalizowania odpowiedzi wybranego filtra, zaczynając od pustego obrazu wejściowego. Technika ta pozwala na uzyskanie obrazu wejściowego, na który wybrany filtr reaguje w sposób maksymalny.
Proces ten jest prosty: wystarczy zbudować funkcję straty maksymalizującą wartość wybranego filtra w wybranej warstwie konwolucyjnej, a następnie skorzystać z algorytmu stochastycznego spadku gradientowego w celu zmodyfikowania obrazu wejściowego, tak aby uzyskać maksymalną wartość aktywacji. Oto przykład funkcji straty aktywacji filtra 1 warstwy block3_conv1 sieci VGG16 wytrenowanej na zbiorze obrazów ImageNet:
r
r library(keras) model <- application_vgg16( weights = , include_top = FALSE ) layer_name <- 3_conv1
filter_index <- 1 layer_output <- get_layer(model, layer_name)$output loss <- k_mean(layer_output[,,,filter_index])
W celu zaimplementowania algorytmu spadku gradientowego będziemy musieli ustalić gradient straty z uwzględnieniem danych wejściowych modelu. W związku z tym skorzystamy z funkcji k_gradients.
r
r # The call to gradients
returns a list of tensors (of size 1 in this case) # hence we only keep the first element – which is a tensor. grads <- k_gradients(loss, model$input)[[1]]
Niezbyt oczywistym rozwiązaniem wspomagającym algorytm spadku gradientowego jest normalizacja tensora gradientu poprzez podzielenie go przez jego normę L2 (pierwiastek kwadratowy średniej kwadratów wartości tensora). Zapewni to stały zakres modyfikacji obrazu wejściowego.
r
r # We add 1e-5 before dividing so as to avoid accidentally dividing by 0. grads <- grads / (k_sqrt(k_mean(k_square(grads))) + 1e-5)
Teraz musimy jakoś obliczyć wartość tensora straty i tensora gradientu. W tym celu możemy zdefiniować wewnętrzną funkcję pakietu Keras: funkcja iterate przyjmuje tensor Numpy (w formie listy tensorów o rozmiarze 1) i zwraca listę dwóch tensorów Numpy: wartość straty i wartość gradientu.
r
r iterate <- k_function(list(model$input), list(loss, grads)) # Let’s test it c(loss_value, grads_value) %<-% iterate(list(array(0, dim = c(1, 150, 150, 3))))
Teraz mo?emy zdefiniowa? p?tl? implementuj?c? stochastyczny spadek wzd?u? gradientu:
r
r # We start from a gray image with some noise input_img_data <- array(runif(150 * 150 * 3), dim = c(1, 150, 150, 3)) * 20 + 128 step <- 1 # this is the magnitude of each gradient update for (i in 1:40) { # Compute the loss value and gradient value c(loss_value, grads_value) %<-% iterate(list(input_img_data)) # Here we adjust the input image in the direction that maximizes the loss input_img_data <- input_img_data + (grads_value * step) }
Uzyskany w ten sposób tensor obrazu jest tensorem liczb zmiennoprzecinkowych o kształcie (1, 150, 150, 3). Znajdują się w nim wartości z zakresu od 0 do 255, które nie muszą być liczbami całkowitymi. W związku z tym w celu wyświetlenia obrazu musimy jeszcze przetworzyć ten tensor. Zrobimy to za pomocą prostej funkcji narzędziowej.
r
r deprocess_image <- function(x) {
dms <- dim(x)
# normalize tensor: center on 0., ensure std is 0.1 x <- x - mean(x) x <- x / (sd(x) + 1e-5) x <- x * 0.1
# clip to [0, 1] x <- x + 0.5 x <- pmax(0, pmin(x, 1))
# Reshape to original image dimensions array(x, dim = dms) }
Mamy już wszystkie elementy, których potrzebujemy. Połączmy je w celu utworzenia funkcji przyjmującej w charakterze parametru wejściowego nazwę warstwy i indeks filtra. Funkcja ta powinna zwracać gotowy obraz wzorca maksymalizującego aktywację wybranego filtra.
r
r generate_pattern <- function(layer_name, filter_index, size = 150) {
# Build a loss function that maximizes the activation # of the nth filter of the layer considered. layer_output <- model\(get_layer(layer_name)\)output loss <- k_mean(layer_output[,,,filter_index])
# Compute the gradient of the input picture wrt this loss grads <- k_gradients(loss, model$input)[[1]]
# Normalization trick: we normalize the gradient grads <- grads / (k_sqrt(k_mean(k_square(grads))) + 1e-5)
# This function returns the loss and grads given the input picture iterate <- k_function(list(model$input), list(loss, grads))
# We start from a gray image with some noise input_img_data <- array(runif(size * size * 3), dim = c(1, size, size, 3)) * 20 + 128
# Run gradient ascent for 40 steps step <- 1 for (i in 1:40) { c(loss_value, grads_value) %<-% iterate(list(input_img_data)) input_img_data <- input_img_data + (grads_value * step) }
img <- input_img_data[1,,,] deprocess_image(img) }
Wypróbujmy działanie tej funkcji :
r
r library(grid) grid.raster(generate_pattern(3_conv1, 1))
Wygląda na to, że filtr nr 1 warstwy block3_conv1 reaguje na wzór kropek.
Teraz możemy zacząć zabawę i przyjrzeć się wizualizacjom wszystkich filtrów wchodzących w skład wszystkich warstw. Dla uproszczenia przyjrzymy się 64 pierwszym filtrom każdej warstwy i będziemy brać pod uwagę tylko pierwszą warstwę każdego bloku konwolucji (block1_conv1, block2_conv1, block3_conv1, block4_conv1 i block5_conv1). Wygenerowane wartości umieścimy w siatce o wymiarach 88 składającej się z wzorców filtrów o rozdzielczości 64x64, a między wzorcami umieścimy czarne linie rozgraniczające .
r
r library(grid) library(gridExtra) dir.create(_filters) for (layer_name in c(1_conv1, 2_conv1, 3_conv1, 4_conv1)) { size <- 140
png(paste0(_filters/, layer_name, .png), width = 8 * size, height = 8 * size)
grobs <- list() for (i in 0:7) { for (j in 0:7) { pattern <- generate_pattern(layer_name, i + (j*8) + 1, size = size) grob <- rasterGrob(pattern, width = unit(0.9, ), height = unit(0.9, )) grobs[[length(grobs)+1]] <- grob }
}
grid.arrange(grobs = grobs, ncol = 8) dev.off() }
block1_conv1
block2_conv1