Las redes neuronales convolucionales (CNN) son excelentes: pueden detectar características en una imagen sin importar dónde. Bueno, no exactamente. No son indiferentes a cualquier tipo de movimiento. Cambiar hacia arriba o hacia abajo, hacia la izquierda o hacia la derecha, está bien; girar alrededor de un eje no lo es. Esto se debe a cómo funciona la convolución: recorrer por fila, luego recorrer por columna (o al revés). Si queremos “más” (por ejemplo, detección exitosa de un objeto al revés), necesitamos extender la convolución a una operación que sea equivariante de rotación. Una operación que es equivariante a algún tipo de acción no solo registrará la característica movida per se, sino que también realizará un seguimiento de qué acción concreta hizo que apareciera donde está.
Esta es la segunda publicación de una serie que presenta las CNN equivalentes de grupo (GCNN).. El primero Fue una introducción de alto nivel sobre por qué los querríamos y cómo funcionan. Allí presentamos al actor clave, el grupo de simetría, que especifica qué tipos de transformaciones deben tratarse de manera equivariante. Si no lo ha hecho, primero eche un vistazo a esa publicación, ya que aquí haré uso de la terminología y los conceptos que introdujo.
Hoy codificamos un GCNN easy desde cero. El código y la presentación siguen estrictamente un computadora portátil proporcionado como parte del programa 2022 de la Universidad de Amsterdam Curso de aprendizaje profundo. Nunca se les puede agradecer lo suficiente por poner a disposición materiales de aprendizaje tan excelentes.
A continuación, mi intención es explicar el pensamiento normal y cómo la arquitectura resultante se construye a partir de módulos más pequeños, a cada uno de los cuales se le asigna un propósito claro. Por esa razón, no reproduciré todo el código aquí; en su lugar, haré uso del paquete gcnn
. Sus métodos están muy comentados; así que para ver algunos detalles, no dudes en mirar el código.
A partir de hoy, gcnn
implementa un grupo de simetría: (C_4)el que sirve como ejemplo continuo a lo largo de la publicación uno. Sin embargo, es directamente extensible y utiliza jerarquías de clases en todas partes.
Paso 1: el grupo de simetría (C_4)
Al codificar una GCNN, lo primero que debemos proporcionar es una implementación del grupo de simetría que nos gustaría usar. Aquí lo tienes (C_4)el grupo de cuatro elementos que gira 90 grados.
podemos preguntar gcnn
crear uno para nosotros e inspeccionar sus elementos.
torch_tensor
0.0000
1.5708
3.1416
4.7124
[ CPUFloatType{4} ]
Los elementos están representados por sus respectivos ángulos de rotación: (0), (frac{pi}{2}), (pi)y (frac{3 pi}{2}).
Los grupos son conscientes de la identidad y saben construir la inversa de un elemento:
C_4$id
g1 <- elems[2]
C_4$inverse(g1)
torch_tensor
0
[ CPUFloatType{1} ]
torch_tensor
4.71239
[ CPUFloatType{} ]
Aquí lo que más nos importa son los elementos del grupo. acción. En cuanto a la implementación, debemos distinguir entre sus acciones entre sí y su acción en el espacio vectorial. (mathbb{R}^2)donde viven nuestras imágenes de entrada. La primera parte es fácil: puede implementarse simplemente añadiendo ángulos. De hecho, esto es lo que gcnn
hace cuando le pedimos que nos deje g1
guiarse por g2
:
g2 <- elems[3]
# in C_4$left_action_on_H(), H stands for the symmetry group
C_4$left_action_on_H(torch_tensor(g1)$unsqueeze(1), torch_tensor(g2)$unsqueeze(1))
torch_tensor
4.7124
[ CPUFloatType{1,1} ]
¿Qué pasa con el unsqueeze()
¿s? Desde (C_4)es lo último razón de ser es ser parte de una crimson neuronal, left_action_on_H()
funciona con lotes de elementos, no con tensores escalares.
Las cosas son un poco menos sencillas cuando la acción del grupo en (mathbb{R}^2) está preocupado. Aquí necesitamos el concepto de representación del grupo. Este es un tema complicado, en el que no entraremos aquí. En nuestro contexto precise, funciona así: tenemos una señal de entrada, un tensor con el que nos gustaría operar de alguna manera. (Ese “de algún modo” será convolución, como veremos pronto). Para hacer que esa operación sea equivariante en grupo, primero hacemos que la representación aplique el inverso acción grupal a la entrada. Hecho esto, continuamos con la operación como si nada hubiera pasado.
Para dar un ejemplo concreto, digamos que la operación es una medida. Imagínese a un corredor, parado al pie de un sendero de montaña, listo para correr cuesta arriba. Nos gustaría registrar su altura. Una opción que tenemos es tomar la medida y luego dejar que suban. Nuestra medición será tan válida arriba de la montaña como lo fue aquí abajo. Alternativamente, podríamos ser educados y no hacerlos esperar. Una vez que están arriba les pedimos que bajen y cuando regresan medimos su altura. El resultado es el mismo: la altura del cuerpo es equivariante (más que eso: invariante, par) a la acción de correr hacia arriba o hacia abajo. (Por supuesto, la altura es una medida bastante aburrida. Pero algo más interesante, como la frecuencia cardíaca, no habría funcionado tan bien en este ejemplo).
Volviendo a la implementación, resulta que las acciones grupales están codificadas como matrices. Hay una matriz para cada elemento del grupo. Para (C_4)el llamado estándar La representación es una matriz de rotación:
[
begin{bmatrix} cos(theta) & -sin(theta) sin(theta) & cos(theta) end{bmatrix}
]
En gcnn
la función que aplica esa matriz es left_action_on_R2()
. Al igual que su hermano, está diseñado para trabajar con lotes (tanto de elementos de grupo como de (mathbb{R}^2) vectores). Técnicamente, lo que hace es rotar la cuadrícula en la que está definida la imagen y luego volver a muestrear la imagen. Para hacer esto más concreto, el código de ese método tiene el siguiente aspecto.
Aquí hay una cabra.
img_path <- system.file("imgs", "z.jpg", package deal = "gcnn")
img <- torchvision::base_loader(img_path) |> torchvision::transform_to_tensor()
img$permute(c(2, 3, 1)) |> as.array() |> as.raster() |> plot()
Primero, llamamos C_4$left_action_on_R2()
para rotar la cuadrícula.
# Grid form is [2, 1024, 1024], for a 2nd, 1024 x 1024 picture.
img_grid_R2 <- torch::torch_stack(torch::torch_meshgrid(
checklist(
torch::torch_linspace(-1, 1, dim(img)[2]),
torch::torch_linspace(-1, 1, dim(img)[3])
)
))
# Remodel the picture grid with the matrix illustration of some group factor.
transformed_grid <- C_4$left_action_on_R2(C_4$inverse(g1)$unsqueeze(1), img_grid_R2)
En segundo lugar, volvemos a muestrear la imagen en la cuadrícula transformada. La cabra ahora mira hacia el cielo.
Paso 2: la convolución de elevación
Queremos hacer uso de las tecnologías existentes y eficientes. torch
funcionalidad tanto como sea posible. Concretamente queremos utilizar nn_conv2d()
. Sin embargo, lo que necesitamos es un núcleo de convolución que sea equivalente no sólo a la traducción, sino también a la acción de (C_4). Esto se puede lograr teniendo un núcleo para cada rotación posible.
Implementar esa thought es exactamente lo que LiftingConvolution
hace. El principio es el mismo que antes: primero, se gira la cuadrícula y luego, el núcleo (matriz de peso) se vuelve a muestrear en la cuadrícula transformada.
¿Por qué, sin embargo, llamar a esto un levantando convolución? El núcleo de convolución routine opera en (mathbb{R}^2); mientras que nuestra versión extendida opera en combinaciones de (mathbb{R}^2) y (C_4). En lenguaje matemático, ha sido levantado hacia producto semidirecto (mathbb{R}^2rveces C_4).
lifting_conv <- LiftingConvolution(
group = CyclicGroup(order = 4),
kernel_size = 5,
in_channels = 3,
out_channels = 8
)
x <- torch::torch_randn(c(2, 3, 32, 32))
y <- lifting_conv(x)
y$form
[1] 2 8 4 28 28
Dado que internamente LiftingConvolution
utiliza una dimensión adicional para realizar el producto de traslaciones y rotaciones, el resultado no es de cuatro, sino de cinco dimensiones.
Paso 3: convoluciones grupales
Ahora que estamos en el “espacio extendido por grupos”, podemos encadenar varias capas donde tanto la entrada como la salida están convolución de grupo capas. Por ejemplo:
group_conv <- GroupConvolution(
group = CyclicGroup(order = 4),
kernel_size = 5,
in_channels = 8,
out_channels = 16
)
z <- group_conv(y)
z$form
[1] 2 16 4 24 24
Todo lo que queda por hacer es empaquetar esto. eso es lo que gcnn::GroupEquivariantCNN()
hace.
Paso 4: CNN equivalente al grupo
podemos llamar GroupEquivariantCNN()
así.
cnn <- GroupEquivariantCNN(
group = CyclicGroup(order = 4),
kernel_size = 5,
in_channels = 1,
out_channels = 1,
num_hidden = 2, # variety of group convolutions
hidden_channels = 16 # variety of channels per group conv layer
)
img <- torch::torch_randn(c(4, 1, 32, 32))
cnn(img)$form
[1] 4 1
A easy vista, esto GroupEquivariantCNN
Parece cualquier CNN antigua… ¿no sería por el group
argumento.
Ahora, cuando inspeccionamos su salida, vemos que la dimensión adicional ha desaparecido. Esto se debe a que después de una secuencia de capas de convolución de grupo a grupo, el módulo proyecta una representación que, para cada elemento del lote, conserva solo canales. Por lo tanto, promedia no sólo las ubicaciones (como lo hacemos normalmente) sino también la dimensión del grupo. Una capa lineal ultimate proporcionará la salida del clasificador solicitado (de dimensión out_channels
).
Y ahí tenemos la arquitectura completa. Es hora de un mundo actual (ish) prueba.
¡Dígitos girados!
La thought es entrenar dos convnets, una CNN “regular” y otra equivalente en grupo, en el conjunto de entrenamiento MNIST routine. Luego, ambos se evalúan en un conjunto de prueba aumentado donde cada imagen se gira aleatoriamente mediante una rotación continua entre 0 y 360 grados. no esperamos GroupEquivariantCNN
ser “perfecto” – no si nos equipamos con (C_4) como grupo de simetría. estrictamente, con (C_4)la equivarianza se extiende sólo a cuatro posiciones. Pero esperamos que funcione significativamente mejor que la arquitectura estándar de sólo cambio equivalente.
Primero, preparamos los datos; en explicit, el conjunto de prueba aumentado.
dir <- "/tmp/mnist"
train_ds <- torchvision::mnist_dataset(
dir,
obtain = TRUE,
rework = torchvision::transform_to_tensor
)
test_ds <- torchvision::mnist_dataset(
dir,
prepare = FALSE,
rework = operate(x) >
torchvision::transform_to_tensor()
)
train_dl <- dataloader(train_ds, batch_size = 128, shuffle = TRUE)
test_dl <- dataloader(test_ds, batch_size = 128)
¿Cómo se ve?
Primero definimos y entrenamos una CNN convencional. Es tan comparable a GroupEquivariantCNN()
en cuanto a arquitectura, como sea posible, y tiene el doble de canales ocultos, para tener una capacidad normal comparable.
default_cnn <- nn_module(
"default_cnn",
initialize = operate(kernel_size, in_channels, out_channels, num_hidden, hidden_channels) {
self$conv1 <- torch::nn_conv2d(in_channels, hidden_channels, kernel_size)
self$convs <- torch::nn_module_list()
for (i in 1:num_hidden) {
self$convs$append(torch::nn_conv2d(hidden_channels, hidden_channels, kernel_size))
}
self$avg_pool <- torch::nn_adaptive_avg_pool2d(1)
self$final_linear <- torch::nn_linear(hidden_channels, out_channels)
},
ahead = operate(x) >
self$final_linear()
x
)
fitted <- default_cnn |>
luz::setup(
loss = torch::nn_cross_entropy_loss(),
optimizer = torch::optim_adam,
metrics = checklist(
luz::luz_metric_accuracy()
)
) |>
luz::set_hparams(
kernel_size = 5,
in_channels = 1,
out_channels = 10,
num_hidden = 4,
hidden_channels = 32
) %>%
luz::set_opt_hparams(lr = 1e-2, weight_decay = 1e-4) |>
luz::match(train_dl, epochs = 10, valid_data = test_dl)
Practice metrics: Loss: 0.0498 - Acc: 0.9843
Legitimate metrics: Loss: 3.2445 - Acc: 0.4479
Como period de esperar, la precisión en el conjunto de prueba no es tan buena.
A continuación, entrenamos la versión equivalente en grupo.
fitted <- GroupEquivariantCNN |>
luz::setup(
loss = torch::nn_cross_entropy_loss(),
optimizer = torch::optim_adam,
metrics = checklist(
luz::luz_metric_accuracy()
)
) |>
luz::set_hparams(
group = CyclicGroup(order = 4),
kernel_size = 5,
in_channels = 1,
out_channels = 10,
num_hidden = 4,
hidden_channels = 16
) |>
luz::set_opt_hparams(lr = 1e-2, weight_decay = 1e-4) |>
luz::match(train_dl, epochs = 10, valid_data = test_dl)
Practice metrics: Loss: 0.1102 - Acc: 0.9667
Legitimate metrics: Loss: 0.4969 - Acc: 0.8549
Para la CNN equivalente en grupo, las precisiones en los conjuntos de prueba y entrenamiento son mucho más cercanas. ¡Ese es un buen resultado! Concluyamos el exploit de hoy retomando un pensamiento de la primera publicación de más alto nivel.
Un desafío
Volviendo al conjunto de prueba aumentado, o mejor dicho, a las muestras de dígitos mostrados, notamos un problema. En la fila dos, columna cuatro, hay un dígito que “en circunstancias normales” debería ser un 9, pero, muy probablemente, es un 6 al revés. (Para un humano, lo que sugiere esto es la cosa parecida a un garabato que parece encontrarse más a menudo con seises que con nueves.) Sin embargo, podrías preguntar: ¿esto tener ser un problema? ¿Quizás la crimson sólo necesita aprender las sutilezas, el tipo de cosas que un humano detectaría?
A mi modo de ver, todo depende del contexto: qué se debe lograr realmente y cómo se va a utilizar una aplicación. Con dígitos en una letra, no veo ninguna razón por la que un solo dígito deba aparecer al revés; en consecuencia, la equivarianza de rotación completa sería contraproducente. En pocas palabras, llegamos al mismo imperativo canónico que los defensores del aprendizaje automático justo y equitativo nos siguen recordando:
¡Piense siempre en la forma en que se utilizará una aplicación!
En nuestro caso, sin embargo, hay otro aspecto, uno técnico. gcnn::GroupEquivariantCNN()
es un contenedor easy, ya que todas sus capas utilizan el mismo grupo de simetría. En principio, no es necesario hacer esto. Con más esfuerzo de codificación, se pueden usar diferentes grupos según la posición de una capa en la jerarquía de detección de características.
Aquí, déjame decirte finalmente por qué elegí la foto de la cabra. La cabra se ve a través de una valla roja y blanca, un patrón –ligeramente girado, debido al ángulo de visión– formado por cuadrados (o bordes, si se prefiere). Ahora, para tal valla, tipos de equivarianza de rotación como la codificada por (C_4) tiene mucho sentido. Sin embargo, preferiríamos no mirar hacia el cielo a la cabra en sí, como lo ilustré. (C_4) acción antes. Por lo tanto, lo que haríamos en una tarea de clasificación de imágenes del mundo actual es usar capas bastante flexibles en la parte inferior y capas cada vez más restringidas en la parte superior de la jerarquía.
¡Gracias por leer!
Foto por Marjan Blan | @marjanblan en desempaquetar