Градиентно спускане
Накратко: Градиентното спускане намира теглата на модела стъпка по стъпка: смята накъде расте грешката и мести теглата малко в обратната посока, колкото казва скоростта на учене.
Проблем
Най-добрата права, без гадаене
Колегите от урока за линейната регресия избраха права на око. Сега складът иска програмата сама да намери най-добрата. Точна формула има само за правата, а следващите модели (невронните мрежи) нямат такава. Трябва начин, който работи винаги.
Задачата засега е по-малка: складът е известен (6 часа), а търсим само колко часа добавя всеки 100 km, w. Разстоянието е в стотици km, за да са числата малки:
// разстояние в стотици km (0.3 е 30 km) и часове до доставкатаconst DIST = [0.3, 0.5, 0.8, 1.0, 1.3, 1.6, 2.0, 2.4, 2.7, 3.0];const HOURS = [10, 11, 17, 17, 23, 24, 31, 33, 40, 44];const B = 6; // складът: 6 часа, известно еconst NUDGE = 0.0001;Грешката е същата MSE като в линейната регресия, вече само функция на w:
// MSE на правата часове = w · разстояние + 6function loss(w) { let sum = 0; for (let i = 0; i < DIST.length; i++) { const error = w * DIST[i] + B - HOURS[i]; sum += error * error; } return sum / DIST.length;}Можем да пробваме w = 0, 1, 2, … до 100 и да вземем най-малката грешка. С едно w това става. С 10 тегла са 100¹⁰ проби, а една невронна мрежа има милиони тегла.
Идеята
Побутни и виж
Вместо да пробваш всичко, застани на едно w и питай: ако мръдна малко надясно, грешката пада ли или расте?
const before = loss(0);const after = loss(0.01);console.log(`w = 0: грешка ${before.toFixed(2)}`);console.log(`w = 0.01: грешка ${after.toFixed(2)}`);console.log(`наклон: ${((after - before) / 0.01).toFixed(2)}`);w = 0: грешка 485.00w = 0.01: грешка 484.21наклон: -78.97Побутването с 0,01 намали грешката с 0,79. Разделено на побутването, това е наклонът: −78,97, с колко се мени грешката на единица w. Минусът значи „надясно е надолу“.
С по-малко побутване (0,0001) наклонът е по-точен. Ето го на четири места:
// наклонът: побутни w и виж с колко се мени грешкатаfunction slope(w) { return (loss(w + NUDGE) - loss(w)) / NUDGE;}for (const w of [0, 6, 12, 18]) { console.log(`w = ${w}: грешка ${loss(w).toFixed(2)}, наклон ${slope(w).toFixed(2)}`);}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, с положителен го намалява. Колкото по-стръмно, толкова по-голяма крачката.
Едно минаване (сметни наклона, направи крачка) е епоха. Повтаряме:
let w = 0;const rate = 0.1;for (let epoch = 1; epoch <= 10; epoch++) { w -= rate * slope(w); console.log(`епоха ${epoch}: w = ${w.toFixed(2)}, грешка ${loss(w).toFixed(2)}`);}епоха 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 на правата, която колегите избраха на око.
Това е градиентното спускане: стъпка по стъпка надолу по наклона на грешката, докато не стигнеш дъното. „Градиент“ е наклонът, когато параметрите са повече от един.
Виж го
Топката на кривата
function slope(w) { return (loss(w + NUDGE) - loss(w)) / NUDGE;}
let w = 0;const rate = 0.1;for (let epoch = 1; epoch <= 10; epoch++) { w -= rate * slope(w);}function slope(w) { return (loss(w + NUDGE) - loss(w)) / NUDGE;}
let w = 0;const rate = 0.1;for (let epoch = 1; epoch <= 10; epoch++) { w -= rate * slope(w);}function slope(w) { return (loss(w + NUDGE) - loss(w)) / NUDGE;}
let w = 0;const rate = 0.01;for (let epoch = 1; epoch <= 10; epoch++) { w -= rate * slope(w);}function slope(w) { return (loss(w + NUDGE) - loss(w)) / NUDGE;}
let w = 0;const rate = 0.35;for (let epoch = 1; epoch <= 10; epoch++) { 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 (const rate of [0.01, 0.1, 0.35]) { let w = 0; const seen = []; for (let epoch = 1; epoch <= 20; epoch++) { w -= rate * slope(w); if (epoch <= 3 || epoch === 20) seen.push(w.toFixed(2)); } console.log(`скорост ${rate.toFixed(2)}: w = ${seen.join(", ")}, грешка ${loss(w).toFixed(2)}`);}скорост 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 · разстояние + bfunction loss(w, b) { let sum = 0; for (let i = 0; i < DIST.length; i++) { const error = w * DIST[i] + b - HOURS[i]; sum += error * error; } return sum / DIST.length;}let w = 0, b = 0;const rate = 0.1;for (let epoch = 1; epoch <= 1000; epoch++) { const slopeW = (loss(w + NUDGE, b) - loss(w, b)) / NUDGE; const slopeB = (loss(w, b + NUDGE) - loss(w, b)) / NUDGE; w -= rate * slopeW; b -= rate * slopeB; if ([1, 10, 100, 1000].includes(epoch)) { console.log(`епоха ${epoch}: w = ${w.toFixed(2)}, b = ${b.toFixed(2)}, грешка ${loss(w, b).toFixed(2)}`); }}епоха 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:
// същите поръчки, но разстоянието е в kmconst KM = [30, 50, 80, 100, 130, 160, 200, 240, 270, 300];const HOURS = [10, 11, 17, 17, 23, 24, 31, 33, 40, 44];const NUDGE = 0.0001;Със скорост 0,1 спускането се разлива от първата епоха:
let w = 0, b = 0;const rate = 0.1;for (let epoch = 1; epoch <= 3; epoch++) { const slopeW = (loss(w + NUDGE, b) - loss(w, b)) / NUDGE; const slopeB = (loss(w, b + NUDGE) - loss(w, b)) / NUDGE; w -= rate * slopeW; b -= rate * slopeB; console.log(`скорост 0.1, епоха ${epoch}: w = ${w.toFixed(0)}`);}скорост 0.1, епоха 1: w = 977скорост 0.1, епоха 2: w = -6304921скорост 0.1, епоха 3: w = 40699231079Наклонът по w расте с големината на признака: всяко w се умножава по 300 вместо по 3, и при w = 0 наклонът е почти −10 000. Тогава намаляваме скоростта, докато спре да се разлива:
let w = 0, b = 0;const rate = 0.00002;for (let epoch = 1; epoch <= 1000; epoch++) { const slopeW = (loss(w + NUDGE, b) - loss(w, b)) / NUDGE; const slopeB = (loss(w, b + NUDGE) - loss(w, b)) / NUDGE; w -= rate * slopeW; b -= rate * slopeB; if ([10, 100, 1000].includes(epoch)) { console.log(`епоха ${epoch}: w = ${w.toFixed(4)}, b = ${b.toFixed(4)}, грешка ${loss(w, b).toFixed(2)}`); }}епоха 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. Какво ще отпечата една стъпка?
let w = 2;const slope = -20;const rate = 0.1;w -= rate * slope;console.log(w.toFixed(1));0,1 · (−20) = −2, а w -= −2 добавя 2: w става 4,0. Отрицателен наклон значи, че w расте. 0,0 би било крачка по наклона (нагоре), а −18,0 е изваждане на наклона без скоростта.
Пробвай
Попълни
Кое действие прави крачката срещу наклона?
for (let epoch = 1; epoch <= 10; epoch++) {
w rate * slope(w);
}-=: изваждаме скоростта по наклона. Наклонът сочи накъде грешката расте, а ние искаме обратното.
Пробвай
Намери грешката
Колега пише спускането, но грешката по епохи расте все по-бързо. На кой ред е грешката?
Избери реда с грешката.
+= прави крачка по наклона, тоест нагоре по кривата. Трябва -=: срещу наклона.
Идеята
Кога спускането боли
- Грешна скорост. Твърде голяма се разлива, твърде малка пълзи. Ако грешката по епохи расте или скача, първо намали скоростта.
- Признаци в различен мащаб. km и брой пратки в една права: наклоните се различават стотици пъти и нито една скорост не става. Мащабирай признаците преди обучението.
- Не само една долина. Грешката на правата е една гладка купа и дъното е едно. При невронните мрежи кривата има много долини и плата: спускането стига някое дъно, не непременно най-ниското, и резултатът зависи от началното w.
- Кога да спреш. Тук епохите са зададени. На практика се спира, когато грешката престане да пада, а дали моделът е добър за нови поръчки се мери отделно (оценката на модела).
- Цената. Всяка епоха минава през всички данни. С милиони примери и милиони тегла обучението върви часове на специални сървъри; тази инфраструктура (машини за обучение, пускане на модела в продукция) се учи в DevOps курса ZeroToOps.
По-точно, за любопитните
Наклонът по формула. Побутването е приближение. За MSE наклонът има точна формула (производната): 2 · средното на грешка · разстояние. Ето двата начина един до друг:
// наклонът по формула: 2 · средното на грешка · разстояниеfunction slopeByFormula(w) { let sum = 0; for (let i = 0; i < DIST.length; i++) { const error = w * DIST[i] + B - HOURS[i]; sum += error * DIST[i]; } return 2 * sum / DIST.length;}for (const w of [0, 12]) { console.log(`при w = ${w}: с числа ${slope(w).toFixed(2)}, по формула ${slopeByFormula(w).toFixed(2)}`);}при w = 0: с числа -79.00, по формула -79.00при w = 12: с числа -1.53, по формула -1.53Еднакви са до втория знак. Формулата е по-бърза (един проход вместо два за всеки параметър), затова библиотеките смятат производните автоматично по кода на модела. Това е обратното разпространение от урока за обучението на мрежа.
Градиентът. При много параметри наклоните им заедно са градиентът (вектор). Стъпката е същата за всеки: параметър -= скорост · неговият наклон.
Части от данните. С милиони примери една епоха е бавна. Затова наклонът се смята на малка случайна част (mini-batch) и крачките са много повече. Това е стохастичното градиентно спускане (SGD). Подобрените му версии (например Adam) пазят и „инерция“ на предишните крачки.
Упражнение
Куриерът, научен сам
Реши задачата в редактора си.
В урока за линейната регресия колегите сравниха три прави за цената на куриера. Сега програмата да намери най-добрата сама:
// пратки: тегло в kg и цена на куриера в €const KG = [0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0];const EUR = [4.10, 4.60, 5.00, 5.60, 6.40, 7.50, 8.30, 9.40];const 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)?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: static: общото за всички · Масиви: push, splice и sort · Какво е машинно учене · Линейна регресия
- Продължава в: Класификация: измама или не · Оценка: точността лъже · Преобучение: моделът, който наизустява · Обратно разпространение
- Задачи по темата: Как учи модел