Недавно я был очарован тем, насколько интересно было бы создать математически ориентированное приложение и развернуть его в любом масштабе без каких-либо ограничений по размеру модели, платформе или необходимости в вызовах API. Я знаю, что у Python достаточно библиотеки для работы с прототипами проектов машинного обучения, однако не многие говорят о масштабировании этого проекта, особенно когда вы не хотите делать это через веб-API.

Я считаю, что истинный интеллект не должен полагаться только на вызовы API, чтобы модель была доступна в масштабе. Это увлечение привело меня к исследованию того, что потребуется для использования C ++ для машинного обучения и общего интеллекта.

Я убежден, что математические достоинства как Matlab, так и Python основаны на базовом коде c / c ++. Следовательно, фундаментально масштабируемая технология для работы с математическими вычислениями, связанными с машинным обучением, с учетом невероятно быстрого сценария, вероятно, потребует, чтобы вы могли углубиться в низкоуровневое программирование, особенно с c / c ++.

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

После изучения C ++ с помощью практического курса Udemy теперь задача состоит в том, чтобы интегрировать простое приложение для распознавания лиц в Android.

Описание будет включать некоторые предварительные подходы к тому, что вам нужно для создания проекта C ++ и развертывания в Android или любой другой среде ОС.

Компоненты:

  1. Настройте проект C ++ для машинного обучения с помощью opencv.
  2. Изучение PCA для создания собственных граней
  3. Настройка библиотеки вывода .so для многоплатформенного развертывания
  4. Разработка jni-оболочки для библиотеки вывода.
  5. Использование библиотеки в Android или любого java кода.

Первая часть будет посвящена изучению алгоритма машинного обучения с OpenCV. В этом случае мы собираемся изучить самый простой алгоритм распознавания лиц, используя анализ главных компонентов для собственных лиц. Сообщество машинного обучения хорошо знакомо с этим в Python, особенно с такими инструментами, как sci-kit learn, но когда приходит на ум производство и особенно автономное / на устройстве, необходимость делать это из другого измерения целесообразно.

OpenCV поставляется с очень хорошим api для изучения анализа основных компонентов, и его довольно легко изучить, как только вы настроите все свои данные.

Вот шаги:

Создайте проект cmake на C ++ с помощью OpenCV.

Ключевой частью вашего CMakeFile.txt является обеспечение наличия библиотеки OpenCV в проекте 0 и ее доступности для компиляции вашей библиотеки. В идеале, прежде чем просить Cmake найти OpenCV для вас, важно, чтобы на вашем компьютере была установлена ​​библиотека OpenCV.

find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
set(LIBS ${OpenCV_LIBS})
target_link_libraries(featureCalculation ${LIBS})

PS: Мне пришлось настроить cmake по-другому для обучения и вывода. Я поделюсь обоими

Изучите анализ главных компонентов.

Теперь, когда доступен OpenCV, изучать PCA довольно просто. Вот логическая инволюция:

Прочитать все данные изображения в виде массива.
Используя cv :: glob из OpenCV, все имена файлов заканчивающиеся на .jpg, .png или / и .jpeg, можно прочитать с помощью cv :: imread, и может продолжаться предварительная обработка данных изображения.

Обрезать лица.
Это важно, потому что PCA гораздо лучше справляется с изображением лица, чем со всем изображением. Я обнаружил, что Многозадачные каскадные сверточные сети (MTCNN) являются наиболее надежной, но простой и минимальной моделью обнаружения и кадрирования лиц. Существует реализация на C ++ с использованием исходной модели с сетью Caffe в OpenCV.

Преобразование обрезанных лиц в оттенки серого.
Эта часть довольно проста. Используя cv :: cvtColor (originalimagemat, grayscaleimagematcontainer, cv :: COLOR_BGR2GRAY), мы можем преобразовать исходное изображение BGR в оттенки серого в OpenCV.

Другая предварительная обработка. Еще одна предварительная обработка предназначена для проверки правильности типов данных. Это очень важно, потому что C ++ сильно зависит от точности типов данных. На этом этапе довольно легко внести ошибки, поэтому необходимо тщательно проверять правильность ваших типов данных. Помимо этого, рекомендуется нормализовать данные изображения и преобразовать размер всех изображений в согласованную форму. PCA работает, только если данные находятся в одном измерении. Из коробки с OpenCV мы можем использовать следующие функции, чтобы позаботиться о предварительной обработке:

cv :: resize (originalImage, containermatofnewimage, size) для изменения размера изображения и originalmat :: convertTo (newmatnormalized, CV_32FC3, 1 / 255.0) , чтобы нормализовать значения пикселей изображения.

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

Чтобы создать таблицу данных, можно использовать std :: vector для хранения всех изображений (с надеждой, что они поместятся в памяти), которые затем копируются в каждую строку матрица данных. Вот вспомогательная функция, которая делает именно это из вектора изображений mat.

cv::Mat createdatamatrix(std::vector<cv::Mat> imageArray) {
cv::Mat datamatrix(static_cast<int>(imageArray.size()), imageArray[0].rows * imageArray[0].cols, CV_32F);
unsigned int i;
for(i=0; i < imageArray.size(); i++) {
   cv::Mat imageRow = imageArray[i].clone().reshape(1, 1);
   cv::Mat rowIData = datamatrix.row(i);
   imageRow.copyTo(rowIData);
  }
  return datamatrix;
}

cv :: reshape () помогает преобразовывать массивы матов в различные формы, причем (1, 1) буквально означает, что мы хотим, чтобы данные располагались в одной строке.

Изучите реальный алгоритм анализа компонентов Pricipal.
Теперь, когда мы создали таблицу данных с предварительно обработанными изображениями лиц, изучение модели PCA обычно проходит гладко. Так же гладко, как передача таблицы данных в экземпляр OpenCV pca с ожидаемым максимальным количеством компонентов, таких как cv :: PCA pca (datatable, cv :: Mat (), cv :: PCA :: DATA_AS_ROW, number_of_components) . Благодаря этому у нас есть изученный PCA, написанный на C ++, который готов к использованию в производственной среде.

Чтобы передать эту модель для использования в любой среде, в open cv есть объект FileStorage, который позволяет сохранить мат как есть. Таким образом, я могу сохранить этот файл и передать его имя файла через jni для OpenCV, чтобы воссоздать экземпляры модели для вывода. Ну, это так же мило - служить модели.

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

Сохраните объект модели pca для вывода в производственной среде.
Что именно нужно сохранить в объекте pca, так это среднее и собственные векторы обученного ПК, иногда может быть хорошей идеей также сохранить собственные значения на случай, если вы хотите построить свою собственную проекцию собственных граней, но OpenCV уже реализовал Экземпляр pca- ›project, который помогает в выводе и генерации собственных граней. В любом случае вот как сохранить вашу модель:

void Facepca::savemodel(cv::PCA pcaModel, const std::stringfilename)
{
  cv::FileStorage fs(filename,cv::FileStorage::WRITE);
  fs << “mean” << pcaModel.mean;
  fs << “e_vectors” << pcaModel.eigenvectors;
  fs << “e_values” << pcaModel.eigenvalues;
  fs.release();
}

Уровень вывода

После сохранения модели уровень вывода будет включать загрузку сохраненной модели, значения которой хранятся в файле yml также через OpenCV и разработаны для формирования механизма вывода с необходимыми модулями предварительной обработки данных, который, наконец, объединен как. so для развертывания в среде на основе Linux. Для других платформ коды cpp могут быть скомпилированы для создания dll или dylib для windows и mac соответственно. .

Поскольку нас больше беспокоит развертывание модели в приложении для Android, основное внимание будет уделено созданию механизма вывода файлов .so на основе сохраненной модели.

Создание библиотеки вывода .so

Загрузить модель PCA
Для вывода мы позволяем OpenCV загружать существующий файл модели из сохраненного .yml , после чего мы передаем собственные значения, собственные векторы и среднее значение новому объекту PCA, который затем можно назвать проектом pca- ›, чтобы создать проекцию нового изображения.

Вот пример кода, который загружает сохраненную модель OpenCV FileStorage.

cv::PCA newPcaModel;
cv::PCA loadmodel(cv::PCA newPcaModel, const std::string filename){
   cv::FileStorage fs(filename,cv::FileStorage::READ);
   fs[“mean”] >> newPcaModel.mean ;
   fs[“e_vectors”] >> newPcaModel.eigenvectors ;
   fs[“e_values”] >> newPcaModel.eigenvalues ;
   fs.release();
   return newPcaModel;
}
loadmodel(newPcaModel, “path-to-saved-yml-file”);

После загрузки модели объект newPcaModel теперь содержит сохраненную модель из существующих параметров обучения, то есть собственных значений pca, собственных векторов и среднего значения. Следовательно, когда выполняется проекция лица, гарантируется, что возвращенные данные относятся к набору обучающих данных.

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

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

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

Имея это в виду, затем нам нужно убедиться, что библиотека может извлекать изображение из строки base64 и отправлять его в OpenCV.

Декодирование строки base64 в C ++ довольно нетривиально, однако читатель может сослаться на эту ссылку во фрагменте кода, который делает это.

После декодирования строки изображения base64 мы затем преобразуем строку в вектор беззнакового символа (uchar), который можно рассматривать как значения изображения.

OpenCV может декодировать вектор uchar в изображение с помощью вызова функции cv :: imdecode (vectorUchar, flag). Этот процесс возвращает изображение Mat, с которым может быть произведена дальнейшая предварительная обработка.

Затем изображение может пройти этапы предварительной обработки;

  • Удаление лица
  • Преобразуйте кадрированные лица в оттенки серого.
  • Изменение размера изображения
  • Нормализация изображения
  • Создать матрицу данных

Так же, как описано в обучающей части этой статьи выше.

Последним этапом вывода нового изображения является использование объекта loaded pca для создания проекции лица в новом изображении.

Фрагмент, который делает это, будет выглядеть так:

newPcaModel->project(datamatrix.row(0));

Часть распознавания или проверки происходит, когда вы проецируете загруженный объект pca на две грани и сравниваете проекции (собственные грани), используя метрики расстояния, например евклидово расстояние или косинусное сходство.

Разработка jni-оболочки для библиотеки вывода

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

Однако для создания jni важно понимать, как происходит связь с java.

Первый путь - создать класс Java и функции, которые вы хотите использовать в своем классе java, с входными параметрами.

Это будет то, что jni будет использовать в качестве имени функции при создании кода cpp. Согласованность между именем пути к классам методов в Java и именем в cpp очень важно.

Предположим, что метод, который мы будем использовать для сопоставления функций pca, называется matchpcafeatures (), и тогда наш класс java может выглядеть так.

package org.example.code
class MatchFeatures {
static float matchpcafeatures(modelfilename: String, image: String, projectionToCompare: Array[Float])
}

С указанными выше java-классом и методом наш заголовочный файл jni будет выглядеть примерно так.

#include <jni.h>
/* Header for class com_example_code_MatchFeatures */
#ifndef _Included_com_example_code_MatchFeatures
#define _Included_com_example_code_MatchFeatures
#ifdef __cplusplus
extern “C” {
#endif
JNIEXPORT jfloat JNICALL Java_com_example_code_MatchFeatures_matchpcafeatures
(JNIEnv *, jobject, jstring, jstring, jfloatArray);
#ifdef __cplusplus
}
#endif
#endif

Вам пока не нужно беспокоиться о деталях extern C, основное внимание уделяется имени метода в файле заголовка jni.

Далее мы рассмотрим, как использовать приведенный выше код Java для вывода. Давайте сначала разработаем мост jni для этого метода.

Последние 3 параметра заголовка jni точно такие же и находятся в точном положении, что и параметры в методе java.

Поэтому мы будем обрабатывать эти параметры, поскольку они являются ключевыми требованиями клиента к нашему механизму вывода C ++.

Ниже показано, как подключить эти входные параметры и вернуть значение, которое код Java может принять и продолжить другие процессы.

/**
* Match features of pca
* */
JNIEXPORT jfloat JNICALL Java_com_seamfix_qrcode_FaceFeatures_matchpcafeatures
(JNIEnv * env, jobject obj, jstring pcafilename, jstring imagestring, jfloatArray projectionToCompare){
  const char *pcastring_char;
  pcastring_char = env->GetStringUTFChars(pcafilename, 0);
  if(pcastring_char == NULL) {
    return 0.0f;
  }
  const char *imagestring_char;
  imagestring_char = env->GetStringUTFChars(imagestring, 0);
  if(imagestring_char == NULL) {
     return 0.0f;
  }
  //Get file name string as a string for cpp
  std::string stdfilename(pcastring_char);
  cv::PCA pca;
  //Class InferencPca holds the preprocesing and inference methods
  InferencePca ef;
  std::vector<cv::Mat> imagevec;
  //Get image as base64
  cv::Mat image = ef.readBase64Image(imagestring_char);
  ef.loadmodel(pca, stdfilename);
  imagevec.push_back(image);
  cv::Mat datamatrix = ef.createdatamatrix(imagevec);
  cv::Mat projection = ef.project(datamatrix.row(0));
  //Load the existing vector.
  std::vector<float> initialProjectionPoints;
  //Load existing features -- needed to do comparison of faces
  jsize intArrayLen = env->GetArrayLength(existingfeatures);
  jfloat *pointvecBody = 
      env->GetFloatArrayElements(existingfeatures, 0);
  for (int i =0; i < intArrayLen; i++) {
     initialProjectionPoints.push_back(pointvecBody[i]);
  }
  std::vector<float> newProjectionpoints =     
                ef.matToVector(projection);
  float comparisonScores = 
     ef.compareProjections(newProjectionpoints, 
                            initialProjectionPoints);
  env->ReleaseFloatArrayElements(existingfeatures, pointvecBody, 0);
  env->ReleaseStringUTFChars(pcafilename, pcastring_char);
  return comparisonScores;
}

Таким образом, мы настроены на создание нашей библиотеки .so и выпуск ее в виде кода Java для успешного использования. Протокол для создания библиотеки указан в CMakeLists.txt.

Это выглядит так:

find_package(JNI REQUIRED)
#Include jni directories
include_directories(${JNI_INCLUDE_DIRS})
file (GLOB_RECURSE SOURCE_FILE src/*.h src/*.cpp)
set(LIBS ${JNI_LIBRARIES})
add_library(matchprojection SHARED ${SOURCE_FILE})
target_link_libraries(matchprojection ${LIBS})

При сборке проекта должен генерироваться файл lib-matchprojection.so, который можно добавить в ваш Java-проект.

Однако для Android это немного сложно в том смысле, что инструмент сборки отличается, а не официальная цепочка инструментов сборки cmake, у Android есть собственный инструмент сборки для собственных кодов cpp. Это называется NDK. Это то, что будет использоваться для создания собственных кодов C ++ для сгенерированных файлов .so, совместимых с Android.

Сборка .so для Android с использованием NDK сама по себе будет отдельным учебником, поэтому я его пока пропущу.

Но, как правило, после завершения сборки с использованием NDK у вас будет тот же lib-matchprojection.so, который можно использовать в вашем приложении для Android.

Использование библиотеки .so в приложении для Android.

Использование сгенерированной библиотеки в приложении Android ничем не отличается от использования ее в любом приложении Java.

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

Чтобы загрузить библиотеку в любую программу Java, включая Android, убедитесь, что библиотека .so находится в пути к классам вашей программы, некоторые поместят ее в папку с именем lib или jniLibs. При этом я могу использовать вызов функции для загрузки библиотеки, как показано ниже:

System.loads(“native-lib”)

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

MatchFeatures mf = new MatchFeatures();
float matchscores = mf.matchpcafeatures(storedpacfilepath, imagebase64string, anotherimageprojectionarray);

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

Заключение:

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

Даже у TensorFlow есть API для загрузки моделей глубокого обучения в C ++, а также для использования моделей tflite в C ++.

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

Я работаю специалистом по анализу данных и инженером по машинному обучению в компании Seamfix Nigeria Ltd., где я сосредоточен на том, чтобы функции обработки данных попали в рабочую среду.