El aprendizaje automático a partir de datos similares a imágenes puede ser muchas cosas: divertido (perros versus gatos), socialmente útil (imágenes médicas) o socialmente dañino (vigilancia). En comparación, los datos tabulares (el pan y la mantequilla de la ciencia de datos) pueden parecer más mundanos.
Es más, si está particularmente interesado en el aprendizaje profundo (DL) y busca los beneficios adicionales que se pueden obtener del huge knowledge, las grandes arquitecturas y la gran computación, es mucho más possible que construya un escaparate impresionante sobre el primero. en lugar de este último.
Entonces, para los datos tabulares, ¿por qué no utilizar bosques aleatorios, aumento de gradiente u otros métodos clásicos? Se me ocurren al menos algunas razones para aprender sobre DL para datos tabulares:
-
Incluso si todas sus características son de escala de intervalo u ordinales, por lo que requieren “sólo” alguna forma de regresión (no necesariamente lineal), la aplicación de DL puede generar beneficios de rendimiento debido a algoritmos de optimización sofisticados, funciones de activación, profundidad de capa y más (más interacciones de todos ellos).
-
Si, además, existen características categóricas, los modelos DL pueden beneficiarse de incrustar aquellos en el espacio continuo, descubriendo similitudes y relaciones que pasan desapercibidas en representaciones codificadas en caliente.
-
¿Qué pasa si la mayoría de las funciones son numéricas o categóricas, pero también hay texto en la columna F y una imagen en la columna G? Con DL, diferentes módulos pueden trabajar en diferentes modalidades que alimentan sus resultados en un módulo común, para tomar el management desde allí.
Orden del día
En esta publicación introductoria, mantenemos la arquitectura sencilla. No experimentamos con optimizadores sofisticados o no linealidades. Tampoco añadimos procesamiento de texto o imágenes. Sin embargo, utilizamos incrustaciones, y de manera bastante destacada. Por lo tanto, de la lista anterior, arrojaremos luz sobre el segundo, dejando los otros dos para publicaciones futuras.
En pocas palabras, lo que veremos es
-
Cómo crear una costumbre conjunto de datosadaptado a los datos específicos que tienes.
-
Cómo manejar una combinación de datos numéricos y categóricos.
-
Cómo extraer representaciones de espacio continuo de los módulos de incrustación.
Conjunto de datos
El conjunto de datos, Hongosfue elegido por su abundancia de columnas categóricas. Es un conjunto de datos inusual para usar en DL: fue diseñado para que los modelos de aprendizaje automático infieran reglas lógicas, como en: IF a Y NO b O do […]entonces es un incógnita.
Las setas se clasifican en dos grupos: comestibles y no comestibles. La descripción del conjunto de datos enumera cinco reglas posibles con sus precisiones resultantes. Si bien lo menos que queremos abordar aquí es el tema tan debatido de si la DL es adecuada para el aprendizaje de reglas o cómo podría adaptarse mejor al aprendizaje de reglas, nos permitiremos un poco de curiosidad y comprobaremos qué sucede si eliminamos sucesivamente todas las reglas. columnas utilizadas para construir esas cinco reglas.
Ah, y antes de empezar a copiar y pegar: aquí está el ejemplo en un Cuaderno de colaboración de Google.
library(torch)
library(purrr)
library(readr)
library(dplyr)
library(ggplot2)
library(ggrepel)
obtain.file(
"https://archive.ics.uci.edu/ml/machine-learning-databases/mushroom/agaricus-lepiota.knowledge",
destfile = "agaricus-lepiota.knowledge"
)
mushroom_data <- read_csv(
"agaricus-lepiota.knowledge",
col_names = c(
"toxic",
"cap-shape",
"cap-surface",
"cap-color",
"bruises",
"odor",
"gill-attachment",
"gill-spacing",
"gill-size",
"gill-color",
"stalk-shape",
"stalk-root",
"stalk-surface-above-ring",
"stalk-surface-below-ring",
"stalk-color-above-ring",
"stalk-color-below-ring",
"veil-type",
"veil-color",
"ring-type",
"ring-number",
"spore-print-color",
"inhabitants",
"habitat"
),
col_types = rep("c", 23) %>% paste(collapse = "")
) %>%
# can as effectively take away as a result of there's simply 1 distinctive worth
choose(-`veil-type`)
En torch
, dataset()
crea una clase R6. Como ocurre con la mayoría de las clases R6, normalmente será necesario un initialize()
método. A continuación utilizamos initialize()
para preprocesar los datos y almacenarlos en partes convenientes. Más sobre eso en un minuto. Antes de eso, tenga en cuenta los otros dos métodos dataset
tiene que implementar:
-
.getitem(i)
. Este es todo el propósito de undataset
: Recuperar y devolver la observación ubicada en algún índice que se le solicite. ¿Qué índice? Eso lo decidirá quien llama, undataloader
. Durante el entrenamiento, normalmente queremos permutar el orden en el que se utilizan las observaciones, sin preocuparnos por el orden en el caso de validación o datos de prueba. -
.size()
. Este método, nuevamente para el uso de undataloader
indica cuántas observaciones hay.
En nuestro ejemplo, ambos métodos son sencillos de implementar. .getitem(i)
utiliza directamente su argumento para indexar los datos, y .size()
devuelve el número de observaciones:
mushroom_dataset <- dataset(
title = "mushroom_dataset",
initialize = operate(indices) {
knowledge <- self$prepare_mushroom_data(mushroom_data[indices, ])
self$xcat <- knowledge[[1]][[1]]
self$xnum <- knowledge[[1]][[2]]
self$y <- knowledge[[2]]
},
.getitem = operate(i) {
xcat <- self$xcat[i, ]
xnum <- self$xnum[i, ]
y <- self$y[i, ]
checklist(x = checklist(xcat, xnum), y = y)
},
.size = operate() {
dim(self$y)[1]
},
prepare_mushroom_data = operate(enter) {
enter <- enter %>%
mutate(throughout(.fns = as.issue))
target_col <- enter$toxic %>%
as.integer() %>%
`-`(1) %>%
as.matrix()
categorical_cols <- enter %>%
choose(-toxic) %>%
choose(the place(operate(x) nlevels(x) != 2)) %>%
mutate(throughout(.fns = as.integer)) %>%
as.matrix()
numerical_cols <- enter %>%
choose(-toxic) %>%
choose(the place(operate(x) nlevels(x) == 2)) %>%
mutate(throughout(.fns = as.integer)) %>%
as.matrix()
checklist(checklist(torch_tensor(categorical_cols), torch_tensor(numerical_cols)),
torch_tensor(target_col))
}
)
En cuanto al almacenamiento de datos, hay un campo para el destino, self$y
pero en lugar de lo esperado self$x
Vemos campos separados para características numéricas (self$xnum
) y categóricos (self$xcat
). Esto es sólo por conveniencia: este último se pasará a módulos integrados, que requieren que sus entradas sean del tipo torch_long()
a diferencia de la mayoría de los otros módulos que, de forma predeterminada, funcionan con torch_float()
.
En consecuencia, entonces, todos prepare_mushroom_data()
Lo que hace es dividir los datos en esas tres partes.
Aparte indispensable: En este conjunto de datos, realmente todo Las características resultan ser categóricas; es solo que para algunos, solo hay dos tipos. Técnicamente, podríamos haberlas tratado igual que las funciones no binarias. Pero como normalmente en DL simplemente dejamos las características binarias como están, aprovechamos esto como una ocasión para mostrar cómo manejar una combinación de varios tipos de datos.
nuestra costumbre dataset
definido, creamos instancias de capacitación y validación; cada uno tiene su compañero dataloader
:
train_indices <- pattern(1:nrow(mushroom_data), measurement = flooring(0.8 * nrow(mushroom_data)))
valid_indices <- setdiff(1:nrow(mushroom_data), train_indices)
train_ds <- mushroom_dataset(train_indices)
train_dl <- train_ds %>% dataloader(batch_size = 256, shuffle = TRUE)
valid_ds <- mushroom_dataset(valid_indices)
valid_dl <- valid_ds %>% dataloader(batch_size = 256, shuffle = FALSE)
Modelo
En torch
cuanto tu modularizar Tus modelos dependen de ti. A menudo, los altos grados de modularización mejoran la legibilidad y ayudan con la resolución de problemas.
Aquí factorizamos la funcionalidad de incrustación. Un embedding_module
para pasar las características categóricas únicamente, llamará torch
‘s nn_embedding()
en cada uno de ellos:
embedding_module <- nn_module(
initialize = operate(cardinalities) {
self$embeddings = nn_module_list(lapply(cardinalities, operate(x) nn_embedding(num_embeddings = x, embedding_dim = ceiling(x/2))))
},
ahead = operate(x) {
embedded <- vector(mode = "checklist", size = size(self$embeddings))
for (i in 1:size(self$embeddings)) {
embedded[[i]] <- self$embeddings[[i]](x[ , i])
}
torch_cat(embedded, dim = 2)
}
)
El modelo principal, cuando se llama, comienza incorporando las características categóricas, luego agrega la entrada numérica y continúa procesando:
web <- nn_module(
"mushroom_net",
initialize = operate(cardinalities,
num_numerical,
fc1_dim,
fc2_dim) {
self$embedder <- embedding_module(cardinalities)
self$fc1 <- nn_linear(sum(map(cardinalities, operate(x) ceiling(x/2)) %>% unlist()) + num_numerical, fc1_dim)
self$fc2 <- nn_linear(fc1_dim, fc2_dim)
self$output <- nn_linear(fc2_dim, 1)
},
ahead = operate(xcat, xnum) {
embedded <- self$embedder(xcat)
all <- torch_cat(checklist(embedded, xnum$to(dtype = torch_float())), dim = 2)
all %>% self$fc1() %>%
nnf_relu() %>%
self$fc2() %>%
self$output() %>%
nnf_sigmoid()
}
)
Ahora cree una instancia de este modelo, pasando, por un lado, los tamaños de salida para las capas lineales y, por el otro, las cardinalidades de las características. Este último será utilizado por los módulos de incrustación para determinar sus tamaños de salida, siguiendo una regla easy “incrustar en un espacio del tamaño de la mitad del número de valores de entrada”:
cardinalities <- map(
mushroom_data[ , 2:ncol(mushroom_data)], compose(nlevels, as.issue)) %>%
hold(operate(x) x > 2) %>%
unlist() %>%
unname()
num_numerical <- ncol(mushroom_data) - size(cardinalities) - 1
fc1_dim <- 16
fc2_dim <- 16
mannequin <- web(
cardinalities,
num_numerical,
fc1_dim,
fc2_dim
)
gadget <- if (cuda_is_available()) torch_device("cuda:0") else "cpu"
mannequin <- mannequin$to(gadget = gadget)
Capacitación
El ciclo de capacitación ahora es “lo mismo de siempre”:
optimizer <- optim_adam(mannequin$parameters, lr = 0.1)
for (epoch in 1:20) {
mannequin$prepare()
train_losses <- c()
coro::loop(for (b in train_dl) {
optimizer$zero_grad()
output <- mannequin(b$x[[1]]$to(gadget = gadget), b$x[[2]]$to(gadget = gadget))
loss <- nnf_binary_cross_entropy(output, b$y$to(dtype = torch_float(), gadget = gadget))
loss$backward()
optimizer$step()
train_losses <- c(train_losses, loss$merchandise())
})
mannequin$eval()
valid_losses <- c()
coro::loop(for (b in valid_dl) {
output <- mannequin(b$x[[1]]$to(gadget = gadget), b$x[[2]]$to(gadget = gadget))
loss <- nnf_binary_cross_entropy(output, b$y$to(dtype = torch_float(), gadget = gadget))
valid_losses <- c(valid_losses, loss$merchandise())
})
cat(sprintf("Loss at epoch %d: coaching: %3f, validation: %3fn", epoch, imply(train_losses), imply(valid_losses)))
}
Loss at epoch 1: coaching: 0.274634, validation: 0.111689
Loss at epoch 2: coaching: 0.057177, validation: 0.036074
Loss at epoch 3: coaching: 0.025018, validation: 0.016698
Loss at epoch 4: coaching: 0.010819, validation: 0.010996
Loss at epoch 5: coaching: 0.005467, validation: 0.002849
Loss at epoch 6: coaching: 0.002026, validation: 0.000959
Loss at epoch 7: coaching: 0.000458, validation: 0.000282
Loss at epoch 8: coaching: 0.000231, validation: 0.000190
Loss at epoch 9: coaching: 0.000172, validation: 0.000144
Loss at epoch 10: coaching: 0.000120, validation: 0.000110
Loss at epoch 11: coaching: 0.000098, validation: 0.000090
Loss at epoch 12: coaching: 0.000079, validation: 0.000074
Loss at epoch 13: coaching: 0.000066, validation: 0.000064
Loss at epoch 14: coaching: 0.000058, validation: 0.000055
Loss at epoch 15: coaching: 0.000052, validation: 0.000048
Loss at epoch 16: coaching: 0.000043, validation: 0.000042
Loss at epoch 17: coaching: 0.000038, validation: 0.000038
Loss at epoch 18: coaching: 0.000034, validation: 0.000034
Loss at epoch 19: coaching: 0.000032, validation: 0.000031
Loss at epoch 20: coaching: 0.000028, validation: 0.000027
Si bien la pérdida en el conjunto de validación sigue disminuyendo, pronto veremos que la pink ha aprendido lo suficiente para obtener una precisión del 100%.
Evaluación
Para verificar la precisión de la clasificación, reutilizamos el conjunto de validación, ya que de todos modos no lo hemos utilizado para realizar ajustes.
mannequin$eval()
test_dl <- valid_ds %>% dataloader(batch_size = valid_ds$.size(), shuffle = FALSE)
iter <- test_dl$.iter()
b <- iter$.subsequent()
output <- mannequin(b$x[[1]]$to(gadget = gadget), b$x[[2]]$to(gadget = gadget))
preds <- output$to(gadget = "cpu") %>% as.array()
preds <- ifelse(preds > 0.5, 1, 0)
comp_df <- knowledge.body(preds = preds, y = b[[2]] %>% as_array())
num_correct <- sum(comp_df$preds == comp_df$y)
num_total <- nrow(comp_df)
accuracy <- num_correct/num_total
accuracy
1
Uf. No hay ningún fracaso vergonzoso para el enfoque DL en una tarea donde las reglas sencillas son suficientes. Además, hemos sido muy parcos en cuanto al tamaño de la pink.
Antes de concluir con una inspección de las incrustaciones aprendidas, divirtámonos oscureciendo cosas.
Haciendo la tarea más difícil
Las siguientes reglas (con las precisiones que las acompañan) se informan en la descripción del conjunto de datos.
Disjunctive guidelines for toxic mushrooms, from most basic
to most particular:
P_1) odor=NOT(almond.OR.anise.OR.none)
120 toxic instances missed, 98.52% accuracy
P_2) spore-print-color=inexperienced
48 instances missed, 99.41% accuracy
P_3) odor=none.AND.stalk-surface-below-ring=scaly.AND.
(stalk-color-above-ring=NOT.brown)
8 instances missed, 99.90% accuracy
P_4) habitat=leaves.AND.cap-color=white
100% accuracy
Rule P_4) may additionally be
P_4') inhabitants=clustered.AND.cap_color=white
These rule contain 6 attributes (out of twenty-two).
Evidentemente, no se hace distinción entre conjuntos de entrenamiento y de prueba; pero de todos modos nos quedaremos con nuestra división 80:20. Eliminaremos sucesivamente todos los atributos mencionados, comenzando con los tres que permitieron una precisión del 100% y continuando hacia arriba. Estos son los resultados que obtuve al sembrar el generador de números aleatorios de esta manera:
cap-color, inhabitants, habitat |
0.9938 |
cap-color, inhabitants, habitat, stalk-surface-below-ring, stalk-color-above-ring |
1 |
cap-color, inhabitants, habitat, stalk-surface-below-ring, stalk-color-above-ring, spore-print-color |
0.9994 |
cap-color, inhabitants, habitat, stalk-surface-below-ring, stalk-color-above-ring, spore-print-color, odor |
0.9526 |
Aún así, 95% de razón… Si bien experimentos como este son divertidos, parece que también pueden decirnos algo serio: think about el caso de la llamada “desescalada” al eliminar características como raza, género o ingresos. ¿Cuántas variables proxy pueden quedar todavía que permitan inferir los atributos enmascarados?
Una mirada a las representaciones ocultas
Al observar la matriz de pesos de un módulo de incrustación, lo que vemos son las representaciones aprendidas de los valores de una característica. La primera columna categórica fue cap-shape
; extraigamos sus correspondientes incrustaciones:
torch_tensor
-0.0025 -0.1271 1.8077
-0.2367 -2.6165 -0.3363
-0.5264 -0.9455 -0.6702
0.3057 -1.8139 0.3762
-0.8583 -0.7752 1.0954
0.2740 -0.7513 0.4879
[ CPUFloatType{6,3} ]
El número de columnas es tres, ya que eso es lo que elegimos al crear la capa de incrustación. El número de filas es seis, lo que coincide con el número de categorías disponibles. Podemos buscar categorías por función en la descripción del conjunto de datos (agaricus-lepiota.nombres):
cap_shapes <- c("bell", "conical", "convex", "flat", "knobbed", "sunken")
Para la visualización, es conveniente realizar un análisis de componentes principales (pero existen otras opciones, como t-SNE). Aquí están las seis formas de gorras en un espacio bidimensional:
pca <- prcomp(cap_shape_repr, heart = TRUE, scale. = TRUE, rank = 2)$x[, c("PC1", "PC2")]
pca %>%
as.knowledge.body() %>%
mutate(class = cap_shapes) %>%
ggplot(aes(x = PC1, y = PC2)) +
geom_point() +
geom_label_repel(aes(label = class)) +
coord_cartesian(xlim = c(-2, 2), ylim = c(-2, 2)) +
theme(facet.ratio = 1) +
theme_classic()
Naturalmente, lo interesantes que le parezcan los resultados depende de cuánto le importe la representación oculta de una variable. Análisis como estos pueden convertirse rápidamente en una actividad en la que se debe aplicar extrema precaución, ya que cualquier sesgo en los datos se traducirá inmediatamente en representaciones sesgadas. Además, la reducción al espacio bidimensional puede ser adecuada o no.
Con esto concluye nuestra introducción a torch
para datos tabulares. Si bien el enfoque conceptual se centró en las características categóricas y en cómo usarlas en combinación con las numéricas, también nos hemos ocupado de proporcionar antecedentes sobre algo que surgirá una y otra vez: definir una dataset
adaptado a la tarea en cuestión.
¡Gracias por leer!