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