Обновление (18 мая 2021 г.). Сегодня я закончил свою книгу: Глубокое обучение с помощью PyTorch: пошаговое руководство: руководство для начинающих.

Вступление

PyTorch - это самый быстрорастущий фреймворк глубокого обучения, который также используется Fast.ai в его MOOC, Глубокое обучение для программистов и его "библиотека".

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

Кроме того, по словам Андрея Карпати, использование PyTorch может даже улучшить ваше здоровье :-)

Мотивация

Существует много руководств по PyTorch, и его документация довольно полная и обширная. Итак, почему вам следует продолжать читать это пошаговое руководство?

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

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

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

Оглавление

Простая проблема регрессии

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

По этой причине в этом руководстве я остановлюсь на простой и знакомой проблеме: линейная регрессия с одной функцией x! Нет ничего проще…

Генерация данных

Начнем генерировать синтетические данные: мы начнем с вектора из 100 точек для нашей функции x и создадим наши метки с использованием a = 1, b = 2 и некоторого гауссовского шума.

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

Мы знаем, что a = 1 и b = 2, но теперь давайте посмотрим, насколько близко мы можем подойти к истинным значениям, используя градиентный спуск и 80 точек в тренировка набор

Градиентный спуск

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

Шаг 1. Вычислите убыток

Для задачи регрессии потеря определяется как среднеквадратическая ошибка (MSE), то есть среднее значение всех квадратов разностей между метками. (y) и прогнозы (a + bx).

Стоит отметить, что если мы используем все точки в обучающем наборе (N) для вычисления потерь, мы выполняем пакетный градиент. спуск. Если бы мы каждый раз использовали одну точку, это был бы стохастический градиентный спуск. Все остальное (n) между 1 и N характеризует мини-пакетный градиентный спуск.

Шаг 2: вычислите градиенты

градиент - это частная производная - почему частичная? Потому что вычисляется относительно (относительно) одного одного параметра. У нас есть два параметра: a и b, поэтому мы должны вычислить две частные производные.

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

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

Шаг 3. Обновите параметры

На последнем этапе мы используем градиенты для обновления параметров. Поскольку мы пытаемся минимизировать наши потери, мы меняем знак градиента для обновления.

Есть еще один параметр, который следует учитывать: скорость обучения, обозначаемая греческой буквой эта (которая выглядит как буква n), который является мультипликативным коэффициентом, который нам нужно применить к градиенту для обновления параметра.

Как выбрать скорость обучения? Это отдельная тема, выходящая за рамки данной публикации.

Шаг 4: промойте и повторите!

Теперь мы используем обновленные параметры, чтобы вернуться к шагу 1 и перезапустить процесс.

Эпоха считается полной, если каждая точка уже использовалась для вычисления потерь. Для пакетного градиентного спуска это тривиально, поскольку для вычисления потерь используются все точки - одна эпоха совпадает с одним обновлением. Для стохастического градиентного спуска одна эпоха означает N обновлений, а для мини-пакетного (размера n), одна эпоха имеет N / n обновлений.

Повторение этого процесса снова и снова для многих эпох - это, в двух словах, обучение модели.

Линейная регрессия в Numpy

Пришло время реализовать нашу модель линейной регрессии с использованием градиентного спуска с использованием только Numpy.

Подождите ... Я думал, что этот урок посвящен PyTorch!

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

Для обучения модели есть два шага инициализации:

  • Случайная инициализация параметров / весов (у нас их всего два, a и b) - строки 3 и 4;
  • Инициализация гиперпараметров (в нашем случае только скорость обучения и количество эпох) - строки 9 и 11;

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

Для каждой эпохи есть четыре шага обучения:

  • Прогнозы вычислительной модели - это прямой проход - строка 15;
  • Вычислите потери, используя прогнозы и метки и соответствующую функцию потерь для текущей задачи - строки 18 и 20;
  • Вычислите градиенты для каждого параметра - строки 23 и 24;
  • Обновите параметры - строки 27 и 28;

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

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

# a and b after initialization
[0.49671415] [-0.1382643]
# a and b after our gradient descent
[1.02354094] [1.96896411]
# intercept and coef from Scikit-Learn
[1.02354075] [1.96896447]

Они соответствуют до 6 знаков после запятой - у нас есть полностью работающая реализация линейной регрессии с использованием Numpy.

Пора ФАКЕЛ :-)

PyTorch

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

В Deep Learning мы везде видим тензоры. Фреймворк Google не зря называется TensorFlow! Что такое тензор?

Тензор

В Numpy у вас может быть массив, который имеет три измерения, верно? Это, с технической точки зрения, тензор.

скаляр (одно число) имеет нулевые измерения, а вектор - один размерности, матрица имеет два измерения, а тензор - три или более измерений. Вот и все!

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

Загрузка данных, устройств и CUDA

Как перейти от массивов Numpy к тензорам PyTorch, - спросите вы? Вот для чего годится from_numpy. Однако он возвращает тензор ЦП.

Но я хочу использовать свой навороченный графический процессор…, - скажете вы. Не волнуйтесь, для этого и годится to(). Он отправляет ваш тензор на любое указанное вами устройство, включая ваш графический процессор (обозначаемый как cuda или cuda:0).

«Что, если я хочу, чтобы мой код переключился на CPU, если графический процессор недоступен?», вы можете спросить… PyTorch снова вернулся - вы можете использовать cuda.is_available(), чтобы узнать, есть ли у вас графический процессор в вашем распоряжении и соответствующим образом настройте ваше устройство.

Вы также можете легко привести его к более низкой точности (32-битное число с плавающей запятой), используя float().

Если вы сравните типы обеих переменных, вы получите ожидаемое: numpy.ndarray для первой и torch.Tensor для второй.

Но где же «живет» ваш милый тензор? В вашем процессоре или графическом процессоре? Вы не можете сказать… но если вы используете PyTorch type(), он покажет свое местоположение - torch.cuda.FloatTensor - в данном случае тензор графического процессора.

Мы также можем пойти другим путем, превратив тензоры обратно в массивы Numpy, используя numpy(). Это должно быть просто как x_train_tensor.numpy() но ...

TypeError: can't convert CUDA tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.

К сожалению, Numpy не может обрабатывать тензоры графического процессора… вам нужно сначала сделать их тензорами процессора, используя cpu().

Создание параметров

Чем отличается тензор, используемый для данных - вроде тех, которые мы только что создали - от тензора, используемого в качестве (обучаемого ) параметр / вес?

Последние тензоры требуют вычисления его градиентов, поэтому мы можем обновить их значения (то есть значения параметров). Вот для чего хорош requires_grad=True аргумент. Он сообщает PyTorch, что мы хотим, чтобы он вычислял для нас градиенты.

У вас может возникнуть соблазн создать простой тензор для параметра и позже отправить его на выбранное вами устройство, как мы это сделали с нашими данными, верно? Не так быстро…

Первый фрагмент кода создает два хороших тензора для наших параметров, градиентов и всего остального. Но это тензоры ЦП.

# FIRST
tensor([-0.5531], requires_grad=True)
tensor([-0.7314], requires_grad=True)

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

# SECOND
tensor([0.5158], device='cuda:0', grad_fn=<CopyBackwards>) tensor([0.0246], device='cuda:0', grad_fn=<CopyBackwards>)

В третьем блоке мы сначала отправляем наши тензоры на устройство, а затем затем используем метод requires_grad_(), чтобы установить его requires_grad в True на месте.

# THIRD
tensor([-0.8915], device='cuda:0', requires_grad=True) tensor([0.3616], device='cuda:0', requires_grad=True)

В PyTorch каждый метод, оканчивающийся на подчеркивание (_), вносит изменения на месте, то есть они изменит базовую переменную.

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

tensor([0.6226], device='cuda:0', requires_grad=True) tensor([1.4505], device='cuda:0', requires_grad=True)

Намного проще, правда?

Теперь, когда мы знаем, как создавать тензоры, требующие градиентов, давайте посмотрим, как PyTorch обрабатывает их - в этом роль…

Автоград

Autograd - это пакет автоматического распознавания от PyTorch. Благодаря этому нам не нужно беспокоиться о частных производных, цепных правилах и т. Д.

Итак, как нам сказать PyTorch сделать свое дело и вычислить все градиенты? Вот для чего backward() годится.

Вы помните отправную точку для вычисления градиентов? Это был убыток, поскольку мы рассчитали его частные производные по отношению к стоимости. наши параметры. Следовательно, нам нужно вызвать метод backward() из соответствующей переменной Python, например, loss.backward().

А как насчет фактических значений градиентов? Мы можем проверить их, посмотрев на grad атрибут тензора.

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

Что означает подчеркивание (_) в конце имени метода? Ты помнишь? Если нет, вернитесь к предыдущему разделу и узнайте.

Итак, давайте отбросим ручное вычисление градиентов и вместо этого воспользуемся как backward(), так и zero_() методами.

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

При первой попытке, если мы будем использовать ту же структуру обновления, что и в нашем коде Numpy, мы получим странную ошибку ниже ... но мы можем получить подсказку того, что происходит, глядя на сам тензор - мы снова «потеряли» градиент при переназначении результатов обновления нашим параметрам. Таким образом, атрибут grad оказывается None и вызывает ошибку ...

# FIRST ATTEMPT
tensor([0.7518], device='cuda:0', grad_fn=<SubBackward0>)
AttributeError: 'NoneType' object has no attribute 'zero_'

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

# SECOND ATTEMPT
RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.

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

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

Итак, как мы можем сказать PyTorch о том, что он «отступил» и позволил нам обновить наши параметры, не нарушая его причудливый график динамических вычислений? Вот для чего хорош torch.no_grad(). Это позволяет нам выполнять обычные операции Python над тензорами, независимо от графа вычислений PyTorch.

Наконец, нам удалось успешно запустить нашу модель и получить результирующие параметры. Разумеется, они совпадают с теми, которые мы получили в нашей реализации, предназначенной только для Numpy.

# THIRD ATTEMPT
tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)

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

«К сожалению, никто не может сказать, что такое граф динамических вычислений. Вы должны увидеть это сами ». Морфеус

Насколько хороша была «Матрица»? Верно-верно? Но, шутки в сторону, я хочу, чтобы вы тоже видели график сами!

Пакет PyTorchViz и его make_dot(variable) метод позволяют нам легко визуализировать график, связанный с заданной переменной Python.

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

Если мы вызовем make_dot(yhat), мы получим крайний левый график на рисунке 3 ниже:

Давайте подробнее рассмотрим его составляющие:

  • синие прямоугольники: они соответствуют тензорам, которые мы используем в качестве параметров, те, которые мы просим PyTorch для вычисления градиентов для;
  • серый прямоугольник: операция Python, в которой задействован тензор вычисления градиента или его зависимости;
  • зеленое поле: то же, что и серое поле, за исключением того, что оно является отправной точкой для вычисления градиентов (при условии, что backward()метод вызывается из переменной, используемой для визуализации график) - они вычисляются снизу вверх на графике.

Если мы построим графики для error (в центре) и loss (справа) переменных, то единственное различие между ними и первым - это число из промежуточных шагов (серые поля).

Теперь внимательно посмотрите на зеленое поле на крайнем левом графике: там есть две стрелки, указывающие к нему, поскольку он складывает две переменные, a и b*x. Кажется очевидным, правда?

Затем посмотрите на серый прямоугольник того же графика: на нем выполняется умножение, а именно b*x. Но на него указывает только одна стрелка! Стрелка находится в синем поле, соответствующем нашему параметру b.

Почему у нас нет поля для наших данных x? Ответ: мы не вычисляем градиенты для этого! Таким образом, даже несмотря на то, что в операциях, выполняемых графом вычислений, задействовано больше тензоров, он только показывает тензоры вычисления градиента и его зависимости.

Что произойдет с графом вычислений, если мы установим requires_grad равным False для нашего параметра a?

Неудивительно, что синего прямоугольника, соответствующего параметру a, больше нет! Достаточно просто: без градиентов, без графика.

Самое лучшее в графике динамических вычислений - это то, что вы можете сделать его настолько сложным, насколько захотите. Вы даже можете использовать операторы потока управления (например, операторы if) для управления потоком градиентов (очевидно!) :-)

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

Оптимизатор

До сих пор мы вручную обновляли параметры, используя вычисленные градиенты. Вероятно, это нормально для двух параметров ... но что, если бы у нас было их целиком?! Мы используем один из оптимизаторов PyTorch, например SGD или Adam.

Оптимизатор принимает параметры, которые мы хотим обновить, скорость обучения, которую мы хотим использовать (и, возможно, многие другие гиперпараметры!), И выполняет обновляет своим методом step().

Кроме того, нам больше не нужно обнулять градиенты один за другим. Мы просто вызываем zero_grad() метод оптимизатора, и все!

В приведенном ниже коде мы создаем оптимизатор стохастического градиентного спуска (SGD) для обновления наших параметров a и b.

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

Давайте проверим два наших параметра, до и после, чтобы убедиться, что все по-прежнему работает нормально:

# BEFORE: a, b
tensor([0.6226], device='cuda:0', requires_grad=True) tensor([1.4505], device='cuda:0', requires_grad=True)
# AFTER: a, b
tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)

Прохладный! Мы оптимизировали процесс оптимизации :-) Что осталось?

Потеря

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

Обратите внимание, что nn.MSELoss фактически создает для нас функцию потерь - это НЕ сама функция потерь. Кроме того, вы можете указать метод сокращения, который будет применяться, то есть как вы хотите агрегировать результаты для отдельных точек - вы можете усреднить их (сокращение = 'mean ') или просто суммируйте их (сокращение =' сумма ').

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

Теперь наш код выглядит так:

На данный момент осталось изменить только один фрагмент кода: прогнозы. Пришло время познакомить с тем, как PyTorch реализует ...

Модель

В PyTorch модель представлена ​​обычным классом Python, который наследуется от класса Module.

Наиболее фундаментальные методы, которые необходимо реализовать:

  • __init__(self): он определяет составные части модели - в нашем случае - два параметра, a и b. .

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

  • forward(self, x): он выполняет фактическое вычисление, то есть выводит прогноз, учитывая входное значение x.

Однако вам не следует вызывать метод forward(x). Вы должны вызвать саму модель, как в model(x), чтобы выполнить прямой проход и вывести прогнозы.

Давайте построим подходящую (но простую) модель для нашей задачи регрессии. Должно получиться так:

В методе __init__ мы определяем наши два параметра, a и b, используя класс Parameter(), чтобы сообщить PyTorch, что эти тензоры следует рассматривать как параметры модели, атрибутом которой они являются.

Почему мы должны об этом заботиться? Таким образом, мы можем использовать parameters() метод нашей модели для получения итератора по всем параметрам модели, даже по тем параметрам вложенных моделей, которые мы можно использовать для загрузки нашего оптимизатора (вместо того, чтобы самостоятельно создавать список параметров!).

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

ВАЖНО: нам нужно отправить нашу модель на то же устройство, на котором находятся данные. Если наши данные состоят из тензоров графического процессора, наша модель также должна «жить» внутри графического процессора.

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

Теперь напечатанные операторы будут выглядеть так - окончательные значения для параметров a и b остаются прежними, так что все в порядке :-)

OrderedDict([('a', tensor([0.3367], device='cuda:0')), ('b', tensor([0.1288], device='cuda:0'))])
OrderedDict([('a', tensor([1.0235], device='cuda:0')), ('b', tensor([1.9690], device='cuda:0'))])

Надеюсь, вы заметили в коде одно конкретное утверждение, которому я назначил комментарий «Что это?!?» - model.train().

В PyTorch модели имеют метод train(), который, к сожалению, НЕ выполняет шаг обучения. Его единственная цель - перевести модель в режим обучения. Почему это важно? В некоторых моделях могут использоваться такие механизмы, как, например, отсев, которые имеют различное поведение на этапах обучения и оценки.

Вложенные модели

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

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

В методе __init__ мы создали атрибут, содержащий нашу вложенную Linear модель.

В методе forward() мы вызываем саму вложенную модель для выполнения прямого прохода (примечание, мы не вызываем _54 _! ).

Теперь, если мы вызовем метод parameters() этой модели, PyTorch будет определять параметры своих атрибутов рекурсивным образом. Вы можете попробовать это сами, используя что-то вроде: [*LayerLinearRegression().parameters()], чтобы получить список всех параметров. Вы также можете добавить новые Linear атрибуты, и даже если вы вообще не используете их в прямом проходе, они все равно будут перечислены в parameters().

Последовательные модели

Наша модель была достаточно простой… Вы можете подумать: «Зачем вообще нужно создавать для нее класс ?!» Ну, вы правы…

Для простых моделей, которые используют стандартные слои, где выходные данные одного слоя последовательно передаются в качестве входных данных следующему, мы можем использовать э ... Последовательная модель :-)

В нашем случае мы бы построили последовательную модель с одним аргументом, то есть с уровнем Linear, который мы использовали для обучения нашей линейной регрессии. Модель будет выглядеть так:

# Alternatively, you can use a Sequential model
model = nn.Sequential(nn.Linear(1, 1)).to(device)

Достаточно просто, правда?

Шаг обучения

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

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

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

Затем мы можем использовать эту универсальную функцию для создания train_step() функции, которая будет вызываться внутри нашего цикла обучения. Теперь наш код должен выглядеть так ... видите, насколько сейчас крошечный цикл обучения?

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

Набор данных

В PyTorch набор данных представлен обычным классом Python, который наследуется от класса Dataset. Вы можете рассматривать его как своего рода список кортежей Python, каждый кортеж которого соответствует одной точке (функциям, метке).

Наиболее фундаментальные методы, которые необходимо реализовать:

  • __init__(self): он принимает любые аргументы, необходимые для создания списка кортежей - это может быть имя файла CSV, который будет загружен и обработан. ; это может быть два тензора, один для объектов, другой для меток; или что-нибудь еще, в зависимости от поставленной задачи.

Нет необходимости загружать весь набор данных в метод конструктора (__init__). Если ваш набор данных большой (например, десятки тысяч файлов изображений), его одновременная загрузка не будет эффективно использовать память. Рекомендуется загружать их по запросу (всякий раз, когда вызывается __get_item__).

  • __get_item__(self, index): он позволяет индексировать набор данных, поэтому он может работать как список (dataset[i]) - он должен возвращать кортеж. (функции, метка), соответствующие запрошенной точке данных. Мы можем либо вернуть соответствующие фрагменты нашего предварительно загруженного набора данных или тензоров, либо, как упоминалось выше, загрузить их по запросу (как в этом примере).
  • __len__(self): он должен просто возвращать размер всего набора данных, поэтому при каждой выборке его индексирование ограничивается фактическим размером.

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

Опять же, вы можете подумать: зачем тратить столько времени на то, чтобы обернуть пару тензоров в класс?. И, опять же, вы правы ... если набор данных - это не что иное, как пара тензоров, мы можем использовать класс TensorDataset PyTorch, который будет неплохо многое из того, что мы сделали в нашем пользовательском наборе данных выше.

Вы заметили, что мы создали наши обучающие тензоры из массивов Numpy, но мы не отправляли их на устройство? Итак, теперь они тензоры CPU! Почему?

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

Хорошо, хорошо, но опять же, почему мы все равно создаем набор данных? Мы делаем это, потому что хотим использовать…

DataLoader

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

Поэтому для этой работы мы используем класс PyTorch DataLoader. Мы сообщаем ему, какой набор данных использовать (тот, который мы только что создали в предыдущем разделе), желаемый размер мини-пакета и хотим ли мы перемешать это или нет. Вот и все!

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

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

next(iter(train_loader))

Как это меняет наш цикл обучения? Давай проверим!

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

Для больших наборов данных загрузка данных выборка за выборкой (в тензор ЦП) с использованием Dataset's __get_item__, а затем отправка всех образцов, принадлежащих к одной и той же мини-партии, сразу на ваш графический процессор (устройство) - это способ получить наилучшее использование вашей графики ОЗУ карты.

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

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

Случайное разделение

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

Затем для каждого подмножества данных мы создаем соответствующий DataLoader, поэтому наш код выглядит следующим образом:

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

Оценка

Это последняя часть нашего пути - нам нужно изменить цикл обучения, включив в него оценку нашей модели, то есть вычисление потери проверки . Первый шаг - включить еще один внутренний цикл для обработки мини-пакетов, которые поступают из загрузчика проверки, отправляя их на то же устройство, что и наша модель. Затем мы делаем прогнозы, используя нашу модель (строка 23), и вычисляем соответствующие потери (строка 24).

Это почти все, но есть два небольших, но важных момента, которые следует учитывать:

  • torch.no_grad(): даже если это не повлияет на нашу простую модель, рекомендуется заключить внутренний цикл проверки в этот диспетчер контекста, чтобы отключить любое вычисление градиента, которое вы можете непреднамеренно запустить - градиенты относятся к обучению, а не к этапам проверки;
  • eval(): единственное, что он делает, это переводит модель в режим оценки (как это делал ее train() аналог), поэтому модель может корректировать свое поведение относительно некоторых операций, таких как Dropout .

Теперь наш цикл обучения должен выглядеть так:

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

Где полный рабочий код со всеми наворотами?, - спросите вы? Вы можете найти его здесь.

Последние мысли

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

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

Обновление (18 мая 2021 г.). Сегодня я закончил свою книгу: Глубокое обучение с помощью PyTorch: пошаговое руководство: руководство для новичков.

Если у вас есть мысли, комментарии или вопросы, оставьте комментарий ниже или свяжитесь со мной в Twitter.