Хабрахабр

[Перевод] Экскурсия по PyTorch

Привет, Хабр!

Не пропустите! Еще до конца мая у нас выйдет перевод книги Франсуа Шолле "Глубокое обучение на Python" (примеры с использованием библиотек Keras и Tensorflow).

Сегодня вашему вниманию предлагается перевод статьи Питера Голдсборо, готового устроить вам долгую прогулку ознакомительную экскурсию по этой библиотеке. Но мы, естественно, смотрим в надвигающееся будущее и начинаем присматриваться к еще более инновационной библиотеке PyTorch. За этой работой я довольно хорошо усвоил, каковы сильные, а каковы слабые стороны TensorFlow – а также познакомился с конкретными архитектурными решениями, оставляющими поле для конкуренции. Под катом много и интересно.
Последние два года я всерьез занимался TensorFlow – писал статьи по этой библиотеке, выступал с лекциями о расширении ее бэкенда, либо использовал в моих собственных исследованиях, связанных с глубоким обучением. Сегодня PyTorch весьма популярна в исследовательском сообществе; почему – расскажу в следующих абзацах. С таким багажом я недавно присоединился к команде PyTorch в отделе по исследованиям искусственного интеллекта в компании Facebook (FAIR) – пожалуй, в настоящее время это сильнейший конкурент TensorFlow.

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

Общая картина и философия

Для начала рассмотрим, что представляет собой PyTorch с фундаментальной точки зрения, какую модель программирования приходится применять, работая с ней, а также как она вписывается в экосистему современных инструментов глубокого обучения:

Сверх этого PyTorch предлагает насыщенный API для решения прикладных задач, связанных с нейронными сетями. В сущности, PyTorch – это библиотека на Python, обеспечивающая тензорные вычисления с GPU-ускорением, подобно NumPy.

Напротив, расчетные графы в PyTorch динамические и определяются на лету. PyTorch отличается от других фреймворков машинного обучения тем, что здесь не используются статические расчетные графы – определяемые заранее, сразу и окончательно – как в TensorFlow, Caffe2 или MXNet. Этот граф создается имплицитно – то есть, библиотека сама записывает поток данных, идущих через программу, и связывает вызовы функций (узлы) вместе (посредством ребер) в расчетный граф. Таким образом, при каждом вызове слоев в модели PyTorch динамически определяется новый расчетный граф.

Сравнение динамических и статических графов

В целом, в большинстве сред программирования при сложении двух переменных x и y, означающих числа, получается их суммарное значение (результат сложения). Давайте подробнее разберемся, чем статические графы отличаются от динамических. Например, на Python:

In [1]: x = 4
In [2]: y = 2
In [3]: x + y
Out[3]: 6

Но не в TensorFlow. В TensorFlow x и y будут не числами как таковыми, а описателями узлов графа, представляющих эти значения, но не содержащих их явно. Более того (что даже важнее), при сложении x и y получится не сумма этих чисел, а описатель расчетного графа, который даст искомое значение лишь после того, как будет выполнен:

In [1]: import tensorflow as tf
In [2]: x = tf.constant(4)
In [3]: y = tf.constant(2)
In [4]: x + y
Out[4]: <tf.Tensor 'add:0' shape=() dtype=int32>

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

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

Возвращаясь к разбору простого вышеприведенного случая, можно убедиться, что программирование с PyTorch по ощущению напоминает «естественный» Python:

In [1]: import torch
In [2]: x = torch.ones(1) * 4
In [3]: y = torch.ones(1) * 2
In [4]: x + y
Out[4]: 6
[torch.FloatTensor of size 1]

PyTorch немного отличается от базовой логики программирования на Python в одном конкретном аспекте: библиотека записывает выполнение работающей программы. То есть, PyTorch тихонько “выслеживает”, какие операции вы совершаете над ее типами данных, и за кулисами – опять! – собирает расчетный граф. Такой расчетный граф нужен для автоматического дифференцирования, поскольку должен в обратном направлении проходить по цепочке операций, давшей результирующее значение, чтобы вычислить производные (для обратного автоматического дифференцирования). Серьезное отличие этого расчетного графа (вернее, способа сборки этого расчетного графа) от варианта из TensorFlow или MXNet заключается в том, что новый граф собирается «жадно», на лету, при интерпретации каждого фрагмента кода.

Более того, тогда как PyTorch динамически обходит граф в обратном направлении всякий раз, когда вы запрашиваете производную значения, TensorFlow просто вставляет в граф дополнительные узлы, которые (неявно) вычисляют эти производные и интерпретируются точно как все остальные узлы. Напротив, в Tensorflow расчетный граф строится лишь однажды, за это отвечает метапрограмма (ваш код). Здесь разница между динамическими и статическими графами проявляется особенно отчетливо.

Поток управления – это аспект, на котором особенно сказывается данный выбор. Выбор, с какими расчетными графами работать – статическими или динамическими – серьезно упрощает процесс программирование в одном из этих окружений. Например, в Tensorflow для обеспечения ветвления есть операция tf.cond(), принимающая в качестве ввода три подграфа: условный подграф и два подграфа для двух веток развития условия: if и else. В окружении со статическими графами поток управления должен быть представлен на уровне графа в виде специализированных узлов. В ситуации с динамическим графом все это упрощается. Аналогично, циклы в графах Ternsorflow следует представлять как операции tf.while(), принимающие в качестве ввода condition и подграф body. Таким образом, неуклюжий и путаный код Tensorflow: Поскольку графы при каждой интерпретации просматриваются из кода Python как есть, управление потоком можно нативно реализовать на языке, используя условия if и циклы while, как в любой другой программе.

import tensorflow as tf x = tf.constant(2, shape=[2, 2])
w = tf.while_loop( lambda x: tf.reduce_sum(x) < 100, lambda x: tf.nn.relu(tf.square(x)), [x])

Превращается в естественный и понятный код PyTorch:

import torch.nn
from torch.autograd import Variable x = Variable(torch.ones([2, 2]) * 2)
while x.sum() < 100: x = torch.nn.ReLU()(x**2)

Естественно, с точки зрения легкости программирования польза динамических графов этим далеко не ограничивается. Просто иметь возможность проверять промежуточные значения при помощи инструкций print (а не при помощи узлов tf.Print()) или в отладчике – уже большой плюс. Разумеется, динамизм может как оптимизировать программируемость, так и ухудшать производительность – то есть, оптимизировать такие графы сложнее. Поэтому, отличия и компромиссы между PyTorch и TensorFlow во многом такие же, как и между динамическим интерпретируемым языком, например, Python, и статическим компилируемым языком, например, C или C++. Первый проще и работать с ним быстрее, а из второго и третьего удобнее собирать сущности, хорошо поддающиеся оптимизации. Это и есть компромисс между гибкостью и производительностью.

Замечание об API PyTorch

«batteries-included»). Хочу сделать общее замечание по поводу API PyTorch, в особенности касающееся расчета нейронных сетей по сравнению с другими библиотеками, например, TensorFlow или MXNet — этот API обвешан множеством модулей (т.н. Но он лишен «стандартной библиотеки» для наиболее распространенных программных фрагментов, которые программисту при работе приходится воспроизводить тысячи раз. Как отметил один мой коллега, API Tensorflow так по-настоящему и не вышел за «сборочный» уровень, в том смысле, что этот API предоставляет лишь простейшие инструкции по сборке, необходимые для создания расчетных графов (сложение, умножение, поточечные функции, т.д.). Правда, к сожалению, не один, а с десяток – в порядке соперничества. Поэтому, чтобы выстраивать более высокоуровневые API поверх Tensorflow, приходится полагаться на помощь сообщества.
Действительно, сообщество создало такие высокоуровневые API. Как правило, между этими API совсем мало общего, так что вам по факту придется изучить 5 разных фреймворков, а не только TensorFlow. Таким образом, в неудачный день можно прочитать пять статей по своей специализации – и во всех пяти обнаружить разные «фронтенды» для TensorFlow. Вот некоторые наиболее популярные из этих API:

PyTorch, в свою очередь, уже оснащена самыми ходовыми элементами, нужными для ежедневных исследований в области глубокого обучения. В принципе, в ней есть «нативный» Keras-подобный API в пакете torch.nn, обеспечивающий сцепление высокоуровневых модулей нейронных сетей.

Место PyTorch в общей экосистеме

До PyTorch уже существовали другие библиотеки, например, Chainer или DyNet, предоставлявшие подобный динамический API. Итак, объяснив, чем PyTorch отличается от статических графовых фреймворков вроде MXNet, TensorFlow или Theano, должен сказать, что PyTorch, фактически, не уникальна в своем подходе к вычислению нейронных сетей. Однако, сегодня PyTorch популярнее этих альтернатив.

Основная рабочая нагрузка в продакшене у нас сейчас приходится на Caffe2 – это статический графовый фреймворк, выстроенный на основе Caffe. Кроме того, PyTorch – не единственный фреймворк, используемый в Facebook. Чтобы подружить ту гибкость, что дает исследователю PyTorch, с достоинствами статических графов в сфере продакшен-оптимизации, в Facebook также разрабатывают ONNX, своеобразный формат обмена обмена информацией между PyTorch, Caffe2 и другими библиотеками, например, MXNet или CNTK.

Torch обертывает базу кода, написанную на C, благодаря чему она становится быстрой и эффективной. Наконец, маленькое историческое отступление: до PyTorch, существовала Torch – совсем старая (образца начала 2000-х) библиотека для научных вычислений, написанная на языке Lua. Далее поговорим об этом API на Python. В принципе, PyTorch обертывает ровно ту же базу кода на C (правда, с дополнительным промежуточным уровнем абстрагирования), а пользователю выставляет API на Python.

Работа с PyTorch

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

Тензоры

Тип данных tensor по значению и функциям очень похож на ndarray из NumPy. Наиболее фундаментальный тип данных в PyTorch — это tensor. Тензоры PyTorch можно создавать при помощи конструктора torch. Более того, поскольку PyTorch нацелена на разумную интероперабельность с NumPy, API tensor также напоминает API ndarray (но не идентичен ему). Tensor, принимающего в качестве ввода размерности тензора и возвращающий тензор, который занимает неинициализированную область памяти:


import torch
x = torch.Tensor(4, 4)

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

  • torch.rand: значения инициализируются из случайного равномерного распределения,
  • torch.randn: значения инициализируются из случайного нормального распределения,
  • torch.eye(n): единичная матрица вида n×nn×n,
  • torch.from_numpy(ndarray): тензор PyTorch на основе ndarray из NumPy
  • torch.linspace(start, end, steps): 1-D тензор со значениями steps, равномерно распределенными между start и end,
  • torch.ones : тензор с одними единицами,
  • torch.zeros_like(other): тензор такой же формы, что и other и с одними нулями,
  • torch.arange(start, end, step): 1-D тензор со значениями, заполненными из диапазона.

Аналогично ndarray из NumPy, тензоры PyTorch предоставляют очень насыщенный API для комбинации с другими тензорами, а также для ситуативных изменений. Также, как и в NumPy, унарные и бинарные операции обычно можно выполнить при помощи функций из модуля torch, например, torch.add(x, y) или непосредственно при помощи методов в тензорных объектах, например, x.add(y). Для самых общих мест найдутся операторы перегрузки, например, x + y. Более того, для многих функций существуют ситуативные альтернативы, которые будут не создавать новый тензор, а изменять экземпляр получателя. Эти функции называются так же, как и стандартные варианты, однако, содержат в названии нижнее подчеркивание, например: x.add_(y).

Избранные операции:

torch.add(x, y): поэлементное сложение
torch.mm(x, y): умножение матриц (не matmul или dot),
torch.mul(x, y): поэлементное умножение
torch.exp(x): поэлементная экспонента
torch.pow(x, power): поэлементное возведение в степень
torch.sqrt(x): поэлементное возведение в квадрат
torch.sqrt_(x): ситуативное поэлементное возведение в квадрат
torch.sigmoid(x): поэлементная сигмоида
torch.cumprod(x): произведение всех значений
torch.sum(x): сумма всех значений
torch.std(x): стандартное отклонение всех значений
torch.mean(x): среднее всех значений

Тензоры PyTorch также можно преобразовывать непосредственно в ndarray NumPy при помощи функции torch. Тензоры во многом поддерживают семантику, знакомую по ndarray из NumPy, например, транслирование, сложное (прихотливое) индексирование (x[x > 5]) и поэлементные реляционные операторы (x > y). Наконец, поскольку основное превосходство тензоров PyTorch по сравнению с ndarray NumPy – это GPU-ускорение, к вашим услугам также есть функция torch. Tensor.numpy(). Tensor.cuda(), копирующая тензорную память на GPU-устройство с поддержкой CUDA, если таковое имеется.

Autograd

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

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

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

Чтобы тензор можно было записывать, его нужно обернуть в torch.autograd. Tensor из PyTorch пока не обладает полноценными механизмами для участия в автоматическом дифференцировании. Класс Variable предоставляет практически такой же API, как и Tensor, но дополняет его возможностью взаимодействия с torch.autograd. Variable. Точнее, в Variable записывается история операций над Tensor. Function именно ради автоматического дифференцирования.

Variable очень просто. Пользоваться torch.autograd. Нужно просто передать ему Tensor и сообщить torch, требует ли эта переменная записывать градиенты:

x = torch.autograd.Variable(torch.ones(4, 4), requires_grad=True)

Функция requires_grad может потребовать значения False, например, при вводе данных или работе с метками, поскольку такая информация обычно не дифференцируется. Однако, они все равно должны быть Variables, чтобы подходить для автоматического дифференцирования. Обратите внимание: requires_grad по умолчанию равна False, следовательно, для обучаемых параметров ее нужно устанавливать в True.

Так вычисляется градиент этого тензора относительно листьев расчетного графа (всех входных значений, повлиявших на данное). Для расчета градиентов и выполнения автоматического дифференцирования к Variable применяют функцию backward(). Затем эти градиенты собираются в член grad класса Variable:

In [1]: import torch
In [2]: from torch.autograd import Variable
In [3]: x = Variable(torch.ones(1, 5))
In [4]: w = Variable(torch.randn(5, 1), requires_grad=True)
In [5]: b = Variable(torch.randn(1), requires_grad=True)
In [6]: y = x.mm(w) + b # mm = matrix multiply
In [7]: y.backward() # perform automatic differentiation
In [8]: w.grad
Out[8]:
Variable containing: 1 1 1 1 1
[torch.FloatTensor of size (5,1)]
In [9]: b.grad
Out[9]:
Variable containing: 1
[torch.FloatTensor of size (1,)]
In [10]: x.grad
None

Поскольку все Variable кроме входных значений являются результатами операций, с каждой Variable ассоциирован grad_fn, представляющий собой функцию torch.autograd.Function для расчета обратного шага. Для входных значений он равен None:

In [11]: y.grad_fn
Out[11]: <AddBackward1 at 0x1077cef60>
In [12]: x.grad_fn
None
torch.nn

Модуль torch.nn предоставляет пользователям PyTorch функционал, специфичный для нейронных сетей. Один из важнейших его членов — torch.nn.Module, представляющий многоразовый блок операций и связанные с ним (обучаемые) параметры, чаще всего используемые в слоях нейронных сетей. Модули могут содержать иные модули и неявно получать функцию backward() для обратного распространения. Пример модуля — torch.nn.Linear(), представляющий линейный (плотный/полносвязный) слой (т.e. аффинное преобразование Wx+bWx+b):

In [1]: import torch
In [2]: from torch import nn
In [3]: from torch.autograd import Variable
In [4]: x = Variable(torch.ones(5, 5))
In [5]: x
Out[5]:
Variable containing: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
[torch.FloatTensor of size (5,5)]
In [6]: linear = nn.Linear(5, 1)
In [7]: linear(x)
Out[7]:
Variable containing: 0.3324 0.3324 0.3324 0.3324 0.3324
[torch.FloatTensor of size (5,1)]

При обучении часто приходится вызывать в модуле функцию backward(), чтобы вычислять градиенты для его переменных. Поскольку при вызове backward() устанавливается член grad у Variables, также существует метод nn.Module.zero_grad(), сбрасывающий член grad всех Variable на ноль. Ваш обучающий цикл обычно вызывает zero_grad() в самом начале, либо непосредственно перед вызовом backward(), чтобы сбросить градиенты для следующего шага оптимизации.

Это делается очень просто – наследуем класс от torch.nn. При написании собственных моделей для нейронных сетей зачастую приходится писать собственные подклассы модуля для инкапсуляции распространенного функционала, который вы хотите интегрировать с PyTorch. Например, вот модуль, который я написал для одной из моих моделей (в ней ко входной информации добавляется гауссовский шум): Module и даем ему метод forward.

class AddNoise(torch.nn.Module): def __init__(self, mean=0.0, stddev=0.1): super(AddNoise, self).__init__() self.mean = mean self.stddev = stddev def forward(self, input): noise = input.clone().normal_(self.mean, self.stddev) return input + noise

Для соединения или сцепления модулей в полнофункциональные модели можно воспользоваться контейнером torch.nn.Sequential(), которому передают последовательность модулей – и он, в свою очередь, начинает действовать как самостоятельный модуль, при каждом вызове последовательно вычисляющий те модули, которые ему передали. Например:

In [1]: import torch
In [2]: from torch import nn
In [3]: from torch.autograd import Variable
In [4]: model = nn.Sequential( ...: nn.Conv2d(1, 20, 5), ...: nn.ReLU(), ...: nn.Conv2d(20, 64, 5), ...: nn.ReLU()) ...: In [5]: image = Variable(torch.rand(1, 1, 32, 32))
In [6]: model(image)
Out[6]:
Variable containing:
(0 ,0 ,.,.) = 0.0026 0.0685 0.0000 ... 0.0000 0.1864 0.0413 0.0000 0.0979 0.0119 ... 0.1637 0.0618 0.0000 0.0000 0.0000 0.0000 ... 0.1289 0.1293 0.0000 ... ⋱ ... 0.1006 0.1270 0.0723 ... 0.0000 0.1026 0.0000 0.0000 0.0000 0.0574 ... 0.1491 0.0000 0.0191 0.0150 0.0321 0.0000 ... 0.0204 0.0146 0.1724

Потери

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

  • torch.nn.MSELoss: средняя квадратичная функция потерь
  • torch.nn.BCELoss: функция потерь бинарной кросс-энтропии,
  • torch.nn.KLDivLoss: функция потерь информационного расхождения Кульбака-Лейблера

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

In [1]: import torch
In [2]: import torch.nn
In [3]: from torch.autograd import Variable
In [4]: x = Variable(torch.randn(10, 3))
In [5]: y = Variable(torch.ones(10).type(torch.LongTensor))
In [6]: weights = Variable(torch.Tensor([0.2, 0.2, 0.6]))
In [7]: loss_function = torch.nn.CrossEntropyLoss(weight=weights)
In [8]: loss_value = loss_function(x, y)
Out [8]: Variable containing: 1.2380
[torch.FloatTensor of size (1,)]

Оптимизаторы

Module) и функций потерь остается рассмотреть только оптимизатор, запускающий стохастический градиентный спуск (вариант). После «первоэлементов» нейронных сетей (nn. Для этого в PyTorch предоставляется пакет torch.optim, в котором определяется ряд распространенных алгоритмов оптимизации, в частности:

Каждый из этих оптимизаторов создается со списком объектов-параметров, обычно извлекаемых методом parameters() из подкласса nn.Module, определяющим, какие значения будет обновлять оптимизатор. Кроме такого списка параметров каждый оптимизатор принимает некоторое количество дополнительных аргументов, помогающих сконфигурировать стратегию оптимизации. Например:

In [1]: import torch
In [2]: import torch.optim
In [3]: from torch.autograd import Variable
In [4]: x = Variable(torch.randn(5, 5))
In [5]: y = Variable(torch.randn(5, 5), requires_grad=True)
In [6]: z = x.mm(y).mean() # Perform an operation
In [7]: opt = torch.optim.Adam([y], lr=2e-4, betas=(0.5, 0.999))
In [8]: z.backward() # Calculate gradients
In [9]: y.data
Out[9]:
-0.4109 -0.0521 0.1481 1.9327 1.5276
-1.2396 0.0819 -1.3986 -0.0576 1.9694 0.6252 0.7571 -2.2882 -0.1773 1.4825 0.2634 -2.1945 -2.0998 0.7056 1.6744 1.5266 1.7088 0.7706 -0.7874 -0.0161
[torch.FloatTensor of size 5x5]
In [10]: opt.step() # Обновляем y по правилам обновления градиентов Adam
In [11]: y.data
Out[11]:
-0.4107 -0.0519 0.1483 1.9329 1.5278
-1.2398 0.0817 -1.3988 -0.0578 1.9692 0.6250 0.7569 -2.2884 -0.1775 1.4823 0.2636 -2.1943 -2.0996 0.7058 1.6746 1.5264 1.7086 0.7704 -0.7876 -0.0163
[torch.FloatTensor of size 5x5]

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

Эти вспомогательные классы находятся в модуле torch.utils.data module. Для удобства в PyTorch предоставляется ряд утилит для загрузки датасетов, их предварительной обработки и взаимодействия с ними. Здесь следует обратить внимание на две основные концепции:

  1. Dataset, инкапсулирующий источник данных,
  2. DataLoader, отвечающий за загрузку датасета, возможно, в параллельном режиме.

Для создания новых датасетов наследуется класс torch.utils.data.Dataset и переопределяется метод __len__, так, чтобы он возвращал количество образцов в датасете, а также метод __getitem__ для доступа к единичному значению по конкретному индексу. Например, так выглядит простой датасет, в котором инкапсулирован диапазон целых чисел:

import math class RangeDataset(torch.utils.data.Dataset): def __init__(self, start, end, step=1): self.start = start self.end = end self.step = step def __len__(self, length): return math.ceil((self.end - self.start) / self.step) def __getitem__(self, index): value = self.start + index * self.step assert value < self.end return value

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

Однако, было бы гораздо удобнее, если бы датасет сам реализовывал протокол итератора, и мы могли бы сами перебирать образцы при помощи for sample in dataset. Чтобы перебрать датасет, можно, в принципе, применить цикл for i in range и обращаться к образцам при помощи __getitem__. Объект DataLoader принимает датасет и ряд опций, конфигурирующих процедуру извлечения образца. К счастью, такой функционал предоставляется в классе DataLoader. Для этого конструктор DataLoader принимает аргумент num_workers. Например, можно параллельно загружать образцы, задействовав множество процессов. Простой пример: Обратите внимание: DataLoader всегда возвращает пакеты, размер которых задается в параметре batch_size.

dataset = RangeDataset(0, 10)
data_loader = torch.utils.data.DataLoader( dataset, batch_size=4, shuffle=True, num_workers=2, drop_last=True) for i, batch in enumerate(data_loader): print(i, batch)

Здесь значение batch_size равно 4, поэтому возвращаемые тензоры будут содержать ровно по четыре значения. Если передать shuffle=True, то последовательность индексов для доступа к данным перемешивается, так что отдельные образцы возвращаются в случайном порядке. Мы также передали drop_last=True, поэтому, если для последнего пакета в датасете осталось меньше образцов, чем указано в batch_size, то этот пакет не возвращается. Наконец, мы задали для num_workers значение «два», то есть, выборкой данных параллельно займутся два процесса. После того, как DataLoader будет создан, перебор датасета и, соответственно, извлечение пакетов, станет простым и естественным.

Например, если __getitem__ возвращает словарь, то DataLoader агрегирует значения этого словаря в единое отображение, соответствующее одному пакету, использующему одинаковые ключи. Вот последнее интересное наблюдение, которым я хочу поделиться: DataLoader содержит довольно нетривиальную логику, определяющую, как комплектовать отдельные образцы, возвращенные в методе __getitem__ вашего датасета, в очередной пакет, возвращаемый DataLoader при переборе. Чтобы переопределить это поведение, можно передать аргумент функции для параметра collate_fn объекту DataLoader. Это значит, что, если метод __getitem__ датасета возвращает dict(example=example, label=label), то пакет, возвращенный DataLoader, вернет нечто наподобие dict(example=[example1, example2, ...], label=[label1, label2, ...]), то есть, распаковывая значения отдельных образцов, мы переупаковываем их под единым ключом для словаря пакета.

CIFAR10. Обратите внимание: в пакете torchvision предоставляется ряд готовых датасетов, например, torchvision.datasets. То же касается пакетов torchaudio и torchtext.

Заключение

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть