ML для мобільного розробника: Google Cloud для тренування ML-моделі

💡 Усі статті, обговорення, новини про Mobile — в одному місці. Приєднуйтесь до Mobile спільноти!

Цей текст буде корисний мобільним розробникам, які хочуть тренувати наявні ML-моделі на власних даних і використовувати їх у створюванні мобільних додатків.

Чому обчислювальні можливості мого комп’ютера можуть не підійти для тренування ML-моделі?

Якщо ви хочете навчити персептрон, щоб він виконував операцію XOR, можна навчити таку нейромережу навіть на старенькому мобільному.

Але деякі рішення для розпізнавання образів потребують значної обчислювальної потужності. Наприклад, для тренування YOLO (алгоритму розпізнавання об’єктів і їхнього розташування на фото) потрібні тижні (якщо не місяці) тренування на досить потужному CPU. На топових GPU час тренування може зменшитися з декількох днів до кількох годин. Можна, звісно, витратити декілька тисяч доларів на останню модель Nvidia Tesla GPU, але якщо ви не працюєте із цим активно, то, імовірно, таке придбання буде марним. Окрім того, треба зазначити, що інколи щоб пришвидшити обчислення, їх треба «розпаралелити» на декілька таких GPU. Тому досить часто доречно використовувати cloud-потужності.

Від чого залежить час тренування моделі?

Це залежить від багатьох параметрів: від розміру набору даних, на якому тренуватимете модель, від кількості ваг (weights, кількості каліброваних параметрів нейромережі), кількості ітерацій тощо.

Тобто сам процес навчання нейронної мережі можна назвати «калібрацією» ваг, і масив цих ваг + структура самої нейронної мережі формують pre-trained model, яку й завантажуватимуть на мобільний пристрій у нашому прикладі.

Що таке epoch, step, iteration, loss, batch size, tensor shape, over-fitting?

Якщо говорити про код оригінальних репозиторіїв з реалізаціями ML-моделей, то для розробника, який не дуже обізнаний із цією темою, може виявитися чимало незнайомих термінів. Розглянемо деякі з них.

Для тренування нейронних мереж широко використовують алгоритми градієнтного спуску й зворотного поширення помилки.

Масив даних, на яких тренуватимемо нашу модель, поділяється на N-ну кількість партій (batches), і розмір кожної з них — це batch size. Далі, коли кожна із цих партій даних передається вперед і назад по обчислювальному графу (розраховуючи зворотне поширення помилки) через нейронну мережу, це і є одна epoch.

Щоб знайти найкраще значення окремої ваги, коли значення помилки найменше, виконують рух уздовж уявного графіка по градієнту (вектору, який указує напрямок до зростання якоїсь величини) через деякий крок (step), і для цього потрібно декілька ітерацій. Тобто iterations — це кількість batches, потрібних для того, щоб закінчити одну epoch.

Loss — це число, яке характеризується loss-функцією, що вказує, наскільки поганим було прогнозування моделі на одному прикладі. Якщо прогноз моделі ідеальний (що завжди малоймовірно), loss дорівнює нулю, інакше — loss більший.

Кожна штучна нейронна мережа має input і output, тому щоб «згодувати» їй дані (та одержати вихідне значення), треба їх привести до відповідного формату, тобто до N-розмірного масиву. І форма (shape) — це кількість елементів у кожній з його розмірностей.

РангФормаНомер розмірностіПриклад
0[]0-DТензор 0-D. Скаляр
1[D0]1-DТензор 1-D форми [5]
2[D0, D1]2-DТензор 2-D форми [3, 4]
3[D0, D1, D2]3-DТензор 3-D форми [1, 4, 3]
N[D0, D1...Dn-1]n-DТензор форми N-D [D0, D1...Dn-1]

Наприклад, таку картинку, створену лише із 4 пікселів (зелений, чорний, синій, червоний), можна представити як N-розмірний масив

[   
   [ [0, 255, 0],   [0, 0, 0]   ], 
   [ [0, 0, 255],  [255, 0, 0] ]
 ]
з shape [2, 2, 3] (висота, ширина, RGB).

Припустимо, ви намалювали червоними крапками знайомий ще зі школи графік функції y = x. Вийшло не надто рівно, і ви хочете побудувати ML-модель, щоб вона змогла домалювати продовження цього графіка.

Over-fitting — це коли ваша модель ідеально «прилягає» до даних, на яких вона тренується, у цьому прикладі — до всіх нерівностей, червоних крапок, які ви намалювали. Але далі намалювати продовження графіка правильно вона не може, тобто вже на тестових даних робить значні помилки.

Appropriate fitting — це коли ваша модель правильно виокремила закономірності даних, і в цьому прикладі може правильно домалювати графік, тобто добре працює на тестових даних.

Under-fitting — це коли ваша модель погано працює і на даних для тренування, і на даних для тестування.

Але іноді, у реальному житті, припустимо використовувати моделі, які можна назвати over-fitted, які ідеально працюють лише на деякому діапазоні даних (але за умови, що вони спрощують обчислення). Наприклад, формула додавання швидкостей (звичних людині в повсякденному житті) досить проста — просте додавання. Але якщо розглядати досить великий діапазон швидкостей, аж до порівнянних зі швидкістю світла, — вона вже має складніший вигляд. Тобто перша формула працює лише на одному виокремленому діапазоні даних, а друга — на ще ширшому. Але досить часто другою формулою можна знехтувати й заради спрощення обчислень використовувати першу.

Крок 1. Готуємо проект до тренування на Google Cloud

Для прикладу я вибрав проект open-source із розпізнавання об’єктів та їхніх координат на фото — YOLOv3, який використовує Keras.

Спочатку відредагуємо структуру нашого проекту:

trainer		 # Директорія яка містить train-модуль 
--- __init__.py
---  ….		# тут будуть файли нашого open source проекту
setup.py		# файл з dependencies

Вміст setup.py:
from setuptools import setup, find_packages

setup(name='some_project',
      version='1.0',
      packages=find_packages(),
      include_package_data=True,
      description='.......',
      author='…',
      license='Unlicense',
      install_requires=[
          'Keras==2.1.5',
          'tensorflow-gpu==1.6.0',
          'h5py==2.8.0',
          'numpy',
          'argparse',
          'Pillow',
          'matplotlib',
      ]) 

Для зберігання файлів нашого набору даних, який має обсяг декілька гігабайтів, використовуватимемо Google Cloud Storage. Створимо storage bucket за допомогою команди в консолі Google Cloud:

  gsutil mb -p [PROJECT_NAME] -c [STORAGE_CLASS] -l [BUCKET_LOCATION] -b on gs://[BUCKET_NAME]/ 
Де PROJECT_NAME — назва нашого проекту в Google Cloud.

STORAGE_CLASS бувають Multi-Regional Storage, Regional Storage, Nearline Storage і Coldline Storage. Докладніше про storage class можна почитати тут.

BUCKET_LOCATION — розташування вашого storage bucket — може бути:

Для свого прикладу я використав такі параметри: storageclass — coldline, region — us-east1.

Далі треба завантажити файли набору даних. Я використав VOC dataset.

Для копіювання цих файлів до Cloud Storage у консолі Google Cloud використаємо команду:
gsutil -m cp -R [SOURCE_LOCAL_LOCATION]gs://[BUCKET_NAME]

З іншими командами gsutil можна ознайомитися тут.

Далі бажано всі операції File I/O робити через Bucket I/O, який чудово зреалізували в модулі tensorflow.python.lib.io.

from tensorflow.python.lib.io import file_io  # for better file I/O
import io
from PIL import Image
 
def gs_open(path, mode='r'):
   return file_io.FileIO(path, mode)
 
def gs_file_exists(path):
   return file_io.file_exists(path)
 
def gs_copy_file(src, dest):
   if not file_io.file_exists(src):
        raise Exception("Src file doesn't exist at %s" % src)
   file_io.copy(src, dest, overwrite=True)
 
def gs_open_image(path):
   file = gs_open(path, "rb")
   image_data = file.read()
   file.close()
   return Image.open(io.BytesIO(image_data))

Для цього окремого прикладу — тренування YOLO — нам потрібно ще створити train-file, який містить шляхи до картинок з набору даних, координати об’єктів на них та їхній тип (клас):

path/to/img1.jpg   x11,y11,x12,y12,some_class_A   x21,y21,x22,y22,some_class_B …
path/to/img2.jpg   x11,y11,x12,y12,some_class_B   …
…….

Де x11, y11, x12, y12 — координати «прямокутника» шуканого об’єкта на фото, some_class — клас об’єкта (число, усі класи можна подивитися у файлі classes.txt).

Для автоматизації цього процесу в репозиторії є скрипт voc_annotation.py.

python voc_annotation.py  --voc_path gs://[YOUR_BUCKET_NAME]/VOCdevkit  --voc_classes_path model_data/voc_classes.txt

Результат — створений файл 2012_train.txt.

Ми одержали таку структуру файлів на Cloud Storage:

Крок 2. Створення ML Cloud Job

Для створення Cloud Job у консолі Google Cloud запустимо команду:

gcloud ai-platform jobs submit training ${job_name} --job-dir ${job_dir} \
 --python-version 3.5   \
 --runtime-version 1.9  \
 --package-path ./trainer           `# модуль trainer`  \
 --module-name trainer.train   `# файл train.py`    \
  --region ${region}     \
 --scale-tier BASIC_GPU   `# single NVIDIA Tesla K80 GPU` \
  --  `# Окремо параметри для train.py` \
  --weights_stage "${job_dir}/weights_stage_exported_tiny.h5"   `# Наша stage pre-trained model, яка повинна створитися наприкінці` \
 --weights_final "${job_dir}/weights_final_exported_tiny.h5"     `# Наша final pre-trained model, яку маємо створити наприкінці` \
 --anchors_file "gs://${bucket_name}/tiny_yolo_anchors.txt" \
 --annotation_file "gs://${bucket_name}/2012_train_tiny.txt" \
 --classes_file "gs://${bucket_name}/voc_classes_tiny.txt"

Щоб кожного разу не писати команду з параметрами, я вивів її в окремий bash-скрипт.

Якщо зазирнете в логи, то побачите, що значення loss із кожною epoch дещо зменшується:

Найкраще значення loss — це близьке до нуля.

Після закінчення тренування знайдемо наші pre-trained моделі тут:

Повний код можна подивитися в цьому репозиторії.

Крок 3. Завантаження моделі на мобільному пристрої

Розглянемо декілька можливостей:

  • Core ML (iOS/Mac);
  • Metal Performance Shaders (iOS/Mac).

3.1 Core ML

Для завантаження через Core ML потрібно конвертувати нашу модель до відповідного формату.

Для Keras (*.h5):

#!/usr/bin/env python
importcoremltools
your_model = coremltools.converters.keras.convert('your_model.h5', input_names=['image'], output_names=['output'], image_input_names='image')
your_model.save('your_model_name.mlmodel')

Для TensorFlow (*.pb, *.proto):

import tfcoreml as tf_converter
 
tf_converter.convert(tf_model_path='my_model.pb',
                    mlmodel_path='my_model.mlmodel',
                    input_name_shape_dict=input_tensor_shapes,
                    output_feature_names=output_tensor_names)

Де input_tensor_shapes — це форма вхідного N-розмірного масиву, у разі YOLO v3 (tiny): [416, 416, 3], формат запису — [height, width, rgb values].

А output_tensor_names — це назви значень output, у прикладі YOLO це output1, output2, output3, де output1 shape = [13, 13], output2 shape = [26, 26], output3 shape = [52, 52].

Під час додавання моделі Core ML у проект Xcode автогенерується клас Yolov3. Проглянувши його реалізацію, ми бачимо, як і звідки завантажується наша модель.

Як ми бачимо, вона завантажується з директорії Yolov3.mlmodelc (з app bundle), де зберігаються файли, зокрема model.espresso.net (структура моделі), model.espresso.weights (ваги).

Треба зазначити, що файли моделі не зашифровані, тож їх просто можуть «украсти» для використання в іншому застосунку.

Якщо ви хочете дізнатися, як правильно шифрувати й розшифровувати ML-моделі, я можу написати для цього окрему статтю :)

Для того щоб наша модель опрацювала картинку, треба викликати цей метод з параметром CVPixelBuffer:

Одержати CVPixelBuffer з відеопотоку камери можна за допомогою цього методу в делегаті AVCaptureVideoDataOutputSampleBufferDelegate:

Треба також зазначити, що якщо ви берете CVPixelBuffer з AVCaptureSession (AVFoundation), то він має формат кольорової моделі RGB.

Але якщо ви берете CVPixelBuffer з [ARFrame.capturedImage] (ARKit), то він матиме вже формат YUV.

Ось що може «бачити» ваша нейромережа, коли замість очікуваного RGB ви завантажили в її YUV:

Отже, це може негативно позначитися на результатах якості розпізнавання. Тому раджу завжди конвертувати до відповідного формату, а також до відповідних розмірів зображення.

У YOLO input shape дорівнює [416, 416, 3], тому висота й ширина зображення має бути 416.

Докладніший приклад використання Core ML окремо для YOLOv3 можна знайти в цьому репозиторії.

Core ML може працювати як лише на CPU, так і на GPU. GPU-реалізацію створили на основі Metal Performance Shaders.

3.2 Metal Performance Shaders

Metal Performance Shaders містить колекцію високооптимізованих обчислювальних і графічних шейдерів, розроблених для того, щоб просто та ефективно інтегрувати у ваш застосунок Metal. Вони спеціально налаштовані, щоб скористатися унікальними апаратними характеристиками кожного виду GPU для забезпечення оптимальної обчислювальної потужності.

Якщо ви розглянете стек-трейс майбутнього виконання вашої моделі в Core ML, можете побачити там класи внутрішнього C++ ML-фреймворка Apple з назвою Espresso й Metal Command Buffer.

Далі поясню, як можна працювати з Metal Command Buffer.

Обчислювальний граф нашої моделі створюємо послідовно за допомогою обчислювальних нод.

SomeNode1, SomeNode2, … SomeNodeN — класи нод. Є input- і output-ноди. Серед наявних класів нод присутні MPSCNNPoolingMaxNode (max pooling), MPSCNNConvolutionNode (convolution), MPSCNNNeuronReLU (ReLU activation) тощо.

Граф нод у нашій ML-моделі можна подивитися за допомогою python через keras.utils.plot_model (для Keras).

Наприклад, граф моделі YOLO, яку ми розглядаємо — такий.

device (MTL Device) — інтерфейс Metal для GPU, який можна використовувати для графіки й паралельних обчислень.

Щоб працювати з вхідними даними в GPU-CPU shared memory space, їх треба підготувати для цього за допомогою конвертації в MTLTexture й далі через створення MPSImage.

Після виклику [MPSNNGraph.executeAsync] весь наш граф кодується в MTL Command Buffer для виконання вже на GPU.

У [MPSNNGraph.executeAsync] ми можемо одержати outputImage у вигляді MPSImage, з якої можна вже скопіювати масив вихідних значень.

Докладніший приклад використання Metal Performance Shaders окремо для YOLO можна знайти в цьому репозиторії.

Якщо ви не хочете, щоб вашу ML-модель використовував хтось сторонній і міг просто інтегрувати в чужий застосунок, краще застосовувати безпосередньо Metal Performance Shaders, ніж Core ML. Але щоб розглянути всі методи захисту, краще присвятити цьому окрему статтю.

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось1
До обраногоВ обраному1
LinkedIn



5 коментарів

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

Хороша стаття, дякую!
А на скільки вигідніше по часу / грошам використовувати cloud провайдери аніж свій GPU?

СвоЇ потужності виходять набагато більше дешевшими, але за умови постійного їх використання. Дуже непогану машину для deep learning можна зібрати за 4-5 тис. доларів, і користуватися нею ви будете скоріш за все декілька років. За ті ж 4-5 тис. можна 2-3 місяці використовувати cloud з тими самими потужностями в режимі нон-стоп.

З мінусів свого власного заліза можна назвати ненульовий maintenance cost, в який входять як мінімум ваш (або когось іншого) час на збірку, встановлення/оновлення софту і так далі.

Да, cloud — это дорого. Имеет смысл только если сделать нужно что-то очень быстро (например, если нужно резко что-то натренировать на 8-16 GPU), а персональных ресурсов под это нет. Ну или если есть уверенность, что это разовое дело, и при этом довольно кратковременное.

По поводу бюджетов: конечно, что-то сносное для большинства задач можно учить на машине с одной 1080, которая обойдется в максимум 1к со всем обвесом. Но, для полноценной разработки уже неплохо бы иметь хотя бы 2, а то и 4 1080ti карты, каждая из которых обойдется уже в 500-1000 долларов (смотря где и как брать).

Підписатись на коментарі