Android
April 25, 2023

Нейронные сети и Android: как их создавать и использовать в мобильных приложениях

Маски в Snapchat и Запрещёноgram, категоризация фотографий в галерее, улучшение качества фотографий в Google Camera — все эти фичи в приложениях используют нейронные сети. Будущее мобильных устройств тесно связано с развитием нейронных сетей и их интеграцией в различные приложения: нейросети помогают усовершенствовать пользовательский опыт и повысить эффективность работы приложений.

Меня зовут Артём Пустовой, я Android-разработчик в Surf. Мы, как и весь мир, тоже поймали тренд на нейросети и встраиваем их в наш рабочий процесс. Расскажу, как создавать свои модели и как их использовать в Android-приложении. Разберём, как использовать в приложениях нейросети с аппаратным ускорением локально, без подключения к сети.

Какие виды нейронных сетей существуют

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

Зачем это нужно знать в статье про Android? Если вы хотите создавать и обучать нейросети, а не берёте готовую модель, важно знать, какой тип сети подходит для решения конкретной задачи, и понимать её принцип работы.

Прямые нейронные сети. Самые простые: информация передается только в одном направлении, от входных данных к выходным. Используются для классификации и регрессии.

Свёрточные нейронные сети. Оптимизированы для обработки изображений: используют слои для выделения признаков изображения, и например, снижения веса и разрешения изображения.

Свёрточные нейросети

Рекуррентные нейронные сети. Нужны для обработки естественного языка, генерации текста.

Рекуррентные нейросети

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

Глубокие нейросети

Рекурсивные. Нужны для обработки деревьев или графа. Пример: обработка естественного языка, генерация текста. Применяются везде, где данные имеют иерархию.

Self-Organizing Maps (SOM) для кластеризации данных и визуализации высокоразмерных данных.

Чтобы глубже познакомиться с нейронными сетями, рекомендую: * Цикл статей на Хабре «Нейронные сети для начинающих»* Книгу «Создаем нейронную сеть» Тарика Рашида

Инструменты для внедрения нейронок в Android

Для создания и внедрения нейронных сетей в Android приложение можно применять различные инструменты. Рассмотрим наиболее популярные.

Инструменты для Android

TensorFlow Lite. Библиотека специально разработана для работы с мобильными устройствами. Обеспечивает высокую производительность и компактный размер моделей. Поддерживает интеграцию с NNAPI: помогает ускорить выполнение на аппаратном уровне.

Keras и PyTorch. Обе библиотеки обладают богатым набором инструментов для обучения нейронных сетей и поддерживают экспорт моделей в формат TensorFlow Lite. Если вы хотите создать сложную нейронную сеть с нестандартной архитектурой, можно использовать эти библиотеки.

ML Kit. Сервис предоставляет готовые решения для ряда задач: например, распознавание изображений и звука, анализ текста и так далее. Но при использовании ML Kit вы можете ограничены в возможностях настройки моделей.

NNAPI (Neural Networks API) — это API, разработанное компанией Google. Позволяет использовать аппаратное ускорение для выполнения вычислений нейронных сетей на устройствах с операционной системой Android.

NNAPI: остановимся подробнее

Почему мы решили рассказать чуть подробнее именно о NNAPI? На Android-устройствах этот инструмент может сильно ускорить выполнение операций при работе с нейронными сетями.

Чем полезен NNAPI при работе с нейронками на Android

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

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

NNAPI поддерживают разные библиотеки, включая TensorFlow Lite.

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

Официальная информация от Google: NNAPI в 3 раза ускоряет время ожидания и в 3,7 раза снижает потребление заряда

Подробности про «внутренности» NNAPI, архитектуру и принцип работы хорошо описаны в документации.

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

Сочетание TensorFlow Lite и NNAPI может быть хорошим выбором в ситуациях, когда требуется максимально эффективное использование аппаратных ресурсов для запуска нейронных сетей на устройствах Android.

Но прежде чем встраивать нейронную сеть в Android-приложение, необходима сама нейронная сеть. Давайте разберем, как нам создать модель, которую мы сможем использовать с Tensorflow LIte.

Как создать собственную модель

Немного теории — без неё не обойтись

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

Нейрон состоит из трёх элементов:

  • входы,
  • веса,
  • функция активации.

Вес — это величина, которая показывает важность каждого входа. Оптимальные значения весов могут помочь нейронной сети достичь высокой точности в выполнении задач, для которых она была обучена. Изначально эти параметры случайны и в процессе обучения корректируются. В алгоритмах линейной регрессии обычно будет диапазон (0, 1), если функция активации — сигмоида.

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

Разные функции активации

Схема нейронной сети:

Сигнал в нейронной сети распространяется от входных нейронов к выходным нейронам через слои нейронов.

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

Вот и всё. По сути у нас есть какие-то входные данные, которые мы умножаем на веса и суммируем. После передаём в функцию активации и далее — в следующий нейрон.

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

Чтобы нейронная сеть могла обучаться, необходимо подготовить размеченные данные: входные данные и ожидаемый результат. Например, для функции y = 2x-1 входные данные — это x, а ожидаемый результат — y.

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

Для тех, кому интересно, как считается ошибка и как корректируются нейроны, рекомендую всё то же:* Цикл статей на Хабре «Нейронные сети для начинающих»* Книгу «Создаем нейронную сеть» Тарика Рашида

Приступим к созданию собственной модели

Для своей модели будем подсчитывать ошибку среднеквадратичной функцией, а обучать сеть методом градиентного спуска.

Чтобы сделать простую модель, понадобится немного Python. Также нам нужны будут две библиотеки: TensorFlow и NumPy.

Добавляем зависимости:

import tensorflow as tf

import numpy as np

Создадим сетку, которая будет считать функцию 2*x - 1. У нас будет один вход и один выход.

model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(1,), name='input'),
    tf.keras.layers.Dense(units=1, activation='linear', name='output')
])

В этом примере определяем модель, которая состоит из одного входного слоя (Input) и одного выходного слоя (Dense) с единственным выходным узлом. Используем класс Sequential. Входной слой имеет форму (shape) (1,), то есть он принимает одномерный вектор с одним значением.

Мы выбрали линейную функцию активации для выходного слоя, потому что решали задачу регрессии. Цель регрессии — предсказать непрерывную целевую переменную (в данном случае, значение y) на основе входных данных (в данном случае, значение x).

Далее надо скомпилировать модель. Мы выбрали градиентный спуск, потому что это один из самых популярных методов оптимизации в глубоком обучении. Градиентный спуск оптимизирует веса модели на основе градиента функции потерь по отношению к весам.

Для подсчета ошибок используем среднеквадратичную функцию. Функция потерь показывает, насколько сильно предсказанные значения отклоняются от реальных значений.

model.compile(optimizer=tf.keras.optimizers.SGD(lr=0.01), loss='mean_squared_error')

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

Обучаем модель на наборе данных, состоящем из пяти пар входных и выходных значений. Входные значения хранятся в массиве x_train, а соответствующие выходные значения — в массиве y_train. Обучение выполняется на протяжении 500 эпох — то есть проходов через обучающий набор данных. Величина эта эмпирическая — зависит от количества данных и степени точности, которая требуется от модели.

x_train = np.array([-1.0, 0.0, 1.0, 2.0, 3.0])

y_train = np.array([-3.0, -1.0, 1.0, 3.0, 5.0])

model.fit(x_train, y_train, epochs=500)

После запуска видно, как величина ошибки уменьшается.

Протестируем нашу модель. Как видим ниже, она верно считает результат.

Далее форматируем модель в формат .tflite и сохраняем её.

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

Как использовать готовую модель с аппаратным ускорением NNAPI

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

При выборе модели нужно учитывать:

  • Размер модели: она должна быть достаточно маленькой, чтобы поместиться на устройстве и быстро загружаться.
  • Точность должна быть достаточно высокой для решения поставленной задачи.

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

Выбираем модель из TensorFlow Hub. Кидаем её в assets и запрещаем в gradle сжатие. Если файл модели сжать, размер изменится и приложение может не смочь правильно загрузить модель. Это приведёт к ошибкам в работе модели и, в конечном итоге, к сбоям приложения.

androidResources {
   noCompress 'tflite'
}

Вставляем зависимости.

implementation 'org.tensorflow:tensorflow-lite-task-vision:0.4.0'
implementation 'org.tensorflow:tensorflow-lite-gpu-delegate-plugin:0.4.0'
implementation 'org.tensorflow:tensorflow-lite-gpu:2.9.0'

NNAPI уже входит в одну из них.

Создаём image classifier: он устанавливает порог оценки для результатов классификации изображения.

private fun setupImageClassifier() {
   val threshold: Float = 0.5f
   val maxResults: Int = 3
   val numThreads: Int = 2
   val optionsBuilder = ImageClassifier.ImageClassifierOptions.builder()
       .setScoreThreshold(threshold)
       .setMaxResults(maxResults)

   val baseOptionsBuilder = BaseOptions.builder().setNumThreads(numThreads)
   baseOptionsBuilder.useNnapi()
   optionsBuilder.setBaseOptions(baseOptionsBuilder.build())

   val modelName = "model.tflite"

   try {
       imageClassifier =
           ImageClassifier.createFromFileAndOptions(context, modelName, optionsBuilder.build())
   } catch (e: IllegalStateException) {
       Log.e("TF", "TFLite failed to load model with error: " + e.message)
   }
}

Получился image-классификатор с уже обученной моделью. Можем выполнять код.

На вход подаём bitmap — его надо получить из картинки. Картинку прогоняем через image-процессор и получаем выходной результат.

// Create preprocessor for the image.
val imageProcessor = ImageProcessor.Builder()
   .build()

// Preprocess the image and convert it into a TensorImage for classification.
val tensorImage = imageProcessor.process(TensorImage.fromBitmap(image))

val imageProcessingOptions = ImageProcessingOptions.builder()
   .setOrientation(getOrientationFromRotation(rotation))
   .build()

val results = imageClassifier?.classify(tensorImage, imageProcessingOptions)

Результаты:


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

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

Источник