Один из способов преодоления этой проблемы — векторизация . Мы выполняем над массивом операцию, которая изначально определяется для конкретных элементов, и перезаписываем ее как операцию над всем массивом. Для обновления матрицы ссылок перезапись может происходить так:
Здесь I — единичная матрица, а
— векторное произведение. Чтобы получить векторизацию, мы задаем новый оператор — попарное сложение векторов, обозначаемое ⊕. Его можно определить так:
Этот оператор требует дополнительной памяти, но все же не так много, как при цикловой реализации. При векторизованном переформулировании правила обновления мы можем записать более эффективную с точки зрения памяти и времени реализацию:
def Lt(L, wwt, p, N):
# we only need the case of adding a single vector to itself (нам нужен только случай добавления единичного вектора к самому себе)
def pairwise_add(v):
n = v.get_shape(). as_list()[0]
# an NxN matrix of duplicates of u along the columns (матрицы NxN копий u по столбцам)
V = tf.concat(1, [v] * n)
return V + V
I = tf.constant(np.identity(N, dtype=np.float32))
updated = (1 — pairwise_add(wwt)) * L + tf.matmul(wwt, p)
updated = updated * (1 — I) # eliminate self-links
return updated
Примерно то же можно сделать и для правила выделения взвешиваний. Одно правило для каждого элемента вектора мы разбиваем на несколько операций, которые будут работать со всем вектором сразу.
1. При сортировке вектора использования для получения списка свободной памяти берем также сам вектор отсортированного использования.
2. Вычисляем вектор кумулятивного произведения отсортированного использования. Каждый элемент соответствует части произведения в первоначальном поэлементном правиле.
3. Умножаем вектор кумулятивного произведения на (1-вектор отсортированного использования). Полученный вектор — выделение взвешивания, но в отсортированном порядке, а не в исходном порядке ячейки записи.
4. Для каждого элемента неупорядоченного выделения взвешивания берем его значение и помещаем в соответствующий индекс списка свободной памяти. Полученный вектор — то выделение взвешивания, которое нам и нужно.
На рис. 8.11 приведен этот же процесс с числовыми примерами.
Рис. 8.11. Векторизованный процесс расчета выделения взвешивания
Может показаться, что для сортировки на шаге 1 и переупорядочивания весов на шаге 4 нам все равно понадобятся циклы, но, к счастью, TensorFlow предлагает такие символические операции, которые позволяют выполнять эти действия, не прибегая к циклам Python.
Для сортировки мы будем использовать tf.nn.top_k. Эта операция принимает тензор и число k и выдает как отсортированные высшие значения k в порядке убывания, так и их индексы. Чтобы получить вектор отсортированного использования в порядке возрастания, нужно взять высшие значения N вектора, обратного вектору использования. Привести отсортированные значения к исходным знакам можно, умножив получившийся вектор на −1:
sorted_ut, free_list = tf.nn.top_k(-1 * ut, N)
sorted_ut *= -1
Для переупорядочивания весов выделения воспользуемся новой структурой данных TensorFlow под названием TensorArray. Эти массивы можно считать символической альтернативой списку Python. Сначала получаем пустой массив тензоров размера N, содержащий веса в верном порядке, затем помещаем туда значения в правильном порядке методом экземпляра scatter(indices, values). Он получает в качестве второго аргумента тензор и рассеивает значения по первому измерению по всему массиву, причем первый аргумент — список индексов ячеек, по которым мы хотим рассеять соответствующие значения. В нашем случае первый аргумент — список свободной памяти, а второй — неупорядоченные выделения взвешиваний. Получив массив с весами в нужных местах, используем еще один метод экземпляра — pack() — для помещения всего массива в объект Tensor:
empty_at = tf.TensorArray(tf.float32, N)
full_at = empty_at.scatter(free_list, out_of_location_at)
a_t = full_at.pack()
Последняя часть реализации, где необходимо введение циклов, — собственно цикл контроллера, который проходит по каждому шагу входной последовательности, обрабатывая ее. Поскольку векторизация работает только при поэлементном определении операций, цикл контроллера векторизовать нельзя. К счастью, TensorFlow снова дает нам возможность избежать циклов for в Python, что существенно отразилось бы на производительности; это метод символического цикла . Он работает так же, как большинство символических операций: вместо разворачивания настоящего цикла в граф определяется узел, который пройдет как цикл при выполнении самого графа.
Читать дальше
Конец ознакомительного отрывка
Купить книгу