Преобучение: моделът, който наизустява
Накратко: Преобучението (overfitting) е, когато моделът наизустява обучаващите данни и греши на нови; валидацията го хваща, а по-прост модел и регуляризация го лекуват.
Проблем
Перфектният модел, който обещава 5 минути
Магазинът обещава на клиента кога ще пристигне поръчката. Времето се учи от минали поръчки, както в първия урок за машинното учене: признакът е разстоянието до клиента, етикетът са минутите до вратата.
Колега пробва два модела върху 8 минали поръчки. Правата от линейната регресия греши с няколко минути. Неговата крива не греши нито веднъж:
line = fit(train, 1, 0)curve = fit(train, 7, 0)print(f"права (степен 1): MSE на обучението {mse(line, train):.2f}")print(f"крива (степен 7): MSE на обучението {mse(curve, train):.2f}")for km in [3.5, 21.5]: print(f"поръчка {km:.1f} км: права {predict(line, km):.0f} мин, крива {predict(curve, km):.0f} мин")права (степен 1): MSE на обучението 9.35крива (степен 7): MSE на обучението 0.00поръчка 3.5 км: права 24 мин, крива 5 минпоръчка 21.5 км: права 70 мин, крива 79 минКривата е с MSE 0,00: перфектна върху миналото. После идва нова поръчка на 3,5 км. Кривата обещава 5 минути, правата 24. Истинското време се оказва 22 минути. За поръчка на 21,5 км кривата казва 79, правата 70, а истината е 68.
Моделът, който никога не греши върху миналото, е най-лош за бъдещето. Това е преобучение (overfitting): моделът е наизустил примерите, заедно с шума им, вместо да научи зависимостта.
Виж го
Кривата от степен d
Правата е w₀ + w₁ · x. Кривата добавя степени на x: w₀ + w₁ · x + w₂ · x² + … + w₇ · x⁷. Това е полином от степен 7, с 8 тегла. Степен 1 е правата.
Поръчките са примерни, но наподобяват истински: по-далеч е по-дълго, с няколко минути шум (трафик, асансьор, чакане на вратата). Освен 8-те за обучение има 8 други за валидация и 8 за теста. Защо три групи, е в следващата карта.
# обучение: км до клиента и минути до врататаtrain = [(2, 19), (5, 23), (7, 38), (10, 42), (13, 52), (16, 54), (19, 63), (22, 69)]# валидация: други поръчки, само за избораval = [(3, 24), (6.5, 31), (9, 36), (12, 44), (15, 52), (18, 57), (21, 70), (4, 27)]# тест: пуска се веднъж, накраяtest = [(3.5, 22), (8, 34), (11, 38), (14, 51), (17, 59), (20, 63), (6, 30), (21.5, 68)]x е разстоянието, преместено около 12 км и смалено десет пъти. Иначе 22⁷ е около 2,5 милиарда и сметките губят точност.
# x е разстоянието около 12 км, смалено десет пътиdef powers(km, degree): x = (km - 12) / 10 p = [1.0] for j in range(1, degree + 1): p.append(p[j - 1] * x) return p
def predict(w, km): p = powers(km, len(w) - 1) total = 0.0 for j in range(len(w)): total += w[j] * p[j] return total
def mse(w, data): total = 0.0 for km, minutes in data: e = predict(w, km) - minutes total += e * e return total / len(data)Теглата можеш да намериш с градиентно спускане, но при степен 7 то върви милиони стъпки. Има пряк път: най-добрите тегла са решението на система уравнения, с толкова уравнения, колкото са теглата. Системата се решава с метода на Гаус, като в училище, само че с 8 неизвестни. Кодът е тук, за да е видимо всичко. Не е нужно да го следиш ред по ред:
# най-добрите тегла направо: система уравненияdef fit(data, degree, lam): m = degree + 1 a = [[0.0] * m for _ in range(m)] b = [0.0] * m for km, minutes in data: p = powers(km, degree) for i in range(m): b[i] += p[i] * minutes for j in range(m): a[i][j] += p[i] * p[j] # наказанието: λ по диагонала, без свободния член for i in range(1, m): a[i][i] += lam return solve(a, b)# методът на Гаус: решава a · w = bdef solve(a, b): n = len(b) for c in range(n): p = c for r in range(c + 1, n): if abs(a[r][c]) > abs(a[p][c]): p = r a[c], a[p] = a[p], a[c] b[c], b[p] = b[p], b[c] for r in range(c + 1, n): f = a[r][c] / a[c][c] for k in range(c, n): a[r][k] -= f * a[c][k] b[r] -= f * b[c] x = [0.0] * n for r in range(n - 1, -1, -1): s = b[r] for k in range(r + 1, n): s -= a[r][k] * x[k] x[r] = s / a[r][r] return xРедът с наказанието засега получава 0. Ще стигнем до него.
Идеята
Обучение, валидация и тест
В урока за оценката данните бяха две групи: обучение и тест. Сега трябва и трета. Ето защо: искаме да изберем степента. Ако я избираме по грешката на обучението, винаги печели 7, защото тя е 0. Ако я избираме по теста, тестът вече участва в решение и не е честен накрая.
Затова данните са три групи:
- обучение (training set): от него се учат теглата;
- валидация (validation set): по нея се сравняват моделите и се избира степента;
- тест (test set): пуска се веднъж, накрая, за избрания модел.
print("MSE: обучение / валидация")best, best_val = 1, float("inf")for degree in range(1, 8): w = fit(train, degree, 0) train_err = mse(w, train) val_err = mse(w, val) print(f"степен {degree}: " f"{train_err:.2f} / {val_err:.2f}") if val_err < best_val: best_val = val_err best = degreetest_err = mse(fit(train, best, 0), test)print(f"избрана степен: {best}, " f"тест: {test_err:.2f}")MSE: обучение / валидациястепен 1: 9.35 / 3.64степен 2: 6.74 / 9.35степен 3: 6.61 / 9.09степен 4: 5.64 / 11.95степен 5: 3.75 / 18.81степен 6: 3.30 / 29.06степен 7: 0.00 / 121.20избрана степен: 1, тест: 5.57Грешката на обучението пада с всяка степен: 9,35, 6,74, …, 0,00. Грешката на валидацията расте: 3,64, 9,35, …, 121,20. Колкото повече тегла има кривата, толкова по-добре помни 8-те точки и толкова по-зле познава нови. Валидацията избира степен 1, правата, а тестът ѝ е 5,57 (корен от 5,57 е около 2,4 минути средна грешка).
Едно нещо изглежда странно: правата е с по-малка грешка на валидацията (3,64), отколкото на обучението (9,35). При 8 поръчки случайността е голяма: валидацията просто е попаднала на поръчки, по-близки до правата. Затова при малко данни не се доверявай на втория знак.
Виж го
Кривата се гърчи
print("MSE: обучение / валидация")best, best_val = 1, float("inf")for degree in range(1, 8): w = fit(train, degree, 0) train_err = mse(w, train) val_err = mse(w, val) print(f"степен {degree}: " f"{train_err:.2f} / {val_err:.2f}") if val_err < best_val: best_val = val_err best = degreetest_err = mse(fit(train, best, 0), test)print(f"избрана степен: {best}, " f"тест: {test_err:.2f}")print("обучение / валидация, max |w|")best_lam, best_val = 0, float("inf")for lam in [0, 0.001, 0.01, 0.1, 1, 10]: w = fit(train, 7, lam) train_err = mse(w, train) val_err = mse(w, val) print(f"λ {lam:.3f}: {train_err:.2f} / " f"{val_err:.2f}, {biggest(w):.2f}") if val_err < best_val: best_val = val_err best_lam = lamtest_err = mse(fit(train, 7, best_lam), test)print(f"избрано λ: {best_lam:.3f}, " f"тест: {test_err:.2f}")Стъпка по стъпка, с думи
- 8 поръчки за обучение: разстоянието в км и минутите до вратата. По-далеч е по-дълго, с малко шум.
- Степен 1 е права. Пропуска повечето точки с няколко минути: MSE на обучението 9,35.
- Всяка степен добавя едно тегло. Кривата се огъва повече и се доближава до всяка точка от обучението.
- Степен 7 има 8 тегла за 8 точки и минава през всяка: MSE на обучението 0,00. Изглежда перфектно.
- Сега 8 други поръчки, за валидация. Между точките на обучението кривата отскача: MSE на валидацията 121,20.
- За всяка степен: грешката на обучението пада, на валидацията расте. Валидацията избира степен 1, правата.
- Друг лек: степен 7, но с наказание за големи тегла. С λ = 0,1 кривата се успокоява: MSE на валидацията 9,55.
- Кривата, която наизусти обучението, е най-лоша за нови поръчки. Тестът: 5,57 за правата, 9,96 за кривата с λ = 0,1.
Кривата от степен 7 уцелва всяка синя точка и пропуска оранжевите с десетки минути. Стълбчетата отдолу казват същото за всяка степен: синьото (обучението) пада, оранжевото (валидацията) расте.
Пробвай
Намери грешката
Колега избира степента така. Програмата му винаги избира 7. На кой ред е грешката?
Избери реда с грешката.
Грешката се мери върху train, данните, от които кривата се е учила. Там най-висока степен винаги е най-добра (при 7 грешката е 0). Изборът трябва да е по val: mse(w, val).
Идеята
Три лека
1. По-прост модел. Валидацията вече го избра: правата. Когато зависимостта е почти права, правата е достатъчна, а всяко тегло отгоре е място за шум.
2. Повече данни. Кривата от степен 7 има 8 тегла. С 8 точки може да мине точно през всяка. С 16 точки не може, затова трябва да мине между тях, а това изглажда шума:
both = train + valfor data in [train, both]: print(f"степен 7, {len(data)} поръчки: тест {mse(fit(data, 7, 0), test):.2f}")степен 7, 8 поръчки: тест 84.44степен 7, 16 поръчки: тест 9.41train + val е нов списък с редовете на двата.Тестът пада от 84,44 на 9,41. Тук за обучението ползваме и валидацията, затова изборът на степен вече няма с какво да се направи. В истински проект за това има кръстосана валидация (виж „По-точно“).
3. Наказание за големи тегла. Гърчещата се крива има огромни тегла: най-голямото е 571,59. Кривата уцелва всяка точка, защото огромни тегла с различни знаци почти се унищожават, а остатъкът от тях гърчи кривата. Регуляризацията добавя към грешката наказание: λ по сбора на квадратите на теглата. Сега обучението търси тегла, които и уцелват точките, и са малки. В кода това е единственият ред с наказанието във fit. λ се избира с валидацията, както степента:
# най-голямото тегло без свободния членdef biggest(w): return max(abs(x) for x in w[1:])print("обучение / валидация, max |w|")best_lam, best_val = 0, float("inf")for lam in [0, 0.001, 0.01, 0.1, 1, 10]: w = fit(train, 7, lam) train_err = mse(w, train) val_err = mse(w, val) print(f"λ {lam:.3f}: {train_err:.2f} / " f"{val_err:.2f}, {biggest(w):.2f}") if val_err < best_val: best_val = val_err best_lam = lamtest_err = mse(fit(train, 7, best_lam), test)print(f"избрано λ: {best_lam:.3f}, " f"тест: {test_err:.2f}")обучение / валидация, max |w|λ 0.000: 0.00 / 121.20, 571.59λ 0.001: 2.80 / 26.49, 36.79λ 0.010: 3.59 / 17.08, 19.16λ 0.100: 5.72 / 9.55, 20.21λ 1.000: 19.95 / 16.71, 13.35λ 10.000: 100.67 / 93.35, 4.92избрано λ: 0.100, тест: 9.96Последното число на всеки ред (max |w|) е най-голямото тегло по абсолютна стойност, без свободния член. С λ = 0,1 най-голямото тегло пада от 571,59 до 20,21, а валидацията от 121,20 до 9,55. Твърде силното наказание (λ = 10) смачква кривата почти до хоризонтална линия: грешката става голяма и на обучението (100,67). Това е обратното на преобучение, недообучение: моделът е твърде ограничен, за да хване зависимостта.
Тестът на избраното λ е 9,96: много по-добре от 84,44, но правата (5,57) пак е най-добра. Регуляризацията не прави от крива права, тя само не ѝ дава да полудее.
Пробвай
Попълни
Наказанието отива по диагонала на системата, без свободния член (i започва от 1). Какво се добавя?
# наказанието: λ по диагонала, без свободния член
for i in range(1, m):
a[i][i] += Добавя се λ. Да добавиш λ по диагонала е същото като да добавиш λ · (сбора на квадратите на теглата) към сбора на квадратите на грешките. Свободният член не се наказва, защото той само мести кривата нагоре или надолу (средното време), а не я гърчи.
Пробвай
Предвиди изхода
Грешка с наказание: MSE е 2, теглата са 3 и −4, λ е 0,1. Какво ще отпечата?
mse = 2.0w = [3, -4]lam = 0.1penalty = 0.0for x in w: penalty += x * xprint(f"{mse + lam * penalty:.2f}")Наказанието е λ · (3² + (−4)²) = 0,1 · 25 = 2,5. Квадратът прави −4 на 16, затова знакът не помага. Общо 2 + 2,5 = 4,50.
Идеята
Най-близкият съсед наизустява
Най-близките съседи от първия урок имат същата болест. С k = 1 моделът връща времето на най-близката минала поръчка. За поръчка от обучението най-близката е самата тя, затова грешката на обучението е 0:
# средното време на k-те най-близки поръчкиdef knn(data, km, k): used = [False] * len(data) total = 0.0 for _ in range(k): best = -1 for i in range(len(data)): if used[i]: continue if best < 0 or abs(data[i][0] - km) < abs(data[best][0] - km): best = i used[best] = True total += data[best][1] return total / k
def knn_mse(train, data, k): total = 0.0 for km, minutes in data: e = knn(train, km, k) - minutes total += e * e return total / len(data)print("MSE: обучение / валидация")for k in range(1, 5): print(f"k = {k}: {knn_mse(train, train, k):.1f} / {knn_mse(train, val, k):.1f}")MSE: обучение / валидацияk = 1: 0.0 / 28.9k = 2: 22.0 / 11.2k = 3: 19.0 / 19.7k = 4: 48.7 / 23.2k = 1 е наизустяване в най-чист вид: 0,0 на обучението, 28,9 на валидацията. С k = 2 средното на двама съседи изглажда шума и валидацията пада на 11,2. По-голямо k пак влошава: средното от далечни поръчки размива зависимостта. Тук k е като степента при кривата: избира се с валидацията.
Идеята
Кога вреди
- Валидация с малко данни. 8 поръчки за валидация дават шумни числа. Изборът може да е случаен: при степен 2 и 3 разликата (9,35 и 9,09) е шум. Колкото по-малко данни, толкова по-прост модел избирай.
- Валидацията, пипана много пъти. Пробваш 100 степени, λ и признаци и избираш най-доброто на валидацията. Някое от тях е най-добро случайно. Затова тестът стои настрана, недокоснат.
- Регуляризация без мащаб. Наказанието е еднакво за всички тегла. Ако единият признак е в километри, а другият в метри, теглото на метрите е хиляда пъти по-малко и почти не се наказва. Признаците се мащабират първо, както тук
xе смалено. - Твърде силно наказание. λ = 10 дава недообучение: моделът не хваща и обучението. Гледай и двете грешки: ако и двете са високи, моделът е твърде прост.
- Повече данни не са безплатни. Всяка поръчка в обучението е истинска поръчка, която трябва да се събере и провери. Понякога по-простият модел е по-евтиният лек.
По-точно, за любопитните
Формулата. Програмата търси теглата, при които сборът на квадратите на грешките плюс λ · (w₁² + w₂² + … + w₇²) е най-малък. MSE е същият сбор, делен на броя на поръчките, затова все едно λ е 8 пъти по-малко пред MSE. Това е L2 регуляризация (ridge). Ако наказанието е сборът на абсолютните стойности, |w₁| + |w₂| + …, е L1 (lasso): тя прави някои тегла точно 0, затова избира и признаци.
Защо по диагонала. Най-добрите тегла са там, където наклонът на грешката е 0 по всяко тегло. Всяко тегло дава едно уравнение. Наклонът на λ · w² е 2 · λ · w, а всички останали членове на наклона също са с двойка отпред. След деленето на 2 наказанието е λ · w в уравнението на същото тегло, тоест по диагонала на системата.
Кръстосана валидация. При малко данни: делиш обучението на 5 части, учиш се на 4 и мериш на петата, пет пъти, и вземаш средното. Всяка поръчка е веднъж валидация, а изборът не зависи от една случайна група.
Други лекове. При градиентното спускане: спиране, когато грешката на валидацията започне да расте (early stopping). При невронните мрежи: dropout, който на всяка стъпка изключва случайни неврони. Идеята е същата: моделът да не може да разчита на наизустени подробности.
В истинския код. В scikit-learn това са PolynomialFeatures, Ridge (λ се казва alpha), Lasso, KNeighborsRegressor, cross_val_score и GridSearchCV, който пробва стойностите и избира с кръстосана валидация. В ML.NET регуляризацията е параметър на треньорите (L2Regularization). Методът на Гаус там е заменен с по-стабилни методи, защото при висока степен системата е чувствителна към закръглянето.
for km, minutes in data разглобява всяка двойка. a[c], a[p] = a[p], a[c] разменя два реда.Упражнение
Избери k
Реши задачата в редактора си.
Довърши програмата с k-NN: избери k с валидацията (най-малката грешка на валидацията), после пусни теста само за избраното k.
MSE: обучение / валидацияk = 1: 0.0 / 28.9k = 2: 22.0 / 11.2k = 3: 19.0 / 19.7k = 4: 48.7 / 23.2избрано k: 2, тест: 16.9Валидацията избира k = 2, а тестът е 16,9. Сравни с правата: 5,57. За тази почти права зависимост правата печели и от съседите.
Още задачи
- Степените с повече данни. Учи степените от 1 до 7 на 16 поръчки (обучение и валидация) и печатай грешката на обучението и на теста. От коя степен нататък тестът започва да расте?
- Мрежа от λ. Пробвай λ от 0,02 до 0,5 през 0,02 за степен 7. Коя λ избира валидацията и какъв е тестът ѝ?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Какво е машинно учене · Линейна регресия · Градиентно спускане · Оценка: точността лъже
- Продължава в: Неврон · Обучението от начало до край · Цифрите от касовата бележка · Оценяване: тестов набор за асистента
- Задачи по темата: Как учи модел