Trabalhando com tensores esparsos

Veja no TensorFlow.org Executar no Google Colab Ver no GitHub Baixar caderno

Ao trabalhar com tensores que contêm muitos valores zero, é importante armazená-los de maneira eficiente em termos de espaço e tempo. Os tensores esparsos permitem armazenamento e processamento eficientes de tensores que contêm muitos valores zero. Os tensores esparsos são usados ​​extensivamente em esquemas de codificação como TF-IDF como parte do pré-processamento de dados em aplicativos NLP e para pré-processamento de imagens com muitos pixels escuros em aplicativos de visão computacional.

Tensores esparsos no TensorFlow

TensorFlow representa tensores esparsos por meio do objeto tf.SparseTensor . Atualmente, tensores esparsos no TensorFlow são codificados usando o formato de lista de coordenadas (COO). Esse formato de codificação é otimizado para matrizes hiper-esparsas, como embeddings.

A codificação COO para tensores esparsos é composta por:

  • values : Um tensor 1D com forma [N] contendo todos os valores diferentes de zero.
  • indices : Um tensor 2D com forma [N, rank] , contendo os índices dos valores diferentes de zero.
  • dense_shape : Um tensor 1D com forma [rank] , especificando a forma do tensor.

Um valor diferente de zero no contexto de um tf.SparseTensor é um valor que não é codificado explicitamente. É possível incluir explicitamente valores zero nos values de uma matriz esparsa COO, mas esses "zeros explícitos" geralmente não são incluídos ao se referir a valores diferentes de zero em um tensor esparso.

Criando um tf.SparseTensor

Construa tensores esparsos especificando diretamente seus values , indices e dense_shape .

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

Quando você usa a função print() para imprimir um tensor esparso, ela mostra o conteúdo dos três tensores de 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))

É mais fácil entender o conteúdo de um tensor esparso se os values diferentes de zero estiverem alinhados com seus indices correspondentes. Defina uma função auxiliar para imprimir tensores esparsos de modo que cada valor diferente de zero seja mostrado em sua própria linha.

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

Você também pode construir tensores esparsos a partir de tensores densos usando tf.sparse.from_dense e convertê-los novamente em 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)

Manipulando tensores esparsos

Use os utilitários no pacote tf.sparse para manipular tensores esparsos. Ops como tf.math.add que você pode usar para manipulação aritmética de tensores densos não funcionam com tensores esparsos.

Adicione tensores esparsos da mesma 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 esparsos com matrizes 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 tensores esparsos usando tf.sparse.concat e separe-os 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)

Se você estiver usando o TensorFlow 2.4 ou superior, use tf.sparse.map_values para operações elementares em valores diferentes de zero em tensores esparsos.

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)

Observe que apenas os valores diferentes de zero foram modificados – os valores zero permanecem zero.

De forma equivalente, você pode seguir o padrão de design abaixo para versões anteriores do 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)

Como usar tf.SparseTensor com outras APIs do TensorFlow

Os tensores esparsos funcionam de forma transparente com estas APIs do TensorFlow:

Exemplos são mostrados abaixo para algumas das APIs acima.

tf.keras

Um subconjunto da API tf.keras oferece suporte a tensores esparsos sem operações caras de conversão ou conversão. A API Keras permite passar tensores esparsos como entradas para um modelo Keras. Defina sparse=True ao chamar tf.keras.Input ou tf.keras.layers.InputLayer . Você pode passar tensores esparsos entre camadas Keras e também fazer com que os modelos Keras os retornem como saídas. Se você usar tensores esparsos em camadas tf.keras.layers.Dense em seu modelo, eles produzirão tensores densos.

O exemplo abaixo mostra como passar um tensor esparso como uma entrada para um modelo Keras se você usar apenas camadas que suportam entradas esparsas.

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

A API tf.data permite que você crie pipelines de entrada complexos a partir de peças simples e reutilizáveis. Sua estrutura de dados principal é tf.data.Dataset , que representa uma sequência de elementos em que cada elemento consiste em um ou mais componentes.

Como criar conjuntos de dados com tensores esparsos

Crie conjuntos de dados a partir de tensores esparsos usando os mesmos métodos usados ​​para construí-los a partir de tf.Tensor s ou NumPy, como tf.data.Dataset.from_tensor_slices . Essa operação preserva a escassez (ou natureza esparsa) dos dados.

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 dados em lote e desagrupamento com tensores esparsos

Você pode agrupar (combinar elementos consecutivos em um único elemento) e desagrupar conjuntos de dados com tensores esparsos usando os métodos Dataset.batch e 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}>

Você também pode usar tf.data.experimental.dense_to_sparse_batch para agrupar elementos do conjunto de dados de formas variadas em tensores esparsos.

Transformando conjuntos de dados com tensores esparsos

Transforme e crie tensores esparsos em Datasets 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.train.Exemplo

tf.train.Example é uma codificação protobuf padrão para dados do TensorFlow. Ao usar tensores esparsos com tf.train.Example , você pode:

tf.function

O decorador tf.function pré-computa gráficos do TensorFlow para funções Python, o que pode melhorar substancialmente o desempenho do seu código do TensorFlow. Os tensores esparsos funcionam de forma transparente com funções tf.function e 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)

Distinguindo valores ausentes de valores zero

A maioria das operações em tf.SparseTensor s tratam valores ausentes e valores zero explícitos de forma idêntica. Isso ocorre por design - um tf.SparseTensor deve agir exatamente como um tensor denso.

No entanto, existem alguns casos em que pode ser útil distinguir valores zero de valores ausentes. Em particular, isso permite uma maneira de codificar dados ausentes/desconhecidos em seus dados de treinamento. Por exemplo, considere um caso de uso em que você tem um tensor de pontuações (que pode ter qualquer valor de ponto flutuante de -Inf a +Inf), com algumas pontuações ausentes. Você pode codificar esse tensor usando um tensor esparso onde os zeros explícitos são pontuações zero conhecidas, mas os valores zero implícitos na verdade representam dados ausentes e não zero.

Observe que algumas operações como tf.sparse.reduce_max não tratam valores ausentes como se fossem zero. Por exemplo, quando você executa o bloco de código abaixo, a saída esperada é 0 . No entanto, devido a essa exceção, a saída é -3 .

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

Em contraste, quando você aplica tf.math.reduce_max a um tensor denso, a saída é 0 conforme o esperado.

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

Leitura adicional e recursos