Перші кроки з ML.NET: як навчити машину розпізнавати об’єкти

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт усім, я Максим Павлов, Microsoft Solutions Consultant у компанії Ciklum. У цій статті хочу поділитися досвідом та порадами щодо початку роботи з фреймворком ML.NET. Це безкоштовна бібліотека машинного навчання (machine learning, ML) від Microsoft, створена для розширення екосистеми .NET можливостями машинного навчання. Ця бібліотека надає можливість тривіальних інтеграцій з існуючими ML-моделями та пропонує чимало інструментів для створення власних моделей.

Цей матеріал буде корисний .NET-ентузіастам, фахівцям з машинного навчання, які прагнуть розширити свій арсенал, і всім тим, хто цікавиться темою машинного навчання та шукає доступних можливостей для початку роботи з ML. Якщо ви .NET-розробник, після ознайомлення з матеріалами цієї статті, ви зможете написати С# код, який використовує ML.NET для вирішення певних задач машинного навчання — аналізу тексту для ідентифікації тону (sentiment analysis) та розпізнавання об’єктів (object detection). Ви навчитесь використовувати наявну, попередньо натреновану модель машинного навчання (YOLOv4), яка здатна розпізнавати об’єкти певних типів на будь-якому зображенні.

Трохи теорії

Машинне навчання (Machine learning) — підрозділ більш широкої галузі досліджень, що стосуються штучного інтелекту (AI). В той час як розробки в сфері AI взагалом спрямовані на те, щоб комп’ютери могли імітувати людські здатності, задача ML — надати змогу компьютерам краще виконувати певні завдання по мірі того як вони отримують більше матеріалів (даних) для аналізу, без прямого алгоритмічного програмування тих чи інших здібностей. Так, комп’ютер «навчається» коли виконує поставлені перед ним задачі.

В свою чергу, потрібно ввести ще один термін — глибинне навчання (deep learning). Deep learning є суб підрозділом машинного навчання. Його квінтесенцією є створення моделей глибинного навчання (deep learning models), які використовують кілька обчислювальних шарів (computational layers) у архітектурі моделі.

Щоб не забирати ваш час додатковим теоретичним матеріалом щодо ML, пропоную детальніше ознайомитися з цими термінами у відповідних статтях, наприклад почати з цієї статті у Вікіпедії. Утім, ґрунтовна теоретична підготовка не так вже й важлива, щоб почати експериментувати з глибинним навчанням на практиці — у чому і пропоную вам переконатися далі.

ML-моделі використовують вхідні дані (input data), котрі були їм доступні під час фази навчання (та були конвертовані у «ваги» нейроної мережі, вважайте ваги «конфігурацією поведінки моделі), щоб передбачати максимально-корректні результати (output data) на основі нових вхідних даних, яких модель «ще не бачила». Наприклад, якщо модель натренували на чималій кількості зображень котиків та прямо зазначали «це кіт, ось тут на зображенні», то їй можна дати будь яке нове зображення і, відносно впевнено, вона зможе «вгадати», чи є кіт на ній, чи ні, та де він, якщо є.

Фінальне «should know» — про різницю алгоритму та моделі машинного навчання:

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

Тож, ML-алгоритм + дані = ML-модель.

Якщо модель — це програма, то в якому вона форматі

Моделі машинного навчання можуть створюватись у різних форматах. Зазвичай, формат визначається програмним забезпеченням, як використовується для створення такої моделі. Найбільш популярними інструментами для створення ML-моделей є — TensorFlow, Keras, Caffe, Torch.

Існує і формат моделі, створений спеціально для кросплатформенного використання — ONNX (читається як «онікс»). ONNX — загальна платформа, більшість форматів ML-моделей можна конвертувати з та в ONNX. Можна й тренувати алгоритми одразу в ONNX модель.

Щоб розглянути можливості машинного навчання на практиці, візьмемо для демонстрації модель «комп’ютерного бачення» YOLOv4. Демо-проект в цій статті використовує цю популярну модель, яку розробники одразу пропонують у ONNX форматі. Але спочатку, пропоную розібрати «ази» ML.NET на більш тривіальному прикладі.

Достатньо простий приклад ML.NET

Якщо ви хочете спробувати самостійно «погратися» з ML.NET, пропоную спробувати нескладний приклад від Microsoft ось тут. Він проведе вас через інсталяцію залежностей для вашого першого ML.NET-проекту. Під кінець тьюторіала у вас буде автоматичного згенероване рішення з двома проектами: бібліотекою класів та консольним застосунком. У моєму випадку це виглядає так:

Просте автоматично згенероване рішення з прикладом реалізації алгоритму класифікації

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

В процесі створення проекту генерується кілька цікавих для нас класів. У цій статті, утім, ми не вдаватимемось в деталі щодо того, як створити чи натренувати модель, натомість сфокусуємось на її використанні. Переконаний, що кращий спосіб звикнути до нової технології — спочатку повикористовувати її, а вже потім розібратися, як вона влаштована та перейти до більш складних її аспектів.

ModelBuilder.cs в проекті SampleClassification.ConsoleApp — згенерований клас, який показує, як можна створити та зберегти модель на основі автоматично обраного набору ML-алгоритмів. Проект SampleClassification.Model містить інший клас під назвою ConsumeModel. Він пропонує функціональність, яку інші класи можуть «використати» для роботи зі згенерованою моделлю. Давайте розберемося детальніше, як воно працює.

  1. ConsumeModel — обгортковий (wrapper) клас, який має метод Predict.

Як ми побачимо, типове використання ML-моделі в коді включає визначення типів вхідних та вихідних даних, які дають кодові можливість презентувати передані моделі параметри та формат очікуваної відповіді, яку створює модель після опрацювання вхідних даних. Класи ModelInput та ModelOutput у згенерованому рішенні — якраз такі.

  1. Метод Predict «бере» об’єкт класу ModelInput в якості параметру, застосовує «магію ML» та повертає об’єкт класу ModelOutput.

Клас ModelInput виглядає наступним чином:

Перш ніж продовжити, давайте швидко розглянемо, для чого потрібні атрибути ColumnName та LoadColumn. Коли ML.NET запускає пайплайн перетворення даних (він робить це під час навчання нової моделі та використання існуючої), він використовує «віртуальный контейнер» для зберігання даних, які обробляються та передаються з одного кроку пайплайну в інший. Таким контейнером служить об’єк, що реалізує інтерфейс IDataView.

Реалізація інтерфейсу IDataView використовується в ML.NET, щоб зберігати як проміжні так і вихідні дані. Вважайте це класичною таблицею даних (як в SQL) у пам’яті, яка реалізує інтерфейс IDataView. Таблиця оперує стовпцями та рядками (класична таблична модель даних — tabular data model). Щоразу, коли нам потрібно примапити щось з IDataView до нашого POCO-об’єкта чи навпаки, ми використовуємо атрибути, щоб мати змогу «сказати ML.NET», із яким стовпцем потрібно працювати. [ColumnName («col0»), LoadColumn (0)] по суті означає «взяти значення з Property (в даному випадку «col0»), завантажити його в стовпець з індексом 0 (перший), і назвати його (стовпець) «col0» для подальшого використання механізмами ML.NET. Оскільки це вхідна модель, атрибути підказують ML.NET, як спарсити ці POCO-об’єкти в стандартні констракти IDataView.

Клас ModelOutput виглядає ось так:

У цьому контексті, [ColumnName("PredictedLabel«)] означає, що наша модель сконфігурована для виведення результату, а результат, який нас цікавить знаходиться у Property Prediction, розташований у стовпці PredictedLabel. Якщо зазирнути у ModelBuilder, який я тут детально не описуватиму, ми побачимо саме це в конфігурації моделі:

Тепер, коли ми розібралися з моделями Input та Output, давайте завершимо огляд ConsumeModel.cs. На початку класу ми маємо Property — PredictionEngine. Це lazy-loaded об’єкт типу PredictionEngine<ModelInput, ModelOutput>. Він використовується у методі Predict, який ми бачили раніше, щоб робити передбачення. Коли ви просуватиметеся в опануванні ML.NET, побачите, що це хоч і не обов’язкова, але дуже хороша практика загортання пайплайна ML.NET у сутність загального класу PredictionEngine. Він виконує функцію інкапсуляційного класу для всіх процесів, необхідних, щоб отримати передбачення від моделі.

Як і з усіма іншими lazy-loaded об’єктами, коли до нього звертаються вперше, викликається метод ініціалізації (у нашому випадку — CreatePredictionEngine).

Давайте швиденько поглянемо на його вміст:

Ось і він, святий грааль ML.NET — класс MLContext. Документація Microsoft надає стислий опис задачі цього класу:

MLContext подібний до RequestContext у ASP.NET, де «все, що нам потрібно для обробки веб-запиту» «монтується» на визначений об’єкт, з яким ми взаємодіємо. У ML.NET таким класом виступає MLContext.

Нині ж, коли у нас є «точка монтування» (інстанс MLContext), щоб створити механізм який робитиме прогнози, ми завантажуємо з файлу попередньо побудовану модель (викликаємо mlContext.Model.Load). На цьому етапі механізм прогнозування створено і повернуто (викликаємо mlContext.Model.CreatePredictionEngine <ModelInput, ModelOutput>).

Досить тривіально: усього 3 кроки — і у нас є обгортка, яка бере вхідні дані і повертає нам бажаний результат. Що ж, очевидно, це далеко не все. Прикладом «що ще» є ймовірності (наскільки модель впевнена у зробленому прогнозі). Але про це пізніше. Тепер давайте розглянемо, як це працює з візуальними даними, а не лише текстом.

Розпізнавання об’єктів з ML.NET

Комп’ютерна програма здатна взяти будь-який текст і спробувати визначити, чи має він позитивний або негативний характер. Вона також надає інформацію про те, наскільки вона «впевнена» у прогнозі. Це вже досить круто, але давайте подивимося, як ми можемо використовувати ML.NET для інспектування даних з камери та виявлення об’єктів «on the fly».

У нашому простому прикладі передбачення настрою тексту вище ми спиралися на екземпляр класу PredictionEngine <ModelInput, ModelOutput>, щоб робити прогнози. У разі задачі для комп’ютерного зору, в рамках якої ми хочемо виявляти об’єкти, ми могли б зробити те ж саме, але тоді, логічно, що ModelInput буде зображенням, а ModelOutput має містити інформацію про об’єкти, які були виявлені алгоритмом на наданому зображенні. Що ж, виявляється, саме так воно і може працювати.

Ось більш широка діаграма, яка містить .NET класи, залучені до «plug-and-play» імплементації ML.NET, яку ми розглянемо як приклад.

Проект-приклад, побудований на базі цієї архітектури класів, який ми будемо розглядати у цій статті далі можна знайти за цим посиланням на GitHub.

Перш ніж ми розберемо запропоновану ієрархію класів, дозвольте мені коротко пояснити термін YOLO та яку модель комп’ютерного зору ми будемо використовувати у нашому практичному прикладі.

У машинному навчанні YOLO — це варіація популярної абревіатури, яка позначає «Ви дивитесь лише раз» (You Only Look Once). Це назва алгоритму виявлення об’єктів, який здобув популярність завдяки своїй продуктивності та простоті. «Дивитесь лише раз» вказує на суттєвий аспект використовуваного алгоритму — виявлення об’єкта здійснюється за допомогою єдиного проходження через нейронну мережу. Передбачення щодо всього зображення відбувається під час одного запуску алгоритму, що означає збільшення продуктивності. Згорткова нейронна мережа (convolutional neural network) використовується для прогнозування одночасно ймовірностей наявності об’єктів різних класів та їх розташування на вхідному зображенні. YOLO також має значний потенціал для навчання.

Протягом останніх років мережа YOLO мала кілька версій. Ми зосередимось на «вже застарілому» YOLOv4 Олексія Бочковського. Можна легко завантажити версію ONNX цієї моделі з репозиторію ONNX на GitHub тут. (Він вам знадобиться, якщо ви хочете запускати мій демонстраційний проект локально).

Але повернемося до коду .NET.

Нашим кодом, який використовує YOLO (using code) може бути будь-що: веб чи десктопний застосунок, чи реалізація API. Допоки using code може надавати нашому детектору об’єктів зображення, ми повинні мати можливість виявляти та класифікувати об’єкти на ньому.

На діаграмі вище IPredictionProvider та YOLOPredictionProvider — це загальні класи-обгортки, синтаксичий сахар, який компактно пакує фактичну «суть» реалізації ML.NET, яка, як і наш перший демонстраційний проект, покладається на MLContext для виконання всієї роботи. Давайте розглянемо ці класи детальніше.

IPredictionProvider — інтерфейс, який буде використовуватись нашим using code’ом:

YOLOPredicionProvider — це реалізація IPredictionProvider. Істотна частина «налаштування» відбувається в конструкторі класу. Для належної перевірки заглянемо до початкового коду, назва файлу — YoloV4PredictionProvider.cs, рядки 33–63.

Розберемо код, який реалізує ініціалізацію та конфігурацію провайдера. Спочатку створюємо екземпляр MLContext. Наступний рядок призначає об’єкт типу EstimatorChain <OnnxTransformer> до замінної pipeline. Як випливає з назви, ми створюємо ланцюжок «естіматорів», які будуть обробляти вхідні дані, щоб отримувати вихід, який і буде нашим прогнозом.

Першим оцінювачем у пайплайні буде ResizingImageEstimator, клас, який бере зображення з IDataView (пам’ятаєте, ми зустріли його в попередньому прикладі?), змінює його розмір і розміщує зображення зі змінами у стовпці з іменем, вказаним у параметрі outputColumnName — тобто «input_1: 0» у нашому прикладі. Таким чином, він і бере, і повертає зображення (бітмап).

Наступним у рядку є ImagePixelExtractingEstimator, доданий за допомогою виклику .Append (mlContext.Transforms.ExtractPixels...). Це естіматор, який бере зображення та перетворює його на масив... вектор.

Остаточний і найскладніший оператор .Append додає OnnxScoringEstimator. Він фактично виконує основне навантаження — намагається ідентифікувати об’єкти на вхідному зображенні викликаючи роботу моделі YOLO. Пам’ятайте, що на цьому етапі зображення вже перетворюється на числовий вектор після проходження через логіку в ImagePixelExtractingEstimator..

Давайте розглянемо, як ми налаштовуємо OnnxScoringEstimator з параметрами:

shapeDictionary — це параметр конфігурації, який дозволяє користувачам моделі (нам, розробникам, у цьому випадку) вказати «розмірну конфігурацію» вхідних та вихідних даних моделі. Те, які формати підходять для конкретної моделі, можна дізнатися з її документації.

Параметри inputColumnNames та outputColumnNames дозволяють нам вказати ім’я з вхідних та вихідних даних після перетворення. Тут ми визначаємо найменування, яке буде використовуватися в IDataView. Нормальною практикою є досліджувати «сигнатуру» моделі Onnx за допомогою візуального інструменту аналізу моделей, такого як Netron, або через код. Netron візуалізує модель YOLOv4 Onnx, яку ми використовуємо, так:

Це може підказати нам, які назви вхідних даних та результатів модель очікує та видає. Добре якщо вони відповідають проекції в нашому IDataView. Деякі параметри в Netron можуть відображатися як unk_ - це означає невідомий маркер, який декодер не може обробити. Дуже часто існує необхідність в отриманні додаткової документації та прикладів використання від авторів моделі, оскільки, як показує приклад, не всі параметри «автоматично виявляються» за допомогою таких утиліт, як Netron.

Параметр modelFile очевидний: він дозволяє вказати, де модель .onnx знаходиться на диску, щоб наше середовище виконання ml.net могло її знайти та взаємодіяти з нею.

gpuDeviceId визначає, який GPU в системі використовувати під час роботи з моделлю, null у нашому випадку означає, що ми не маємо наміру використовувати GPU.

fallbackToCpu контролює, чи OnnxRuntime намагатиметься виконувати центральний процесор замість GPU.

Після створення EstimatorChain нам потрібно створити TransformerChain. Пам’ятайте, що кінцевою метою є правильно налаштований екземпляр PredictionEngine. Ми майже там.

Трансформатор отримується шляхом передачі вхідної моделі (класу, що описує вхідні параметри) в EstimatorChain під час виклику методу .Fit (...

Так ми «приміряємо» вхідну модель, яку ми маємо надати пайплайну оцінювачів, щоб підготувати його та отримати TransformerChain. Оцінювачі — це алгоритми, які беруть DataView та видають Трансформери (Transformer). Трансформер — це алгоритм, який бере інстанс DataView як вихідні дані, та видає оброблений DataView на виході. Наприклад, трансформер бере DataView з зображенням і виводить DataView з прогнозами.

YoloV4BitmapData — це клас, який буде частиною IDataView як частина «вхідних» даних.

Останнім кроком для створення PredictionEngine є виклик MLContext.Model.CreatePredictionEngine <TSrc, TDst> і надання як вхідних, так і очікуваних вихідних структур — знову ж таки, чистих класів C# у випадку ML.NET.

Давайте обговоримо, POCO об’єкти яких класів ми використовуємо як вхідні та вихідні дані в PredictionEngine.

З YoloV4BitmapData все очевидно:

Утім, із вихідним класом, YoloV4Prediction, — ні.

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

Якорі (anchors) — загальновідомий термін в сфері object detection. Їхнє використання не обмежується набором алгоритмів YOLO. Якорі визначають масштаб та форму, за допомогою якої алгоритм ML намагатиметься ідентифікувати об’єкти на вхідному зображенні. Точне налаштування якорів дозволяє моделі бути більш ефективною при прогнозуванні об’єктів. У нашому випадку ми просто знаємо, які якорі використовуються з YOLOv4, і використовуємо їх у типах .NET.

Величина кроку (stride) також є загальним поняттям, яке, у випадку YOLO, визначає «крок» між обмежувальними рамками. Оскільки виявлення об’єктів у YOLOv4 ґрунтується на рівні концентрації потенційних обмежувальних блоків, визначених кожним «предиктором» у моделі, конфігурація кроку також впливає на те, чи під час виявлення деякі об’єкти виявляються або пропускаються. У нашому «Класі що обробляє результати від YOLO» величина кроку потрібна для отримання обмежувальних рамок, які можна накласти на вхідне зображення.

Параметр XYScale використовується для того, щоб визначити реальні обмежувальні рамки об’єкта, який був виявлений, враховуючи, що вихідний прогноз з моделі спочатку не може бути зіставлений з вхідним зображенням. Оскільки ми хочемо намалювати межі виявленого об’єкта на вхідному зображенні, нам потрібно знати це значення для моделі.

Змінна shapes також визначена для YOLOv4 і використовується під час парсингу результатів.

GetResults класу YoloV4Prediction — це місце, де відбувається фактичний «парсинг» виводу моделі. Тому в кінці ми можемо зробити «звичайні обмежувальні рамки» для передбачених об’єктів доступними для коду, що визвав наші ML.NET-класи (using code).

Після того, як наш код у GetResults парсить «рідну для моделі» інформацію для передбачення, ми отримуємо досить чистий POCO — екземпляр класу YoloV4Result:

Завдяки властивостям цього класу ми можемо легко «використовувати» ці дані в застосунку. Він по суті повідомляє нам, де знаходяться виявлені об’єкти, яка мітка класу (який тип об’єкта виявляється) і наскільки впевнена модель щодо цього передбачення.

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

Висновки

ML.NET 1.6 — це вже досить зріла бібліотека для роботи з ML. Вона дозволяє нам автоматизувати різноманітні ML-задачі, дотримуватися роботи з керованим кодом і в останній версії навіть вибрати, чи хочемо ми прив’язати свою роботу до GPU або CPU.

Вихідний код для розглянутих прикладів:

Аналіз настрою тексту — репозиторій на GitHub

Виявлення об’єктів YOLOv4 — репозиторій на GitHub

👍НравитсяПонравилось7
В избранноеВ избранном5
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

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