Получете интуиция за разликата между оптимизаторите SGD vs RMSprop vs Adam

Когато хората за първи път започнат машинно обучение в PyTorch, може да видите скрипт на PyTorch като този.

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

Най-важният ред в целия този код (първият) обаче е скрит от целия абстрактен код на високо ниво, който PyTorch предлага, и въпреки че това прави кодирането на невронни мрежи лесно в дългосрочен план, това ни пречи да получим интуиция за това кои оптимизатори работят по-добре и какви са разликите между различните видове оптимизатори.

В тази статия просто ще обясня трите най-често срещани вида оптимизатори, ще покажа как се различават оптимизаторите и производителността и ще ви покажа как можете да кодирате оптимизаторите от нулата.

Съдържание

  1. Бързо опресняване на градиентно спускане (SGD)
  2. SGD с инерция
  3. RMSprop
  4. Адам

За всички наши примери ще разглеждаме или проста линейна регресия, или MNIST класификация на цифри

Ще започнем с бързо опресняване на градиентното спускане — не се колебайте да пропуснете, ако сте запознати.

Стохастичен градиентен спускане

Обяснението за този раздел идва от бележника SGD, който е част от втори урок от fast.ai v3 курса на Kaggle (име на бележника за търсене: fastai-v3-lesson-2-sgd)

Като се има предвид тази диаграма на разсейване, нашата цел е моделът за машинно обучение да възстанови двата параметъра на нашия регресионен модел: mи b.

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

Сега как всъщност правим тези промени в стойностите на загубите си? Чрез градиентно спускане.

Градиентът е производната на грешката по отношение на въпросния параметър. Нека разбием това.

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

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

Това е! Това е градиентно спускане. Нека вземем всичко, което обсъдихме, и го поставим в малка сбита, обобщаваща формула (за ситуации извън обикновената линейна регресия).

Сега нека опитаме да приложим това в електронна таблица на excel с градиентно спускане и да видим колко добре работи! За да работите заедно с мен, изтеглете електронната таблица с градиентно спускане от тази връзка:https://course.fast.ai/videos/?lesson=5.

Новите ни параметри са m = 30и b = 2.

Нека прегледаме тези данни за 4 епохи. Една епоха се определя от периода от време, след който нашият модел е наблюдавал всичкинаши точки от данни.

Нещо не е наред.

Какво ще кажете за увеличаване на степента на обучение алфа? Бихте си помислили, че това ще проработи, но води до грешки в електронната таблица поради това, което е известно като проблем с експлодиращия градиент — градиентите стават твърде големи и изведнъж достигаме стойности на грешка от 10¹⁵³ .

Импулс

Нека опитаме SGD с нещо, наречено моментум. Инерцията ни казва да изчислим стъпката (стойността, която изваждаме от параметрите) малко по-различно. Вместо да изваждаме производната по отношение на параметъра, умножен по скоростта на обучение, ще извадим (0,9 *предходната стъпка) + (0,1 * градиента).

По същество ние казваме на нашия модел да придава само 10% важност на градиента, който току-що изчислихме, и 90% важност на предишната стъпка, която моделът е направил спрямо параметрите.

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

Ще назовем нашия нов параметър импулс като β.

Сега нашата нова стъпка изглежда нещо като...

Ето електронната таблица след две епохи с импулс. В горния ляв ъгъл можете да видите, че резултатите ни дори не са по-добри от последната електронна таблица... Към следващото ни издание — RMSprop.

RMSprop

V(t) е това, което се нарича експоненциално претеглена пълзяща средна. Причината, поради която се нарича експоненциално претеглен, е, че (1-бета) * градиентният квадратен фактор му позволява да поддържа запис на всички предишни квадратни стойности на градиента.

Забележете, че v(t) е в знаменателя на стъпковата функция. Това създава обратна връзка, при която ако експоненциално претеглената пълзяща средна е по-голяма, тогава стъпката е по-малка, а ако средната е по-малка, стъпката е по-голяма.

Спомняте ли си, когато нашите параметри се актуализираха супер бавно в първата SGD електронна таблица? Сега, с RMSprop, нашите параметри ще се актуализират много по-бързо поради обратната връзка между градиента и размера на стъпката.

Виж това! С 2 епохи достигаме до наклон от 4,72 — не съвсем до наклона от 30, към който се стремим, но три пъти по-добър от наклона от 1,5, който получихме чрез SGD.

Предстои още едно подобрение...

Адам

Сега това, което ще направим, е да комбинираме идеите за инерция с принципа на RMSprop, включващ обратната връзка.

Ето изненада - Адам всъщност не е нищо ново. Мислете за това като комбинация между импулса и изпълнението на RMSprop.

За да опростите нещата за себе си, първо игнорирайте m(t) отгоре - забележете обратната връзка, запазена с v(t). Тъй като експоненциално претеглената пълзяща средна на градиентите намалява, стъпката се увеличава, за да се сближат по-бързо. Сега игнорирайте v(t) и погледнете m(t). Това е пряка връзка — ако предишната стъпка е голяма, тогава mt също ще бъде голяма, увеличавайки стойността на стъпката, тъй като е в числителя.

Ето го – малък v(t) и голям (m(t)) ще позволят промени в нашите параметри, които се случват много по-бързо от всички други оптимизатори, които сме разглеждали досега.

Ние сме в 2 епохи и...

b=29,25! — това е много, много близо до нашата цел от m=30.

a има леко превишаване, но също е приблизително близо до 2.

Реализации на код и графики на загуби в PyTorch за набор от данни на MNIST

Възпроизвеждането на тези реализации на оптимизаторите, които кодирах от нулата, трябва да ви помогне да придобиете по-задълбочено разбиране на включената математика.

Ето трите имплементации на код за моментум, RMSprop и Адам съответно поставени една до друга. Забележете елемента за намаляване на теглото, включен във всички примери на код. Намаляването на теглото може също да се нарече регуляризация на L2. Наказването на нашата функция за загуба за големи стойности на теглото ни позволява да обобщим по-добре към въображаем тестов набор.

Първо, нека се запознаем с модела - тук използваме проста мрежа с предварителна връзка с един линеен слой, наследен от модула nn.Linear. Има два параметъра. Първата е тегловна матрица с размери 784 на 10. Втората е вектор на отклонение с размери 1 на 10. Нашата цел е да изпробваме различните оптимизатори, за да видим кой може да минимизира загубата в най-краткия интервал от време (нашата загуба функция е MSE загуба)

Преди да кодирам оптимизаторите, инициализирах три параметъра, които участват в оптимизацията. Първият е prev_step — това е параметърът, който се умножава по константата на импулса бета по време на SGD с импулс. След това имаме prev_mo_step —това се използва за изчисляване на нашата нова m(t) стойност за оптимизатора на adam. И накрая, имаме prev_exp_avg_step — това е стойността на съхранената предишна експоненциално претеглена подвижна средна на квадратните градиенти (представени от v(t)).

SGD

Ето кода за SGD. Първо, ние изчисляваме прогнозираните стойности на y и прилагаме затихване на теглото, където добавяме същите градиенти на квадрат към нашата променлива w2. След това използваме loss.backward, за да изчислим градиентите (p.grad).

След като зададем torch.no_grad()==true, ние актуализираме нашите параметри p.

Изчислението на стъпка [i] е най-важният ред в този код. Забележете как умножаваме p.grad по (1-импулс) и умножаваме предишната стъпка по импулса.

След като изчислим стъпката, изваждаме скоростта на обучение, умножена по стъпката с оператора .sub_().

RMSprop

Накрая, след нулиране на градиента, задаваме предишната стъпка равна на нашата стъпка, която току-що изпълнихме, за да се подготвим за следващата актуализация.

Единственото нещо, което се различава между кода за всеки от тези оптимизатори, е входните аргументи и начина, по който тази стъпка [i] се изчислява.

В RMSprop нашата стъпка се изчислява в две стъпки. Първо трябва да получим експоненциално претеглената пълзяща средна v(t). Както се вижда в кода, ние повдигаме нашия градиент на квадрат, умножаваме го по (1-импулс) и след това вземаме нашата предишна претеглена средна стойност и умножаваме по импулса.

За да изчислим стъпката, вземаме градиента и разделяме на корена на този новоизчислен v(t) и накрая актуализираме нашите параметри. Подобно на SGD, трябва да зададем променлива, инициализираща предишната ни стъпка (prev_exp_avg_step), преди да върнем стойности на загуба.

Адам

Отново — няма разлика освен в това как се изчислява стъпката, освен че този път добавяме още един параметър mo_step. Стойностите на step[i] за adam се изчисляват подобно на RMSprop — все пак разделяме на корена на exp_avg_step. Този път обаче дивидентът е нашата mo_step[i]. Тук можете ясно да видите как Адам взема предвид нашите m(t) и v(t).

Аргументите momentum1 и 2 се използват за задаване на коефициентите за линейните комбинации, използвани за съставяне на стъпките impulum и exp_avg (в този случай използвах 0,9 и за двете).

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

Парцели за загуба

Ето едно упражнение — опитайте се да отгатнете към кой оптимизатор принадлежи всяка от графиките на загубите по-долу, като използвате вашето разбиране за скоростта, с която всеки оптимизатор се спуска.

По-долу са обозначените отговори:

Забележете как графиката на загубите за SGD всъщност не се сближава в оптимума до ~ 800 актуализации. Адам и RMSprop се събират приблизително по едно и също време (~250 актуализации), но забележете как спускането на Адам е малко по-стръмно.

Защо това има значение

Има много нюанси, включени в изпълнението на моделите за задълбочено обучение, от броя на скритите до слоевете до използвания оптимизатор до набора за скорост на обучение. Повечето начинаещи може да не сметнат за важно да научат за оптимизирането им първоначално, но колкото по-бързо научите тези нюанси — толкова по-бързо ще изненадате себе си с най-съвременните резултати.

Очаквайте скоро статия заоптимизиране на скоростта на обучение 👀.

Поздравления за Джеръми Хауърд, основател на библиотеката и курса fast.ai, на които се основава голяма част от тази статия. Изключително съм благодарен за стремежа ви да направите резултатите от най-съвременното задълбочено обучение достъпни за всички.

Хей! Аз съм Мукунд Мурти, 16-годишен, запален по пресечната точка между машинното обучение и откриването на лекарства. Благодаря, че прочетохте тази статия! Надявам се, че ви е било полезно :)

Чувствайте се свободни да разгледате моите други статии в Medium и да се свържете с мен в LinkedIn!

Ако искате да обсъдим някоя от темите по-горе, ще се радвам да се свържа с вас! (Изпратете ми имейл на [email protected] или ми изпратете съобщение в LinkedIn) Също така, не се колебайте да разгледате уебсайта ми на адрес mukundhmurthy.com.

Моля, запишете се за моя месечен бюлетин тук, ако се интересувате да следите напредъка ми :)