Posit AI Weblog: Treine em R, execute em Android: segmentação de imagens com tocha


Em certo sentido, a segmentação de imagens não é muito diferente da classificação de imagens. Acontece que em vez de categorizar uma imagem como um todo, a segmentação resulta em um rótulo para cada pixel. E como na classificação de imagens, as categorias de interesse dependem da tarefa: primeiro plano versus fundo, digamos; diferentes tipos de tecidos; diferentes tipos de vegetação; and many others.

O presente publish não é o primeiro deste weblog a tratar desse assunto; e como todos os anteriores, utiliza uma arquitetura U-Internet para atingir seu objetivo. As características centrais (desta postagem, não da U-Internet) são:

  1. Ele demonstra como realizar o aumento de dados para uma tarefa de segmentação de imagem.

  2. Ele usa luz, torchinterface de alto nível, para treinar o modelo.

  3. Isto Rastreamentos JIT o modelo treinado e o salva para implantação em dispositivos móveis. (JIT sendo o acrônimo comumente usado para o torch compilador just-in-time.)

  4. Inclui código de prova de conceito (embora não seja uma discussão) do modelo salvo sendo executado no Android.

E se você acha que isso por si só não é emocionante o suficiente – nossa tarefa aqui é encontrar cães e gatos. O que poderia ser mais útil do que um aplicativo móvel para garantir que você consiga distinguir seu gato do sofá fofo em que ele está descansando?

Posit AI Weblog: Treine em R, execute em Android: segmentação de imagens com tocha

Treinar em R

Começamos preparando os dados.

Pré-processamento e aumento de dados

Conforme fornecido por torchdatasetso Conjunto de dados de animais de estimação Oxford vem com três variantes de dados de destino para escolher: a classe geral (gato ou cachorro), a raça particular person (há trinta e sete delas) e uma segmentação em nível de pixel com três categorias: primeiro plano, limite e plano de fundo. Este último é o padrão; e é exatamente o tipo de alvo que precisamos.

Uma chamada para oxford_pet_dataset(root = dir) irá acionar o obtain inicial:

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

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

ds <- oxford_pet_dataset(root = dir)

As imagens (e máscaras correspondentes) vêm em tamanhos diferentes. Para o treinamento, entretanto, precisaremos que todos eles tenham o mesmo tamanho. Isso pode ser feito passando rework = e target_transform = argumentos. Mas e quanto ao aumento de dados (basicamente sempre uma medida útil a ser tomada)? Think about que fazemos uso de inversão aleatória. Uma imagem de entrada será invertida – ou não – de acordo com alguma probabilidade. Mas se a imagem for invertida, é melhor que a máscara também esteja! As transformações de entrada e destino não são independentes, neste caso.

Uma solução é criar um wrapper em torno oxford_pet_dataset() que nos permite “enganchar” o .getitem() método, assim:

pet_dataset <- torch::dataset(
  
  inherit = oxford_pet_dataset,
  
  initialize = operate(..., dimension, normalize = TRUE, augmentation = NULL) {
    
    self$augmentation <- augmentation
    
    input_transform <- operate(x) {
      x <- x %>%
        transform_to_tensor() %>%
        transform_resize(dimension) 
      # we'll make use of pre-trained MobileNet v2 as a characteristic extractor
      # => normalize as a way to match the distribution of photographs it was skilled 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 courses
      x <- transform_resize(x, dimension, interpolation = 0)
    }
    
    tremendous$initialize(
      ...,
      rework = input_transform,
      target_transform = target_transform
    )
    
  },
  .getitem = operate(i) {
    
    merchandise <- tremendous$.getitem(i)
    if (!is.null(self$augmentation)) 
      self$augmentation(merchandise)
    else
      checklist(x = merchandise$x, y = merchandise$y(1,..))
  }
)

Tudo o que precisamos fazer agora é criar uma função personalizada que nos permita decidir qual aumento aplicar a cada par entrada-alvo e, em seguida, chamar manualmente as respectivas funções de transformação.

Aqui, viramos, em média, a cada segunda imagem e, se o fizermos, viramos a máscara também. A segunda transformação – orquestrando mudanças aleatórias em brilho, saturação e contraste – é aplicada apenas à imagem 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)
  
  checklist(x = x, y = y(1,..))
  
}

Agora fazemos uso do invólucro, pet_dataset()para instanciar os conjuntos de treinamento e validação e criar os respectivos carregadores de dados.

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

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

Definição de modelo

O modelo implementa uma arquitetura U-Internet clássica, com um estágio de codificação (a passagem “para baixo”), um estágio de decodificação (a passagem “para cima”) e, mais importante, uma “ponte” que passa recursos preservados do estágio de codificação para camadas correspondentes no estágio de decodificação.

Codificador

Primeiro, temos o codificador. Ele usa um modelo pré-treinado (MobileNet v2) como extrator de recursos.

O codificador divide os blocos de extração de recursos do MobileNet v2 em vários estágios e aplica um estágio após o outro. Os respectivos resultados são salvos em uma lista.

encoder <- nn_module(
  
  initialize = operate() {
    mannequin <- model_mobilenet_v2(pretrained = TRUE)
    self$phases <- nn_module_list(checklist(
      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 <- checklist()
    for (i in 1:size(self$phases)) {
      x <- self$phases((i))(x)
      options((size(options) + 1)) <- x
    }
    options
  }
)

Decodificador

O decodificador é composto por blocos configuráveis. Um bloco recebe dois tensores de entrada: um que é o resultado da aplicação do bloco decodificador anterior e outro que contém o mapa de recursos produzido no estágio do codificador correspondente. Na passagem direta, primeiro o primeiro é ampliado e passado por uma não linearidade. O resultado intermediário é então anexado ao segundo argumento, o mapa de recursos canalizado. No tensor resultante, uma convolução é aplicada, seguida por outra não linearidade.

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(checklist(x, skip), dim = 2)

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

O próprio decodificador “apenas” instancia e percorre os blocos:

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 nível superior

Finalmente, o módulo de nível superior gera a pontuação da turma. Em nossa tarefa, existem três courses de pixels. O submódulo de produção de partituras pode então ser apenas uma convolução last, produzindo três canais:

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()
  }
)

Treinamento de modelo e avaliação (visible)

Com luzo treinamento do modelo é uma questão de dois verbos, setup() e match(). A taxa de aprendizagem foi determinada, para este caso específico, usando luz::lr_finder(); você provavelmente terá que alterá-lo ao experimentar diferentes formas de aumento de dados (e diferentes conjuntos de dados).

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)

Aqui está um trecho de como o desempenho do treinamento se desenvolveu no meu 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

Números são apenas números – quão bom é o modelo treinado na segmentação de imagens de animais de estimação? Para descobrir, geramos máscaras de segmentação para as primeiras oito observações no conjunto de validação e as plotamos sobrepostas nas imagens. Uma maneira conveniente de plotar uma imagem e sobrepor uma máscara é fornecida pelo raster pacote.

As intensidades de pixel devem estar entre zero e um, e é por isso que, no wrapper do conjunto de dados, fizemos com que a normalização pudesse ser desativada. Para plotar as imagens reais, apenas instanciamos um clone de valid_ds isso deixa os valores dos pixels inalterados. (As previsões, por outro lado, ainda terão que ser obtidas do conjunto de validação authentic.)

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

Finalmente, as previsões são geradas em um loop e sobrepostas nas imagens uma por uma:

indices <- 1:8

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

png("pet_segmentation.png", width = 1200, peak = 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(machine = "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 segmentação aprendidas, sobrepostas em imagens do conjunto de validação.

Agora vamos executar este modelo “na natureza” (bem, mais ou menos).

Rastreamento JIT e execução no Android

Rastrear o modelo treinado irá convertê-lo em um formato que pode ser carregado em ambientes sem R – por exemplo, de Python, C++ ou Java.

Acessamos o torch modelo subjacente ao ajustado luz objeto e rastreá-lo – onde rastrear significa chamá-lo uma vez, em uma observação de amostra:

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

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

O modelo rastreado agora pode ser salvo para uso com Python ou C++, assim:

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

No entanto, como já sabemos que gostaríamos de implantá-lo no Android, utilizamos a função especializada jit_save_for_mobile() que, adicionalmente, gera bytecode:

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

E é isso para o lado R!

Para rodar em Android, fiz uso intenso do Android do PyTorch Cell aplicativos de exemploespecialmente o segmentação de imagem um.

O código de prova de conceito actual para esta postagem (que foi usado para gerar a imagem abaixo) pode ser encontrado aqui: https://github.com/skeydan/ImageSegmentation. (Mas esteja avisado – é meu primeiro aplicativo Android!).

Claro, ainda temos que tentar encontrar o gato. Aqui está o modelo, executado em um emulador de dispositivo no Android Studio, em três imagens (do Oxford Pet Dataset) selecionadas, em primeiro lugar, por uma ampla variedade de dificuldade e, em segundo lugar, bem… pela fofura:

Onde está meu gato?

Obrigado por ler!

Parkhi, Omkar M., Andrea Vedaldi, Andrew Zisserman e CV Jawahar. 2012. “Gatos e Cães.” Em Conferência IEEE sobre Visão Computacional e Reconhecimento de Padrões.

Ronneberger, Olaf, Philipp Fischer e Thomas Brox. 2015. “U-Internet: Redes Convolucionais para Segmentação de Imagens Biomédicas.” CoRR abs/1505.04597. http://arxiv.org/abs/1505.04597.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *