Данные
Данные в наборе получены от тридцати добровольцев с закрепленным на поясе Samsung Galaxy S II. Сигналы с гироскопа и акселерометра были специальным образом обработаны и преобразованы в 561 признак. Для этого использовалась многоступенчатая фильтрация, преобразование Фурье и некоторые стандартные статистические преобразования, всего 17 функций: от математического ожидания до расчета угла между векторами. Подробности обработки я упускаю, их можно найти в репозитории. Обучающая выборка содержит 7352 кейса, тестовая – 2947. Обе выборки размечены метками, соответствующими шести активностям: walking, walking_up, walking_down, sitting, standing и laying.
В качестве базового алгоритма для экспериментов я выбрал Random Forest. Выбор был основан на том, что у RF есть встроенный механизм оценки важности переменных, который я хотел испытать, так как перспектива самостоятельно выбирать признаки из 561 переменной меня несколько пугала. Также я решил попробовать машину опорных векторов (SVM). Знаю, что это классика, но ранее мне её использовать не приходилось, было интересно сравнить качество моделей для обоих алгоритмов.
Скачав и распаковав архив с данными, я понял что с ними придется повозиться. Все части набора, имена признаков, метки активностей были в разных файлах. Для загрузки файлов в среду R использовал функцию read.table(). Пришлось запретить автоматическое преобразование строк в факторные переменные, так как оказалось, что есть дубли имен переменных. Кроме того, имена содержали некорректные символы. Эту проблему решил следующей конструкцией с функцией sapply(), которая в R часто заменяет стандартный цикл for:
editNames <- function(x) {
y <- var_names[x,2]
y <- sub("BodyBody", "Body", y)
y <- gsub("-", "", y)
y <- gsub(",", "_", y)
y <- paste0("v",var_names[x,1], "_",y)
return(y)
}
new_names <- sapply(1:nrow(var_names), editNames)
Склеил все части наборов при помощи функций rbind() и cbind(). Первая соединяет строки одинаковой структуры, вторая колонки одинаковой длины.
После того как я подготовил обучающую и тестовую выборки, встал вопрос о необходимости предобработки данных. Используя функцию range() в цикле, рассчитал диапазон значений. Оказалось, что все признаки находятся в рамках [-1,1] и значит не нужно ни нормализации, ни масштабирования. Затем проверил наличие признаков с сильно смещенным распределением. Для этого воспользовался функцией skewness() из пакета e1071:
SkewValues <- apply(train_data[,-1], 2, skewness)
head(SkewValues[order(abs(SkewValues),decreasing = TRUE)],3)
Apply() — функция из той же категории, что и sapply(), используется, когда что-то нужно сделать по столбцам или строкам. train_data[,-1] — набор данных без зависимой переменной Activity, 2 показывает что нужно рассчитывать значение по столбцам. Этот код выводит три худшие переменные:
v389_fBodyAccJerkbandsEnergy57_64 v479_fBodyGyrobandsEnergy33_40 v60_tGravityAcciqrX 14.70005 12.33718 12.18477
Чем ближе эти значения к нулю, тем меньше перекос распределения, а здесь он, прямо скажем, немаленький. На этот случай в caret есть реализация BoxCox-трансформации. Читал, что Random Forest не чувствителен к подобным вещам, поэтому решил признаки оставить как есть и заодно посмотреть, как с этим справится SVM.
Осталось выбрать критерий качества модели: точность или верность Accuracy или критерий согласия Kappa. Если кейсы по классам распределены неравномерно, нужно использовать Kappa, это та же Accuracy только с учетом вероятности случайным образом «вытащить» тот или иной класс. Сделав summary() для столбца Activity проверил распределение:
WALKING WALKING_UP WALKING_DOWN SITTING STANDING LAYING 1226 1073 986 1286 1374 1407
Кейсы распределены практически поровну, кроме может быть walking_down (кто-то из добровольцев видимо не очень любил спускаться по лестнице), а значит можно использовать Accuracy.
Обучение
Обучил модель RF на полном наборе признаков. Для этого была использована следующая конструкция на R:
fitControl <- trainControl(method="cv", number=5)
set.seed(123)
forest_full <- train(Activity~., data=train_data,
method="rf", do.trace=10, ntree=100,
trControl = fitControl)
Здесь предусмотрена k-fold кросс-валидация с k=5. По умолчанию обучается три модели с разным значением mtry (кол-во признаков, которые случайным образом выбираются из всего набора и рассматриваются в качестве кандидата при каждом разветвлении дерева), а затем по точности выбирается лучшая. Количество деревьев у всех моделей одинаково ntree=100.
Для определения качества модели на тестовой выборке я взял функцию confusionMatrix(x, y) из caret, где x – вектор предсказанных значений, а y – вектор значений из тестовой выборки. Вот часть её выдачи:
Reference Prediction WALKING WALKING_UP WALKING_DOWN SITTING STANDING LAYING WALKING 482 38 17 0 0 0 WALKING_UP 7 426 37 0 0 0 WALKING_DOWN 7 7 366 0 0 0 SITTING 0 0 0 433 51 0 STANDING 0 0 0 58 481 0 LAYING 0 0 0 0 0 537 Overall Statistics Accuracy : 0.9247 95% CI : (0.9145, 0.9339)
Обучение на полном наборе признаков заняло около 18 минут на ноутбуке с Intel Core i5. Можно было сделать в несколько раз быстрее на OS X, задействовав при помощи пакета doMC несколько ядер процессора, но для Windows такой штуки нет, на сколько мне известно.
Caret поддерживает несколько реализаций SVM. Я выбрал svmRadial (SVM с ядром – радиальной базисной функцией), она чаще используется c caret, и является общим инструментом, когда нет каких-то специальных сведений о данных. Чтобы обучить модель с SVM, достаточно поменять значение параметра method в функции train() на svmRadial и убрать параметры do.trace и ntree. Алгоритм показал следующие результаты: Accuracy на тестовой выборке – 0.952. При этом обучение модели с пятикратной кросс-валидацией заняло чуть более 7 минут. Оставил себе заметку на память: не хвататься сразу за Random Forest.
Важность переменных
Получить результаты встроенной оценки важности переменных в RF можно при помощи функции varImp() из пакета caret. Конструкция вида plot(varImp(model),20) отобразит относительную важность первых 20 признаков:
«Acc» в названии обозначает, что эта переменная получена при обработке сигнала с акселерометра, «Gyro», соответственно, с гироскопа. Если внимательно посмотреть на график, можно убедиться, что среди наиболее важных переменных нет данных с гироскопа, что лично для меня удивительно и необъяснимо. («Body» и «Gravity» это два компонента сигнала с акселерометра, t и f временной и частотный домены сигнала).
Подставлять в RF признаки, отобранные по важности, бессмысленное занятие, он их уже отобрал и использовал. А вот с SVM может получиться. Начал с 10% наиболее важных переменных и стал увеличивать на 10% каждый раз контролируя точность, найдя максимум, сократил шаг сначала до 5%, потом до 2.5% и, наконец, до одной переменной. Итог – максимум точности оказался в районе 490 признаков и составил 0.9545 что лучше значения на полном наборе признаков всего на четверть процента (дополнительная пара правильно классифицированных кейсов). Эту работу можно было бы автоматизировать, так в составе caret есть реализация RFE (Recursive feature elimination), он рекурсивно убирает и добавляет по одной переменной и контролирует точность модели. Проблем с ним две, RFE работает очень медленно (внутри у него Random Forest), для сходного по количеству признаков и кейсов набора данных, процесс бы занял около суток. Вторая проблема — точность на обучающей выборке, которую оценивает RFE, это совсем не тоже самое, что точность на тестовой.
Код, который извлекает из varImp и упорядочивает по убыванию важности имена заданного количества признаков, выглядит так:
imp <- varImp(model)[[1]]
vars <- rownames(imp)[order(imp$Overall, decreasing=TRUE)][1:56]
Фильтрация признаков
Для очистки совести решил попробовать еще какой-нибудь метод отбора признаков. Выбрал фильтрацию признаков на основе расчета information gain ratio (в переводе встречается как информационный выигрыш или прирост информации), синоним дивергенции Кульбака-Лейблера. IGR это мера различий между распределениями вероятностей двух случайных величин.
Для расчета IGR я использовал функцию information.gain() из пакета FSelector. Для работы пакета нужна JRE. Там, кстати, есть и другие инструменты, позволяющие отбирать признаки на основе энтропии и корреляции. Значения IGR обратны «расстоянию» между распределениями и нормированы [0,1], т.е. чем ближе к единице, тем лучше. После вычисления IGR я упорядочил список переменных в порядке убывания IGR, первые 20 выглядели следующим образом:
IGR дает совсем другой набор «важных» признаков, с важными совпали только пять. Гироскопа в топе снова нет, зато очень много признаков с X компонентой. Максимальное значение IGR – 0.897, минимальное – 0. Получив упорядоченный перечень признаков поступил с ним также как и с важностью. Проверял на SVM и RF, значимо увеличить точность не получилось.
Думаю, что отбор признаков в похожих задачах работает далеко не всегда, и причин здесь как минимум две. Первая причина связана с конструированием признаков. Исследователи, которые подготовили набор данных, старались «выжить» всю информацию из сигналов с датчиков и делали это наверняка осознанно. Какие-то признаки дают больше информации, какие-то меньше (только одна переменная оказалась с IGR, равным нулю). Это хорошо видно, если построить графики значений признаков с разным уровнем IGR. Для определенности я выбрал 10-й и 551-й. Видно, что для признака с высоким IGR точки хорошо визуально разделимы, с низким IGR – напоминают цветовое месиво, но очевидно, несут какую-то порцию полезной информации.
Вторая причина связана с тем, что зависимая переменная это фактор с количеством уровней больше двух (здесь – шесть). Достигая максимума точности в одном классе, мы ухудшаем показатели в другом. Это можно показать на матрицах несоответствия для двух разных наборов признаков с одинаковой точностью:
Accuracy: 0.9243, 561 variables Reference Prediction WALKING WALKING_UP WALKING_DOWN SITTING STANDING LAYING WALKING 483 36 20 0 0 0 WALKING_UP 1 428 44 0 0 0 WALKING_DOWN 12 7 356 0 0 0 SITTING 0 0 0 433 45 0 STANDING 0 0 0 58 487 0 LAYING 0 0 0 0 0 537 Accuracy: 0.9243, 526 variables Reference Prediction WALKING WALKING_UP WALKING_DOWN SITTING STANDING LAYING WALKING 482 40 16 0 0 0 WALKING_UP 8 425 41 0 0 0 WALKING_DOWN 6 6 363 0 0 0 SITTING 0 0 0 429 44 0 STANDING 0 0 0 62 488 0 LAYING 0 0 0 0 0 537
В верхнем варианте меньше ошибок у первых двух классов, в нижнем – у третьего и пятого.
Подвожу итоги:
1. SVM «сделала» Random Forest в моей задаче, работает в два раза быстрее и дает более качественную модель.
2. Было бы правильно понимать физический смысл переменных. И, кажется, на гироскопе в некоторых устройствах можно сэкономить.
3. Важность переменных из RF можно использовать для отбора переменных с другими методами обучения.
4. Отбор признаков на основе фильтрации не всегда дает повышение качества модели, но при этом позволяет сократить количество признаков, сократив время обучения при незначительной потери в качестве (при использовании 20% важных переменных точность в SVM оказалась всего на 3% ниже максимума).
Код для этой статьи можно найти в моем репозитарии.
Несколько дополнительных ссылок:
- Feature Selection with the Caret R Package
- Random Forests, Leo Breiman and Adele Cutler
UPD:
По совету kenoma сделал анализ главных компонент (PCA). Использовал вариант с функцией preProcess из caret:
pca_mod <- preProcess(train_data[,-1],
method="pca",
thresh = 0.95)
pca_train_data <- predict(pca_mod, newdata=train_data[,-1])
dim(pca_train_data)
# [1] 7352 102
pca_train_data$Activity <- train_data$Activity
pca_test_data <- predict(pca_mod, newdata=test_data[,-1])
pca_test_data$Activity <- test_data$Activity
Отсечка 0.95, получилось 102 компоненты.
Точность RF на тестовой выборке: 0.8734 (на 5% ниже точности на полном наборе)
Точность SVM: 0.9386 (на один с небольшим процента ниже). Я считаю, что этот результат весьма неплох, так что совет оказался полезным.
Комментарии (12)
kenoma
07.05.2015 18:45+1А вы не пробовали снизить размерность данных перед обучением моделей? Например, с помощью метода главных компонент (PCA, princomp в R) можно было бы глазками посмотреть на то, как выглядит многообразие данных, а там уже решать, что эффективнее было бы применить.
khmelkoff Автор
08.05.2015 11:03Нет, не пробовал. Интересовал только отбор переменных. Зато пробовал RFE, о чем в статье писать не стал. Неудачный опыт. 10 часов на трех ядрах, 60 отобранных признаков и точность в районе 0.8 на тестовой выборке, что на 10% хуже полного набора. Да, PCA встроен прямо в caret, вызывается через функцию preProcess.
kenoma
08.05.2015 11:46А эти признаки сильно разнятся от ранжирования признаков RF и SVM? Просто у меня есть подозрение, что ваши целевые кластеры могут быть не совсем «правильной» формы, ведь судя по описанию у вас куча признаков зависит друг от друга нелинейным образом. Так что мне кажется целесообразным было бы скомбинировать скоррелированные признаки и работать уже с ними. Но прежде всего я бы сделал какой нибудь разведочный анализ посредством визуализации данных — первые компоненты pca, t-sne или что там еще есть в caret. Вдруг вы увидите структуру, которая идеально впишется под разбор попсовыми методами кластеризации.
khmelkoff Автор
08.05.2015 12:20Да, есть отличия. Но тенденция общая, на 50-60 «важных» признаках точность быстро растет, а потом выходит на плато. Но эти 50-60 признаков дают в районе 0.8. Я раньше всегда отбирал признаки через information gain, обычно получалось. Это ничем не хуже чем считать корреляцию. Тут igr падает от максимума до нуля практически равномерно по всему набору.
kenoma
08.05.2015 14:10Согласен, энтропийные показатели хороши в своем деле, однако я пытаюсь настойчиво намекнуть на мысль, что представление признаков у вас избыточное и есть такие, которые наверняка друг от дружки зависят. Или для вас принципиальным является вопрос, неизменности исходных признаков?
khmelkoff Автор
11.05.2015 17:22Я считаю, что ребята старались не зря выдумывая переменные. И, скажем, мне не понятно, как связаны между собой мат.ожидание уровня сигнала и угол между векторами. Я попробую на этой неделе сделать PCA, о результатах напишу.
khmelkoff Автор
12.05.2015 17:21Сделал PCA, см. update в статье. 102 компоненты при пороге в 0.95. Форест на 5% ниже, SVM на один с небольшим.
kenoma
12.05.2015 19:02Спасибо большое за апдейт! Хотя 102 компоненты это все равно многовато, но уже примерно в 4-5 раз меньше исходной размерности. На самом-то деле, тут много еще чего можно сделать, но я думаю, для поставленной задачи и без того хороший результат.
akrot
08.05.2015 09:15Занятно=)
На данных из UCI, конечно, обучаться не очень интересно — зачастую там вся работа уже сделана и «фичи» уже есть. Попробуйте придумать задачу из жизни и попытаться самому построить матрицу «объект-признак» — это было бы очень круто
ariel32
Все стесняются, просмотры и добавление в избранное растут, а комментариев нет :)
Спасибо за статью в целом и за популяризацию R в частности! Было любопытно глянуть, равно как и предыдущую вашу публикацию.
khmelkoff Автор
Спасибо на добром слове.