У попередній статті ми створили нейронну мережу в NumPy, відтворили її в Keras і досягли приблизно 97% точності перевірки на MNIST. Ми використовували один прихований шар із 128 нейронів, швидкість навчання 0,1 і розмір партії 32.

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

Кожен експеримент нижче має інтерактивну діаграму. Ви можете клацати мітки, щоб показати або приховати окремі цикли, перемикання між переглядами втрати і точності, перемикання «показати базову лінію», щоб включити початкове вимірювання випадкових ваг (епоха 0), і розгорніть розділ «Журнал обчислень», щоб побачити необроблені результати навчання. На діаграмах за замовчуванням відображаються показники навчання — наш аналіз базується на значеннях навчання — але ви можете натиснути «val», щоб накласти показники перевірки для порівняння. Блоки коду зі значком marimo (видимим при наведенні) посилаються на інтерактивний блокнот, де ви можете самостійно провести експеримент і змінити код.

Ми використаємо той самий набір даних і налаштування моделі з попередньої статті:

import keras
import numpy as np

# Training: 60000 images, Test: 10000 images
(train_images, train_labels), (test_images, test_labels) = keras.datasets.mnist.load_data()

# Flatten 28×28 → 784 and normalize to [0, 1]
X_train = train_images.reshape(-1, 784).astype("float32") / 255.0
X_test = test_images.reshape(-1, 784).astype("float32") / 255.0

y_train = train_labels
y_test = test_labels

Швидкість навчання

Коли ми дивилися на як мережі навчаються, ми побачили, як швидкість навчання контролює розмір кроку градієнтного спуску. Занадто малий і навчання йде дуже повільно; занадто великий, і він перевищує мінімум, що ускладнює зближення до низьких втрат. Ми продемонстрували це на 2-параметричній моделі з інтерактивним віджетом. Тепер давайте подивимося на те саме явище в реальній мережі з понад 100 000 параметрами.

Ми порівняємо п’ять темпів навчання протягом десяти епох:

for lr in [0.001, 0.01, 0.1, 1.0, 10.0]:
    model = keras.Sequential([
        keras.layers.Dense(128, activation="relu", input_shape=(784,)),
        keras.layers.Dense(10, activation="softmax"),
    ])

    model.compile(
        optimizer=keras.optimizers.SGD(learning_rate=lr),
        loss="sparse_categorical_crossentropy",
        metrics=["accuracy"],
    )

    model.fit(X_train, y_train, epochs=10, batch_size=32,
              validation_split=0.2, verbose=2)

Давайте подивимося, як змінюються втрати за епоху для кожної швидкості навчання:

Loss by Learning Rate
Computation log

Увімкніть «показати базову лінію», щоб побачити вимірювання перед тренуванням. Запуски починаються з втрати 2,3, з деякими відмінностями від випадкової ініціалізації. Призначення рівно 0,1 ймовірності кожному з десяти класів дасть −log⁡(0.1)≈2.3-\log(0.1) \approx 2.3; випадкові ваги не гарантують рівномірного розподілу.

При lr = 10,0 навчання в цьому циклі не вдається. Втрати підскочують до 12,2 у першій епосі та залишаються на рівні 2,5 після цього, тоді як точність навчання залишається близько 10%. Ці показники показують, що модель не вивчає корисні розрізнення цифр; вони самі по собі не говорять нам, чи кожне зображення отримує однакове передбачення.

Давайте вимкнемо lr=10.0, клацнувши його мітку — тому що його сплеск до 12.2 стискає вісь y і ускладнює перегляд деталей на інших кривих. Приховання цієї кривої полегшує порівняння чотирьох решти прогонів:

  • lr = 0,001 — втрати досягають 0,41 після десяти епох, що все ще перевищує 0,33, досягнутих lr=0,1 після однієї епохи. Навчання йде повільно з таким розміром кроку.
  • lr = 0,01 — втрата досягає 0,18 і продовжує падати. Додаткове навчання може допомогти, але це не обов’язково досягне такого самого рішення, як lr=0,1.
  • lr = 0,1 — найменша остаточна втрата під час тренування серед цих пробіжок, приблизно 0,03. Більшість початкових покращень відбувається в перші дві-три епохи.
  • lr = 1,0 — втрати при навчанні сягають приблизно 0,15, тоді як втрати підтвердження коливаються в межах 0,20–0,24 у кінцевих епохах. Увімкніть криві перевірки, щоб побачити, що точність перевірки також знижується ближче до кінця.

Швидкість навчання також залежить від оптимізатора. Адам використовує поточні оцінки градієнтів та їхніх квадратів для масштабування оновлень для кожного параметра, але його швидкість навчання все ще потребує налаштування. У наступній статті ми розглянемо оптимізатори, графіки темпів навчання та розминку.

Розмір партії

Ми порівняємо розміри партій 1, 32, 256 і 60 000. Оскільки 20% залишено на перевірку, лише 48 000 зображень використовуються для навчання, тому batch_size=60 000 поміщає ці 48 000 зображень в один пакет.

for bs in [1, 32, 256, 60000]:
    model = keras.Sequential([
        keras.layers.Dense(128, activation="relu", input_shape=(784,)),
        keras.layers.Dense(10, activation="softmax"),
    ])

    model.compile(
        optimizer=keras.optimizers.SGD(learning_rate=0.1),
        loss="sparse_categorical_crossentropy",
        metrics=["accuracy"],
    )

    model.fit(X_train, y_train, epochs=10, batch_size=bs,
              validation_split=0.2, verbose=2)

Давайте подивимося, як змінюються втрати за епоху для кожного розміру партії:

Loss by Batch Size
Computation log

Середній рівень — bs=32 і bs=256 — обидва працюють добре, з bs=32 зближується швидше:

  • bs = 32 — втрати різко падають з 2,4 до 0,03 до епохи 10. З 1500 пакетами на епоху модель отримує часті оновлення з шумовими, але приблизно правильними градієнтами.
  • bs = 256 — повільніше, але стабільно. Втрата падає до 0,17 після 10 епох, і крива все ще йде вниз. Кожна епоха має лише 188 пакетів (проти 1500 для bs=32), тому менше оновлень, але кожне оновлення базується на надійнішому середньому градієнті.

Найменша та найбільша партії поводяться по-різному:

  • bs = 1 — нестабільний з lr=0,1. Кожен градієнт походить від одного зображення, і великі оновлення можуть скасувати попередній прогрес. Усереднення за більшою кількістю зображень зменшує цю варіацію.
  • bs = 60000 — втрати падають приблизно з 2,4 до 1,6 після десяти епох. Кожна епоха робить лише одне оновлення, тому десять епох дають десять кроків градієнта, порівняно з 15 000 при bs=32. Використання повного градієнта навчання не компенсує так мало оновлень за такої швидкості навчання.

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

Налаштуйте разом розмір партії та швидкість навчання. У циклі bs=1 зменшення lr з 0,1 до 0,001 зменшує втрати до 0,09. Більші партії можуть виграти від більшої швидкості навчання, але зв’язок не є універсальним правилом масштабування.

Глибина та ширина мережі

Наша базова мережа має один прихований шар із 128 нейронів. Що відбувається, коли ми йдемо ширше, глибше або і те, і інше? Давайте з’ясуємо — ми навчимо п’ять варіацій, а все інше буде незмінним (lr=0,1, bs=32, 10 епох):

configs = {
    "narrow (32)":  [32],
    "baseline (128)": [128],
    "wide (512)":   [512],
    "deep (2×128)": [128, 128],
    "deep (3×128)": [128, 128, 128],
}

for name, hidden_sizes in configs.items():
    model = keras.Sequential()
    model.add(keras.layers.Dense(hidden_sizes[0], activation="relu", input_shape=(784,)))
    for size in hidden_sizes[1:]:
        model.add(keras.layers.Dense(size, activation="relu"))
    model.add(keras.layers.Dense(10, activation="softmax"))

    model.compile(
        optimizer=keras.optimizers.SGD(learning_rate=0.1),
        loss="sparse_categorical_crossentropy",
        metrics=["accuracy"],
    )

    model.fit(X_train, y_train, epochs=10, batch_size=32,
              validation_split=0.2, verbose=2)

Давайте подивимося, як зміниться втрата для кожної архітектури:

Loss by Architecture
Computation log

Кілька речей виділяються:

  • Ширина допомагає: «ширина» тут означає кількість нейронів в одному прихованому шарі — перехід від 32 до 128 до 512 нейронів стабільно зменшує втрати. Ширший шар має більше параметрів для виявлення шаблонів. Але параметри 512 у 4 рази перевищують параметри 128 лише для невеликого покращення втрат — зменшення віддачі.
  • Глибина також допомагає: додавання другого прихованого шару (2×128) досягає менших втрат, ніж одношарова базова лінія, навіть якщо загальна кількість параметрів подібна. Більш глибокі мережі можуть вивчати ієрархічні особливості — перший рівень може виявляти краї, другий рівень може поєднувати краї у форми.
  • Третій рівень не покращив цей цикл: його кінцеві втрати близькі до базового рівня, а втрати під час перевірки коливаються більше. Ці криві самі по собі не визначають причину або не встановлюють, що градієнти зникають.

Для цього завдання MNIST корисними початковими точками є один або два прихованих шари та ширина 128–512. Порівняйте ефективність перевірки перед додаванням потужності. Тип шару також має значення: згорткові шари можуть використовувати просторову структуру зображень, яку наша повністю зв’язана модель ігнорує.

Функції активації: sigmoid vs ReLU

Ми порівняємо sigmoid і ReLU, щоб дослідити поведінку градієнта, про яку йдеться в теоретичній статті. Sigmoid вносить похідну щонайбільше 0,25 на кожному шарі; ReLU дає 1 для активних нейронів і 0 для неактивних. Ці фактори впливають на те, як градієнти поширюються через глибоку мережу.

У Keras перемикання функцій активації — це просто зміна рядка — "relu" проти "sigmoid". Ми навчимо мережі з 1, 3 і 5 прихованими шарами, щоб побачити, як глибина взаємодіє з вибором активації:

for n_layers in [1, 3, 5]:
    for activation in ["relu", "sigmoid"]:
        model = keras.Sequential()
        model.add(keras.layers.Dense(128, activation=activation, input_shape=(784,)))
        for _ in range(n_layers - 1):
            model.add(keras.layers.Dense(128, activation=activation))
        model.add(keras.layers.Dense(10, activation="softmax"))

        model.compile(
            optimizer=keras.optimizers.SGD(learning_rate=0.1),
            loss="sparse_categorical_crossentropy",
            metrics=["accuracy"],
        )

        model.fit(X_train, y_train, epochs=10, batch_size=32,
                  validation_split=0.2, verbose=2)

Давайте подивимося, як зміниться втрата для кожної комбінації глибини та функції активації:

Loss by Activation Function
Computation log

Під час цих пробіжок тренувати сигмовидну м’ясоподібну м’ясоподібну м’ясоподібну м’ясоподібну м’язи стає важче, оскільки глибина збільшується:

  • 1 прихований шар — обидві активації працюють добре. Втрати ReLU падають до 0,03 до епохи 10, сигмоїди до 0,15. Лише з одним шаром градієнт проходить через одну активацію, тому скорочення градієнта сигмовида не має великого значення.
  • 3 приховані шари — сигмовидна починає відставати. Втрата ReLU сягає 0,016, сигмоподібна – лише 0,14 — майже в 10 разів вище. Sigmoid помітно повільніше в перші кілька епох — її втрати в епоху 3 все ще вищі, ніж у ReLU після епохи 1.
  • 5 прихованих шарів — сигмовид повністю виходить з ладу. Втрата ледь змінюється від початкового значення ~2,3 протягом усіх 10 епох — модель практично нічого не навчилася. Водночас ReLU з 5 шарами досягає втрати 0,024 — практично ідентично 1 і 3 шарам.

Ці результати узгоджуються зі зникаючими градієнтами. У п’яти сигмоподібних шарах лише похідні активації вносять коефіцієнт щонайбільше 0.255≈0.0010.25^5 \approx 0.001. Вагові матриці також впливають на повний градієнт, тому це не точна оцінка градієнта, що досягає першого шару.

ReLU не зменшує градієнт на етапі активації для активних нейронів, що допомагає пояснити його результати тут. Це не гарантує стабільності градієнтів у всій мережі.

ReLU є корисною відправною точкою для цієї моделі. Leaky ReLU та ELU дозволяють ненульові градієнти для негативних вхідних даних і можуть допомогти, коли нейрони залишаються неактивними. Інші архітектури використовують різні активації, включаючи GELU в деяких трансформаторах. У стандартному LSTM sigmoid керує гейтами, тоді як tanh використовується для значень-кандидатів і вихідного перетворення стану комірки.

Кількість епох

У всіх наведених вище експериментах ми тренувалися протягом 10 епох. Але скільки епох ви повинні тренуватися? Давайте дізнаємось, навчивши нашу найкращу конфігурацію (lr=0,1, розмір партії 32) протягом 50 епох і спостерігаючи, що відбувається:

model = keras.Sequential([
    keras.layers.Dense(128, activation="relu", input_shape=(784,)),
    keras.layers.Dense(10, activation="softmax"),
])

model.compile(
    optimizer=keras.optimizers.SGD(learning_rate=0.1),
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)

history = model.fit(X_train, y_train, epochs=50, batch_size=32,
                    validation_split=0.2, verbose=2)

Втрати під час навчання падають від 0,33 в епосі 1 до 0,0015 в епосі 50. Повідомлена точність навчання досягає 100% до епохи 29. Це показує чудову відповідність набору для навчання, але не визначає, наскільки добре модель узагальнює.

Втрати перевірки покращуються з 0,19 в епоху 1 до 0,075 приблизно в епоху 13, потім припиняють покращуватися й починають повільно збільшуватися — 0,078 в епоху 20, 0,081 в епоху 30, 0,085 в епоху 50. Тим часом точність перевірки зберігається приблизно на 98% починаючи з епохи 13 і далі, і майже не рухається протягом решти 37 епох.

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

Занадто мало епох може залишити модель недостатньою; подальше навчання може призвести до переобладнання. Точка зупинки залежить від моделі, даних та інших гіперпараметрів, тому ми контролюємо продуктивність перевірки під час навчання.

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

early_stop = keras.callbacks.EarlyStopping(
    monitor="val_loss",
    patience=5,
    restore_best_weights=True,
)

model.fit(X_train, y_train, epochs=100, batch_size=32,
          validation_split=0.2,
          callbacks=[early_stop])

Встановіть верхню межу, наприклад 100 епох. З patience=5 навчання припиняється після п’яти послідовних епох без покращення втрат перевірки. restore_best_weights=True відновлює ваги з найкращої епохи перевірки. Ви все одно вибираєте контрольований показник, терпіння та максимальний бюджет навчання.

У наступних статтях ми докладніше розглянемо переобладнання та ранню зупинку.

Автоматичний пошук гіперпараметрів

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

Стратегії пошуку відрізняються тим, як вони вибирають наступну конфігурацію та використовують доступний бюджет оцінки:

  • Пошук у сітці. Виберіть кінцевий набір значень для кожного гіперпараметра та оцініть кожну комбінацію. Три параметри з п’ятьма значеннями потребують 125 запусків. Це зручно для невеликих просторів пошуку та недорогих оцінок.
  • Випадковий пошук. Зразки комбінацій із вибраних діапазонів або розподілів. Якщо лише кілька параметрів сильно впливають на результат, довільний пошук може досліджувати більш чіткі значення для цих параметрів, ніж сітка з тим самим бюджетом. Дивіться Bergstra & Bengio, 2012.
  • Координатний спуск. Почніть із конфігурації, змінюйте один параметр за раз і продовжуйте вдосконалення. Повторіть процес, щоб попередні варіанти можна було переглянути після зміни інших параметрів. Це дозволяє уникнути оцінки повної сітки, але може застрягти, якщо жодна зміна не допоможе, навіть якщо зміна кількох параметрів разом допоможе.
  • Байєсова оптимізація. Підберіть імовірнісну модель до попередніх конфігурацій і оцінок, а потім використовуйте її для вибору наступної оцінки, збалансовуючи дослідження та багатообіцяючі результати. Це може зменшити кількість дорогих оцінок, але його ефективність залежить від простору пошуку та моделі.

Виберіть стратегію пошуку на основі вартості оцінки, простору пошуку та доступного бюджету. Випадковий пошук забезпечує корисну базову лінію; локальний пошук або пошук на основі моделі може допомогти, коли оцінки дорогі. Наприклад, Папір AgentDiet налаштував чотири гіперпараметри для 100 завдань агента, змінюючи один параметр за раз. Автори оновили два налаштування після першого раунду та не виявили подальших покращень у другому.

Розгортки ваг і зміщень і Оптуна надають інструменти для сітки, випадкового пошуку та пошуку на основі моделі, автоматично відстежуючи кожен запуск і порівнюючи результати, а Тюнер Keras пропонує те саме, інтегроване безпосередньо в Keras. Ці інструменти допомагають керувати експериментами, коли ручне відстеження стає громіздким.

Перегляд зображення

Змінюйте масштаб жестом щипка або клавішами + / −. Переміщуйте зображення перетягуванням або стрілками. Esc — закрити.