Увеличение каждого элемента тензора предшественником в Tensorflow 2.0 - PullRequest
7 голосов
/ 08 марта 2020

Я новичок в tenorflow 2.0 , и ничего не сделал, кроме разработки и обучения некоторых искусственных нейронных сетей из стандартного кода. Я пытаюсь выполнить упражнение для новичков в новом тензорном потоке. Я создал некоторый код, но он не работает. Ниже приведено определение проблемы :


Предполагается, что у нас есть тензор M рациональных чисел в форме (a, b, c) и скаляр p ∈ (0, 1) (память фактор), давайте создадим функцию, которая будет возвращать тензор N в форме (a, b, c). Каждый элемент из N тензоров, движущихся по оси c, должен быть увеличен на значение предшественника, умноженное на p.

При условии, что у нас есть тензор:

T = [x1, x2, x3, x4]

в форме (1, 1, 4), мы хотели бы получить вектор:

[x1, x2+x1·p, x3+(x2+x1·p)·p, x4+(x3+(x2+x1·p)·p)*p] 

Решение должно быть создано в Tensorflow 2.0 и должно быть направлено на обеспечение кратчайшего времени выполнения на ЦПУ. Созданный график должен позволять эффективно вычислять производную как по тензору M, так и по значению p.


Это код , который я создал до сих пор :

import tensorflow as tf

@tf.function
def vectorize_predec(t, p):
    last_elem = 0
    result = []
    for el in t:
        result.append(el + (p * last_elem))
        last_elem = el + (p * last_elem)
    return result

p = tf.Variable(0.5, dtype='double')

m = tf.constant([[0, 1, 2, 3, 4],
          [1, 3, 5, 7, 10],
          [1, 1, 1, -1, 0]])

vectorize_predec(m, p)

Но это выдает TypeError.

Я просмотрел документацию, я видел такие функции, как cumsum и polyeval, но я не уверен, что они соответствуют моим потребностям. Насколько я понимаю, мне нужно написать собственную функцию клиента, помеченную @tf.function. Я также не уверен, как правильно обрабатывать трехмерные тензоры в соответствии с определением задачи (добавление предшественника должно происходить на последней ( "c" ) оси).

Я видел в документации (здесь: https://www.tensorflow.org/tutorials/customization/performance), что есть способы измерить размер полученного графика. Хотя я не уверен, как «график» позволяет эффективно вычислять производную как по тензорному M, так и по значению p. Ответы на ELI5 оценены, или, по крайней мере, некоторые материалы, которые я могу прочитать, чтобы лучше научиться.

Большое спасибо!

Ответы [ 2 ]

5 голосов
/ 11 марта 2020

Я дам вам несколько разных методов для реализации этого. Я думаю, что наиболее очевидным решением является использование tf.scan:

import tensorflow as tf

def apply_momentum_scan(m, p, axis=0):
    # Put axis first
    axis = tf.convert_to_tensor(axis, dtype=tf.int32)
    perm = tf.concat([[axis], tf.range(axis), tf.range(axis + 1, tf.rank(m))], axis=0)
    m_t = tf.transpose(m, perm)
    # Do computation
    res_t = tf.scan(lambda a, x: a * p + x, m_t)
    # Undo transpose
    perm_t = tf.concat([tf.range(1, axis + 1), [0], tf.range(axis + 1, tf.rank(m))], axis=0)
    return tf.transpose(res_t, perm_t)

Однако вы также можете реализовать это как конкретный матричный продукт, если вы строите матрицу экспоненциальных факторов:

import tensorflow as tf

def apply_momentum_matmul(m, p, axis=0):
    # Put axis first and reshape
    m = tf.convert_to_tensor(m)
    p = tf.convert_to_tensor(p)
    axis = tf.convert_to_tensor(axis, dtype=tf.int32)
    perm = tf.concat([[axis], tf.range(axis), tf.range(axis + 1, tf.rank(m))], axis=0)
    m_t = tf.transpose(m, perm)
    shape_t = tf.shape(m_t)
    m_tr = tf.reshape(m_t, [shape_t[0], -1])
    # Build factors matrix
    r = tf.range(tf.shape(m_tr)[0])
    p_tr = tf.linalg.band_part(p ** tf.dtypes.cast(tf.expand_dims(r, 1) - r, p.dtype), -1, 0)
    # Do computation
    res_tr = p_tr @ m_tr
    # Reshape back and undo transpose
    res_t = tf.reshape(res_tr, shape_t)
    perm_t = tf.concat([tf.range(1, axis + 1), [0], tf.range(axis + 1, tf.rank(m))], axis=0)
    return tf.transpose(res_t, perm_t)

Это также можно переписать, чтобы избежать первого переноса (который в TensorFlow стоит) с tf.tensordot:

import tensorflow as tf

def apply_momentum_tensordot(m, p, axis=0):
    # Put axis first and reshape
    m = tf.convert_to_tensor(m)
    # Build factors matrix
    r = tf.range(tf.shape(m)[axis])
    p_mat = tf.linalg.band_part(p ** tf.dtypes.cast(tf.expand_dims(r, 1) - r, p.dtype), -1, 0)
    # Do computation
    res_t = tf.linalg.tensordot(m, p_mat, axes=[[axis], [1]])
    # Transpose
    last_dim = tf.rank(res_t) - 1
    perm_t = tf.concat([tf.range(axis), [last_dim], tf.range(axis, last_dim)], axis=0)
    return tf.transpose(res_t, perm_t)

Три функции будут использовать аналогичным образом:

import tensorflow as tf

p = tf.Variable(0.5, dtype=tf.float32)
m = tf.constant([[0, 1, 2, 3, 4],
                 [1, 3, 5, 7, 10],
                 [1, 1, 1, -1, 0]], tf.float32)
# apply_momentum is one of the functions above
print(apply_momentum(m, p, axis=0).numpy())
# [[ 0.    1.    2.    3.    4.  ]
#  [ 1.    3.5   6.    8.5  12.  ]
#  [ 1.5   2.75  4.    3.25  6.  ]]
print(apply_momentum(m, p, axis=1).numpy())
# [[ 0.      1.      2.5     4.25    6.125 ]
#  [ 1.      3.5     6.75   10.375  15.1875]
#  [ 1.      1.5     1.75   -0.125  -0.0625]]

Использование матричного произведения более асимптотически сложно, но может быть быстрее сканирования. Вот небольшой тест:

import tensorflow as tf
import numpy as np

# Make test data
tf.random.set_seed(0)
p = tf.constant(0.5, dtype=tf.float32)
m = tf.random.uniform([100, 30, 50], dtype=tf.float32)

# Axis 0
print(np.allclose(apply_momentum_scan(m, p, 0).numpy(), apply_momentum_matmul(m, p, 0).numpy()))
# True
print(np.allclose(apply_momentum_scan(m, p, 0).numpy(), apply_momentum_tensordot(m, p, 0).numpy()))
# True
%timeit apply_momentum_scan(m, p, 0)
# 11.5 ms ± 610 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit apply_momentum_matmul(m, p, 0)
# 1.36 ms ± 18.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit apply_momentum_tensordot(m, p, 0)
# 1.62 ms ± 7.39 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

# Axis 1
print(np.allclose(apply_momentum_scan(m, p, 1).numpy(), apply_momentum_matmul(m, p, 1).numpy()))
# True
print(np.allclose(apply_momentum_scan(m, p, 1).numpy(), apply_momentum_tensordot(m, p, 1).numpy()))
# True
%timeit apply_momentum_scan(m, p, 1)
# 4.27 ms ± 60.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit apply_momentum_matmul(m, p, 1)
# 1.27 ms ± 36.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit apply_momentum_tensordot(m, p, 1)
# 1.2 ms ± 11.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

# Axis 2
print(np.allclose(apply_momentum_scan(m, p, 2).numpy(), apply_momentum_matmul(m, p, 2).numpy()))
# True
print(np.allclose(apply_momentum_scan(m, p, 2).numpy(), apply_momentum_tensordot(m, p, 2).numpy()))
# True
%timeit apply_momentum_scan(m, p, 2)
# 6.29 ms ± 64.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit apply_momentum_matmul(m, p, 2)
# 1.41 ms ± 21.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit apply_momentum_tensordot(m, p, 2)
# 1.05 ms ± 26 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Итак, матричный продукт, похоже, выиграл. Давайте посмотрим, если это масштабируется:

import tensorflow as tf
import numpy as np

# Make test data
tf.random.set_seed(0)
p = tf.constant(0.5, dtype=tf.float32)
m = tf.random.uniform([1000, 300, 500], dtype=tf.float32)

# Axis 0
print(np.allclose(apply_momentum_scan(m, p, 0).numpy(), apply_momentum_matmul(m, p, 0).numpy()))
# True
print(np.allclose(apply_momentum_scan(m, p, 0).numpy(), apply_momentum_tensordot(m, p, 0).numpy()))
# True
%timeit apply_momentum_scan(m, p, 0)
# 784 ms ± 6.78 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit apply_momentum_matmul(m, p, 0)
# 1.13 s ± 76.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit apply_momentum_tensordot(m, p, 0)
# 1.3 s ± 27 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# Axis 1
print(np.allclose(apply_momentum_scan(m, p, 1).numpy(), apply_momentum_matmul(m, p, 1).numpy()))
# True
print(np.allclose(apply_momentum_scan(m, p, 1).numpy(), apply_momentum_tensordot(m, p, 1).numpy()))
# True
%timeit apply_momentum_scan(m, p, 1)
# 852 ms ± 12.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit apply_momentum_matmul(m, p, 1)
# 659 ms ± 10.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit apply_momentum_tensordot(m, p, 1)
# 741 ms ± 19.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# Axis 2
print(np.allclose(apply_momentum_scan(m, p, 2).numpy(), apply_momentum_matmul(m, p, 2).numpy()))
# True
print(np.allclose(apply_momentum_scan(m, p, 2).numpy(), apply_momentum_tensordot(m, p, 2).numpy()))
# True
%timeit apply_momentum_scan(m, p, 2)
# 1.06 s ± 16.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit apply_momentum_matmul(m, p, 2)
# 924 ms ± 17 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit apply_momentum_tensordot(m, p, 2)
# 483 ms ± 10.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Ну, теперь это уже не так ясно. Сканирование все еще не супер быстрое, но матричные продукты иногда медленнее. Как вы можете себе представить, если вы go для еще больших тензоров, сложность матричных продуктов будет доминировать во временах.

Итак, если вы хотите самое быстрое решение и знаете, что ваши тензоры не станут огромными, используйте один реализации матричного продукта. Если у вас все хорошо с хорошей скоростью, но вы хотите убедиться, что у вас не хватает памяти (матричное решение также требует гораздо больше) и время предсказуемо, вы можете использовать решение для сканирования.

Примечание. Тесты выше были выполнены на CPU, результаты могут значительно отличаться на GPU.

3 голосов
/ 13 марта 2020

Вот ответ, который просто предоставляет некоторую информацию, и наивное решение для исправления кода - не реальная проблема (пожалуйста, обратитесь к ниже для выяснения причины).

Прежде всего, TypeError проблема несовместимых типов в тензорах вашей ранней попытки. Некоторые тензоры содержат числа с плавающей запятой (double), некоторые содержат целые числа. Это помогло бы отобразить сообщение об ошибке full :

TypeError: Input 'y' of 'Mul' Op has type int32 that does not match type float64 of argument 'x'.

, которое может привести к правильному следу (несмотря на ужасные детали трассировки стека).

Вот наивное исправление, чтобы заставить код работать (с предостережениями против целевой задачи):

import tensorflow as tf

@tf.function
def vectorize_predec(t, p):
    _p = tf.transpose(
        tf.convert_to_tensor(
            [p * t[...,idx] for idx in range(t.shape[-1] - 1)],
            dtype=tf.float64))
    _p = tf.concat([
        tf.zeroes((_p.shape[0], 1), dtype=tf.float64),
        _p
    ], axis=1)
    return t + _p

p = tf.Variable(0.5, dtype='double')

m = tf.constant([[0, 1, 2, 3, 4],
          [1, 3, 5, 7, 10],
          [1, 1, 1, -1, 0]], dtype=tf.float64)

n = tf.constant([[0.0, 1.0, 2.5, 4.0, 5.5],
          [1.0, 3.5, 6.5, 9.5, 13.5],
          [1.0, 1.5, 1.5, -0.5, -0.5]], dtype=tf.float64)
print(f'Expected: {n}')

result = vectorize_predec(m, p)
print(f'Result: {result}')

tf.test.TestCase().assertAllEqual(n, result)

Основные изменения:

  • Тензор m получает dtype=tf.float64 для соответствия оригиналу double, поэтому ошибка типа исчезает.
  • Эта функция в основном полностью переписана. Наивная идея состоит в том, чтобы использовать определение проблемы, которое не указывает, вычисляются ли значения в N до или после обновлений. Вот версия до обновления, проще. Решение проблемы, которая кажется «реальной», требует больше работы над этой функцией (см. Другие ответы, и я могу подробнее поработать здесь).

Как работает функция:

  • Рассчитывает ожидаемые приращения p * x1, p * x2 и т. Д. c в стандартный массив Python. Обратите внимание, что он останавливается перед последним элементом последнего измерения, так как мы сместим массив.
  • Он преобразует массив в тензор с tf.convert_to_tensor, поэтому добавляем массив в граф вычислений. Транспонирование необходимо для соответствия исходной форме тензора (мы могли бы избежать этого).
  • Он добавляет нули в начале каждого измерения вдоль последней оси.
  • Результатом является сумма оригинальный тензор и построенный.

Значения становятся x1 + 0.0 * p, затем x2 + x1 * p, et c. Это иллюстрирует некоторые функции и проблемы, которые нужно рассмотреть (типы, формы), но я признаю, что это обманывает и не решает реальную проблему.

Кроме того, этот код неэффективен на любом оборудовании. Это просто иллюстративно, и потребуется (1) исключить массив Python, (2) устранить транспонирование, (3) исключить операцию сцепления. Надеюсь, отличная тренировка: -)


Дополнительные примечания:

  • Задача требует решения по тензорам формы (a, b, c). Код, которым вы делитесь, работает с тензорами формы (a, b), поэтому исправление кода все равно не решит проблему.
  • Проблема требует рациональных чисел. Не уверен, что цель, и этот ответ оставляет это требование в стороне.
  • Форма T = [x1, x2, x3, x4] на самом деле (4,), предполагая xi скаляры.
  • Почему tf.float64 ? По умолчанию мы получаем tf.float32, и удаление double сделает код работающим. Но в этом примере теряется смысл в том, что типы имеют значение, поэтому выбор явного типа не по умолчанию (и более уродливого кода).
...