Към съдържанието
wannacode

Кръстосана проверка (cross-validation)

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

Проблем

„Дървото или правата?“

Магазинът иска да знае колко поръчки ще направи всеки клиент през следващите 4 месеца. С това решава на кого да прати карта за отстъпка и колко стока да поръча. Признаците са от поръчките преди 1 юли 2026, както в урока за класификация: брой поръчки, дни от последната, откога купува и средна поръчка. Целта е броят поръчки от 1 юли до 31 октомври.

Ти казваш „линейна регресия“, а колегата казва „дърво с 3 нива“. Пускате ги на 6 различни разделяния на обучение и тест (random_state от 0 до 5) и мерите MAE в поръчки на клиент:

Pythonданни: таблиците на магазина

Клиентите са 1 279 и правят средно 2,70 поръчки за 4-те месеца. При разделяния 0, 1, 2 и 4 дървото греши по-малко, а при 3 и 5 правата. Ако бяхте пуснали само разделяне 2, колегата щеше да спечели с 1,25 срещу 1,31. С разделяне 5 печелиш ти. Кой е прав?

Идеята

Една проверка е една извадка

Тестовата част е 256 клиенти, избрани случайно. Като средната от 30 поръчки в урока за извадките, и грешката на 256 клиенти е шумна: друга извадка дава друго число. Грешката на правата скача от 1,31 до 1,43 само заради това кои клиенти са попаднали в теста.

Разликата между двата модела е няколко стотни. Шумът от разделянето е десетина стотни. С една проверка не можеш да кажеш кой е по-добър, както не можеш да кажеш колко е средната поръчка от 30 поръчки.

1. При разделяне 0 дървото греши с 1,31 поръчки, а правата с 1,35. Какво следва от това?

Идеята

Кръстосана проверка

Решението е да провериш много пъти и всеки клиент да е в теста точно веднъж. Делиш клиентите на 5 части. Обучаваш на 4 и мериш на петата, после сменяш коя е тестовата. Получаваш 5 грешки за всеки модел. Това се казва кръстосана проверка (cross-validation).

KFold дели на части, а cross_val_score обучава и мери модела на всяка. С shuffle=True клиентите се разбъркват, преди да се разделят:

Pythonданни: таблиците на магазина

Четири пъти учиш на 1 023 клиенти и проверяваш на 256, веднъж на 1 024 и 255. scikit-learn винаги търси най-голямото число, затова грешката идва с минус (neg_mean_absolute_error), а минусът отпред я обръща.

Базовата линия (средната, DummyRegressor) греши с 2,70 ± 0,09. Правата греши с 1,34 ± 0,06, а дървото с 1,38 ± 0,09. Числото след ± е разсейването на петте грешки (std). И двата модела бият базовата линия ясно, а разликата между тях (0,04) е по-малка от разсейването. Честният извод е „правата е малко по-добра, но не ясно“. Когато два модела са толкова близо, взимаш по-простия.

По-точно, за любопитните

Двата модела се сравняват на едни и същи части: затова kf е един и с random_state. cv=5 също дели на 5, но без да разбърква: първата част са първите 256 клиенти по ред. Ако таблицата е подредена (по номер, по дата на първата поръчка), частите са различни една от друга и оценката се изкривява.

Пробвай

Гората по-добра ли е?

Колегата предлага случайна гора (RandomForestRegressor с 50 дървета, max_depth=5). Провери я с кръстосана проверка на същите части kf. Запиши в forest_mae и line_mae петте грешки (MAE, положителни) на гората и на правата, в gap разликата в средната грешка (гора минус права), а в spread разсейването на грешките на гората.

Pythonданни: таблиците на магазина

Идеята

Преобучение

Защо дървото е с 3 нива, а не с 20? Колкото по-дълбоко е, толкова по-фино дели клиентите. Ето грешката на обучаващите и на тестовите клиенти за дълбочина от 1 до 12:

Pythonданни: таблиците на магазина

На обучаващите клиенти грешката пада всеки път: от 1,87 до 0,41. На тестовите пада до дълбочина 3 (1,50) и после расте до 1,91. Дърво без ограничение стига 23 нива и греши с 0,001 на обучаващите, почти нула, а на тестовите с 2,00.

Дълбокото дърво не е научило повече за клиентите. То е запомнило всеки обучаващ клиент заедно с шума му: един клиент случайно е поръчал повече, друг по-малко. Това се казва преобучение (overfitting). Признакът е голяма разлика между грешката при обучение и при тест. Затова грешка върху данните, на които моделът е учил, нищо не казва за нови клиенти.

Пробвай

Избери дълбочината

Избери дълбочината на дървото с кръстосана проверка. Запиши в cv_mae (Series с индекс дълбочината от 1 до 10) средната MAE от cross_val_score с частите kf, а в best_depth дълбочината с най-малка грешка.

Pythonданни: таблиците на магазина

Идеята

Когато редът е времето

Клиентите могат да се разбъркат: един клиент не знае нищо за друг. Дните не могат. Ако прогнозираш поръчките за утре, моделът ще има само миналото. Разбъркани части биха го учили и на дни след тестовите, а това е изтичането от предишния урок.

TimeSeriesSplit дели по времето: учи на началото и проверява на следващите дни, после учи на повече и проверява на още по-късни. Пример: поръчките за деня от деня от седмицата, поръчките преди 7 дни и средната за предишните 7 дни:

Pythonданни: таблиците на магазина

Първите 7 дни нямат „преди 7 дни“ и отпадат, остават 358. Всяка тестова част е 59 дни след обучаващата, а обучаващата расте: първо 2 месеца, накрая 10. Правата греши средно с 4,51 ± 0,18 поръчки на ден.

Пробвай

Бие ли базовата линия?

Сравни правата с базовата линия (DummyRegressor, средната на обучаващите дни) с TimeSeriesSplit на 5 части. Запиши в line_mae и base_mae петте грешки (MAE, положителни) на всеки.

Pythonданни: таблиците на магазина

Идеята

Тестът се пипа веднъж

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

Затова редът е:

  1. Отделяш тестова част в началото и не я гледаш.
  2. Избираш модела и настройките му (дълбочина, признаци) с кръстосана проверка само на обучаващата част.
  3. Обучаваш избрания модел на цялата обучаваща част и го мериш на теста веднъж. Това число казваш на управителя.
Pythonданни: таблиците на магазина

На 1 023-те обучаващи клиенти базовата линия греши с 2,74 ± 0,17, правата с 1,32 ± 0,06, а дървото с 1,36 ± 0,07. Правата е с по-малка грешка и е по-проста: избираш нея. 256-те тестови клиенти още не са пипани.

По-точно, за любопитните

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

Идеята

Кога модел не трябва

Модел струва време: данни, обучение, следене дали не се разваля. Понякога не си струва:

  • Малко данни. С няколко десетки клиенти грешката на кръстосаната проверка скача толкова, че не можеш да кажеш дали моделът бие средната.
  • Правило без модел върши същото. „Клиентът ще купува със същото темпо“ е едно деление. Ако моделът не го бие ясно, правилото е по-добро: всеки го разбира и няма какво да се развали.
  • Цената на грешката. Ако грешката е скъпа (откажеш кредит, спреш карта), моделът трябва да е много сигурен или човек да проверява.
  • Никой не действа по прогнозата. Ако от числото не следва решение (на кого купон, колко стока), моделът е само упражнение.

Правата има коефициент 0,469 пред броя поръчки досега, а другите признаци почти не участват. Значи тя е почти „половината от досегашните поръчки“: 8 месеца преди, 4 месеца след. Провери го в упражнението.

Упражнение

Присъдата

Кръстосаната проверка на обучаващата част избра правата. Обучи я на X_train и я провери веднъж на теста: запиши в model обучения модел, в test_mae грешката му (MAE) на тестовите клиенти, а в base_mae грешката на базовата линия на теста.

Pythonданни: таблиците на магазина

Бие ли правата едно правило без модел? Правилото е „клиентът ще купува със същото темпо“: за 4 месеца половината от поръчките му за 8-те месеца преди. Запиши в rule прогнозата на правилото за всеки тестов клиент, а в rule_mae грешката му на теста.

Pythonданни: таблиците на магазина

Тест

Провери се

1. Защо кръстосаната проверка дава по-надеждна оценка от едно разделяне?
2. Дърво без ограничение греши с 0,001 поръчки на обучаващите клиенти и с 2,00 на тестовите. Какво значи това?
3. Прогнозираш поръчките за деня. Защо TimeSeriesSplit, а не KFold(shuffle=True)?
4. Проверил си 10 модела на тестовата част и си избрал най-добрия. Колко е честна грешката му на теста?
5. Правилото „половината от досегашните поръчки“ греши с 1,41, а правата с 1,38. Какво е разумно?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·