Частина 1. CoreML

Ну, ви можете сказати, що якщо ви хочете запустити модель NN на пристрої iOS, то CoreML є найкращим рішенням. Так, це правильно, коли ви готові до використання .mlmodel. Основна перевага CoreML полягає в тому, що в деяких випадках він використовує Neural Engine. Це надзвичайно швидко, але дуже обмежено. Neural Engine використовується не для багатьох шарів, і це не дуже корисно для періодичних мереж. У моєму випадку це потенційно може пришвидшити лише рівень Conv1d, що не матиме великої різниці. Крім того, CoreML може використовувати графічний процесор і резервний для CPU. Але перш ніж ви зможете використовувати свою модель в CoreML, вам потрібно експортувати його з чогось. І тут починається біль.

висновок

Я використовував tensorflow 2 для навчання своєї моделі, і я думав, що було б досить просто експортувати модель до CoreML, але це не так. Перш за все, я спробував основний пакет python coremltools від Apple. Хм ... Вибачте, але він ще не підтримує tf 2! Я використовував tf2 Keras API і думав, що можу використовувати Kerasand tf 1, і все повинно бути добре, оскільки coremltools підтримує Keras. Але цього не сталося. Я якось експортував свою модель; однак це було абсолютно непридатним для використання.

Потім я спробував знайти щось для експорту моделі tf2, але кожен скрипт, який я знайшов, не працював нормально. Потім я дізнався, що TFLite мав делегат CoreML, що означало, що він міг використовувати CoreML якось під капотом. Тоді я вирішив пропустити CoreML і перейдіть до TFLite.

Експорт моделі в TFLite набагато простіше, але у нього також є деякі проблеми.

Дозвольте мені показати вам усі підводні камені, які я знайшов під час експорту своєї моделі в TFLite та умовивід на пристрої iOS:

  1. Щоб експортувати модель, яка створить інтерпретатор на пристрої без помилок, потрібно використовувати експериментальний новий перетворювач.

Коли я використовував перетворювач за замовчуванням, він створив ще один порожній підграф, і Interpreter на iOS не вдалося створити.

2. Я насправді не знаю, чому, але з моєю моделлю, яку я мав, я не міг запустити її із звичайним стручком ObjC. Він завжди повертав нуль при отриманні вхідних/вихідних тензорів. Я успішно запустив свою мережу, лише використовуючи API TensorFlowLiteC 0.0.1 щоночі. Я не тестував версію Swift, оскільки використовував ObjC у поточному проекті.

Гаразд, моя мережа працювала успішно, але вона оброблялася повільно. Як я вже згадував раніше, TFLite має CoreML Delegate і GPU Delegate, і я подумав, що можу зробити висновок швидше. Що ж, я зібрав свою мережу за допомогою CoreML Delegate, що було використано на GPU Delegate із пристроєм TFLite модель не була супер крутою, я вирішив зібрати свою мережу за допомогою Metal. Можливо, графічний процесор повинен запустити його швидше.

За збірку мого НН з металом, Я писав не всі шейдери з нуля, оскільки Apple створила багато NN-шарів у рамках Metal Performance Shaders. У мене був шейдер Spectrogram, тому це звучало як хороший план. Я виправив свій код шару спектрограми, тому він був суміщений з MPS, а також написав шар Conv1d. Я забув, що можу моделювати Conv1d за допомогою Conv2d. Але навіть після того, як я це згадав, вирішив зберегти. До речі, ось репо з цими шейдерами.

techpro-studio/MetalAudioShaders

Щоб запустити приклад проекту, клонуйте репо та запустіть спочатку установку pod з каталогу Example. MetalAudioShaders - це ...

github.com

Всі шейдери, що залишились, були реалізовані Apple. Я був дуже радий, коли дізнався, що рівень GRU розроблений Apple. Пізніше я був розчарований, але перш за все. Ну, я зібрав свою мережу шар за шаром, подивився результати та порівняв їх із моделлю Кераса. MPS дуже дратує, оскільки орієнтована на зображення. Наприклад, шейдер для BatchNorm приймає лише MPSImage, але мої шейдери (Spectro, Conv1d, використані матриці) і рівень GRU працюють краще з матрицями (щодо документації Apple), тому мені потрібно було скопіювати матрицю на зображення і навпаки. У будь-якому випадку, все працювало добре, поки я не застряг у шарі GRU.

Я не розумів, чому, але це щоразу повертало недійсні результати. Чесно кажучи, я навіть не розумів, як правильно надсилати дані на цей шар, оскільки не було прикладу правильного їх використання. Крім того, це зовсім не проблема, яку можна шукати, оскільки навіть на офіційному веб-сайті розробника Apple ви не знайдете жодних документів про періодичні шари в MPS. Усі документи лише в коді. Пізніше я знайшов один невеликий приклад у сесії WWDC, коли вони використовували рівень LSTM, і я зафіксував вхідні дані в GRU, але він все одно повернув неправильні результати. Я вирішив застосувати рівень GRU на ЦП для кращого розуміння. Це повинно допомогти мені знайти рішення про те, як правильно налаштувати рівень GRU у MPS.

Я вирішив використовувати фреймворк Accelerate для складання шару GRU, оскільки він використовує векторизовану математику. Перш ніж розпочати впровадження, я шукав формулу GRU в google і спочатку відкрив російську Вікіпедію, оскільки це моя рідна мова. Я подивився формулу і виявив, що вона дещо відрізняється від того, що я дізнався на Coursera. Ось версія “російської Вікі”.

Потім я відкрив англійську Вікі і з’ясував, що формула точно така ж, як я її вивчив.

Якщо ви уважно на них поглянете, зрештою ви виявите цю різницю. Варіант російської Вікі має "перевернуті вихідні ворота", і я вважав, що це просто помилка у формулі. Оскільки я дізнався, що формула англійської Вікі така ж, як я дізнався на Coursera, я вирішив застосувати цей варіант. Ну, коли я впровадив свій рівень GRU, я побачив, що він дав інші результати, ніж Keras. Для тестування я використовував малі матриці, оскільки результати легше бачити. Ну, а потім, після кількох годин покрокової налагодження мого коду та друку тензорів, я з’ясував, в чому проблема. У вас є ідеї? Зрештою, проблема полягала в “перекиданні воріт”. Це було просто смішно. Я вирівнявся з варіантом “російська Вікі”, і мій рівень GRU почав повертати ті самі результати, що й у Keras. Тоді я згадав, що MPSGRUDescriptor має змінну “flipOutputGates”. Ха-ха, у нього є змінна для цього милиці з іншою формулою.

Потім я нарешті зафіксував усі речі навколо рівня MPS GRU, так що це було абсолютно так само, як у моїй реалізації та Keras. Раніше я також забув описати функції активації. Я думав, що в ньому використовуються типові та сигмоподібні, але це не так. Вам потрібно описати функції активації в періодичних шарах MPS. Майте це на увазі. Я був схвильований і думав, що буду балотуватися, і MPS дасть мені той самий результат. Але ні! Це все-таки дало мені щось інше.

Я вирішив скористатися функцією “Підтримка рівня коду”. Нехай вони налагодять цей шейдер. Я створив приклад коду як з Accelerate, так і з Metal і надіслав їм запит. Після цього я вирішив реалізувати свою мережу за допомогою саморобних шарів.

Я подивився на бібліотеку BNNS. У нього хороший API, тому я вирішив застосувати те саме в C. У мене був високошвидкісний фільтр спектрограми з попереднього rnd, тому мій перший шар для NN був готовий. Я вирівняв його з BNNS-подібним і продовжував рухатися шар за шаром. Наступним шаром був Conv1d. Я згадав, що Conv1d можна імітувати за допомогою Conv2d. Я вирішив спробувати фільтр BNNSConvolution, я все налаштував, але це не спрацювало: D Не знаю чому, але цей фільтр не спрацював. Після цього я подумав, що це добре, що я реалізував шар Conv1d для Metal, оскільки він також не міг працювати потенційно. Я не тестував його, оскільки мені дуже лінь: D Ну, я просто зберігаю цей фільтр BNNSConvolution у кошику та реалізував свій власний за допомогою .

Я реалізував свою NN, використовуючи власні шари поетапно. Все пройшло добре, поки я не застряг на шарі GRU. Так, цей чудовий шар GRU знову. Коли я писав сценарій для експорту ваг, я виявив, що іноді ухил мав форму (n,), а іноді (2, n). Я не зрозумів, чому він мав (2, n), оскільки його додали лише один раз до формули. Після налагодження Keras, переглядаючи PyTorch, я нарешті з’ясував, що цей шар має реалізацію v2 від PyTorch:

Подивившись на формулу PyTorch, стає цілком зрозумілим, чому упередження має форму (2, n). Нарешті, я виправив свій рівень GRU, і покроково зібрав свою мережу.

Я був надзвичайно щасливий, коли збирав NN, використовуючи власні шари. Я створив бібліотеку на Github. Не соромтеся користуватися цим та вносити його. До речі, GRU має прапори v2 та flipOutputGates, тому ви можете запускати всі можливі реалізації. Ось репо моєї бібліотеки.

techpro-studio/NNToolkitCore

Бібліотека C з NN-фільтрами. GRU, BatchNorm, щільний, Conv1d, активація. Реалізовано на Apple Accelerate. GitHub вдома ...

github.com

Крім того, мене вразив виступ Accelerate. Обробка звукового буфера 5 секунд з частотою 16 кГц, float32 на моєму Mac зайняла близько 2–3 мс. На iPhone 11 це зайняло 5–6 мс. Потім я порівняв цей результат з TFLite: прискорюйте висновки швидше в 6–7 разів. Можна сказати, я божевільний, 35 мс - це не повільно, і чому я все це почав. Але раніше я використовував різні гіперпараметри для своєї мережі. Я використовував 44 кГц 10 секунд, 196 ядер conv1d замість 46 і 128 одиниць в GRU замість 64. В результаті у мене було 0,4 секунди на IPhon 8. Крім того, ви можете сказати, що я міг би налаштувати ці гіперпараметри замість того, щоб збирати з Metal і написання власних шарів. Можливо! Але мені подобається досвід, який я отримав із усім тим лайном, яке я пройшов. Крім того, моя бібліотека може працювати на watchOS, що абсолютно здорово.

Крім того, я отримав відповідь від Apple щодо рівня GRU. Вони сказали, що я повинен створити звіт про помилку: D Цікаво, що цей шар був доданий до MPS 3 роки тому, але вони не виправили його. Можливо, їм це байдуже, і, можливо, я був першим, хто виявив цю помилку.