Посмотреть на TensorFlow.org | Запустить в Google Colab | Посмотреть на GitHub | Скачать блокнот |
При работе с тензорами, содержащими большое количество нулевых значений, важно хранить их компактно и экономно по времени. Разреженные тензоры обеспечивают эффективное хранение и обработку тензоров, содержащих много нулевых значений. Разреженные тензоры широко используются в схемах кодирования, таких как TF-IDF , как часть предварительной обработки данных в приложениях НЛП и для предварительной обработки изображений с большим количеством темных пикселей в приложениях компьютерного зрения.
Разреженные тензоры в TensorFlow
TensorFlow представляет разреженные тензоры через объект tf.SparseTensor
. В настоящее время разреженные тензоры в TensorFlow кодируются с использованием формата списка координат (COO). Этот формат кодирования оптимизирован для сверхразреженных матриц, таких как вложения.
Кодирование COO для разреженных тензоров состоит из:
-
values
: одномерный тензор формы[N]
, содержащий все ненулевые значения. -
indices
: двумерный тензор формы[N, rank]
, содержащий индексы ненулевых значений. -
dense_shape
: одномерный тензор с shape[rank]
, задающий форму тензора.
Ненулевое значение в контексте tf.SparseTensor
— это значение, которое не закодировано явно. Можно явно включить нулевые значения в values
разреженной матрицы COO, но эти «явные нули» обычно не включаются при ссылке на ненулевые значения в разреженном тензоре.
Создание tf.SparseTensor
Создайте разреженные тензоры, непосредственно указав их values
, indices
и dense_shape
.
import tensorflow as tf
st1 = tf.SparseTensor(indices=[[0, 3], [2, 4]],
values=[10, 20],
dense_shape=[3, 10])
Когда вы используете функцию print()
для печати разреженного тензора, она показывает содержимое трех компонентных тензоров:
print(st1)
SparseTensor(indices=tf.Tensor( [[0 3] [2 4]], shape=(2, 2), dtype=int64), values=tf.Tensor([10 20], shape=(2,), dtype=int32), dense_shape=tf.Tensor([ 3 10], shape=(2,), dtype=int64))
Содержимое разреженного тензора легче понять, если ненулевые values
выровнены с соответствующими им indices
. Определите вспомогательную функцию для красивого вывода разреженных тензоров таким образом, чтобы каждое ненулевое значение отображалось в отдельной строке.
def pprint_sparse_tensor(st):
s = "<SparseTensor shape=%s \n values={" % (st.dense_shape.numpy().tolist(),)
for (index, value) in zip(st.indices, st.values):
s += f"\n %s: %s" % (index.numpy().tolist(), value.numpy().tolist())
return s + "}>"
print(pprint_sparse_tensor(st1))
<SparseTensor shape=[3, 10] values={ [0, 3]: 10 [2, 4]: 20}>
Вы также можете создавать разреженные тензоры из плотных тензоров, используя tf.sparse.from_dense
, и преобразовывать их обратно в плотные тензоры, используя tf.sparse.to_dense
.
st2 = tf.sparse.from_dense([[1, 0, 0, 8], [0, 0, 0, 0], [0, 0, 3, 0]])
print(pprint_sparse_tensor(st2))
<SparseTensor shape=[3, 4] values={ [0, 0]: 1 [0, 3]: 8 [2, 2]: 3}>
st3 = tf.sparse.to_dense(st2)
print(st3)
tf.Tensor( [[1 0 0 8] [0 0 0 0] [0 0 3 0]], shape=(3, 4), dtype=int32)
Управление разреженными тензорами
Используйте утилиты из пакета tf.sparse
для управления разреженными тензорами. Такие операции, как tf.math.add
, которые можно использовать для арифметических операций с плотными тензорами, не работают с разреженными тензорами.
Добавьте разреженные тензоры той же формы с помощью tf.sparse.add
.
st_a = tf.SparseTensor(indices=[[0, 2], [3, 4]],
values=[31, 2],
dense_shape=[4, 10])
st_b = tf.SparseTensor(indices=[[0, 2], [7, 0]],
values=[56, 38],
dense_shape=[4, 10])
st_sum = tf.sparse.add(st_a, st_b)
print(pprint_sparse_tensor(st_sum))
<SparseTensor shape=[4, 10] values={ [0, 2]: 87 [3, 4]: 2 [7, 0]: 38}>
Используйте tf.sparse.sparse_dense_matmul
для умножения разреженных тензоров на плотные матрицы.
st_c = tf.SparseTensor(indices=([0, 1], [1, 0], [1, 1]),
values=[13, 15, 17],
dense_shape=(2,2))
mb = tf.constant([[4], [6]])
product = tf.sparse.sparse_dense_matmul(st_c, mb)
print(product)
tf.Tensor( [[ 78] [162]], shape=(2, 1), dtype=int32)
Соедините разреженные тензоры вместе с помощью tf.sparse.concat
и разберите их с помощью tf.sparse.slice
.
sparse_pattern_A = tf.SparseTensor(indices = [[2,4], [3,3], [3,4], [4,3], [4,4], [5,4]],
values = [1,1,1,1,1,1],
dense_shape = [8,5])
sparse_pattern_B = tf.SparseTensor(indices = [[0,2], [1,1], [1,3], [2,0], [2,4], [2,5], [3,5],
[4,5], [5,0], [5,4], [5,5], [6,1], [6,3], [7,2]],
values = [1,1,1,1,1,1,1,1,1,1,1,1,1,1],
dense_shape = [8,6])
sparse_pattern_C = tf.SparseTensor(indices = [[3,0], [4,0]],
values = [1,1],
dense_shape = [8,6])
sparse_patterns_list = [sparse_pattern_A, sparse_pattern_B, sparse_pattern_C]
sparse_pattern = tf.sparse.concat(axis=1, sp_inputs=sparse_patterns_list)
print(tf.sparse.to_dense(sparse_pattern))
tf.Tensor( [[0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0] [0 0 0 0 1 1 0 0 0 1 1 0 0 0 0 0 0] [0 0 0 1 1 0 0 0 0 0 1 1 0 0 0 0 0] [0 0 0 1 1 0 0 0 0 0 1 1 0 0 0 0 0] [0 0 0 0 1 1 0 0 0 1 1 0 0 0 0 0 0] [0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0]], shape=(8, 17), dtype=int32)
sparse_slice_A = tf.sparse.slice(sparse_pattern_A, start = [0,0], size = [8,5])
sparse_slice_B = tf.sparse.slice(sparse_pattern_B, start = [0,5], size = [8,6])
sparse_slice_C = tf.sparse.slice(sparse_pattern_C, start = [0,10], size = [8,6])
print(tf.sparse.to_dense(sparse_slice_A))
print(tf.sparse.to_dense(sparse_slice_B))
print(tf.sparse.to_dense(sparse_slice_C))
tf.Tensor( [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 1] [0 0 0 1 1] [0 0 0 1 1] [0 0 0 0 1] [0 0 0 0 0] [0 0 0 0 0]], shape=(8, 5), dtype=int32) tf.Tensor( [[0] [0] [1] [1] [1] [1] [0] [0]], shape=(8, 1), dtype=int32) tf.Tensor([], shape=(8, 0), dtype=int32)
Если вы используете TensorFlow 2.4 или выше, используйте tf.sparse.map_values
для поэлементных операций с ненулевыми значениями в разреженных тензорах.
st2_plus_5 = tf.sparse.map_values(tf.add, st2, 5)
print(tf.sparse.to_dense(st2_plus_5))
tf.Tensor( [[ 6 0 0 13] [ 0 0 0 0] [ 0 0 8 0]], shape=(3, 4), dtype=int32)
Обратите внимание, что были изменены только ненулевые значения — нулевые значения остаются нулевыми.
Точно так же вы можете следовать приведенному ниже шаблону проектирования для более ранних версий TensorFlow:
st2_plus_5 = tf.SparseTensor(
st2.indices,
st2.values + 5,
st2.dense_shape)
print(tf.sparse.to_dense(st2_plus_5))
tf.Tensor( [[ 6 0 0 13] [ 0 0 0 0] [ 0 0 8 0]], shape=(3, 4), dtype=int32)
Использование tf.SparseTensor
с другими API TensorFlow
Разреженные тензоры прозрачно работают с этими API TensorFlow:
-
tf.keras
-
tf.data
-
tf.Train.Example
protobuf -
tf.function
-
tf.while_loop
-
tf.cond
-
tf.identity
-
tf.cast
-
tf.print
-
tf.saved_model
-
tf.io.serialize_sparse
-
tf.io.serialize_many_sparse
-
tf.io.deserialize_many_sparse
-
tf.math.abs
-
tf.math.negative
-
tf.math.sign
-
tf.math.square
-
tf.math.sqrt
-
tf.math.erf
-
tf.math.tanh
-
tf.math.bessel_i0e
-
tf.math.bessel_i1e
Примеры показаны ниже для нескольких из вышеперечисленных API.
tf.keras
Подмножество API tf.keras
поддерживает разреженные тензоры без дорогостоящих операций приведения или преобразования. Keras API позволяет передавать разреженные тензоры в качестве входных данных для модели Keras. Установите sparse=True
при вызове tf.keras.Input
или tf.keras.layers.InputLayer
. Вы можете передавать разреженные тензоры между слоями Keras, а также заставлять модели Keras возвращать их в качестве выходных данных. Если вы используете разреженные тензоры в слоях tf.keras.layers.Dense
в своей модели, они будут выводить плотные тензоры.
В приведенном ниже примере показано, как передать разреженный тензор в качестве входных данных для модели Keras, если вы используете только слои, поддерживающие разреженные входные данные.
x = tf.keras.Input(shape=(4,), sparse=True)
y = tf.keras.layers.Dense(4)(x)
model = tf.keras.Model(x, y)
sparse_data = tf.SparseTensor(
indices = [(0,0),(0,1),(0,2),
(4,3),(5,0),(5,1)],
values = [1,1,1,1,1,1],
dense_shape = (6,4)
)
model(sparse_data)
model.predict(sparse_data)
array([[-1.3111044 , -1.7598825 , 0.07225233, -0.44544357], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. ], [ 0.8517609 , -0.16835624, 0.7307872 , -0.14531797], [-0.8916302 , -0.9417639 , 0.24563438, -0.9029659 ]], dtype=float32)
tf.data
API tf.data
позволяет создавать сложные конвейеры ввода из простых, повторно используемых частей. Его основная структура данных — tf.data.Dataset
, которая представляет собой последовательность элементов, в которой каждый элемент состоит из одного или нескольких компонентов.
Создание наборов данных с разреженными тензорами
Создавайте наборы данных из разреженных тензоров, используя те же методы, которые используются для их построения из tf.Tensor
или NumPy, например tf.data.Dataset.from_tensor_slices
. Эта операция сохраняет разреженность (или разреженный характер) данных.
dataset = tf.data.Dataset.from_tensor_slices(sparse_data)
for element in dataset:
print(pprint_sparse_tensor(element))
<SparseTensor shape=[4] values={ [0]: 1 [1]: 1 [2]: 1}> <SparseTensor shape=[4] values={}> <SparseTensor shape=[4] values={}> <SparseTensor shape=[4] values={}> <SparseTensor shape=[4] values={ [3]: 1}> <SparseTensor shape=[4] values={ [0]: 1 [1]: 1}>
Пакетирование и разделение наборов данных с разреженными тензорами
Вы можете пакетировать (объединять последовательные элементы в один элемент) и разделять наборы данных с разреженными тензорами, используя методы Dataset.batch
и Dataset.unbatch
соответственно.
batched_dataset = dataset.batch(2)
for element in batched_dataset:
print (pprint_sparse_tensor(element))
<SparseTensor shape=[2, 4] values={ [0, 0]: 1 [0, 1]: 1 [0, 2]: 1}> <SparseTensor shape=[2, 4] values={}> <SparseTensor shape=[2, 4] values={ [0, 3]: 1 [1, 0]: 1 [1, 1]: 1}>
unbatched_dataset = batched_dataset.unbatch()
for element in unbatched_dataset:
print (pprint_sparse_tensor(element))
<SparseTensor shape=[4] values={ [0]: 1 [1]: 1 [2]: 1}> <SparseTensor shape=[4] values={}> <SparseTensor shape=[4] values={}> <SparseTensor shape=[4] values={}> <SparseTensor shape=[4] values={ [3]: 1}> <SparseTensor shape=[4] values={ [0]: 1 [1]: 1}>
Вы также можете использовать tf.data.experimental.dense_to_sparse_batch
для пакетной обработки элементов набора данных различной формы в разреженные тензоры.
Преобразование наборов данных с разреженными тензорами
Преобразуйте и создайте разреженные тензоры в наборах данных с помощью Dataset.map
.
transform_dataset = dataset.map(lambda x: x*2)
for i in transform_dataset:
print(pprint_sparse_tensor(i))
<SparseTensor shape=[4] values={ [0]: 2 [1]: 2 [2]: 2}> <SparseTensor shape=[4] values={}> <SparseTensor shape=[4] values={}> <SparseTensor shape=[4] values={}> <SparseTensor shape=[4] values={ [3]: 2}> <SparseTensor shape=[4] values={ [0]: 2 [1]: 2}>
tf.train.Пример
tf.train.Example
— стандартная кодировка protobuf для данных TensorFlow. При использовании разреженных тензоров с tf.train.Example
вы можете:
Считайте данные переменной длины в
tf.SparseTensor
с помощьюtf.io.VarLenFeature
. Однако вместо этого вам следует рассмотреть возможность использованияtf.io.RaggedFeature
.Считайте произвольные разреженные данные в
tf.SparseTensor
с помощьюtf.io.SparseFeature
, который использует три отдельных функциональных ключа для храненияindices
,values
иdense_shape
.
tf.function
Декоратор tf.function
предварительно вычисляет графы TensorFlow для функций Python, что может существенно повысить производительность вашего кода TensorFlow. Разреженные тензоры прозрачно работают как с tf.function
так и с конкретными функциями .
@tf.function
def f(x,y):
return tf.sparse.sparse_dense_matmul(x,y)
a = tf.SparseTensor(indices=[[0, 3], [2, 4]],
values=[15, 25],
dense_shape=[3, 10])
b = tf.sparse.to_dense(tf.sparse.transpose(a))
c = f(a,b)
print(c)
tf.Tensor( [[225 0 0] [ 0 0 0] [ 0 0 625]], shape=(3, 3), dtype=int32)
Отличие пропущенных значений от нулевых значений
Большинство операций с tf.SparseTensor
обрабатывают отсутствующие значения и явные нулевые значения. Это задумано — предполагается, что tf.SparseTensor
действует точно так же, как плотный тензор.
Однако есть несколько случаев, когда может быть полезно отличить нулевые значения от пропущенных значений. В частности, это позволяет одним из способов кодировать отсутствующие/неизвестные данные в ваших обучающих данных. Например, рассмотрим вариант использования, в котором у вас есть тензор оценок (который может иметь любое значение с плавающей запятой от -Inf до +Inf) с некоторыми отсутствующими оценками. Вы можете закодировать этот тензор, используя разреженный тензор, где явные нули представляют собой известные нулевые оценки, но неявные нулевые значения фактически представляют отсутствующие данные, а не ноль.
Обратите внимание, что некоторые операции, такие как tf.sparse.reduce_max
, не обрабатывают отсутствующие значения, как если бы они были равны нулю. Например, когда вы запускаете приведенный ниже блок кода, ожидаемый результат равен 0
. Однако из-за этого исключения вывод равен -3
.
print(tf.sparse.reduce_max(tf.sparse.from_dense([-5, 0, -3])))
tf.Tensor(-3, shape=(), dtype=int32)
Напротив, когда вы применяете tf.math.reduce_max
к плотному тензору, выходной сигнал равен 0, как и ожидалось.
print(tf.math.reduce_max([-5, 0, -3]))
tf.Tensor(0, shape=(), dtype=int32)
Дополнительная литература и ресурсы
- Обратитесь к руководству по тензорам, чтобы узнать о тензорах.
- Прочтите руководство по рваным тензорам, чтобы узнать, как работать с рваными тензорами — типом тензора, позволяющим работать с неоднородными данными.
- Ознакомьтесь с этой моделью обнаружения объектов в TensorFlow Model Garden , которая использует разреженные тензоры в декодере данных
tf.Example
.