Градиентно спускане
Накратко: Градиентното спускане намира теглата на модела стъпка по стъпка: смята накъде расте грешката и мести теглата малко в обратната посока, колкото казва скоростта на учене.
Проблем
Най-добрата права, без гадаене
Колегите от урока за линейната регресия избраха права на око. Сега складът иска програмата сама да намери най-добрата. Точна формула има само за правата, а следващите модели (невронните мрежи) нямат такава. Трябва начин, който работи винаги.
Задачата засега е по-малка: складът е известен (6 часа), а търсим само колко часа добавя всеки 100 km, w. Разстоянието е в стотици km, за да са числата малки:
# разстояние в стотици km (0.3 е 30 km) и часове до доставкатаDIST = [0.3, 0.5, 0.8, 1.0, 1.3, 1.6, 2.0, 2.4, 2.7, 3.0]HOURS = [10, 11, 17, 17, 23, 24, 31, 33, 40, 44]B = 6 # складът: 6 часа, известно еNUDGE = 0.0001Грешката е същата MSE като в линейната регресия, вече само функция на w:
# MSE на правата часове = w · разстояние + 6def loss(w): total = 0 for x, hours in zip(DIST, HOURS): error = w * x + B - hours total += error * error return total / len(DIST)zip(DIST, HOURS) върви по двата списъка едновременно: на всяка стъпка дава едно разстояние и часовете му.Можем да пробваме w = 0, 1, 2, … до 100 и да вземем най-малката грешка. С едно w това става. С 10 тегла са 100¹⁰ проби, а една невронна мрежа има милиони тегла.
Идеята
Побутни и виж
Вместо да пробваш всичко, застани на едно w и питай: ако мръдна малко надясно, грешката пада ли или расте?
before = loss(0)after = loss(0.01)print(f"w = 0: грешка {before:.2f}")print(f"w = 0.01: грешка {after:.2f}")print(f"наклон: {(after - before) / 0.01:.2f}")w = 0: грешка 485.00w = 0.01: грешка 484.21наклон: -78.97Побутването с 0,01 намали грешката с 0,79. Разделено на побутването, това е наклонът: −78,97, с колко се мени грешката на единица w. Минусът значи „надясно е надолу“.
С по-малко побутване (0,0001) наклонът е по-точен. Ето го на четири места:
# наклонът: побутни w и виж с колко се мени грешкатаdef slope(w): return (loss(w + NUDGE) - loss(w)) / NUDGEfor w in [0, 6, 12, 18]: print(f"w = {w}: грешка {loss(w):.2f}, наклон {slope(w):.2f}")w = 0: грешка 485.00, наклон -79.00w = 6: грешка 127.21, наклон -40.26w = 12: грешка 1.83, наклон -1.53w = 18: грешка 108.87, наклон 37.21Наклонът казва две неща. Знакът е посоката: минус значи, че w трябва да расте, плюс, че трябва да намалее (при w = 18 вече сме подминали дъното). Големината е колко сме далеч: −79 далеч от дъното, −1,53 почти на дъното.
Идеята
Крачка срещу наклона
Правилото е един ред: w -= скорост · наклон. Срещу наклона, защото наклонът сочи нагоре. С отрицателен наклон това увеличава w, с положителен го намалява. Колкото по-стръмно, толкова по-голяма крачката.
Едно минаване (сметни наклона, направи крачка) е епоха. Повтаряме:
w = 0rate = 0.1for epoch in range(1, 11): w -= rate * slope(w) print(f"епоха {epoch}: w = {w:.2f}, грешка {loss(w):.2f}")епоха 1: w = 7.90, грешка 62.36епоха 2: w = 10.70, грешка 9.28епоха 3: w = 11.69, грешка 2.61епоха 4: w = 12.04, грешка 1.77епоха 5: w = 12.17, грешка 1.67епоха 6: w = 12.21, грешка 1.65епоха 7: w = 12.23, грешка 1.65епоха 8: w = 12.23, грешка 1.65епоха 9: w = 12.24, грешка 1.65епоха 10: w = 12.24, грешка 1.65Първата крачка е голяма (от 0 до 7,90), защото кривата е стръмна. Към дъното наклонът намалява, а с него и крачките: от епоха 7 нататък w почти не мърда. Грешката 1,65 е по-малка от 1,83 на правата, която колегите избраха на око.
Това е градиентното спускане: стъпка по стъпка надолу по наклона на грешката, докато не стигнеш дъното. „Градиент“ е наклонът, когато параметрите са повече от един.
Виж го
Топката на кривата
def slope(w): return (loss(w + NUDGE) - loss(w)) / NUDGE
w = 0rate = 0.1for epoch in range(1, 11): w -= rate * slope(w)def slope(w): return (loss(w + NUDGE) - loss(w)) / NUDGE
w = 0rate = 0.1for epoch in range(1, 11): w -= rate * slope(w)def slope(w): return (loss(w + NUDGE) - loss(w)) / NUDGE
w = 0rate = 0.01for epoch in range(1, 11): w -= rate * slope(w)def slope(w): return (loss(w + NUDGE) - loss(w)) / NUDGE
w = 0rate = 0.35for epoch in range(1, 11): w -= rate * slope(w)Стъпка по стъпка, с думи
- Отдолу: грешката за всяко w; топката тръгва от w = 0. Отгоре: правата с това w.
- Побутни w малко: грешката пада. Наклонът е -79,00: надолу е надясно.
- Една стъпка: w -= 0,1 · (-79,00) и w става 7,90. Срещу наклона, тоест надолу.
- Всяко минаване е епоха. Към дъното наклонът намалява, а с него и стъпките. След 10: w = 12,24.
- Скорост 0,01: сивата топка пълзи. След 10 епохи е едва на w = 5,96.
- Скорост 0,35: червената топка прескача дъното, всеки път по-високо. Грешката расте: разлива се.
- С 0,1 топката стои на дъното: w = 12,24, грешка 1,65. Никой не е гадал w: то е намерено.
Отдолу е грешката за всяко w, а топката е текущото w. Отгоре е правата с това w върху поръчките: когато топката стигне дъното, правата минава през точките. Сивата и червената топка са същото правило с друга скорост.
Идеята
Скоростта на учене
Числото, по което се умножава наклонът, е скоростта на учене (learning rate). То решава колко дълга е крачката. Три скорости, по 20 епохи (w след първите три и след двадесетата):
for rate in [0.01, 0.1, 0.35]: w = 0 seen = [] for epoch in range(1, 21): w -= rate * slope(w) if epoch <= 3 or epoch == 20: seen.append(f"{w:.2f}") print(f"скорост {rate:.2f}: w = {', '.join(seen)}, грешка {loss(w):.2f}")скорост 0.01: w = 0.79, 1.53, 2.22, 9.02, грешка 35.14скорост 0.10: w = 7.90, 10.70, 11.69, 12.24, грешка 1.65скорост 0.35: w = 27.65, -7.18, 36.69, -1224.61, грешка 4938139.70- 0,01 е твърде малка. Посоката е правилна, но след 20 епохи w е едва 9,02. Стига дъното, но след стотици епохи.
- 0,1 е точна. Три крачки и w е близо, после се успокоява на 12,24.
- 0,35 е твърде голяма. Първата крачка прескача дъното до 27,65. Там наклонът е още по-голям, следващата крачка е още по-дълга и w лети до −7,18, после до 36,69. Грешката става милиони: спускането се разлива.
Добрата скорост не се смята, а се пробва. Обичайно е да започнеш с 0,1 или 0,01 и да гледаш грешката по епохи: ако расте или скача, скоростта е голяма; ако пада едва-едва, е малка.
Идеята
w и b заедно
Досега b беше известно. Сега го търсим и него: всеки параметър има свой наклон (побутваш само него), и всеки прави своя крачка в една и съща епоха.
# MSE на правата часове = w · разстояние + bdef loss(w, b): total = 0 for x, hours in zip(DIST, HOURS): error = w * x + b - hours total += error * error return total / len(DIST)w, b, rate = 0, 0, 0.1for epoch in range(1, 1001): slope_w = (loss(w + NUDGE, b) - loss(w, b)) / NUDGE slope_b = (loss(w, b + NUDGE) - loss(w, b)) / NUDGE w -= rate * slope_w b -= rate * slope_b if epoch in (1, 10, 100, 1000): print(f"епоха {epoch}: w = {w:.2f}, b = {b:.2f}, грешка {loss(w, b):.2f}")епоха 1: w = 9.77, b = 5.00, грешка 29.77епоха 10: w = 12.21, b = 6.03, грешка 1.66епоха 100: w = 12.41, b = 5.65, грешка 1.62епоха 1000: w = 12.41, b = 5.64, грешка 1.62След 1000 епохи w = 12,41 часа на 100 km и b = 5,64 часа. Формулата за най-добрата права от линейната регресия дава 0,1241 часа на km и 5,64 часа: същата права. Спускането не знае формулата; то само върви надолу.
Идеята
Мащабът на признаците
Защо разстоянието беше в стотици km? Ето същите поръчки в km:
# същите поръчки, но разстоянието е в kmKM = [30, 50, 80, 100, 130, 160, 200, 240, 270, 300]HOURS = [10, 11, 17, 17, 23, 24, 31, 33, 40, 44]NUDGE = 0.0001Със скорост 0,1 спускането се разлива от първата епоха:
w, b, rate = 0, 0, 0.1for epoch in range(1, 4): slope_w = (loss(w + NUDGE, b) - loss(w, b)) / NUDGE slope_b = (loss(w, b + NUDGE) - loss(w, b)) / NUDGE w -= rate * slope_w b -= rate * slope_b print(f"скорост 0.1, епоха {epoch}: w = {w:.0f}")скорост 0.1, епоха 1: w = 977скорост 0.1, епоха 2: w = -6304921скорост 0.1, епоха 3: w = 40699231079Наклонът по w расте с големината на признака: всяко w се умножава по 300 вместо по 3, и при w = 0 наклонът е почти −10 000. Тогава намаляваме скоростта, докато спре да се разлива:
w, b, rate = 0, 0, 0.00002for epoch in range(1, 1001): slope_w = (loss(w + NUDGE, b) - loss(w, b)) / NUDGE slope_b = (loss(w, b + NUDGE) - loss(w, b)) / NUDGE w -= rate * slope_w b -= rate * slope_b if epoch in (10, 100, 1000): print(f"епоха {epoch}: w = {w:.4f}, b = {b:.4f}, грешка {loss(w, b):.2f}")епоха 10: w = 0.1513, b = 0.0013, грешка 9.44епоха 100: w = 0.1513, b = 0.0063, грешка 9.42епоха 1000: w = 0.1510, b = 0.0563, грешка 9.28Сега не се разлива, но b почти не мърда: след 1000 епохи е 0,06 вместо 5,64, а w стои на 0,151 вместо на 0,124. За b крачката е твърде малка. Една скорост не става и за двете. Решението е мащабът: признаците се привеждат в близък обхват преди обучението (тук делене на 100; често се вади средното и се дели на разсейването). Без това нито една скорост не върши работа.
Пробвай
Предвиди изхода
w е 2, наклонът там е −20, а скоростта е 0,1. Какво ще отпечата една стъпка?
w = 2slope = -20rate = 0.1w -= rate * slopeprint(f"{w:.1f}")0,1 · (−20) = −2, а w -= −2 добавя 2: w става 4,0. Отрицателен наклон значи, че w расте. 0,0 би било крачка по наклона (нагоре), а −18,0 е изваждане на наклона без скоростта.
Пробвай
Попълни
Кое действие прави крачката срещу наклона?
for epoch in range(1, 11):
w rate * slope(w)-=: изваждаме скоростта по наклона. Наклонът сочи накъде грешката расте, а ние искаме обратното.
Пробвай
Намери грешката
Колега пише спускането, но грешката по епохи расте все по-бързо. На кой ред е грешката?
Избери реда с грешката.
+= прави крачка по наклона, тоест нагоре по кривата. Трябва -=: срещу наклона.
Идеята
Кога спускането боли
- Грешна скорост. Твърде голяма се разлива, твърде малка пълзи. Ако грешката по епохи расте или скача, първо намали скоростта.
- Признаци в различен мащаб. km и брой пратки в една права: наклоните се различават стотици пъти и нито една скорост не става. Мащабирай признаците преди обучението.
- Не само една долина. Грешката на правата е една гладка купа и дъното е едно. При невронните мрежи кривата има много долини и плата: спускането стига някое дъно, не непременно най-ниското, и резултатът зависи от началното w.
- Кога да спреш. Тук епохите са зададени. На практика се спира, когато грешката престане да пада, а дали моделът е добър за нови поръчки се мери отделно (оценката на модела).
- Цената. Всяка епоха минава през всички данни. С милиони примери и милиони тегла обучението върви часове на специални сървъри; тази инфраструктура (машини за обучение, пускане на модела в продукция) се учи в DevOps курса ZeroToOps.
По-точно, за любопитните
Наклонът по формула. Побутването е приближение. За MSE наклонът има точна формула (производната): 2 · средното на грешка · разстояние. Ето двата начина един до друг:
# наклонът по формула: 2 · средното на грешка · разстояниеdef slope_by_formula(w): total = 0 for x, hours in zip(DIST, HOURS): error = w * x + B - hours total += error * x return 2 * total / len(DIST)for w in [0, 12]: print(f"при w = {w}: с числа {slope(w):.2f}, по формула {slope_by_formula(w):.2f}")при w = 0: с числа -79.00, по формула -79.00при w = 12: с числа -1.53, по формула -1.53Еднакви са до втория знак. Формулата е по-бърза (един проход вместо два за всеки параметър), затова библиотеките смятат производните автоматично по кода на модела. Това е обратното разпространение от урока за обучението на мрежа.
Градиентът. При много параметри наклоните им заедно са градиентът (вектор). Стъпката е същата за всеки: параметър -= скорост · неговият наклон.
Части от данните. С милиони примери една епоха е бавна. Затова наклонът се смята на малка случайна част (mini-batch) и крачките са много повече. Това е стохастичното градиентно спускане (SGD). Подобрените му версии (например Adam) пазят и „инерция“ на предишните крачки.
Упражнение
Куриерът, научен сам
Реши задачата в редактора си.
В урока за линейната регресия колегите сравниха три прави за цената на куриера. Сега програмата да намери най-добрата сама:
# пратки: тегло в kg и цена на куриера в €KG = [0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0]EUR = [4.10, 4.60, 5.00, 5.60, 6.40, 7.50, 8.30, 9.40]NUDGE = 0.0001Напиши функцията на грешката (MSE) за цена = w · kg + b, после 1000 епохи градиентно спускане за w и b заедно, от w = 0 и b = 0, със скорост 0,05 и побутване 0,0001. Отпечатай w, b и грешката с по 4 знака.
w = 0.9538, b = 3.6204, MSE 0.0038Още задачи
- Скоростта. Пусни със скорост 0,1. Какво става? Намери най-голямата скорост, при която грешката още пада.
- Спиране. Вместо 1000 епохи спри, когато и двата наклона са по-малки от 0,001 по абсолютна стойност. След колко епохи?
- Сравнение. Колко по-малка е грешката от най-добрата права на колегите (A, MSE 0,0144)?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Общото за всички: атрибути на класа · Списъци: append, remove и sort · Какво е машинно учене · Линейна регресия
- Продължава в: Класификация: измама или не · Оценка: точността лъже · Преобучение: моделът, който наизустява · Обратно разпространение
- Задачи по темата: Как учи модел