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

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

PyTorch — это популярная среда глубокого обучения для Python, которая имеет чистый API и позволяет писать код, который действительно похож на Python. Благодаря этому очень интересно создавать модели и проводить эксперименты с машинным обучением с помощью PyTorch в Python.

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

  • загрузите набор данных MNIST (набор данных «Hello World» для машинного обучения) с помощью загрузчика данных PyTorch.
  • объявляем архитектуру нашей модели
  • выбрать оптимизатор
  • реализовать тренировочный цикл
  • определить точность обученной модели

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

Итак, давайте начнем писать код.

Первое, что нам нужно сделать, это импортировать необходимые пакеты. Поскольку мы используем PyTorch, нам нужно импортировать пакеты torch и torchvision.

import torch
import torchvision as tv

Загрузка данных

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

t = tv.transforms.ToTensor()
mnist_training = tv.datasets.MNIST(
    root='/tmp/mnist',
    train=True,
    download=True,
    transform=t
)
mnist_val = tv.datasets.MNIST(
    root='/tmp/mnist', 
    train=False, 
    download=True, 
    transform=t
)

Сначала мы создаем экземпляр ToTensor(), который используем для преобразования изображений, полученных из пакета datasets, в тензоры. Нам нужен этот шаг, так как все функции PyTorch работают с тензорами. Если вы не знакомы с тензорами, то это просто причудливое название для многомерных массивов. Тензор имеет ранг. Например, тензор ранга 0 — это скаляр, тензор ранга 1 — вектор, ранг 2 — матрица и так далее.

Затем мы загружаем наш набор данных для обучения и проверки. С помощью root мы можем указать каталог, который используется для хранения набора данных на диске. Если мы установим train в true, тренировочный набор будет загружен. В противном случае загружается набор проверки. Если мы установим для download значение true, PyTorch загрузит наборы данных и сохранит их в каталоге, указанном с помощью root. Наконец, мы можем указать преобразование, которое следует применять к каждому примеру набора данных для обучения и проверки. В нашем случае это просто ToTensor().

Укажите архитектуру нашей модели

Далее мы указываем архитектуру нашей модели.

model = torch.nn.Sequential(
    torch.nn.Linear(28*28, 128),
    torch.nn.ReLU(),
    torch.nn.Linear(128, 10)
)

Выбор оптимизатора и функции потерь

Далее мы указываем оптимизатор и функцию потерь.

Мы используем оптимизатор Адама. Первым параметром мы указываем параметры нашей модели, которые оптимизатору необходимо оптимизировать. Вторым параметром lr мы указываем скорость обучения.

Во второй строке мы выбираем CrossEntropyLoss в качестве функции потерь (другое часто используемое слово для функции потерь — критерий). Эта функция берет ненормализованный (N x 10) размерный вывод нашего выходного слоя (N — количество выборок нашего пакета) и вычисляет потери между выходом сети и целевыми метками. Целевые метки представлены в виде N-мерного вектора (или, точнее, тензора ранга 1), который содержит индексы классов входных выборок. Как видите, CrossEntropyLoss — очень удобная функция. Во-первых, нам не нужен уровень нормализации, такой как softmax, в конце нашей сети. Во-вторых, нам не нужно конвертировать между различными представлениями для меток. Наша сеть выводит 10-мерный вектор оценок, а целевые метки предоставляются в виде вектора индексов классов (целое число от 0 до 9).

Затем мы создаем загрузчик данных для обучающего набора данных.

loader = torch.utils.data.DataLoader(
    mnist_training, 
    batch_size=500, 
    shuffle=True
)

Загрузчик данных используется для извлечения образцов из набора данных. Мы можем использовать загрузчик данных, чтобы легко перебирать партии образцов. Здесь мы создаем загрузчик, который возвращает 500 выборок из обучающего набора данных на каждой итерации. Если мы установим shuffle в true, сэмплы будут перемешиваться в пакете.

Обучение модели машинного обучения

Теперь у нас есть все необходимое для обучения нашей модели.

for epoch in range(10):
    for imgs, labels in loader:
        n = len(imgs)
        imgs = imgs.view(n, -1)
        predictions = model(imgs)  
        loss = loss_fn(predictions, labels) 
        opt.zero_grad()
        loss.backward()
        opt.step()
    print(f"Epoch: {epoch}, Loss: {float(loss)}")

Мы используем 10 эпох для обучения нашей сети (строка 1). В каждую эпоху мы итерируем загрузчик, чтобы получить 500 изображений с их метками на каждой итерации (строка 2). Переменная imgs представляет собой тензор формы (500, 1, 28, 28). Переменная labels представляет собой тензор ранга 1 с 500 индексами классов.

В строке 3 мы сохраняем количество изображений текущего пакета в переменной n. В строке 4 мы преобразуем тензор imgs из формы (n, 1, 28, 28) в тензор формы (n, 784). В строке 5 мы используем нашу модель для прогнозирования меток всех изображений нашего текущего пакета. Затем в строке 6 мы вычисляем разницу между этими предсказаниями и реальными фактами. Тензор predictions — это тензор формы (n, 10), а labels — это тензор ранга 1, содержащий индексы классов. В строках с 7 по 9 мы сбрасываем градиенты для всех параметров нашей сети, вычисляем градиент и обновляем параметры модели.

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

Определите точность

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

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

n = 10000
loader = torch.utils.data.DataLoader(mnist_val, batch_size=n)
images, labels = iter(loader).next()

Наш проверочный набор данных mnist_val содержит 10 000 изображений. Чтобы получить все эти изображения, мы используем DataLoader и устанавливаем batch_size на 10000. Затем мы можем получить данные, просто создав итератор из загрузчика данных и вызвав next() на этом итераторе, чтобы получить первый элемент.

Результатом является кортеж. Первый элемент этого кортежа — тензор формы (10000, 1, 28, 28). Второй элемент — это тензор ранга 1, содержащий индексы классов изображений.

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

predictions = model(images.view(n, -1))

Прежде чем мы сможем предоставить данные нашей модели, нам нужно изменить ее (аналогично тому, что мы уже сделали в цикле обучения). Входной тензор для нашей модели должен иметь форму (n, 784). Когда данные имеют правильную форму, мы можем использовать их в качестве входных данных для нашей модели.

Результатом является тензор формы (10000, 10). Для каждого изображения нашего проверочного набора этот тензор хранит оценки для каждой из десяти возможных меток. Чем выше балл для ярлыка, тем больше вероятность того, что образец относится к соответствующему ярлыку.

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

predicted_labels = predictions.argmax(dim=1)

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

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

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

torch.sum(predicted_labels == labels) / n

Сравниваем predicted_labels с labels на равенство и получаем вектор булевых значений. Элемент этого вектора является истинным, если два элемента в одной и той же позиции равны. В противном случае элемент является ложным. Затем мы используем sum для подсчета количества истинных элементов и делим это число на n.

Если мы выполним все эти шаги, мы должны достичь точности примерно 97%.

Полный код также доступен на GitHub.

Заключение

Как инженер-программист, я люблю чистый код. Иногда я сижу перед каким-то небольшим кусочком кода полчаса или даже дольше, просто чтобы сделать его изящнее и красивее.

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

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