В епичното приключение на аудио обработката и невронните мрежи с Джулия, днешната тема е RNNs! Ще разгледам как изградих първия си основен RNN в Julia с Flux.jl. Всичко това е в преследване на Требекийския проект.

всеки вход

Както описах в предишната си публикация, проектът, по който работя, се нарича Trebekian, където искам да разширя приложението за любопитни факти CLI на моя партньор, като гласът на Алекс Требек прочете въпросите на глас. Така се роди Trebekian.jl.

Днес научих как да използвам Flux (изцяло пакета от невронни мрежи на Джулия) за обучение на RNN, който има много проста задача: предоставяне на сумата от всички елементи в предоставения масив.

RNNs: Aside

Какво е RNN? Той означава „повтаряща се невронна мрежа“ - основно RNN е напълно свързана или плътна единица, която има състояние. Когато се подава последователност от входове, той извършва линейна операция (), но след това подава изхода като вход към следващия вход. Така че изходът във времевата стъпка е функция на входовете, теглата и отклоненията и изхода в момента .

Класическият RNN модул обикновено се изобразява така:

Където функцията наистина може да бъде всичко! В случая на класически “RNN” хората обикновено имат предвид линейна единица като с някакъв вид активираща функция.

Разбира се, има цяла област на изследване, посветена на изучаването на RNN и тяхната теория и приложение, но за целите на този проект няма да стигаме твърде надолу по заешката дупка (все още). Достатъчно е да се каже, че основната „повтаряща се” структура на RNN може да приеме много форми. Ако търсите повече материали за четене, разгледайте LSTM (които се използват за модели на последователност в последователност в компютърно зрение) и GRU (които се използват силно при обработката на аудио). Има много много други и ви насърчавам да направите някои от собствените си изследвания, за да научите повече.

Защо RNNs

За Trebekian искаме да използваме RNN, защото целта е да вземем последователност от данни (т.е. изречение) и да го превърнем в друга последователност (т.е. аудио). За да направим това, знаем, че ще ни трябва някакъв вид „скрито състояние“, което се предлага от повтарящ се модел. Ще бъде забавно да разбера какво точно ще работи за това приложение, но със сигурност знам, че това ще бъде повтарящ се модел!

Както винаги, за да науча повече по тази тема, започвам с прост пример, за който знам, че може да бъде решен чрез RNN. Тестовият случай, с който ще работим, е формулиран съвсем просто: като се въведе масив с променлива дължина, изчислете сумата от входовете. Това е наистина лесно за тестване, лесно за генериране на данни за обучение и е НАИСТИНА проста линейна функция, която може да бъде изразена от линейна единица. И така, започваме с всички машини!

Генериране на данни

Първо искаме да генерираме примерни данни за влак и тестове. В Джулия това е доста лесно да се направи:

Взимаме някои преки пътища, като генерираме само произволни малки масиви, които съдържат стойностите от 1 до 10 и с променлива дължина от 2 до 7. Тъй като задачата, която се опитваме да научим, е доста проста, ние я приемаме! За тестовите данни просто вземаме това, което вече сме генерирали, и умножаваме както векторите за обучение, така и резултатите от обучението по 2 - знаем, че пак ще работи! За да бъде опростено, ние генерираме същото количество данни за обучение и тестване - обикновено това няма да е така, но в тази ситуация, когато данните се получават лесно, ние ги приемаме!

Синтаксисът (v -> sum (v)). (Train_data) използва анонимна функция ((v -> sum (v))) и точният оператор, за да приложи тази функция за сумиране към всеки от масивите в нашите данни за обучение.

Създайте модела

След това искаме да създадем нашия модел. Това е част от „магията“ на машинното обучение, тъй като трябва да формулирате правилно модела си, или ще получите несетивни данни. С Flux със сигурност имаме достатъчно функции, за да започнем, така че моделът, с който избрах да започна, е този:

Това, което прави, е да се създаде единична линейна RNN единица, която взема по един елемент в даден момент и произвежда по един елемент за всеки вход. Искаме един вход към един изход, защото искаме да направим акумулатор - за всеки вход изходът трябва да съдържа сумата от него и всички предишни входове.

Моделът захранва изхода обратно към себе си, без (в този случай) да няма активираща функция, приложена към изхода (преди да бъде подадена обратно към себе си). Функцията за активиране по подразбиране е функция tanh във Flux, но тя изрязва изхода между -1 и 1, което не е добре, ако се опитвате да направите RNN за сумиране! Затова вместо това предоставяме RNN единица от Flux анонимна функция като активация, която не прави нищо на входа - той просто предава изхода директно напред към следващия блок. Това е доста нетипично за дизайна на невронна мрежа, но добрата част тук е, че знаем нещо за нашия проблем - знаем, че искаме машина за сумиране, така че знаем, че би било доста лесно да се научи без сложни функции за активиране, и в всъщност невъзможно с този по подразбиране! По-късно в тази публикация ще ви покажа какво се случва, ако се опитате да обучите това с функцията за активиране на tanh по подразбиране ...

Има цяла теория на функциите за активиране, в която няма да навлизам тук. Това е една от онези заешки дупки, в които може да се потопим по-нататък в пътешествието на Требекян, но не и в този момент!

Сега, за да оцените модела на поредица от входове, трябва да го извикате така:

Забележете точковото обозначение тук - тъй като нашият RNN взема само по един вход наведнъж, трябва да приложим RNN върху последователността от входове, които предоставяме един по един. След това, ако вземем последния елемент от изхода (след като е видял цялата последователност), очакваме да видим сумата от всички входове.

Влак! и Оценете

След като дефинирахме нашия модел, ние настроихме обучението и оценката. Това е може би най-малкото код, който някога съм използвал, за да създам обучение и оценка на всеки език, с който съм правил невронни мрежи ...

Сега единствената странност е тази част:

Има 2 важни неща, които трябва да се отбележат:

  1. Когато извикате RNN на входна последователност, той ще изведе изход за всеки вход (защото трябва да го подаде обратно към себе си). Така че, ако искате да направите „много към едно“ повече или модел, при който генерирате единичен изход за вход с променлива дължина, трябва да вземете последния елемент (в Julia, със синтаксиса [end]), който да използвате като изход. И отново използваме точковото обозначение, за да приложим модела, както обсъдихме по-горе.
  2. Трябва да се обадите на Flux.reset! (Simple_rnn) след всяко обаждане за преминаване напред/оценка. Тъй като RNN има скрито състояние, искате да сте сигурни, че няма да замърсявате бъдещи обаждания до RNN с това скрито състояние. Вижте тази страница с документация на Flux за повече информация.

По време на обучението използваме обратно извикване за оценка (ограничено на максимум 1/секунда), за да покажем изхода.

Функцията за загуба, която избрах за тази реализация, беше проста загуба на абсолютна разлика в стойността, за да бъде опростена. Подобно на функциите за активиране, има цяла теория на функциите за загуби и наистина зависи от вашия проблем, коя от тях е най-подходяща. В нашия прост случай ние го улесняваме!

Събирайки всичко, ето как изглежда изходът, след като стартираме този кодов фрагмент в обвивката на Julia:

И когато тестваме модела на някои входове, ето какво получаваме. Удивително е как направихме суматор RNN, който може да работи с отрицателни числа, дори когато в нашия набор от данни няма отрицателни числа!

Ние също искаме да проверим здравословното си състояние, като разгледаме директно параметрите. RNN от този тип трябва да има 3 параметъра: тегло за входа, тегло за входа от предишния стъпка на времето и пристрастие. Когато проверяваме параметрите на нашия модел, бихме очаквали, че двете тегла за входа (текуща и предишна) са едновременно 1 и че пристрастието е 0, точно както при суматор. За щастие, точно това имаме!

Ех! Направихме суматор!

По-горе посочих избора на модели като важна част от машинното обучение. Постоянно ми се напомня за това в ежедневната ми работа (правя компютърно зрение, софтуер, машинно обучение, анализ на данни за роботика) и отново ми беше напомнено за това тук. Преди да разгледам дефиницията на Flux на RNN, не разбрах, че функцията за активиране по подразбиране е tanh, която отрязва функцията до диапазона [-1, 1]. Изпълнение на същия код за обучение/оценка по-горе, но с този модел:

Дадоха някои фантастично лоши резултати:

Обърнете внимание как загубата не намалява. Ако оценим модела, който току-що обучихме на целия набор от тестови данни, виждате, че всичко има максималната стойност, която може да има - 1:

Тази подсказка ме накара да се потопя в изпълнението на Flux RNN, за да разбера как да доставя персонализирана (в случая не) функция за активиране.