Обучение модели представляет собой итеративный процесс: на каждой итерации модель делает предположение о выходных данных, вычисляет ошибку предположения (loss) и собирает производные этой ошибки по своим параметрам (как мы было описано в предыдущей статье), а затем оптимизирует эти параметры с помощью градиентного спуска (gradient descent). Рассмотрим, как мы можем оптимизировать эти параметры.
В общем случае цикла обучения выглядит следующим образом: проходим по набору данных несколько раз и выполняем в каждой итерации следующие шаги:
Прямой проход: пропускаем пакет данных через модель для вычисления прогнозов.
Вычисление потерь: используем функцию потерь (loss function) для вычисления разницы между прогнозами и реальными метками.
Обратный проход: используем метод backward() для вычисления градиентов потерь относительно параметров модели.
Обновление параметров: используем оптимизатор для обновления параметров модели на основе вычисленных градиентов
Затем этот процесс повторяется. Вместе с циклом обучения также применяется цикл проверки/тестирования, который представляет итерацию по тестовому набору данных для проверки эффективности модели.
Для управления процессом обучения и оптимизации модели применяются специальные настраиваемые параметры, которые называются гиперпараметры (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("Завершено!")
Рассмотрим этот код поэтапно, хотя с большей частью мы уже сталкивались.
Прежде всего на основе набора 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
Функция 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. После того как параметры были обновлены, необходимо обнулить градиенты для следующего пакета, чтобы избежать смешивания градиентов от разных пакетов.
В конце идет отслеживание прогресса для наглядного вывода на консоль:
if batch % 100 == 0:
Блок для печати информации о прогрессе на каждые 100 пакетов
loss, current = loss.item(), batch * batch_size + len(X)
Получает скалярное значение потерь (loss.item()) и вычисляет количество обработанных примеров (current)
print(f"loss: {loss:>7f} [{current:>5d}/{size:>5d}]")
Печатает текущее значение потерь и прогресс обучения (сколько образцов обработано из общего числа)
Если вкратце суммировать, то функция train_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 не обучает модель, не обновляет веса модели, а только оценивает в пакетном режиме для обработки больших данных, вычисляя две основные метрики:
точность и средние потери.
Далее определяем все необходимые компоненты: модель, устройство, параметры модели, функцию потерь и оптимизации:
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)
И в конце собственно выполняем цикл обучения, количество итераций которого равно количеству заданных эпох:
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 (значение ошибки) уменьшается, что говорит об улучшении качества модели. Но мы можем увеличить количество эпох и тем самым еще больше
увеличить показатели точности модели.