6 июля была выпущена полная версия книги «Глубокое обучение с помощью PyTorch». Это отличная книга, и я только начал ее изучать. Третья глава посвящена основам тензорных операций и создания, и я подумал, что было бы неплохо написать этот пост об одном из самых фундаментальных компонентов библиотек глубокого обучения, что-то под названием тензор.

Если вы читаете это, скорее всего, вы встречали слово «тензор» и, вероятно, использовали его на своем пути к глубокому обучению. Тензор - это причудливое слово, которое присваивается всем многомерным массивам в информатике вместо того, чтобы называть разные массивы разными словами, как в математике, где 1-мерный массив называется вектором, а 2-мерный массив называется матрицей. Итак, в информатике и глубоком обучении скаляр может быть 0-мерным тензором, список чисел может быть 1-мерным тензором и так далее.

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

X = torch.tensor([[1., 2.], [3., 4.], [5., 6.]])
X.storage() 
>>output:
1.0
2.0
3.0
4.0
5.0
6.0
[torch.FloatStorage of size 6]

В приведенном выше фрагменте X - это тензор 3 на 2, но когда мы вызываем для него метод storage (), PyTorch дает нам реальное хранилище X в ОЗУ, которое, конечно же, представляет собой одномерный массив с размером 6. Мы можем сэкономить этот объект хранения в переменной и посмотреть его тип.

storage = tensor.storage()
type(storage), storage.dtype 
>>output:
(torch.FloatStorage, torch.float32)

Теперь давайте перенесем наш тензор X в X_t и посмотрим, изменится ли хранилище. Вы можете распечатать хранилище нового тензора X_t или проверить его «id», чтобы увидеть, совпадает ли он с идентификатором X. Также обратите внимание на форму, которая была транспонирована.

X_t = X.transpose(0,1)
X_t.storage(), X_t.shape
>>output:
1.0
2.0
3.0
4.0
5.0
6.0
[torch.FloatStorage of size 6], torch.Size([2, 3])
id(X_t.storage())==id(X.storage())
>>output:
True

Итак, похоже, хранилище цело. Но, если хранилище не изменилось, что метод транспонирования сделал на X? Вот в чем дело. Тензоры PyTorch на самом деле являются объектами, которые имеют некоторые атрибуты и методы, как и другие объекты в Python. «Шаг» - это свойство тензора, которое определяет, сколько элементов следует пропустить в массиве хранения, чтобы получить следующий элемент в данном измерении в исходном тензоре! Возможно, это определение не очень помогло; Итак, давайте проясним это. В первом измерении X, которое является строками тензора 3 на 2, чтобы перейти от первого элемента первой строки (номер 1.) ко второму элементу второй строки (номер 3.), нам нужно пройти 2 элемента. далее в массиве хранения, чтобы достичь этого (помните, что массив хранения представляет собой список чисел от 1. до 6.). Итак, шаг в этом измерении будет 2. А как насчет следующего измерения (измерения столбца)? В этом случае нам не нужно пропускать какое-либо число в массиве хранения, поэтому шаг будет равен 1. Если мы вызовем метод stride () для X, мы получим эти два точных числа для шагов каждого измерения.

X.stride()
>>output:
(2,1)

Ниже вы можете увидеть картинку из книги о тензоре 3 на 3. Вы можете видеть, что шаг по первому измерению равен 3, а по второму измерению равен 1 в этом конкретном примере.

Это отличная возможность! При транспонировании или выполнении некоторых других конкретных операций с тензорами PyTorch не изменяет основное хранилище чисел в ОЗУ, а просто изменяет свойство шага объекта тензора, чтобы показать другое представление этого хранилища. Итак, думайте о тензорах как о порте или окне, через которое видно реальное хранилище.

X_t
>>output:
tensor([[1., 3., 5.],
         [2., 4., 6.]])

Давайте определим новый тензор X_prime с данными X_t и посмотрим, как он продвигается:

X_prime = torch.tensor([[1., 3., 5.],
                        [2., 4., 6.]])
X_prime.stride(), X_prime.storage()
>>output:
(3, 1),
 1.0
 3.0
 5.0
 2.0
 4.0
 6.0
 [torch.FloatStorage of size 6]

Итак, X_prime и X_t имеют одинаковые данные в одном и том же порядке (в тензорах!), Но с разными массивами хранения и разными шагами по ним.

Теперь, когда мы изучили шаг, мы можем узнать о еще одной концепции тензоров: смежных тензорах. Рассмотрим наш тензор X_prime. Если мы перемещаемся по одной строке в этом тензоре, элементы размещаются как в его хранилище, и нам не нужно перепрыгивать через какое-либо число. Такой тензор называется «смежным». Однако, если мы перемещаемся по одной строке в нашем тензоре X_t, порядок отличается от того, что мы видим в его хранилище, и мы должны пропустить некоторые числа в хранилище, чтобы перейти к получению следующего элемента тензора; Итак, X_t не является смежным.

X_prime.is_contiguous(), X_t.is_contiguous()
>>output:
True, False

Кстати, сделать тензор смежным так же просто, как вызвать метод contiguous () для этого тензора:

X_t = X_t.contiguous()
X_t.is_contiguous()
>>output:
True

Если вы проверите хранилище X_t после этой операции, вы увидите, что оно изменилось и похоже на хранилище X_prime.

Стоит отметить, что некоторые операции, такие как «просмотр» в PyTorch, работают только с непрерывными тензорами.

В последней части этого поста я хочу ввести еще один термин, «смещение», который очень прост. Когда вы разбиваете тензор путем индексации, как вы могли догадаться, базовое хранилище остается неизменным! Опять таки! На этот раз PyTorch дает вам другой тензор, который показывает другое представление основного массива хранения. Чтобы сохранить в новом тензоре дополнительную информацию о том, где начать тензор из основного хранилища, эта информация сохраняется в другом свойстве объекта тензора с именем «storage_offset». Посмотрим на код:

X = torch.tensor([[1., 2.], [3., 4.], [5., 6.]])
X_chunk = X[2]
X_chunk, X_chunk.storage_offset()
>>output:
tensor([5., 6.]), 4

В приведенном выше коде мы выбрали третью строку нашего тензора X и сохранили ее в новом тензоре с именем X_chunk, а затем распечатали новый тензор и его смещение памяти. Смещение хранилища сообщает нам (или, может быть, PyTorch!), Что этот новый тензор является представлением в том же хранилище, что и хранилище X, но начинается с элемента в хранилище с индексом 4 (то есть номером 5). Вот и все! Теперь вы знаете, что такое смещение!

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

Многие идеи были вдохновлены: Учебником по глубокому обучению с помощью PyTorch. https://pytorch.org/deep-learning-with-pytorch