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"))
1 | ANDANDO |
2 | Walking_Upstairs |
3 | Walking_downstairs |
4 | SENTADO |
5 | DE PÉ |
6 | Deitando |
7 | Stand_to_sit |
8 | Sit_to_stand |
9 | Sit_to_lie |
10 | Lie_to_sit |
11 | Stand_to_lie |
12 | Lie_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()
01 | 01 | ACC_EXP01_USER01.TXT | gyro_exp01_user01.txt |
02 | 01 | ACC_EXP02_USER01.TXT | gyro_exp02_user01.txt |
03 | 02 | ACC_EXP03_USER02.TXT | gyro_exp03_user02.txt |
04 | 02 | ACC_EXP04_USER02.TXT | gyro_exp04_user02.txt |
05 | 03 | ACC_EXP05_USER03.TXT | gyro_exp05_user03.txt |
06 | 03 | ACC_EXP06_USER03.TXT | gyro_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 dataFiles
consulte 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 experiment
Assim, userId
e 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)
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_SIT
Assim, STAND_TO_LIE
Assim, SIT_TO_STAND
Assim, SIT_TO_LIE
Assim, LIE_TO_STAND
e 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_LIE
que 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.