Trabajar con tensores escasos

Ver en TensorFlow.org Ejecutar en Google Colab Ver en GitHub Descargar libreta

Cuando se trabaja con tensores que contienen muchos valores cero, es importante almacenarlos de manera eficiente en cuanto a espacio y tiempo. Los tensores dispersos permiten el almacenamiento y procesamiento eficientes de tensores que contienen muchos valores cero. Los tensores dispersos se utilizan ampliamente en esquemas de codificación como TF-IDF como parte del procesamiento previo de datos en aplicaciones NLP y para el procesamiento previo de imágenes con una gran cantidad de píxeles oscuros en aplicaciones de visión artificial.

Tensores dispersos en TensorFlow

TensorFlow representa tensores dispersos a través del objeto tf.SparseTensor . Actualmente, los tensores dispersos en TensorFlow se codifican con el formato de lista de coordenadas (COO). Este formato de codificación está optimizado para matrices hiperdispersas como incrustaciones.

La codificación COO para tensores dispersos se compone de:

  • values : un tensor 1D con forma [N] que contiene todos los valores distintos de cero.
  • indices : un tensor 2D con forma [N, rank] , que contiene los índices de los valores distintos de cero.
  • dense_shape : un tensor 1D con forma [rank] , que especifica la forma del tensor.

Un valor distinto de cero en el contexto de un tf.SparseTensor es un valor que no está codificado explícitamente. Es posible incluir explícitamente valores cero en los values de una matriz dispersa de COO, pero estos "ceros explícitos" generalmente no se incluyen cuando se hace referencia a valores distintos de cero en un tensor disperso.

Creando un tf.SparseTensor

Construya tensores dispersos especificando directamente sus values , indices y dense_shape .

import tensorflow as tf
st1 = tf.SparseTensor(indices=[[0, 3], [2, 4]],
                      values=[10, 20],
                      dense_shape=[3, 10])

Cuando usa la función print() para imprimir un tensor disperso, muestra el contenido de los tres tensores componentes:

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))

Es más fácil comprender el contenido de un tensor disperso si los values distintos de cero están alineados con sus indices correspondientes. Defina una función auxiliar para imprimir tensores dispersos de manera que cada valor distinto de cero se muestre en su propia línea.

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}>

También puede construir tensores dispersos a partir de tensores densos usando tf.sparse.from_dense y volver a convertirlos en tensores densos usando 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)

Manipulación de tensores dispersos

Utilice las utilidades del paquete tf.sparse para manipular tensores dispersos. Operaciones como tf.math.add que puede usar para la manipulación aritmética de tensores densos no funcionan con tensores dispersos.

Agregue tensores dispersos de la misma forma usando 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}>

Use tf.sparse.sparse_dense_matmul para multiplicar tensores dispersos con matrices densas.

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)

Junte los tensores dispersos usando tf.sparse.concat y sepárelos usando 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)

Si usa TensorFlow 2.4 o superior, use tf.sparse.map_values para operaciones elementales en valores distintos de cero en tensores dispersos.

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)

Tenga en cuenta que solo se modificaron los valores distintos de cero: los valores cero permanecen en cero.

De manera equivalente, puede seguir el patrón de diseño a continuación para versiones anteriores de 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)

Usar tf.SparseTensor con otras API de TensorFlow

Los tensores dispersos funcionan de manera transparente con estas API de TensorFlow:

A continuación se muestran ejemplos de algunas de las API anteriores.

tf.keras

Un subconjunto de la API tf.keras admite tensores dispersos sin costosas operaciones de conversión o conversión. La API de Keras le permite pasar tensores dispersos como entradas a un modelo de Keras. Establezca sparse=True cuando llame a tf.keras.Input o tf.keras.layers.InputLayer . Puede pasar tensores dispersos entre capas de Keras y también hacer que los modelos de Keras los devuelvan como salidas. Si usa tensores dispersos en tf.keras.layers.Dense capas en su modelo, generarán tensores densos.

El siguiente ejemplo muestra cómo pasar un tensor disperso como entrada a un modelo de Keras si usa solo capas que admiten entradas dispersas.

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

La API tf.data le permite crear canalizaciones de entrada complejas a partir de piezas simples y reutilizables. Su estructura de datos central es tf.data.Dataset , que representa una secuencia de elementos en la que cada elemento consta de uno o más componentes.

Creación de conjuntos de datos con tensores dispersos

Cree conjuntos de datos a partir de tensores dispersos utilizando los mismos métodos que se utilizan para crearlos a partir de tf.Tensor s o NumPy, como tf.data.Dataset.from_tensor_slices . Esta operación preserva la escasez (o la naturaleza escasa) de los datos.

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}>

Conjuntos de datos por lotes y sin lotes con tensores dispersos

Puede agrupar (combinar elementos consecutivos en un solo elemento) y desagrupar conjuntos de datos con tensores dispersos utilizando los métodos Dataset.batch y Dataset.unbatch respectivamente.

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}>

También puede usar tf.data.experimental.dense_to_sparse_batch para agrupar elementos de conjuntos de datos de diferentes formas en tensores dispersos.

Transformar conjuntos de datos con tensores dispersos

Transforme y cree tensores dispersos en conjuntos de datos usando 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.tren.Ejemplo

tf.train.Example es una codificación protobuf estándar para datos de TensorFlow. Al usar tensores dispersos con tf.train.Example , puede:

tf.function

El decorador tf.function los gráficos de TensorFlow para las funciones de Python, lo que puede mejorar sustancialmente el rendimiento de su código de TensorFlow. Los tensores dispersos funcionan de forma transparente tanto con tf.function como con funciones concretas .

@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)

Distinguir los valores perdidos de los valores cero

La mayoría de las operaciones en tf.SparseTensor s tratan los valores faltantes y los valores cero explícitos de manera idéntica. Esto es por diseño: se supone que un tf.SparseTensor actúa como un tensor denso.

Sin embargo, hay algunos casos en los que puede ser útil distinguir los valores cero de los valores perdidos. En particular, esto permite una forma de codificar datos faltantes/desconocidos en tus datos de entrenamiento. Por ejemplo, considere un caso de uso en el que tiene un tensor de puntajes (que puede tener cualquier valor de coma flotante desde -Inf hasta +Inf), con algunos puntajes faltantes. Puede codificar este tensor utilizando un tensor disperso donde los ceros explícitos son puntuaciones cero conocidas, pero los valores cero implícitos en realidad representan datos faltantes y no cero.

Tenga en cuenta que algunas operaciones como tf.sparse.reduce_max no tratan los valores perdidos como si fueran cero. Por ejemplo, cuando ejecuta el bloque de código a continuación, el resultado esperado es 0 . Sin embargo, debido a esta excepción, la salida es -3 .

print(tf.sparse.reduce_max(tf.sparse.from_dense([-5, 0, -3])))
tf.Tensor(-3, shape=(), dtype=int32)

Por el contrario, cuando aplica tf.math.reduce_max a un tensor denso, la salida es 0 como se esperaba.

print(tf.math.reduce_max([-5, 0, -3]))
tf.Tensor(0, shape=(), dtype=int32)

Más lecturas y recursos