Библиотека PyTorch: нейронные сети на Python
Основы работы с PyTorch
PyTorch (torch) - библиотека с открытым исходным кодом для задач глубокого обучения. Основные компоненты: тензоры (аналоги массивов NumPy с поддержкой GPU), автоматическое дифференцирование (autograd) для вычисления градиентов, модули для построения нейронных сетей (nn.Module) и утилиты для загрузки данных.
Рассмотрим построение простой полносвязной сети для классификации изображений (набор MNIST). Сначала импортируем необходимые модули:
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.transforms as transforms
import torchvision.datasets as datasets
from torch.utils.data import DataLoader
Torch python (фреймворк pytorch)
Загружаем данные и создаём загрузчик:
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)
библиотека torch python (библиотека pytorch в python)
Определяем нейронную сеть:
class SimpleNet(nn.Module):
def __init__(self):
super(SimpleNet, self).__init__()
self.fc1 = nn.Linear(784, 128)
self.fc2 = nn.Linear(128, 64)
self.fc3 = nn.Linear(64, 10)
self.relu = nn.ReLU()
def forward(self, x):
x = x.view(-1, 784) # из [batch,1,28,28] в [batch,784]
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.fc3(x)
return x
model = SimpleNet()
классификация изображений python с использованием resnet50 (классификация изображений с resnet50)
Задаём функцию потерь и оптимизатор:
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
Цикл обучения (одна эпоха):
for epoch in range(1):
model.train()
for images, labels in train_loader:
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# Оценка на тестовой выборке
model.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
outputs = model(images)
_, predicted = torch.max(outputs, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f'Accuracy: {100 * correct / total:.2f}%')
Этот код составляет основу обучения любой нейронной сети в PyTorch.
Типичные ошибки и их решение:
- Ошибка размерности: при передаче изображений без преобразования в одномерный вектор. Решение: использовать
x.view(-1, 784)илиnn.Flatten(). - Потери не уменьшаются: возможно, слишком большой или малый learning rate. Стоит проверить значения лосса на первых итерациях.
- Ошибка CUDA out of memory: уменьшить batch_size или использовать меньшую модель.
Как создать нейронную сеть без использования nn.Module?
Можно определить слои вручную и использовать только функционал nn.functional, но такой подход менее нагляден и редко применяется на практике.
import torch.nn.functional as F
class SimpleNetManual(nn.Module):
def __init__(self):
super().__init__()
self.fc1_weight = nn.Parameter(torch.randn(784, 128) * 0.1)
self.fc1_bias = nn.Parameter(torch.zeros(128))
self.fc2_weight = nn.Parameter(torch.randn(128, 64) * 0.1)
self.fc2_bias = nn.Parameter(torch.zeros(64))
self.fc3_weight = nn.Parameter(torch.randn(64, 10) * 0.1)
self.fc3_bias = nn.Parameter(torch.zeros(10))
def forward(self, x):
x = x.view(-1, 784)
x = F.relu(F.linear(x, self.fc1_weight, self.fc1_bias))
x = F.relu(F.linear(x, self.fc2_weight, self.fc2_bias))
x = F.linear(x, self.fc3_weight, self.fc3_bias)
return x
Проблема:
Неудобно управлять весами, проще использоватьnn.Linear.Как использовать кастомную функцию потерь?
Можно создать собственный класс, наследующий nn.Module, или простую функцию.
def mae_loss(output, target):
return torch.mean(torch.abs(output - target))
# Использование в цикле
loss = mae_loss(predictions, targets)
Ошибка:
Необходимо обеспечить дифференцируемость функции. Использовать операции из torch, а не из Python.Как переключиться на GPU?
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
# ... остальной код
Типичная ошибка:
Если данные остались на CPU, модель не сможет обработать их - произойдёт исключение. Проверятьimages.device == model.device.Как сохранить и загрузить модель?
torch.save(model.state_dict(), 'model.pth')
# Загрузка
model = SimpleNet()
model.load_state_dict(torch.load('model.pth', map_location=device))
model.eval()
Распространённая ошибка:
Загружать state_dict в модель с другой архитектурой. Keys должны совпадать.Расширенные примеры работы с PyTorch
Использование nn.Sequential для создания модели
Модель можно описать последовательным контейнером, что компактно, но менее гибко для сложных архитектур.
model = nn.Sequential(
nn.Flatten(),
nn.Linear(784, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 10)
)
Результат: создаётся тот же классификатор, что и в примере выше.
Пользовательский модуль с несколькими слоями и пропуском соединения (skip connection)
Этот пример показывает, как объединять несколько слоёв внутри одного модуля.
class ResidualBlock(nn.Module):
def __init__(self, in_features, out_features):
super().__init__()
self.linear1 = nn.Linear(in_features, out_features)
self.bn1 = nn.BatchNorm1d(out_features)
self.relu = nn.ReLU()
self.linear2 = nn.Linear(out_features, out_features)
self.bn2 = nn.BatchNorm1d(out_features)
self.shortcut = nn.Identity() if in_features == out_features else nn.Linear(in_features, out_features)
def forward(self, x):
identity = self.shortcut(x)
out = self.relu(self.bn1(self.linear1(x)))
out = self.bn2(self.linear2(out))
out += identity
return self.relu(out)
class ResNetLike(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.block1 = ResidualBlock(256, 256)
self.block2 = ResidualBlock(256, 128)
self.fc2 = nn.Linear(128, 10)
self.flatten = nn.Flatten()
def forward(self, x):
x = self.flatten(x)
x = self.fc1(x)
x = self.block1(x)
x = self.block2(x)
x = self.fc2(x)
return x
model = ResNetLike()
print(model)
Проблема:
Неправильные размерности при пропуске. Перед сложением нужно убедиться, чтоshortcut выдаёт тензор той же формы, что и основной поток.Использование планировщика скорости обучения (scheduler)
Планировщики изменяют learning rate во время обучения, что часто улучшает сходимость.
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)
for epoch in range(10):
# обучение...
scheduler.step()
print(f"Epoch {epoch}, LR: {optimizer.param_groups[0]['lr']:.6f}")
Epoch 0, LR: 0.010000 Epoch 1, LR: 0.010000 Epoch 2, LR: 0.010000 Epoch 3, LR: 0.001000 ...
Трансферное обучение с предобученной моделью (torchvision)
Загружаем предобученный ResNet-18 и заменяем последний полносвязный слой для нового числа классов.
import torchvision.models as models
model = models.resnet18(pretrained=True)
# Заморозить все слои, кроме последнего
for param in model.parameters():
param.requires_grad = False
# Заменяем выходной слой
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 10) # для 10 классов
# Тренируем только последний слой
optimizer = optim.SGD(model.fc.parameters(), lr=0.001)
Работа с нестандартными данными: пользовательский Dataset
Пример Dataset, который загружает изображения из папки и применяет аугментацию.
from PIL import Image
import os
class CustomDataset(torch.utils.data.Dataset):
def __init__(self, root_dir, transform=None):
self.root_dir = root_dir
self.transform = transform
self.images = [f for f in os.listdir(root_dir) if f.endswith('.png')]
def __len__(self):
return len(self.images)
def __getitem__(self, idx):
img_path = os.path.join(self.root_dir, self.images[idx])
image = Image.open(img_path).convert('RGB')
if self.transform:
image = self.transform(image)
label = 0 # пример, можно загрузить из имени файла
return image, label
Ручное вычисление градиентов без autograd (не рекомендуется)
Для образовательных целей можно реализовать обратное распространение вручную.
x = torch.randn(3, requires_grad=False)
w = torch.randn(3, requires_grad=True)
b = torch.randn(1, requires_grad=True)
y = (x * w).sum() + b
# Принудительно вызвать backward
y.backward()
print(w.grad) # градиент по w
tensor([ 0.2134, -1.2356, 0.7843])
Использование нескольких оптимизаторов для разных частей модели
Например, для GAN: дискриминатор и генератор обучаются с разными learning rate.
optimizerD = optim.Adam(discriminator.parameters(), lr=0.0002)
optimizerG = optim.Adam(generator.parameters(), lr=0.0001)
Параллельное обучение на нескольких GPU (DataParallel)
if torch.cuda.device_count() > 1:
model = nn.DataParallel(model)
model.to(device)
Проблема:
DataParallel может вызывать медленную работу из-за накладных расходов. Для современных архитектур лучше использовать DistributedDataParallel.