popov . dev

Main

Library

Articles

Основы библиотек...

Основы библиотеки PyTorch

Что такое PyTorch?

PyTorch - это платформа машинного обучения с открытым исходным кодом, которая обеспечивает тензорные вычисления с плавным ускорением на графическом процессоре, используя преимущества параллельной обработки и глубоких нейронных сетей с ленточной системой автоматического управления, обеспечивая гибкость за счет модификации на лету.

Установка

Установим библиотеку с помощью пакетного менеджера pip:

pip3 install torch torchvision torchaudio

Проверим что мы все установили правильно, можно вызвать в терминале интерпретатор python и запустить следующий код, для проверки установленной версии:

import torch
print(torch.__version__)

В нашем случае используется версия 2.2.2. Если код вызовет ошибку, значит что-то было установлено неправильно.

Tensor (тензор)

tensor - это универсальный n-мерный массив, такой же, как numpy array, и обеспечивающий ускоренные и эффективные вычисления на GPU.

# Создание тензора из списка 
x = torch.tensor([2.5, 0.1], [0.5, 9.6])

# Преобразование numpy массива в тензор
a = np.ones(5)
b = torch.from_numpy(a)

Autograd (автоград)

Автоград создает вычислительный график для вычисления градиентов при обратном распространении.

# создадим вычислительный график для
# вычисления градиентов в обратном направлении
x = torch.randn(3, requires_grad=True) 

# присвоим весовой вектор для вычисления градиентов
v = torch.tensor([1.0, 0.1, 0.001], dtype=torch.float32)

# вычислим dz/dx (градиенты z относительно x)
z.backward(v) # SCALAR  

# значения для векторного произведения Якобиана
print(x.grad)

Transforms (преобразование)

transforms позволяют изменять данные различными способами перед вводом их в модель.

import torchvision.transforms as transforms

# умножим входные данные на заданный коэффициент
class MulTransform:
    def __init__(self, factor):
        self.factor = factor
    def __call__(self, sample):
        inputs, targets = sample
        inputs *= self.factor
        return inputs, targets

# Создает экземпляр пользовательского класса WineDataset,
# применяя преобразование
dataset = EnergyDataset(transform=MulTransform(4))

transforms.Compose допускает цепочку из нескольких преобразований.

composed = transforms.Compose([
        transforms.RandomResizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize(mean, std)]

Dataset и DataLoader

from torch.utils.data import Dataset

# класс Dataset
class EnergyDataset(Dataset):
    def __init__(self):
        # загрузка данных
        xy = np.loadtxt('energy.csv', delimiter=',', dtype = np.float32, skiprows=1)
        self.x = torch.from_numpy(xy[:,1:])
        self.y = torch.from_numpy(xy[:,[0]]) # (n_samples, 1)
        self.n_samples = xy.shape[0]
    def __getitem__(self, index):
        # разрешаем индексацию
        return self.x[index], self.y[index]
    def __len__(self):
        # возращаем размер датасета
        return self.n_samples

# создаем экземпляр датасета
dataset = EnergyDataset()

# доступ к отдельным данным
first_data = dataset[0]
features, labels = first_data
print(features, labels)

DataLoader делит выборки на более мелкие пакеты, поскольку вычисления градиента для обучающих данных больших наборов данных занимают много времени. Здесь оптимизация основана только на пакетах.

import torchvision
from torch.utils.data import DataLoader

train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True,
    download=True, transform=composed)
test_dataset = torchvision.datasets.CIFAR10(root='./data', train=False,
    download=True, transform=composed)

train_loader = DataLoader(train_dataset, batch_size=batch_size,
    shuffle=True) # [4, 3, 32, 32]
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

Нейронная сеть с прямой связью

# Полностью подключенная нейронная сеть
# с одним скрытым слоем
class NeuralNet(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super(NeuralNet, self).__init__()
        self.l1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.l2 = nn.Linear(hidden_size, num_classes)

    def forward(self, x):
        out = self.l1(x)
        out = self.relu(out)
        out = self.l2(out)
        # не применяйте здесь softmax из-за перекрестной энтропии
        return out

model = NeuralNet(input_size, hidden_size, num_classes).to(device)

Класс NeuralNet наследуется от nn.Module и содержит два полностью связанных уровня (nn.Linear) с функцией активации ReLU между ними.

Метод forward() определяет вычисления нейронной сети, при которых входной сигнал x передается через первый полностью подключенный уровень, за которым следует функция активации ReLU и, наконец, выходной уровень.

Наконец, создается модельный экземпляр нейронной сети с определенными классами input_size, hidden_size и num_classes и перемещается на указанное устройство (GPU или CPU CPU CPU) для вычисления.

Сверточная нейронная сеть

class ConvNet(nn.Module):
    def __init__(self):
        super(ConvNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5) # 3 цветовых канала, выход - 6, ядро - 5
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16*5*5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        # -> [n, 3, 32, 32]
        out = self.pool(F.relu(self.conv1(x)))    # -> [n, 6, 14, 14]
        out = self.pool(F.relu(self.conv2(out)))  # -> [n, 16, 5, 5]
        out = out.view(-1, 16*5*5)                # -> [n, 16*5*5] == [n, 400]
        out = F.relu(self.fc1(out))               # -> [n, 120]
        out = F.relu(self.fc2(out))               # -> [n, 84]
        out = self.fc3(out)                       # -> [n, 10]
        return out 

model = ConvNet().to(device)

Класс ConvNet также наследуется от nn.Модуль. Архитектура включает в себя два сверточных уровня (conv1 и conv2) с максимальным объединением (pool) между ними. За сверточными слоями следуют три полностью связанных слоя (fc1, fc2 и fc3) для окончательной классификации.

Обучение

Criterion - это функция потерь, используемая для вычисления разницы между фактическими метками и прогнозируемыми метками во время обучения, определяющая оптимизацию. например. MSE, Cross-Entropy (перекрестная энтропия) и т.д.

Optimizer - это алгоритм оптимизации, используемый для обновления параметров модели на основе вычисленных градиентов во время обратного распространения, чтобы минимизировать потери и улучшить производительность модели. например. SGD, Adam и т.д.

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr = learning_rate)

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

  • Прямой проход: вычисление прогноза
  • Обратный проход: gradients ([w,b] = model.parameters())
  • Обновление весов: optimizer.step()
n_total_steps = len(train_loader)

for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):
        # shape: [4, 3, 32, 32], input_layer: 3 входных, 6 выходных канала, 5 размер ядра
        images = images.to(device)
        labels = labels.to(device)

        # прямой проход
        outputs = model(images)

        # потери
        loss = criterion(outputs, labels)

        # обратный проход
        loss.backward()

        # обновление весов
        optimizer.step()

        # обнуляем градиенты
        optimizer.zero_grad()

        if (i+1)%2000 == 0:
            print('Эпоха {}/{} Шаг {}/{} Потери {:.4f}'.format(epoch+1, num_epochs, i+1, total_steps, loss.item()))
Шаги (или этапы) - это отдельные обновления оптимизации, основанные на пакетах данных, в то время как Эпохи (или периоды) предполагают просмотр всего набора данных. Выбор между шагами и эпохами зависит от конкретных требований задачи машинного обучения и характеристик набора данных.

Трансфертное обучение

Отключите сеть (freeze), за исключением последнего уровня. т.е. загрузите предварительно обученную модель и сбросьте только последний полностью подключенный уровень. Короче говоря, мы используем предварительно обученные функции и выполняем тонкую настройку только последнего уровня, чтобы специализировать модель для новой задачи.

model_conv = models.resnet18(
    weights =" ResNet18_Weights.DEFAULT")
for param in model_conv.parameters():
    param.requires_grad = False # чтобы градиенты не вычислялись в backward()

num_features = model_conv.fc.in_features
model_conv.fc = nn.Linear(num_features, 2) # относится к существующему подключенному слою модели
model_conv = model_conv.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model_conv.fc.parameters(), lr=0.001, momentum=0.9

Здесь мы загружаем предварительно подготовленную модель resnet18 model_conv и замораживаем все ее параметры, кроме конечного полностью подключенного слоя model_conv.fc. Затем мы заменяем конечный полностью подключенный слой новым слоем nn.Linear(num_features, 2) для адаптации модели к задаче бинарной классификации, где размер выходных данных установлен равным 2.

Сохранение и загрузка моделей

PATH = 'model_state_dict.pth'
torch.save(model.state_dict(), PATH)

# создадим модель с сохраненными параметрами
loaded_model = Model(n_input_features=6)
loaded_model.load_state_dict(torch.load(PATH))
loaded_model.eval()
# сохраним контрольную точку
checkpoint = {
        "epoch": 90,
        "model_state": model.state_dict(),
        "optim_state": optimizer.state_dict()}
torch.save(checkpoint, "checkpoint.pth")

# загрузим контрольную точку
loaded_checkpoint = torch.load('checkpoint.pth')

model = Model(n_input_features=6)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# присвоим загруженную информацию контрольной точки
model.load_state_dict(loaded_checkpoint ['model_state'])
optimizer.load_state_dict(loaded_checkpoint ['optim_state'])
print(optimizer.state_dict())

Подводные камни

Потеря Cross-Entropy неявно применяет уровень SoftMax

При использовании функции потери перекрестной энтропии в PyTorch убедитесь, что в качестве входных данных используются логиты (необработанные оценки), а не вероятности, полученные из слоя SoftMax, поскольку для кросс-энтропии используется SoftMax. Использование вероятностей может привести к неверным результатам.

zero_grad()

Перед каждым обратным распространением градиенты параметров модели должны быть очищены, чтобы эти градиенты не накапливались в пакетах.

# обнуление градиентов
optimizer.zero_grad()

to.device()

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

# настройки устройства
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# передача данных и модели на устройство
images = images.to(device)
labels = labels.to(device)
model.to(device)

PyTorch против TensorFlow

График динамических и статических вычислений

В PyTorch используется динамический график вычислений. Это позволяет определять и изменять график "на лету" во время обучения, что способствует созданию гибкой архитектуры.

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

Распределенное обучение

PyTorch предлагает встроенную поддержку распределенного обучения, что упрощает обучение на нескольких графических процессорах и узлах, используя абстракции torch.nn.parallel.DistributedDataParallel.

TensorFlow использует TensorFlow Distributed API tf.distribute.Strategy для возможностей распределенного обучения. Это позволяет определять и управлять обучением распределенной модели.

Comments

In order to leave your opinion, you need to register on the website