Машинне навчання · Блок 5 · Тема 24

Що модель не вивчить сама

Частину чисел моделі знаходить навчання. Іншу частину хтось мусить написати руками ще до того, як навчання почалося — і саме вони найчастіше вирішують, чи буде модель робочою.

Та сама дошка оголошень про вживані телефони, та сама задача: за відносною ціною й віком акаунта відрізнити приманку від чесного продавця. Модель уже є — у темі про k найближчих сусідів вона впізнавала шахрайство з точністю 0.92. Але зверни увагу на слово, яке ми тоді сказали мимохідь: «дев'ять сусідів». Звідки взялася дев'ятка?

Її ніхто не обчислив. Її написала людина. І поки вона написана навмання, уся акуратна арифметика поверх неї стоїть на піску. Ця тема — про те, як таких чисел не вгадувати, а підбирати, і як не обдурити себе результатом підбору.

01 / РозрізненняДва види чисел у моделі

Візьмемо найзрозумілішу модель курсу — логістичну регресію — і навчимо її на 520 оголошеннях нашого архіву. Після навчання всередині неї лежать три числа:

w0 = −1.824    wціна = −1.231    wвік = −1.917

Ці три числа знайшов сам алгоритм. Ніхто їх не вигадував: він рухався градієнтним спуском, доки помилка на навчальних оголошеннях не перестала спадати, і зупинився саме тут. Це параметри моделі — те, заради чого навчання взагалі існує.

А тепер подивись на рядок, який запустив це навчання. Там було ще одне число — C = 1.0, сила регуляризації. Його не знаходив ніхто: воно стояло в коді до першої ітерації. Постав замість нього 0.01, запусти те саме навчання на тих самих даних — і алгоритм зупиниться в іншому місці:

w0 = −1.098    wціна = −0.388    wвік = −0.634

Інші ваги, інша модель, інша якість: оцінка на крос-валідації падає з 0.892 до 0.767. Число C не є параметром моделі — воно керує тим, які параметри навчання знайде. Такі числа звуться гіперпараметрами (hyperparameter).

Схема 1 · Що задає людина, а що знаходить алгоритм

Одна модель, один запуск навчання. Ліворуч — те, що написано в коді заздалегідь; праворуч — те, що з'явилось у моделі після навчання.

Стрілка йде в один бік. Гіперпараметри впливають на параметри, а не навпаки: навчання не має жодного способу «підправити» C зсередини. Тому крутити цю ручку може тільки хтось ззовні — і саме цим ми зайняті всю тему.

Різниця формулюється одним реченням: параметр модель знаходить із даних під час навчання, гіперпараметр людина задає до навчання. Перевірка проста — спитай себе, чи змінилося б це число, якби ти дав алгоритму інші дані, не змінивши жодного рядка коду. Ваги змінилися б. C — ні.

модельпараметри — знаходить навчаннягіперпараметри — задаєш ти
лінійна регресіякоефіцієнт при кожній ознаці та вільний членчи є штраф на ваги і який (alpha)
логістична регресіяті самі вагиC, тип штрафу, метод оптимізації
поліноміальна регресіякоефіцієнти многочленастепінь многочлена
kNNнічого — модель це сам архівk, вид відстані, ваги сусідів
наївний Баєсчастоти слів і апріорні ймовірності класівзгладжування α
дерево рішеньяка ознака і який поріг у кожному вузлімаксимальна глибина, мінімум об'єктів у листі
нейромережаусі ваги зв'язківкількість шарів і нейронів, швидкість навчання

Два рядки цієї таблиці варті окремої уваги, бо ламають зручну картинку.

kNN не має параметрів узагалі. Його «навчання» — це запам'ятати таблицю; жодного числа алгоритм не обчислює. Уся модель складається з даних і гіперпараметрів. Тому підбір k для kNN — це не налаштування деталі, а вибір самої моделі.

А масштабувальник — навпаки, має параметри. У нашому конвеєрі перед kNN стоїть StandardScaler, і він рахує з даних чотири числа: середнє й стандартне відхилення кожної ознаки. На навчальних 520 оголошеннях вийшло 0.883 і 0.332 для відносної ціни, 178.3 і 105.7 для віку акаунта. Ці чотири числа — повноцінні параметри: вони прийшли з даних. А от рішення взяти саме StandardScaler, а не MinMaxScaler, — гіперпараметр.

Гіперпараметром може бути не тільки число. Вид відстані ('euclidean' проти 'manhattan'), спосіб масштабування, навіть сам алгоритм — усе це вибір, зроблений до навчання. Формально вибір між kNN і наївним Баєсом теж є гіперпараметром, просто дуже великим. Саме тому інструменти підбору вміють перебирати цілі конвеєри, а не лише числа всередині них.

02 / НеможливістьЧому їх не вивчити разом із вагами

Питання, яке виникає негайно: якщо ваги ми знаходимо мінімізацією помилки, чому б не знайти так само й k? Перебрати всі значення, порахувати помилку на навчальних даних, узяти найменшу. Спробуймо — і подивимось, що вийде.

Ось точність нашого kNN на тих самих 520 навчальних оголошеннях, на яких він і навчався:

kточність на навчанніоцінка крос-валідаціїточність на 280 відкладених
11.00000.88850.8750
30.95380.94040.9071
90.94040.92500.9214
250.91730.90580.9179
510.89420.88270.8929

Мінімум помилки на навчанні стоїть при k = 1, і точність там рівно 1.0000. Причина дурна: найближчий сусід навчального оголошення — воно саме, тож помилитись ніде. Але на відкладених даних це найгірший рядок таблиці — 0.8750, гірше за всі інші k.

Це не властивість kNN. Це загальне правило, і воно варте того, щоб сформулювати його жирним: гіперпараметр майже завжди керує складністю моделі, а помилка на навчанні монотонно спадає зі складністю. Степінь многочлена, глибина дерева, кількість нейронів, 1/k у сусідах — усе це те саме. Тому оптимізація гіперпараметра по навчальній помилці не «іноді помиляється», а завжди впирається в найскладніший доступний варіант.

Схема 2 · Три криві, які розходяться

Той самий kNN на дошці оголошень, k від 1 до 51. Одна крива міряє модель на даних, на яких вона вчилась, дві інші — на тих, яких вона не бачила.

Помаранчева крива — точність на навчанні: вона спадає майже монотонно, тож свій максимум 1.000 має рівно там, де модель найгірша. Рожева — крос-валідація на навчальній частині, бірюзова — 280 відкладених оголошень. Обидві чесні криві спершу різко зростають і лише потім спадають: при k = 1 модель справді погана, і не бачить цього тільки та крива, яку рахували на власних навчальних даних.

Є й друга причина, суто технічна. Ваги ми підбираємо градієнтним спуском, бо помилка від них залежить гладко: зсунув вагу на тисячну — помилка змінилась на тисячну. Від k нічого гладко не залежить: k ціле, і між 8 і 9 немає нічого. А гіперпараметр «вид відстані» взагалі не число. Похідну по такому не візьмеш, тож залишається єдиний спосіб: перебрати варіанти й поміряти кожен.

Звідси й назва. «Гіпер-» тут означає «над»: ці числа стоять над процесом навчання й керують ним ззовні. Підбір гіперпараметрів — це другий, зовнішній цикл оптимізації поверх першого. І в нього своя, окрема логіка помилок — уся решта теми про неї.

03 / ЧесністьНа чому міряти під час підбору

Отже, перебираємо й міряємо. Питання одне: міряємо на чому?

На навчальній вибірці не можна — щойно показали чому. Спокуса номер два — міряти на тестовій: вона ж якраз відкладена, модель її не бачила. Подивись на таблицю вище ще раз: найкраща точність на відкладених 280 оголошеннях дорівнює 0.9286 і досягається при k = 17. Візьмімо сімнадцять?

Ні. І причина не в тому, що це «трохи негарно». Тестова вибірка існує рівно для одного: дати одне число, яке ніщо не встигло зіпсувати. Щойно ти обрав по ній хоч один гіперпараметр, вона перестала бути незалежною — інформація про неї просочилась у модель через твоє рішення. І тепер у тебе немає жодної вибірки, щоб сказати, скільки модель дає насправді. Ти витратив єдиний одноразовий вимірювач на службову задачу.

Це та сама помилка, що й вимірювання на навчанні, тільки поверхом вище. Там модель підганяла ваги під дані, які потім міряла. Тут ти підганяєш гіперпараметр під дані, які потім міряєш. Механізм ідентичний, і наслідок теж — оптимістична оцінка.

Правильний розподіл ролей ми вже розібрали в темі Train / Validation / Test: навчальна частина підбирає параметри, валідаційна — гіперпараметри, тестова один раз наприкінці називає результат. Проблема в тому, що валідаційну частину доводиться відрізати від навчальної, а даних завжди шкода.

Саме цю проблему знімає крос-валідація: замість одного відрізаного шматка вона по черзі робить валідаційним кожен фолд і усереднює. Кожне оголошення один раз побуває перевіркою і K−1 разів навчанням, і жоден рядок не пропадає. Для підбору це рівно те, що треба, тому GridSearchCV у назві має ці дві літери.

Наш підбір k виглядає так. Беремо 520 навчальних оголошень, ріжемо на 5 стратифікованих фолдів, для кожного k рахуємо середню точність по фолдах. Перемагає k = 3 з оцінкою 0.9404 ± 0.0128. Тестові 280 оголошень при цьому не чіпались жодного разу — і коли ми нарешті їх відкрили, обрана модель дала 0.9071.

Придивись до розкиду, а не лише до середнього. Переможець дав 0.9404, а k = 7 — 0.9385. Різниця 0.002 при розкиді по фолдах 0.013. Це не «трійка краща за сімку», це два однакові результати. Реальний висновок такий: годиться будь-яке невелике непарне k, і вибір конкретного значення — питання смаку, а не даних.

04 / ОптимізмПрокляття переможця

Тепер найважливіше в темі — і те, що найчастіше пропускають.

Перечитай два останні числа. Найкраща оцінка при підборі — 0.9404. Та сама модель на справді відкладених даних — 0.9071. Розрив 0.033, і він у передбачуваний бік: оцінка, за якою обирали, виявилась завищеною.

Механізм чистий і не має нічого спільного з машинним навчанням. Кожна оцінка на валідації складається з двох доданків: справжньої якості моделі й випадкового шуму конкретного розбиття. Коли ти береш максимум із багатьох таких оцінок, ти обираєш не лише найкращу модель — ти заодно обираєш найудачливіший шум. Максимум із багатьох зашумлених чисел систематично більший за максимум їхніх справжніх значень. Це називають прокляттям переможця (winner's curse).

𝔼[ maxj ŝj ] > maxj 𝔼[ ŝj ]

Зліва — те, що ти бачиш у звіті: середнє значення найкращої з перебраних оцінок. Справа — найкраща зі справжніх якостей, тобто те, на що модель має право розраховувати завтра. Нерівність строга завжди, коли в оцінок є хоч якийсь шум, а він є завжди.

Інтерактив 1 · Звідки береться завищення

Кожна пара точок — один кандидат: рожева — його оцінка на валідації, бірюзова — його справжня якість. Пошук бачить лише рожеві й бере найвищу.

найкраща валідаційна
її чесна якість
завищення
найкращий кандидат
Як читати. Кандидати тут різні за якістю — від 0.880 до 0.905, — тож перебір справді корисний: чим більше варіантів, тим вищий «найкращий кандидат». Але рожева перемога росте швидше за бірюзову правду, і різниця між ними — нижня крива — і є завищення. Постав σ = 0 — шуму немає, і завищення зникає повністю при будь-якій кількості кандидатів. Числа усереднені по 400 незалежних повторах, щоб не тремтіли; хмара вгорі показує один із цих повторів.

Тепер — чесна перевірка на наших даних, бо один розбіг у 0.033 ще нічого не доводить. У ньому змішані дві причини: завищення від вибору й звичайна різниця між двома вибірками. Розділимо їх експериментом. Візьмемо дванадцять різних розбиттів дошки на навчальну й відкладену частини, а сітку скоротимо до тринадцяти значень k. Для кожного порахуємо дві речі: наскільки оцінка завищена для обраного за крос-валідацією k і наскільки — для заздалегідь зафіксованого k = 9, який ніхто не обирав.

що міряємосередній розрив «оцінка мінус правда»
фіксоване k = 9−0.0071 — оцінка навіть трохи занижена
обране за CV−0.0030
різниця+0.0041 — це і є ціна вибору

Чотири тисячних. Менше, ніж лякає теорія, — і ця розбіжність сама по собі корисна. Причина ось у чому: тринадцять перебраних значень k — це не тринадцять незалежних кандидатів. Модель із k = 21 і модель із k = 25 майже однакові, їхні помилки на тих самих фолдах збігаються, тож і шум у них спільний. Максимум із тринадцяти скорельованих чисел зміщений приблизно так само, як максимум із кількох незалежних.

Практичне правило, яке звідси випливає. Розмір завищення залежить не від кількості перебраних варіантів, а від кількості незалежних серед них — і від того, наскільки шумна сама оцінка. Гладка одновимірна сітка по одному гіперпараметру майже безпечна. Небезпечно інше: багато незв'язаних ручок одразу, маленька валідаційна вибірка, шумна метрика (наприклад, F1 на рідкісному класі) і сотні спроб. Саме там розрив із чотирьох тисячних перетворюється на кілька відсотків.

Ліки називаються вкладеною крос-валідацією (nested cross-validation). Ідея в тому, щоб не мати одну вибірку, яка одночасно й обирає, і оцінює. Циклів стає два.

Схема 3 · Вкладена крос-валідація

Зовнішній цикл ділить дані й міряє. Внутрішній цикл живе всередині кожного зовнішнього фолда й тільки обирає гіперпараметри.

Головна властивість. Зовнішній фолд не бере участі в підборі — його не бачить ні одна з внутрішніх моделей. Тому число, яке він видає, чесне. Ціна питання видна одразу: замість K навчань виходить K × комбінацій × K.

Прогнали це на всіх 800 оголошеннях дошки: п'ять зовнішніх фолдів, усередині кожного — повний підбір k по п'яти внутрішніх. Результат такий.

І окрема деталь, помітна лише у вкладеній схемі: по п'яти зовнішніх фолдах підбір обрав різні k — 7, 5, 9, 19, 9. Тобто «найкраще k» не є властивістю задачі, воно міняється від однієї вибірки до іншої. Ще один аргумент не сприймати переможця сітки надто серйозно.

Коли вкладеність не потрібна. Якщо в тебе є повноцінна тестова вибірка, яку ти відкриваєш рівно один раз наприкінці, — вона вже виконує роль зовнішнього циклу. Вкладена схема потрібна тоді, коли даних мало й окремий тест виділити шкода, або коли треба чесно порівняти кілька алгоритмів, кожен зі своїм підбором: без вкладеності виграє той, у кого сітка була більшою.

05 / ПеребірСітка проти випадкового пошуку

Ми весь час казали «перебираємо варіанти», не уточнюючи, як саме. Способів три, і між першими двома є контрінтуїтивна різниця, заради якої й написаний цей розділ.

Сітка

Пошук по сітці (grid search) — це повний перебір: задаєш список значень для кожного гіперпараметра й пробуєш усі комбінації. Просто, відтворювано, тривіально паралелиться. Вада одна, зате арифметична: кількість комбінацій — це добуток довжин списків. Чотири гіперпараметри по шість значень дають не 24 варіанти, а 64 = 1296.

Випадковий пошук

Випадковий пошук (random search) не будує сітки взагалі. Ти задаєш діапазон для кожного гіперпараметра й кількість спроб, а кожна спроба бере випадкову точку з цих діапазонів. Звучить як капітуляція: замість акуратного перебору — кидання дротиків. Але при однаковому бюджеті він зазвичай виграє, і причина не у везінні.

Ось вона. У реальних задачах гіперпараметри не рівноцінні: із п'яти ручок дві щось вирішують, а три майже ні. Сітка про це не знає й ділить бюджет порівну між осями. Візьми сітку 5 × 5 = 25 спроб: важливу вісь вона обмацає рівно в п'яти точках, а решту 20 запусків витратить на повторення тих самих п'яти значень при різних неважливих. Випадковий пошук за ті самі 25 спроб дасть 25 різних значень важливої осі — бо жодні дві його точки не збігаються по жодній координаті.

Інтерактив 2 · Однаковий бюджет, різне покриття

Два гіперпараметри. Горизонтальна вісь вирішує майже все, вертикальна майже нічого — фон залежить від неї так слабко, що це непомітно оком.

бюджет спроб
максимум фону
сітка знайшла
випадковий знайшов
Смужки під квадратами — це вся суть. Кожна риска на них означає одне перевірене значення важливого гіперпараметра. У сітки їх стільки, скільки в неї стовпців, скільки б спроб вона не робила; у випадкового пошуку — стільки, скільки спроб. Зверни увагу й на те, що сітка веде себе нерівно: при стороні 4 їй щастить, стовпець лягає майже точно на пік, а при стороні 5 пік проходить між стовпцями. Випадковий пошук такої лотереї не має, бо просто додає нові точки.

Перевіримо на наших даних, а не на картинці. Візьмемо повну сітку з трьох ручок kNN: 26 значень k, два способи зважувати сусідів, дві відстані — разом 104 комбінації, помножені на 5 фолдів, тобто 520 навчань. Переможець: k = 17, ваги за відстанню, манхеттенська відстань, оцінка 0.9404, на відкладених 0.9321.

Тепер той самий простір випадковим пошуком, 26 спроб — 130 навчань, учетверо дешевше. Він знаходить ту саму комбінацію з тим самим числом 0.9404. Уся різниця в тому, що 390 навчань не відбулися.

Байєсівська оптимізація

Третій спосіб коротко, бо повне занурення в нього — окрема тема. Замість того щоб кидати точки навмання, можна після кожної спроби будувати сурогатну модель залежності «гіперпараметри → якість» і обирати наступну точку там, де очікуване покращення найбільше: частково поблизу вже знайденого доброго, частково там, де ще нічого не відомо. Це байєсівська оптимізація, і реалізації в optuna, scikit-optimize чи hyperopt. Вона окупається, коли одне навчання коштує довго — години на великій нейромережі, — і не окупається, коли навчання триває секунду: сама розумна процедура вибору точки тоді дорожча за спробу.

06 / ЦінаСкільки моделей ти насправді навчаєш

Найчастіша неприємність підбору не інтелектуальна, а арифметична: людина запускає GridSearchCV, іде пити каву й повертається до процесу, який доведеться вбити. Порахуймо, щоб такого не було.

навчань = (значень на ручку)кількість ручок × згорток крос-валідації

Ця формула — уся математика розділу, але множники в ній ростуть по-різному. Кількість згорток входить лінійно: перейшов з 5 на 10 — стало вдвічі довше. А кількість ручок стоїть у показнику: додав п'яту ручку з шістьма значеннями — і робота збільшилась ушестеро.

Інтерактив 3 · Рахунок за перебір

Покрути три повзунки й подивись, як швидко нешкідливі числа перетворюються на дні. Одне навчання прийнято за 0.8 секунди.

комбінацій
навчань
час сітки
вкладена CV
Три стовпчики — логарифмічна шкала, інакше найкоротший був би невидимим. Випадковий пошук тут завжди має фіксований бюджет 60 спроб — тому його стовпчик не реагує на розмір сітки взагалі, і в цьому вся суть: ти призначаєш ціну заздалегідь, а не отримуєш рахунок постфактум.

Що з цим роблять на практиці:

07 / Вибір ручокЩо крутити, а що ні

Останнє й найпрактичніше. У більшості моделей десятки аргументів, але вирішують один-два. Порівняй два наші власні виміри.

kNN, гіперпараметр k: оцінка гуляє від 0.8827 при k = 51 до 0.9404 при k = 3. Розмах 0.058 — учетверо більший за розкид самої оцінки. Цю ручку крутити треба обов'язково.

Логістична регресія, гіперпараметр C: 0.7673 при C = 0.01, далі 0.8808, потім 0.8923 — і при C рівному 1, 10 чи 100 оцінка не змінюється взагалі. Тобто змістовна частина діапазону — це два порядки внизу, а все, що вище одиниці, — марно витрачені запуски.

Звідси процедура, яка економить більше часу, ніж будь-яка розумна бібліотека: перш ніж будувати сітку, прогони кожну ручку окремо в широкому діапазоні й подивись на розмах метрики. Ручка, у якої розмах менший за розкид крос-валідації, у сітку не потрапляє. Зазвичай після такої перевірки з десяти аргументів у сітці лишаються два.

Схема 4 · Пам'ятка: головна ручка кожної моделі

З чого починати підбір і що станеться, коли крутнеш убік.

модельголовний гіперпараметрщо робитьу який бік крутити
kNNn_neighborsскільки сусідів голосуєбільше — гладша межа й менше реакції на шум; менше — межа повторює кожну точку
поліноміальна регресіяdegreeстепінь многочленабільше — гнучкіше й нестійкіше; рідко потрібно понад 3
гребенева, Lassoalphaсила штрафу на вагибільше — простіша модель; шукати по логарифмічній шкалі
логістична регресіяCзворотна сила штрафубільше C — слабший штраф; діапазон 0.01…10 зазвичай вичерпний
SVM з ядром RBFC і gammaжорсткість межі й радіус впливу точкикрутити тільки разом, обидва по логарифмічній сітці
наївний Баєсalphaзгладжування Лапласабільше — обережніші ймовірності; нуль ламає модель
дерево рішеньmax_depthскільки питань поспільбільше — точніше на навчанні й гірше на нових даних
випадковий лісmax_featuresскільки ознак бачить кожен розрізменше — дерева різноманітніші; кількість дерев просто беруть великою
градієнтний бустингlearning_rateвнесок одного дереваменший крок вимагає більше дерев; крутити пару разом
будь-що на градієнтному спускуlearning_rateдовжина крокузавеликий — розліт, замалий — не встигне дійти
Як цим користуватись. Почни з одного рядка своєї моделі, знайди робочий діапазон по логарифмічній шкалі — і лише потім думай, чи є друга ручка, варта уваги. Значення за замовчуванням у scikit-learn обрані непогано: вони рідко бувають найкращими, але майже ніколи не бувають катастрофічними.
Логарифмічна шкала — не примха. Для alpha, C, gamma і швидкості навчання значуща не різниця, а відношення: між 0.001 і 0.01 різниця така сама за змістом, як між 1 і 10. Рівномірна сітка від 0 до 10 з кроком 0.5 повністю пропустить цікаву зону й перевірить двадцять разів те саме. Тому в RandomizedSearchCV для таких ручок беруть loguniform, а в сітці пишуть [0.001, 0.01, 0.1, 1, 10].

08 / ПідсумокЩо варто винести

Блок про якість моделі закінчується там, де почався. Ми навчилися міряти, навчилися не довіряти одному вимірюванню — і тепер бачимо, що сам процес пошуку найкращої моделі створює нову, тоншу форму самообману. Кожен рівень чесності відкриває наступний рівень нечесності поверхом вище.

ідеясутьчому важливо
параметрнавчання знаходить із данихваги, пороги, частоти
гіперпараметрлюдина задає до навчаннякерує тим, які параметри вийдуть
не по навчаннюпомилка на навчанні спадає зі складністютакий підбір завжди обере найскладніше
не по тестувибір робить тест залежнимлишаєшся без жодного чесного числа
крос-валідаціяпідбір на фолдах навчальної частинидані не витрачаються на валідацію
прокляття переможцямаксимум зашумлених оцінок завищений«найкраща CV» — не прогноз якості
вкладена CVвибір і оцінка на різних данихєдиний спосіб чесно порівняти алгоритми
випадковий пошуктой самий бюджет, більше значень важливої осісітка марнує спроби на неважливі ручки
бюджеткомбінації в показнику, згортки — множникомрахувати до запуску, а не після

І головна думка теми, якщо лишити одну. Найкраща оцінка, отримана під час підбору, — це не очікувана якість моделі. Це найкраще з того, що показав шум на твоїй валідації. Очікувану якість дає лише вимірювання, у якому підбір не брав участі: окремий тест, відкритий один раз, або зовнішній цикл вкладеної крос-валідації.

Далі починається блок про дерева й ансамблі, і там усе сказане знадобиться одразу. У дерева рішень ручок помітно більше, ніж у kNN: глибина, мінімальний розмір листа, критерій розрізу, обмеження на кількість листків. У випадкового лісу й градієнтного бустингу їх більше десятка, вони взаємодіють між собою, а одне навчання коштує вже не мілісекунди. Саме там сітка з розділу 6 перетворюється на добу очікування, а вміння відрізнити дві важливі ручки від восьми байдужих стає не теорією, а щоденною роботою.

Далі в практиці. У practice.ipynb ти підбереш k руками через крос-валідацію, звіриш свої числа з GridSearchCV до останнього знака, проженеш RandomizedSearchCV з учетверо меншим бюджетом, побачиш завищення переможця у власних числах і закінчиш вкладеною крос-валідацією.

Далі в темі

Теорію прочитано. Тепер закріпи її на практиці.