2.1. Манипуляция данными¶
Чтобы решать любые задачи, нам нужен способ хранения и обработки данных.
Как правило, с данными необходимо выполнить две важные вещи: (i)
получить их; и (ii) обработать, когда они уже находятся в компьютере.
Нет смысла собирать данные, если нет способа их сохранить, поэтому для
начала давайте разберемся с \(n\)-мерными массивами, которые мы
также называем тензорами. Если вы уже знакомы с научным пакетом
вычислений NumPy, это покажется вам простым. Во всех современных
фреймворках глубокого обучения класс тензора (ndarray в MXNet,
Tensor в PyTorch и TensorFlow) напоминает NumPy’s ndarray, но с
добавлением нескольких ключевых возможностей. Во-первых, класс тензора
поддерживает автоматическое дифференцирование. Во-вторых, он использует
графические процессоры (GPU) для ускорения численных вычислений, тогда
как NumPy работает только на центральных процессорах (CPU). Эти свойства
делают кодирование нейронных сетей простым, а их выполнение — быстрым.
2.1.1. Начало работы¶
Для начала импортируем библиотеку PyTorch. Обратите внимание, что имя
пакета — torch.
import torch
[Тензор представляет собой (возможно, многомерный) массив числовых значений.] В одномерном случае, то есть когда для данных требуется только одна ось, тензор называется вектором. При наличии двух осей тензор называется матрицей. При \(k > 2\) осях мы отказываемся от специальных названий и просто называем объект тензором \(k^\textrm{th}\)-го порядка.
PyTorch предоставляет множество функций для создания новых тензоров,
предварительно заполненных значениями. Например, вызвав arange(n),
мы можем создать вектор значений, равномерно распределенных от 0
(включительно) до n (не включительно). По умолчанию размер шага
равен \(1\). Если не указано иное, новые тензоры хранятся в основной
памяти и предназначены для вычислений на процессоре.
x = torch.arange(12, dtype=torch.float32)
x
Каждое из этих значений называется элементом тензора. Тензор x
содержит 12 элементов. Общее число элементов в тензоре можно узнать с
помощью его метода numel.
x.numel()
Мы можем получить форму тензора (длину вдоль каждой оси), обратившись
к его атрибуту shape. Поскольку в данном случае мы имеем дело с
вектором, shape содержит всего один элемент и совпадает с размером.
x.shape
Мы можем изменить форму тензора, не меняя его размер или значения,
вызвав reshape. Например, мы можем преобразовать наш вектор x
формы (12,) в матрицу X формы (3, 4). Этот новый тензор сохраняет
все элементы, но перестраивает их в матрицу. Обратите внимание, что
элементы нашего вектора располагаются построчно, поэтому
x[3] == X[0, 3].
X = x.reshape(3, 4)
X
Обратите внимание, что указание всех компонентов формы в reshape
избыточно. Поскольку мы уже знаем размер нашего тензора, мы можем
вычислить один компонент формы, зная остальные. Например, для тензора
размера \(n\) и целевой формы (\(h\), \(w\)) мы знаем, что
\(w = n/h\). Чтобы автоматически вывести один из компонентов формы,
мы можем указать -1 для того компонента, который должен быть
вычислен автоматически. В нашем случае, вместо вызова
x.reshape(3, 4), мы могли бы эквивалентным образом вызвать
x.reshape(-1, 4) или x.reshape(3, -1).
Практикующим специалистам часто требуется работать с тензорами,
инициализированными нулями или единицами. Мы можем создать тензор, все
элементы которого равны 0 и формой (2, 3, 4) с помощью функции
zeros.
torch.zeros((2, 3, 4))
Аналогичным образом мы можем создать тензор, состоящий из одних единиц,
вызвав ones.
torch.ones((2, 3, 4))
Мы часто хотим случайным образом (и независимо) выбрать каждый элемент из заданного распределения вероятностей. Например, параметры нейронных сетей часто инициализируются случайным образом. Приведённый ниже фрагмент создает тензор, элементы которого берутся из стандартного гауссова (нормального) распределения со средним 0 и стандартным отклонением 1.
torch.randn(3, 4)
Наконец, мы можем создать тензор, задав точные значения для каждого элемента, передав (возможно, вложенные) списки Python, содержащие числовые литералы. В данном случае мы создаём матрицу с помощью списка списков, где внешний список соответствует оси 0, а внутренний — оси 1.
torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
2.1.2. Индексация и срезы¶
Как и в списках Python, мы можем обращаться к элементам тензора с
помощью индексации (отсчёт начинается с 0). Чтобы получить элемент,
основываясь на его позиции относительно конца списка, можно использовать
отрицательную индексацию. Наконец, мы можем получать целые диапазоны
индексов с помощью срезов (например, X[start:stop]), при этом
возвращаемое значение включает первый индекс (start), но не
последний (stop). Наконец, когда для тензора нулевого порядка
(\(k^\textrm{th}\)) указан только один индекс (или срез), он
применяется вдоль оси 0. Таким образом, в приведённом ниже коде [-1]
выбирает последнюю строку, а [1:3] — вторую и третью строки.
X[-1], X[1:3]
Помимо чтения, мы также можем записывать элементы матрицы, указывая индексы.
X[1, 2] = 17
X
Если мы хотим присвоить несколько элементов одно и то же значение, мы
используем индексацию в левой части операции присваивания. Например,
[:2, :] выбирает первую и вторую строки, а : выбирает все
элементы вдоль оси 1 (столбца). Хотя мы обсуждали индексацию для матриц,
это также работает для векторов и тензоров с более чем двумя
измерениями.
X[:2, :] = 12
X
2.1.3. Операции¶
Теперь, когда мы знаем, как создавать тензоры и как обращаться к их элементам, мы можем начать манипулировать ими с помощью различных математических операций. Одной из самых полезных категорий являются поэлементные операции. Они применяют стандартную скалярную операцию к каждому элементу тензора. Для функций, принимающих два тензора в качестве входных данных, поэлементные операции применяют некоторый стандартный бинарный оператор к каждой паре соответствующих элементов. Мы можем создать поэлементную функцию из любой функции, отображающей скаляр в скаляр.
В математической нотации такие унарные скалярные операторы (принимающие один вход) обозначаются сигнатурой \(f: \mathbb{R} \rightarrow \mathbb{R}\). Это означает, что функция отображает любое действительное число в другое действительное число. Большинство стандартных операторов, включая унарные, такие как \(e^x\), могут применяться поэлементно.
torch.exp(x)
Аналогичным образом мы обозначаем бинарные скалярные операторы,
которые отображают пары действительных чисел в одно действительное
число, сигнатурой
\(f: \mathbb{R}, \mathbb{R} \rightarrow \mathbb{R}\). Для любых двух
векторов \(\mathbf{u}\) и \(\mathbf{v}\) одинаковой формы и
бинарного оператора \(f\) мы можем получить вектор
\(\mathbf{c} = F(\mathbf{u},\mathbf{v})\), задав
\(c_i \gets f(u_i, v_i)\) для всех \(i\), где \(c_i, u_i\) и
\(v_i\) — это \(i^\textrm{th}\) элементы векторов
\(\mathbf{c}, \mathbf{u}\) и \(\mathbf{v}\). Таким образом, мы
получили вектор
\(F: \mathbb{R}^d, \mathbb{R}^d \rightarrow \mathbb{R}^d\),
перенеся скалярную функцию в поэлементную векторную операцию.
Стандартные арифметические операторы сложения (+), вычитания
(-), умножения (*), деления (/) и возведения в степень
(**) также были перенесены в поэлементные операции для тензоров
одинаковой формы произвольной формы.
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y
Помимо поэлементных вычислений, мы также можем выполнять операции линейной алгебры, такие как скалярные произведения и умножение матриц. Мы подробно рассмотрим их в Section 2.3.
Мы также можем сконкатенировать несколько тензоров, соединяя их последовательно для формирования большего тензора. Для этого достаточно передать список тензоров и указать, по какой оси следует выполнять конкатенацию. В приведённом ниже примере показано, что происходит при конкатенации двух матриц по строкам (ось 0) вместо столбцов (ось 1). Мы видим, что длина оси 0 первого выходного тензора (\(6\)) равна сумме длин оси 0 двух входных тензоров (\(3 + 3\)); в то время как длина оси 1 второго выходного тензора (\(8\)) равна сумме длин оси 1 двух входных тензоров (\(4 + 4\)).
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
Иногда нам нужно создать бинарный тензор с помощью логических
выражений. Возьмем X == Y в качестве примера. Для каждой позиции
i, j, если X[i, j] и Y[i, j] равны, то соответствующий
элемент результата принимает значение 1, в противном случае —
значение 0.
X == Y
Суммирование всех элементов тензора даёт тензор, содержащий только один элемент.
X.sum()
2.1.4. Транслирование¶
К этому моменту вы уже знаете, как выполнять поэлементные бинарные операции над двумя тензорами одинаковой формы. При определённых условиях, даже если формы различаются, мы по-прежнему можем выполнять поэлементные бинарные операции, используя механизм транслирования. Транслирование работает по следующей двухшаговой процедуре: (i) расширение одного или обоих массивов путём копирования элементов вдоль осей с длиной 1 так, чтобы после этого преобразования оба тензора имели одинаковую форму; (ii) выполнение поэлементной операции над полученными массивами.
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b
Поскольку a и b являются матрицами размеров \(3\times1\) и
\(1\times2\) соответственно, их формы не совпадают. Транслирование
создаёт матрицу большего размера \(3\times2\), дублируя матрицу
a по столбцам и матрицу b по строкам, после чего выполняется
поэлементное сложение.
a + b
2.1.5. Экономия памяти¶
Выполнение операций может привести к выделению новой памяти для хранения
результатов. Например, если мы запишем Y = X + Y, мы отменим ссылку
на тензор, на который ранее указывал Y, и вместо этого направим
Y на только что выделенную область памяти. Мы можем
продемонстрировать эту проблему с помощью функции id() в Python,
которая позволяет получить точный адрес объекта в памяти. Обратите
внимание, что после выполнения Y = Y + X, id(Y) указывает на
другое место. Это связано с тем, что Python сначала вычисляет Y + X,
выделяя новую память для результата, а затем направляет Y на эту
новую область памяти.
before = id(Y)
Y = Y + X
id(Y) == before
Это может быть нежелательно по двум причинам. Во-первых, мы не хотим постоянно выделять память без необходимости. В машинном обучении у нас часто есть сотни мегабайт параметров, и мы обновляем их все несколько раз в секунду. По возможности мы хотим выполнять эти обновления на месте. Во-вторых, на одни и те же параметры могут ссылаться несколько переменных. Если мы не обновляем значения на месте, нам нужно быть осторожными и обновлять все эти ссылки, чтобы не допустить утечки памяти или случайного обращения к устаревшим параметрам.
К счастью, выполнение операций на месте — это просто. Мы можем
присвоить результат операции ранее выделенному массиву Y, используя
нотацию срезов: Y[:] = <expression>. Чтобы проиллюстрировать эту
концепцию, мы перезапишем значения тензора Z, после его
инициализации с помощью zeros_like, так, чтобы он имел ту же форму,
что и Y.
Z = torch.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))
Если значение X не используется в последующих вычислениях, мы можем
также использовать X[:] = X + Y или X += Y для снижения
накладных расходов на память операции.
before = id(X)
X += Y
id(X) == before
2.1.6. Преобразование в другие объекты Python¶
Преобразование в тензор NumPy (ndarray), или наоборот, выполняется
просто. Тензор PyTorch и массив NumPy разделяют общую память, и
изменение одного из них с помощью операции на месте также изменит
другой.
A = X.numpy()
B = torch.from_numpy(A)
type(A), type(B)
Чтобы преобразовать тензор размера 1 в скаляр Python, мы можем
вызвать функцию item или встроенные функции Python.
a = torch.tensor([3.5])
a, a.item(), float(a), int(a)
2.1.7. Итог¶
Класс тензор является основным интерфейсом для хранения и обработки данных в библиотеках глубокого обучения. Тензоры предоставляют множество функций, включая методы создания, индексацию и срезы, базовые математические операции, транслирование, эффективное по памяти присваивание, а также преобразование в другие объекты Python и обратно.
2.1.8. Упражнения¶
Запустите код в этом разделе. Замените условное выражение
X == YнаX < YилиX > Yи посмотрите, какой тензор получится.Замените два тензора, участвующих в механизме транслирования, на тензоры других форм, например, трёхмерные. Совпадает ли результат с ожидаемым?