Classificando a atividade física dos dados do smartphone


Introdução

Neste publish, descreveremos como usar dados de acelerômetro e giroscópio para smartphones para prever as atividades físicas dos indivíduos que carregam os telefones. Os dados usados neste publish são provenientes do Reconhecimento baseado em smartphone de atividades humanas e conjunto de dados de transições posturais Distribuído pela Universidade da Califórnia, Irvine. Trinta indivíduos foram encarregados de realizar várias atividades básicas com um movimento de gravação de smartphone anexado usando um acelerômetro e giroscópio.

Antes de começarmos, vamos carregar as várias bibliotecas que usaremos na análise:


library(keras)     # Neural Networks
library(tidyverse) # Information cleansing / Visualization
library(knitr)     # Desk printing
library(rmarkdown) # Misc. output utilities 
library(ggridges)  # Visualization

Conjunto de dados de atividades

Os dados usados neste publish vêm do Reconhecimento baseado em smartphone de atividades humanas e conjunto de dados de transições posturais(Reyes-Ortiz et al. 2016) Distribuído pela Universidade da Califórnia, Irvine.

Quando baixado do hyperlink acima, os dados contêm duas ‘partes’ diferentes. Um que foi pré-processado usando várias técnicas de extração de recursos, como transformação de Quick Fourier e outra RawData Seção que simplesmente fornece as direções brutas x, y, z de um acelerômetro e giroscópio. Nenhuma da filtragem de ruído padrão ou extração de recursos usadas nos dados do acelerômetro foi aplicada. Este é o conjunto de dados que usaremos.

A motivação para trabalhar com os dados brutos deste publish é ajudar a transição do código/conceitos para os dados de séries temporais em domínios menos bem caracterizados. Embora um modelo mais preciso possa ser feito utilizando os dados filtrados/limpos fornecidos, a filtragem e a transformação podem variar bastante de tarefa para tarefa; exigindo muito esforço guide e conhecimento de domínio. Uma das coisas bonitas sobre aprendizado profundo é que a extração de recursos é aprendida com os dados, não o conhecimento externo.

Etiquetas de atividade

Os dados têm codificações inteiras para as atividades que, embora não sejam importantes para o próprio modelo, são úteis para serem usadas. Vamos carregá -los primeiro.


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

activityLabels %>% kable(align = c("c", "l"))
1ANDANDO
2Walking_Upstairs
3Walking_downstairs
4SENTADO
5DE PÉ
6Deitando
7Stand_to_sit
8Sit_to_stand
9Sit_to_lie
10Lie_to_sit
11Stand_to_lie
12Lie_to_stand

Em seguida, carregamos na chave dos rótulos para o RawData. Este arquivo é uma lista de todas as observações ou gravações de atividades individuais, contidas no conjunto de dados. A chave para as colunas é retirada dos dados README.txt.


Column 1: experiment quantity ID, 
Column 2: person quantity ID, 
Column 3: exercise quantity ID 
Column 4: Label begin level 
Column 5: Label finish level 

Os pontos de partida e last estão em número de amostras de log de sinal (registradas em 50Hz).

Vamos dar uma olhada nas primeiras 50 linhas:


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

labels %>% 
  head(50) %>% 
  paged_table()

Nomes de arquivos

Em seguida, vejamos os arquivos reais dos dados do usuário fornecidos para nós em RawData/


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

(1) "acc_exp01_user01.txt" "acc_exp02_user01.txt"
(3) "acc_exp03_user02.txt" "acc_exp04_user02.txt"
(5) "acc_exp05_user03.txt" "acc_exp06_user03.txt"

Há um esquema de nomeação de arquivos de três partes. A primeira parte é o tipo de dados que o arquivo contém: acc para acelerômetro ou gyro para giroscópio. Em seguida, o número do experimento e o último é o ID do usuário para a gravação. Vamos carregá -los em um quadro de dados para facilitar o uso posterior.


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, "person|.txt")
  ) %>% 
  unfold(kind, filePath)

fileInfo %>% head() %>% kable()
0101ACC_EXP01_USER01.TXTgyro_exp01_user01.txt
0201ACC_EXP02_USER01.TXTgyro_exp02_user01.txt
0302ACC_EXP03_USER02.TXTgyro_exp03_user02.txt
0402ACC_EXP04_USER02.TXTgyro_exp04_user02.txt
0503ACC_EXP05_USER03.TXTgyro_exp05_user03.txt
0603ACC_EXP06_USER03.TXTgyro_exp06_user03.txt

Lendo e coleta de dados

Antes que possamos fazer qualquer coisa com os dados, desde que precisamos colocá-los em um formato amigável ao modelo. Isso significa que queremos ter uma lista de observações, sua classe (ou rótulo de atividade) e os dados correspondentes à gravação.

Para obter isso, examinaremos cada um dos arquivos de gravação presentes em dataFilesconsulte quais observações estão contidas na gravação, extraia essas gravações e retorne tudo para um modelo fácil de modelar com o DataFrame.


# Learn contents of single file to a dataframe with accelerometer and gyro information.
readInData <- operate(experiment, userId){
  genFilePath = operate(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"))
  )
}

# Perform to learn a given file and get the observations contained alongside
# with their lessons.

loadFileData <- operate(curExperiment, curUserId) {
  
  # load sensor information from file into dataframe
  allData <- readInData(curExperiment, curUserId)

  extractObservation <- operate(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 by way of 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)

# cache work. 
write_rds(allObservations, "allObservations.rds")
allObservations %>% dim()

Explorando os dados

Agora que temos todos os dados carregados junto com o experimentAssim, userIde exercise Rótulos, podemos explorar o conjunto de dados.

Duração das gravações

Vamos primeiro olhar para a duração das gravações por atividade.


allObservations %>% 
  mutate(recording_length = map_int(information,nrow)) %>% 
  ggplot(aes(x = recording_length, y = activityName)) +
  geom_density_ridges(alpha = 0.8)

Classificando a atividade física dos dados do smartphone

O fato de que existe uma diferença no comprimento da gravação entre os diferentes tipos de atividades exige que tenhamos um pouco de cuidado com a maneira como procedemos. Se treinarmos o modelo em todas as aulas ao mesmo tempo, teremos que encerrar todas as observações na duração do mais longo, o que deixaria uma grande maioria das observações, com uma enorme proporção de seus dados sendo apenas preenchidos. Por causa disso, ajustaremos nosso modelo ao maior ‘grupo’ de atividades de comprimento de observações, incluem STAND_TO_SITAssim, STAND_TO_LIEAssim, SIT_TO_STANDAssim, SIT_TO_LIEAssim, LIE_TO_STANDe LIE_TO_SIT.

Uma direção futura interessante seria tentar usar outra arquitetura como um RNN que pode lidar com entradas de comprimento variável e treiná -lo em todos os dados. No entanto, você correria o risco de o modelo aprender simplesmente que, se a observação for longa, provavelmente é uma das quatro lessons mais longas que não generalizariam para um cenário em que você estava executando esse modelo em uma transmissão em tempo actual de dados.

Atividades de filtragem

Com base em nosso trabalho acima, vamos subconjuntar os dados para ser apenas das atividades de interesse.


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())

filteredObservations %>% paged_table()

Portanto, após nossa poda agressiva dos dados, teremos uma quantidade respeitável de dados nos quais nosso modelo pode aprender.

Treinamento/teste dividido

Antes de prosseguirmos para explorar os dados para o nosso modelo, na tentativa de ser o mais justo possível com nossas medidas de desempenho, precisamos dividir os dados em um trem e um conjunto de testes. Como cada usuário realizou todas as atividades apenas uma vez (com exceção de quem fez apenas 10 das 12 atividades) dividindo -se userId Garantiremos que nosso modelo veja novas pessoas exclusivamente quando o testarmos.


# 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. 
trainData <- filteredObservations %>% 
  filter(userId %in% trainIds)

testData <- filteredObservations %>% 
  filter(userId %in% testIds)

Visualização de atividades

Agora que reduzimos nossos dados removendo as atividades e dividindo um conjunto de testes, podemos realmente visualizar os dados de cada classe para ver se há alguma forma imediatamente discernível que nosso modelo pode ser capaz de entender.

Primeiro, vamos descompactar nossos dados do quadro de dados de uma fila por observação para uma versão arrumada de todas as observações.


unpackedObs <- 1:nrow(trainData) %>% 
  map_df(operate(rowNum){
    dataRow <- trainData(rowNum, )
    dataRow$information((1)) %>% 
      mutate(
        activityName = dataRow$activityName, 
        observationId = dataRow$observationId,
        time = 1:n() )
  }) %>% 
  collect(studying, worth, -time, -activityName, -observationId) %>% 
  separate(studying, into = c("kind", "route"), sep = "_") %>% 
  mutate(kind = ifelse(kind == "a", "acceleration", "gyro"))

Agora temos um conjunto de nossas observações, vamos visualizá -las!


unpackedObs %>% 
  ggplot(aes(x = time, y = worth, coloration = route)) +
  geom_line(alpha = 0.2) +
  geom_smooth(se = FALSE, alpha = 0.7, measurement = 0.5) +
  facet_grid(kind ~ activityName, scales = "free_y") +
  theme_minimal() +
  theme( axis.textual content.x = element_blank() )

Portanto, pelo menos nos padrões de dados do acelerômetro emergem definitivamente. Alguém poderia imaginar que o modelo pode ter problemas com as diferenças entre LIE_TO_SIT e LIE_TO_STAND como eles têm um perfil semelhante, em média. O mesmo vale para SIT_TO_STAND e STAND_TO_SIT.

Pré -processamento

Antes que possamos treinar a rede neural, precisamos tomar algumas etapas para pré -processar os dados.

Observações de preenchimento

Primeiro, decidiremos qual comprimento para preencher (e truncar) nossas seqüências, descobrindo qual é o comprimento do 98º percentil. Ao não usar o comprimento de observação mais longo, isso nos ajudará a evitar gravações externas extra-longas bagunçando o preenchimento.


padSize <- trainData$information %>% 
  map_int(nrow) %>% 
  quantile(p = 0.98) %>% 
  ceiling()
padSize

98% 
334 

Agora, simplesmente precisamos converter nossa lista de observações em matrizes e depois usar o Tremendous Helpful pad_sequences() Funciona em Keras para encaixar todas as observações e transformá -las em um tensor 3D para nós.


convertToTensor <- . %>% 
  map(as.matrix) %>% 
  pad_sequences(maxlen = padSize)

trainObs <- trainData$information %>% convertToTensor()
testObs <- testData$information %>% convertToTensor()
  
dim(trainObs)

(1) 286 334   6

Maravilhoso, agora temos nossos dados em um bom formato de rede neural de rede de um tensor 3D com dimensões (, , ).

Codificação única

Há uma última coisa que precisamos fazer antes que possamos treinar nosso modelo, e isso é transformar nossas aulas de observação de números inteiros em vetores em um ou fictício codificado. Felizmente, novamente Keras nos forneceu uma função muito útil para fazer exatamente isso.


oneHotClasses <- . %>% 
  {. - 7} %>%        # deliver integers all the way down to 0-6 from 7-12
  to_categorical() # One-hot encode

trainY <- trainData$exercise %>% oneHotClasses()
testY <- testData$exercise %>% oneHotClasses()

Modelagem

Arquitetura

Como temos dados de séries temporais densas temporalmente, usaremos as camadas convolucionais 1D. Com dados temporalmente densos, um RNN precisa aprender dependências muito longas para entender os padrões, os CNNs podem simplesmente empilhar algumas camadas convolucionais para criar representações de padrão de comprimento substancial. Como também estamos simplesmente procurando uma única classificação de atividade para cada observação, podemos apenas usar o pool para ‘resumir’ a visão CNNS dos dados em uma camada densa.

Além de empilhar dois layer_conv_1d() camadas, usaremos norma e abandono em lote (a variante espacial(Tompson et al. 2014) nas camadas convolucionais e padrão na densa) para regularizar a rede.


input_shape <- dim(trainObs)(-1)
num_classes <- dim(trainY)(2)

filters <- 24     # variety of convolutional filters to be taught
kernel_size <- 8  # what number of time-steps every conv layer sees.
dense_size <- 48  # measurement of our penultimate dense layer. 

# Initialize mannequin
mannequin <- keras_model_sequential()
mannequin %>% 
  layer_conv_1d(
    filters = filters,
    kernel_size = kernel_size, 
    input_shape = input_shape,
    padding = "legitimate", 
    activation = "relu"
  ) %>%
  layer_batch_normalization() %>%
  layer_spatial_dropout_1d(0.15) %>% 
  layer_conv_1d(
    filters = filters/2,
    kernel_size = kernel_size,
    activation = "relu",
  ) %>%
  # Apply common pooling:
  layer_global_average_pooling_1d() %>% 
  layer_batch_normalization() %>%
  layer_dropout(0.2) %>% 
  layer_dense(
    dense_size,
    activation = "relu"
  ) %>% 
  layer_batch_normalization() %>%
  layer_dropout(0.25) %>% 
  layer_dense(
    num_classes, 
    activation = "softmax",
    title = "dense_output"
  ) 

abstract(mannequin)

______________________________________________________________________
Layer (kind)                   Output Form                Param #    
======================================================================
conv1d_1 (Conv1D)              (None, 327, 24)             1176       
______________________________________________________________________
batch_normalization_1 (BatchNo (None, 327, 24)             96         
______________________________________________________________________
spatial_dropout1d_1 (SpatialDr (None, 327, 24)             0          
______________________________________________________________________
conv1d_2 (Conv1D)              (None, 320, 12)             2316       
______________________________________________________________________
global_average_pooling1d_1 (Gl (None, 12)                  0          
______________________________________________________________________
batch_normalization_2 (BatchNo (None, 12)                  48         
______________________________________________________________________
dropout_1 (Dropout)            (None, 12)                  0          
______________________________________________________________________
dense_1 (Dense)                (None, 48)                  624        
______________________________________________________________________
batch_normalization_3 (BatchNo (None, 48)                  192        
______________________________________________________________________
dropout_2 (Dropout)            (None, 48)                  0          
______________________________________________________________________
dense_output (Dense)           (None, 6)                   294        
======================================================================
Complete params: 4,746
Trainable params: 4,578
Non-trainable params: 168
______________________________________________________________________

Treinamento

Agora podemos treinar o modelo usando nossos dados de teste e treinamento. Observe que usamos callback_model_checkpoint() Para garantir que economizemos apenas a melhor variação do modelo (desejável, pois em algum momento do treinamento, o modelo pode começar a exceder ou parar de melhorar).


# Compile mannequin
mannequin %>% compile(
  loss = "categorical_crossentropy",
  optimizer = "rmsprop",
  metrics = "accuracy"
)

trainHistory <- mannequin %>%
  match(
    x = trainObs, y = trainY,
    epochs = 350,
    validation_data = listing(testObs, testY),
    callbacks = listing(
      callback_model_checkpoint("best_model.h5", 
                                save_best_only = TRUE)
    )
  )

O modelo está aprendendo algo! Temos uma precisão respeitável de 94,4% nos dados de validação, não é ruim com seis lessons possíveis para escolher. Vamos analisar o desempenho da validação um pouco mais profundo para ver onde o modelo está bagunçando.

Avaliação

Agora que temos um modelo treinado, vamos investigar os erros que ele cometeu em nossos dados de teste. Podemos carregar o melhor modelo a partir do treinamento com base na precisão da validação e, em seguida, analisar cada observação, o que o modelo previu, a alta probabilidade que atribuiu e o rótulo de atividade verdadeira.


# dataframe to get labels onto one-hot encoded prediction columns
oneHotToLabel <- activityLabels %>% 
  mutate(quantity = quantity - 7) %>% 
  filter(quantity >= 0) %>% 
  mutate(class = paste0("V",quantity + 1)) %>% 
  choose(-number)

# Load our greatest mannequin checkpoint
bestModel <- load_model_hdf5("best_model.h5")

tidyPredictionProbs <- bestModel %>% 
  predict(testObs) %>% 
  as_data_frame() %>% 
  mutate(obs = 1:n()) %>% 
  collect(class, prob, -obs) %>% 
  right_join(oneHotToLabel, by = "class")

predictionPerformance <- tidyPredictionProbs %>% 
  group_by(obs) %>% 
  summarise(
    highestProb = max(prob),
    predicted = label(prob == highestProb)
  ) %>% 
  mutate(
    fact = testData$activityName,
    right = fact == predicted
  ) 

predictionPerformance %>% paged_table()

Primeiro, vejamos o quão ‘confiante’ o modelo foi se a previsão estava correta ou não.


predictionPerformance %>% 
  mutate(outcome = ifelse(right, 'Right', 'Incorrect')) %>% 
  ggplot(aes(highestProb)) +
  geom_histogram(binwidth = 0.01) +
  geom_rug(alpha = 0.5) +
  facet_grid(outcome~.) +
  ggtitle("Chances related to prediction by correctness")

Tranquilizadoramente, parece que o modelo estava, em média, menos confiante sobre suas classificações para os resultados incorretos do que os corretos. (Embora o tamanho da amostra seja pequeno demais para dizer qualquer coisa definitivamente.)

Vamos ver quais atividades o modelo teve mais dificuldade em usar uma matriz de confusão.


predictionPerformance %>% 
  group_by(fact, predicted) %>% 
  summarise(depend = n()) %>% 
  mutate(good = fact == predicted) %>% 
  ggplot(aes(x = fact,  y = predicted)) +
  geom_point(aes(measurement = depend, coloration = good)) +
  geom_text(aes(label = depend), 
            hjust = 0, vjust = 0, 
            nudge_x = 0.1, nudge_y = 0.1) + 
  guides(coloration = FALSE, measurement = FALSE) +
  theme_minimal()

Vemos que, como sugeriu a visualização preliminar, o modelo teve um pouco de problemas com a distinção entre LIE_TO_SIT e LIE_TO_STAND lessons, juntamente com o SIT_TO_LIE e STAND_TO_LIEque também têm perfis visuais semelhantes.

Direções futuras

A direção futura mais óbvia para seguir essa análise seria tentar tornar o modelo mais geral trabalhando com mais tipos de atividades fornecidas. Outra direção interessante seria não separar as gravações em ‘observações’ distintas, mas mantê -las como um conjunto de dados de streaming, assim como uma implantação do mundo actual de um modelo funcionaria, e ver o quão bem um modelo poderia classificar dados de streaming e detectar mudanças na atividade.

Gal, Yarin e Zoubin Ghahramani. 2016. “DROPOUT como uma aproximação bayesiana: representando a incerteza do modelo na aprendizagem profunda.” Em Conferência Internacional sobre aprendizado de máquina1050-9.

Graves, Alex. 2012. “Rotulagem de sequência supervisionada”. Em Rotulagem de sequência supervisionada com redes neurais recorrentes5-13. Springer.

Kononenko, Igor. 1989. “Redes neurais bayesianas”. Cibernética biológica 61 (5). Springer: 361–70.

Lecun, Yann, Yoshua Bengio e Geoffrey Hinton. 2015. “Aprendizagem profunda”. Natureza 521 (7553). Grupo de publicação da natureza: 436.

Reyes-Ortiz, Jorge-L, Luca Oneto, Albert Samà, Xavier Parra e Davide Anguita. 2016. “Reconhecimento de atividades humanas consciente da transição usando smartphones.” Neurocomputing 171. Elsevier: 754-67.

Tompson, Jonathan, Ross Goroshin, Arjun Jain, Yann Lecun e Christoph Bregler. 2014. “Localização eficiente de objetos usando redes convolucionais”. Corr ABS/1411.4280. http://arxiv.org/abs/1411.4280.

Deixe um comentário

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