PYTORCH С НУЛЯ — ТЕНЗОРЫ И НЕЙРОСЕТИ

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

*https://www.youtube.com/watch?v=8l_aDqLRrVg
**https://300.ya.ru/v_FkC2MKbr

таймкоды

00:00:00 Введение в PyTorch

  • PyTorch — популярная библиотека для работы с нейронными сетями.
  • Код на PyTorch пишется развёрнуто, что помогает лучше понимать и контролировать действия.
  • В видео рассматривается основы работы с тензорами и обучение нейронной сети с использованием векторов и матриц.

00:00:26 Основы тензоров в PyTorch

  • Тензоры — основной объект в PyTorch, объединяющий скаляры, векторы, матрицы и тензоры более высокого ранга.
  • Скаляры используются для метрик модели, векторы — для параметров объекта, матрицы — для всей выборки данных.
  • Тензоры более высокого ранга применяются для изображений, видео и других видов данных.

00:01:25 Установка и базовые операции с тензорами

  • Установка PyTorch через конфигуратор на официальном сайте или команду pip install torch.
  • Создание тензоров с помощью функции tensor и выполнение базовых математических операций: сложение, умножение, возведение в степень.
  • Вычисление базовых статистик: среднее, стандартное отклонение с помощью методов mean и std.

00:02:45 Работа с матрицами

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

00:04:22 Проект по оценке качества вина

  • Описание набора данных: 6500 бутылок португальского вина с 11 химическими параметрами и оценкой жюри.
  • Разбиение данных на обучающую и валидационную выборки.
  • Индексация в тензорах для перемешивания и разделения данных.

00:07:06 Нормализация данных

  • Нормализация признаков: вычитание среднего значения и деление на стандартное отклонение.
  • Вычисление среднего и стандартного отклонения с помощью методов mean и std.
  • Обратная операция для восстановления исходных значений.

00:08:17 Подготовка данных к обучению

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

00:10:13 Завершение подготовки данных

  • Сохранение тензоров mean и std для последующей нормализации новых данных.
  • Возможность обработки и сохранения данных белого вина.

00:10:28 Место PyTorch в Python-экосистеме

  • Porch — одна из самых популярных библиотек для работы с нейронными сетями.
  • NumPy — старая библиотека, часто используемая в датасайнс-проектах.
  • Интерфейс NumPy и PyTorch имеет много общего, что облегчает адаптацию пользователей.

00:11:27 Преимущества PyTorch перед NumPy

  • NumPy не поддерживает автоматическое дифференцирование и вычисления на видеокартах.
  • Эти функции критически важны для работы с нейронными сетями.

00:11:45 Хранение данных в PyTorch и NumPy

  • В PyTorch и NumPy массивы хранятся как непрерывные участки памяти.
  • В Python списки хранятся свободно, что позволяет хранить объекты любого типа, но замедляет операции.
  • Пример сравнения скорости сложения массивов показывает преимущество тензоров.

00:12:51 Типы данных в PyTorch

  • PyTorch использует типы данных с указанием разрядности: int64 и float32.
  • Разрядность влияет на точность и объём памяти.
  • По умолчанию используются 32-битные числа с плавающей точкой для баланса точности и скорости.

00:13:51 Автоматическое приведение типов

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

00:15:10 Вычисления на видеокартах

  • Видеокарты ускоряют операции между тензорами благодаря параллельной работе ядер.
  • PyTorch позволяет выбирать устройство для обработки каждого тензора.
  • Метод to создаёт копию тензора в памяти указанного устройства.

00:16:03 Перенос тензоров на видеокарту

  • Для переноса тензора на видеокарту используется строка cuda.
  • При переносе части тензоров на видеокарту могут возникнуть ошибки, требующие переноса обоих тензоров на одно устройство.
  • Альтернативные варианты для работы с видеокартами: Google Colab и KGL.com.

00:17:17 Оценка скорости операций

  • Пример умножения матриц показывает значительное ускорение операций на видеокартах.
  • Большинство операций выполняются в 10–100 раз быстрее на видеокартах по сравнению с процессором.

00:17:49 Введение в градиентный спуск

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

00:18:46 Коэффициент обучения и автоматическое дифференцирование

  • Коэффициент обучения альфа может принимать значения от 0,0001 до 0,1.
  • При нескольких аргументах каждый обновляется независимо.
  • Автоматическое дифференцирование в PyTorch позволяет вычислять производные без ручного труда.

00:19:43 Пример автоматического дифференцирования

  • PyTorch отслеживает операции между тензорами и позволяет вычислить производную любого выражения.
  • Для вычисления производной тензор должен быть помечен флагом «град».
  • Пример: вычисление производной функции f = a^2 в точке a = 2.5.

00:22:12 Проблемы с накоплением производных

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

00:23:28 Создание и обучение нейронной сети

  • Простейшая нейронная сеть с одним слоем: параметры — матрицы весов W и вектор смещений b.
  • Предсказание модели равно произведению матрицы признаков и матрицы весов, сложенному с вектором смещений.
  • Обучение направлено на минимизацию отклонения предсказаний от реальных оценок сомелье.

00:25:13 Практическое обучение нейронной сети

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

00:27:14 Заключение

  • Разбор посвящён возможностям тензоров и процессу обучения нейронных сетей.
  • Упоминание о готовых компонентах PyTorch и планах на будущее.
  • Призыв подписаться на канал и присоединиться к телеграм-сообществу.

In this video

Интро
0:00
Всем привет! Сегодня разбираем PyTorch с нуля. Это одна из самых популярных библиотек
0:04
для работы с нейронными сетями. Код на PyTorch пишется развернуто — это помогает лучше понимать
0:09
и контролировать каждое действие. Но даже несмотря на это, работа с готовыми моделями,
0:13
слоями и оптимизаторами все еще может вызывать вопросы. Поэтому мы опустимся на уровень ниже,
0:19
узнаем как устроен PyTorch и обучим нейронную сеть, используя только векторы и матрицы.
Тензоры
0:27
Библиотеки редко представляют собой коллекцию равноценных объектов.
0:30
Большинство имеет многослойную архитектуру, в центре который находится главный объект,
0:35
выполняющий все основные операции. Хороший разработчик может не знать
0:38
наизусть имена всех методов, но он понимает сильные и слабые стороны
0:42
этого объекта. Это позволяет писать более эффективный код и быстро исправлять ошибки.
0:47
PyTorch построен именно так, и его основным объектом является тензор. Тензор — это термин
0:52
из линейной алгебры, который обобщает несколько других. К тензорам относятся обычные числа или
0:57
скаляры (их называют нульмерными тензорами), векторы (одномерные тензоры), матрицы (двумерные
1:02
тензоры) и тензоры более высокого ранга, у которых нет специальных названий.
1:07
На практике скаляры могут использовать для метрик модели, например оценки ее точности.
1:11
Векторы могут содержать параметры конкретного объекта, например рост, вес и возраст человека,
1:17
а матрицы — те же данные, но для всей выборки — например, данные ста человек.
1:21
Тензоры более высокого ранга используются для изображений, видео и других видов данных.
1:25
С точки зрения программирования тензоры являются многомерными массивами,
1:29
но конкретная реализация в PyTorch имеет несколько особенностей, которые мы разберем по ходу видео.
1:34
Для начала установим PyTorch. Для этого разработчики рекомендуют выбрать нужную команду
1:39
через конфигуратор на официальном сайте. Но если у вас нет видеокарты от NVidia или вы не хотите
1:44
сейчас устанавливать на нее драйвера с поддержкой CUDA, будет достаточно выполнить команду:
1:50
Подождем окончания установки, импортируем «torch» в шапке модуля и попробуем создать
1:54
несколько тензоров: для этого вызываем функцию «tensor» со списком чисел. Можно
1:59
использовать целые числа и числа с плавающей точкой. В конце выведем оба тензора на экран:
2:04
Большая часть работы нейронных сетей построена на базовых математических операциях между тензорами,
2:09
таких как сложение и умножение. Для них не нужно писать циклы: работайте с тензорами так
2:14
же как работали бы с числами. Например, умножим тензор «a» на 3 и возведем тензор «b» в квадрат:
2:19
PyTorch повторит операцию между числом и каждым элементом тензора.
2:23
Точно также работают операции между двумя
2:25
тензорами одинакового размера. Попробуем сложить «a» и «b»:
2:28
В результате получим тензор «c», каждый элемент
2:31
которого будет равен сумме элементов «a» и «b» на соответствующих позициях.
2:35
В некоторых задачах могут потребоваться базовые статистики, такие как среднее
2:38
или стандартное отклонение. Их можно вычислить с помощью методов:
2:42
метод «mean» возвращает среднее, а «std» — стандартное отклонение:
2:46
Базовые операции можно комбинировать с подсчетом статистик для вычисления
2:49
ошибок или метрик. Например, чтобы получить средний квадрат отклонения
2:53
между тензорами «a» и «b» сначала создадим тензор «error», равный квадрату их разности,
2:57
а затем вычислим его среднее с помощью метода «mean()»:
3:00
Мы разобрали операции на примере векторов, но они касаются и тензоров более высокого ранга,
3:05
например матриц. Для создания матрицы нужно передать функции «tensor» вложенный список
3:09
чисел (внутренние списки будут строками матрицы).
3:12
Создадим пару матриц размером 2×2 и выведем их на экран:
3:16
Так же как и векторы, их можно сложить, умножить или возвести в степень:
3:20
Кроме поэлементных операций для матриц определена
3:23
операция матричного произведения — для нее используется собака («@»):
3:26
Результатом будет сумма произведений строк левой матрицы и колонок правой матрицы. В нейронных
3:31
сетях эту операцию используют для подсчета взвешенной суммы всех признаков объекта,
3:35
чтобы получить предсказание или новые признаки. Позже мы используем ее на практике.
3:40
Операции для подсчета статистик работают с матрицами так же как и с векторами и
3:44
применяются к матрице целиком. Например, вызов метода «mean» возвращает среднее всех элементов:
3:50
Но при работе с матрицами у нас есть возможность вычислить среднее и другие статистики отдельно
3:54
для строк или колонок. Для этого нужно передать соответствующему методу порядок оси,
3:59
которая будет свернута, в качестве аргумента. Например, чтобы вычислить
4:02
среднее в каждой колонке, нужно свернуть все строки — в этом случае передаем ноль:
4:07
Все эти операции будут составлять основную часть нашего кода и позволят справиться с
4:11
большинством задач. Чуть позже поговорим о том, чего еще не хватает для обучения нейронных сетей,
4:15
а пока предлагаю перейти к практике и разобраться, как выглядят первые шаги в реальном проекте.
Подготовка данных
4:23
Сегодня выполним небольшой проект по оценке качества вина. У нас есть набор данных 6500
4:28
бутылок португальского вина Vinho Verde разных производителей и ценового сегмента.
4:32
Каждая бутылка была проанализирована в лаборатории и прошла слепую дегустацию
4:36
жюри из трех сомелье. Таким образом, у нас есть 11 химических параметров,
4:40
таких как содержание алкоголя, сахара и консервантов, и итоговая оценка
4:44
жюри от 0 до 10 баллов. Именно ее нам и предстоит определить по параметрам вина.
4:49
Если хотите выполнять все действия вместе со мной, скачайте файлы «wine-red.pt» и «wine-white.pt»
4:54
из моей группы в Telegram и положите в папку с проектом. В первом файле находятся данные
4:58
красных вин, а во втором — белых, но набор и порядок колонок в обоих файлах идентичны.
5:03
Обычно работу над проектом начинают с разведочного анализа и очистки данных,
5:06
но в нашем случае все довольно просто. В данных нет ни пропусков, ни выбросов,
5:10
и каждый из 11 параметров в той или иной степени коррелирует с результатом. Поэтому мы можем
5:15
оставить данные как есть и сосредоточиться на следующих шагах — разбиении и нормализации.
5:20
Для наглядности рассмотрим все действия на небольшой матрице
5:23
прежде чем работать с полным набором данных:
5:25
Для начала посмотрим, как и зачем делается разбиение. Нейронные сети и другие модели никогда
5:30
не обучают на всем наборе данных, потому что в этом случае не останется возможности проверить,
5:35
что модель дает адекватные предсказания на незнакомых данных. Вместо этого из
5:39
набора данных случайным образом выбирают от 50% до 90% объектов для обучения модели,
5:44
а оставшуюся часть используют для валидации — на них модель не обучают, а только проверяют.
5:49
На практике обычно проще сначала перемешать матрицу исходных данных,
5:53
а затем взять нужное количество строк с конца и с начала. В PyTorch оба этих действия
5:57
можно выполнить с помощью индексации. Индексация в тензорах работает так же,
6:01
как и в списках — чтобы получить строку матрицы, нужно передать ее номер в квадратных скобках:
6:06
Каждая строка также будет тензором.
6:08
Вместо одного индекса можно передать любую последовательность индексов. Например,
6:12
выведем матрицу в обратном порядке:
6:14
Чтобы перейти к случайному порядку, используем функцию «randperm» — она
6:18
ожидает целое число и возвращает тензор целых чисел от нуля до заданного числа в
6:22
случайном порядке. Попробуем запустить код несколько раз:
6:25
Каждый раз мы будем получать индексы в разной последовательности.
6:29
Теперь используем тензор «index» в качестве индекса,
6:31
чтобы переставить строки исходной матрицы случайным образом:
6:35
Перемешанную матрицу осталось разделить — для этого пользуются
6:37
срезами. Присвоим первые две строки переменной «train_data» и оставшиеся
6:41
две — переменной «val_data». Выведем обе матрицы на экран:
6:45
Позже нам понадобится разделить матрицу не только горизонтально,
6:48
но и вертикально, чтобы отделить признаки от истинных значений. Сделать это также
6:52
можно с помощью индексации. Для этого передаем пустой срез по первой оси (то
6:56
есть берем все строки) и нужный срез по второй — с его помощью выбираем колонки:
7:00
Получаем две матрицы: первая содержит все колонки матрицы «X_train» кроме последней,
7:05
а вторая — только последнюю.
7:06
Теперь поговорим о нормализации. Вы могли заметить, что каждая колонка имеет разный
7:11
диапазон значений. Это происходит, потому что каждый параметр измеряется в разных единицах
7:15
и имеет свои физические пределы. Проблема в том, что признаки с наибольшими значениями
7:20
будут расцениваться алгоритмом обучения как более важные. Это приведет к ухудшению
7:25
метрик модели. Чтобы этого избежать, значения в каждой колонке приводят к одинаковому диапазону
7:30
с помощью сдвига и масштабирования. Этот процесс называют нормализацией.
7:34
Один из самых распространенных способов нормализации заключается в том,
7:38
чтобы вычесть из каждой колонки среднее значение и разделить результат на стандартное
7:42
отклонение. Тогда большинство значений будет находиться в диапазоне от -3 до +3.
7:47
Вычислим среднее и стандартное отклонение с помощью методов «mean» и «std». Передаем
7:52
ноль в качестве аргумента, чтобы посчитать значения в каждой колонке:
7:55
Для нормализации вычитаем из исходной матрицы средние
7:58
значения и делим разность на стандартное отклонение:
8:01
Теперь значения в каждой колонке находятся примерно в одном и том же диапазоне.
8:04
К сожалению, в нормализованном виде прочитать данные невозможно.
8:08
Но мы всегда можем посчитать исходные значения с помощью обратной операции.
8:12
Умножим данные на стандартное отклонение и добавим среднее:
8:15
Получаем исходную матрицу.
8:17
Теперь мы знаем все, чтобы выполнить подготовку данных от начала до конца. Загрузим данные
8:21
красного вина. Для этого вызываем функцию «load» и передаем ей имя файла в качестве аргумента:
8:27
PyTorch не отображает большие тензоры полностью. Чтобы понять,
8:31
сколько в тензоре «data» строк и колонок, выведем на экран его атрибут «shape»:
8:35
Он показывает, что тензор содержит 1599 строк (это общий размер набора данных) и 12 колонок:
8:41
11 колонок признаков и одна колонка с оценкой сомелье — она находится в конце.
8:46
Сначала перемешаем данные. Для этого создаем случайную последовательность индексов с помощью
8:51
функции «randperm», передаем ей количество строк матрицы и сразу же используем результат:
8:55
Перемешанную матрицу сначала разделяем на матрицу признаков и матрицу истинных значений,
9:00
затем в каждой матрице выбираем первые 1000 строк для обучения, а оставшиеся строки
9:05
оставляем для валидации. Вы можете использовать другое количество строк, но не делайте набор
9:09
валидации слишком маленьким. Выведем размеры каждой из четырех полученных матриц на экран:
9:14
Все выглядит нормально: матрицы признаков содержат по 11 колонок,
9:18
а матрицы истинных значений — по одной.
9:20
Осталось выполнить нормализацию. И здесь есть пара нюансов. Во-первых, нормализации мы
9:25
подвергаем только признаки, целевые значения нужно оставить как есть. Иногда их могут нормализовать
9:29
или логарифмировать если они принимают слишком большие значения, но это не наш случай. Во-вторых,
9:34
среднее и стандартное отклонение нужно вычислить только для выборки обучения (то есть переменной
9:39
«X_train») и использовать полученные значения для нормализации обеих выборок:
9:43
Нельзя нормализовывать всю выборку до разбиения — это приведет к просачиванию части информации из
9:48
выборки валидации в выборку обучения. Также нельзя нормализовывать выборки по отдельности, иначе одни
9:53
и те же значения в выборке обучения и выборке валидации могут стать разными после нормализации.
9:59
Теперь осталось только сохранить все тензоры. Делаем это с помощью функции «save» — передаем
10:03
ей тензор и имя файла с расширением «.pt». Повторяем для всех четырех тензоров:
10:08
Перед началом обучения нам останется только загрузить подготовленные тензоры с помощью
10:12
функции «load». В общем случае стоило бы также сохранить тензоры «mean» и «std» — без них
10:17
не получится нормализовать новые данные. Можете сделать это по своему усмотрению.
10:22
А для практики можете таким же образом обработать и сохранить данные белого вина.
PyTorch и NumPy
10:29
Нам осталось обсудить несколько особенностей тензоров, связанных с обучением нейронных сетей.
10:33
Но перед этим предлагаю немного порефлексировать на тему места PyTorch в Python экосистеме.
10:38
Если вы изучаете Python уже некоторое время, то могли слышать о NumPy. Это одна из самых
10:43
старых Python библиотек, первый релиз которой состоялся еще 2006 году. NumPy часто используется
10:48
в Data Science проектах, а в ее основе лежат массивы — объекты, очень похожие на тензоры.
10:53
Особенно поражают сходства в интерфейсе — некоторые функции и методы в PyTorch
10:57
и NumPy называются одинаково и делают то же самое. Иногда
11:00
достаточно заменить «torch.tensor» на «np.array», и код будет работать:
11:05
Эти сходства легко объяснить: разработчики PyTorch отлично знали, что многие пользователи
11:09
уже знакомы с NumPy, и похожий интерфейс помог бы им быстрее адаптироваться. Но не совсем понятно,
11:14
зачем было переписывать NumPy вместо того, чтобы использовать его как зависимость и
11:19
просто дополнить библиотеку компонентами для работы с нейронными сетями. К тому же,
11:23
этот подход уже успешно использовали создатели SciPy, Scikit-learn и Pandas.
11:27
Причина в том, что в отличие от тензоров NumPy массивы никогда не поддерживали две операции,
11:32
которые необходимы для работы с нейронными сетями: автоматическое дифференцирование и вычисления на
11:37
видеокартах. Предлагаю разобрать эти операции по порядку и перейти к обучению нейронных сетей.
Хранение тензоров
11:45
Сначала обсудим вычисления на видеокартах и то,
11:47
как тензоры хранятся в памяти. Заодно если вы не знали, как Python поддерживает
11:51
смешанные типы данных в списках, будет отличная возможность об этом поговорить.
11:55
В классической реализации массив представляет собой структуру данных,
11:58
для которой заранее выделяется непрерывный участок памяти, разбитый на ячейки равного
12:03
размера. Это позволяет хранить несколько объектов одного типа
12:06
и быстро получать к ним доступ. В PyTorch и NumPy используется именно такой подход.
12:10
Но эта модель идет вразрез с динамической типизацией Python,
12:14
поэтому хранение списков устроено немного по-другому. Данные в списках хранятся свободно,
12:18
так же как и другие объекты, а списки представляют собой массивы
12:22
указателей на эти объекты. Это позволяет хранить в списках объекты любого типа.
12:27
В общем случае списками пользоваться удобнее, но они занимают больше памяти и работают намного
12:32
медленнее. Для примера сравним скорость сложения двух массивов из 100 миллионов чисел. Списки
12:37
нужно складывать через циклы, например создавая пары на ходу. А для тензоров будет достаточно
12:42
использовать оператор сложения. В обоих случаях засекаем время прямо перед началом операции:
12:51
Это не лучший тест, и конкретные результаты будут зависеть от многих факторов,
12:55
но тензоры в любом случае окажутся намного быстрее.
12:58
Хранение тензоров в памяти имеет и другие особенности, к которым Python разработчики
13:02
могут быть не готовы. В отличие от простых типов «int» и «float» в PyTorch используются
13:07
типы с указанием разрядности. Тип данных тензора можно получить через атрибут «dtype»:
13:13
Числа 64 и 32 обозначают разрядность. Чем она выше, тем шире диапазон возможных значений числа
13:19
и его точность. Например, 16-битные числа с плавающей точкой смогут представить число с
13:25
точностью только до 2 знаков после запятой, 32-битные — до 7 знаков, а 64-битные — до 15
13:32
знаков. При этом числа с большей разрядностью занимают больше памяти и замедляют вычисления.
13:37
В PyTorch по умолчанию используются 32-битные числа с плавающей точкой для баланса точности
13:42
и скорости основных операций. Целые числа используются намного реже. С
13:47
ними не идут на компромиссы и хранят их со стандартной точностью 64 бита.
13:51
В большинстве случаев вам не придется думать о типах — PyTorch делает все возможное,
13:55
чтобы выполнить операции между тензорами разных типов автоматически. В редких случаях, когда
14:00
типы нужно привести вручную, используйте «int64» и «float32» вместо встроенных типов «int» и «float».
14:06
Посмотрим на несколько примеров. Большая
14:08
часть операций между тензорами разных типов не вызовет ошибок:
14:11
Например, здесь при сложении PyTorch автоматически приводит типы «int64»
14:15
и «float32» к более общему «float32». Но в некоторых случаях это невозможно. Например,
14:21
здесь операция составного присвоения закончится ошибкой:
14:24
Проблема в том, что мы не создаем новый тензор, а пытаемся записать результат операции «a + b»
14:29
типа «float32» в исходный тензор «a» типа «int64». Чтобы исправить ошибку,
14:34
нужно сначала вручную привести тензор «a» к типу «float32». Для
14:38
этого вызываем метод «to» и используем тип «torch.float32» в качестве аргумента:
14:43
Теперь оба тензора имеют тип «float32», и операция составного присвоения не вызовет ошибок:
14:48
Другие ошибки типизации можно встретить при использовании готовых компонентов PyTorch:
14:53
функций потерь, слоев и оптимизаторов. Эти ошибки исправляются аналогично,
14:57
а текст исключения подскажет вам, какой именно тензор имеет неправильный тип.
Видеокарты
15:03
Теперь поговорим о том, почему видеокарты постоянно упоминаются
15:06
в контексте нейронных сетей, и как мы можем ускорить операции между тензорами.
15:10
Современные видеокарты состоят из тысяч слабых ядер. Эти ядра могут работать независимо и
15:15
параллельно, но по отдельности они справляются только с примитивными операциями. Поэтому если
15:20
исходную задачу можно разбить на большое количество простых независимых подзадач,
15:24
ее выполнение на видеокарте займет намного меньше времени, чем на процессоре. Именно
15:29
к таким задачам относится большинство операций между тензорами, в том числе те,
15:32
которые используются при обучении нейронных сетей.
15:35
PyTorch поддерживает вычисления на видеокартах и позволяет выбрать
15:38
устройство для обработки каждого тензора независимо. Чтобы получить имя устройства,
15:43
которое отвечает за операции с конкретным тензором, обратимся к его атрибуту «device»:
15:48
По умолчанию каждый тензор обрабатывается процессором.
15:51
Изменить устройство с помощью метода «to», который мы уже использовали для изменения
15:55
типа данных. В данном случае нужно передать в качестве аргумента строку
15:58
с именем нового устройства. Для переноса на видеокарту используем строку «cuda»:
16:03
При вызове метод «to» создает копию тензора в памяти указанного устройства:
16:07
в оперативной памяти для «cpu» и в видеопамяти для «cuda».
16:11
Так как у меня нет видеокарты NVidia, запуск кода приведет к ошибке:
16:15
Вы можете получить ту же ошибку, если установили неправильные драйвера или версию PyTorch. Если
16:20
у вас нет видеокарты или попытки заставить PyTorch работать растянулись на несколько часов,
16:25
можете рассмотреть альтернативный вариант — Google Colab или Kaggle.com. Обе площадки
16:29
дают бесплатный доступ к видеокартам через Jupyter ноутбуки с подготовленной средой и
16:33
установленными библиотеками. Там этот код выполнится без проблем:
16:37
Но я сейчас не буду менять редактор и воспользуюсь аналогом видеокарты
16:41
MacBook-а — PyTorch ее также поддерживает. Для этого вместо «cuda» я использую «mps»:
16:46
При переносе части тензоров на видеокарту вы будете часто сталкиваться с одной и той же
16:50
ошибкой. Например, мы можем ее получить, если умножим тензор «a» на тензор «b»:
16:56
Проблема в том, что тензор «a» находится в видеопамяти,
16:59
а тензор «b» — в оперативной памяти. В этой ситуации PyTorch не пытается угадать, какое
17:04
из двух устройств должно выполнять операцию, и требует перенести оба тензора на одно устройство.
17:09
Чтобы исправить ошибку, перенесем тензор «b» в видеопамять:
17:12
Так как оба тензора находились в видеопамяти,
17:14
результат их операции тоже будет записан в видеопамять.
17:17
Теперь оценим скорость выполнения операций на процессоре и видеокарте
17:21
на примере умножения матриц. Создадим две матрицы случайных чисел размером «(10^4
17:25
\times 10^4)» с помощью функции «randn»: передаем ей количество строк и колонок.
17:30
Засекаем время перед операцией и выводим время выполнения на экран. Повторяем то
17:34
же самое для двух других матриц, но в этот раз переносим их на видеокарту:
17:38
Конкретные числа будут зависеть от комбинации процессора и видеокарты,
17:42
но большинство операций будет выполняться в 10 или даже 100 раз быстрее.
Автодифф
17:49
Прежде чем перейти к обучению нейронных сетей, обсудим еще одну часть функционала тензоров,
17:54
которая поможет нам сократить количество ручной работы.
17:56
Нейронные сети обучаются с помощью градиентного спуска. Это алгоритм оптимизации,
18:01
который позволяет найти минимум любой функции с помощью ее производной. Градиентный спуск
18:05
начинается с выбора случайной точки. В этой точке вычисляется производная,
18:09
а следующие действия зависят от ее модуля и знака.
18:12
Если производная принимает небольшое по модулю положительное значение,
18:16
значение самой функции немного увеличивается с увеличением аргумента. В этом случае нужно
18:21
немного уменьшить значение аргумента, чтобы прийти в точку с меньшим значением
18:25
функции. Если производная наоборот, принимает небольшое по модулю отрицательное значение,
18:30
нужно немного увеличить значение аргумента. А если модуль производной достаточно большой,
18:34
значит функция резко изменяет значение и можно делать более широкие шаги.
18:38
Все четыре случая можно обобщить одной формулой:
18:41
То есть, нужно вычесть из значения аргумента производную функции по этому
18:45
аргументу. Само значение производной в большинстве случаев слишком велико,
18:49
поэтому его умножают на небольшое число, обозначенное здесь как «α». Его называют
18:53
коэффициентом обучения. В зависимости от условий он может принимать значения от 0.0001 до 0.1.
19:00
Если у функции несколько аргументов, каждый аргумент обновляют независимо
19:03
следуя той же формуле, а вместо термина «производная» используют термин «градиент».
19:08
После перехода в новую точку все действия повторяются. Как правило делают достаточно
19:12
большое количество итераций, либо отслеживают изменение функции и останавливают алгоритм,
19:17
когда изменение становится незначительным.
19:19
При обучении нейронных сетей градиентный спуск используют для минимизации отклонения
19:23
предсказаний модели от истинных значений, а роль аргументов выполняют параметры модели.
19:28
Исторически именно вычисление производных представляло значительные трудности.
19:32
Первые попытки использования градиентных методов оптимизации были предприняты в 60-х,
19:37
но надежный способ вычисления производных для многослойных моделей появился только 1974 году.
19:44
И если ранние исследователи вычисляли производные по каждому параметру вручную,
19:48
современные библиотеки берут работу с производными на себя — это называется
19:51
автоматическим дифференцированием. В PyTorch оно является частью внутреннего функционала тензоров.
19:57
PyTorch отслеживает операции между тензорами и позволяет в любой момент вычислить производную
20:01
выражения по любому тензору, который в нем участвовал. Есть только два ограничения:
20:06
результат выражения должен быть скаляром, а тензор,
20:08
по которому мы хотим вычислить производную, должен быть помечен специальным флагом.
20:13
Чтобы стало понятнее, рассмотрим авто-дифференцирование на примере.
20:16
Допустим, нам нужно вычислить производную функции «f=a^2» в точке «a=2.5». Для начала
20:22
создадим тензор «a» — передадим конструктору «tensor» число 2.5 и флаг «requires_grad=True»:
20:27
Теперь PyTorch будет отслеживать все операции, в которых участвует «a» и сможет
20:31
вычислить производную итогового выражения. В остальном «a» ведет себя как обычный тензор.
20:36
Вычислим значение «f=a^2», а для получения производной «df/da» вызовем метод «backward»:
20:43
Получаем «None», потому что вызов «backward» не возвращает производную,
20:46
а записывает ее в тот тензор, по которому она была
20:48
вычислена. Значение производной будет записано в атрибут «grad» тензора «a»:
20:53
Мы использовали очень простую функцию, но PyTorch справится с выражением любой сложности:
20:58
В выражении также может участвовать сколько угодно тензоров — один вызов
21:02
«backward» вычислит производные по всем тензорам, помеченным флагом «requires_grad»:
21:06
Значения «a» и «b» могут находиться в одном тензоре — обозначим его как «x»
21:10
и обновим функцию: вместо «a» будет использоваться первый элемент «x»,
21:14
а вместо «b» — второй. Результатом будет один тензор из двух элементов:
21:19
Теперь попробуем найти с помощью градиентного
21:21
спуска такое значение вектора «x», при котором величина «f» будет минимальной.
21:25
Для начала выберем случайное значение вектора «x». Для этого вместо функции «tensor» вызываем
21:30
функцию «randn» — она возвращает тензор заданного размера со случайными числами
21:34
из нормального распределения. Так как нам нужно два числа, передаем двойку в качестве первого
21:39
аргумента и флаг «requires_grad» — в качестве второго. Выведем значения «x» и «f» на экран:
21:45
При запуске получаем случайный вектор и соответствующее ему значение функции.
21:49
Возьмем коэффициент обучения «α=0.1» и выполним 50 итераций градиентного спуска. На каждой итерации
21:55
вычисляем значение функции, ее производную и обновляем значение «x», вычитая из него
21:59
производную «f» по «x», умноженную на коэффициент обучения. В конце цикла выводим текущее значение
22:05
функции — с каждой итерацией оно становиться меньше. После цикла выводим значение «x»:
22:10
Первый запуск кода приводит к ошибке. Ее причина в том,
22:13
что тензор «x» участвует в операции для вычисления самого себя, что делает
22:17
невозможным вычисление производной. На самом деле нам и не нужно знать производную этой операции.
22:22
Чтобы исправить ошибку, выполним операцию обновления «x» в контекстном менеджере
22:26
«no_grad»: он сигнализирует PyTorch о том,
22:28
что эту операцию не нужно рассматривать во время вычисления производных:
22:32
Теперь код работает, но полученное значение функции далеко от минимума,
22:35
который должен быть равен нулю.
22:37
Если внимательно посмотреть на значения функции во время итераций, можно заметить,
22:41
что оно несколько раз снижается практически до нуля, но затем снова резко увеличивается.
22:46
Проблема в том, что PyTorch не обновляет значение производной на каждой итерации,
22:50
а суммирует его с предыдущим. Поэтому мы сначала делаем шаги в правильном
22:53
направлении, но вместо того, чтобы замедляться,
22:56
постоянно набираем скорость и проскакиваем точку минимума, после чего пытаемся вернуться.
23:01
Некоторые оптимизаторы грамотно используют эффект набора скорости: одна из первых реализаций такого
23:05
алгоритма принадлежит Юрию Нестерову. Но в нашем случае накопление производных с предыдущих шагов
23:10
нарушает работу алгоритма. Чтобы избежать этого, необходимо вручную сбросить значение
23:15
производной в конце каждой итерации. Для этого присваиваем атрибуту «grad» значение «None»:
23:20
В таком варианте алгоритм корректно определяет точку
23:22
минимума (0; 4), в которой значение функции равно нулю.
Обучение
23:28
Теперь мы знаем все операции для того, чтобы создать и обучить нейронную
23:32
сеть для оценки качества вина. Начнем с простейшей нейронной сети с одним слоем:
23:36
ее параметрами будут матрица весов «w» и вектор смещений «b».
23:40
Предсказание модели равно произведению матрицы признаков и матрицы весов,
23:44
сложенному с вектором смещений:
23:46
Чтобы понять, что здесь происходит,
23:47
перепишем эту формулу в скалярной форме применительно к нашей задаче:
23:51
Оценка вина равна сумме произведений обучаемых параметров модели «w_i» и значений признаков.
23:56
Каждый из параметров характеризует положительное или негативное влияние признака на итоговую
24:01
оценку вина. К сумме произведений добавляется смещение «b», которое можно интерпретировать
24:06
как примерную оценку средней бутылки вина — не хорошего, но и не плохого.
24:10
Нейронная сеть с большим количеством слоев будет иметь несколько матриц
24:13
весов и смещений. Она вычислит взвешенные комбинации признаков несколько раз прежде
24:18
чем делать предсказание. Мы обсуждали это более подробно в отдельном видео про нейронные сети,
24:23
поэтому если вам здесь что-то будет непонятно, можете туда заглянуть.
24:26
В процессе обучения мы будем подбирать значения параметров модели таким образом,
24:30
чтобы уменьшить ее отклонение от реальных оценок сомелье на выборке обучения. Для
24:35
этого мы будем искать минимум функции потерь с помощью градиентного спуска.
24:38
Так как это задача регрессии, в качестве функции потерь используем средний квадрат
24:42
отклонения между предсказаниями модели и истинными значениями:
24:46
Его величина зависит от самих данных, и двух матриц параметров модели «w» и
24:50
«b». Алгоритм градиентного спуска в этом случае будет выглядеть следующим образом:
24:55
То есть, на каждой итерации мы будем вычитать из параметра градиент
24:58
функции потерь по этому параметру, умноженный на коэффициент обучения.
25:02
Перейдем к практике. Для начала загрузим данные красного вина,
25:05
которые мы подготовили на предыдущем шаге:
25:13
Используем функцию «randn» для создания тензоров со случайными параметрами модели.
25:17
Количество строк матрицы весов «w» должно совпадать с количеством признаков,
25:21
а количество колонок — с количеством величин, которые предсказывает модель.
25:25
В нашем случае это будет матрица 11×1. Вектор смещений «b» будет состоять из одного числа:
25:31
Вычислим предсказания модели на выборке обучения и сравним их размерность с размерностью матрицы
25:36
истинных значений. Для этого выведем атрибуты «shape» обеих матриц на экран:
25:40
Предсказания модели вычисляются без проблем, а их размерность совпадает
25:44
с размерностью матрицы истинных значений. Это значит, что можно двигаться дальше.
25:48
Вычислим средний квадрат отклонения между матрицей предсказаний и матрицей
25:52
истинных значений и выведем результат на экран:
25:54
Его значение сейчас неважно. Главное,
25:57
что мы получили скаляр и не получили ошибок при вычислении.
26:00
Пропишем шаги градиентного спуска. Используем коэффициент обучения, равный 0.01 и сделаем 1000
26:05
итераций. На каждой итерации вычислим предсказания модели, величину функции потерь и ее градиент.
26:11
В контекстном менеджере «no_grad» обновим параметры модели и не сбросим градиенты
26:15
перед следующей итерацией. В конце выводим текущее значение функции потерь:
26:22
Видим, что значение функции потерь стабильно уменьшается.
26:25
Осталось проверить модель на выборке валидации. Снова вычислим предсказания модели,
26:30
но на этот раз используем контекстный менеджер «no_grad», потому что значения
26:33
производных нам уже не понадобятся. После этого посчитаем средний модуль разности предсказаний
26:38
и истинных значений — это будет среднее отклонение модели от оценки сомелье в баллах:
26:43
Получаем примерно полбалла. Это неплохой результат, но вы можете попробовать
26:47
улучшить его. Для этого можно изменить количество шагов градиентного спуска,
26:51
коэффициент обучения или использовать модель с большим количеством слоев.
26:54
Пример обучения двухслойной модели можете найти в моем Telegram канале.
26:58
Если вы довольны результатом, параметры модели можно сохранить с помощью функции «save».
27:02
В этом примере мы не переносили тензоры в видеопамять, потому что все обучение
27:06
уже занимает меньше секунды. Но мы еще вернемся к операциям на видеокартах,
27:10
когда будем разбирать более сложные проекты.
27:13
На этом предлагаю заканчивать. Я специально не включил в этот разбор готовые компоненты PyTorch,
27:18
чтобы уделить все внимание возможностям тензоров и процессу обучения нейронных
27:22
сетей. Если хотите посмотреть, как тот же код будет выглядеть с использованием готовых слоев,
27:27
функций потерь и оптимизаторов, можете посмотреть предыдущее видео.
27:30
А в следующем разборе мы поговорим обо всех готовых компонентах PyTorch
27:34
и максимально приблизимся к уровню реальных data science проектов.
27:37
Поэтому если нравится мой контент, можете подписаться на канал чтобы не пропустить
27:41
новые видео. А еще заходите пообщаться в наше Telegram сообщество питонистов — там
27:45
можно увидеть анонсы и повлиять на будущий контент. Увидимся в код ревью, всем пока!

Поделиться: