14  Segmentacja obrazów

Do tej pory skupialiśmy się na modelach klasyfikacji obrazów gdzie wejściem był obraz, a wyjściem etykieta: “Ten obraz prawdopodobnie zawiera kota; ten inny prawdopodobnie zawiera psa”. Ale klasyfikacja obrazów jest tylko jednym z kilku możliwych zastosowań głębokiego uczenia w wizji komputerowej. Ogólnie rzecz biorąc, istnieją trzy podstawowe zadania wizji komputerowej:

Rysunek 14.1: Przykłady wykorzystania sieci splotowych w zadaniach z wizji komputerowej

Głębokie uczenie dla wizji komputerowej obejmuje również szereg nieco bardziej niszowych zadań poza tymi trzema, takich jak ocena podobieństwa obrazów (szacowanie, jak bardzo dwa obrazy są podobne wizualnie), wykrywanie punktów kluczowych (wskazywanie atrybutów zainteresowania na obrazie, takich jak rysy twarzy), ocena pozy czy humoru, estymacja siatki 3D i tak dalej. Ale na początek klasyfikacja obrazów, segmentacja obrazów i wykrywanie obiektów stanowią podstawę, z którą powinniśmy poznać. Większość zastosowań wizji komputerowej sprowadza się do jednego z tych trzech zadań.

Segmentacja obrazu z głębokim uczeniem polega na użyciu modelu do przypisania klasy do każdego piksela w obrazie, a tym samym segmentacji obrazu na różne strefy (takie jak “tło” i “pierwszy plan” lub “droga”, “samochód” i “chodnik”). Ta ogólna kategoria technik może być wykorzystana do zasilania znacznej liczby cennych aplikacji w edycji obrazu i wideo, autonomicznej jazdy, robotyki, obrazowania medycznego, itp. Istnieją dwa różne rodzaje segmentacji obrazu, o których powinniśmy wiedzieć:

W tym przykładzie skupimy się na segmentacji semantycznej: ponownie przyjrzymy się obrazom kotów i psów, i tym razem nauczymy się, jak odróżnić główny obiekt od jego tła.

Rysunek 14.2: Przykłady segmentacji

Będziemy pracować z zestawem danych Oxford-IIIT Pets (http://www.robots.ox.ac.uk/~vgg/data/pets/), który zawiera 7390 zdjęć różnych ras kotów i psów, wraz z maskami segmentacji pierwszego planu i tła dla każdego zdjęcia. Maska segmentacyjna jest odpowiednikiem etykiety: jest to obraz o tym samym rozmiarze co obraz wejściowy, z jednym kanałem koloru, gdzie każda wartość całkowita odpowiada klasie odpowiadającego jej piksela na obrazie wejściowym. W naszym przypadku piksele masek segmentacyjnych mogą przyjmować jedną z trzech wartości całkowitych:

  1. pierwszy plan
  2. tło
  3. kontur
Kod
# eval: false
library(fs)
data_dir <- path("/Users/majerek/Downloads/pets_dataset")
dir_create(data_dir)

data_url <- path("http://www.robots.ox.ac.uk/~vgg/data/pets/data") 
for (filename in c("images.tar.gz", "annotations.tar.gz")) {
  # uncomment for the first time
 # download.file(url = data_url / filename,
 #             destfile = data_dir / filename)
 # untar(data_dir / filename, exdir = data_dir)
}

Obrazy wejściowe zapisywane są jako pliki JPG w folderze images/ (np. images/Abyssinian_1.jpg), a odpowiadająca im maska segmentacyjna zapisywana jest jako plik PNG o tej samej nazwie w folderze annotations/trimaps/ (np. annotations/trimaps/Abyssinian_1.png). Przygotujmy ramki danych (technicznie rzecz biorąc, tibble) z kolumnami dla naszych ścieżek do plików wejściowych, a także listę odpowiadających im ścieżek do plików maski:

Kod
input_dir <- data_dir / "images"
target_dir <- data_dir / "annotations/trimaps/"

image_paths <- tibble::tibble(
 input = sort(dir_ls(input_dir, glob = "*.jpg")),
 target = sort(dir_ls(target_dir, glob = "*.png")))

Aby mieć pewność, że dopasujemy obraz do właściwego celu, posortujemy obie listy. Wektory ścieżek sortują się tak samo, ponieważ cele i ścieżki obrazów mają tę samą bazową nazwę pliku. Następnie, aby pomóc nam śledzić ścieżki i upewnić się, że nasze wektory wejściowe i docelowe pozostają zsynchronizowane, łączymy je w dwukolumnową ramkę danych (używamy tibble(), aby stworzyć ramkę danych):

Kod
tibble::glimpse(image_paths)
Rows: 7,390
Columns: 2
$ input  <fs::path> "/Users/majerek/Downloads/pets_dataset/images/Abyssinian_1…
$ target <fs::path> "/Users/majerek/Downloads/pets_dataset/annotations/trimaps…

Jak wygląda jedno z tych wejść i jego maska? Najpierw zdefiniujemy funkcję pomocniczą, która wykreśli tensor TensorFlow zawierający obraz przy użyciu funkcji plot() programu R:

Kod
display_image_tensor <- function(x, ..., max = 255,
                                 plot_margins = c(0, 0, 0, 0)) {   
  if(!is.null(plot_margins))
    par(mar = plot_margins)
 
  x |>
    as.array() |>
    drop() |>
    as.raster(max = max) |>
    plot(..., interpolate = FALSE)
}

W wywołaniu as.raster() ustawiamy max = 255, ponieważ podobnie jak w przypadku MNIST, obrazy są zakodowane jako uint8.

Kod
library(tensorflow)
image_tensor <- image_paths$input[10] |> 
   tf$io$read_file() |> 
   tf$io$decode_jpeg()

str(image_tensor)
<tf.Tensor: shape=(448, 500, 3), dtype=uint8, numpy=…>
Kod
display_image_tensor(image_tensor)
Rysunek 14.3: Przykładowy obraz z bazy

Zdefiniujemy również funkcję do wyświetlania obrazu docelowego. Obraz docelowy jest również odczytywany jako uint8, ale tym razem w tensorze obrazu docelowego znajdują się tylko wartości (1, 2, 3). Aby go wykreślić, odejmujemy 1, aby etykiety miały zakres od 0 do 2, a następnie ustawiamy max = 2, aby etykiety miały wartości 0 (czarny), 1 (szary) i 2 (biały).

Kod
display_target_tensor <- function(target) display_image_tensor(target - 1, max = 2)   

target <- image_paths$target[10] |> 
   tf$io$read_file() |> 
   tf$io$decode_png()
str(target)
<tf.Tensor: shape=(448, 500, 1), dtype=uint8, numpy=…>
Kod
display_target_tensor(target)
Rysunek 14.4: Przykładowa maska

Następnie załadujmy nasze wejścia i cele do dwóch TF Datasets i podzielmy pliki na zestawy treningowe i walidacyjne. Ponieważ zbiór danych jest bardzo mały, możemy po prostu załadować wszystko do pamięci:

Kod
library(tfdatasets)
tf_read_image <- function(path, format = "image", resize = NULL, ...) {

  img <- path |> 
    tf$io$read_file()  |> 
    tf$io[[paste0("decode_", format)]](...)

  if (!is.null(resize)) {
    img <- img |> 
      tf$image$resize(as.integer(resize))
  }
  img
}

img_size <- c(200, 200)

tf_read_image_and_resize <- function(..., resize = img_size) {
  tf_read_image(..., resize = resize)
}

make_dataset <- function(paths_df) {
  tensor_slices_dataset(paths_df) |> 
    dataset_map(function(path) {
      image <- path$input |> 
        tf_read_image_and_resize("jpeg", channels = 3L)
      target <- path$target |> 
        tf_read_image_and_resize("png", channels = 1L)
      target <- target - 1
      list(image, target)
    }) |> 
    dataset_cache() |> 
    dataset_shuffle(buffer_size = nrow(paths_df)) |>
    dataset_batch(32)
}

set.seed(44)
num_val_samples <- 1000
val_idx <- sample.int(nrow(image_paths), num_val_samples)

val_paths <- image_paths[val_idx, ]
train_paths <- image_paths[-val_idx, ]

validation_dataset <- make_dataset(val_paths)
train_dataset <- make_dataset(train_paths)

Teraz zdefiniujemy model:

Kod
library(keras)
get_model <- function(img_size, num_classes) {
  conv <- function(..., padding = "same", activation = "relu") { 
    layer_conv_2d(..., padding = padding, activation = activation)
  }

  conv_transpose <- function(..., padding = "same", activation = "relu") { 
    layer_conv_2d_transpose(..., padding = padding, activation = activation)
  }

  input <- layer_input(shape = c(img_size, 3))
  output <- input |>
    layer_rescaling(scale = 1 / 255) |>
    conv(64, 3, strides = 2) |>
    conv(64, 3) |>
    conv(128, 3, strides = 2) |>
    conv(128, 3) |>
    conv(256, 3, strides = 2) |>
    conv(256, 3) |>
    conv_transpose(256, 3) |>
    conv_transpose(256, 3, strides = 2) |>
    conv_transpose(128, 3) |>
    conv_transpose(128, 3, strides = 2) |>
    conv_transpose(64, 3) |>
    conv_transpose(64, 3, strides = 2) |>
    conv(num_classes, 3, activation = "softmax")

  keras_model(input, output)
}

model <- get_model(img_size = img_size, num_classes = 3)
model
Model: "model"
________________________________________________________________________________
 Layer (type)                       Output Shape                    Param #     
================================================================================
 input_1 (InputLayer)               [(None, 200, 200, 3)]           0           
 rescaling (Rescaling)              (None, 200, 200, 3)             0           
 conv2d_6 (Conv2D)                  (None, 100, 100, 64)            1792        
 conv2d_5 (Conv2D)                  (None, 100, 100, 64)            36928       
 conv2d_4 (Conv2D)                  (None, 50, 50, 128)             73856       
 conv2d_3 (Conv2D)                  (None, 50, 50, 128)             147584      
 conv2d_2 (Conv2D)                  (None, 25, 25, 256)             295168      
 conv2d_1 (Conv2D)                  (None, 25, 25, 256)             590080      
 conv2d_transpose_5 (Conv2DTranspo  (None, 25, 25, 256)             590080      
 se)                                                                            
 conv2d_transpose_4 (Conv2DTranspo  (None, 50, 50, 256)             590080      
 se)                                                                            
 conv2d_transpose_3 (Conv2DTranspo  (None, 50, 50, 128)             295040      
 se)                                                                            
 conv2d_transpose_2 (Conv2DTranspo  (None, 100, 100, 128)           147584      
 se)                                                                            
 conv2d_transpose_1 (Conv2DTranspo  (None, 100, 100, 64)            73792       
 se)                                                                            
 conv2d_transpose (Conv2DTranspose  (None, 200, 200, 64)            36928       
 )                                                                              
 conv2d (Conv2D)                    (None, 200, 200, 3)             1731        
================================================================================
Total params: 2880643 (10.99 MB)
Trainable params: 2880643 (10.99 MB)
Non-trainable params: 0 (0.00 Byte)
________________________________________________________________________________

Pierwsza połowa modelu bardzo przypomina sieć splotową używaną do klasyfikacji obrazów: stos warstw Conv2D ze stopniowo zwiększającymi się rozmiarami filtrów. Obniżamy próbkowanie naszych obrazów dwukrotnie aż trzy razy, kończąc na aktywacjach o rozmiarze (25, 25, 256). Celem tej pierwszej połowy jest zakodowanie obrazów w mniejsze mapy cech, gdzie każde miejsce przestrzenne (lub piksel) zawiera informację o dużym przestrzennym kawałku oryginalnego obrazu. Można to rozumieć jako rodzaj kompresji.

Jedną z ważnych różnic pomiędzy pierwszą połową tego modelu a modelami klasyfikacyjnymi, które widziałeś wcześniej, jest sposób, w jaki dokonujemy downsamplingu: w sieciach splotowych klasyfikacyjnych z wcześniejszego rozdziału używaliśmy warstw MaxPooling2D do downsamplingu map cech. W tym przypadku downsamplingu dokonujemy poprzez dodanie strides do każdej kolejnej warstwy konwolucji. Robimy to, ponieważ w przypadku segmentacji obrazu bardzo zależy nam na przestrzennej lokalizacji informacji w obrazie, ponieważ musimy wyprodukować docelowe maski pikselowe jako wyjście modelu. Kiedy wykonujemy max pooling 2 × 2, całkowicie niszczymy informacje o lokalizacji w każdym oknie poolingu: zwracamy jedną wartość na okno, z zerową wiedzą o tym, z której z czterech lokalizacji pochodzi ta wartość. Tak więc, mimo że warstwy max pooling dobrze sprawdzają się w zadaniach klasyfikacji, to w przypadku zadania segmentacji nie są tak skuteczne. W międzyczasie, konwolucje paskowe (strides) lepiej radzą sobie z downsamplingiem map cech, zachowując jednocześnie informacje o lokalizacji.

Druga połowa modelu to stos warstw Conv2DTranspose. Co to jest? Cóż, wyjściem pierwszej połowy modelu jest mapa cech o kształcie (25, 25, 256), ale chcemy, aby nasze ostateczne wyjście miało taki sam kształt jak maski docelowe, (200, 200, 3). W związku z tym musimy zastosować coś w rodzaju odwrotności dotychczasowych przekształceń - coś, co spowoduje zwiększenie próbkowania map funkcji zamiast ich zmniejszenia. Jest to cel warstwy Conv2DTranspose: można o niej myśleć jak o warstwie konwolucji, która uczy się zwiększać próbkowanie. Jeśli mamy wejście o kształcie (100, 100, 64) i przepuścimy je przez warstwę layer_conv_2d(128, 3, strides = 2, padding = "same"), otrzymamy wyjście o kształcie (50, 50, 128). Jeśli przepuścimy to wyjście przez warstwę layer_conv_2d_transpose(64, 3, strides = 2, padding = "same"), otrzymamy z powrotem wyjście o kształcie (100, 100, 64), takie samo jak oryginał. Tak więc po skompresowaniu naszych danych wejściowych do map funkcji o kształcie (25, 25, 256) poprzez stos warstw Conv2D, możemy po prostu zastosować odpowiednią sekwencję warstw Conv2DTranspose, aby uzyskać z powrotem obrazy o kształcie (200, 200, 3).

Możemy teraz skompilować i dopasować nasz model:

Kod
model |>
 compile(optimizer = "rmsprop",
         loss = "sparse_categorical_crossentropy")   

callbacks <- list(
 callback_model_checkpoint("models/oxford_segmentation.keras",
                             save_best_only = TRUE))

history <- model |> fit(
 train_dataset,
 epochs = 50,
 callbacks = callbacks,
 validation_data = validation_dataset
)

Podczas treningu możesz zobaczyć ostrzeżenie takie jak Corrupt JPEG data: premature end of data segment. To dlatego, że zbiór danych nie jest perfekcyjnie przygotowany.

Kod
model <- load_model_tf("models/oxford_segmentation.keras")
load("models/oxford_seg_hist.rda")
plot(history)

Widać, że w połowie drogi, około epoki 25, model zaczyna się nadmiernie dopasowywać.

Kod
test_image <- val_paths$input[4] |>
  tf_read_image_and_resize("jpeg", channels = 3L)

predicted_mask_probs <- 
  model(test_image[tf$newaxis, , , ])

predicted_mask <- 
  tf$argmax(predicted_mask_probs, axis = -1L)

predicted_target <- predicted_mask + 1

par(mfrow = c(1, 2))
display_image_tensor(test_image)
display_target_tensor(predicted_target)
Rysunek 14.5: Obraz testowy i jego przewidywana maska segmentacyjna

Jest kilka małych artefaktów w naszej przewidywanej masce. Niemniej jednak nasz model wydaje się działać poprawnie.

14.1 Nowoczesne architektury sieci splotowych

Architektura modelu jest sumą wyborów, które zostały dokonane przy jego tworzeniu: jakich warstw użyć, jak je skonfigurować i w jakim układzie je połączyć. Te wybory definiują przestrzeń hipotez twojego modelu: przestrzeń możliwych funkcji, które mogą być przeszukiwane przez spadek gradientu, parametryzowane przez wagi modelu. Podobnie jak w przypadku inżynierii cech, dobra przestrzeń hipotez koduje wcześniejszą wiedzę, którą posiadamy na temat danego problemu i jego rozwiązania. Na przykład, użycie warstw konwolucji oznacza, że z góry wiemy, że istotne wzory obecne na obrazach wejściowych są niezmienne względem translacji. Aby skutecznie uczyć się z danych, musimy przyjąć założenia dotyczące tego, czego szukamy.

Jeśli dokonamy niewłaściwego wyboru architektury, nasz model może utknąć z nieoptymalnymi metrykami i żadna ilość danych treningowych go nie uratuje. I odwrotnie, dobra architektura modelu przyspieszy uczenie i umożliwi efektywne wykorzystanie dostępnych danych treningowych, zmniejszając zapotrzebowanie na duże zbiory danych. Dobra architektura modelu to taka, która zmniejsza rozmiar przestrzeni wyszukiwania lub w inny sposób ułatwia konwergencję do dobrego punktu przestrzeni wyszukiwania. Podobnie jak w przypadku inżynierii cech, architektura modelu polega na uproszczeniu problemu do rozwiązania przez spadek gradientu.

Architektura modelu jest bardziej sztuką niż nauką. Doświadczeni inżynierowie uczenia maszynowego są w stanie intuicyjnie poskładać wydajne modele przy pierwszej próbie, podczas gdy początkujący często mają problemy ze stworzeniem modelu, który w ogóle się uczy. Kluczowym słowem jest tu intuicja: nikt nie jest w stanie podać jasnego wyjaśnienia, co działa, a co nie. Eksperci polegają na kojarzeniu wzorców, umiejętności, którą nabywają poprzez bogate doświadczenie praktyczne.

W dalszej części omówimy kilka podstawowych praktyk architektury sieci konwolucyjnych: w szczególności połączenia resztkowe (ang. residual connection), normalizację partii (ang. batch normalization) i konwolucje separowalne (ang. separable convolutions).

Większość sieci splotowych często charakteryzuje się strukturami przypominającymi piramidy (hierarchie cech). Przypomnij sobie na przykład progresję w liczbie filtrów konwolucyjnych, których użyliśmy w pierwszej sieci splotowej: 32, 64, 128. Liczba filtrów rośnie wraz z głębokością warstw, podczas gdy rozmiar map cech odpowiednio się kurczy. Ten sam wzór zauważymy w blokach modelu VGG16 (patrz Rysunek 14.6).

Rysunek 14.6: Architektura sieci VGG16

Ogólnie rzecz biorąc, głęboki stos wąskich warstw działa lepiej niż płytki stos dużych warstw. Istnieje jednak pewne ograniczenie, jak głęboko można układać warstwy, a wiąże się z problem znikających gradientów. To prowadzi nas do naszego pierwszego istotnego wzorca architektury modelu: połączeń resztkowych.

14.1.1 Połączenia rezydualne

Wsteczna propagacja używania do uczenia modeli głębokich przypomina grę w “głuchy telefon”, gdzie kolejne osoby szepczą sobie do ucha przekazując pewną - wymyśloną przez pierwszego gracza - informację. Po kilku przejściach informacja ta jest znacznie zniekształcona. Podobnie jest w uczeniu sieci, gdzie propagacją błędów wstecz powoduje wprowadzenie pewnych błędów w kolejnych warstwach sieci. Każda kolejna funkcja w łańcuchu wprowadza pewną ilość szumu. Jeśli łańcuch funkcji jest zbyt głęboki, szum ten zaczyna przytłaczać informacje o gradiencie i wsteczna propagacja przestaje działać. Jest to problem znikających gradientów.

Rozwiązanie jest proste: wystarczy wymusić, aby każda funkcja w łańcuchu była nieniszcząca - zachowywała “czystą” wersję informacji zawartej w poprzednim wejściu. Najłatwiejszym sposobem wdrożenia tego jest użycie połączenia rezydualnego.

Jest to bardzo proste: wystarczy dodać wejście warstwy lub bloku warstw z powrotem do jej wyjścia (patrz Rysunek 14.7). Połączenie rezydualne działa jak skrót informacyjny wokół destrukcyjnych lub zaszumiających bloków (takich jak bloki zawierające aktywacje relu lub warstwy dropout), przekazując informację o gradiencie błędu z wczesnych warstw, tak aby propagować przez głęboką sieć. Technika ta została wprowadzona w 2015 roku wraz z rodziną modeli ResNet (opracowanych przez He i in. (2015)).

Rysunek 14.7: Połączenie rezydualne

Zauważmy, że dodanie wejścia z powrotem do wyjścia bloku sugeruje, że wyjście powinno mieć taki sam kształt jak wejście. Jednak tak nie jest, jeśli nasz blok zawiera warstwy konwolucyjne ze zwiększoną liczbą filtrów lub warstwę max-pooling. W takich przypadkach używamy warstwy 1x1 layer_conv_2d() bez aktywacji, aby liniowo rzutować resztę na pożądany kształt wyjścia. Zazwyczaj używamy paddingu = "same" w warstwach konwolucji w bloku docelowym, aby uniknąć przestrzennego downsamplingu spowodowanego paddingiem, a w projekcji resztkowej używamy strides.

Kod
# klasyczne podejście
inputs <- layer_input(shape = c(32, 32, 3))
x <- inputs |> layer_conv_2d(32, 3, activation = "relu")
residual <- x
x <- x |> layer_conv_2d(64, 3, activation = "relu", padding = "same")
residual <- residual |> layer_conv_2d(64, 1)
x <- layer_add(c(x, residual))
Kod
# podejście wykorzystujące stride
inputs <- layer_input(shape = c(32, 32, 3))
x <- inputs |> layer_conv_2d(32, 3, activation = "relu")
residual <- x
x <- x |>
  layer_conv_2d(64, 3, activation = "relu", padding = "same") |>
  layer_max_pooling_2d(2, padding = "same")
residual <- residual |>
  layer_conv_2d(64, 1, strides = 2)
x <- layer_add(list(x, residual))

Aby uczynić te pomysły bardziej konkretnymi, oto przykład prostej sieci splotowej zorganizowanej w serię bloków, z których każdy składa się z dwóch warstw konwolucji i jednej opcjonalnej warstwy max-pooling, z połączeniem rezydualnym wokół każdego bloku:

Kod
inputs <- layer_input(shape = c(32, 32, 3))
x <- layer_rescaling(inputs, scale = 1/255)

residual_block <- function(x, filters, 
                           pooling = FALSE) {
  residual <- x
  x <- x |>
    layer_conv_2d(filters, 3, activation = "relu", padding = "same") |>
    layer_conv_2d(filters, 3, activation = "relu", padding = "same")

  if (pooling) {
    x <- x |> layer_max_pooling_2d(pool_size = 2, padding = "same")
    residual <- residual |> layer_conv_2d(filters, 1, strides = 2)
  } else if (filters != dim(residual)[4]) {
    residual <- residual |> layer_conv_2d(filters, 1)
  }

  layer_add(list(x, residual))
}

outputs <- x |>
  residual_block(filters = 32, pooling = TRUE) |>
  residual_block(filters = 64, pooling = TRUE) |>
  residual_block(filters = 128, pooling = FALSE) |>
  layer_global_average_pooling_2d() |>
  layer_dense(units = 1, activation = "sigmoid")
model <- keras_model(inputs = inputs, outputs = outputs)
model
Model: "model_1"
________________________________________________________________________________
 Layer (type)           Output Shape            Param   Connected to            
                                                 #                              
================================================================================
 input_4 (InputLayer)   [(None, 32, 32, 3)]     0       []                      
 rescaling_1 (Rescalin  (None, 32, 32, 3)       0       ['input_4[0][0]']       
 g)                                                                             
 conv2d_14 (Conv2D)     (None, 32, 32, 32)      896     ['rescaling_1[0][0]']   
 conv2d_13 (Conv2D)     (None, 32, 32, 32)      9248    ['conv2d_14[0][0]']     
 max_pooling2d_1 (MaxP  (None, 16, 16, 32)      0       ['conv2d_13[0][0]']     
 ooling2D)                                                                      
 conv2d_15 (Conv2D)     (None, 16, 16, 32)      128     ['rescaling_1[0][0]']   
 add_2 (Add)            (None, 16, 16, 32)      0       ['max_pooling2d_1[0][0]'
                                                        , 'conv2d_15[0][0]']    
 conv2d_17 (Conv2D)     (None, 16, 16, 64)      18496   ['add_2[0][0]']         
 conv2d_16 (Conv2D)     (None, 16, 16, 64)      36928   ['conv2d_17[0][0]']     
 max_pooling2d_2 (MaxP  (None, 8, 8, 64)        0       ['conv2d_16[0][0]']     
 ooling2D)                                                                      
 conv2d_18 (Conv2D)     (None, 8, 8, 64)        2112    ['add_2[0][0]']         
 add_3 (Add)            (None, 8, 8, 64)        0       ['max_pooling2d_2[0][0]'
                                                        , 'conv2d_18[0][0]']    
 conv2d_20 (Conv2D)     (None, 8, 8, 128)       73856   ['add_3[0][0]']         
 conv2d_19 (Conv2D)     (None, 8, 8, 128)       14758   ['conv2d_20[0][0]']     
                                                4                               
 conv2d_21 (Conv2D)     (None, 8, 8, 128)       8320    ['add_3[0][0]']         
 add_4 (Add)            (None, 8, 8, 128)       0       ['conv2d_19[0][0]',     
                                                         'conv2d_21[0][0]']     
 global_average_poolin  (None, 128)             0       ['add_4[0][0]']         
 g2d (GlobalAveragePoo                                                          
 ling2D)                                                                        
 dense (Dense)          (None, 1)               129     ['global_average_pooling
                                                        2d[0][0]']              
================================================================================
Total params: 297697 (1.14 MB)
Trainable params: 297697 (1.14 MB)
Non-trainable params: 0 (0.00 Byte)
________________________________________________________________________________

Dzięki połączeniom rezydualnym można budować sieci o dowolnej głębokości, bez konieczności martwienia się o znikające gradienty.

14.1.2 Normalizacja partii

Normalizacja jest szeroką kategorią metod, które mają na celu uczynienie różnych próbek widzianych przez model uczenia maszynowego bardziej podobnymi do siebie, co pomaga modelowi w uczeniu się i generalizacji na nowych danych. Najbardziej powszechną formą normalizacji danych jest ta, którą już widziałeś kilka razy w tej książce: wyśrodkowanie danych na zero poprzez odjęcie średniej od danych i nadanie danym jednostkowego odchylenia standardowego poprzez podzielenie danych przez ich odchylenie standardowe. W efekcie przyjmuje się założenie, że dane mają rozkład normalny (lub gaussowski) i upewnia się, że rozkład ten jest wyśrodkowany i przeskalowany do jednostkowej wariancji:

Kod
normalize_data <- apply(data, <axis>, function(x) (x - mean(x)) / sd(x))

Poprzednie przykłady w tej książce normalizowały dane przed wprowadzeniem ich do modeli. Ale normalizacja danych może być interesująca po każdym przekształceniu dokonanym przez sieć: nawet jeśli dane wchodzące do sieci Dense lub Conv2D mają średnią 0 i jednostkową wariancję, nie ma powodu, by a priori oczekiwać, że tak będzie w przypadku danych wychodzących. Czy normalizacja aktywacji pośrednich mogłaby pomóc?

Normalizacja partii danych jest to rodzaj warstwy (layer_batch_normalization() w keras) wprowadzonej w 2015 roku przez Ioffe i Szegedy (2015); może ona adaptacyjnie normalizować dane, nawet gdy średnia i wariancja zmieniają się w czasie treningu. Podczas szkolenia używa średniej i wariancji bieżącej partii danych do normalizacji próbek, a podczas wnioskowania (gdy wystarczająco duża partia reprezentatywnych danych może nie być dostępna), używa wykładniczej średniej ruchomej i wariancji danych widzianych podczas szkolenia.

Chociaż w oryginalnym artykule stwierdzono, że normalizacja partii działa poprzez “redukcję wewnętrznego przesunięcia kowariancji”, nikt tak naprawdę nie wie na pewno, dlaczego normalizacja partii pomaga. Różne hipotezy istnieją, ale nie ma pewności.

W praktyce, głównym efektem normalizacji partii wydaje się być to, że pomaga ona w propagacji gradientu - podobnie jak połączenia resztkowe - i tym samym pozwala na tworzenie głębszych sieci. Niektóre bardzo głębokie sieci mogą być trenowane tylko wtedy, gdy zawierają wiele warstw BatchNormalization. Na przykład, normalizacja partii jest szeroko stosowana w wielu zaawansowanych architekturach sieci konwolucyjnych, które są dostarczane z keras, takich jak ResNet50, EfficientNet i Xception.

Zarówno layer_dense() jak i layer_conv_2d() wykorzystują wektor obciążeń (ang. bias), wyuczoną zmienną, której celem jest uczynienie warstwy afiniczną, a nie czysto liniową. Na przykład, funkcja layer_ conv_2d() zwraca, y = conv(x, kernel) + bias, a layer_dense() zwraca y = dot(x, kernel) + bias. Ponieważ krok normalizacji zajmie się wyśrodkowaniem wyjścia warstwy na zero, wektor obciążenia nie jest już potrzebny w użyciu funkcji layer_batch_normalization(), a warstwę można utworzyć bez niego za pomocą opcji use_bias = FALSE.

Co ważne, generalnie zalecałbym umieszczanie aktywacji poprzedniej warstwy po warstwie normalizacji wsadowej (choć to wciąż temat do dyskusji).

Kod
x |>
 layer_conv_2d(32, 3, activation = "relu") |>
 layer_batch_normalization()

# lub

x |>
  layer_conv_2d(32, 3, use_bias = FALSE) |>
  layer_batch_normalization() |>
  layer_activation("relu")

Intuicyjnym powodem tego podejścia jest to, że normalizacja partii skupi naszes wejścia na zerze, podczas gdy nasza aktywacja relu używa zera jako punktu zwrotnego dla utrzymania lub porzucenia aktywowanych kanałów: robienie normalizacji przed aktywacją maksymalizuje wykorzystanie relu.

Normalizacja partii wiążę się też z szeregiem pułapek. Jedna z głównych dotyczy dostrajania: podczas dostrajania modelu, który zawiera warstwy BatchNormalization, zaleca się pozostawienie tych warstw zamrożonymi (wywołaj freeze_weights(), aby ustawić ich atrybut trainable na FALSE). W przeciwnym razie, będą one aktualizować swoją wewnętrzną średnią i wariancję, co może kolidować z bardzo małymi aktualizacjami zastosowanymi w otaczających warstwach Conv2D:

Kod
batch_norm_layer_s3_classname <- class(layer_batch_normalization())[1]
batch_norm_layer_s3_classname
[1] "keras.layers.normalization.batch_normalization.BatchNormalization"
Kod
is_batch_norm_layer <- function(x) inherits(x, batch_norm_layer_s3_classname)

model <- application_efficientnet_b0()

for (layer in model$layers) {
  if (is_batch_norm_layer(layer)) {
    layer$trainable <- FALSE
  }
}

model
Model: "efficientnetb0"
________________________________________________________________________________
 Layer (type)       Output Shape         Para   Connected to         Trainable  
                                         m #                                    
================================================================================
 input_5 (InputLay  [(None, 224, 224,    0      []                   Y          
 er)                3)]                                                         
 rescaling_2 (Resc  (None, 224, 224, 3   0      ['input_5[0][0]']    Y          
 aling)             )                                                           
 normalization (No  (None, 224, 224, 3   7      ['rescaling_2[0][0   Y          
 rmalization)       )                           ]']                             
 rescaling_3 (Resc  (None, 224, 224, 3   0      ['normalization[0]   Y          
 aling)             )                           [0]']                           
 stem_conv_pad (Ze  (None, 225, 225, 3   0      ['rescaling_3[0][0   Y          
 roPadding2D)       )                           ]']                             
 stem_conv (Conv2D  (None, 112, 112, 3   864    ['stem_conv_pad[0]   Y          
 )                  2)                          [0]']                           
 stem_bn (BatchNor  (None, 112, 112, 3   128    ['stem_conv[0][0]'   N          
 malization)        2)                          ]                               
 stem_activation (  (None, 112, 112, 3   0      ['stem_bn[0][0]']    Y          
 Activation)        2)                                                          
 block1a_dwconv (D  (None, 112, 112, 3   288    ['stem_activation[   Y          
 epthwiseConv2D)    2)                          0][0]']                         
 block1a_bn (Batch  (None, 112, 112, 3   128    ['block1a_dwconv[0   N          
 Normalization)     2)                          ][0]']                          
 block1a_activatio  (None, 112, 112, 3   0      ['block1a_bn[0][0]   Y          
 n (Activation)     2)                          ']                              
 block1a_se_squeez  (None, 32)           0      ['block1a_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block1a_se_reshap  (None, 1, 1, 32)     0      ['block1a_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block1a_se_reduce  (None, 1, 1, 8)      264    ['block1a_se_resha   Y          
  (Conv2D)                                      pe[0][0]']                      
 block1a_se_expand  (None, 1, 1, 32)     288    ['block1a_se_reduc   Y          
  (Conv2D)                                      e[0][0]']                       
 block1a_se_excite  (None, 112, 112, 3   0      ['block1a_activati   Y          
  (Multiply)        2)                          on[0][0]',                      
                                                 'block1a_se_expan              
                                                d[0][0]']                       
 block1a_project_c  (None, 112, 112, 1   512    ['block1a_se_excit   Y          
 onv (Conv2D)       6)                          e[0][0]']                       
 block1a_project_b  (None, 112, 112, 1   64     ['block1a_project_   N          
 n (BatchNormaliza  6)                          conv[0][0]']                    
 tion)                                                                          
 block2a_expand_co  (None, 112, 112, 9   1536   ['block1a_project_   Y          
 nv (Conv2D)        6)                          bn[0][0]']                      
 block2a_expand_bn  (None, 112, 112, 9   384    ['block2a_expand_c   N          
  (BatchNormalizat  6)                          onv[0][0]']                     
 ion)                                                                           
 block2a_expand_ac  (None, 112, 112, 9   0      ['block2a_expand_b   Y          
 tivation (Activat  6)                          n[0][0]']                       
 ion)                                                                           
 block2a_dwconv_pa  (None, 113, 113, 9   0      ['block2a_expand_a   Y          
 d (ZeroPadding2D)  6)                          ctivation[0][0]']               
 block2a_dwconv (D  (None, 56, 56, 96)   864    ['block2a_dwconv_p   Y          
 epthwiseConv2D)                                ad[0][0]']                      
 block2a_bn (Batch  (None, 56, 56, 96)   384    ['block2a_dwconv[0   N          
 Normalization)                                 ][0]']                          
 block2a_activatio  (None, 56, 56, 96)   0      ['block2a_bn[0][0]   Y          
 n (Activation)                                 ']                              
 block2a_se_squeez  (None, 96)           0      ['block2a_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block2a_se_reshap  (None, 1, 1, 96)     0      ['block2a_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block2a_se_reduce  (None, 1, 1, 4)      388    ['block2a_se_resha   Y          
  (Conv2D)                                      pe[0][0]']                      
 block2a_se_expand  (None, 1, 1, 96)     480    ['block2a_se_reduc   Y          
  (Conv2D)                                      e[0][0]']                       
 block2a_se_excite  (None, 56, 56, 96)   0      ['block2a_activati   Y          
  (Multiply)                                    on[0][0]',                      
                                                 'block2a_se_expan              
                                                d[0][0]']                       
 block2a_project_c  (None, 56, 56, 24)   2304   ['block2a_se_excit   Y          
 onv (Conv2D)                                   e[0][0]']                       
 block2a_project_b  (None, 56, 56, 24)   96     ['block2a_project_   N          
 n (BatchNormaliza                              conv[0][0]']                    
 tion)                                                                          
 block2b_expand_co  (None, 56, 56, 144   3456   ['block2a_project_   Y          
 nv (Conv2D)        )                           bn[0][0]']                      
 block2b_expand_bn  (None, 56, 56, 144   576    ['block2b_expand_c   N          
  (BatchNormalizat  )                           onv[0][0]']                     
 ion)                                                                           
 block2b_expand_ac  (None, 56, 56, 144   0      ['block2b_expand_b   Y          
 tivation (Activat  )                           n[0][0]']                       
 ion)                                                                           
 block2b_dwconv (D  (None, 56, 56, 144   1296   ['block2b_expand_a   Y          
 epthwiseConv2D)    )                           ctivation[0][0]']               
 block2b_bn (Batch  (None, 56, 56, 144   576    ['block2b_dwconv[0   N          
 Normalization)     )                           ][0]']                          
 block2b_activatio  (None, 56, 56, 144   0      ['block2b_bn[0][0]   Y          
 n (Activation)     )                           ']                              
 block2b_se_squeez  (None, 144)          0      ['block2b_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block2b_se_reshap  (None, 1, 1, 144)    0      ['block2b_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block2b_se_reduce  (None, 1, 1, 6)      870    ['block2b_se_resha   Y          
  (Conv2D)                                      pe[0][0]']                      
 block2b_se_expand  (None, 1, 1, 144)    1008   ['block2b_se_reduc   Y          
  (Conv2D)                                      e[0][0]']                       
 block2b_se_excite  (None, 56, 56, 144   0      ['block2b_activati   Y          
  (Multiply)        )                           on[0][0]',                      
                                                 'block2b_se_expan              
                                                d[0][0]']                       
 block2b_project_c  (None, 56, 56, 24)   3456   ['block2b_se_excit   Y          
 onv (Conv2D)                                   e[0][0]']                       
 block2b_project_b  (None, 56, 56, 24)   96     ['block2b_project_   N          
 n (BatchNormaliza                              conv[0][0]']                    
 tion)                                                                          
 block2b_drop (Dro  (None, 56, 56, 24)   0      ['block2b_project_   Y          
 pout)                                          bn[0][0]']                      
 block2b_add (Add)  (None, 56, 56, 24)   0      ['block2b_drop[0][   Y          
                                                0]',                            
                                                 'block2a_project_              
                                                bn[0][0]']                      
 block3a_expand_co  (None, 56, 56, 144   3456   ['block2b_add[0][0   Y          
 nv (Conv2D)        )                           ]']                             
 block3a_expand_bn  (None, 56, 56, 144   576    ['block3a_expand_c   N          
  (BatchNormalizat  )                           onv[0][0]']                     
 ion)                                                                           
 block3a_expand_ac  (None, 56, 56, 144   0      ['block3a_expand_b   Y          
 tivation (Activat  )                           n[0][0]']                       
 ion)                                                                           
 block3a_dwconv_pa  (None, 59, 59, 144   0      ['block3a_expand_a   Y          
 d (ZeroPadding2D)  )                           ctivation[0][0]']               
 block3a_dwconv (D  (None, 28, 28, 144   3600   ['block3a_dwconv_p   Y          
 epthwiseConv2D)    )                           ad[0][0]']                      
 block3a_bn (Batch  (None, 28, 28, 144   576    ['block3a_dwconv[0   N          
 Normalization)     )                           ][0]']                          
 block3a_activatio  (None, 28, 28, 144   0      ['block3a_bn[0][0]   Y          
 n (Activation)     )                           ']                              
 block3a_se_squeez  (None, 144)          0      ['block3a_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block3a_se_reshap  (None, 1, 1, 144)    0      ['block3a_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block3a_se_reduce  (None, 1, 1, 6)      870    ['block3a_se_resha   Y          
  (Conv2D)                                      pe[0][0]']                      
 block3a_se_expand  (None, 1, 1, 144)    1008   ['block3a_se_reduc   Y          
  (Conv2D)                                      e[0][0]']                       
 block3a_se_excite  (None, 28, 28, 144   0      ['block3a_activati   Y          
  (Multiply)        )                           on[0][0]',                      
                                                 'block3a_se_expan              
                                                d[0][0]']                       
 block3a_project_c  (None, 28, 28, 40)   5760   ['block3a_se_excit   Y          
 onv (Conv2D)                                   e[0][0]']                       
 block3a_project_b  (None, 28, 28, 40)   160    ['block3a_project_   N          
 n (BatchNormaliza                              conv[0][0]']                    
 tion)                                                                          
 block3b_expand_co  (None, 28, 28, 240   9600   ['block3a_project_   Y          
 nv (Conv2D)        )                           bn[0][0]']                      
 block3b_expand_bn  (None, 28, 28, 240   960    ['block3b_expand_c   N          
  (BatchNormalizat  )                           onv[0][0]']                     
 ion)                                                                           
 block3b_expand_ac  (None, 28, 28, 240   0      ['block3b_expand_b   Y          
 tivation (Activat  )                           n[0][0]']                       
 ion)                                                                           
 block3b_dwconv (D  (None, 28, 28, 240   6000   ['block3b_expand_a   Y          
 epthwiseConv2D)    )                           ctivation[0][0]']               
 block3b_bn (Batch  (None, 28, 28, 240   960    ['block3b_dwconv[0   N          
 Normalization)     )                           ][0]']                          
 block3b_activatio  (None, 28, 28, 240   0      ['block3b_bn[0][0]   Y          
 n (Activation)     )                           ']                              
 block3b_se_squeez  (None, 240)          0      ['block3b_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block3b_se_reshap  (None, 1, 1, 240)    0      ['block3b_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block3b_se_reduce  (None, 1, 1, 10)     2410   ['block3b_se_resha   Y          
  (Conv2D)                                      pe[0][0]']                      
 block3b_se_expand  (None, 1, 1, 240)    2640   ['block3b_se_reduc   Y          
  (Conv2D)                                      e[0][0]']                       
 block3b_se_excite  (None, 28, 28, 240   0      ['block3b_activati   Y          
  (Multiply)        )                           on[0][0]',                      
                                                 'block3b_se_expan              
                                                d[0][0]']                       
 block3b_project_c  (None, 28, 28, 40)   9600   ['block3b_se_excit   Y          
 onv (Conv2D)                                   e[0][0]']                       
 block3b_project_b  (None, 28, 28, 40)   160    ['block3b_project_   N          
 n (BatchNormaliza                              conv[0][0]']                    
 tion)                                                                          
 block3b_drop (Dro  (None, 28, 28, 40)   0      ['block3b_project_   Y          
 pout)                                          bn[0][0]']                      
 block3b_add (Add)  (None, 28, 28, 40)   0      ['block3b_drop[0][   Y          
                                                0]',                            
                                                 'block3a_project_              
                                                bn[0][0]']                      
 block4a_expand_co  (None, 28, 28, 240   9600   ['block3b_add[0][0   Y          
 nv (Conv2D)        )                           ]']                             
 block4a_expand_bn  (None, 28, 28, 240   960    ['block4a_expand_c   N          
  (BatchNormalizat  )                           onv[0][0]']                     
 ion)                                                                           
 block4a_expand_ac  (None, 28, 28, 240   0      ['block4a_expand_b   Y          
 tivation (Activat  )                           n[0][0]']                       
 ion)                                                                           
 block4a_dwconv_pa  (None, 29, 29, 240   0      ['block4a_expand_a   Y          
 d (ZeroPadding2D)  )                           ctivation[0][0]']               
 block4a_dwconv (D  (None, 14, 14, 240   2160   ['block4a_dwconv_p   Y          
 epthwiseConv2D)    )                           ad[0][0]']                      
 block4a_bn (Batch  (None, 14, 14, 240   960    ['block4a_dwconv[0   N          
 Normalization)     )                           ][0]']                          
 block4a_activatio  (None, 14, 14, 240   0      ['block4a_bn[0][0]   Y          
 n (Activation)     )                           ']                              
 block4a_se_squeez  (None, 240)          0      ['block4a_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block4a_se_reshap  (None, 1, 1, 240)    0      ['block4a_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block4a_se_reduce  (None, 1, 1, 10)     2410   ['block4a_se_resha   Y          
  (Conv2D)                                      pe[0][0]']                      
 block4a_se_expand  (None, 1, 1, 240)    2640   ['block4a_se_reduc   Y          
  (Conv2D)                                      e[0][0]']                       
 block4a_se_excite  (None, 14, 14, 240   0      ['block4a_activati   Y          
  (Multiply)        )                           on[0][0]',                      
                                                 'block4a_se_expan              
                                                d[0][0]']                       
 block4a_project_c  (None, 14, 14, 80)   1920   ['block4a_se_excit   Y          
 onv (Conv2D)                            0      e[0][0]']                       
 block4a_project_b  (None, 14, 14, 80)   320    ['block4a_project_   N          
 n (BatchNormaliza                              conv[0][0]']                    
 tion)                                                                          
 block4b_expand_co  (None, 14, 14, 480   3840   ['block4a_project_   Y          
 nv (Conv2D)        )                    0      bn[0][0]']                      
 block4b_expand_bn  (None, 14, 14, 480   1920   ['block4b_expand_c   N          
  (BatchNormalizat  )                           onv[0][0]']                     
 ion)                                                                           
 block4b_expand_ac  (None, 14, 14, 480   0      ['block4b_expand_b   Y          
 tivation (Activat  )                           n[0][0]']                       
 ion)                                                                           
 block4b_dwconv (D  (None, 14, 14, 480   4320   ['block4b_expand_a   Y          
 epthwiseConv2D)    )                           ctivation[0][0]']               
 block4b_bn (Batch  (None, 14, 14, 480   1920   ['block4b_dwconv[0   N          
 Normalization)     )                           ][0]']                          
 block4b_activatio  (None, 14, 14, 480   0      ['block4b_bn[0][0]   Y          
 n (Activation)     )                           ']                              
 block4b_se_squeez  (None, 480)          0      ['block4b_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block4b_se_reshap  (None, 1, 1, 480)    0      ['block4b_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block4b_se_reduce  (None, 1, 1, 20)     9620   ['block4b_se_resha   Y          
  (Conv2D)                                      pe[0][0]']                      
 block4b_se_expand  (None, 1, 1, 480)    1008   ['block4b_se_reduc   Y          
  (Conv2D)                               0      e[0][0]']                       
 block4b_se_excite  (None, 14, 14, 480   0      ['block4b_activati   Y          
  (Multiply)        )                           on[0][0]',                      
                                                 'block4b_se_expan              
                                                d[0][0]']                       
 block4b_project_c  (None, 14, 14, 80)   3840   ['block4b_se_excit   Y          
 onv (Conv2D)                            0      e[0][0]']                       
 block4b_project_b  (None, 14, 14, 80)   320    ['block4b_project_   N          
 n (BatchNormaliza                              conv[0][0]']                    
 tion)                                                                          
 block4b_drop (Dro  (None, 14, 14, 80)   0      ['block4b_project_   Y          
 pout)                                          bn[0][0]']                      
 block4b_add (Add)  (None, 14, 14, 80)   0      ['block4b_drop[0][   Y          
                                                0]',                            
                                                 'block4a_project_              
                                                bn[0][0]']                      
 block4c_expand_co  (None, 14, 14, 480   3840   ['block4b_add[0][0   Y          
 nv (Conv2D)        )                    0      ]']                             
 block4c_expand_bn  (None, 14, 14, 480   1920   ['block4c_expand_c   N          
  (BatchNormalizat  )                           onv[0][0]']                     
 ion)                                                                           
 block4c_expand_ac  (None, 14, 14, 480   0      ['block4c_expand_b   Y          
 tivation (Activat  )                           n[0][0]']                       
 ion)                                                                           
 block4c_dwconv (D  (None, 14, 14, 480   4320   ['block4c_expand_a   Y          
 epthwiseConv2D)    )                           ctivation[0][0]']               
 block4c_bn (Batch  (None, 14, 14, 480   1920   ['block4c_dwconv[0   N          
 Normalization)     )                           ][0]']                          
 block4c_activatio  (None, 14, 14, 480   0      ['block4c_bn[0][0]   Y          
 n (Activation)     )                           ']                              
 block4c_se_squeez  (None, 480)          0      ['block4c_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block4c_se_reshap  (None, 1, 1, 480)    0      ['block4c_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block4c_se_reduce  (None, 1, 1, 20)     9620   ['block4c_se_resha   Y          
  (Conv2D)                                      pe[0][0]']                      
 block4c_se_expand  (None, 1, 1, 480)    1008   ['block4c_se_reduc   Y          
  (Conv2D)                               0      e[0][0]']                       
 block4c_se_excite  (None, 14, 14, 480   0      ['block4c_activati   Y          
  (Multiply)        )                           on[0][0]',                      
                                                 'block4c_se_expan              
                                                d[0][0]']                       
 block4c_project_c  (None, 14, 14, 80)   3840   ['block4c_se_excit   Y          
 onv (Conv2D)                            0      e[0][0]']                       
 block4c_project_b  (None, 14, 14, 80)   320    ['block4c_project_   N          
 n (BatchNormaliza                              conv[0][0]']                    
 tion)                                                                          
 block4c_drop (Dro  (None, 14, 14, 80)   0      ['block4c_project_   Y          
 pout)                                          bn[0][0]']                      
 block4c_add (Add)  (None, 14, 14, 80)   0      ['block4c_drop[0][   Y          
                                                0]',                            
                                                 'block4b_add[0][0              
                                                ]']                             
 block5a_expand_co  (None, 14, 14, 480   3840   ['block4c_add[0][0   Y          
 nv (Conv2D)        )                    0      ]']                             
 block5a_expand_bn  (None, 14, 14, 480   1920   ['block5a_expand_c   N          
  (BatchNormalizat  )                           onv[0][0]']                     
 ion)                                                                           
 block5a_expand_ac  (None, 14, 14, 480   0      ['block5a_expand_b   Y          
 tivation (Activat  )                           n[0][0]']                       
 ion)                                                                           
 block5a_dwconv (D  (None, 14, 14, 480   1200   ['block5a_expand_a   Y          
 epthwiseConv2D)    )                    0      ctivation[0][0]']               
 block5a_bn (Batch  (None, 14, 14, 480   1920   ['block5a_dwconv[0   N          
 Normalization)     )                           ][0]']                          
 block5a_activatio  (None, 14, 14, 480   0      ['block5a_bn[0][0]   Y          
 n (Activation)     )                           ']                              
 block5a_se_squeez  (None, 480)          0      ['block5a_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block5a_se_reshap  (None, 1, 1, 480)    0      ['block5a_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block5a_se_reduce  (None, 1, 1, 20)     9620   ['block5a_se_resha   Y          
  (Conv2D)                                      pe[0][0]']                      
 block5a_se_expand  (None, 1, 1, 480)    1008   ['block5a_se_reduc   Y          
  (Conv2D)                               0      e[0][0]']                       
 block5a_se_excite  (None, 14, 14, 480   0      ['block5a_activati   Y          
  (Multiply)        )                           on[0][0]',                      
                                                 'block5a_se_expan              
                                                d[0][0]']                       
 block5a_project_c  (None, 14, 14, 112   5376   ['block5a_se_excit   Y          
 onv (Conv2D)       )                    0      e[0][0]']                       
 block5a_project_b  (None, 14, 14, 112   448    ['block5a_project_   N          
 n (BatchNormaliza  )                           conv[0][0]']                    
 tion)                                                                          
 block5b_expand_co  (None, 14, 14, 672   7526   ['block5a_project_   Y          
 nv (Conv2D)        )                    4      bn[0][0]']                      
 block5b_expand_bn  (None, 14, 14, 672   2688   ['block5b_expand_c   N          
  (BatchNormalizat  )                           onv[0][0]']                     
 ion)                                                                           
 block5b_expand_ac  (None, 14, 14, 672   0      ['block5b_expand_b   Y          
 tivation (Activat  )                           n[0][0]']                       
 ion)                                                                           
 block5b_dwconv (D  (None, 14, 14, 672   1680   ['block5b_expand_a   Y          
 epthwiseConv2D)    )                    0      ctivation[0][0]']               
 block5b_bn (Batch  (None, 14, 14, 672   2688   ['block5b_dwconv[0   N          
 Normalization)     )                           ][0]']                          
 block5b_activatio  (None, 14, 14, 672   0      ['block5b_bn[0][0]   Y          
 n (Activation)     )                           ']                              
 block5b_se_squeez  (None, 672)          0      ['block5b_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block5b_se_reshap  (None, 1, 1, 672)    0      ['block5b_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block5b_se_reduce  (None, 1, 1, 28)     1884   ['block5b_se_resha   Y          
  (Conv2D)                               4      pe[0][0]']                      
 block5b_se_expand  (None, 1, 1, 672)    1948   ['block5b_se_reduc   Y          
  (Conv2D)                               8      e[0][0]']                       
 block5b_se_excite  (None, 14, 14, 672   0      ['block5b_activati   Y          
  (Multiply)        )                           on[0][0]',                      
                                                 'block5b_se_expan              
                                                d[0][0]']                       
 block5b_project_c  (None, 14, 14, 112   7526   ['block5b_se_excit   Y          
 onv (Conv2D)       )                    4      e[0][0]']                       
 block5b_project_b  (None, 14, 14, 112   448    ['block5b_project_   N          
 n (BatchNormaliza  )                           conv[0][0]']                    
 tion)                                                                          
 block5b_drop (Dro  (None, 14, 14, 112   0      ['block5b_project_   Y          
 pout)              )                           bn[0][0]']                      
 block5b_add (Add)  (None, 14, 14, 112   0      ['block5b_drop[0][   Y          
                    )                           0]',                            
                                                 'block5a_project_              
                                                bn[0][0]']                      
 block5c_expand_co  (None, 14, 14, 672   7526   ['block5b_add[0][0   Y          
 nv (Conv2D)        )                    4      ]']                             
 block5c_expand_bn  (None, 14, 14, 672   2688   ['block5c_expand_c   N          
  (BatchNormalizat  )                           onv[0][0]']                     
 ion)                                                                           
 block5c_expand_ac  (None, 14, 14, 672   0      ['block5c_expand_b   Y          
 tivation (Activat  )                           n[0][0]']                       
 ion)                                                                           
 block5c_dwconv (D  (None, 14, 14, 672   1680   ['block5c_expand_a   Y          
 epthwiseConv2D)    )                    0      ctivation[0][0]']               
 block5c_bn (Batch  (None, 14, 14, 672   2688   ['block5c_dwconv[0   N          
 Normalization)     )                           ][0]']                          
 block5c_activatio  (None, 14, 14, 672   0      ['block5c_bn[0][0]   Y          
 n (Activation)     )                           ']                              
 block5c_se_squeez  (None, 672)          0      ['block5c_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block5c_se_reshap  (None, 1, 1, 672)    0      ['block5c_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block5c_se_reduce  (None, 1, 1, 28)     1884   ['block5c_se_resha   Y          
  (Conv2D)                               4      pe[0][0]']                      
 block5c_se_expand  (None, 1, 1, 672)    1948   ['block5c_se_reduc   Y          
  (Conv2D)                               8      e[0][0]']                       
 block5c_se_excite  (None, 14, 14, 672   0      ['block5c_activati   Y          
  (Multiply)        )                           on[0][0]',                      
                                                 'block5c_se_expan              
                                                d[0][0]']                       
 block5c_project_c  (None, 14, 14, 112   7526   ['block5c_se_excit   Y          
 onv (Conv2D)       )                    4      e[0][0]']                       
 block5c_project_b  (None, 14, 14, 112   448    ['block5c_project_   N          
 n (BatchNormaliza  )                           conv[0][0]']                    
 tion)                                                                          
 block5c_drop (Dro  (None, 14, 14, 112   0      ['block5c_project_   Y          
 pout)              )                           bn[0][0]']                      
 block5c_add (Add)  (None, 14, 14, 112   0      ['block5c_drop[0][   Y          
                    )                           0]',                            
                                                 'block5b_add[0][0              
                                                ]']                             
 block6a_expand_co  (None, 14, 14, 672   7526   ['block5c_add[0][0   Y          
 nv (Conv2D)        )                    4      ]']                             
 block6a_expand_bn  (None, 14, 14, 672   2688   ['block6a_expand_c   N          
  (BatchNormalizat  )                           onv[0][0]']                     
 ion)                                                                           
 block6a_expand_ac  (None, 14, 14, 672   0      ['block6a_expand_b   Y          
 tivation (Activat  )                           n[0][0]']                       
 ion)                                                                           
 block6a_dwconv_pa  (None, 17, 17, 672   0      ['block6a_expand_a   Y          
 d (ZeroPadding2D)  )                           ctivation[0][0]']               
 block6a_dwconv (D  (None, 7, 7, 672)    1680   ['block6a_dwconv_p   Y          
 epthwiseConv2D)                         0      ad[0][0]']                      
 block6a_bn (Batch  (None, 7, 7, 672)    2688   ['block6a_dwconv[0   N          
 Normalization)                                 ][0]']                          
 block6a_activatio  (None, 7, 7, 672)    0      ['block6a_bn[0][0]   Y          
 n (Activation)                                 ']                              
 block6a_se_squeez  (None, 672)          0      ['block6a_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block6a_se_reshap  (None, 1, 1, 672)    0      ['block6a_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block6a_se_reduce  (None, 1, 1, 28)     1884   ['block6a_se_resha   Y          
  (Conv2D)                               4      pe[0][0]']                      
 block6a_se_expand  (None, 1, 1, 672)    1948   ['block6a_se_reduc   Y          
  (Conv2D)                               8      e[0][0]']                       
 block6a_se_excite  (None, 7, 7, 672)    0      ['block6a_activati   Y          
  (Multiply)                                    on[0][0]',                      
                                                 'block6a_se_expan              
                                                d[0][0]']                       
 block6a_project_c  (None, 7, 7, 192)    1290   ['block6a_se_excit   Y          
 onv (Conv2D)                            24     e[0][0]']                       
 block6a_project_b  (None, 7, 7, 192)    768    ['block6a_project_   N          
 n (BatchNormaliza                              conv[0][0]']                    
 tion)                                                                          
 block6b_expand_co  (None, 7, 7, 1152)   2211   ['block6a_project_   Y          
 nv (Conv2D)                             84     bn[0][0]']                      
 block6b_expand_bn  (None, 7, 7, 1152)   4608   ['block6b_expand_c   N          
  (BatchNormalizat                              onv[0][0]']                     
 ion)                                                                           
 block6b_expand_ac  (None, 7, 7, 1152)   0      ['block6b_expand_b   Y          
 tivation (Activat                              n[0][0]']                       
 ion)                                                                           
 block6b_dwconv (D  (None, 7, 7, 1152)   2880   ['block6b_expand_a   Y          
 epthwiseConv2D)                         0      ctivation[0][0]']               
 block6b_bn (Batch  (None, 7, 7, 1152)   4608   ['block6b_dwconv[0   N          
 Normalization)                                 ][0]']                          
 block6b_activatio  (None, 7, 7, 1152)   0      ['block6b_bn[0][0]   Y          
 n (Activation)                                 ']                              
 block6b_se_squeez  (None, 1152)         0      ['block6b_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block6b_se_reshap  (None, 1, 1, 1152)   0      ['block6b_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block6b_se_reduce  (None, 1, 1, 48)     5534   ['block6b_se_resha   Y          
  (Conv2D)                               4      pe[0][0]']                      
 block6b_se_expand  (None, 1, 1, 1152)   5644   ['block6b_se_reduc   Y          
  (Conv2D)                               8      e[0][0]']                       
 block6b_se_excite  (None, 7, 7, 1152)   0      ['block6b_activati   Y          
  (Multiply)                                    on[0][0]',                      
                                                 'block6b_se_expan              
                                                d[0][0]']                       
 block6b_project_c  (None, 7, 7, 192)    2211   ['block6b_se_excit   Y          
 onv (Conv2D)                            84     e[0][0]']                       
 block6b_project_b  (None, 7, 7, 192)    768    ['block6b_project_   N          
 n (BatchNormaliza                              conv[0][0]']                    
 tion)                                                                          
 block6b_drop (Dro  (None, 7, 7, 192)    0      ['block6b_project_   Y          
 pout)                                          bn[0][0]']                      
 block6b_add (Add)  (None, 7, 7, 192)    0      ['block6b_drop[0][   Y          
                                                0]',                            
                                                 'block6a_project_              
                                                bn[0][0]']                      
 block6c_expand_co  (None, 7, 7, 1152)   2211   ['block6b_add[0][0   Y          
 nv (Conv2D)                             84     ]']                             
 block6c_expand_bn  (None, 7, 7, 1152)   4608   ['block6c_expand_c   N          
  (BatchNormalizat                              onv[0][0]']                     
 ion)                                                                           
 block6c_expand_ac  (None, 7, 7, 1152)   0      ['block6c_expand_b   Y          
 tivation (Activat                              n[0][0]']                       
 ion)                                                                           
 block6c_dwconv (D  (None, 7, 7, 1152)   2880   ['block6c_expand_a   Y          
 epthwiseConv2D)                         0      ctivation[0][0]']               
 block6c_bn (Batch  (None, 7, 7, 1152)   4608   ['block6c_dwconv[0   N          
 Normalization)                                 ][0]']                          
 block6c_activatio  (None, 7, 7, 1152)   0      ['block6c_bn[0][0]   Y          
 n (Activation)                                 ']                              
 block6c_se_squeez  (None, 1152)         0      ['block6c_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block6c_se_reshap  (None, 1, 1, 1152)   0      ['block6c_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block6c_se_reduce  (None, 1, 1, 48)     5534   ['block6c_se_resha   Y          
  (Conv2D)                               4      pe[0][0]']                      
 block6c_se_expand  (None, 1, 1, 1152)   5644   ['block6c_se_reduc   Y          
  (Conv2D)                               8      e[0][0]']                       
 block6c_se_excite  (None, 7, 7, 1152)   0      ['block6c_activati   Y          
  (Multiply)                                    on[0][0]',                      
                                                 'block6c_se_expan              
                                                d[0][0]']                       
 block6c_project_c  (None, 7, 7, 192)    2211   ['block6c_se_excit   Y          
 onv (Conv2D)                            84     e[0][0]']                       
 block6c_project_b  (None, 7, 7, 192)    768    ['block6c_project_   N          
 n (BatchNormaliza                              conv[0][0]']                    
 tion)                                                                          
 block6c_drop (Dro  (None, 7, 7, 192)    0      ['block6c_project_   Y          
 pout)                                          bn[0][0]']                      
 block6c_add (Add)  (None, 7, 7, 192)    0      ['block6c_drop[0][   Y          
                                                0]',                            
                                                 'block6b_add[0][0              
                                                ]']                             
 block6d_expand_co  (None, 7, 7, 1152)   2211   ['block6c_add[0][0   Y          
 nv (Conv2D)                             84     ]']                             
 block6d_expand_bn  (None, 7, 7, 1152)   4608   ['block6d_expand_c   N          
  (BatchNormalizat                              onv[0][0]']                     
 ion)                                                                           
 block6d_expand_ac  (None, 7, 7, 1152)   0      ['block6d_expand_b   Y          
 tivation (Activat                              n[0][0]']                       
 ion)                                                                           
 block6d_dwconv (D  (None, 7, 7, 1152)   2880   ['block6d_expand_a   Y          
 epthwiseConv2D)                         0      ctivation[0][0]']               
 block6d_bn (Batch  (None, 7, 7, 1152)   4608   ['block6d_dwconv[0   N          
 Normalization)                                 ][0]']                          
 block6d_activatio  (None, 7, 7, 1152)   0      ['block6d_bn[0][0]   Y          
 n (Activation)                                 ']                              
 block6d_se_squeez  (None, 1152)         0      ['block6d_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block6d_se_reshap  (None, 1, 1, 1152)   0      ['block6d_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block6d_se_reduce  (None, 1, 1, 48)     5534   ['block6d_se_resha   Y          
  (Conv2D)                               4      pe[0][0]']                      
 block6d_se_expand  (None, 1, 1, 1152)   5644   ['block6d_se_reduc   Y          
  (Conv2D)                               8      e[0][0]']                       
 block6d_se_excite  (None, 7, 7, 1152)   0      ['block6d_activati   Y          
  (Multiply)                                    on[0][0]',                      
                                                 'block6d_se_expan              
                                                d[0][0]']                       
 block6d_project_c  (None, 7, 7, 192)    2211   ['block6d_se_excit   Y          
 onv (Conv2D)                            84     e[0][0]']                       
 block6d_project_b  (None, 7, 7, 192)    768    ['block6d_project_   N          
 n (BatchNormaliza                              conv[0][0]']                    
 tion)                                                                          
 block6d_drop (Dro  (None, 7, 7, 192)    0      ['block6d_project_   Y          
 pout)                                          bn[0][0]']                      
 block6d_add (Add)  (None, 7, 7, 192)    0      ['block6d_drop[0][   Y          
                                                0]',                            
                                                 'block6c_add[0][0              
                                                ]']                             
 block7a_expand_co  (None, 7, 7, 1152)   2211   ['block6d_add[0][0   Y          
 nv (Conv2D)                             84     ]']                             
 block7a_expand_bn  (None, 7, 7, 1152)   4608   ['block7a_expand_c   N          
  (BatchNormalizat                              onv[0][0]']                     
 ion)                                                                           
 block7a_expand_ac  (None, 7, 7, 1152)   0      ['block7a_expand_b   Y          
 tivation (Activat                              n[0][0]']                       
 ion)                                                                           
 block7a_dwconv (D  (None, 7, 7, 1152)   1036   ['block7a_expand_a   Y          
 epthwiseConv2D)                         8      ctivation[0][0]']               
 block7a_bn (Batch  (None, 7, 7, 1152)   4608   ['block7a_dwconv[0   N          
 Normalization)                                 ][0]']                          
 block7a_activatio  (None, 7, 7, 1152)   0      ['block7a_bn[0][0]   Y          
 n (Activation)                                 ']                              
 block7a_se_squeez  (None, 1152)         0      ['block7a_activati   Y          
 e (GlobalAverageP                              on[0][0]']                      
 ooling2D)                                                                      
 block7a_se_reshap  (None, 1, 1, 1152)   0      ['block7a_se_squee   Y          
 e (Reshape)                                    ze[0][0]']                      
 block7a_se_reduce  (None, 1, 1, 48)     5534   ['block7a_se_resha   Y          
  (Conv2D)                               4      pe[0][0]']                      
 block7a_se_expand  (None, 1, 1, 1152)   5644   ['block7a_se_reduc   Y          
  (Conv2D)                               8      e[0][0]']                       
 block7a_se_excite  (None, 7, 7, 1152)   0      ['block7a_activati   Y          
  (Multiply)                                    on[0][0]',                      
                                                 'block7a_se_expan              
                                                d[0][0]']                       
 block7a_project_c  (None, 7, 7, 320)    3686   ['block7a_se_excit   Y          
 onv (Conv2D)                            40     e[0][0]']                       
 block7a_project_b  (None, 7, 7, 320)    1280   ['block7a_project_   N          
 n (BatchNormaliza                              conv[0][0]']                    
 tion)                                                                          
 top_conv (Conv2D)  (None, 7, 7, 1280)   4096   ['block7a_project_   Y          
                                         00     bn[0][0]']                      
 top_bn (BatchNorm  (None, 7, 7, 1280)   5120   ['top_conv[0][0]']   N          
 alization)                                                                     
 top_activation (A  (None, 7, 7, 1280)   0      ['top_bn[0][0]']     Y          
 ctivation)                                                                     
 avg_pool (GlobalA  (None, 1280)         0      ['top_activation[0   Y          
 veragePooling2D)                               ][0]']                          
 top_dropout (Drop  (None, 1280)         0      ['avg_pool[0][0]']   Y          
 out)                                                                           
 predictions (Dens  (None, 1000)         1281   ['top_dropout[0][0   Y          
 e)                                      000    ]']                             
================================================================================
Total params: 5330571 (20.33 MB)
Trainable params: 5246532 (20.01 MB)
Non-trainable params: 84039 (328.28 KB)
________________________________________________________________________________

14.1.3 Konwolucje separowalne

A gdybym powiedział, że istnieje warstwa, której możemy użyć jako zamiennika layer_ conv_2d(), która sprawi, że nasz model będzie mniejszy (mniej trenowanych parametrów) i szczuplejszy (mniej operacji zmiennoprzecinkowych) i spowoduje, że wykona kilka punktów procentowych lepiej swoje zadanie? To właśnie robi warstwa konwolucji separowalnej wgłębnie (layer_separable_conv_2d() w keras). Warstwa ta wykonuje konwolucję przestrzenną na każdym kanale swojego wejścia, niezależnie, przed zmieszaniem kanałów wyjściowych poprzez konwolucję punktową (konwolucja 1 × 1), jak pokazano na Rysunek 14.8

Rysunek 14.8: Konwolucja separowalna

Jest to równoznaczne z rozdzieleniem uczenia się cech przestrzennych i uczenia się cech kanałowych. W podobny sposób, jak konwolucja opiera się na założeniu, że wzory w obrazach nie są związane z konkretnymi lokalizacjami, konwolucja separowalna wgłębnie opiera się na założeniu, że lokalizacje przestrzenne w aktywacjach pośrednich są wysoce skorelowane, ale różne kanały są wysoce niezależne . Ponieważ założenie to jest generalnie prawdziwe dla reprezentacji obrazów uczonych przez głębokie sieci neuronowe, służy jako użyteczne założenie, które pomaga modelowi bardziej efektywnie wykorzystać dane treningowe. Model z silniejszymi założeniami dotyczącymi struktury informacji, które będzie musiał przetworzyć, jest lepszym modelem - o ile założenia te są poprawne.

Konwolucja separowalna wymaga znacznie mniej parametrów i wymaga mniejszej ilości obliczeń niż zwykła konwolucja, a jednocześnie ma porównywalną moc reprezentacji. W rezultacie otrzymujemy mniejsze modele, które szybciej osiągają zbieżność i są mniej podatne na overfitting. Te zalety stają się szczególnie ważne, gdy trenujemy małe modele od podstaw na ograniczonych zbiorach danych.

Konwolucje separowalne wgłębnie są podstawą architektury Xception, wysokowydajnej sieci splotowej.

14.1.4 Przykład wykorzystania sieci Xception

Dla przypomnienia, oto zasady architektury sieci splotowej, które poznaliśmy do tej pory:

  • Nasz model powinien być zorganizowany w powtarzające się bloki warstw, zwykle składających się z wielu warstw konwolucji i warstwy max pooling.
  • Liczba filtrów w naszych warstwach powinna rosnąć wraz ze zmniejszaniem się rozmiaru przestrzennych map cech.
  • Głębokie i wąskie sieci są lepsze niż szerokie i płytkie.
  • Wprowadzenie połączeń rezydualnych wokół bloków warstw pomaga trenować głębsze sieci.
  • Korzystne może być wprowadzenie warstw normalizacji partii po twoich warstwach konwolucji.
  • Korzystne może być zastąpienie layer_conv_2d() przez layer_separable_conv_2d(), które są bardziej wydajne pod względem parametrów.

Zbierzmy te pomysły razem w jeden model. Jego architektura będzie przypominać sieć Xception. Zastosujemy ją do zadania psy vs. koty z poprzedniego rozdziału.

Kod
data_augmentation <- keras_model_sequential() |>
  layer_random_flip("horizontal") |>
  layer_random_rotation(0.1) |>
  layer_random_zoom(0.2)

inputs <- layer_input(shape = c(180, 180, 3))

x <- inputs |>
  data_augmentation() |>
  layer_rescaling(scale = 1 / 255)

x <- x |>
  layer_conv_2d(32, 5, use_bias = FALSE)

for (size in c(32, 64, 128, 256, 512)) {
  residual <- x

  x <- x |>
    layer_batch_normalization() |>
    layer_activation("relu") |>
    layer_separable_conv_2d(size, 3, padding = "same", use_bias = FALSE) |>
    layer_batch_normalization() |>
    layer_activation("relu") |>
    layer_separable_conv_2d(size, 3, padding = "same", use_bias = FALSE) |>
    layer_max_pooling_2d(pool_size = 3, strides = 2, padding = "same")

  residual <- residual |>
    layer_conv_2d(size, 1, strides = 2, padding = "same", use_bias = FALSE)

  x <- layer_add(list(x, residual))
}

outputs <- x |>
  layer_global_average_pooling_2d() |>
  layer_dropout(0.5) |>
  layer_dense(1, activation = "sigmoid")

model <- keras_model(inputs, outputs)

train_dataset <- image_dataset_from_directory(
  "/Users/majerek/Downloads/cats_and_dogs_small/train/",
  image_size = c(180, 180),
  batch_size = 32
)

validation_dataset <- image_dataset_from_directory(
  "/Users/majerek/Downloads/cats_and_dogs_small/validation",
  image_size = c(180, 180),
  batch_size = 32
)

callbacks <- list(callback_model_checkpoint(filepath = "models/vgg16_cat_dog.keras", save_best_only = T))

model |>
  keras::compile(
    loss = "binary_crossentropy",
    optimizer = "rmsprop",
    metrics = "accuracy"
  )

history <- model |>
  fit(
    train_dataset,
    epochs = 90,
    callbacks = callbacks,
    validation_data = validation_dataset
  )
Kod
model <- load_model_tf("models/vgg16_cat_dog.keras")
load("models/vgg16_cat_dog_hist.rda")
plot(history)

Nasz nowy model osiąga dokładność testu 90,2%, w porównaniu z 83% dla modelu z poprzedniego rozdziału. Jak widać, stosowanie najlepszych architektur ma natychmiastowy, znaczący wpływ na wydajność modelu! Chcąc jeszcze bardziej poprawić wydajność, powinniśmy zacząć systematycznie dostrajać hiperparametry naszej architektury.

14.2 Co widzi sieć konwolucyjna?

Podstawowym problemem podczas budowania aplikacji wizji komputerowej jest kwestia możliwości interpretacji: dlaczego nasz klasyfikator uznał, że dany obraz zawiera lodówkę, podczas gdy wszystko, co widzimy, to ciężarówka? Jest to szczególnie istotne w przypadkach użycia, w których głębokie uczenie jest wykorzystywane do uzupełnienia ludzkiej wiedzy, jak na przykład w przypadkach użycia obrazowania medycznego. Zakończymy ten rozdział zapoznając Cię z szeregiem różnych technik wizualizacji tego, czego uczą się sieci splotowe i zrozumienia podejmowanych przez nie decyzji.

Często mówi się, że modele głębokiego uczenia są “czarnymi skrzynkami”: uczą się reprezentacji, które są trudne do wyodrębnienia i przedstawienia w formie czytelnej dla człowieka. Chociaż jest to częściowo prawdą w przypadku niektórych typów modeli głębokiego uczenia, to zdecydowanie nie jest to prawdą w przypadku sieci splotowych. Reprezentacje uczone przez sieci konwolucyjne są wygodne w wizualizacji, w dużej mierze dlatego, że są to reprezentacje wizualnych koncepcji. Od 2013 roku opracowano szeroki wachlarz technik wizualizacji i interpretacji tych reprezentacji. Nie będziemy badać ich wszystkich, ale omówimy trzy z najbardziej przystępnych i użytecznych:

  • Wizualizacja pośrednich wyjść sieci (pośrednich aktywacji) - przydatna do zrozumienia, jak kolejne warstwy sieci przekształcają swoje dane wejściowe, oraz do uzyskania pierwszego wyobrażenia o znaczeniu poszczególnych filtrów sieci.
  • Wizualizacja filtrów sieci splotowych - przydatna do dokładnego zrozumienia, na jaki wzór wizualny lub pojęcie reaguje każdy filtr w sieci.
  • Wizualizacja map ciepła aktywacji klas w obrazie - przydatna do zrozumienia, które części obrazu zostały zidentyfikowane jako należące do danej klasy, co pozwala na lokalizację obiektów w obrazach.

Do pierwszej metody - wizualizacji aktywacji - użyjemy małej sieci, którą wytrenowaliśmy od podstaw na problemie klasyfikacji psy-vs-koty. W przypadku dwóch kolejnych metod użyjemy wstępnie wytrenowanego modelu Xception.

14.2.0.1 Wizualizacja aktywacji pośrednich

Wizualizacja pośrednich aktywacji polega na wyświetleniu wartości zwracanych przez różne warstwy konwolucji i łączenia w modelu, przy określonym wejściu (wyjście warstwy często nazywane jest jej aktywacją, wyjściem funkcji aktywacji). Daje to wgląd w to, jak dane wejściowe są rozkładane na różne filtry uczone przez sieć. Chcemy wizualizować mapy cech o trzech wymiarach: szerokości, wysokości i głębokości (kanały). Każdy kanał koduje względnie niezależne cechy, więc właściwym sposobem wizualizacji tych map cech jest niezależne wykreślenie zawartości każdego kanału jako obrazu 2D. Zacznijmy od załadowania modelu:

Kod
model <- load_model_tf("models/convnet_from_scratch_with_augmentation.keras")
model
Model: "model"
________________________________________________________________________________
 Layer (type)                       Output Shape                    Param #     
================================================================================
 input_1 (InputLayer)               [(None, 180, 180, 3)]           0           
 sequential (Sequential)            (None, 180, 180, 3)             0           
 rescaling (Rescaling)              (None, 180, 180, 3)             0           
 conv2d_4 (Conv2D)                  (None, 178, 178, 32)            896         
 max_pooling2d_3 (MaxPooling2D)     (None, 89, 89, 32)              0           
 conv2d_3 (Conv2D)                  (None, 87, 87, 64)              18496       
 max_pooling2d_2 (MaxPooling2D)     (None, 43, 43, 64)              0           
 conv2d_2 (Conv2D)                  (None, 41, 41, 128)             73856       
 max_pooling2d_1 (MaxPooling2D)     (None, 20, 20, 128)             0           
 conv2d_1 (Conv2D)                  (None, 18, 18, 256)             295168      
 max_pooling2d (MaxPooling2D)       (None, 9, 9, 256)               0           
 conv2d (Conv2D)                    (None, 7, 7, 256)               590080      
 flatten (Flatten)                  (None, 12544)                   0           
 dropout (Dropout)                  (None, 12544)                   0           
 dense (Dense)                      (None, 1)                       12545       
================================================================================
Total params: 991041 (3.78 MB)
Trainable params: 991041 (3.78 MB)
Non-trainable params: 0 (0.00 Byte)
________________________________________________________________________________

Następnie pobieramy obraz wejściowy - zdjęcie kota, które nie jest częścią obrazów, na których sieć była trenowana.

Kod
img_path <- get_file(
  fname = "cat.jpg",
  origin = "https://img-datasets.s3.amazonaws.com/cat.jpg")

img_tensor <- img_path %>%
  tf_read_image(resize = c(180, 180))

display_image_tensor(img_tensor)

Aby wyodrębnić mapy cech, które chcemy obejrzeć, stworzymy model Keras, który przyjmuje partie obrazów jako dane wejściowe i który wyprowadza aktywacje wszystkich warstw konwolucji.

Kod
conv_layer_s3_classname <- class(layer_conv_2d(NULL, 1, 1))[1]
pooling_layer_s3_classname <- class(layer_max_pooling_2d(NULL))[1]

is_conv_layer <- function(x) inherits(x, conv_layer_s3_classname)
is_pooling_layer <- function(x) inherits(x, pooling_layer_s3_classname)

layer_outputs <- list()
for (layer in model$layers)
  if (is_conv_layer(layer) || is_pooling_layer(layer))
    layer_outputs[[layer$name]] <- layer$output

activation_model <- keras_model(inputs = model$input,
                                outputs = layer_outputs)

Po podaniu obrazu wejściowego, model ten zwraca wartości aktywacji warstw w oryginalnym modelu, w postaci listy. Ten model ma jedno wejście i osiem wyjść: jedno wyjście na każdą aktywację warstwy.

Kod
activations <- activation_model |> 
  predict(img_tensor[tf$newaxis,,,])
1/1 - 0s - 69ms/epoch - 69ms/step
Kod
str(activations)
List of 9
 $ conv2d_4       : num [1, 1:178, 1:178, 1:32] 0 0 0 0 0 0 0 0 0 0 ...
 $ max_pooling2d_3: num [1, 1:89, 1:89, 1:32] 0 0 0 0 0 0 0 0 0 0 ...
 $ conv2d_3       : num [1, 1:87, 1:87, 1:64] 0.0393 0.0651 0.0795 0.0466 0.0943 ...
 $ max_pooling2d_2: num [1, 1:43, 1:43, 1:64] 0.0651 0.0795 0.0961 0.0799 0.1202 ...
 $ conv2d_2       : num [1, 1:41, 1:41, 1:128] 0.1349 0.1584 0.1228 0.0913 0 ...
 $ max_pooling2d_1: num [1, 1:20, 1:20, 1:128] 0.16708 0.12279 0.00736 0 0 ...
 $ conv2d_1       : num [1, 1:18, 1:18, 1:256] 0 0 0 0 0 0 0 0 0 0 ...
 $ max_pooling2d  : num [1, 1:9, 1:9, 1:256] 0 0 0 0 0 0 0 0 0 0 ...
 $ conv2d         : num [1, 1:7, 1:7, 1:256] 0 0 0 0 0.102 ...

Przyjrzyjmy się bliżej aktywacjom pierwszej warstwy.

Kod
first_layer_activation <- activations[[names(layer_outputs)[1]]]
dim(first_layer_activation)
[1]   1 178 178  32

Jest to mapa funkcji o wymiarach 178 × 178 z 32 kanałami. Spróbujmy wykreślić trzeci kanał aktywacji pierwszej warstwy oryginalnego modelu (patrz Rysunek 14.9).

Kod
plot_activations <- function(x, ...) {

  x <- as.array(x)

  if(sum(x) == 0)
      return(plot(as.raster("gray")))

  rotate <- function(x) t(apply(x, 2, rev))
  image(rotate(x), asp = 1, axes = FALSE, useRaster = TRUE,
        col = terrain.colors(256), ...)
}

plot_activations(first_layer_activation[, , , 3])
Rysunek 14.9: Wykres wartości trzeciego kanału pierwszej warstwy aktywacji

Kanał ten wydaje się kodować krawędzi ukośne - ale zauważmy, że nasze kanały mogą się różnić, ponieważ konkretne filtry uczone przez warstwy konwolucji nie są deterministyczne.

Wykreślmy teraz pełną wizualizację wszystkich aktywacji w sieci. Wyodrębnimy i wykreślimy każdy kanał w każdej z warstw aktywacji, a następnie ułożymy wyniki w jedną dużą siatkę, z kanałami ułożonymi obok siebie.

Kod
for (layer_name in names(layer_outputs)) {
  layer_output <- activations[[layer_name]]

  n_features <- dim(layer_output) %>% tail(1)
  par(mfrow = n2mfrow(n_features, asp = 1.75),
      mar = rep(.1, 4), oma = c(0, 0, 1.5, 0))
  for (j in 1:n_features)
    plot_activations(layer_output[, , , j])
  title(main = layer_name, outer = TRUE)
}

Należy tu zwrócić uwagę na kilka rzeczy:

  • Pierwsza warstwa działa jako zbiór różnych detektorów krawędzi. Na tym etapie, aktywacje zachowują prawie wszystkie informacje obecne na początkowym obrazie.
  • W miarę zagłębiania się, aktywacje stają się coraz bardziej abstrakcyjne i mniej wizualnie interpretowalne. Zaczynają kodować pojęcia wyższego rzędu, takie jak “kocie ucho” czy “kocie oko”. Głębsze prezentacje niosą coraz mniej informacji o wizualnej zawartości obrazu, a coraz więcej informacji związanych z klasą obrazu.
  • Rozproszenie aktywacji rośnie wraz z głębokością warstwy: w pierwszej warstwie prawie wszystkie filtry są aktywowane przez obraz wejściowy, ale w kolejnych warstwach coraz więcej filtrów jest pustych. Oznacza to, że wzór zakodowany przez filtr nie występuje na obrazie wejściowym.

Wykazaliśmy właśnie ważną, uniwersalną cechę reprezentacji uczonych przez głębokie sieci neuronowe: cechy wydobywane przez warstwę stają się coraz bardziej abstrakcyjne wraz z głębokością warstwy. Aktywacje wyższych warstw niosą coraz mniej informacji o konkretnym widzianym wejściu, a coraz więcej informacji o celu (w tym przypadku klasie obrazu: kot czy pies). Głęboka sieć neuronowa działa jak przebieg destylacji informacji, w którym surowe dane (w tym przypadku obrazy RGB) są wielokrotnie przekształcane w taki sposób, że nieistotne informacje są odfiltrowywane (np. specyficzny wygląd wizualny obrazu), a użyteczne informacje są eksponowane i udoskonalane.

Jest to podobne do sposobu, w jaki ludzie i zwierzęta postrzegają świat: po obserwacji sceny przez kilka sekund człowiek może pamiętać, jakie abstrakcyjne obiekty były w niej obecne (rower, drzewo), ale może nie pamiętać konkretnego wyglądu tych obiektów. W rzeczywistości, gdybyśmy próbowali narysować z pamięci ogólny rower, istnieje prawdopodobieństwo, że nie udałoby się to nawet w najmniejszym stopniu, mimo że w swoim życiu widzieliśmy tysiące rowerów. Nasz mózg nauczył się całkowicie abstrahować od informacji wizualnych - przekształcać je w wysokopoziomowe koncepcje wizualne, jednocześnie odfiltrowując nieistotne szczegóły wizualne - co sprawia, że zapamiętanie wyglądu rzeczy wokół nas jest niezwykle trudne.

14.2.0.2 Wizualizacja filtrów sieci splotowych

Innym prostym sposobem na sprawdzenie filtrów wyuczonych przez sieci splotowe jest wyświetlenie wzorca wizualnego, na który każdy filtr ma reagować. Można to zrobić za pomocą spadku gradientu w przestrzeni wejściowej: zastosowanie spadku gradientu do wartości obrazu wejściowego sieci splotowej tak, aby zmaksymalizować odpowiedź określonego filtra, zaczynając od pustego obrazu wejściowego. Wynikowy obraz wejściowy będzie taki, na który wybrany filtr maksymalnie reaguje.

Spróbujmy to zrobić z filtrami modelu Xception, wytrenowanego na ImageNet. Proces jest prosty: zbudujemy funkcję straty, która maksymalizuje wartość danego filtra w danej warstwie konwolucji, a następnie użyjemy stochastycznego spadku gradientu, aby dopasować wartości obrazu wejściowego tak, by zmaksymalizować tę wartość aktywacji. Najpierw zainicjujmy model Xception, załadowany wagami wytrenowanymi na zbiorze danych ImageNet.

Kod
model <- application_xception(
  weights = "imagenet",
  include_top = FALSE
)

Interesują nas warstwy konwolucyjne modelu - Conv2D i SeparableConv2D. Musimy znać ich nazwy, aby móc pobrać ich dane wyjściowe. Wypiszmy ich nazwy, w kolejności głębokości.

Kod
for (layer in model$layers)
 if(any(grepl("Conv2D", class(layer))))
   print(layer$name)
[1] "block1_conv1"
[1] "block1_conv2"
[1] "block2_sepconv1"
[1] "block2_sepconv2"
[1] "conv2d_23"
[1] "block3_sepconv1"
[1] "block3_sepconv2"
[1] "conv2d_24"
[1] "block4_sepconv1"
[1] "block4_sepconv2"
[1] "conv2d_25"
[1] "block5_sepconv1"
[1] "block5_sepconv2"
[1] "block5_sepconv3"
[1] "block6_sepconv1"
[1] "block6_sepconv2"
[1] "block6_sepconv3"
[1] "block7_sepconv1"
[1] "block7_sepconv2"
[1] "block7_sepconv3"
[1] "block8_sepconv1"
[1] "block8_sepconv2"
[1] "block8_sepconv3"
[1] "block9_sepconv1"
[1] "block9_sepconv2"
[1] "block9_sepconv3"
[1] "block10_sepconv1"
[1] "block10_sepconv2"
[1] "block10_sepconv3"
[1] "block11_sepconv1"
[1] "block11_sepconv2"
[1] "block11_sepconv3"
[1] "block12_sepconv1"
[1] "block12_sepconv2"
[1] "block12_sepconv3"
[1] "block13_sepconv1"
[1] "block13_sepconv2"
[1] "conv2d_26"
[1] "block14_sepconv1"
[1] "block14_sepconv2"

Zauważmy, że separowalne warstwy conv2D tutaj są nazwane block6_sepconv1, block7_sepconv2, i tak dalej. Xception jest zorganizowany w bloki, z których każdy zawiera kilka warstw konwolucyjnych. Teraz stwórzmy drugi model, który zwraca wyjście konkretnej warstwy - model ekstraktora cech. Ponieważ nasz model jest modelem zbudowanym za pomocą API, jest on inspekcyjny: możemy zapytać o wyjście jednej z jego warstw i ponownie użyć go w nowym modelu. Nie trzeba kopiować całego kodu Xception.

Kod
layer_name <- "block3_sepconv1"
layer <- model %>% keras::get_layer(name = layer_name)
feature_extractor <- keras_model(inputs = model$input,
                                 outputs = layer$output)

Aby użyć ekstraktora, wystarczy wywołać go na jakichś danych wejściowych (zauważmy, że Xception wymaga, aby dane wejściowe były wstępnie przetworzone za pomocą funkcji xception_preprocess_input()).

Kod
activation <- img_tensor %>%
   .[tf$newaxis, , , ] %>%
   xception_preprocess_input() %>%
   feature_extractor()

str(activation)
<tf.Tensor: shape=(1, 44, 44, 256), dtype=float32, numpy=…>

Użyjmy naszego modelu ekstraktora cech do zdefiniowania funkcji, która zwraca wartość skalarną określającą, jak bardzo dany obraz wejściowy “aktywuje” dany filtr w warstwie. Jest to “funkcja straty”, którą będziemy maksymalizować podczas procesu wznoszenia gradientu:

Kod
compute_loss <- function(image, filter_index) {
  activation <- feature_extractor(image)

  filter_index <- as_tensor(filter_index, "int32")
  filter_activation <-
      activation[, , , filter_index, style = "python"] # aby program wiedział, że kodowanie pierwszego indeksu zaczyna się od 0 - jak w Python

  mean(filter_activation[, 3:-3, 3:-3])
}

Ustawmy funkcję krokową gradientu zstępującego, korzystając z funkcji GradientTape(). Nieoczywistą sztuczką pomagającą w płynnym przebiegu procesu spadku gradientu jest normalizacja tensora gradientu poprzez podzielenie go przez jego normę L2 (pierwiastek kwadratowy ze średniej kwadratowej wartości w tensorze). Dzięki temu wielkość aktualizacji obrazu wejściowego jest zawsze w tym samym zakresie.

Kod
gradient_ascent_step <-
  function(image, filter_index, learning_rate) {
    with(tf$GradientTape() %as% tape, {
      tape$watch(image)
      loss <- compute_loss(image, filter_index)
    })
    grads <- tape$gradient(loss, image)
    grads <- tf$math$l2_normalize(grads)
    image + (learning_rate * grads)
  }

Teraz mamy już wszystkie elementy. Połączmy je w funkcję R, która pobiera jako dane wejściowe nazwę warstwy i indeks filtra i zwraca tensor reprezentujący wzór, który maksymalizuje aktywację określonego filtra. Zauważmy, że użyjemy funkcji tf_function(), aby przyspieszyć działanie.

Kod
c(img_width, img_height) %<-% c(200, 200)

generate_filter_pattern <- tf_function(function(filter_index) {
  iterations <- 30
  learning_rate <- 10
  image <- tf$random$uniform(
    minval = 0.4, maxval = 0.6,
    shape = shape(1, img_width, img_height, 3)
  )

  for (i in seq(iterations))
      image <- gradient_ascent_step(image, filter_index, learning_rate)
  image[1, , , ]
})

Wynikowy tensor obrazu jest tablicą zmiennoprzecinkową o kształcie (200, 200, 3), z wartościami, które nie mogą być liczbami całkowitymi z zakresu [0, 255]. Dlatego też musimy poddać ten tensor postprocessingowi, aby przekształcić go w obraz możliwy do wyświetlenia. Zrobimy to za pomocą operacji na tensorach i zawiniemy w tf_function(), aby również przyspieszyć.

Kod
deprocess_image <- tf_function(function(image, crop = TRUE) {
  image <- image - mean(image)
  image <- image / tf$math$reduce_std(image)
  image <- (image * 64) + 128
  image <- tf$clip_by_value(image, 0, 255)
  if(crop)
    image <- image[26:-26, 26:-26, ]
  image
})

generate_filter_pattern(filter_index = as_tensor(2L)) %>%
 deprocess_image() %>%
  display_image_tensor()

Zauważmy, że złożyliśmy tutaj filter_index z as_tensor(). Robimy to, ponieważ tf_funkcja() kompiluje oddzielną zoptymalizowaną funkcję dla każdego unikalnego sposobu jej wywoływana, a różne stałe liczą się jako unikalna sygnatura wywołania. Wygląda na to, że trzeci filtr w warstwie block3_sepconv1 reaguje na wzór poziomych linii, nieco przypominający wodę.

Kod
par(mfrow = c(8, 8))
for (i in seq(0, 63)) {
  generate_filter_pattern(filter_index = as_tensor(i)) %>%
    deprocess_image() %>%
    display_image_tensor(plot_margins = rep(.1, 4))
}
Rysunek 14.10: Kilka wzorów filtrów dla warstw block2_sepconv1, block4_sepconv1 i block8_sepconv1

Te wizualizacje filtrów (patrz Rysunek 14.10) mówią wiele o tym, jak warstwy sieci splotowej widzą świat: każda warstwa w sieci splotowej uczy się zbioru filtrów w taki sposób, że jej dane wejściowe mogą być wyrażone jako kombinacja filtrów. Jest to podobne do tego, jak transformata Fouriera rozkłada sygnały na funkcje trygonometryczne. Filtry w tych blokach filtrów sieci splotowej stają się coraz bardziej złożone i wyrafinowane, gdy zagłębiamy się w model:

  • Filtry z pierwszych warstw w modelu kodują proste krawędzie kierunkowe i kolory (lub kolorowe krawędzie, w niektórych przypadkach).
  • Filtry z warstw położonych nieco dalej w górę stosu, takich jak block4_sepconv1, kodują proste tekstury wykonane z kombinacji krawędzi i kolorów.
  • Filtry z wyższych warstw zaczynają przypominać tekstury występujące w naturalnych obrazach: pióra, oczy, liście i tak dalej.

14.2.0.3 Wizualizacja map ciepła aktywacji klasowych

Przedstawimy jeszcze jedną technikę wizualizacji - przydatną do zrozumienia, które części danego obrazu doprowadziły sieci splotowe do ostatecznej decyzji klasyfikacyjnej. Jest to pomocne w “debugowaniu” procesu decyzyjnego sieci konwolucyjnych, szczególnie w przypadku błędu klasyfikacji (problem nazywany interpretacją modelu). Może to również pozwolić na zlokalizowanie konkretnych obiektów na obrazie.

Ta ogólna kategoria technik nazywana jest wizualizacją mapy aktywacji klas (ang. Class Activation Map - CAM) i polega na tworzeniu map ciepła aktywacji klas na obrazach wejściowych. Mapa ciepła aktywacji klas to dwuwymiarowa siatka wyników związanych z konkretną klasą wyjściową, obliczona dla każdej lokalizacji na dowolnym obrazie wejściowym, wskazująca jak ważna jest każda lokalizacja w odniesieniu do rozważanej klasy. Na przykład, biorąc pod uwagę obrazek wprowadzony do sieci splotowej psy-vs-koty, wizualizacja CAM pozwoli nam wygenerować mapę ciepła dla klasy “kot”, wskazując jak podobne do kotów są różne części obrazka, a także mapę ciepła dla klasy “pies”, wskazując jak podobne do psów są części obrazka.

Konkretna implementacja, której użyjemy, to ta opisana w artykule zatytułowanym “Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization”(Selvaraju i in. 2020).

Grad-CAM polega na tym, że bierzemy wyjściową mapę cech warstwy konwolucji, daną obrazowi wejściowemu, i ważymy każdy kanał w tej mapie cech przez gradient klasy względem kanału. Intuicyjnie, jednym ze sposobów zrozumienia tej sztuczki jest wyobrażenie sobie, że ważymy przestrzenną mapę “jak intensywnie obraz wejściowy aktywuje różne kanały” przez “jak ważny jest każdy kanał w odniesieniu do klasy”, w wyniku czego otrzymujemy przestrzenną mapę “jak intensywnie obraz wejściowy aktywuje klasę”. Zademonstrujmy tę technikę przy użyciu wstępnie wytrenowanego modelu Xception.

Kod
model <- application_xception(weights = "imagenet")

Rozważmy obraz dwóch słoni afrykańskich pokazany na Rysunek 14.11. Przekształćmy ten obraz w coś, co model Xception może odczytać: model był trenowany na obrazach o rozmiarze 299 × 299, wstępnie przetworzonych zgodnie z kilkoma regułami, które są spakowane w funkcji użytkowej xception_preprocess_input().

Kod
img_path <- get_file(
  fname = "elephant.jpg",
  origin = "https://img-datasets.s3.amazonaws.com/elephant.jpg")
img_tensor <- tf_read_image(img_path, resize = c(299, 299))
preprocessed_img <- img_tensor[tf$newaxis, , , ] %>%
  xception_preprocess_input()

display_image_tensor(img_tensor)
Rysunek 14.11: Przykładowy obraz słoni

Możemy teraz uruchomić wstępnie wytrenowaną sieć na obrazie i zdekodować jej wektor predykcji z powrotem do formatu czytelnego dla człowieka:

Kod
preds <- predict(model, preprocessed_img)
1/1 - 1s - 517ms/epoch - 517ms/step
Kod
str(preds)
 num [1, 1:1000] 5.51e-06 2.75e-05 1.73e-05 1.19e-05 1.15e-05 ...
Kod
imagenet_decode_predictions(preds, top=3)[[1]]
  class_name class_description      score
1  n02504458  African_elephant 0.90519828
2  n01871265            tusker 0.05259836
3  n02504013   Indian_elephant 0.01615973

Trzy najlepsze klasy przewidywane dla tego obrazu to:

  • Słoń afrykański (z 90% prawdopodobieństwem)
  • Tusker (z 5% prawdopodobieństwem)
  • Słoń indyjski (z niemal 2% prawdopodobieństwem)

Sieć rozpoznała obraz jako zawierający nieokreśloną ilość słoni afrykańskich. Wpisem w wektorze predykcji, który został maksymalnie aktywowany, jest wpis odpowiadający klasie “słoń afrykański”, o indeksie 387:

Kod
which.max(preds[1, ])
[1] 387

Aby zwizualizować, które części obrazu są najbardziej podobne do afrykańskich słoni, skonfigurujmy proces Grad-CAM. Najpierw tworzymy model, który mapuje obraz wejściowy na aktywacje ostatniej warstwy konwolucyjnej.

Kod
last_conv_layer_name <- "block14_sepconv2_act"
classifier_layer_names <- c("avg_pool", "predictions")
last_conv_layer <- model %>% get_layer(last_conv_layer_name)
last_conv_layer_model <- keras_model(model$inputs,
                                     last_conv_layer$output)

Po drugie, tworzymy model, który odwzorowuje aktywacje ostatniej warstwy konwolucyjnej na końcowe predykcje klas.

Kod
classifier_input <- layer_input(batch_shape = last_conv_layer$output$shape)

x <- classifier_input
for (layer_name in classifier_layer_names)
 x <- get_layer(model, layer_name)(x)

classifier_model <- keras_model(classifier_input, x)

Następnie obliczamy gradient najwyższej przewidywanej klasy dla naszego obrazu wejściowego w odniesieniu do aktywacji ostatniej warstwy konwolucji.

Kod
with (tf$GradientTape() %as% tape, {
  last_conv_layer_output <- last_conv_layer_model(preprocessed_img)
  tape$watch(last_conv_layer_output)
  preds <- classifier_model(last_conv_layer_output)
  top_pred_index <- tf$argmax(preds[1, ])
  top_class_channel <- preds[, top_pred_index, style = "python"]
})

grads <- tape$gradient(top_class_channel, last_conv_layer_output)

Teraz zastosujemy łączenie i ważenie ważności do tensora gradientu, aby uzyskać naszą mapę ciepła aktywacji klas.

Kod
pooled_grads <- mean(grads, axis = c(1,2,3), keepdims = T)
heatmap <- (last_conv_layer_output * pooled_grads) |> 
  mean(axis = -1) %>%
  .[1,,]

par(mar = c(0, 0, 0, 0))
plot_activations(heatmap)

Na koniec, nałóżmy mapę ciepła aktywacji na oryginalny obraz. Wycinamy wartości mapy ciepła do palety kolorów, a następnie konwertujemy do obiektu rastrowego R. Zwróćmy uwagę, że upewniliśmy się, że przekazaliśmy alfa = .4 do palety, tak abyśmy nadal widzieli oryginalny obraz, gdy nałożymy na niego mapę ciepła (Zobacz Rysunek 14.12).

Kod
pal <- hcl.colors(256, palette = "Spectral", alpha = .4, rev = TRUE)
heatmap <- as.array(heatmap)
heatmap[] <- pal[cut(heatmap, 256)]
heatmap <- as.raster(heatmap)

img <- tf_read_image(img_path, resize = NULL)
display_image_tensor(img)
rasterImage(heatmap, 0, 0, ncol(img), nrow(img), interpolate = FALSE)
Rysunek 14.12: Mapa cieplna aktywacji klasy słonia afrykańskiego na zdjęciu testowym

Ta technika wizualizacji odpowiada na dwa ważne pytania:

  • Dlaczego sieć uznała, że ten obraz zawiera słonia afrykańskiego?
  • Gdzie na obrazie znajduje się słoń afrykański?

W szczególności interesujące jest to, że uszy słoniowego malca są silnie aktywowane: prawdopodobnie w ten sposób sieć potrafi odróżnić słonie afrykańskie od indyjskich.