На этом шаге мы рассмотрим суть и примеры использования механизма расширения.
Наша предыдущая реализация naive_add() поддерживает только сложение тензоров второго ранга с идентичными формами. Но в слое Dense, представленном на предыдущих шагах, мы складывали двумерный тензор с вектором. Что происходит при сложении, когда формы складываемых тензоров отличаются?
Когда это возможно и не вызывает неоднозначности, меньший тензор расширяется так, чтобы его новая форма соответствовала форме большего тензора. Расширение выполняется в два этапа. 1 2
Рассмотрим конкретный пример. Допустим, у нас имеются тензоры X с формой (32, 10) и у с формой (10,):
import numpy as np X = np.random.random((32, 10)) # X — матрица случайных чисел с формой (32, 10) y = np.random.random((10,)) # y — вектор случайных чисел с формой (10,)
Сначала нужно добавить первую пустую ось в вектор y, чтобы привести его форму к виду (1, 10):
y = np.expand_dims(y, axis=0) # Теперь y имеет форму (1, 10)
Затем скопируем y по этой новой оси 32 раза, чтобы в результате получился тензор Y с формой (32, 10), где Y[i, :] == y для i в диапазоне range(0, 32).
# Скопировать y 32 раза вдоль оси 0, чтобы получить Y с формой (32, 10)
Y = np.concatenate([y] * 32, axis=0)
После этого можно сложить X и Y, которые имеют одинаковую форму.
В фактической реализации новый двумерный тензор, конечно же, не создается, потому что это было бы неэффективно. Операция копирования выполняется чисто виртуально: она происходит на алгоритмическом уровне, а не в памяти. Но такое представление с копированием вектора для новой оси является полезной мысленной моделью. Вот как могла бы выглядеть наивная реализация:
def naive_add_matrix_and_vector(x, y): assert len(x.shape) == 2. # Убедиться что x — двумерный тензор NumPy assert len(y.shape) == 1. # Убедиться что y — вектор NumPy assert x.shape[1] == y.shape[0] x = x.copy() # Исключить перезапись исходного тензора for i in range(x.shape[0]): for j in range(x.shape[1]): x[i, j] += y[j] return x
Прием расширения в общем случае можно использовать в поэлементных операциях с двумя тензорами, если один тензор имеет форму (a, b, ... n, n + 1, ... m), а другой - форму (n, n + 1, ... m). В этом случае при расширении будут добавлены оси до n - 1.
Следующий пример показывает применение поэлементной операции maximum к двум тензорам с разными формами посредством расширения:
import numpy as np x = np.random.random((64, 3, 32, 10)) # x — тензор случайных чисел, # имеющий форму (64, 3, 32, 10) y = np.random.random((32, 10)) # — тензор случайных чисел, имеющий форму (32, 10) z = np.maximum(x, y) # Получившийся тензор z имеет форму (64, 3, 32, 10) аналогично x
На следующем шаге мы рассмотрим скалярное произведение тензоров.