image

Введение


В сегодняшние дни есть много разных способов взаимодействия со смартфонами: тач-скрин, аппаратные кнопки, сканер отпечатков пальцев, видео камера (например система распознавания лиц), D-PAD, кнопки на гарнитуре, и так далее. Но что насчет использования жестов движений?

Например быстрое перемещение телефона вправо или влево держа его в руке может очень точно отражать намерение перейти на следующую или предыдущую песню в плей-листе. Или же вы можете быстро перевернуть телефон верх ногами и потом назад для обновления контента приложения. Внедрение такого взаимодействия выглядит многообещающим и буквально добавляет новое измерение в UX. Эта статья описывает как реализовать подобное используя машинное обучение и библиотеку Tensorflow для Android.

Описание


Давайте определимся с конечной целью. Хотелось бы чтоб смартфон распознавал быстрые движения влево и вправо.

Также хотелось бы чтоб реализация была в Android библиотеке и ее можно было легко интегрировать в любое другое приложение.

Жесты могут быть записаны на смартфоне используя несколько датчиков: акселерометр, гироскоп, магнитометр, и другие. Позже набор записанных жестов может быть использован в алгоритмах машинного обучения для распознавания.

Для записи данных будет разработано специальное приложение под Android. Препроцессинг и обучение будет производится на ПК в среде Jupyter Notebook используя язык Python и библиотеку TensorFlow. Распознавание жестов будет реализовано в демонстрационном приложении с использованием результатов обучения. В конце, мы разработаем готовую к использования Android библиотеку для распознавания жестов, которая может быть легко интегрирована в другие приложения.

Наш план реализации:

  • Собрать данные на телефоне
  • Разработать и обучить нейронную сеть
  • Экспортировать нейронную сеть на смартфон
  • Разработать тестовое приложение для Android
  • Разработать Android библиотеку

Реализация


Подготовка данных


Для начала давайте определимся какие датчики и какой тип данных могут описать наши жесты. Похоже для точного описания этих жестов должны быть использованы и акселерометр и гироскоп.

Акселерометр очевидно измеряет ускорение и соответственно, перемещение:

image

Акселерометр имеет интересные нюанс — он измеряет не только ускорение непосредственно телефона, но также и ускорение свободного падения которое приблизительно равно 9.8 м/с2. Это значит что величина вектора ускорения лежащего на на столе телефона будет равна 9.8. Такие значения не могут быть использованы напрямую и должны быть вычтены из значения вектора ускорения свободного падения. Это непростая задача потому что она требует совместной обработки данных магнетометра и акселерометра. К счастью, Android имеет специальный датчик «Линейный Акселерометр» который производит необходимые расчеты и возвращает корректные значения.

Гироскоп, с другой стороны, измеряет вращение:

image

Попробуем определить, какие значения будут коррелировать с нашими жестами. Очевидно, что в акселерометре (имеется ввиду линейный акселерометр) значения X и Y будут в достаточно большой степени описывать жесты. Значение же Z акселерометра вряд ли будет зависеть от наших жестов.

Что касается датчика гироскопа, кажется, что жесты слегка влияют на ось Z. Однако для упрощения реализации я предлагаю не включать его в расчет. В этом случае наш детектор жестов распознает перемещение телефона не только в руке, но и вдоль горизонтальной линии — например, на столе. Но это не слишком большая проблема.

Таким образом, нам необходимо разработать Android-приложение, которое может записывать данные акселерометра.

Я разработал такое приложение. Вот скриншот записанного жеста «вправо»:

image

Как вы видите, оси X и Y очень сильно реагируют на жест. Ось Z также реагирует, но, как мы решили, она не будет включена в обработку.

Вот жест «влево»:

image

Обратите внимание, что значения X почти противоположны значениям из предыдущего жеста.

Еще одна вещь, которую нужно упомянуть, — это частота дискретизации данных. Она отражает то, как часто данные обновляются и напрямую влияет на объем данных за интервал времени.

Еще одна вещь, которую следует учитывать — это продолжительность жестов. Это значение, как и многие другие, следует выбирать эмпирически. Я установил, что продолжительность жестов длится не более 1 секунды, но, чтобы сделать значение более подходящим для расчетов, я округлил его до 1.28 секунды.

Выбранная частота обновления данных составляет 128 точек на 1.28 секунды, что дает задержку в 10 миллисекунд (1.28 / 128). Это значения должно быть передано в метод registerListener.

Идея состоит в том, чтобы обучить нейронную сеть распознаванию таких сигналов в потоке данных с акселерометра.

Итак, дальше нам нужно записать в файлы много семплов жестов. Конечно, один и тот же тип жестов (вправо или влево) должен быть помечен одним и тем-же же тегом. Трудно сказать заранее, сколько образцов необходимо для обучения сети, но это может быть определено в результате обучения.

Тапнув где либо на графике, вы выделите семпл — т.е. участок графика длиной 128 точек:

image

Теперь кнопка «сохранить» будет активной. Нажатие по ней автоматически сохранит семпл в файле в рабочем каталоге в файл с именем вида «{label}_{timestamp}.log» Рабочий каталог может быть выбран в меню приложения.

Также обратите внимание, что после сохранения текущего семпла, следующий будет выбран автоматически. Следующий жест выбирается с использованием очень простого алгоритма: найти первую запись абсолютное значение X которой больше 3, затем перемотать назад 20 точек.

Такая автоматизация позволяет нам быстро сохранить много семплов. Я записал по 500 семплов на жест. Сохраненные данные должны быть скопированы на ПК для дальнейшей обработки. (Обработка и обучение прямо на телефоне выглядит интересно, но TensorFlow для Android в настоящее время не поддерживает обучение).

На снимке, представленном ранее, диапазон данных составляет примерно ±6. Однако, если вы сильнее махнете телефоном, он может достичь ±10. Данные лучше нормализовать так, чтоб диапазон был ±1, что намного лучше соответствует формату данных нейронной сети. Для этого я просто разделил все данные на константу — в моем случае 9.

Следующим шагом, который нужно выполнить перед началом обучения, это фильтрация данных для устранения высокочастотных колебаний. Такие колебания не имеют отношения к нашим жестам.

Существует много способов фильтрации данных. Один из них — фильтр Скользящее среднее. Вот пример того, как он работает:

image

Обратите внимание, что максимальные значения X данных теперь составляют половину оригинала. Поскольку мы будем выполнять такую же фильтрацию данных в режиме реального времени во время распознавания, это не должно быть проблемой.

Последним шагом для улучшения обучения является аугментация данных. Этот процесс расширяет исходный набор данных, выполняя некоторые манипуляции. В нашем случае я просто переместил данные влево и вправо на несколько точек:

image

Проектирование нейронной сети


Проектирование нейронной сети — не простая задача и требует некоторого опыта и интуиции. С другой стороны, нейронные сети хорошо изучены для некоторых типов задач, и вы можете просто приспособить существующую сеть. Наша задача очень похожа на задачу классификации изображений; вход можно рассматривать как изображение с высотой 1 пиксель (и это так и есть — первая операция преобразует входные двумерных данных [128 столбцов x 2 канала] в трехмерные данные [1 строка x 128 колонок x 2 канала]).

Таким образом, вход нейронной сети это массив [128, 2].

Выход нейронной сети представляет собой вектор с длиной, равной числу меток. В нашем случае это 2 числа с плавающим типом данных двойной точности.

Ниже приведена схема нейронной сети:

image

И подробная схема, полученная в TensorBoard:

image

Эта схема содержит некоторые вспомогательные узлы, необходимые только для обучения. Позже я покажу чистую, оптимизированную картину.

Обучение


Обучение будет проводиться на ПК в среде Jupyter Notebook с использованием Python и библиотеки TensorFlow. Notebook можно запустить в среде Conda, используя следующий файл конфигурации. Вот некоторые гипер-параметры обучения:

Оптимизатор: Adam
Количество эпох обучения: 3
Скорость обучения (learning rate): 0.0001

Набор данных разделен на обучающие и проверочные в соотношении 7 к 3.

Качество обучения можно контролировать с помощью значений точности обучения и тестирования. Точность обучения должна приближаться, но не достигать 1. Слишком низкое значение будет указывать на плохое и неточное распознавание, а слишком высокое значение приведет к переобучению модели и может привести к некоторым артефактам во время распознавания, например, распознавание с ненулевым значением для данных без жестов. Хорошая точность тестирования является доказательством того, что обученная модель может распознавать данные которых раньше никогда не видела.

Протокол обучения:

('Epoch: ', 0, ' Training Loss: ', 0.054878365, ' Training Accuracy: ', 0.99829739)
('Epoch: ', 1, ' Training Loss: ', 0.0045060506, ' Training Accuracy: ', 0.99971622)
('Epoch: ', 2, ' Training Loss: ', 0.00088313385, ' Training Accuracy: ', 0.99981081)
('Testing Accuracy:', 0.99954832)

Граф TensorFlow и связанные с ним данные можно сохранить в файлы, используя следующие методы:

saver = tf.train.Saver()
with tf.Session() as session:
    session.run(tf.global_variables_initializer())
    # save the graph
    tf.train.write_graph(session.graph_def, '.', 'session.pb', False)

    for epoch in range(training_epochs):
               # train

    saver.save(session, './session.ckpt')

Полный код можно найти здесь.

Экспорт нейронной сети


Как сохранить данные TensorFlow было показано в предыдущем разделе. Граф сохраняется в файле session.pb, а данные обучения (веса и т.д.) сохраняются в нескольких файлах «session.ckpt». Эти файлы могут быть достаточно большими:

session.ckpt.data-00000-of-00001                 3385232
session.ckpt.index                                 895
session.ckpt.meta                                65920
session.pb                                       47732

Граф и данные обучения можно заморозить и преобразовать в один файл, подходящий для работы на мобильном устройстве.

Чтобы заморозить его, скопируйте файл tensorflow/python/tools/freeze_graph.py в каталог скрипта и выполните следующую команду:

python freeze_graph.py --input_graph=session.pb         --input_binary=True         --input_checkpoint=session.ckpt         --output_graph=frozen.pb         --output_node_names=labels_output

где output_graph выходной файл, а output_node_names — имя выходного узла. Это значение указывается в коде Python.

Полученный файл меньше предыдущих, но все еще достаточно большой:

frozen.pb 1130835


Вот как выглядит эта модель в TensorBoard:

image

Чтобы получить такое изображение, скопируйте файл tensorflow/python/tools/import_pb_to_tensorboard.py в каталог скрипта и запустите:

python import_pb_to_tensorboard.py --model_dir=frozen.pb --log_dir=tmp 

где frozen.pb — файл модели.

Теперь запустите TensorBoard:

tensorboard --logdir=tmp

Существует несколько способов оптимизации модели для мобильной среды. Чтобы запустить описанные далее команды, вам необходимо скомпилировать TensorFlow из исходников:

1. Удаление неиспользуемых узлов и общая оптимизация. Выполните:

bazel build tensorflow/tools/graph_transforms:transform_graph
bazel-bin/tensorflow/tools/graph_transforms/transform_graph --in_graph=mydata/frozen.pb --out_graph=mydata/frozen_optimized.pb --inputs='x_input' --outputs='labels_output' --transforms='strip_unused_nodes(type=float, shape="128,2") remove_nodes(op=Identity, op=CheckNumerics) round_weights(num_steps=256) fold_constants(ignore_errors=true) fold_batch_norms fold_old_batch_norms'

Результат:

image

2. Квантование (конвертирование формата данных с плавающей запятой в 8-битный целочисленный формат). Выполните:

bazel-bin/tensorflow/tools/graph_transforms/transform_graph --in_graph=mydata/frozen_optimized.pb --out_graph=mydata/frozen_optimized_quant.pb --inputs='x_input' --outputs='labels_output' --transforms='quantize_weights strip_unused_nodes' 

В результате выходной файл имеет размер 287129 байт по сравнению с исходным 3,5 МБ. Этот файл можно использовать в TensorFlw для Android.

Демонстрационное приложение для Android


Чтобы выполнить распознавание сигналов в приложении Android, необходимо подключить библиотеку TensorFlow для Android в проект. Добавьте библиотеку к зависимостям gradle:

dependencies {
    implementation 'org.tensorflow:tensorflow-android:1.4.0'
}

Теперь вы можете получить доступ к API TensorFlow через класс TensorFlowInferenceInterface. Сначала поместите файл «frozen_optimized_quant.pb» в каталог «assets» вашего приложения (т.е. «app/src/main/assets») и загрузите его в коде (например, при запуске Activity, однако, как обычно, лучше производить любые операции ввода/вывода в фоновом потоке):

inferenceInterface = new TensorFlowInferenceInterface(getAssets(), “file:///android_asset/frozen_optimized_quant.pb”);

Обратите внимание, как указан файл модели.

Наконец, можно выполнить распознавание:

float[] data = new float[128 * 2];
String[] labels = new String[]{"Right", "Left"};
float[] outputScores = new float[labels.length];

// populate data array with accelerometer data

inferenceInterface.feed("x_input", data, new long[] {1, 128, 2});
inferenceInterface.run(new String[]{“labels_output”});
inferenceInterface.fetch("labels_output", outputScores);

Данные поступают на вход нашего «черного ящика» в виде одномерного массива, содержащего последовательные данные X и Y акселерометра, то есть формат данных [x1, y1, x2, y2, x3, y3, ..., x128, y128].

На выходе мы имеем два числа с плавающей запятой в диапазоне 0...1, значения которых — это соответствие входных данных жестам «влево» или «вправо». Обратите внимание, что сумма этих значений равна 1. Таким образом, например, если входной сигнал не совпадает ни с левым, ни с правым жестом, то выход будет близок к [0,5, 0,5]. Для упрощения лучше преобразовать эти значения в абсолютные значения 0...1, используя простую математику.

Кроме того, не забываем выполнить фильтрацию и нормализацию данных перед запуском распознавания.

Вот скриншот окна тестирования демонстрационного приложения:

image

где красные и зеленые линии представляют собой предварительно обработанный сигнал в режиме реального времени. Желтые и голубые линии относятся к «исправленным» «правым» и «левым» жестам соответственно. «Время» — это время обработки одного семпла, и оно довольно низкое, что позволяет выполнять распознавание в реальном времени (два миллисекунды означают, что обработка может выполняться со скоростью 500 Гц, мы же настроили акселерометр на обновление со скоростью 100 Гц).

Как вы можете видеть, есть некоторые нюансы. Во-первых, существуют некоторые ненулевые значения распознавания даже для «пустого» сигнала. Во-вторых, каждый жест имеет длительное «истинное» распознавание в центре со значением, близким к 1.0, и небольшим противоположным распознаванием по краям.

Похоже, для выполнения точного фактического распознавания жестов требуется дополнительная обработка.

Библиотека под Android


Я реализовал распознавание с помощью TensorFlow вместе с дополнительной обработкой выходных данных в отдельной библиотеке под Android. Библиотека и демонстрационное приложение находятся здесь.

Чтобы использовать библиотеку в своем приложении, добавьте зависимость на библиотеку в файл gradle:

repositories { maven { url "https://dl.bintray.com/rii/maven/" } }
dependencies {
    ...
    implementation 'uk.co.lemberg:motiondetectionlib:1.0.0'
}

создайте MotionDetector слушатель:

private final MotionDetector.Listener gestureListener = new MotionDetector.Listener() {
   @Override
   public void onGestureRecognized(MotionDetector.GestureType gestureType) {
     Log.d(TAG, "Gesture detected: " + gestureType);
   }
};

и включите распознавание:

MotionDetector motionDetector = new MotionDetector(context, gestureListener);
motionDetector.start();

Заключение


Мы прошли все этапы разработки и внедрения распознавания жестов движения в приложении Android, используя библиотеку TensorFlow: сбор и предварительную обработку данных, разработку и обучение нейронной сети, а также разработку тестового приложения и готовой к использованию библиотеки для Android. Описанный подход может использоваться для любых других задач распознавания или классификации. Полученная библиотека может быть интегрирована в любое другое приложение для Android, чтобы сделать возможным управление ним с помощью жестов движения.

Надеюсь, вы нашли эту статью полезной, вы также можете посмотреть видео обзор ниже.
Если у вас есть идея проекта, но не знаете, с чего начать, мы всегда здесь, чтобы Вам помочь.


P.S. На самом деле я также являюсь и автором оригинальной англоязычной версии статьи, которая была опубликована на blog.lemberg.co.uk, так что могу ответить на технические вопросы.

Комментарии (5)


  1. Calc
    17.01.2018 18:48

    С картинками косяк. Мелкие получились, нечитабельные.
    В оригинале нормальные


  1. dmitry_ch
    17.01.2018 22:12

    Нажав на данные, семпл будет автоматически подсвечен

    Не посчитайте меня занудой, но это — «проходя по мосту, с меня слетела шляпа». «Если нажать на данные, семпл будет автоматически подсвечен», как-то так.


  1. erwins22
    18.01.2018 09:01

    зачем тут полное соединение?


    1. r_ii Автор
      18.01.2018 12:52

      Такая уж модель. Зачем оно нужно в общем коротко освещено здесь


  1. ne-bo
    19.01.2018 12:15

    Спасибо, было интересно)