От нуля до героя Создайте свою первую модель машинного обучения с помощью PyTorch

Создайте свою первую модель машинного обучения с PyTorch

 

Мотивация

 

PyTorch – самый популярный фреймворк глубокого обучения на основе Python. Он обеспечивает огромную поддержку для всех архитектур машинного обучения и конвейеров данных. В этой статье мы рассмотрим основы фреймворка, чтобы помочь вам начать реализацию ваших алгоритмов.

Все реализации машинного обучения имеют 4 основных этапа:

  • Обработка данных
  • Архитектура модели
  • Цикл обучения
  • Оценка

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

 

Импорты

 

import torch
import torch.nn as nn
import torch.optim as  optim

from torch.utils.data import DataLoader

# Используем набор данных MNIST, предоставленный PyTorch
from torchvision.datasets.mnist import MNIST
import torchvision.transforms as transforms

# Импорт модели, реализованной в другом файле
from model import Classifier

import matplotlib.pyplot as plt

 

Модуль torch.nn обеспечивает поддержку архитектур нейронных сетей и имеет встроенные реализации популярных слоев, таких как Dense Layers, Convolutional Neural Networks и многих других.

Модуль torch.optim предоставляет реализации оптимизаторов, таких как стохастический градиентный спуск и Adam.

Другие вспомогательные модули предоставляют поддержку обработки данных и преобразований. Мы более подробно рассмотрим каждый из них позже.

 

Определение гиперпараметров

 

Каждый гиперпараметр будет подробно объяснен, где это уместно. Однако наилучшей практикой является их объявление в начале файла для удобства изменения и понимания.

INPUT_SIZE = 784 # Выровненные изображения размером 28x28
NUM_CLASSES = 10    # Рукописные цифры от 0 до 9.
BATCH_SIZE = 128    # Использование мини-пакетов для обучения
LEARNING_RATE = 0.01    # Шаг оптимизатора
NUM_EPOCHS = 5      # Общее количество эпох обучения

 

Загрузка данных и преобразования

 

data_transforms = transforms.Compose([
        transforms.ToTensor(),
        transforms.Lambda(lambda x: torch.flatten(x))
])

train_dataset = MNIST(root=".data/", train=True, download=True, transform=data_transforms)


test_dataset = MNIST(root=".data/", train=False, download=True, transform=data_transforms)

 

MNIST – популярный набор данных для классификации изображений, предоставляемый по умолчанию в PyTorch. Он состоит из черно-белых изображений 10 рукописных цифр от 0 до 9. Каждое изображение имеет размер 28 пикселей на 28 пикселей, и набор данных содержит 60000 обучающих и 10000 тестовых изображений.

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

Однако мы также передаем дополнительный аргумент transform. В PyTorch все входы и выходы должны находиться в формате Torch.Tensor. Это эквивалент numpy.ndarray в numpy. Этот формат тензора обеспечивает дополнительную поддержку для обработки данных. Однако данные MNIST, которые мы загружаем, находятся в формате PIL.Image. Нам нужно преобразовать изображения в совместимые с PyTorch тензоры. Следовательно, мы передаем следующие преобразования:

data_transforms = transforms.Compose([
        transforms.ToTensor(),
        transforms.Lambda(lambda x: torch.flatten(x))
])

 

Преобразование ToTensor() преобразует изображения в формат тензора. Затем мы передаем дополнительное преобразование Lambda. Функция Lambda позволяет нам реализовать пользовательские преобразования. Здесь мы объявляем функцию для выравнивания входных данных. Изображения имеют размер 28×28, но мы выравниваем их, то есть преобразуем их в одномерный массив размером 28×28 или 784. Это будет важно позже, когда мы реализуем нашу модель.

Функция Compose последовательно объединяет все преобразования. Сначала данные преобразуются в формат тензора, а затем выравниваются в одномерный массив.

Разделение данных на пакеты

В вычислительных и тренировочных целях невозможно передавать полный набор данных в модель сразу. Нам необходимо разделить наш набор данных на мини-пакеты, которые будут подаваться модели последовательно. Это позволяет ускорить обучение и добавить случайность в наш набор данных, что может помочь в стабильном обучении.

PyTorch предоставляет встроенную поддержку для создания пакетов из наших данных. Класс DataLoader из модуля torch.utils может создать пакеты данных на основе модуля torch dataset. Как уже упоминалось выше, у нас уже загружен набор данных.

train_dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
test_dataloader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)

Мы передаем набор данных в наш загрузчик данных и наш гиперпараметр batch_size в качестве аргументов инициализации. Это создает итерируемый загрузчик данных, поэтому мы легко можем итерироваться по каждому пакету, используя простой цикл for.

Исходное изображение имело размерность (784, ) с одной связанной меткой. Затем пакетирование объединяет разные изображения и метки в пакете. Например, если у нас размер пакета равен 64, размер ввода в пакете станет (64, 784), и у нас будет 64 связанные метки для каждого пакета.

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

Определение нашей модели классификации

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

Как описано выше, у нас есть входной тензор размером (784, ) и 10 разных выходных классов, один для каждой цифры от 0 до 9.

** Для реализации модели мы можем игнорировать размер пакета.

import torch
import torch.nn as nn


class Classifier(nn.Module):
    def __init__(
            self,
            input_size:int,
            num_classes:int
        ) -> None:
        super().__init__()
        self.input_layer = nn.Linear(input_size, 512)
        self.hidden_1 = nn.Linear(512, 256)
        self.hidden_2 = nn.Linear(256, 128)
        self.output_layer = nn.Linear(128, num_classes)

        self.activation = nn.ReLU()
     
    def forward(self, x):
        # Пропускаем вход последовательно через каждый слой и активацию
        x = self.activation(self.input_layer(x))
        x = self.activation(self.hidden_1(x))
        x = self.activation(self.hidden_2(x))
        return self.output_layer(x)

Во-первых, модель должна наследоваться от класса torch.nn.Module. Это обеспечивает основные функциональные возможности для архитектур нейронных сетей. Затем мы должны реализовать два метода, __init__ и forward.

В методе __init__ мы объявляем все слои, которые будет использовать модель. Мы используем слои Linear (также называемые Dense) предоставляемые PyTorch. Первый слой отображает вход на 512 нейронов. Мы можем передать input_size в качестве параметра модели, чтобы мы могли использовать его для входа разных размеров. Второй слой отображает 512 нейронов на 256. Третий скрытый слой отображает 256 нейронов из предыдущего слоя на 128. Затем финальный слой наконец уменьшается до размера выхода. Наш размер выхода будет тензором размером (10, ), потому что мы предсказываем десять разных чисел.

Кроме того, мы инициализируем слой активации ReLU для добавления нелинейности в нашу модель.

Функция forward получает изображения, и мы предоставляем код для обработки ввода. Мы используем объявленные слои и последовательно пропускаем наш ввод через каждый слой с промежуточным слоем активации ReLU.

В основном коде мы можем затем инициализировать модель, указав ей размер ввода и вывода для нашего набора данных.

model = Classifier(input_size=784, num_classes=10)
model.to(DEVICE)

После инициализации мы изменяем устройство модели (которое может быть либо CUDA GPU, либо CPU). Мы проверили наше устройство при инициализации гиперпараметров. Теперь нам нужно вручную изменить устройство для наших тензоров и слоев модели.

 

Цикл обучения

 

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

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)

 

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

Мы используем функцию потерь Cross-Entropy, которая применяется в основном для моделей многоклассовой классификации. Она первым делом применяет функцию softmax к прогнозам и вычисляет заданные целевые метки и предсказанные значения.

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

Для нашего цикла обучения мы создаем шаг за шагом и заполняем недостающие части по мере нашего понимания.

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

for epoch in range(NUM_EPOCHS):
        for batch in iter(train_dataloader):
          # Обучаем модель для каждого пакета.

 

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

for epoch in range(NUM_EPOCHS):
        for batch in iter(train_dataloader):
            images, labels = batch # Разделяем входные данные и метки
            # Преобразуем тензорные устройства оборудования на GPU или CPU
            images = images.to(DEVICE)
            labels = labels.to(DEVICE)
         
            # Вызываем функцию model.forward() для генерации прогнозов 
            predictions = model(images)

 

Мы передаем пакет изображений непосредственно в модель, который будет обработан функцией forward, определенной внутри модели. После получения прогнозов мы можем оптимизировать веса нашей модели.

Код оптимизации выглядит следующим образом:

# Вычисляем функцию потерь Cross Entropy
loss = criterion(predictions, labels)
# Очищаем значения градиента с предыдущего пакета
optimizer.zero_grad()
# Вычисляем градиент обратного распространения на основе потери
loss.backward()
# Оптимизируем веса модели
optimizer.step()

 

С использованием приведенного выше кода мы можем вычислить все градиенты обратного распространения и оптимизировать веса модели с использованием оптимизатора Adam. Все вышеуказанные коды вместе могут обучить нашу модель до сходимости.

Полный цикл обучения выглядит следующим образом:

for epoch in range(NUM_EPOCHS):
        total_epoch_loss = 0
        steps = 0
        for batch in iter(train_dataloader):
            images, labels = batch # Разделяем входные данные и метки
            # Преобразуем тензорные устройства оборудования на GPU или CPU
            images = images.to(DEVICE)
            labels = labels.to(DEVICE)
         
            # Вызываем функцию model.forward() для генерации прогнозов         
            predictions = model(images)
         
            # Вычисляем функцию потерь Cross Entropy
            loss = criterion(predictions, labels)
            # Очищаем значения градиента с предыдущего пакета
            optimizer.zero_grad()
            # Вычисляем градиент обратного распространения на основе потери
            loss.backward()
            # Оптимизируем веса модели
            optimizer.step()
         
            steps += 1
            total_epoch_loss += loss.item()
         
        print(f'Эпоха: {epoch + 1} / {NUM_EPOCHS}: Средняя потеря: {total_epoch_loss / steps}')

 

Потеря постепенно уменьшается и приближается к 0. Затем мы можем оценить модель на тестовом наборе данных, который мы объявили изначально.

 

Оценка производительности нашей модели

 

for batch in iter(test_dataloader):
        images, labels = batch
        images = images.to(DEVICE)
        labels = labels.to(DEVICE)
     
        predictions = model(images)
     
        # Берем предсказанную метку с наибольшей вероятностью
        predictions = torch.argmax(predictions, dim=1)
     
        correct_predictions += (predictions == labels).sum().item()
        total_predictions += labels.shape[0]
    
print(f"\nТОЧНОСТЬ ТЕСТА: {((correct_predictions / total_predictions) * 100):.2f}")

 

Аналогично циклу обучения, мы выполняем итерацию по каждой партии в тестовом наборе данных для оценки. Мы генерируем предсказания для входных данных. Однако для оценки нам нужен только метка с наибольшей вероятностью. Функция argmax предоставляет эту функциональность для получения индекса значения с наибольшим значением в нашем массиве предсказаний.

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

 

Результаты

 

Я обучал модель только в течение пяти эпох и достиг точности тестирования более 96 процентов, в сравнении с точностью 10 процентов до обучения. Ниже показаны предсказания модели после обучения пяти эпох.

   

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

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

 

Полный код

 

Полный код выглядит следующим образом: 

model.py:

import torch
import torch.nn as nn


class Classifier(nn.Module):
    def __init__(
            self,
            input_size:int,
            num_classes:int
        ) -> None:
        super().__init__()
        self.input_layer = nn.Linear(input_size, 512)
        self.hidden_1 = nn.Linear(512, 256)
        self.hidden_2 = nn.Linear(256, 128)
        self.output_layer = nn.Linear(128, num_classes)

        self.activation = nn.ReLU()
     
    def forward(self, x):
        # Передаем входные данные последовательно через каждый слой и активацию
        x = self.activation(self.input_layer(x))
        x = self.activation(self.hidden_1(x))
        x = self.activation(self.hidden_2(x))
        return self.output_layer(x)

 

main.py

import torch
import torch.nn as nn
import torch.optim as  optim

from torch.utils.data import DataLoader

# Используемый набор данных MNIST, предоставляемый PyTorch
from torchvision.datasets.mnist import MNIST
import torchvision.transforms as transforms

# Импортируем модель, реализованную в отдельном файле
from model import Classifier

import matplotlib.pyplot as plt

if __name__ == "__main__":
    
    INPUT_SIZE = 784    # Вытянутые изображения размером 28x28
    NUM_CLASSES = 10    # Рукописные цифры 0-9.
    BATCH_SIZE = 128    # Использование мини-партий для обучения
    LEARNING_RATE = 0.01    # Шаг оптимизатора
    NUM_EPOCHS = 5      # Общее количество эпох обучения

    DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
    
    # Будет использоваться для преобразования изображений в тензоры PyTorch
    data_transforms = transforms.Compose([
        transforms.ToTensor(),
        transforms.Lambda(lambda x: torch.flatten(x))
    ])
    
    
    train_dataset = MNIST(root=".data/", train=True, download=True, transform=data_transforms)
    test_dataset = MNIST(root=".data/", train=False, download=True, transform=data_transforms)    
    
    train_dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
    test_dataloader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)
    
    
    model = Classifier(input_size=784, num_classes=10)
    model.to(DEVICE)
    
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
    
    for epoch in range(NUM_EPOCHS):
        total_epoch_loss = 0
        steps = 0
        for batch in iter(train_dataloader):
            images, labels = batch # Отдельные входные данные и метки
            # Преобразование тензоров в соответствующие GPU или CPU
            images = images.to(DEVICE)
            labels = labels.to(DEVICE)
         
            # Вызывает функцию model.forward() для генерации предсказаний         
            predictions = model(images)
         
            # Вычисление потерь перекрестной энтропии
            loss = criterion(predictions, labels)
            # Очистка значений градиента с предыдущей партии
            optimizer.zero_grad()
            # Вычисление градиента обратного распространения на основе потерь
            loss.backward()
            # Оптимизация весов модели
            optimizer.step()
         
            steps += 1
            total_epoch_loss += loss.item()
         
        print(f'Epoch: {epoch + 1} / {NUM_EPOCHS}: Average Loss: {total_epoch_loss / steps}')
    # Сохранение обученной модели
    torch.save(model.state_dict(), 'trained_model.pth')
    
    model.eval()
    correct_predictions = 0
    total_predictions = 0
    for batch in iter(test_dataloader):
        images, labels = batch
        images = images.to(DEVICE)
        labels = labels.to(DEVICE)
     
        predictions = model(images)
     
        # Выбор метки с наибольшей вероятностью
        predictions = torch.argmax(predictions, dim=1)
     
        correct_predictions += (predictions == labels).sum().item()
        total_predictions += labels.shape[0]
    
    print(f"\nТОЧНОСТЬ ТЕСТИРОВАНИЯ: {((correct_predictions / total_predictions) * 100):.2f}")
    
    
    
    # --  Код для построения результатов  -- #
    
    batch = next(iter(test_dataloader))
    images, labels = batch
    
    fig, ax = plt.subplots(nrows=1, ncols=4, figsize=(16,8))
    for i in range(4):
        image = images[i]
        prediction = torch.softmax(model(image), dim=0)
        prediction = torch.argmax(prediction, dim=0)
        # print(type(prediction), type(prediction.item()))
        ax[i].imshow(image.view(28,28))
        ax[i].set_title(f'Предсказание: {prediction.item()}')
    plt.show()

Мухаммад Архам – инженер глубокого обучения, работающий в области компьютерного зрения и обработки естественного языка. Он работал над развертыванием и оптимизацией нескольких генеративных приложений искусственного интеллекта, которые достигли мировых рекордов в Vyro.AI. Ему интересно создание и оптимизация моделей машинного обучения для интеллектуальных систем, и он верит в постоянное совершенствование.