Posit AI Weblog: entrenar en R, ejecutar en Android: segmentación de imágenes con antorcha


En cierto sentido, la segmentación de imágenes no es tan diferente de la clasificación de imágenes. Es solo que en lugar de categorizar una imagen como un todo, la segmentación da como resultado una etiqueta para cada píxel. Y como en la clasificación de imágenes, las categorías de interés dependen de la tarea: primer plano versus fondo, digamos; diferentes tipos de tejido; diferentes tipos de vegetación; etcétera.

La presente publicación no es la primera en este weblog en tratar ese tema; y como todos los anteriores, hace uso de una arquitectura U-Web para lograr su objetivo. Las características centrales (de esta publicación, no de U-Web) son:

  1. Demuestra cómo realizar el aumento de datos para una tarea de segmentación de imágenes.

  2. Usa luz, torchinterfaz de alto nivel, para entrenar el modelo.

  3. Él Seguimientos JIT el modelo entrenado y lo guarda para su implementación en dispositivos móviles. (JIT es el acrónimo comúnmente utilizado para el torch compilador justo a tiempo).

  4. Incluye código de prueba de concepto (aunque no una discusión) del modelo guardado que se ejecuta en Android.

Y si cree que esto en sí mismo no es lo suficientemente emocionante, nuestra tarea aquí es encontrar gatos y perros. ¿Qué podría ser más útil que una aplicación móvil que te asegure que puedes distinguir a tu gato del mullido sofá en el que está descansando?

Un gato del Oxford Pet Dataset (Parkhi et al. (2012)).

Tren en R

Comenzamos preparando los datos.

Preprocesamiento y aumento de datos

Según lo dispuesto por torchdatasetsel Conjunto de datos de mascotas de Oxford viene con tres variantes de datos objetivo para elegir: la clase common (gato o perro), la raza particular person (hay treinta y siete) y una segmentación a nivel de píxel con tres categorías: primer plano, límite y fondo. Este último es el predeterminado; y es exactamente el tipo de objetivo que necesitamos.

una llamada a oxford_pet_dataset(root = dir) activará la descarga inicial:

# want torch > 0.6.1
# could need to run remotes::install_github("mlverse/torch", ref = remotes::github_pull("713")) relying on if you learn this
library(torch) 
library(torchvision)
library(torchdatasets)
library(luz)

dir <- "~/.torch-datasets/oxford_pet_dataset"

ds <- oxford_pet_dataset(root = dir)

Las imágenes (y las máscaras correspondientes) vienen en diferentes tamaños. Sin embargo, para el entrenamiento, necesitaremos que todos sean del mismo tamaño. Esto se puede lograr pasando remodel = y target_transform = argumentos Pero, ¿qué pasa con el aumento de datos (básicamente, siempre es una medida útil a tomar)? Imagina que hacemos uso del volteo aleatorio. Una imagen de entrada se volteará, o no, según alguna probabilidad. Pero si la imagen está volteada, ¡más vale que la máscara también lo esté! En este caso, las transformaciones de entrada y de destino no son independientes.

Una solución es crear un envoltorio alrededor oxford_pet_dataset() que nos permite “engancharnos” al .getitem() método, así:

pet_dataset <- torch::dataset(
  
  inherit = oxford_pet_dataset,
  
  initialize = operate(..., measurement, normalize = TRUE, augmentation = NULL) {
    
    self$augmentation <- augmentation
    
    input_transform <- operate(x) {
      x <- x %>%
        transform_to_tensor() %>%
        transform_resize(measurement) 
      # we'll make use of pre-trained MobileNet v2 as a characteristic extractor
      # => normalize as a way to match the distribution of photos it was educated with
      if (isTRUE(normalize)) x <- x %>%
        transform_normalize(imply = c(0.485, 0.456, 0.406),
                            std = c(0.229, 0.224, 0.225))
      x
    }
    
    target_transform <- operate(x) {
      x <- torch_tensor(x, dtype = torch_long())
      x <- x(newaxis,..)
      # interpolation = 0 makes certain we nonetheless find yourself with integer lessons
      x <- transform_resize(x, measurement, interpolation = 0)
    }
    
    tremendous$initialize(
      ...,
      remodel = input_transform,
      target_transform = target_transform
    )
    
  },
  .getitem = operate(i) {
    
    merchandise <- tremendous$.getitem(i)
    if (!is.null(self$augmentation)) 
      self$augmentation(merchandise)
    else
      listing(x = merchandise$x, y = merchandise$y(1,..))
  }
)

Todo lo que tenemos que hacer ahora es crear una función personalizada que nos permita decidir qué aumento aplicar a cada par de entrada-objetivo y luego llamar manualmente a las respectivas funciones de transformación.

Aquí, volteamos, en promedio, cada segunda imagen y, si lo hacemos, también volteamos la máscara. La segunda transformación, orquestar cambios aleatorios en el brillo, la saturación y el contraste, se aplica solo a la imagen de entrada.

augmentation <- operate(merchandise) {
  
  vflip <- runif(1) > 0.5
  
  x <- merchandise$x
  y <- merchandise$y
  
  if (isTRUE(vflip)) {
    x <- transform_vflip(x)
    y <- transform_vflip(y)
  }
  
  x <- transform_color_jitter(x, brightness = 0.5, saturation = 0.3, distinction = 0.3)
  
  listing(x = x, y = y(1,..))
  
}

Ahora hacemos uso del envoltorio, pet_dataset()para instanciar los conjuntos de entrenamiento y validación, y crear los respectivos cargadores de datos.

train_ds <- pet_dataset(root = dir,
                        cut up = "practice",
                        measurement = c(224, 224),
                        augmentation = augmentation)
valid_ds <- pet_dataset(root = dir,
                        cut up = "legitimate",
                        measurement = c(224, 224))

train_dl <- dataloader(train_ds, batch_size = 32, shuffle = TRUE)
valid_dl <- dataloader(valid_ds, batch_size = 32)

Definición del modelo

El modelo implementa una arquitectura U-Web clásica, con una etapa de codificación (el paso “hacia abajo”), una etapa de decodificación (el paso “arriba”) y, lo que es más importante, un “puente” que pasa las características conservadas desde la etapa de codificación a capas correspondientes en la etapa de decodificación.

codificador

Primero, tenemos el codificador. Utiliza un modelo previamente entrenado (MobileNet v2) como su extractor de características.

El codificador divide los bloques de extracción de características de MobileNet v2 en varias etapas y aplica una etapa tras otra. Los resultados respectivos se guardan en una lista.

encoder <- nn_module(
  
  initialize = operate() {
    mannequin <- model_mobilenet_v2(pretrained = TRUE)
    self$levels <- nn_module_list(listing(
      nn_identity(),
      mannequin$options(1:2),
      mannequin$options(3:4),
      mannequin$options(5:7),
      mannequin$options(8:14),
      mannequin$options(15:18)
    ))

    for (par in self$parameters) {
      par$requires_grad_(FALSE)
    }

  },
  ahead = operate(x) {
    options <- listing()
    for (i in 1:size(self$levels)) {
      x <- self$levels((i))(x)
      options((size(options) + 1)) <- x
    }
    options
  }
)

Descifrador

El decodificador se compone de bloques configurables. Un bloque recibe dos tensores de entrada: uno que es el resultado de aplicar el bloque decodificador anterior y otro que contiene el mapa de características producido en la etapa del codificador coincidente. En el pase hacia adelante, primero se aumenta la muestra del primero y se pasa a través de una no linealidad. Luego, el resultado intermedio se antepone al segundo argumento, el mapa de características canalizado. Sobre el tensor resultante, se aplica una convolución, seguida de otra no linealidad.

decoder_block <- nn_module(
  
  initialize = operate(in_channels, skip_channels, out_channels) {
    self$upsample <- nn_conv_transpose2d(
      in_channels = in_channels,
      out_channels = out_channels,
      kernel_size = 2,
      stride = 2
    )
    self$activation <- nn_relu()
    self$conv <- nn_conv2d(
      in_channels = out_channels + skip_channels,
      out_channels = out_channels,
      kernel_size = 3,
      padding = "similar"
    )
  },
  ahead = operate(x, skip) {
    x <- x %>%
      self$upsample() %>%
      self$activation()

    enter <- torch_cat(listing(x, skip), dim = 2)

    enter %>%
      self$conv() %>%
      self$activation()
  }
)

El decodificador en sí “simplemente” instancia y ejecuta los bloques:

decoder <- nn_module(
  
  initialize = operate(
    decoder_channels = c(256, 128, 64, 32, 16),
    encoder_channels = c(16, 24, 32, 96, 320)
  ) {

    encoder_channels <- rev(encoder_channels)
    skip_channels <- c(encoder_channels(-1), 3)
    in_channels <- c(encoder_channels(1), decoder_channels)

    depth <- size(encoder_channels)

    self$blocks <- nn_module_list()
    for (i in seq_len(depth)) {
      self$blocks$append(decoder_block(
        in_channels = in_channels(i),
        skip_channels = skip_channels(i),
        out_channels = decoder_channels(i)
      ))
    }

  },
  ahead = operate(options) {
    options <- rev(options)
    x <- options((1))
    for (i in seq_along(self$blocks)) {
      x <- self$blocks((i))(x, options((i+1)))
    }
    x
  }
)

Módulo de nivel superior

Finalmente, el módulo de nivel superior genera la puntuación de la clase. En nuestra tarea, hay tres clases de píxeles. El submódulo que produce la partitura puede ser simplemente una convolución ultimate, produciendo tres canales:

mannequin <- nn_module(
  
  initialize = operate() {
    self$encoder <- encoder()
    self$decoder <- decoder()
    self$output <- nn_sequential(
      nn_conv2d(in_channels = 16,
                out_channels = 3,
                kernel_size = 3,
                padding = "similar")
    )
  },
  ahead = operate(x) {
    x %>%
      self$encoder() %>%
      self$decoder() %>%
      self$output()
  }
)

Entrenamiento de modelos y evaluación (visible)

Con luzla formación de modelos es cuestión de dos verbos, setup() y match(). La tasa de aprendizaje se ha determinado, para este caso concreto, utilizando luz::lr_finder(); probablemente tendrá que cambiarlo cuando experimente con diferentes formas de aumento de datos (y diferentes conjuntos de datos).

mannequin <- mannequin %>%
  setup(optimizer = optim_adam, loss = nn_cross_entropy_loss())

fitted <- mannequin %>%
  set_opt_hparams(lr = 1e-3) %>%
  match(train_dl, epochs = 10, valid_data = valid_dl)

Aquí hay un extracto de cómo se desarrolló el rendimiento del entrenamiento en mi caso:

# Epoch 1/10
# Prepare metrics: Loss: 0.504                                                           
# Legitimate metrics: Loss: 0.3154

# Epoch 2/10
# Prepare metrics: Loss: 0.2845                                                           
# Legitimate metrics: Loss: 0.2549

...
...

# Epoch 9/10
# Prepare metrics: Loss: 0.1368                                                           
# Legitimate metrics: Loss: 0.2332

# Epoch 10/10
# Prepare metrics: Loss: 0.1299                                                           
# Legitimate metrics: Loss: 0.2511

Los números son solo números: ¿qué tan bueno es realmente el modelo entrenado para segmentar imágenes de mascotas? Para averiguarlo, generamos máscaras de segmentación para las primeras ocho observaciones en el conjunto de validación y las trazamos superpuestas en las imágenes. Una forma conveniente de trazar una imagen y superponer una máscara es proporcionada por el raster paquete.

Las intensidades de los píxeles deben estar entre cero y uno, por lo que en el envoltorio del conjunto de datos, lo hemos hecho para que la normalización se pueda desactivar. Para trazar las imágenes reales, simplemente instanciamos un clon de valid_ds eso deja los valores de píxel sin cambios. (Las predicciones, por otro lado, aún deberán obtenerse del conjunto de validación unique).

valid_ds_4plot <- pet_dataset(
  root = dir,
  cut up = "legitimate",
  measurement = c(224, 224),
  normalize = FALSE
)

Finalmente, las predicciones se generan en un bucle y se superponen sobre las imágenes una por una:

indices <- 1:8

preds <- predict(fitted, dataloader(dataset_subset(valid_ds, indices)))

png("pet_segmentation.png", width = 1200, top = 600, bg = "black")

par(mfcol = c(2, 4), mar = rep(2, 4))

for (i in indices) {
  
  masks <- as.array(torch_argmax(preds(i,..), 1)$to(gadget = "cpu"))
  masks <- raster::ratify(raster::raster(masks))
  
  img <- as.array(valid_ds_4plot(i)((1))$permute(c(2,3,1)))
  cond <- img > 0.99999
  img(cond) <- 0.99999
  img <- raster::brick(img)
  
  # plot picture
  raster::plotRGB(img, scale = 1, asp = 1, margins = TRUE)
  # overlay masks
  plot(masks, alpha = 0.4, legend = FALSE, axes = FALSE, add = TRUE)
  
}
Máscaras de segmentación aprendidas, superpuestas en imágenes del conjunto de validación.

Ahora a ejecutar este modelo “en la naturaleza” (bueno, más o menos).

JIT-traza y ejecuta en Android

El seguimiento del modelo entrenado lo convertirá en un formulario que se puede cargar en entornos sin R, por ejemplo, desde Python, C++ o Java.

Accedemos a la torch modelo subyacente al ajustado luz objeto y rastrearlo, donde rastrear significa llamarlo una vez, en una observación de muestra:

m <- fitted$mannequin
x <- coro::accumulate(train_dl, 1)

traced <- jit_trace(m, x((1))$x)

El modelo trazado ahora se puede guardar para usarlo con Python o C++, así:

traced %>% jit_save("traced_model.pt")

Sin embargo, dado que ya sabemos que nos gustaría implementarlo en Android, en su lugar hacemos uso de la función especializada jit_save_for_mobile() que, además, genera bytecode:

# want torch > 0.6.1
jit_save_for_mobile(traced_model, "model_bytecode.pt")

¡Y eso es todo por el lado R!

Para ejecutar en Android, hice un uso intensivo de Android de PyTorch Cellular aplicaciones de ejemploespecialmente el Segmentación de imagen uno.

El código de prueba de concepto actual para esta publicación (que se usó para generar la imagen a continuación) se puede encontrar aquí: https://github.com/skeydan/ImageSegmentation. (Sin embargo, tenga cuidado: ¡es mi primera aplicación de Android!).

Por supuesto, todavía tenemos que intentar encontrar al gato. Aquí está el modelo, ejecutado en un emulador de dispositivo en Android Studio, en tres imágenes (del conjunto de datos de mascotas de Oxford) seleccionadas, en primer lugar, por una amplia gama de dificultad y, en segundo lugar, bueno… por su ternura:

¿Dónde está mi gato?

¡Gracias por leer!

Parkhi, Omkar M., Andrea Vedaldi, Andrew Zisserman y CV Jawahar. 2012. “Gatos y perros.” En Conferencia IEEE sobre visión synthetic y reconocimiento de patrones.

Ronneberger, Olaf, Philipp Fischer y Thomas Brox. 2015. “U-Web: redes convolucionales para la segmentación de imágenes biomédicas”. CoRR abs/1505.04597. http://arxiv.org/abs/1505.04597.

Related Articles

¿Cómo elegir el programa de estudios en el extranjero adecuado?

Cuando un estudiante planea estudiar en el extranjero, hay muchos...

Comments

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Same Category

spot_img

Stay in touch!

Follow our Instagram