Несмотря на то, что искусственный интеллект – наиболее хайповая тема в современных ИТ, и учитывая тот факт, что предыдущая мега-история с Internet of Things и Edge Computing до сих пор не забылась, я удивился, что отсутствуют внятные обучалки типа «Hello world» по добавлению machine learning в мобильные приложения на Android. Ну они конечно есть, только совсем не начального уровня. Кроме того, в них предлагается использовать чужие модели для распознавания кошек от собак и рукописных букв в текст итд. Но вот чтобы взять регрессию и с ней поработать – нет (или я не нашел). И в книгах не нашел. Если есть – поделитесь. Ну а пока я буду добавлять свою модель в приложение и параллельно писать этот текст.

История моего приложения есть в публикациях 1, 2, 3. Если коротко, это программа RuLearn для запоминания лексики в иностранных языках или в любой другой области, которая требует механического заучивания. Ее эффективность определяется «кривой забывания» Эббингауза, но как выясняется, интервалы для повторения хорошо было бы адаптировать под сложность изучаемого материала. То есть обучающийся учит новые слова, а приложение учится на его ошибках и подстраивает алгоритмы повторения оптимальным образом. Напрашивается машинное обучение в обучении человека.

Современные ARM-процессоры в мобильных телефонах содержат NPU для работы с нейросетями. Возможно, до сих пор это наименее используемая часть аппаратного обеспечения в вашем мобильнике. Поэтому никакая клиент-серверная архитектура в данном случае не нужна, в финале нашей разработки машинное обучение будет идти на конечном устройстве (бонус - никто с серверной стороны не сможет посмеяться над ошибками пользователя). Но на промежуточном этапе придется использовать модель, созданную на десктопе. Сегодня этим и займемся.

Нативная библиотека для машинного обучения на устройствах под управлением Андроид – это TensorFlow Lite. В статье 3 я описал, как создавалась модель для TensorFlow десктопной версии, осталось ее сохранить и сконвертировать для дальнейшего использования на мобильном устройстве.

converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
with open('ruLearnModel.tflite', 'wb') as f:
    f.write(tflite_model)

Дальше у нас 2 варианта:

  1. Использовать в мобильном приложении нативные вызовы TensorFlow Lite. Так себе вариант, если вы делали модель на десктопе с использованием Keras, которая скрывает от вас API TensorFlow и чувствуете себя неуверенно. Потом все будет очень просто, когда разберетесь, но учтите – TF Lite – это код C++. Если будут возникать ошибки, никакой дебаг не приведет вас к пониманию проблем происходящего. Вылетел указатель куда не надо, поломал библиотеку и приложение и оставил в Logcat непонятную запись, что произошло что-то плохое (непонятно что). Я вернусь к этой теме, когда опишу обучение модели на устройстве в следующей статье и у нас уже будет работающая модель под Андроид, а пока мы пойдем другим путем.

  2. Использовать «обертку», которую может сгенерировать для вас Android Studio. При этом в вашем приложении появляется класс, названный так же, как и ваша модель и вы можете использовать методы этого класса для работы с моделью. Странно, но этот вариант плохо документирован (или опять же, я не нашел – поделитесь, если знаете, где это описано). Попробую исправить это недоразумение.

Для импорта модели не обязательно обновлять Android Studio до последней версии, найдите ее в контекстном меню вот здесь:

где-то далеко в далеких субменю
где-то далеко в далеких субменю

При импорте произойдут 3 вещи:

  1. Модель будет скопирована в проект Android Studio. Во время работы приложения вам не нужно будет доставать ее из asset’ов.

  2. Будут добавлены необходимые зависимости в файл build.gradle(:app).

  3. Будет создан тот самый класс-обертка для вашей модели с названием, которое совпадает с именем файла модели. Моя модель называется ruLearnModel.tflite, класс будет называться ruLearnModel.

Дайте время Android Studio загрузить и проиндексировать то, что ей хочется. После этого на экране появится надпись, что в модели нет метаданных (не пытайтесь их добавить или искать информацию, как это делается - это не нужно для ваших собственных проектов, вы знаете о своей модели больше, чем кто-либо другой). А еще на экране появится заготовка кода для вызова TF Lite на Kotlin и Java. Поскольку примеры обучения модели на устройстве на сайте TensorFlow приведены на Java, будем использовать этот язык и здесь:

try {
    RuLearnModel model = RuLearnModel.newInstance(context);

    // Creates inputs for reference.
    TensorBuffer inputFeature0 = TensorBuffer.createFixedSize(new int[]{1, 4}, DataType.FLOAT32);
    inputFeature0.loadBuffer(byteBuffer);

    // Runs model inference and gets result.
    RuLearnModel.Outputs outputs = model.process(inputFeature0);
    TensorBuffer outputFeature0 = outputs.getOutputFeature0AsTensorBuffer();

    // Releases model resources if no longer used.
    model.close();
} catch (IOException e) {
    // TODO Handle the exception
}

В иерархии классов вашего проекта вы не увидите класса RuLearnModel, а файл модели не появится в ассетах. Тем не менее, через файловый менеджер видно, что в каталоге main создан подкаталог ml и модель скопирована туда. После компиляции в app -> build -> generated -> ml_source_out -> debug -> имя пакета -> ml появится сгенерированный класс для модели. Код выше после импорта всех необходимых библиотек готов к применению за исключением byteBuffer – именно туда нужно передавать входные параметры. Обратите внимание – TensorFlow Lite ожидает входные параметры в виде float32, а int[]{1, 4} – это размерность моей модели – 4 входных параметра в одной строке. Но эти параметры нужно преобразовать в массив float и передать на вход TF Lite:

//строчку
inputFeature0.loadBuffer(byteBuffer);

//меняем на
float[] featureArray = new float[]{
        (float)id, (float) cur_rating, (float) n_repeat, (float) s_lapsed
};
inputFeature0.loadArray(featureArray);

Переменные id, cur_rating, n_repeat и s_lapsed – параметры моей модели из статьи 3. Дальше можно попробовать передать одинаковые параметры обученной модели на питоне и коду в Android Studio, результат должен совпасть.

Между делом мы обошли одну засаду – нормализацию данных. Модель в моем примере плохо воспринимала ненормализованные данные (то есть имеющие разный масштаб – сравните: id = 17, cur_rating = 13, n_repeat = 5, а s_lapsed = 434943!). Поэтому перед обучением модели размерность данных нужно было привести к одному порядку. Этого можно было добиться разными способами, но если вы использовали StandardScaler из sklearn, то каким-то образом пришлось бы передавать данные о среднем значении и стандартном отклонении по колонкам датасета в мобильное приложение, чтобы там повторить эту операцию при каждом запросе к модели. В моем случае слой нормализации встроен в саму сеть и она на входе сама преобразует данные требуемым образом. Удобно!

Hidden text
normalizer = tf.keras.layers.Normalization(axis=-1)
normalizer.adapt(np.array(X))
model = tf.keras.Sequential([
    normalizer,
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dense(64, activation='relu'),
    tf.keras.layers.Dense(1)])

Теперь проверим как модель отработает на базе целиком. Тут решим 2 задачи – выясним, насколько отличается то, что предскажет TF Lite от десктопного TF и заодно узнаем, сколько времени это займет. Казалось бы, можно прочитать таблицу целиком – она занимает в памяти пару десятков килобайт - и передать ее в буфер вида TensorBuffer.createFixedSize(new int[]{<число записей в таблице>, 4}, DataType.FLOAT32). Ведь в TensorFlow на десктопе так можно. Но не тут-то было. Наш класс с моделью (спасибо огромное!) выдает осмысленные описания ошибки, что загрузить подобный буфер в TensorFlow нельзя. Как с этим справиться, я не нашел (подскажите, если вы знаете). Но я не очень расстроился. Этот код нужно выполнить всего один раз в качестве эксперимента, поэтому засекаем время, нудно читаем таблицу построчно, запускаем модель для каждого значения, запоминаем результат. Понятно, модель инициализируется один раз в onCreate и закрывается в onDestroy для Activity. Когда таблица обработана, смотрим сколько прошло. Я тестировал на телефоне 2017 года на Qualcomm Snapdragon 625, без подключения аппаратного ускорения, в build variant: debug да еще и вызывая модель для каждой строчки – то есть максимально неэффективно. Получилось 167 миллисекунд. Делаем вывод, что даже на самых древних девайсах модель для моего приложения будет работать незаметно для пользовательского опыта.

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

from sklearn.metrics import mean_squared_error
print(f"Mean squared error is: {mean_squared_error(predictionTF_Lite, predictionTF)}")
---------------------------------------------
Mean squared error is: 6.711172283236772e-15

А можно выгрузить оба результата в один csv и полюбоваться в Excel на результаты своих усилий за сегодня:

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

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


  1. ret77876
    19.08.2024 16:15
    +1

    Qualcomm Snapdragon 625, без подключения аппаратного ускорения <...> Получилось 167 миллисекунд

    Если я правильно понял, то в итоге модель запускалась не на NPU (которого в данном SoC просто нет, если я правильно погуглил)?


    1. Dima_RziO Автор
      19.08.2024 16:15

      Пока да, интересно было проверить, насколько это будет плохо. Ну и для ускорения нужно использовать нативные вызовы - это на следющий раз. Но я сегодня прочитал вот это. Похоже, с андроида 15 вся поддержка NPU будет удалена из TF Lite. И это странно, то есть имеющимися NPU обычный программист пользоваться не сможет.