Обучение и оптимизация параметров модели

Последнее обновление: 13.12.2025

Обучение модели представляет собой итеративный процесс: на каждой итерации модель делает предположение о выходных данных, вычисляет ошибку предположения (loss) и собирает производные этой ошибки по своим параметрам (как мы было описано в предыдущей статье), а затем оптимизирует эти параметры с помощью градиентного спуска (gradient descent). Рассмотрим, как мы можем оптимизировать эти параметры.

В общем случае цикла обучения выглядит следующим образом: проходим по набору данных несколько раз и выполняем в каждой итерации следующие шаги:

  1. Прямой проход: пропускаем пакет данных через модель для вычисления прогнозов.

  2. Вычисление потерь: используем функцию потерь (loss function) для вычисления разницы между прогнозами и реальными метками.

  3. Обратный проход: используем метод backward() для вычисления градиентов потерь относительно параметров модели.

  4. Обновление параметров: используем оптимизатор для обновления параметров модели на основе вычисленных градиентов

Затем этот процесс повторяется. Вместе с циклом обучения также применяется цикл проверки/тестирования, который представляет итерацию по тестовому набору данных для проверки эффективности модели.

Гиперпараметры

Для управления процессом обучения и оптимизации модели применяются специальные настраиваемые параметры, которые называются гиперпараметры (hyperparameters). Различные значения гиперпараметров могут влиять на скорость обучения. Рассмотрим гиперпараметры, применяемые для обучения:

  • Количество эпох (Epoch): количество итераций по набору данных. По сути сколько раз будет выполняться цикл обучения.

  • Размер пакета (Batch Size): количество выборок данных, прошедших через сеть до обновления параметров

  • Скорость обучения (Learning Rate): частота обновления параметров модели в каждом пакете/эпохе. Меньшие значения приводят к низкой скорости обучения, а большие могут привести к непредсказуемому поведению во время обучения.

Среди этих гиперпараметров нас прежде всего будет интересовать скорость обучения (learning rate), поскольку это, пожалуй, самый важный гиперпараметр, и выбор неправильного значения может привести к тому, что модель никогда не найдет оптимальное решение. Скорость обучения определяет, насколько большой шаг сделает оптимизатор в направлении, противоположном градиенту, чтобы минимизировать функцию потерь. Значение может колебаться в диапазоне от 0.0 до 1.0. Несколько вариантов:

  • Слишком высокий (например, 0.1 или 1.0). Расхождение (Divergence): оптимизатор перепрыгивает через минимум. Потери могут колебаться, расти или становиться NaN

  • Слишком низкий (например, 1e-6). Медленная сходимость: оптимизатор делает слишком маленькие шаги. Обучение занимает чрезмерно много времени, и модель может застрять в локальном минимуме или на плато.

  • Оптимальный: потери плавно и стабильно снижаются, модель эффективно сходится к хорошему минимуму.

  • Одним из распространенных значений является 10-3 (1e-3). Это стартовым значением по умолчанию (default starting point), особенно при использовании популярных адаптивных оптимизаторов, таких как Adam, RMSprop или Adagrad. Адаптивные Оптимизаторы (например, Adam) не используют одну и ту же скорость обучения для всех параметров. Они адаптируют и регулируют эффективную скорость обучения индивидуально для каждого параметра в зависимости от истории градиентов. В этом случае 1e-3 служит удобным общим множителем. Этот вывод был сделан эмпирически: когда был представлен оптимизатор Adam, его авторы и сообщество обнаружили, что 0.001 часто является хорошим, стабильным и безопасным выбором, который позволяет модели начать обучение без "взрыва" (когда веса расходятся).

    Однако 1e-3 - обычно это только начало. В реальных задачах, скорее всего, придется искать лучшее значение. Но в целом 1e-3 - это безопасная и часто эффективная отправная точка.

    Определение функции потерь

    Функция потерь (loss funtion) измеряет степень отличия полученного при обучении результата от целевого значения, и именно ее значение необходимо минимизировать во время обучения. Для вычисления функции потерь мы делаем прогноз, используя входные данные из нашей выборки данных, и сравниваем его с реальным значением метки данных.

    PyTorch предоставляет ряд распространенных функции потерь, в частности:

    • nn.MSELoss (среднеквадратическая ошибка) для задач регрессии

    • nn.NLLLoss (отрицательное логарифмическое правдоподобие) для задач классификации

    • nn.CrossEntropyLoss объединяет nn.LogSoftmax и nn.NLLLoss

    Возьмем в качестве примера последнюю функцию:

    loss_fn = nn.CrossEntropyLoss()

    На выходе функция потерь выдает одно число - насколько сильно модель ошиблась:

    loss_fn = nn.CrossEntropyLoss()
    ......................
    # pred - предсказание модели
    # y - правильный ответ
    loss = loss_fn(pred, y)
    

    Выбор механизма оптимизации

    Затем надо выбрать алгоритм оптимизации. Оптимизация представляет процесс настройки параметров модели для уменьшения ее ошибки на каждом этапе обучения. Алгоритмы оптимизации определяют, как выполняется этот процесс. В PyTorch доступно множество различных оптимизаторов в модуле torch.optim, таких как SGD, Adam, RMSprop и т.д.

    Вся логика оптимизации инкапсулирована в объект оптимизатора. Мы инициализируем оптимизатор, регистрируя параметры модели, которые необходимо обучить, и передавая гиперпараметр скорости обучения. К примеру, возьмем алгорит стохастического градиентного спуска (Stochastic Gradient Descent), который в PyTorch доступен через тип torch.optim.SGD:

    # model представляет модель, а  model.parameters() - ее параметры 
    # learning_rate - скорость обучения
    optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
    

    По сути оптимизатор - это "учитель". Зная ошибку, он чуть-чуть подкручивает параметры (веса) внутри слоев модели, чтобы в следующий раз ошибка была меньше.

    Стоит отметить два основных метода оптимизатора:

    • optimizer.zero_grad() - для сброса градиентов параметров модели. По умолчанию градиенты суммируются; чтобы избежать двойного учета, градиенты обнуляются на каждой итерации.

    • optimizer.step() - для корректировки параметров модели с учетом градиентов, собранных в обратном проходе.

    Пример обучения модели

    Рассмотрим обучение модели на примере встроенного набора данных datasets.FashionMNIST. Итак, определим скрипт со следующим кодом:

    import torch
    from torch import nn
    from torch.utils.data import DataLoader
    from torchvision import datasets
    from torchvision.transforms import ToTensor
    
    training_data = datasets.FashionMNIST(
        root="data",
        train=True,
        download=True,
        transform=ToTensor()
    )
    
    test_data = datasets.FashionMNIST(
        root="data",
        train=False,
        download=True,
        transform=ToTensor()
    )
    
    train_dataloader = DataLoader(training_data, batch_size=64)
    test_dataloader = DataLoader(test_data, batch_size=64)
    
    class NeuralNetwork(nn.Module):
        def __init__(self):
            super().__init__()
            self.flatten = nn.Flatten()
            self.linear_relu_stack = nn.Sequential(
                nn.Linear(28*28, 512),
                nn.ReLU(),
                nn.Linear(512, 512),
                nn.ReLU(),
                nn.Linear(512, 10),
            )
    
        def forward(self, x):
            x = self.flatten(x)
            logits = self.linear_relu_stack(x)
            return logits
    
    
    # обучение модели
    def train_loop(dataloader, model, loss_fn, optimizer, device):
        size = len(dataloader.dataset)
        # устанавливаем модель в режим обучения. 
        # хотя в данном случае это необязательно, но в целом может быть важно для пакетной нормализации и отсева слоев
        model.train()
        for batch, (X, y) in enumerate(dataloader):
    
            # перемещаем тензоры на устройство
            X = X.to(device)
            y = y.to(device)
    
            # Вычисляем предсказание (Forward pass)
            pred = model(X)
            # Вычисляем ошибку (Loss)
            loss = loss_fn(pred, y)
    
            # Обратное распространение ошибки (Backpropagation)
            loss.backward()         # Считаем новые градиенты
            optimizer.step()        # Обновляем веса
            optimizer.zero_grad()   # Сбрасываем градиенты с прошлого шага
    
            if batch % 100 == 0:
                loss, current = loss.item(), batch * batch_size + len(X)
                print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")
    
    
    def test_loop(dataloader, model, loss_fn, device):
        # устанавливаем модель в режим оценки 
        # хотя в данном случае это необязательно, но в целом может быть важно для пакетной нормализации и отсева слоев
        model.eval()
        size = len(dataloader.dataset)
        num_batches = len(dataloader)
        test_loss, correct = 0, 0
    
        # torch.no_grad() отключает вычисление градиентов для экономии памяти,
        # так как при тесте мы не обучаем модель
        with torch.no_grad():
            for X, y in dataloader:
                # перемещаем тензоры на устройство
                X = X.to(device)
                y = y.to(device)
    
                pred = model(X)
                test_loss += loss_fn(pred, y).item()
                # Считаем количество верных ответов
                correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    
        test_loss /= num_batches
        correct /= size
        print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
    
    
    # определяем устройство для обучения модели
    device = torch.accelerator.current_accelerator().type if torch.accelerator.is_available() else "cpu"
    print(device)
    model = NeuralNetwork().to(device)     # устанавливаем модель и перемещаем ее на устройство
    
    learning_rate = 1e-3        # Скорость обучения
    batch_size = 64             # Размер пакета
    epochs = 5                 # Количество эпох
    
    # устанавливаем функцию потерь
    # CrossEntropyLoss отлично подходит для классификации
    loss_fn = nn.CrossEntropyLoss()
    # устанавливаем оптимизатор (SGD - стохастический градиентный спуск)
    # он обновляет веса модели, основываясь на данных
    optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
    
    for t in range(epochs):
        print(f"Эпоха: {t+1}\n-------------------------------")
        train_loop(train_dataloader, model, loss_fn, optimizer, device)
        test_loop(test_dataloader, model, loss_fn, device)
    print("Завершено!")
    

    Рассмотрим этот код поэтапно, хотя с большей частью мы уже сталкивались.

    1. Определение модели

    Прежде всего на основе набора FashionMNIST определим по набору для обучения и тестирования, а также определяем класс модели:

    import torch
    from torch import nn
    from torch.utils.data import DataLoader
    from torchvision import datasets
    from torchvision.transforms import ToTensor
    
    training_data = datasets.FashionMNIST(
        root="data",
        train=True,
        download=True,
        transform=ToTensor()
    )
    
    test_data = datasets.FashionMNIST(
        root="data",
        train=False,
        download=True,
        transform=ToTensor()
    )
    
    train_dataloader = DataLoader(training_data, batch_size=64)
    test_dataloader = DataLoader(test_data, batch_size=64)
    
    class NeuralNetwork(nn.Module):
        def __init__(self):
            super().__init__()
            self.flatten = nn.Flatten()
            self.linear_relu_stack = nn.Sequential(
                nn.Linear(28*28, 512),
                nn.ReLU(),
                nn.Linear(512, 512),
                nn.ReLU(),
                nn.Linear(512, 10),
            )
    
        def forward(self, x):
            x = self.flatten(x)
            logits = self.linear_relu_stack(x)
            return logits
    

    2. Функция обучения train_loop

    Функция train_loop реализует один цикл обучения (эпоху) модели (нейронной сети) - прогоняет данные через сеть, считает ошибку и обновляет веса (учит модель). Эта функция принимает 5 параметров, первые 4 из которых являются стандартными компонентами обучения модели в PyTorch:

    def train_loop(dataloader, model, loss_fn, optimizer, device)
    • dataloader: загрузчик данных, который предоставляет итератор для получения пакетов (батчей) данных (X, y), где X - входные признаки, а y - целевые метки.

    • model: обучаемая модель (например, экземпляр класса nn.Module).

    • loss_fn: функция потерь (критерий), которая измеряет ошибку предсказания модели (например, MSE, CrossEntropyLoss).

    • optimizer: оптимизатор, который обновляет веса модели на основе градиентов (например, SGD, Adam).

    • device: ускоритель (при его наличии).

    В начале функции получаем общее количество образцов в наборе данных для отслеживания прогресса.

    size = len(dataloader.dataset)

    Далее устанавливаем модель в режим обучения. Это критически важно для слоев, которые ведут себя по-разному при обучении (например, Dropout или BatchNorm).

    model.train()

    Далее выполняем итерацию по пакетам данных:

    for batch, (X, y) in enumerate(dataloader):

    Здесь начинается цикл по всем пакетам (батчам) данных, предоставленных объектом dataloader, где

    • batch: индекс текущего пакета.

    • X: входные данные (признаки) текущего пакета.

    • y: истинные метки (целевые значения) для текущего пакета.

    Вначале перемещаем тензоры на ускоритель (если он доступен):

    X = X.to(device)
    y = y.to(device)
    

    Затем выполняется прямой проход (Forward Pass) - передача модели

    pred = model(X)

    Входные данные X подаются в модель, чтобы получить предсказания pred.

    Используя полученные результаты, вычисляем значение потерь (ошибки) путем сравнения предсказаний pred с истинными метками y с помощью заданной функции потерь loss_fn:

    loss = loss_fn(pred, y)

    И далее в дело вступает ключевой момент - обратное распространение (Backpropagation) и оптимизация. Этот этап и является сутью обучения нейронных сетей:

    • loss.backward()

      Обратное распространение (Backpropagation): вычисляются градиенты функции потерь относительно всех обучаемых параметров модели. Эти градиенты сохраняются в атрибуте .grad каждого параметра.

    • optimizer.step()

      Шаг оптимизатора: используя вычисленные градиенты, оптимизатор обновляет параметры модели (веса и смещения) в направлении, уменьшающем потери, согласно выбранному алгоритму оптимизации.

    • optimizer.zero_grad()

      Обнуление градиентов: Градиенты накапливаются в PyTorch. После того как параметры были обновлены, необходимо обнулить градиенты для следующего пакета, чтобы избежать смешивания градиентов от разных пакетов.

    В конце идет отслеживание прогресса для наглядного вывода на консоль:

    1. if batch % 100 == 0:

      Блок для печати информации о прогрессе на каждые 100 пакетов

    2. loss, current = loss.item(), batch * batch_size + len(X)

      Получает скалярное значение потерь (loss.item()) и вычисляет количество обработанных примеров (current)

    3. print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")

      Печатает текущее значение потерь и прогресс обучения (сколько образцов обработано из общего числа)

    Если вкратце суммировать, то функция train_loop выполняет итерацию по всему обучающему набору данных, обновляя веса модели для каждого пакета данных с помощью стандартного цикла обучения: Прямой проход -> Вычисление потерь -> Обратное распространение -> Шаг оптимизатора.

    3. Функция тестирования test_loop

    Функция test_loop предназначена для прохода одного цикла (эпохи) тестирования модели. Она проверяет модель на тестовых данных, чтобы мы видели реальную точность (Accuracy). Эта функция принимает аналогичные 4 параметра:

    def test_loop(dataloader, model, loss_fn, device):
    • dataloader: загрузчик данных, который предоставляет итератор для получения пакетов (батчей) данных (X, y), где X - входные признаки, а y - целевые метки.

    • model: обучаемая модель (например, экземпляр класса nn.Module).

    • loss_fn: функция потерь (критерий), которая измеряет ошибку предсказания модели (например, MSE, CrossEntropyLoss).

    • device: ускоритель (при его наличии).

    В начале функции устанавливаем модель в режим тестирования (оценки)

    model.eval()

    Это важно для ряда типов слоев (например, Dropout или BatchNorm).

    Далее выполняем инициализацию метрик:

    size = len(dataloader.dataset)      # определяем общее количество примеров (образцов) в датасете
    num_batches = len(dataloader)       # подсчитываем количество батчей
    test_loss, correct = 0, 0           # инициализируем счетчики для потерь и правильных предсказаний
    

    Затем определяем блок без вычисления градиентов

    with torch.no_grad():

    Благодаря этому отключается вычисление градиентов (нам же не надо ничего обучать и оптимизировать) и предотвращает накопление истории вычислений, что ускоряет выполнение и экономит память

    Далее выполняем итерацию по пакетам данных:

    for X, y in dataloader:

    Здесь начинается цикл по всем пакетам (батчам) данных, предоставленных объектом dataloader, где

    • X: входные данные (признаки) текущего пакета.

    • y: истинные метки (целевые значения) для текущего пакета.

    Вначале перемещаем тензоры на ускоритель (если он доступен):

    X = X.to(device)
    y = y.to(device)
    

    Пропускаем данные через модель для получения предсказаний

    pred = model(X)

    Входные данные X подаются в модель, чтобы получить предсказания pred.

    Используя полученные результаты, вычисляем значение потерь (ошибки) путем сравнения предсказаний pred с истинными метками y с помощью заданной функции потерь loss_fn:

    test_loss += loss_fn(pred, y).item()

    И подсчитываем количество правильных предсказаний (сравнивая предсказанный класс с истинным)

    correct += (pred.argmax(1) == y).type(torch.float).sum().item()

    Здесь следуюет отметить вызов pred.argmax(1) - это важная операция для задач классификации. В данном случае pred - это тензор в формате [batch_size, num_classes] с предсказаниями модели, которые обычно представляют собой вероятности. Например, если batch_size=3 и есть 4 классов: [3, 4]

    А вызов функции argmax() находит индекс максимального значения. Аргумент 1 (dim=1) указывает, что поиск максимума идет по второму измерению (по классам). (Если было бы dim=0, то поиск шел бы по батчу). Показательный небольшой пример:

    pred = torch.tensor([[ 2.1, -1.0,  0.5,  1.2],  # пример (образец) 1
            [-0.5,  3.2,  1.0,  0.1],  # пример 2
            [ 0.3,  0.8, -0.2,  1.5]]) # пример 3
    pred_max = pred.argmax(1)
    print(pred_max)     # tensor([0, 1, 3])
    

    В задачах классификации модель выдает "уверенность" для каждого класса. Нам нужен конкретный предсказанный класс - тот, у которого наибольшая уверенность, и argmax(1) эффективно выполняет эту операцию для всего батча одновременно.

    В контексте же всего выражения:

    (pred.argmax(1) == y).type(torch.float).sum().item()

    мы получаем следующее:

    • pred.argmax(1): получаем предсказанные классы (индексы от 0 до num_classes-1)

    • == y: сравниваем с истинными метками (y содержит истинные индексы классов) Получаем булев тензор (True/False), где True = правильное предсказание

    • .type(torch.float): преобразуем True -> 1.0, False -> 0.0

    • .sum(): суммируем все единицы (подсчитываем правильные предсказания)

    • .item(): извлекаем числовое значение из тензора

    В конце также идет вывод на консоль итоговых метрик:

    test_loss /= num_batches    # Средние потери: суммарные потери делятся на количество батчей
    correct /= size             # Точность (accuracy): количество правильных предсказаний делится на общее количество примеров
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
    

    Таким образом, функция test_loop не обучает модель, не обновляет веса модели, а только оценивает в пакетном режиме для обработки больших данных, вычисляя две основные метрики: точность и средние потери.

    4. Установка модели и параметров

    Далее определяем все необходимые компоненты: модель, устройство, параметры модели, функцию потерь и оптимизации:

    device = torch.accelerator.current_accelerator().type if torch.accelerator.is_available() else "cpu"
    model = NeuralNetwork().to(device)     # устанавливаем модель и перемещаем ее на устройство
    
    learning_rate = 1e-3        # Скорость обучения
    batch_size = 64             # Размер пакета
    epochs = 5                  # Количество эпох
    
    # устанавливаем функцию потерь
    loss_fn = nn.CrossEntropyLoss()
    # устанавливаем оптимизатор
    optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
    

    5. Выполнение обучения

    И в конце собственно выполняем цикл обучения, количество итераций которого равно количеству заданных эпох:

    for t in range(epochs):
        print(f"Эпоха: {t+1}\n-------------------------------")
        train_loop(train_dataloader, model, loss_fn, optimizer, device)
        test_loop(test_dataloader, model, loss_fn, device)
    

    Запустим приложение на выполнение, и у нас получится вывод типа следующего:

    Эпоха: 1
    -------------------------------
    loss: 2.298254  [   64/60000]
    loss: 2.291720  [ 6464/60000]
    loss: 2.268389  [12864/60000]
    loss: 2.269258  [19264/60000]
    loss: 2.259001  [25664/60000]
    loss: 2.219967  [32064/60000]
    loss: 2.237054  [38464/60000]
    loss: 2.201306  [44864/60000]
    loss: 2.194070  [51264/60000]
    loss: 2.165086  [57664/60000]
    Test Error: 
     Accuracy: 39.2%, Avg loss: 2.164901 
    
    Эпоха: 2
    -------------------------------
    loss: 2.173971  [   64/60000]
    loss: 2.166871  [ 6464/60000]
    loss: 2.108236  [12864/60000]
    loss: 2.123794  [19264/60000]
    loss: 2.085196  [25664/60000]
    loss: 2.015075  [32064/60000]
    loss: 2.052858  [38464/60000]
    loss: 1.975453  [44864/60000]
    loss: 1.975253  [51264/60000]
    loss: 1.901547  [57664/60000]
    Test Error: 
     Accuracy: 57.5%, Avg loss: 1.906210 
    
    Эпоха: 3
    -------------------------------
    loss: 1.937953  [   64/60000]
    loss: 1.912392  [ 6464/60000]
    loss: 1.793437  [12864/60000]
    loss: 1.829720  [19264/60000]
    loss: 1.731108  [25664/60000]
    loss: 1.673018  [32064/60000]
    loss: 1.703881  [38464/60000]
    loss: 1.607794  [44864/60000]
    loss: 1.624694  [51264/60000]
    loss: 1.511455  [57664/60000]
    Test Error: 
     Accuracy: 61.0%, Avg loss: 1.537654 
    
    Эпоха: 4
    -------------------------------
    loss: 1.603127  [   64/60000]
    loss: 1.570501  [ 6464/60000]
    loss: 1.420748  [12864/60000]
    loss: 1.487924  [19264/60000]
    loss: 1.371720  [25664/60000]
    loss: 1.362219  [32064/60000]
    loss: 1.388026  [38464/60000]
    loss: 1.314473  [44864/60000]
    loss: 1.340730  [51264/60000]
    loss: 1.230611  [57664/60000]
    Test Error: 
     Accuracy: 63.0%, Avg loss: 1.266346 
    
    Эпоха: 5
    -------------------------------
    loss: 1.341387  [   64/60000]
    loss: 1.324235  [ 6464/60000]
    loss: 1.160291  [12864/60000]
    loss: 1.263710  [19264/60000]
    loss: 1.134911  [25664/60000]
    loss: 1.157993  [32064/60000]
    loss: 1.192024  [38464/60000]
    loss: 1.130541  [44864/60000]
    loss: 1.159552  [51264/60000]
    loss: 1.063148  [57664/60000]
    Test Error: 
     Accuracy: 64.1%, Avg loss: 1.096730 
    
    Завершено!
    

    Мы видим, что с каждой новой эпохой/итерацией показатель Accuracy (точность) увеличивается, а показатель loss/Avg loss (значение ошибки) уменьшается, что говорит об улучшении качества модели. Но мы можем увеличить количество эпох и тем самым еще больше увеличить показатели точности модели.

    Помощь сайту
    Юмани:
    410011174743222
    Номер карты:
    4048415020898850