Cuando qué no es suficiente
Es cierto que a veces es very important distinguir entre diferentes tipos de objetos. ¿Es un coche que se acerca a toda velocidad hacia mí, en cuyo caso será mejor que me aparte del camino? ¿O es un Doberman enorme (en cuyo caso yo probablemente haría lo mismo)? Sin embargo, a menudo en la vida actual, en lugar de clasificaciónlo que se necesita es de grano fino segmentación.
Al acercarnos a las imágenes, no buscamos una sola etiqueta; en cambio, queremos clasificar cada píxel según algún criterio:
-
En medicina, es posible que queramos distinguir entre diferentes tipos de células o identificar tumores.
-
En diversas ciencias de la tierra, los datos satelitales se utilizan para segmentar superficies terrestres.
-
Para permitir el uso de fondos personalizados, el software program de videoconferencia debe poder distinguir el primer plano del fondo.
La segmentación de imágenes es una forma de aprendizaje supervisado: se necesita algún tipo de verdad elementary. Aquí viene en forma de mascarilla – una imagen, de resolución espacial idéntica a la de los datos de entrada, que designa la verdadera clase para cada píxel. En consecuencia, la pérdida de clasificación se calcula por píxeles; Luego, las pérdidas se suman para producir un agregado que se utilizará en la optimización.
La arquitectura “canónica” para la segmentación de imágenes es U-Web (alrededor desde 2015).
U-Web
Aquí está el prototipo de U-Web, como se muestra en el artículo authentic de Rönneberger et al. papel (Ronneberger, Fischer y Brox 2015).
De esta arquitectura existen numerosas variantes. Podría utilizar diferentes tamaños de capa, activaciones, formas de lograr reducir y aumentar el tamaño, y más. Sin embargo, hay una característica que lo outline: la forma de U, estabilizada por los “puentes” que se cruzan horizontalmente en todos los niveles.
En pocas palabras, el lado izquierdo de la U se parece a las arquitecturas convolucionales utilizadas en la clasificación de imágenes. Scale back sucesivamente la resolución espacial. Al mismo tiempo, otra dimensión –la canales dimensión: se utiliza para construir una jerarquía de características, que van desde muy básicas hasta muy especializadas.
Sin embargo, a diferencia de la clasificación, la salida debe tener la misma resolución espacial que la entrada. Por lo tanto, necesitamos volver a aumentar el tamaño; de esto se encarga el lado derecho de la U. Pero, ¿cómo vamos a llegar a una buena situación? por píxel clasificación, ahora que se ha perdido tanta información espacial?
Para eso están los “puentes”: en cada nivel, la entrada a una capa de muestreo superior es una concatenación de la salida de la capa anterior, que pasó por toda la rutina de compresión/descompresión, y alguna representación intermedia preservada de la fase de reducción. De esta manera, una arquitectura U-Web combina la atención al detalle con la extracción de características.
Segmentación de imágenes cerebrales
Con U-Web, la aplicabilidad del dominio es tan amplia como versatile la arquitectura. Aquí queremos detectar anomalías en los escáneres cerebrales. El conjunto de datos, utilizado en Buda, Saha y Mazurowski (2019)contiene imágenes de resonancia magnética junto con imágenes creadas manualmente INSTINTO Máscaras de segmentación de anomalías. Está disponible en Kaggle.
Bueno, el documento va acompañado de un repositorio de GitHub. A continuación, seguimos de cerca (aunque no replicamos exactamente) el código de aumento de datos y preprocesamiento de los autores.
Como suele ocurrir en el caso de las imágenes médicas, existe un notable desequilibrio de clases en los datos. Para cada paciente, se han tomado secciones en múltiples posiciones. (El número de secciones por paciente varía). La mayoría de las secciones no presentan ninguna lesión; las máscaras correspondientes están coloreadas de negro en todas partes.
Aquí hay tres ejemplos donde las máscaras hacer indicar anomalías:
Veamos si podemos construir una U-Web que genere esas máscaras para nosotros.
Datos
Antes de empezar a escribir, aquí tienes una Cuaderno colaborativo para seguirlo cómodamente.
Usamos pins
para obtener los datos. por favor vea esta introducción si no has usado ese paquete antes.
El conjunto de datos no es tan grande (incluye exploraciones de 110 pacientes diferentes), por lo que tendremos que conformarnos solo con un conjunto de entrenamiento y validación. (No hagas esto en la vida actual, ya que inevitablemente terminarás ajustando esto último).
train_dir <- "knowledge/mri_train"
valid_dir <- "knowledge/mri_valid"
if(dir.exists(train_dir)) unlink(train_dir, recursive = TRUE, drive = TRUE)
if(dir.exists(valid_dir)) unlink(valid_dir, recursive = TRUE, drive = TRUE)
zip::unzip(recordsdata, exdir = "knowledge")
file.rename("knowledge/kaggle_3m", train_dir)
# this can be a duplicate, once more containing kaggle_3m (evidently a packaging error on Kaggle)
# we simply take away it
unlink("knowledge/lgg-mri-segmentation", recursive = TRUE)
dir.create(valid_dir)
De esos 110 pacientes, conservamos 30 para validación. Algunas manipulaciones más de archivos y tendremos una bonita estructura jerárquica, con train_dir
y valid_dir
manteniendo sus subdirectorios por paciente, respectivamente.
valid_indices <- pattern(1:size(sufferers), 30)
sufferers <- checklist.dirs(train_dir, recursive = FALSE)
for (i in valid_indices) {
dir.create(file.path(valid_dir, basename(sufferers[i])))
for (f in checklist.recordsdata(sufferers[i])) {
file.rename(file.path(train_dir, basename(sufferers[i]), f), file.path(valid_dir, basename(sufferers[i]), f))
}
unlink(file.path(train_dir, basename(sufferers[i])), recursive = TRUE)
}
Ahora necesitamos un dataset
que sabe qué hacer con estos archivos.
Conjunto de datos
Como todos torch
conjunto de datos, este tiene initialize()
y .getitem()
métodos. initialize()
crea un inventario de nombres de archivos de escaneo y máscara, para ser utilizados por .getitem()
cuando realmente lee esos archivos. Sin embargo, a diferencia de lo que hemos visto en publicaciones anteriores, .getitem()
no devuelve simplemente pares de entrada-destino en orden. En cambio, siempre que el parámetro random_sampling
Si es cierto, realizará un muestreo ponderado, prefiriendo los artículos con lesiones importantes. Esta opción se utilizará para el conjunto de entrenamiento, para contrarrestar el desequilibrio de clases mencionado anteriormente.
La otra diferencia entre los conjuntos de entrenamiento y validación es el uso del aumento de datos. Las imágenes/máscaras de entrenamiento se pueden voltear, cambiar de tamaño y rotar; Las probabilidades y las cantidades son configurables.
Una instancia de brainseg_dataset
encapsula toda esta funcionalidad:
brainseg_dataset <- dataset(
identify = "brainseg_dataset",
initialize = perform(img_dir,
augmentation_params = NULL,
random_sampling = FALSE) {
self$photos <- tibble(
img = grep(
checklist.recordsdata(
img_dir,
full.names = TRUE,
sample = "tif",
recursive = TRUE
),
sample = 'masks',
invert = TRUE,
worth = TRUE
),
masks = grep(
checklist.recordsdata(
img_dir,
full.names = TRUE,
sample = "tif",
recursive = TRUE
),
sample = 'masks',
worth = TRUE
)
)
self$slice_weights <- self$calc_slice_weights(self$photos$masks)
self$augmentation_params <- augmentation_params
self$random_sampling <- random_sampling
},
.getitem = perform(i) {
index <-
if (self$random_sampling == TRUE)
pattern(1:self$.size(), 1, prob = self$slice_weights)
else
i
img <- self$photos$img[index] %>%
image_read() %>%
transform_to_tensor()
masks <- self$photos$masks[index] %>%
image_read() %>%
transform_to_tensor() %>%
transform_rgb_to_grayscale() %>%
torch_unsqueeze(1)
img <- self$min_max_scale(img)
if (!is.null(self$augmentation_params)) {
scale_param <- self$augmentation_params[1]
c(img, masks) %<-% self$resize(img, masks, scale_param)
rot_param <- self$augmentation_params[2]
c(img, masks) %<-% self$rotate(img, masks, rot_param)
flip_param <- self$augmentation_params[3]
c(img, masks) %<-% self$flip(img, masks, flip_param)
}
checklist(img = img, masks = masks)
},
.size = perform() {
nrow(self$photos)
},
calc_slice_weights = perform(masks) {
weights <- map_dbl(masks, perform(m) {
img <-
as.integer(magick::image_data(image_read(m), channels = "grey"))
sum(img / 255)
})
sum_weights <- sum(weights)
num_weights <- size(weights)
weights <- weights %>% map_dbl(perform(w) {
w <- (w + sum_weights * 0.1 / num_weights) / (sum_weights * 1.1)
})
weights
},
min_max_scale = perform(x) {
min = x$min()$merchandise()
max = x$max()$merchandise()
x$clamp_(min = min, max = max)
x$add_(-min)$div_(max - min + 1e-5)
x
},
resize = perform(img, masks, scale_param) {
img_size <- dim(img)[2]
rnd_scale <- runif(1, 1 - scale_param, 1 + scale_param)
img <- transform_resize(img, dimension = rnd_scale * img_size)
masks <- transform_resize(masks, dimension = rnd_scale * img_size)
diff <- dim(img)[2] - img_size
if (diff > 0) {
high <- ceiling(diff / 2)
left <- ceiling(diff / 2)
img <- transform_crop(img, high, left, img_size, img_size)
masks <- transform_crop(masks, high, left, img_size, img_size)
} else {
img <- transform_pad(img,
padding = -c(
ceiling(diff / 2),
flooring(diff / 2),
ceiling(diff / 2),
flooring(diff / 2)
))
masks <- transform_pad(masks, padding = -c(
ceiling(diff / 2),
flooring(diff /
2),
ceiling(diff /
2),
flooring(diff /
2)
))
}
checklist(img, masks)
},
rotate = perform(img, masks, rot_param) {
rnd_rot <- runif(1, 1 - rot_param, 1 + rot_param)
img <- transform_rotate(img, angle = rnd_rot)
masks <- transform_rotate(masks, angle = rnd_rot)
checklist(img, masks)
},
flip = perform(img, masks, flip_param) {
rnd_flip <- runif(1)
if (rnd_flip > flip_param) {
img <- transform_hflip(img)
masks <- transform_hflip(masks)
}
checklist(img, masks)
}
)
Después de la creación de instancias, vemos que tenemos 2977 pares de entrenamiento y 952 pares de validación, respectivamente:
Como comprobación de corrección, tracemos una imagen y una máscara asociada:
Con torch
es sencillo inspeccionar qué sucede cuando cambia los parámetros relacionados con el aumento. Simplemente elegimos un par del conjunto de validación, al que aún no se le ha aplicado ningún aumento, y llamamos valid_ds$<augmentation_func()>
directamente. Sólo por diversión, usemos aquí parámetros más “extremos” que los que usamos en el entrenamiento actual. (La capacitación actual utiliza la configuración del repositorio GitHub de Mateusz, que asumimos que ha sido elegida cuidadosamente para un rendimiento óptimo).
img_and_mask <- valid_ds[77]
img <- img_and_mask[[1]]
masks <- img_and_mask[[2]]
imgs <- map (1:24, perform(i) {
# scale issue; train_ds actually makes use of 0.05
c(img, masks) %<-% valid_ds$resize(img, masks, 0.2)
c(img, masks) %<-% valid_ds$flip(img, masks, 0.5)
# rotation angle; train_ds actually makes use of 15
c(img, masks) %<-% valid_ds$rotate(img, masks, 90)
img %>%
transform_rgb_to_grayscale() %>%
as.array() %>%
as_tibble() %>%
rowid_to_column(var = "Y") %>%
collect(key = "X", worth = "worth", -Y) %>%
mutate(X = as.numeric(gsub("V", "", X))) %>%
ggplot(aes(X, Y, fill = worth)) +
geom_raster() +
theme_void() +
theme(legend.place = "none") +
theme(facet.ratio = 1)
})
plot_grid(plotlist = imgs, nrow = 4)
Ahora todavía necesitamos los cargadores de datos y nada nos impedirá pasar a la siguiente gran tarea: construir el modelo.
batch_size <- 4
train_dl <- dataloader(train_ds, batch_size)
valid_dl <- dataloader(valid_ds, batch_size)
Modelo
Nuestro modelo ilustra muy bien el tipo de código modular que viene “naturalmente” con torch
. Abordamos las cosas de arriba hacia abajo, comenzando con el propio contenedor U-Web.
unet
se encarga de la composición world: ¿hasta dónde “hacia abajo” vamos, reduciendo la imagen mientras incrementamos el número de filtros, y luego cómo “subimos” nuevamente?
Es importante destacar que también está en la memoria del sistema. En ahead()
realiza un seguimiento de las salidas de las capas que se ven “bajando” para volver a agregarse cuando “suben”.
unet <- nn_module(
"unet",
initialize = perform(channels_in = 3,
n_classes = 1,
depth = 5,
n_filters = 6) {
self$down_path <- nn_module_list()
prev_channels <- channels_in
for (i in 1:depth) {
self$down_path$append(down_block(prev_channels, 2 ^ (n_filters + i - 1)))
prev_channels <- 2 ^ (n_filters + i -1)
}
self$up_path <- nn_module_list()
for (i in ((depth - 1):1)) {
self$up_path$append(up_block(prev_channels, 2 ^ (n_filters + i - 1)))
prev_channels <- 2 ^ (n_filters + i - 1)
}
self$final = nn_conv2d(prev_channels, n_classes, kernel_size = 1)
},
ahead = perform(x) {
blocks <- checklist()
for (i in 1:size(self$down_path)) {
x <- self$down_path[[i]](x)
if (i != size(self$down_path)) {
blocks <- c(blocks, x)
x <- nnf_max_pool2d(x, 2)
}
}
for (i in 1:size(self$up_path)) {
x <- self$up_path[[i]](x, blocks[[length(blocks) - i + 1]]$to(gadget = gadget))
}
torch_sigmoid(self$final(x))
}
)
unet
delega a dos contenedores justo debajo de él en la jerarquía: down_block
y up_block
. Mientras down_block
está “sólo” ahí por razones estéticas (inmediatamente delega en su propio caballo de batalla, conv_block
), en up_block
Vemos los “puentes” de U-Web en acción.
down_block <- nn_module(
"down_block",
initialize = perform(in_size, out_size) {
self$conv_block <- conv_block(in_size, out_size)
},
ahead = perform(x) {
self$conv_block(x)
}
)
up_block <- nn_module(
"up_block",
initialize = perform(in_size, out_size) {
self$up = nn_conv_transpose2d(in_size,
out_size,
kernel_size = 2,
stride = 2)
self$conv_block = conv_block(in_size, out_size)
},
ahead = perform(x, bridge) {
up <- self$up(x)
torch_cat(checklist(up, bridge), 2) %>%
self$conv_block()
}
)
Finalmente, un conv_block
es una estructura secuencial que contiene capas convolucionales, ReLU y de abandono.
conv_block <- nn_module(
"conv_block",
initialize = perform(in_size, out_size) {
self$conv_block <- nn_sequential(
nn_conv2d(in_size, out_size, kernel_size = 3, padding = 1),
nn_relu(),
nn_dropout(0.6),
nn_conv2d(out_size, out_size, kernel_size = 3, padding = 1),
nn_relu()
)
},
ahead = perform(x){
self$conv_block(x)
}
)
Ahora crea una instancia del modelo y, posiblemente, muévelo a la GPU:
gadget <- torch_device(if(cuda_is_available()) "cuda" else "cpu")
mannequin <- unet(depth = 5)$to(gadget = gadget)
Mejoramiento
Entrenamos nuestro modelo con una combinación de entropía cruzada y pérdida de dados.
Este último, aunque no se envía con torch
se puede implementar manualmente:
calc_dice_loss <- perform(y_pred, y_true) {
easy <- 1
y_pred <- y_pred$view(-1)
y_true <- y_true$view(-1)
intersection <- (y_pred * y_true)$sum()
1 - ((2 * intersection + easy) / (y_pred$sum() + y_true$sum() + easy))
}
dice_weight <- 0.3
La optimización utiliza el descenso de gradiente estocástico (SGD), junto con el programador de tasa de aprendizaje de un ciclo introducido en el contexto de clasificación de imágenes con antorcha.
optimizer <- optim_sgd(mannequin$parameters, lr = 0.1, momentum = 0.9)
num_epochs <- 20
scheduler <- lr_one_cycle(
optimizer,
max_lr = 0.1,
steps_per_epoch = size(train_dl),
epochs = num_epochs
)
Capacitación
El ciclo de entrenamiento sigue entonces el esquema recurring. Una cosa a tener en cuenta: cada época, guardamos el modelo (usando torch_save()
), para que luego podamos elegir el mejor, en caso de que el rendimiento se haya degradado posteriormente.
train_batch <- perform(b) {
optimizer$zero_grad()
output <- mannequin(b[[1]]$to(gadget = gadget))
goal <- b[[2]]$to(gadget = gadget)
bce_loss <- nnf_binary_cross_entropy(output, goal)
dice_loss <- calc_dice_loss(output, goal)
loss <- dice_weight * dice_loss + (1 - dice_weight) * bce_loss
loss$backward()
optimizer$step()
scheduler$step()
checklist(bce_loss$merchandise(), dice_loss$merchandise(), loss$merchandise())
}
valid_batch <- perform(b) {
output <- mannequin(b[[1]]$to(gadget = gadget))
goal <- b[[2]]$to(gadget = gadget)
bce_loss <- nnf_binary_cross_entropy(output, goal)
dice_loss <- calc_dice_loss(output, goal)
loss <- dice_weight * dice_loss + (1 - dice_weight) * bce_loss
checklist(bce_loss$merchandise(), dice_loss$merchandise(), loss$merchandise())
}
for (epoch in 1:num_epochs) {
mannequin$practice()
train_bce <- c()
train_dice <- c()
train_loss <- c()
coro::loop(for (b in train_dl) {
c(bce_loss, dice_loss, loss) %<-% train_batch(b)
train_bce <- c(train_bce, bce_loss)
train_dice <- c(train_dice, dice_loss)
train_loss <- c(train_loss, loss)
})
torch_save(mannequin, paste0("model_", epoch, ".pt"))
cat(sprintf("nEpoch %d, coaching: loss:%3f, bce: %3f, cube: %3fn",
epoch, imply(train_loss), imply(train_bce), imply(train_dice)))
mannequin$eval()
valid_bce <- c()
valid_dice <- c()
valid_loss <- c()
i <- 0
coro::loop(for (b in tvalid_dl) {
i <<- i + 1
c(bce_loss, dice_loss, loss) %<-% valid_batch(b)
valid_bce <- c(valid_bce, bce_loss)
valid_dice <- c(valid_dice, dice_loss)
valid_loss <- c(valid_loss, loss)
})
cat(sprintf("nEpoch %d, validation: loss:%3f, bce: %3f, cube: %3fn",
epoch, imply(valid_loss), imply(valid_bce), imply(valid_dice)))
}
Epoch 1, coaching: loss:0.304232, bce: 0.148578, cube: 0.667423
Epoch 1, validation: loss:0.333961, bce: 0.127171, cube: 0.816471
Epoch 2, coaching: loss:0.194665, bce: 0.101973, cube: 0.410945
Epoch 2, validation: loss:0.341121, bce: 0.117465, cube: 0.862983
[...]
Epoch 19, coaching: loss:0.073863, bce: 0.038559, cube: 0.156236
Epoch 19, validation: loss:0.302878, bce: 0.109721, cube: 0.753577
Epoch 20, coaching: loss:0.070621, bce: 0.036578, cube: 0.150055
Epoch 20, validation: loss:0.295852, bce: 0.101750, cube: 0.748757
Evaluación
En esta ejecución, es el modelo remaining el que funciona mejor en el conjunto de validación. Aún así, nos gustaría mostrar cómo cargar un modelo guardado, usando torch_load()
.
Una vez cargado, coloque el modelo en eval
modo:
saved_model <- torch_load("model_20.pt")
mannequin <- saved_model
mannequin$eval()
Ahora, como no tenemos un conjunto de pruebas separado, ya conocemos las métricas promedio fuera de la muestra; pero al remaining lo que nos importa son las máscaras generadas. Veamos algunos, mostrando datos reales y exploraciones por resonancia magnética para comparar.
# with out random sampling, we would primarily see lesion-free patches
eval_ds <- brainseg_dataset(valid_dir, augmentation_params = NULL, random_sampling = TRUE)
eval_dl <- dataloader(eval_ds, batch_size = 8)
batch <- eval_dl %>% dataloader_make_iter() %>% dataloader_next()
par(mfcol = c(3, 8), mar = c(0, 1, 0, 1))
for (i in 1:8) {
img <- batch[[1]][i, .., drop = FALSE]
inferred_mask <- mannequin(img$to(gadget = gadget))
true_mask <- batch[[2]][i, .., drop = FALSE]$to(gadget = gadget)
bce <- nnf_binary_cross_entropy(inferred_mask, true_mask)$to(gadget = "cpu") %>%
as.numeric()
dc <- calc_dice_loss(inferred_mask, true_mask)$to(gadget = "cpu") %>% as.numeric()
cat(sprintf("nSample %d, bce: %3f, cube: %3fn", i, bce, dc))
inferred_mask <- inferred_mask$to(gadget = "cpu") %>% as.array() %>% .[1, 1, , ]
inferred_mask <- ifelse(inferred_mask > 0.5, 1, 0)
img[1, 1, ,] %>% as.array() %>% as.raster() %>% plot()
true_mask$to(gadget = "cpu")[1, 1, ,] %>% as.array() %>% as.raster() %>% plot()
inferred_mask %>% as.raster() %>% plot()
}
También imprimimos la entropía cruzada particular person y las pérdidas de dados; relacionarlos con las máscaras generadas puede generar información útil para el ajuste del modelo.
Pattern 1, bce: 0.088406, cube: 0.387786}
Pattern 2, bce: 0.026839, cube: 0.205724
Pattern 3, bce: 0.042575, cube: 0.187884
Pattern 4, bce: 0.094989, cube: 0.273895
Pattern 5, bce: 0.026839, cube: 0.205724
Pattern 6, bce: 0.020917, cube: 0.139484
Pattern 7, bce: 0.094989, cube: 0.273895
Pattern 8, bce: 2.310956, cube: 0.999824
Si bien están lejos de ser perfectas, la mayoría de estas máscaras no son tan malas: ¡un buen resultado dado el pequeño conjunto de datos!
Resumen
Este ha sido nuestro más complejo. torch
publicar hasta ahora; sin embargo, esperamos que haya aprovechado bien el tiempo. Por un lado, entre las aplicaciones del aprendizaje profundo, la segmentación de imágenes médicas destaca por su gran utilidad social. En segundo lugar, las arquitecturas tipo U-Web se emplean en muchas otras áreas. Y finalmente, una vez más vimos torch
La flexibilidad y el comportamiento intuitivo en acción.
¡Gracias por leer!
Buda, Mateusz, Ashirbani Saha y Maciej A. Mazurowski. 2019. “Asociación de subtipos genómicos de gliomas de grado inferior con características de forma extraídas automáticamente mediante un algoritmo de aprendizaje profundo”. Computadoras en biología y medicina. 109: 218–25. https://doi.org/https://doi.org/10.1016/j.compbiomed.2019.05.002.
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.