Ei bine, puteți spune că, dacă doriți să rulați modelul NN pe un dispozitiv iOS, atunci CoreML este cea mai bună soluție. Da, așa este, când sunteți gata să utilizați .mlmodel. Principalul beneficiu al CoreML este că folosește Neural Engine în unele cazuri. Este extrem de rapid, dar foarte limitat. Neural Engine este utilizat nu pentru multe straturi și nu este foarte util pentru rețelele recurente. În cazul meu, ar putea accelera doar stratul Conv1d care nu ar face prea multe diferențe. De asemenea, CoreML poate utiliza GPU și alternativă la CPU. Dar înainte de a vă putea folosi modelul în CoreML, trebuie să-l exportați din ceva. Și de aici începe durerea.

m-am gândit

Am folosit tensorflow 2 pentru antrenamentul modelului meu și m-am gândit că ar fi destul de simplu să export modelul către CoreML, dar nu a făcut-o. În primul rând, am încercat pachetul principal coremltools python de la Apple. Hmm…. Ne pare rău, dar încă nu acceptă tf 2! Am folosit tf2 Keras API și m-am gândit că aș putea folosi Kerasand tf 1 și toate ar trebui să fie bune, deoarece coremltools acceptă Keras. Dar nu a făcut-o. Mi-am exportat cumva modelul; cu toate acestea a fost total inutilizabil.

Apoi am încercat să găsesc ceva pentru a exporta modelul tf2, dar fiecare script găsit nu funcționa normal. Apoi am aflat că TFLite avea delegat CoreML, ceea ce însemna că putea folosi CoreML cumva sub capotă. Apoi am decis să omit CoreML și treceți la TFLite.

Exportul modelului către TFLite este mult mai simplu, dar are și unele probleme.

Permiteți-mi să vă arăt toate capcanele pe care le-am găsit în timpul exportării modelului meu către TFLite și inferență pe un dispozitiv iOS:

  1. Pentru a exporta un model care va crea un interpret pe dispozitiv fără erori, trebuie să utilizați un nou convertor experimental.

Când am folosit convertorul implicit, a creat încă un subgraf gol, iar Interpretul de pe iOS nu a reușit să creeze.

2. Nu prea știu de ce, dar cu un model pe care l-am avut, nu l-aș putea rula cu ObjC normal pod. Întotdeauna a revenit nul la obținerea tensoarelor de intrare/ieșire. Mi-am rulat rețeaua cu succes numai folosind TensorFlowLiteC API 0.0.1 pe noapte. Nu am testat versiunea Swift de când am folosit ObjC în proiectul curent.

Bine, rețeaua mea a funcționat cu succes, dar a fost lentă pentru procesarea în timp real. După cum am menționat mai devreme, TFLite are delegat CoreML și delegat GPU și am crezut că aș putea face inferențe mai repede. Ei bine, mi-am asamblat rețeaua cu CoreML Delegate care rezervă pe GPU Delegate cu dispozitivul TFLite modelul nu era super cool, am decis să-mi montez rețeaua folosind Metal. Poate că GPU ar trebui să ruleze mai repede.

Pentru asamblarea NN-ului meu cu Metal, Nu am scris toate umbrele de la zero, deoarece Apple a creat o mulțime de straturi NN în cadrul Metal Performance Shaders. Aveam un spectrogram shader, așa că părea un plan bun. Mi-am corectat codul stratului de spectrogramă, așa că a fost aliniat cu MPS și am scris și stratul Conv1d. Am uitat că aș putea simula Conv1d cu Conv2d. Dar chiar și după ce mi-am amintit acest lucru, am decis să îl păstrez. Apropo, iată o repo cu aceste umbrere.

techpro-studio/MetalAudioShaders

Pentru a rula exemplul de proiect, clonați repo-ul și executați mai întâi instalarea podului din directorul Exemplu. MetalAudioShaders este ...

github.com

Toate umbrele rămase au fost implementate de Apple. Am fost foarte fericit când am aflat că stratul GRU a fost dezvoltat de Apple. Am fost dezamăgit mai târziu, dar mai întâi de toate. Ei bine, mi-am asamblat rețeaua strat cu strat, am analizat rezultatele și le-am comparat cu modelul Keras. MPS este foarte enervant, deoarece este axat pe imagini. De exemplu, shader-ul pentru BatchNorm acceptă numai MPSImage, dar umbrele mele (Spectro, Conv1d, matrici folosite) și stratul GRU funcționează mai bine cu matricile (în ceea ce privește documentul Apple), așa că a trebuit să copiez matricea în imagine și invers. Oricum, toate au funcționat bine, până m-am blocat cu stratul GRU.

Nu am înțeles de ce, dar a dat rezultate nevalide de fiecare dată. Pentru a fi sincer, nici măcar nu am înțeles cum să trimit date corect la acest strat, deoarece nu a existat niciun exemplu de utilizare corectă a acestuia. De asemenea, nu este deloc o problemă googlabilă, deoarece chiar și pe site-ul oficial al dezvoltatorului Apple, nu veți găsi documente despre straturile recurente în MPS. Toate documentele sunt doar în cod. Mai târziu am găsit un mic exemplu în sesiunea WWDC când au folosit stratul LSTM și am fixat datele de intrare la GRU, dar totuși a dat rezultate greșite. Am decis să implementez stratul GRU pe CPU pentru o mai bună înțelegere. Ar trebui să mă ajute să găsesc o soluție despre cum să configurez corect stratul GRU în MPS.

Am decis să folosesc cadrul Accelerate pentru asamblarea stratului GRU, deoarece folosește matematică vectorizată. Înainte de a începe implementarea, am căutat o formulă de GRU pe google și am deschis mai întâi Wikipedia rusă, deoarece este limba mea maternă. M-am uitat la formulă și am aflat că era ușor diferită de ceea ce am învățat la Coursera. Iată o versiune „Russian Wiki”.

Apoi am deschis Wiki-ul englezesc și am aflat că formula a fost exact aceeași pe care am învățat-o.

Dacă le priviți cu atenție, veți găsi diferența la final. Varianta Rusiei Wiki are „porți de ieșire răsturnate” și am crezut că este doar o greșeală în formulă. De când am aflat că formula Wiki engleză a fost aceeași cu cea pe care am învățat-o pe Coursera, am decis să implementez această variantă. Ei bine, când mi-am implementat stratul GRU, am văzut că produce rezultate diferite de Keras. În scopul testării, am folosit matrici mici, deoarece rezultatele sunt mai ușor de văzut. Ei bine, după câteva ore de depanare pas cu pas în codul meu și prin imprimarea tensoarelor, am aflat unde este problema. Ai vreo idee? Problema a fost în cele din urmă, în „porțile răsturnate”. A fost doar ridicol. M-am aliniat cu varianta „Russian Wiki”, iar stratul meu GRU a început să dea aceleași rezultate ca în Keras. Apoi mi-am amintit că MPSGRUDescriptor are variabila „flipOutputGates”. Haha, are o variabilă pentru această cârjă cu o formulă diferită.

Apoi am remediat în cele din urmă toate lucrurile din jurul stratului MPS GRU, deci a fost la fel ca în implementarea mea și Keras. Anterior, am uitat să descriu și funcțiile de activare. Am crezut că le folosesc pe cele implicite tanh și sigmoid, dar nu. Trebuie să descrieți funcțiile de activare în straturi MPS recurente. Rețineți acest lucru. Am fost emoționat și m-am gândit că voi alerga, iar MPS îmi va oferi același rezultat. Dar nu a fost! Încă mi-a dat ceva diferit.

Am decis să folosesc caracteristica „Code Level Support”. Lasă-i să depaneze acel shader. Am creat un exemplu de cod atât cu Accelerate, cât și cu Metal și le-am trimis o cerere. După aceea, am decis să îmi implementez rețeaua folosind straturi auto-realizate.

M-am uitat la biblioteca BNNS. Are un API bun, așa că am decis să implementez același lucru în C. Aveam un filtru de spectrogramă de mare viteză din rnd-ul anterior, așa că primul meu strat pentru NN era gata. L-am aliniat cu BNNS-like și am continuat să mă mișc strat cu strat. Următorul strat a fost Conv1d. Mi-am amintit că Conv1d ar putea fi simulat cu operația Conv2d. Am decis să încerc filtrul BNNSConvolution, am configurat totul, dar nu a funcționat: D Nu știu de ce, dar acel filtru nu a funcționat. După aceea, m-am gândit că poate este bine că am implementat stratul Conv1d pentru Metal, deoarece, de asemenea, nu ar putea funcționa potențial. Nu l-am testat, deoarece sunt prea leneș: D Ei bine, păstrez filtrul BNNSConvolution în coșul de gunoi și mi-am implementat propriul folosind .

Mi-am implementat NN folosind straturi personalizate pas cu pas. Totul a mers bine până m-am blocat pe stratul GRU. Da, stratul ăla minunat de GRU din nou. Când am scris un script pentru exportul de greutăți, am aflat că uneori părtinirea avea o formă (n,) și alteori (2, n). Nu am aflat de ce a avut (2, n), deoarece a fost adăugat o singură dată în formulă. După depanarea Keras, uitându-mă la PyTorch, am aflat în cele din urmă că acest strat avea implementare v2, de la PyTorch:

După ce am analizat formula PyTorch, este destul de înțeles de ce prejudecățile au formă (2, n). În cele din urmă, mi-am reparat stratul GRU și, pas cu pas, mi-am asamblat rețeaua.

Am fost extrem de fericit când am asamblat NN folosind straturile făcute de mine. Am creat o bibliotecă pe Github. Simțiți-vă liber să îl folosiți și să-l contribuiți. Apropo, GRU are steaguri v2 și flipOutputGates, astfel încât să puteți rula toate implementările posibile. Iată un repo al bibliotecii mele.

techpro-studio/NNToolkitCore

Biblioteca C cu filtre NN. GRU, BatchNorm, Dense, Conv1d, Activare. Implementat pe Apple's Accelerate. GitHub este acasă ...

github.com

De asemenea, am fost impresionat de performanța Accelerate. Procesarea unui buffer audio de 5 secunde cu 16 kHz, float32 pe mac-ul meu a durat în jur de 2-3 ms. Pe iPhone 11, a durat 5-6 ms. Apoi am comparat acest rezultat cu TFLite: Accelerate deduce mai repede de 6-7 ori. Puteți spune că sunt nebun, 35 ms nu este lent și de ce am început toate acestea. Dar anterior, am folosit diferiți hiperparametri pentru rețeaua mea. Am folosit 44 Khz 10 secunde, 196 nuclee conv1d în loc de 46 și 128 de unități în GRU în loc de 64. Ca urmare, am avut 0,4 secunde pe IPhon 8. De asemenea, puteți spune că aș putea configura acele hiperparametre în loc să le asamblați cu Metal și scriindu-mi propriile straturi. Poate! Dar îmi place experiența pe care am obținut-o cu toată rahatul prin care am trecut. De asemenea, biblioteca mea poate rula pe un watchOS, ceea ce este absolut minunat.

De asemenea, am primit un răspuns de la Apple cu privire la stratul GRU. Au spus că ar trebui să creez un raport de erori: D Fapt este că acest strat a fost adăugat la MPS acum 3 ani, dar nu l-au remediat. Poate că nu le pasă de asta și poate că am fost primul care a aflat acea eroare.