AI Research Foundations, курс 3, часть 2: гиперпараметры, регуляризация, градиенты
Перевод лабораторных 3.5–3.8 курса Google DeepMind «Design and Train Neural Networks»: размер сети и переобучение, ранняя остановка, dropout и weight decay с журналом экспериментов, градиенты через jax.grad и обучение на Keras с Adam.
Вторая половина курса 3 «Design and Train Neural Networks». В первой части вы увидели недо- и переобучение на маленькой языковой модели и собрали свой MLP. Теперь — работа исследователя: как размер сети влияет на обобщение, какие есть средства против переобучения и как их честно сравнить, что такое градиент и как его считает JAX, и как обучить любую модель Keras тремя строками.
Как проходить вместе с курсом ChinaAI
Заголовок раздела «Как проходить вместе с курсом ChinaAI»| Лабораторная | Время | Когда делать | Что нужно |
|---|---|---|---|
| 3.5. Настройте гиперпараметры | 15 мин | После урока «Обобщение, переобучение, регуляризация» | Keras, CPU |
| 3.6. Боремся с переобучением | 25 мин | Вместе с уроком «Остаточные связи и регуляризация» | Keras, CPU |
| 3.7. Градиенты | 25 мин | После уроков «Градиентный спуск» и «Тензоры и autograd в PyTorch» | JAX, CPU |
| 3.8. Обучите модель на Keras | 25 мин | Вместе с уроком «nn.Module, оптимизаторы, цикл обучения» | Keras, CPU |
Все четыре лабораторные идут на процессоре за секунды. В курсе ChinaAI вы считали градиенты руками в micrograd
и через loss.backward() в PyTorch; лабораторная 3.7 показывает третий подход — jax.grad, где градиент возвращает
не поле тензора, а новая функция.
Лабораторная 3.5. Настройте гиперпараметры
Заголовок раздела «Лабораторная 3.5. Настройте гиперпараметры»Ноутбук: gdm_lab_3_5_tune_hyperparameters.ipynb · открыть в Colab
Подберите гиперпараметры, чтобы избежать переобучения. 15 минут.
Во время обучения модель подстраивает свои параметры, чтобы выучить закономерности обучающих данных. Но чтобы она делала это как можно лучше, вам как разработчику модели нужно найти лучшие настройки самой модели и процесса обучения. Например, решить, сколько у модели будет слоёв и сколько её обучать. Такие параметры, которые задаются до начала обучения, называются гиперпараметрами (hyperparameters). В этой лабораторной вы исследуете, как изменение двух видов гиперпараметров влияет на то, насколько хорошо модель учится и обобщает.
Чему вы научитесь
Заголовок раздела «Чему вы научитесь»К концу лабораторной вы поймёте, как число слоёв модели и число нейронов в слое влияют на обучение.
Вы снова используете набор данных из прошлых лабораторных, где классификатор предсказывает следующий токен из трёх слов: «mat», «apple» и «bank». Вы обучите модели с разными гиперпараметрами и изучите, как они учатся, по кривым обучения, доле правильных ответов и границам решений.
В этой лабораторной вы:
- загрузите набор двумерных эмбеддингов;
- создадите разные модели MLP;
- обучите модели и нарисуете кривые обучения и границы решений.
Импорты
Заголовок раздела «Импорты»В этой лабораторной вы в основном используете функции пакета курса ai_foundations, чтобы обучать модель и рисовать
графики.
%%capture# Ставим специальный пакет курса.!pip install "git+https://github.com/google-deepmind/ai-foundations.git@main"
import os # Чтобы менять настройки Keras.os.environ['KERAS_BACKEND'] = 'jax' # Задаём параметр Keras: вычисления — на JAX.
# Используемые пакеты.import jax.numpy as jnp # Чтобы задавать матрицы.import keras # Чтобы менять настройки Keras.import pandas as pd # Чтобы загрузить набор данных.
from ai_foundations import machine_learning # Чтобы задавать и обучать MLP.from ai_foundations import visualizations # Чтобы рисовать данные и границы.from ai_foundations import training # Чтобы печатать потери по ходу обучения.Загружаем набор данных
Заголовок раздела «Загружаем набор данных»Запустите следующую ячейку, чтобы загрузить набор и подготовить массивы jax.Array с обучающими и тестовыми данными.
Обратите внимание: здесь данные поделены на обучающую и тестовую выборки вручную, так что самые трудные примеры (все
выбросы) попали в тестовую выборку. Так проще увидеть разницу между моделями с разными гиперпараметрами.
# Загружаем данные с помощью pandas.df = pd.read_csv("https://storage.googleapis.com/dm-educational/assets/ai_foundations/mat-apple-bank-dataset.csv")
# Достаём эмбеддинги (Embedding_dim_1, Embedding_dim_2) и метки.X = jnp.array(df[["Embedding_dim_1", "Embedding_dim_2"]].values)# Метки: 0 ("mat"), 1 ("apple") или 2 ("bank").y = jnp.array(df["Label"].values)
# Метки, понятные человеку.labels = ["mat", "apple", "bank"]
# Делим данные вручную: первые 61 точка — обучение, остальные 12 — тест.X_train, X_test, y_train, y_test = X[0:61, :], X[61:, :], y[0:61], y[61:]Подбираем гиперпараметры
Заголовок раздела «Подбираем гиперпараметры»# Задайте здесь число скрытых слоёв и их размеры.# Каждое число — число нейронов в слое. Например, значение `[20, 10]`# создаёт MLP с двумя скрытыми слоями размером 20 и 10.hidden_dims = [20, 10]
# Создаём модель и рисуем её структуру.mlp_model = machine_learning.build_mlp(hidden_dims=hidden_dims, n_classes=3)visualizations.visualize_mlp_architecture(hidden_dims, 3)
# Обучаем модель.training_logger = training.CustomAccuracyPrinter(print_every=20)training_history = machine_learning.train_mlp( mlp_model, X_train, y_train, learning_rate=0.005, validation_data=(X_test, y_test), epochs=500, callbacks=[training_logger],)
# Рисуем кривую потерь.visualizations.plot_loss_curve(training_history.history)
# Считаем долю правильных ответов и рисуем границы решений.train_acc = training_history.history["accuracy"][-1]test_acc = training_history.history["val_accuracy"][-1]
print(f"Train accuracy: {train_acc * 100:.2f}%")print(f"Test accuracy: {test_acc * 100:.2f}%")
visualizations.plot_data_and_mlp( X_train, y_train, labels, features_test=X_test, label_ids_test=y_test, mlp_model=mlp_model, title="Decision Boundaries - Your MLP",)Что вы заметили?
Заголовок раздела «Что вы заметили?»Скорее всего, вы заметили, что у сети с одним скрытым слоем из очень немногих нейронов низкая доля правильных ответов и на обучении, и на тесте. Это классический признак недообучения: у модели не хватило ёмкости (capacity), чтобы выучить достаточно сложные границы решений, поэтому она не смогла разделить точки.
Обучая сеть с несколькими слоями, хотя бы один из которых очень большой, вы, вероятно, заметили, что граница решений приняла очень сложную форму, а доля правильных ответов на тесте ниже, чем на обучении (хотя, наверное, всё ещё довольно высокая: набор не слишком сложный). Это признак переобучения. У модели столько параметров, что вместо того чтобы выучить из обучающих данных только полезные закономерности (сигнал), она выучила и закономерности, свойственные именно этим обучающим примерам (шум). Это помешало ей обобщать на примеры тестовой выборки.
Наконец, задав модель с более разумным числом параметров (например, слой из 10 нейронов и слой из 5), вы, вероятно, увидели, что она выучила границу решений, которая хорошо работает и для обучающих примеров, и для отложенной тестовой выборки. Такая модель обобщает лучше.
В этом задании вы исследовали, как размер нейросети влияет на обучение. Вы меняли два гиперпараметра:
- число скрытых слоёв;
- число нейронов в каждом слое.
Пробуя модели разного размера, вы увидели три возможных исхода:
- недообучение: маленькие модели не смогли выучить достаточно закономерностей, потери и на обучении, и на тесте оставались высокими;
- переобучение: очень большие модели идеально выучили закономерности обучающих данных, но на тестовых данных работали хуже — они плохо обобщали;
- хорошо: модели среднего размера хорошо учились и хорошо работали на новых данных.
Вы также изобразили:
- кривые потерь, чтобы следить, как модель учится со временем;
- границы решений, чтобы видеть, как модель разделяет классы.
В следующем задании вы узнаете о дополнительных способах бороться с переобучением.
Лабораторная 3.6. Боремся с переобучением
Заголовок раздела «Лабораторная 3.6. Боремся с переобучением»Ноутбук: gdm_lab_3_6_mitigate_overfitting.ipynb · открыть в Colab
Примените weight decay, dropout и раннюю остановку, чтобы предотвратить переобучение. 25 минут.
В предыдущем задании вы увидели, как легко большие модели переобучаются на маленьких наборах данных. В этой лабораторной вы продолжите подбирать гиперпараметры. Вы возьмёте на себя роль исследователя и поэкспериментируете с тремя распространёнными средствами против переобучения:
- weight decay («затухание весов») — метод, который штрафует большие веса;
- dropout («прореживание») — во время обучения случайно обнуляет выходы нейронов;
- ранняя остановка (early stopping) — останавливает обучение, как только потери на тесте начинают расти.
По ходу каждого эксперимента вы будете записывать, насколько хорошо работает каждый метод.
Чему вы научитесь
Заголовок раздела «Чему вы научитесь»К концу лабораторной вы поймёте, как weight decay, dropout и ранняя остановка влияют на способность модели обобщать.
Вы снова используете набор данных из прошлых лабораторных, где классификатор предсказывает следующий токен из трёх слов «mat», «apple» и «bank». Вы снова обучите модели с разными гиперпараметрами и изучите их поведение по кривым обучения, доле правильных ответов и границам решений.
В этой лабораторной вы:
- загрузите набор двумерных эмбеддингов;
- зададите разные гиперпараметры;
- обучите модели и нарисуете кривые обучения и границы решений.
Импорты
Заголовок раздела «Импорты»%%capture# Ставим специальный пакет курса.!pip install "git+https://github.com/google-deepmind/ai-foundations.git@main"
import os # Чтобы менять настройки Keras.os.environ["KERAS_BACKEND"] = "jax" # Задаём параметр Keras: вычисления — на JAX.
# Используемые пакеты.from collections import namedtuple # Чтобы хранить конфигурации гиперпараметров.import keras # Чтобы задать ваш MLP.import jax.numpy as jnp # Чтобы задавать матрицы.import pandas as pd # Чтобы загрузить набор данных.# Чтобы делить данные на обучающую и тестовую выборки.from sklearn import model_selection
from ai_foundations import machine_learning # Чтобы обучать MLP.from ai_foundations import visualizations # Чтобы рисовать данные и границы.from ai_foundations import training # Чтобы печатать потери по ходу обучения.Загружаем набор данных
Заголовок раздела «Загружаем набор данных»Запустите следующую ячейку, чтобы загрузить набор и подготовить массивы jax.Array. Как и в прошлой лабораторной, данные
поделены вручную так, что самые трудные примеры (все выбросы) попали в тестовую выборку.
# Загружаем данные с помощью pandas.df = pd.read_csv("https://storage.googleapis.com/dm-educational/assets/ai_foundations/mat-apple-bank-dataset.csv")
# Достаём эмбеддинги (Embedding_dim_1, Embedding_dim_2) и метки.X = jnp.array(df[["Embedding_dim_1", "Embedding_dim_2"]].values)# Метки: 0 ("mat"), 1 ("apple") или 2 ("bank").y = jnp.array(df["Label"].values)
# Метки, понятные человеку.labels = ["mat", "apple", "bank"]
# Делим данные вручную.X_train, X_test, y_train, y_test = X[0:61, :], X[61:, :], y[0:61], y[61:]MLP с dropout
Заголовок раздела «MLP с dropout»Следующая ячейка реализует MLP с dropout. Главное: dropout применяется только во время обучения, поэтому, получая предсказания модели, нужно указывать, происходит ли это при обучении или при тестировании.
Как говорилось в предыдущей статье, dropout случайно обнуляет нейроны, и от этого сеть становится устойчивее к различиям
во входных данных. Модель ниже добавляет dropout к выходу каждого скрытого слоя — для этого достаточно операции
keras.layers.Dropout. Доля прореживания (dropout_rate) — вероятность, с которой значение нейрона обнуляется.
def build_mlp( hidden_dims: list[int], n_classes: int, dropout_rate: float) -> keras.Model: """ Строит MLP с выходным слоем SoftMax. После каждого скрытого слоя применяется dropout.
Аргументы: hidden_dims: список размеров всех скрытых слоёв. n_classes: число классов выходного слоя. dropout_rate: доля прореживания — вероятность, с которой значение нейрона обнуляется.
Возвращает: Модель Keras из последовательных операций. """ operations = [] for dim in hidden_dims: operations.append(keras.layers.Dense(dim)) operations.append(keras.layers.Activation("relu")) operations.append(keras.layers.Dropout(dropout_rate)) # работает только при обучении
operations.append(keras.layers.Dense(n_classes)) # Выходной слой: n_classes. operations.append(keras.layers.Softmax())
return keras.Sequential(operations)Подбираем гиперпараметры
Заголовок раздела «Подбираем гиперпараметры»В следующих ячейках вы будете менять по одному гиперпараметру за раз. Для каждого набора гиперпараметров вы обучите и оцените модель, а также посмотрите на кривые потерь и доли правильных ответов каждого запуска.
Запустите следующую ячейку, чтобы задать функцию, которая обучает модель и строит графики для заданного набора гиперпараметров. В ней видны все компоненты обучения, например функция потерь и оптимизатор. Пока не обращайте внимания на детали: о каждом компоненте вы подробнее узнаете в следующих статьях и лабораторных.
# Именованный кортеж для хранения конфигураций гиперпараметров.HyperParameterConfig = namedtuple( "HyperParameterConfig", [ "hidden_dims", "dropout_rate", "weight_decay_strength", "use_early_stopping", ],)
# Словарь для результатов всех ваших экспериментов.experiment_log = {}
def train_and_visualize( hidden_dims: tuple[int, ...] | list[int], dropout_rate: float, weight_decay_strength: float, use_early_stopping: bool,): keras.utils.set_random_seed(124) # Для воспроизводимости. model = build_mlp(list(hidden_dims), 3, dropout_rate)
loss_fn = keras.losses.SparseCategoricalCrossentropy()
learning_rate = 0.01 optimizer = keras.optimizers.Adam( learning_rate=learning_rate, weight_decay=weight_decay_strength )
model.compile(optimizer=optimizer, loss=loss_fn, metrics=["accuracy"])
epochs = 400 # Параметр «терпения» (patience) для ранней остановки: если потери на тесте # не улучшаются столько эпох подряд, обучение останавливается. patience = 20
training_logger = training.CustomAccuracyPrinter(print_every=20)
callbacks = [training_logger] if use_early_stopping: callbacks.append( keras.callbacks.EarlyStopping( monitor="val_loss", verbose=1, patience=patience ) )
# Обучаем модель (с ранней остановкой, если она включена). training_history = model.fit( X_train, y_train, validation_data=(X_test, y_test), epochs=epochs, verbose=0, callbacks=callbacks, )
# Рисуем кривые потерь и доли правильных ответов. visualizations.plot_loss_curve(training_history.history) visualizations.plot_accuracy_curve(training_history.history)
visualizations.plot_data_and_mlp( X_train, y_train, labels=labels, features_test=X_test, label_ids_test=y_test, mlp_model=model, title="Decision Boundaries", )
print(f"Final test loss: {training_history.history['val_loss'][-1]}") print( f"Final test accuracy:" f" {training_history.history['val_accuracy'][-1]*100:.2f}%" )Базовая линия: первый набор гиперпараметров
Заголовок раздела «Базовая линия: первый набор гиперпараметров»Следующая ячейка задаёт первый набор гиперпараметров. Это базовая линия (baseline), с которой вы будете сравнивать все остальные запуски. Для этого задания гиперпараметры в ней выбраны так, что ёмкость модели слишком велика, и модель очень склонна к переобучению. Вместо того чтобы уменьшать ёмкость модели, вы исследуете другие способы бороться с переобучением.
В базовой конфигурации все остальные средства выключены. Запустите следующую ячейку, чтобы задать этот набор параметров, обучить модель и построить графики.
hyperparameters_baseline = HyperParameterConfig( hidden_dims=(10, 1000), # MLP с большим числом параметров. dropout_rate=0.0, # Доля 0.0 — dropout не применяется. weight_decay_strength=0.00, # Значение 0.0 — без weight decay. use_early_stopping=False # Ранняя остановка выключена.)
train_and_visualize(*hyperparameters_baseline)Что вы заметили?
Заголовок раздела «Что вы заметили?»В этом запуске вы, вероятно, увидели, что кривая потерь на тесте после небольшого числа эпох начинает расти и продолжает расти дальше. Кроме того, доля правильных ответов продолжает падать по мере обучения. Обе тенденции — классические признаки переобучающейся модели.
# Сохраняем итоговые потери на тесте базового эксперимента.experiment_log[hyperparameters_baseline] = 0.7096465229988098Задание 1. Ранняя остановка
Заголовок раздела «Задание 1. Ранняя остановка»# Задайте гиперпараметры здесь.hyperparameters_early_stopping = HyperParameterConfig( hidden_dims=(10, 1000), dropout_rate=, weight_decay_strength=, use_early_stopping=)
train_and_visualize(*hyperparameters_early_stopping)Что вы заметили?
Заголовок раздела «Что вы заметили?»В запуске с ранней остановкой вы, вероятно, увидели, что потери на тесте почти не выросли. Дело в том, что механизм ранней остановки прервал обучение, как только потери начали расти (в этом случае — после 49 эпох). Поэтому и доля правильных ответов в этом запуске выше: модель правильно классифицировала все тестовые точки и не переобучилась на закономерностях обучающих данных.
Впишите итоговые потери на тесте в ячейку ниже и запустите её, чтобы сохранить результат эксперимента.
# Впишите итоговые потери на тесте эксперимента с ранней остановкой.experiment_log[hyperparameters_early_stopping] =Задание 2. Умеренный dropout
Заголовок раздела «Задание 2. Умеренный dropout»# Задайте гиперпараметры здесь.hyperparameters_dropout_medium = HyperParameterConfig( hidden_dims=(10, 1000), dropout_rate=, weight_decay_strength=, use_early_stopping=)
train_and_visualize(*hyperparameters_dropout_medium)Что вы заметили?
Заголовок раздела «Что вы заметили?»В запуске с долей прореживания 0,3 вы, вероятно, увидели, что потери и на обучении, и на тесте заметно скачут от эпохи к эпохе. Это следствие случайной природы dropout. Но средство сработало: хотя в некоторые эпохи потери на тесте росли, в целом они оставались низкими даже при долгом обучении. Dropout не даёт модели «вцепиться» в шум: из-за его случайности выходы нейронов меняются каждую эпоху, ведь какие выходы обнулить, каждый раз решает случай.
Впишите итоговые потери на тесте в ячейку ниже и запустите её, чтобы сохранить результат эксперимента.
experiment_log[hyperparameters_dropout_medium] =Задание 3. Сильный dropout
Заголовок раздела «Задание 3. Сильный dropout»# Задайте гиперпараметры здесь.hyperparameters_dropout_high = HyperParameterConfig( hidden_dims=(10, 1000), dropout_rate= , weight_decay_strength= , use_early_stopping=)
train_and_visualize(*hyperparameters_dropout_high)Что вы заметили?
Заголовок раздела «Что вы заметили?»В запуске с долей прореживания 0,7 вы, вероятно, увидели, что потери на обучении и на тесте скачут между эпохами ещё сильнее — опять из-за случайности dropout. При таком сильном прореживании средство перестаёт работать как надо. Когда вероятность обнуления так высока, шума становится столько, что модель уже не может выучить сигнал. Она ошибается и на обучающих, и на тестовых примерах — то есть недообучается.
Это показывает, что методами вроде dropout нужно пользоваться умеренно. Если добавить слишком много случайности, модель тоже не выучит полезных закономерностей и не сможет хорошо обобщать.
Впишите итоговые потери на тесте в ячейку ниже и запустите её, чтобы сохранить результат эксперимента.
experiment_log[hyperparameters_dropout_high] =Задание 4. Умеренный weight decay
Заголовок раздела «Задание 4. Умеренный weight decay»# Задайте гиперпараметры здесь.hyperparameters_weight_decay_medium = HyperParameterConfig( hidden_dims=(10, 1000), dropout_rate=, weight_decay_strength=, use_early_stopping=)
train_and_visualize(*hyperparameters_weight_decay_medium)Что вы заметили?
Заголовок раздела «Что вы заметили?»В запуске с силой weight decay 0,5 вы, вероятно, увидели, что потери на тесте растут гораздо медленнее, чем у базовой модели. Дело в том, что модель штрафуется за очень сильные связи с закономерностями обучающих данных и поэтому меньше склонна к переобучению.
Впишите итоговые потери на тесте в ячейку ниже и запустите её, чтобы сохранить результат эксперимента.
experiment_log[hyperparameters_weight_decay_medium] =Задание 5. Сильный weight decay
Заголовок раздела «Задание 5. Сильный weight decay»# Задайте гиперпараметры здесь.hyperparameters_weight_decay_high = HyperParameterConfig( hidden_dims=(10, 1000), dropout_rate=, weight_decay_strength=, use_early_stopping=)
train_and_visualize(*hyperparameters_weight_decay_high)Что вы заметили?
Заголовок раздела «Что вы заметили?»В запуске с силой weight decay 50,0 вы, вероятно, увидели, что модель ничего не выучила. Как и при слишком большой доле прореживания, слишком сильный weight decay тоже ведёт к недообучению: модель плохо работает и на обучающих, и на тестовых примерах.
Впишите итоговые потери на тесте в ячейку ниже и запустите её, чтобы сохранить результат эксперимента.
experiment_log[hyperparameters_weight_decay_high] =Анализируем результаты
Заголовок раздела «Анализируем результаты»По ходу лабораторной вы пробовали разные сочетания гиперпараметров и в каждом запуске записывали, насколько хорошо модель обобщает на тестовую выборку. Теперь по журналу можно понять, насколько хорошо работают разные настройки.
Запустите следующую ячейку, чтобы изобразить связь между значением гиперпараметра и потерями для каждого из трёх
гиперпараметров. Перед запуском убедитесь, что вы добавили потери каждого эксперимента в experiment_log.
visualizations.visualize_hyperparameter_loss(experiment_log)Изучите графики выше. Какие средства против переобучения хорошо снизили потери на тесте? Какие хуже?
Вы, вероятно, заметили, что и для доли прореживания, и для силы weight decay качество модели сначала улучшалось по сравнению с базовой линией, когда вы ставили небольшое значение. Но оно сильно ухудшалось, когда любое из этих значений становилось слишком большим. Это очень частая картина: обычно приходится попробовать несколько значений, пока не найдёте то, что лучше всего работает для вашей модели и данных.
В этой лабораторной вы меняли по одному гиперпараметру за раз. На практике средства против переобучения часто сочетают — например, dropout и раннюю остановку. Если хотите поэкспериментировать дальше, добавьте в лабораторную ячейки и попробуйте другие сочетания гиперпараметров.
В этом задании вы были исследователем, который ищет лучшие гиперпараметры модели, чтобы не допустить переобучения. Вы попробовали три средства против переобучения:
- раннюю остановку;
- dropout;
- weight decay.
Вы также увидели, как журнал экспериментов (experiment log) помогает изобразить связь между гиперпараметрами и потерями на тестовом наборе. Этот приём пригодится, когда вы будете обучать свои модели.
В следующем задании вы ещё немного узнаете о подборе гиперпараметров и о том, зачем для него нужен отдельный набор данных.
Решения
Заголовок раздела «Решения»Задания 1–5
# Задание 1: ранняя остановка.hyperparameters_early_stopping = HyperParameterConfig( hidden_dims=(10, 1000), dropout_rate=0.0, weight_decay_strength=0.00, use_early_stopping=True)
# Задание 2: умеренный dropout.hyperparameters_dropout_medium = HyperParameterConfig( hidden_dims=(10, 1000), dropout_rate=0.3, weight_decay_strength=0.00, use_early_stopping=False)
# Задание 3: сильный dropout.hyperparameters_dropout_high = HyperParameterConfig( hidden_dims=(10, 1000), dropout_rate=0.7, weight_decay_strength=0.00, use_early_stopping=False)
# Задание 4: умеренный weight decay.hyperparameters_weight_decay_medium = HyperParameterConfig( hidden_dims=(10, 1000), dropout_rate=0.00, weight_decay_strength=0.5, use_early_stopping=False)
# Задание 5: сильный weight decay.hyperparameters_weight_decay_high = HyperParameterConfig( hidden_dims=(10, 1000), dropout_rate=0.0, weight_decay_strength=50.0, use_early_stopping=False)В оригинальных решениях заданий 4 и 5 переменные названы hyperparameters_regularization_medium
и hyperparameters_regularization_high, а в заданиях — hyperparameters_weight_decay_…. Мы привели имена к тем, что
в заданиях, иначе следующие ячейки упадут с NameError.
Лабораторная 3.7. Градиенты
Заголовок раздела «Лабораторная 3.7. Градиенты»Ноутбук: gdm_lab_3_7_gradients.ipynb · открыть в Colab
Автоматически вычисляйте градиенты, чтобы шаг за шагом выучивать границы решений. 25 минут.
В этой лабораторной вы узнаете, как с помощью JAX вычислять градиенты автоматически — это называется автоматическим дифференцированием (automatic differentiation, сокращённо autodiff). Автоматически вычисленными градиентами вы будете раз за разом обновлять границу решений бинарного классификатора, пока она не разделит два класса идеально.
Чему вы научитесь
Заголовок раздела «Чему вы научитесь»К концу лабораторной вы:
- научитесь вычислять градиенты в JAX;
- разберётесь, как этими градиентами обновлять параметры модели.
В этом задании вы снова используете набор данных из прошлых заданий, где каждая точка — двумерный эмбеддинг промпта. Задача модели — предсказать следующий токен («food» или «water»). Модель — нейросеть из одного нейрона с сигмоидной функцией активации.
В этой лабораторной вы:
- загрузите набор двумерных эмбеддингов;
- вручную зададите вычисления модели и функцию потерь функциями JAX;
- автоматически вычислите градиенты этой модели;
- посмотрите, как сдвигается граница решений при обновлениях.
Импорты
Заголовок раздела «Импорты»В этой лабораторной вы в основном используете функции JAX, чтобы задать модель, вычислить градиент, обновить параметры и представить данные.
%%capture# Ставим специальный пакет курса.!pip install "git+https://github.com/google-deepmind/ai-foundations.git@main"
# Используемые пакеты.import jax # Чтобы автоматически вычислять градиенты.import jax.numpy as jnp # Чтобы задать модель и входные матрицы.from IPython.display import display # Чтобы показать набор данных.import pandas as pd # Чтобы загрузить и показать набор данных.from ai_foundations import visualizations # Чтобы рисовать графики.Загружаем набор данных
Заголовок раздела «Загружаем набор данных»Запустите следующую ячейку, чтобы загрузить и напечатать набор данных. Обратите внимание: это набор на два класса, с которым вы работали в одной из первых лабораторных курса, а не набор на три класса, на котором вы недавно изучали MLP и подбор гиперпараметров.
Заметьте также, что в этом задании нет ни валидационной, ни тестовой выборки. Цель задания — не построить модель, которая лучше всех обобщает, а понять, как устроен сам процесс обучения.
# Загружаем данные с помощью pandas.df = pd.read_csv("https://storage.googleapis.com/dm-educational/assets/ai_foundations/food-water-dataset.csv")
# Достаём признаки (Embedding_dim_1, Embedding_dim_2) и метки.# Входные признаки (эмбеддинги).X_train = jnp.array(df[["Embedding_dim_1", "Embedding_dim_2"]].values)labels = df["Label"].values # Метки: "food" или "water".
# Переводим метки в числа (food = 1, water = 0).y_train = jnp.where(labels == "food", 1, 0)
# Печатаем и рисуем загруженные данные для проверки.display(df.head(n=20))
visualizations.plot_data_and_decision_boundary(X_train, labels)Задание 1. Начальная догадка
Заголовок раздела «Задание 1. Начальная догадка»Напомним: вектор весов () задаёт ориентацию границы решений, а смещение () — насколько граница сдвинута.
# Меняйте смещение и веса здесь.b_initial_guess = jnp.array([0.5])w_initial_guess = jnp.array([-0.9, 0.9])
# Рисуем границу для начальной догадки.visualizations.plot_data_and_decision_boundary( X_train, labels, weight_vector=w_initial_guess, bias_term=b_initial_guess, title="Initial Guess for Weights")Задание 2. Реализуем функцию предсказания
Заголовок раздела «Задание 2. Реализуем функцию предсказания»Чтобы JAX мог вычислить градиенты модели, ему нужна реализация функции потерь. А функции потерь, в свою очередь, нужна реализация нейросети, которая выдаёт предсказания.
def predict(w: jax.Array, b: jax.Array, X: jax.Array) -> jax.Array: """Вычисляет предсказанные вероятности одним нейроном.
Считает скалярное произведение входных признаков и весов, затем применяет сигмоиду, чтобы получить предсказания в диапазоне [0, 1].
Аргументы: w: вектор весов модели. Ожидаемая форма (embedding_dim,). b: смещение модели. Ожидаемая форма (1,). X: матрица входных данных: каждая строка — пример, каждый столбец — измерение эмбеддинга. Ожидаемая форма (number_of_examples, embedding_dim).
Возвращает: Массив предсказанных вероятностей, по одной на пример. Форма (number_of_examples,). """
y = ... # Добавьте свой код здесь.
return yЗадаём функцию потерь
Заголовок раздела «Задаём функцию потерь»Запустите следующую ячейку, чтобы задать функцию потерь. Вы обучаете бинарный классификатор, поэтому используйте бинарную перекрёстную энтропию (binary cross-entropy). Здесь — предсказанная вероятность класса 1, а равно 1, если настоящий класс — 1 («food»), и 0 («water») иначе:
Как читать формулу: из двух слагаемых в скобках работает только одно. Если правильный ответ «food» (), потери равны : при уверенном верном ответе это , а при уверенном неверном — уже . Если правильный ответ «water», работает второе слагаемое .
Обычно потери по всему набору данных считают как среднее потерь по всем примерам — это делает функция jnp.mean ниже.
Функция потерь ниже вычисляет предсказания модели на всём наборе данных через вашу функцию predict, используя текущие веса
и текущее смещение . Благодаря этому функция потерь напрямую зависит от и , и средства
автоматического дифференцирования могут сами вычислить градиенты функции потерь по и .
def loss_fn( w: jax.Array, b: jax.Array, X: jax.Array, y_true: jax.Array) -> jax.Array: """Вычисляет бинарную перекрёстную энтропию.
Функция потерь для бинарной классификации: показывает разницу между настоящими метками и вероятностями, предсказанными моделью. Для численной устойчивости добавляется маленький эпсилон, чтобы не получить log(0).
Аргументы: w: вектор весов модели. Ожидаемая форма (embedding_dim,). b: смещение модели. Ожидаемая форма (1,). X: матрица входных данных. Ожидаемая форма (number_of_examples, embedding_dim). y_true: настоящие бинарные метки (0 или 1). Ожидаемая форма (number_of_examples,).
Возвращает: Среднюю бинарную перекрёстную энтропию — одно число. """ y_pred = predict(w, b, X) # Добавляем маленький эпсилон для численной устойчивости. epsilon = 1e-8 return jnp.mean( -( y_true * jnp.log(y_pred + epsilon) + (1 - y_true) * jnp.log(1 - y_pred + epsilon) ) )Запустите следующую ячейку, чтобы вычислить на обучающем наборе потери классификатора с вашими вручную заданными весами.
# Считаем потери для начальных весов.print( "Loss for current weights:" f" {loss_fn(w_initial_guess, b_initial_guess, X_train, y_train):.4f}")Ожидаемый вывод для начальной догадки , : Loss for current weights: 0.7512.
Вычисляем градиенты и обновляем веса
Заголовок раздела «Вычисляем градиенты и обновляем веса»Теперь вы выполните один автоматический шаг обучения. Ячейка ниже с помощью функции JAX grad задаёт функцию, которая
автоматически вычисляет градиент функции потерь на наборе данных.
Этот градиент подсказывает, как поменять веса и смещение, чтобы потери уменьшились. Обновлённый вектор весов вычисляется так:
w_updated = w_current - learning_rate * gradient_wа обновлённое смещение — так:
b_updated = b_current - learning_rate * gradient_bТо есть из исходного вектора весов вычитается градиент. Чтобы обновления были маленькими и постепенными, градиент
умножается на гиперпараметр learning_rate (скорость обучения). Его ставят меньше 1,0, а обычно — намного меньше.
(Зачем нужны постепенные обновления, объяснят в следующих заданиях.)
Запустите следующую ячейку, чтобы выполнить один автоматический шаг обновления.
Постарайтесь понять:
- как градиент указывает направление наискорейшего подъёма (вычитая градиент, идём в направлении наискорейшего спуска);
- как шаг по градиенту сдвигает веса к лучшей классификации данных.
# Возвращаем начальную догадку.b_initial_guess = jnp.array([0.5])w_initial_guess = jnp.array([-0.9, 0.9])
# Задаём функцию, которая вычисляет градиент функции потерь.# `argnums=[0,1]` говорит автоматическому дифференцированию, что градиент нужен# по первому и второму аргументам функции потерь — весам и смещению.grad = jax.grad(loss_fn, argnums=[0, 1])
# Делаем один шаг по градиенту.
# Размер шага при обновлении весов.learning_rate = 0.1
# Вычисляем градиент.gradient = grad(w_initial_guess, b_initial_guess, X_train, y_train)print(f"Gradients: gradient_w = {gradient[0]}, gradient_b = {gradient[1]}")print( f"Loss before update:" f" {loss_fn(w_initial_guess, b_initial_guess, X_train, y_train):.4f}")
# `gradient` — кортеж: градиент по вектору весов и градиент по смещению.gradient_w, gradient_b = gradient
# Обновляем веса и смещение.w_updated = w_initial_guess - learning_rate * gradient_wb_updated = b_initial_guess - learning_rate * gradient_b
print( f"Loss after update:" f" {loss_fn(w_updated, b_updated, X_train, y_train):.4f}")
# Рисуем границу решений после одного шага.visualizations.plot_data_and_decision_boundary( X_train, labels, weight_vector=w_updated, bias_term=b_updated, title="Decision Boundary After One Gradient Step",)Ожидаемый вывод (наш расчёт): градиент по весам примерно [-0.521 -0.475], по смещению — [0.158];
Loss before update: 0.7512, Loss after update: 0.7002. Оба компонента градиента по весам отрицательны, поэтому после
вычитания оба веса растут: вектор поворачивается к облаку «food» около точки .
Градиентный спуск
Заголовок раздела «Градиентный спуск»Алгоритм градиентного спуска (gradient descent) раз за разом обновляет вектор весов и смещение по формулам выше:
w = w - learning_rate * gradient_wи
b = b - learning_rate * gradient_bЗдесь w и b хранят текущие значения весов и смещения; в начале обучения их нужно инициализировать. На практике это
обычно делают случайно, но здесь берутся значения w_initial_guess и b_initial_guess.
Эти обновления выполняются в цикле обучения. Каждая итерация цикла здесь соответствует одной эпохе, так что число выполнений цикла — это число эпох. С каждой итерацией потери на обучении уменьшаются, и модель всё лучше предсказывает на обучающих примерах.
Запустите следующую ячейку, чтобы многократно применить обновления по градиенту и посмотреть, как веса меняются со временем.
Обратите внимание:
- как граница решений сдвигается, всё лучше разделяя два класса;
- как вектор весов постепенно поворачивается к положительному классу.
# Инициализируем вектор весов.w = w_initial_guessb = b_initial_guesslearning_rate = 0.5
for step in range(10): # Делаем 10 шагов обновления.
# Вычисляем градиенты. gradient = grad(w, b, X_train, y_train) gradient_w, gradient_b = gradient
# Обновляем веса и смещение. w = w - learning_rate * gradient_w b = b - learning_rate * gradient_b
print(f"Step {step + 1} loss: {loss_fn(w, b, X_train, y_train):.4f}")
visualizations.plot_data_and_decision_boundary( X_train, labels, weight_vector=w, bias_term=b, title=f"Step {step + 1}" )Ожидаемый вывод (наш расчёт): потери падают с каждым шагом — 0.5195, 0.3809, 0.2967, 0.2426, 0.2054,
0.1785, 0.1582, 0.1422, 0.1294, 0.1189; после 10 шагов , .
Вы увидели, что происходит при обучении модели. Для моделей с несколькими слоями и множеством нейронов процесс гораздо сложнее, но принципы точно такие же. На каждой итерации вычисляется градиент функции потерь, и по нему обновляются параметры. Если повторить этот шаг много раз для всех примеров обучающих данных, модель может выучить сложные закономерности.
Задачи посложнее
Заголовок раздела «Задачи посложнее»visualizations.plot_spiral_data( "https://storage.googleapis.com/dm-educational/assets/ai_foundations/spiral-dataset.csv")В этом задании вы научились автоматически вычислять градиенты с помощью пакета JAX. Затем этими градиентами вы
запустили градиентный спуск и выучили параметры модели, которые дают хорошую границу решений для набора данных. Так
вы увидели процесс обучения любой нейросети, какой бы сложной она ни была.
В следующих заданиях вы узнаете больше о том, как устроены обновления по градиенту и как они выполняются, когда модель обучается на гораздо большем наборе данных.
Решения
Заголовок раздела «Решения»Задание 1
# Один из возможных вариантов.b_initial_guess = jnp.array([0.0])w_initial_guess = jnp.array([1.0, 1.0])Задание 2
def predict(w: jax.Array, b: jax.Array, X: jax.Array) -> jax.Array: # Матрица (n, 2) на вектор (2,) даёт вектор (n,); смещение прибавляется к каждому элементу. y = jax.nn.sigmoid(jnp.dot(X, w) + b)
return yТо же самое на PyTorch
Заголовок раздела «То же самое на PyTorch»Не часть оригинала: так выглядит один шаг лабораторной в PyTorch, на котором написан курс ChinaAI.
import torch
# Те же 20 точек можно взять из CSV; здесь — три для краткости.X = torch.tensor([[0.27, 0.39], [1.06, 0.89], [-1.38, -1.12]])y = torch.tensor([1.0, 1.0, 0.0])
w = torch.tensor([-0.9, 0.9], requires_grad=True) # просим PyTorch следить за градиентомb = torch.tensor([0.5], requires_grad=True)
y_pred = torch.sigmoid(X @ w + b)loss = torch.nn.functional.binary_cross_entropy(y_pred, y)loss.backward() # градиенты появятся в w.grad и b.grad
with torch.no_grad(): # сам шаг не должен попадать в граф w -= 0.1 * w.grad b -= 0.1 * b.gradprint(loss.item(), w, b)В JAX jax.grad(loss_fn) возвращает функцию, которая считает градиенты; в PyTorch градиенты накапливаются в полях
.grad тензоров после backward(), и перед следующим шагом их нужно обнулять.
Лабораторная 3.8. Обучите модель на Keras
Заголовок раздела «Лабораторная 3.8. Обучите модель на Keras»Ноутбук: gdm_lab_3_8_train_your_model_with_keras.ipynb · открыть в Colab
Обучите нейросеть с помощью оптимизаторов, реализованных в Keras. 25 минут.
В предыдущей статье вы узнали, как обучать нейросеть стохастическим градиентным спуском (SGD). В этой лабораторной вы примените эти знания на практике и узнаете, как обучать модели Keras оптимизатором Adam, основанным на SGD.
Чему вы научитесь
Заголовок раздела «Чему вы научитесь»К концу лабораторной вы будете понимать:
- как обучить любую модель Keras, какой бы ни была её архитектура;
- роль оптимизатора и функции потерь и как их вместе использовать для обучения модели.
Как и в прошлых лабораторных, вы работаете с набором эмбеддингов промптов. Цель — построить классификатор, который предсказывает следующее слово («food» или «water») по эмбеддингу промпта.
В этой лабораторной вы:
- загрузите набор данных;
- зададите двухслойную нейросеть на Keras;
- обучите модель реализациями функции потерь, оптимизатора Adam и цикла обучения из Keras.
Импорты
Заголовок раздела «Импорты»В этой лабораторной вы в основном используете пакет keras, чтобы задавать и обучать нейросети.
import os # Чтобы менять настройки Keras.os.environ['KERAS_BACKEND'] = 'jax' # Задаём параметр Keras: вычисления — на JAX.
import jax.numpy as jnp # Чтобы задавать векторы и матрицы и работать с ними.import keras # Чтобы задавать и обучать нейросети.import pandas as pd # Чтобы загрузить и показать данные.Загружаем данные
Заголовок раздела «Загружаем данные»Запустите следующую ячейку, чтобы скачать набор двумерных эмбеддингов предложений. Как и в прошлых лабораторных, цель — предсказать следующее слово из слов «food» (числовая метка 1) и «water» (числовая метка 2).
# Загружаем данные с помощью pandas.df = pd.read_csv("https://storage.googleapis.com/dm-educational/assets/ai_foundations/food-water-dataset.csv")
# Достаём эмбеддинги и метки.X_train = jnp.array(df[["Embedding_dim_1", "Embedding_dim_2"]].values)labels = df["Label"].values # Метки: "food" или "water".# Переводим метки в числа для обучения модели (food = 1, water = 0).y_train = jnp.where(labels == "food", 1, 0)df["Numeric label"] = y_train
# Печатаем загруженные данные для проверки.df.head(n=20)Задаём нейросеть
Заголовок раздела «Задаём нейросеть»Следующая ячейка реализует функцию build_neural_network, которая задаёт двухслойную нейросеть на Keras.
Операции скрытого слоя задаются строкой
operations.append(keras.layers.Dense(hidden_dim, activation="relu"))а операции выходного слоя — строкой
operations.append(keras.layers.Dense(1, activation="sigmoid"))Подумайте, почему в выходном слое этой модели сигмоида. Можно ли заменить её на SoftMax? Когда это было бы полезно?
Изучите и документацию функции build_neural_network, и её реализацию, чтобы понять, как она задаёт нейросеть. Затем
запустите ячейку, чтобы задать функцию и использовать её дальше.
def build_neural_network(hidden_dim: int = 10) -> keras.Model: """ Создаёт на Keras двухслойную нейросеть для бинарной классификации. В скрытом слое функция активации ReLU, в выходном — сигмоида.
Аргументы: hidden_dim: размер скрытого слоя.
Возвращает: Экземпляр keras.Model с этой нейросетью. """
operations = []
# Добавляем операции скрытого слоя с функцией активации ReLU. operations.append(keras.layers.Dense(hidden_dim, activation="relu"))
# Добавляем операции выходного слоя с сигмоидной функцией активации. operations.append(keras.layers.Dense(1, activation="sigmoid"))
# Собираем модель, в которой вход последовательно проходит через все слои. model = keras.Sequential(operations) return modelЗадание 1. Задаём функцию потерь
Заголовок раздела «Задание 1. Задаём функцию потерь»Прежде чем обучать модель, нужно задать и функцию потерь, и оптимизатор. Почти всегда для обоих компонентов можно взять готовые реализации.
# Задаём функцию потерь.loss_fn = # Добавьте свой код здесь.Задание 2. Задаём оптимизатор
Заголовок раздела «Задание 2. Задаём оптимизатор»В качестве оптимизатора вы почти всегда будете использовать Adam. Он реализует более продвинутый шаг обновления по градиенту, чем обычный SGD: подстраивает размер шага под форму функции потерь. На практике этот оптимизатор очень хорошо обучает нейросети.
# Задаём оптимизатор.optimizer = # Добавьте свой код здесь.Собираем всё вместе: метод compile
Заголовок раздела «Собираем всё вместе: метод compile»Когда функция потерь и оптимизатор заданы, их можно соединить для обучения.
Сначала нужно задать модель — экземпляр класса keras.Model. В этом задании MLP задаётся функцией build_neural_network
выше.
Затем, чтобы соединить всё вместе, используется метод модели compile. Он прикрепляет к модели функцию потерь
и оптимизатор. Можно указать и необязательные метрики, например долю правильных ответов: тогда после каждой эпохи
значение метрики будет печататься в журнале обучения.
Запустите следующую ячейку, чтобы задать модель и соединить её с функцией потерь и оптимизатором.
# Фиксируем seed для воспроизводимости.keras.utils.set_random_seed(126)
# Задаём модель.model = build_neural_network(hidden_dim = 10)
# Прикрепляем функцию потерь, оптимизатор и метрики.model.compile(loss=loss_fn, optimizer=optimizer, metrics=["accuracy"])Задание 3. Обучаем модель
Заголовок раздела «Задание 3. Обучаем модель»# Обучите модель методом model.fit().history = # Добавьте свой код здесь.Поздравляем! Теперь вы знаете все шаги обучения нейросетей на Keras. Если в будущем понадобится обучить модель Keras, возвращайтесь к этому ноутбуку, чтобы освежить шаги.
Можно также поэкспериментировать: менять модель, параметры оптимизатора или процедуру обучения и смотреть, как меняется обучение.
В этом задании вы обучили простую нейросеть оптимизатором Adam. Вы узнали, как в Keras соединяются функция потерь, оптимизатор и модель, чтобы обучить любую нейросеть. Вы также узнали, где задаются гиперпараметры: скорость обучения, сила weight decay, число эпох.
В следующих курсах вы примените эти знания, чтобы обучать и дообучать языковые модели. Те модели будут сложнее MLP, с которым вы работали здесь, но принципы останутся теми же.
Решения
Заголовок раздела «Решения»Задание 1
loss_fn = keras.losses.BinaryCrossentropy()Задание 2
optimizer = keras.optimizers.Adam(learning_rate=0.01)Задание 3
# В оригинальном решении epochs=10, хотя в задании просили 100 эпох.# Поставьте 100, как в задании: на 20 точках это займёт секунды.history = model.fit(x=X_train, y=y_train, epochs=100, batch_size=8)Словарик
Заголовок раздела «Словарик»| English | Русский |
|---|---|
| hyperparameter | гиперпараметр |
| capacity (of a model) | ёмкость модели |
| held-out test set | отложенная тестовая выборка |
| weight decay | weight decay (затухание весов) |
| dropout, dropout rate | dropout (прореживание), доля прореживания |
| early stopping, patience | ранняя остановка, «терпение» |
| baseline | базовая линия |
| experiment log | журнал экспериментов |
| automatic differentiation (autodiff) | автоматическое дифференцирование |
| gradient, steepest ascent / descent | градиент, наискорейший подъём / спуск |
| learning rate | скорость обучения (learning rate) |
| gradient descent, stochastic gradient descent (SGD) | градиентный спуск, стохастический градиентный спуск |
| binary cross-entropy | бинарная перекрёстная энтропия |
| optimizer, Adam | оптимизатор, Adam |
| mini-batch, batch size | мини-батч, размер батча |
| callback | обратный вызов (callback) |