На этом шаге мы рассмотрим особенности выполнения этих операций.
Операции relu и сложение - это поэлементные операции, то есть такие, которые применяются к каждому отдельному элементу в тензоре. Они поддаются массовому распараллеливанию (векторизации - термин пришел из архитектуры векторного процессора суперкомпьютера периода 1970-1990). Для реализации поэлементных операций на Python можно использовать цикл for, как в следующем примере реализации операции relu:
def naive_relu(x): # Убедиться что x — двумерный тензор NumPy assert len(x.shape) == 2 x = x.copy() # Исключить перезапись исходного тензора for i in range(x.shape[0]): for j in range(x.shape[1]): x[i, j] = max(x[i, j], 0) return x
Точно так же реализуется сложение:
def naive_add(x, y): # Убедиться что x, y — двумерные тензоры NumPy assert len(x.shape) == 2 assert x.shape == y.shape x = x.copy() # Исключить перезапись исходного тензора for i in range(x.shape[0]): for j in range(x.shape[1]): x[i, j] += y[i, j] return x
Следуя тому же принципу, можно реализовать поэлементное умножение, вычитание и т. д.
При работе с массивами NumPy можно пользоваться уже готовыми, оптимизированными реализациями этих операций, доступными в виде функций из пакета NumPy, которые сами делегируют основную работу реализациям базовых подпрограмм линейной алгебры (Basic Linear Algebra Subprograms, BLAS), если они установлены (конечно же, они должны быть установлены). BLAS - это комплект низкоуровневых, параллельных и эффективных процедур для вычислений с тензорами, которые обычно реализуются на Fortran или C.
Иными словами, при использовании NumPy поэлементные операции можно записывать, как показано ниже, и выполняться они будут почти мгновенно:
import numpy as np z = x + y # Поэлементное сложение z = np.maximum(z, 0.) # Поэлементная операция relu
Давайте измерим, насколько этот процесс "мгновенный":
import time x = np.random.random((20, 100)) y = np.random.random((20, 100)) t0 = time.time() for _ in range(1000): z = x + y z = np.maximum(z, 0.) print("Took: {0:.2f} s".format(time.time() - t0))
У нас эта операция выполнилась за 0,01 секунды, тогда как предыдущей наивной реализации потребовалось 3,62 секунды:
t0 = time.time() for _ in range(1000): z = naive_add(x, y) z = naive_relu(z) print("Took: {0:.2f} s".format(time.time() - t0))
Аналогично при запуске на графическом процессоре код TensorFlow выполняет поэлементные операции с применением полностью векторизованных реализаций CUDA, которые максимально эффективно используют архитектуру графического процессора с высокой степенью параллелизма.
На следующем шаге мы рассмотрим расширение.