Posit AI Weblog: Convnets Variational com TFProbability


Um pouco mais de um ano atrás, em sua bela postagem de convidadoNick Strayer mostrou como classificar um conjunto de atividades diárias usando dados de giroscópio e acelerômetro gravados em smartphone. A precisão foi muito boa, mas Nick passou a inspecionar os resultados de classificação mais de perto. Havia atividades mais propensas a classificação incorreta do que outras? E quanto a esses resultados errôneos: a rede os relatou com igual ou menos confiança do que aqueles que estavam corretos?

Tecnicamente, quando falamos de confiança Dessa maneira, estamos nos referindo ao pontuação obtido para a classe “vencedora” após a ativação do softmax. Se essa pontuação vencedora for de 0,9, podemos dizer “a rede tem certeza de que é um pinguim do Gentoo”; Se for 0,2, concluímos “para a rede, nenhuma opção parecia apropriada, mas Cheetah parecia melhor”.

Esse uso de “confiança” é convincente, mas não tem nada a ver com confiança – ou credibilidade ou previsão, o que você tem – intervalos. O que realmente gostaríamos de poder fazer é colocar distribuições sobre os pesos da rede e fazê -lo Bayesiano. Usando TFProbabilityAs camadas compatíveis com as keras variacionais, isso é algo que realmente podemos fazer.

Adicionando estimativas de incerteza aos modelos Keras com probabatização mostra como usar uma camada densa variacional para obter estimativas de incerteza epistêmica. Neste put up, modificamos o convnet usado na postagem de Nick para ser variacional. Antes de começarmos, vamos resumir rapidamente a tarefa.

A tarefa

Para criar o Reconhecimento baseado em smartphone de atividades humanas e conjunto de dados de transições posturais (Reyes-Ortiz et al. 2016)os pesquisadores tiveram assuntos a pé, sentados, permanecem e transitam de uma dessas atividades para outra. Enquanto isso, dois tipos de sensores de smartphone foram usados ​​para gravar dados de movimento: Acelerômetros medir a aceleração linear em três dimensões, enquanto Giroscópios são usados ​​para rastrear a velocidade angular ao redor dos eixos de coordenadas. Aqui estão os respectivos dados de sensor bruto para seis tipos de atividades da postagem authentic de Nick:

Assim como Nick, vamos aumentar esses seis tipos de atividade e tentar inferi -los dos dados do sensor. É necessária algumas disputas de dados para colocar o conjunto de dados em um formulário com o qual podemos trabalhar; Aqui vamos desenvolver a postagem de Nick e começaremos efetivamente a partir dos dados bem pré-processados ​​e divididos em conjuntos de treinamento e teste:

Observations: 289
Variables: 6
$ experiment     1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 14, 17, 18, 19, 2…
$ userId         1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 7, 7, 9, 9, 10, 10, 11…
$ exercise       7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7…
$ information           (, ,  STAND_TO_SIT, STAND_TO_SIT, STAND_TO_SIT, STAND_TO_S…
$ observationId  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 14, 17, 18, 19, 2…
Observations: 69
Variables: 6
$ experiment     11, 12, 15, 16, 32, 33, 42, 43, 52, 53, 56, 57, 11, …
$ userId         6, 6, 8, 8, 16, 16, 21, 21, 26, 26, 28, 28, 6, 6, 8,…
$ exercise       7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8…
$ information           (, ,  STAND_TO_SIT, STAND_TO_SIT, STAND_TO_SIT, STAND_TO_S…
$ observationId  11, 12, 15, 16, 31, 32, 41, 42, 51, 52, 55, 56, 71, …

O código necessário para chegar a este estágio (copiado da postagem de Nick) pode ser encontrado no apêndice na parte inferior desta página.

Pipeline de treinamento

O conjunto de dados em questão é pequeno o suficiente para caber na memória – mas o seu pode não ser, por isso não pode prejudicar o streaming em ação. Além disso, provavelmente é seguro dizer isso com o tensorflow 2.0, tfdatasets Os pipelines são o maneira de alimentar dados para um modelo.

Depois que o código listado no apêndice é executado, os dados do sensor podem ser encontrados em trainData$informationuma coluna de lista contendo information.bodys, onde cada linha corresponde a um ponto no tempo e cada coluna contém uma das medições. No entanto, nem todas as séries temporais (gravações) são do mesmo comprimento; Assim, seguimos a postagem authentic para encaixar todas as séries em comprimento pad_size (= 338). A forma esperada dos lotes de treinamento será então (batch_size, pad_size, 6).

Inicialmente, criamos nosso conjunto de dados de treinamento:

train_x <- train_data$information %>% 
  map(as.matrix) %>%
  pad_sequences(maxlen = pad_size, dtype = "float32") %>%
  tensor_slices_dataset() 

train_y <- train_data$exercise %>% 
  one_hot_classes() %>% 
  tensor_slices_dataset()

train_dataset <- zip_datasets(train_x, train_y)
train_dataset

Então embaralhe e lote:

n_train <- nrow(train_data)
# the best attainable batch measurement for this dataset
# chosen as a result of it yielded the perfect efficiency
# alternatively, experiment with e.g. completely different studying charges, ...
batch_size <- n_train

train_dataset <- train_dataset %>% 
  dataset_shuffle(n_train) %>%
  dataset_batch(batch_size)
train_dataset

O mesmo para os dados do teste.

test_x <- test_data$information %>% 
  map(as.matrix) %>%
  pad_sequences(maxlen = pad_size, dtype = "float32") %>%
  tensor_slices_dataset() 

test_y <- test_data$exercise %>% 
  one_hot_classes() %>% 
  tensor_slices_dataset()

n_test <- nrow(test_data)
test_dataset <- zip_datasets(test_x, test_y) %>%
  dataset_batch(n_test)

Usando tfdatasets Não significa que não podemos executar uma verificação rápida de sanidade em nossos dados:

first <- test_dataset %>% 
  reticulate::as_iterator() %>% 
  # get first batch (= complete check set, in our case)
  reticulate::iter_next() %>%
  # predictors solely
  .((1)) %>% 
  # first merchandise in batch
  .(1,,)
first
tf.Tensor(
(( 0.          0.          0.          0.          0.          0.        )
 ( 0.          0.          0.          0.          0.          0.        )
 ( 0.          0.          0.          0.          0.          0.        )
 ...
 ( 1.00416672  0.2375      0.12916666 -0.40225476 -0.20463985 -0.14782938)
 ( 1.04166663  0.26944447  0.12777779 -0.26755899 -0.02779437 -0.1441642 )
 ( 1.0250001   0.27083334  0.15277778 -0.19639318  0.35094208 -0.16249016)),
 form=(338, 6), dtype=float64)

Agora vamos construir a rede.

Uma convnet variacional

Construímos a arquitetura convolucional direta do put up de Nick, apenas fazendo pequenas modificações em tamanhos de kernel e números de filtros. Também jogamos fora todas as camadas de abandono; Nenhuma regularização adicional é necessária sobre os anteriores aplicados aos pesos.

Observe o seguinte sobre a rede “bayesificada”.

  • Cada camada é de natureza variacional, os convolucionais (camada_conv_1d_flipout), bem como as camadas densas (Layer_Dense_flipout).

  • Com camadas variacionais, podemos especificar a distribuição de peso anterior, bem como a forma do posterior; Aqui, os padrões são usados, resultando em um posterior padrão anterior e em um campo médio padrão.

  • Da mesma forma, o usuário pode influenciar a função de divergência usada para avaliar a incompatibilidade entre anterior e posterior; Nesse caso, na verdade executamos alguma ação: escalamos a divergência (padrão) KL pelo número de amostras no conjunto de treinamento.

  • Uma última coisa a observar é a camada de saída. É uma camada de distribuição, ou seja, uma camada envolvendo uma distribuição – onde o embrulho significa: treinar a rede é comercial como de costume, mas as previsões são distribuiçõesum para cada ponto de dados.

library(tfprobability)

num_classes <- 6

# scale the KL divergence by variety of coaching examples
n <- n_train %>% tf$forged(tf$float32)
kl_div <- perform(q, p, unused)
  tfd_kl_divergence(q, p) / n

mannequin <- keras_model_sequential()
mannequin %>% 
  layer_conv_1d_flipout(
    filters = 12,
    kernel_size = 3, 
    activation = "relu",
    kernel_divergence_fn = kl_div
  ) %>%
  layer_conv_1d_flipout(
    filters = 24,
    kernel_size = 5, 
    activation = "relu",
    kernel_divergence_fn = kl_div
  ) %>%
  layer_conv_1d_flipout(
    filters = 48,
    kernel_size = 7, 
    activation = "relu",
    kernel_divergence_fn = kl_div
  ) %>%
  layer_global_average_pooling_1d() %>% 
  layer_dense_flipout(
    models = 48,
    activation = "relu",
    kernel_divergence_fn = kl_div
  ) %>% 
  layer_dense_flipout(
    num_classes, 
    kernel_divergence_fn = kl_div,
    identify = "dense_output"
  ) %>%
  layer_one_hot_categorical(event_size = num_classes)

Dizemos à rede para minimizar a probabilidade negativa do log.

nll <- perform(y, mannequin) - (mannequin %>% tfd_log_prob(y))

Isso se tornará parte da perda. A maneira como configuramos este exemplo, essa não é a parte mais substancial. Aqui, o que domina a perda é a soma das divergências de KL, adicionadas (automaticamente) a mannequin$losses.

Em uma configuração como essa, é interessante monitorar ambas as partes da perda separadamente. Podemos fazer isso por meio de duas métricas:

# the KL a part of the loss
kl_part <-  perform(y_true, y_pred) {
    kl <- tf$reduce_sum(mannequin$losses)
    kl
}

# the NLL half
nll_part <- perform(y_true, y_pred) {
    cat_dist <- tfd_one_hot_categorical(logits = y_pred)
    nll <- - (cat_dist %>% tfd_log_prob(y_true) %>% tf$reduce_mean())
    nll
}

Treinamos um pouco mais do que Nick no put up authentic, permitindo a parada precoce.

mannequin %>% compile(
  optimizer = "rmsprop",
  loss = nll,
  metrics = c("accuracy", 
              custom_metric("kl_part", kl_part),
              custom_metric("nll_part", nll_part)),
  experimental_run_tf_function = FALSE
)

train_history <- mannequin %>% match(
  train_dataset,
  epochs = 1000,
  validation_data = test_dataset,
  callbacks = listing(
    callback_early_stopping(endurance = 10)
  )
)

Embora a perda geral diminua linearmente (e provavelmente teria para muitas outras épocas), esse não é o caso da precisão da classificação ou da parte da NLL da perda:

Posit AI Weblog: Convnets Variational com TFProbability

A precisão closing não é tão alta quanto na configuração não variária, embora ainda não seja ruim para um problema de seis lessons. Vemos que, sem regularização adicional, há muito pouco ajuste para os dados de treinamento.

Agora, como obtemos previsões desse modelo?

Previsões probabilísticas

Embora não entremos nisso aqui, é bom saber que acessamos mais do que apenas as distribuições de saída; através deles kernel_posterior Atributo, também podemos acessar as distribuições de peso posterior das camadas ocultas.

Dado o tamanho pequeno do conjunto de testes, calculamos todas as previsões de uma só vez. As previsões agora são distribuições categóricas, uma para cada amostra no lote:

test_data_all <- dataset_collect(test_dataset) %>% { .((1))((1))}

one_shot_preds <- mannequin(test_data_all) 

one_shot_preds
tfp.distributions.OneHotCategorical(
 "sequential_one_hot_categorical_OneHotCategorical_OneHotCategorical",
 batch_shape=(69), event_shape=(6), dtype=float32)

Nós prefixamos essas previsões com one_shot Para indicar sua natureza barulhenta: essas são previsões obtidas em uma única passagem pela rede, todos os pesos da camada sendo amostrados de seus respectivos posteriors.

A partir das distribuições previstas, calculamos a média e o desvio padrão POR (teste) amostra.

one_shot_means <- tfd_mean(one_shot_preds) %>% 
  as.matrix() %>%
  as_tibble() %>% 
  mutate(obs = 1:n()) %>% 
  collect(class, imply, -obs) 

one_shot_sds <- tfd_stddev(one_shot_preds) %>% 
  as.matrix() %>%
  as_tibble() %>% 
  mutate(obs = 1:n()) %>% 
  collect(class, sd, -obs) 

Os desvios padrão assim obtidos podem ser considerados para refletir o geral incerteza preditiva. Podemos estimar outro tipo de incerteza, chamado epistêmicafazendo vários passes pela rede e depois calculando – novamente, por amostra de teste – os desvios padrão dos meios previstos.

mc_preds <- purrr::map(1:100, perform(x) {
  preds <- mannequin(test_data_all)
  tfd_mean(preds) %>% as.matrix()
})

mc_sds <- abind::abind(mc_preds, alongside = 3) %>% 
  apply(c(1,2), sd) %>% 
  as_tibble() %>%
  mutate(obs = 1:n()) %>% 
  collect(class, mc_sd, -obs) 

Juntando tudo, nós temos

pred_data <- one_shot_means %>%
  inner_join(one_shot_sds, by = c("obs", "class")) %>% 
  inner_join(mc_sds, by = c("obs", "class")) %>% 
  right_join(one_hot_to_label, by = "class") %>% 
  prepare(obs)

pred_data
# A tibble: 414 x 6
     obs class       imply      sd    mc_sd label       
                         
 1     1 V1    0.945      0.227   0.0743   STAND_TO_SIT
 2     1 V2    0.0534     0.225   0.0675   SIT_TO_STAND
 3     1 V3    0.00114    0.0338  0.0346   SIT_TO_LIE  
 4     1 V4    0.00000238 0.00154 0.000336 LIE_TO_SIT  
 5     1 V5    0.0000132  0.00363 0.00164  STAND_TO_LIE
 6     1 V6    0.0000305  0.00553 0.00398  LIE_TO_STAND
 7     2 V1    0.993      0.0813  0.149    STAND_TO_SIT
 8     2 V2    0.00153    0.0390  0.102    SIT_TO_STAND
 9     2 V3    0.00476    0.0688  0.108    SIT_TO_LIE  
10     2 V4    0.00000172 0.00131 0.000613 LIE_TO_SIT  
# … with 404 extra rows

Comparando previsões com a verdade do fundamento:

eval_table <- pred_data %>% 
  group_by(obs) %>% 
  summarise(
    maxprob = max(imply),
    maxprob_sd = sd(imply == maxprob),
    maxprob_mc_sd = mc_sd(imply == maxprob),
    predicted = label(imply == maxprob)
  ) %>% 
  mutate(
    fact = test_data$activityName,
    appropriate = fact == predicted
  ) 

eval_table %>% print(n = 20)
# A tibble: 69 x 7
     obs maxprob maxprob_sd maxprob_mc_sd predicted    fact        appropriate
                                        
 1     1   0.945     0.227         0.0743 STAND_TO_SIT STAND_TO_SIT TRUE   
 2     2   0.993     0.0813        0.149  STAND_TO_SIT STAND_TO_SIT TRUE   
 3     3   0.733     0.443         0.131  STAND_TO_SIT STAND_TO_SIT TRUE   
 4     4   0.796     0.403         0.138  STAND_TO_SIT STAND_TO_SIT TRUE   
 5     5   0.843     0.364         0.358  SIT_TO_STAND STAND_TO_SIT FALSE  
 6     6   0.816     0.387         0.176  SIT_TO_STAND STAND_TO_SIT FALSE  
 7     7   0.600     0.490         0.370  STAND_TO_SIT STAND_TO_SIT TRUE   
 8     8   0.941     0.236         0.0851 STAND_TO_SIT STAND_TO_SIT TRUE   
 9     9   0.853     0.355         0.274  SIT_TO_STAND STAND_TO_SIT FALSE  
10    10   0.961     0.195         0.195  STAND_TO_SIT STAND_TO_SIT TRUE   
11    11   0.918     0.275         0.168  STAND_TO_SIT STAND_TO_SIT TRUE   
12    12   0.957     0.203         0.150  STAND_TO_SIT STAND_TO_SIT TRUE   
13    13   0.987     0.114         0.188  SIT_TO_STAND SIT_TO_STAND TRUE   
14    14   0.974     0.160         0.248  SIT_TO_STAND SIT_TO_STAND TRUE   
15    15   0.996     0.0657        0.0534 SIT_TO_STAND SIT_TO_STAND TRUE   
16    16   0.886     0.318         0.0868 SIT_TO_STAND SIT_TO_STAND TRUE   
17    17   0.773     0.419         0.173  SIT_TO_STAND SIT_TO_STAND TRUE   
18    18   0.998     0.0444        0.222  SIT_TO_STAND SIT_TO_STAND TRUE   
19    19   0.885     0.319         0.161  SIT_TO_STAND SIT_TO_STAND TRUE   
20    20   0.930     0.255         0.271  SIT_TO_STAND SIT_TO_STAND TRUE   
# … with 49 extra rows

Os desvios padrão são mais altos para classificações incorretas?

eval_table %>% 
  group_by(fact, predicted) %>% 
  summarise(avg_mean = imply(maxprob),
            avg_sd = imply(maxprob_sd),
            avg_mc_sd = imply(maxprob_mc_sd)) %>% 
  mutate(appropriate = fact == predicted) %>%
  prepare(avg_mc_sd) 
# A tibble: 2 x 5
  appropriate depend avg_mean avg_sd avg_mc_sd
                
1 FALSE      19    0.775  0.380     0.237
2 TRUE       50    0.879  0.264     0.183

Eles são; embora talvez não na medida em que possamos desejar.

Com apenas seis lessons, também podemos inspecionar desvios padrão no nível de pares de previsão de previsão.

eval_table %>% 
  group_by(fact, predicted) %>% 
  summarise(cnt = n(),
            avg_mean = imply(maxprob),
            avg_sd = imply(maxprob_sd),
            avg_mc_sd = imply(maxprob_mc_sd)) %>% 
  mutate(appropriate = fact == predicted) %>%
  prepare(desc(cnt), avg_mc_sd) 
# A tibble: 14 x 7
# Teams:   fact (6)
   fact        predicted      cnt avg_mean avg_sd avg_mc_sd appropriate
                                 
 1 SIT_TO_STAND SIT_TO_STAND    12    0.935  0.205    0.184  TRUE   
 2 STAND_TO_SIT STAND_TO_SIT     9    0.871  0.284    0.162  TRUE   
 3 LIE_TO_SIT   LIE_TO_SIT       9    0.765  0.377    0.216  TRUE   
 4 SIT_TO_LIE   SIT_TO_LIE       8    0.908  0.254    0.187  TRUE   
 5 STAND_TO_LIE STAND_TO_LIE     7    0.956  0.144    0.132  TRUE   
 6 LIE_TO_STAND LIE_TO_STAND     5    0.809  0.353    0.227  TRUE   
 7 SIT_TO_LIE   STAND_TO_LIE     4    0.685  0.436    0.233  FALSE  
 8 LIE_TO_STAND SIT_TO_STAND     4    0.909  0.271    0.282  FALSE  
 9 STAND_TO_LIE SIT_TO_LIE       3    0.852  0.337    0.238  FALSE  
10 STAND_TO_SIT SIT_TO_STAND     3    0.837  0.368    0.269  FALSE  
11 LIE_TO_STAND LIE_TO_SIT       2    0.689  0.454    0.233  FALSE  
12 LIE_TO_SIT   STAND_TO_SIT     1    0.548  0.498    0.0805 FALSE  
13 SIT_TO_STAND LIE_TO_STAND     1    0.530  0.499    0.134  FALSE  
14 LIE_TO_SIT   LIE_TO_STAND     1    0.824  0.381    0.231  FALSE  

Novamente, vemos desvios padrão mais altos para previsões incorretas, mas não em alto grau.

Conclusão

Mostramos como construir, treinar e obter previsões de um convite totalmente variacional. Evidentemente, existem espaço para experimentação: existem implementações de camada alternativa; Um prior diferente poderia ser especificado; A divergência pode ser calculada de maneira diferente; e as opções usuais das opções de ajuste de hiperparâmetro da rede neural.

Então, há a questão das consequências (ou: tomada de decisão). O que vai acontecer em casos de alta incerteza, qual é o caso de alta incerteza? Naturalmente, perguntas como essas estão fora do escopo para este put up, mas de importância essencial em aplicativos do mundo actual. Obrigado pela leitura!

Apêndice

A ser executado antes de executar o código desta postagem. Copiado de Classificando a atividade física dos dados do smartphone.

library(keras)     
library(tidyverse) 

activity_labels <- learn.desk("information/activity_labels.txt", 
                             col.names = c("quantity", "label")) 

one_hot_to_label <- activity_labels %>% 
  mutate(quantity = quantity - 7) %>% 
  filter(quantity >= 0) %>% 
  mutate(class = paste0("V",quantity + 1)) %>% 
  choose(-quantity)

labels <- learn.desk(
  "information/RawData/labels.txt",
  col.names = c("experiment", "userId", "exercise", "startPos", "endPos")
)

dataFiles <- listing.recordsdata("information/RawData")
dataFiles %>% head()

fileInfo <- data_frame(
  filePath = dataFiles
) %>%
  filter(filePath != "labels.txt") %>%
  separate(filePath, sep = '_',
           into = c("kind", "experiment", "userId"),
           take away = FALSE) %>%
  mutate(
    experiment = str_remove(experiment, "exp"),
    userId = str_remove_all(userId, "consumer|.txt")
  ) %>%
  unfold(kind, filePath)

# Learn contents of single file to a dataframe with accelerometer and gyro information.
readInData <- perform(experiment, userId){
  genFilePath = perform(kind) {
    paste0("information/RawData/", kind, "_exp",experiment, "_user", userId, ".txt")
  }
  bind_cols(
    learn.desk(genFilePath("acc"), col.names = c("a_x", "a_y", "a_z")),
    learn.desk(genFilePath("gyro"), col.names = c("g_x", "g_y", "g_z"))
  )
}

# Operate to learn a given file and get the observations contained alongside
# with their lessons.
loadFileData <- perform(curExperiment, curUserId) {

  # load sensor information from file into dataframe
  allData <- readInData(curExperiment, curUserId)
  extractObservation <- perform(startPos, endPos){
    allData(startPos:endPos,)
  }

  # get statement places on this file from labels dataframe
  dataLabels <- labels %>%
    filter(userId == as.integer(curUserId),
           experiment == as.integer(curExperiment))

  # extract observations as dataframes and save as a column in dataframe.
  dataLabels %>%
    mutate(
      information = map2(startPos, endPos, extractObservation)
    ) %>%
    choose(-startPos, -endPos)
}

# scan via all experiment and userId combos and collect information right into a dataframe.
allObservations <- map2_df(fileInfo$experiment, fileInfo$userId, loadFileData) %>%
  right_join(activityLabels, by = c("exercise" = "quantity")) %>%
  rename(activityName = label)

write_rds(allObservations, "allObservations.rds")

allObservations <- readRDS("allObservations.rds")

desiredActivities <- c(
  "STAND_TO_SIT", "SIT_TO_STAND", "SIT_TO_LIE", 
  "LIE_TO_SIT", "STAND_TO_LIE", "LIE_TO_STAND"  
)

filteredObservations <- allObservations %>% 
  filter(activityName %in% desiredActivities) %>% 
  mutate(observationId = 1:n())

# get all customers
userIds <- allObservations$userId %>% distinctive()

# randomly select 24 (80% of 30 people) for coaching
set.seed(42) # seed for reproducibility
trainIds <- pattern(userIds, measurement = 24)

# set the remainder of the customers to the testing set
testIds <- setdiff(userIds,trainIds)

# filter information. 
# be aware S.Ok.: renamed to train_data for consistency with 
# variable naming used on this put up
train_data <- filteredObservations %>% 
  filter(userId %in% trainIds)

# be aware S.Ok.: renamed to test_data for consistency with 
# variable naming used on this put up
test_data <- filteredObservations %>% 
  filter(userId %in% testIds)

# be aware S.Ok.: renamed to pad_size for consistency with 
# variable naming used on this put up
pad_size <- trainData$information %>% 
  map_int(nrow) %>% 
  quantile(p = 0.98) %>% 
  ceiling()

# be aware S.Ok.: renamed to one_hot_classes for consistency with 
# variable naming used on this put up
one_hot_classes <- . %>% 
  {. - 7} %>%        # deliver integers right down to 0-6 from 7-12
  to_categorical()   # One-hot encode

Reyes-Ortiz, Jorge-L., Luca Oneto, Albert Samà, Xavier Parra e Davide Anguita. 2016. “Reconhecimento de atividades humanas consciente da transição usando smartphones”. Neurocomput. 171 (c): 754–67. https://doi.org/10.1016/j.neucom.2015.07.085.

Deixe um comentário

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