Posit AI Weblog: tocha para dados tabulares


O aprendizado de máquina em dados semelhantes a imagens pode ser muitas coisas: divertido (cães versus gatos), útil para a sociedade (imagens médicas) ou prejudicial para a sociedade (vigilância). Em comparação, os dados tabulares – o pão com manteiga da ciência de dados – podem parecer mais mundanos.

Além do mais, se você estiver particularmente interessado em aprendizado profundo (DL) e procurando os benefícios extras obtidos com large information, grandes arquiteturas e large computacional, é muito mais provável que você construa uma vitrine impressionante sobre o primeiro. em vez do último.

Então, para dados tabulares, por que não usar florestas aleatórias, aumento de gradiente ou outros métodos clássicos? Posso pensar em pelo menos alguns motivos para aprender sobre DL para dados tabulares:

  • Mesmo que todos os seus recursos sejam de escala intervalar ou ordinal, exigindo “apenas” alguma forma de regressão (não necessariamente linear), a aplicação de DL pode resultar em benefícios de desempenho devido a algoritmos sofisticados de otimização, funções de ativação, profundidade de camada e muito mais (mais interações de tudo isso).

  • Se, além disso, houver características categóricas, os modelos DL podem lucrar com incorporação aqueles no espaço contínuo, descobrindo semelhanças e relações que passam despercebidas em representações codificadas one-hot.

  • E se a maioria dos recursos forem numéricos ou categóricos, mas também houver texto na coluna F e uma imagem na coluna G? Com a EAD, diferentes modalidades podem ser trabalhadas por diferentes módulos que alimentam seus resultados em um módulo comum, para assumir a partir daí.

Agenda

Nesta postagem introdutória, mantemos a arquitetura simples. Não experimentamos otimizadores sofisticados ou não linearidades. Nem adicionamos processamento de texto ou imagem. No entanto, fazemos uso de embeddings, e com bastante destaque. Assim, a partir da lista acima, vamos esclarecer o segundo, deixando os outros dois para postagens futuras.

Em poucas palavras, o que veremos é

  • Como criar um personalizado conjunto de dadosadaptado aos dados específicos que você possui.

  • Como lidar com uma combinação de dados numéricos e categóricos.

  • Como extrair representações de espaço contínuo dos módulos de incorporação.

Conjunto de dados

O conjunto de dados, Cogumelosfoi escolhido pela abundância de colunas categóricas. É um conjunto de dados incomum para uso em DL: foi projetado para modelos de aprendizado de máquina para inferir regras lógicas, como em: SE um E NÃO b OU c (…), então é um x.

Os cogumelos são classificados em dois grupos: comestíveis e não comestíveis. A descrição do conjunto de dados lista cinco regras possíveis com as precisões resultantes. Embora o mínimo que queiramos abordar aqui seja o tópico acaloradamente debatido sobre se o DL é adequado ou como poderia ser mais adequado para o aprendizado de regras, nos permitiremos alguma curiosidade e verificaremos o que acontece se removermos sucessivamente todos colunas usadas para construir essas cinco regras.

Ah, e antes de começar a copiar e colar: aqui está o exemplo em um Caderno colaborativo do 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.information",
  destfile = "agaricus-lepiota.information"
)

mushroom_data <- read_csv(
  "agaricus-lepiota.information",
  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 properly take away as a result of there's simply 1 distinctive worth
  choose(-`veil-type`)

Em torch, dataset() cria uma classe R6. Tal como acontece com a maioria das courses R6, geralmente haverá necessidade de um initialize() método. Abaixo, usamos initialize() para pré-processar os dados e armazená-los em partes convenientes. Mais sobre isso em um minuto. Antes disso, observe os outros dois métodos a dataset tem que implementar:

  • .getitem(i) . Este é todo o propósito de um dataset: recupera e retorna a observação localizada em algum índice solicitado. Qual índice? Isso será decidido pelo chamador, um dataloader. Durante o treinamento, geralmente queremos permutar a ordem em que as observações são usadas, sem nos importar com a ordem no caso de validação ou dados de teste.

  • .size(). Este método, novamente para uso de um dataloaderindica quantas observações existem.

Em nosso exemplo, ambos os métodos são simples de implementar. .getitem(i) usa diretamente seu argumento para indexar os dados, e .size() retorna o número de observações:

mushroom_dataset <- dataset(
  title = "mushroom_dataset",

  initialize = perform(indices) {
    information <- self$prepare_mushroom_data(mushroom_data(indices, ))
    self$xcat <- information((1))((1))
    self$xnum <- information((1))((2))
    self$y <- information((2))
  },

  .getitem = perform(i) {
    xcat <- self$xcat(i, )
    xnum <- self$xnum(i, )
    y <- self$y(i, )
    
    listing(x = listing(xcat, xnum), y = y)
  },
  
  .size = perform() {
    dim(self$y)(1)
  },
  
  prepare_mushroom_data = perform(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(perform(x) nlevels(x) != 2)) %>%
      mutate(throughout(.fns = as.integer)) %>%
      as.matrix()

    numerical_cols <- enter %>%
      choose(-toxic) %>%
      choose(the place(perform(x) nlevels(x) == 2)) %>%
      mutate(throughout(.fns = as.integer)) %>%
      as.matrix()
    
    listing(listing(torch_tensor(categorical_cols), torch_tensor(numerical_cols)),
         torch_tensor(target_col))
  }
)

Quanto ao armazenamento de dados, existe um campo para o destino, self$ymas em vez do esperado self$x vemos campos separados para recursos numéricos (self$xnum) e categóricos (self$xcat). Isto é apenas por conveniência: este último será passado para módulos de incorporação, que exigem que suas entradas sejam do tipo torch_long()ao contrário da maioria dos outros módulos que, por padrão, funcionam com torch_float().

Assim, então, todos prepare_mushroom_data() O que faz é dividir os dados nessas três partes.

À parte indispensável: Neste conjunto de dados, realmente todos as características são categóricas – só que para alguns existem apenas dois tipos. Tecnicamente, poderíamos tê-los tratado da mesma forma que os recursos não binários. Mas como normalmente em DL deixamos os recursos binários como estão, usamos isso como uma ocasião para mostrar como lidar com uma combinação de vários tipos de dados.

Nosso costume dataset definido, criamos instâncias para treinamento e validação; cada um recebe seu companheiro dataloader:

train_indices <- pattern(1:nrow(mushroom_data), measurement = ground(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

Em torchquanto você modularizar seus modelos dependem de você. Freqüentemente, altos graus de modularização melhoram a legibilidade e ajudam na solução de problemas.

Aqui levamos em consideração a funcionalidade de incorporação. Um embedding_modulepara receber apenas os recursos categóricos, chamará torchde nn_embedding() em cada um deles:

embedding_module <- nn_module(
  
  initialize = perform(cardinalities) {
    self$embeddings = nn_module_list(lapply(cardinalities, perform(x) nn_embedding(num_embeddings = x, embedding_dim = ceiling(x/2))))
  },
  
  ahead = perform(x) {
    embedded <- vector(mode = "listing", size = size(self$embeddings))
    for (i in 1:size(self$embeddings)) {
      embedded((i)) <- self$embeddings((i))(x( , i))
    }
    torch_cat(embedded, dim = 2)
  }
)

O modelo principal, quando chamado, começa incorporando os recursos categóricos, depois anexa a entrada numérica e continua o processamento:

web <- nn_module(
  "mushroom_net",

  initialize = perform(cardinalities,
                        num_numerical,
                        fc1_dim,
                        fc2_dim) {
    self$embedder <- embedding_module(cardinalities)
    self$fc1 <- nn_linear(sum(map(cardinalities, perform(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 = perform(xcat, xnum) {
    embedded <- self$embedder(xcat)
    all <- torch_cat(listing(embedded, xnum$to(dtype = torch_float())), dim = 2)
    all %>% self$fc1() %>%
      nnf_relu() %>%
      self$fc2() %>%
      self$output() %>%
      nnf_sigmoid()
  }
)

Agora instancie este modelo, passando, por um lado, os tamanhos de saída para as camadas lineares e, por outro, as cardinalidades dos recursos. Este último será usado pelos módulos de incorporação para determinar seus tamanhos de saída, seguindo uma regra simples “incorporar em um espaço de tamanho metade do número de valores de entrada”:

cardinalities <- map(
  mushroom_data( , 2:ncol(mushroom_data)), compose(nlevels, as.issue)) %>%
  hold(perform(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)

Treinamento

O ciclo de treinamento agora é “enterprise as traditional”:

optimizer <- optim_adam(mannequin$parameters, lr = 0.1)

for (epoch in 1:20) {

  mannequin$practice()
  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

Embora a perda no conjunto de validação ainda esteja diminuindo, emblem veremos que a rede aprendeu o suficiente para obter uma precisão de 100%.

Avaliação

Para verificar a precisão da classificação, reutilizamos o conjunto de validação, vendo como ainda não o empregamos para ajuste.

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 <- information.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

Ufa. Nenhuma falha embaraçosa para a abordagem EAD em uma tarefa onde regras simples são suficientes. Além disso, temos sido realmente parcimoniosos quanto ao tamanho da rede.

Antes de concluir com uma inspeção dos embeddings aprendidos, vamos nos divertir obscurecendo as coisas.

Tornando a tarefa mais difícil

As regras a seguir (com as precisões que as acompanham) são relatadas na descrição do conjunto de dados.

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 be

    P_4') inhabitants=clustered.AND.cap_color=white

    These rule contain 6 attributes (out of twenty-two). 

Evidentemente, não há distinção entre conjuntos de treinamento e de teste; mas continuaremos com nossa divisão 80:20 de qualquer maneira. Removeremos sucessivamente todos os atributos mencionados, começando pelos três que permitiam 100% de precisão e continuando subindo. Aqui estão os resultados que obtive propagando o gerador de números aleatórios assim:

cap-color, inhabitants, habitat0,9938
cap-color, inhabitants, habitat, stalk-surface-below-ring, stalk-color-above-ring1
cap-color, inhabitants, habitat, stalk-surface-below-ring, stalk-color-above-ring, spore-print-color0,9994
cap-color, inhabitants, habitat, stalk-surface-below-ring, stalk-color-above-ring, spore-print-color, odor0,9526

Ainda 95% corretos… Embora experimentos como esse sejam divertidos, parece que eles também podem nos dizer algo sério: think about o caso da chamada “desprevenção”, removendo características como raça, gênero ou renda. Quantas variáveis ​​proxy ainda podem restar para permitir inferir os atributos mascarados?

Uma olhada nas representações ocultas

Olhando para a matriz de pesos de um módulo de incorporação, o que vemos são as representações aprendidas dos valores de um recurso. A primeira coluna categórica foi cap-shape; vamos extrair seus embeddings correspondentes:

embedding_weights <- vector(mode = "listing")
for (i in 1: size(mannequin$embedder$embeddings)) {
  embedding_weights((i)) <- mannequin$embedder$embeddings((i))$parameters$weight$to(gadget = "cpu")
}

cap_shape_repr <- embedding_weights((1))
cap_shape_repr
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} )

O número de colunas é três, pois foi isso que escolhemos ao criar a camada de incorporação. O número de linhas é seis, correspondendo ao número de categorias disponíveis. Podemos procurar categorias por recurso na descrição do conjunto de dados (agaricus-lepiota.nomes):

cap_shapes <- c("bell", "conical", "convex", "flat", "knobbed", "sunken")

Para visualização, é conveniente fazer análise de componentes principais (mas existem outras opções, como t-SNE). Aqui estão as seis formas de tampa no espaço bidimensional:

pca <- prcomp(cap_shape_repr, middle = TRUE, scale. = TRUE, rank = 2)$x(, c("PC1", "PC2"))

pca %>%
  as.information.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()

Posit AI Weblog: tocha para dados tabulares

Naturalmente, o quão interessantes você acha os resultados depende de quanto você se preocupa com a representação oculta de uma variável. Análises como estas podem rapidamente transformar-se numa actividade onde se deve aplicar extrema cautela, uma vez que quaisquer enviesamentos nos dados traduzir-se-ão imediatamente em representações tendenciosas. Além disso, a redução ao espaço bidimensional pode ou não ser adequada.

Isso conclui nossa introdução ao torch para dados tabulares. Embora o foco conceitual estivesse nas características categóricas e em como usá-las em combinação com as numéricas, tomamos o cuidado de também fornecer informações básicas sobre algo que surgirá repetidamente: definir um dataset adaptado à tarefa em questão.

Obrigado por ler!

Deixe um comentário

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