O Dia da Comunidade de ML é dia 9 de novembro! Junte-nos para atualização de TensorFlow, JAX, e mais Saiba mais

Trabalhando com tensores esparsos

Ver 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 o armazenamento e processamento eficientes de tensores que contêm muitos valores zero. Tensores esparsos são usados extensivamente em esquemas de codificação como TF-IDF como parte de pré-processamento de dados em aplicações de PNL e para imagens de pré-processamento com um monte de pixels escuros em aplicações de visão computacional.

Tensores esparsos no TensorFlow

TensorFlow representa tensores esparso através da tf.SparseTensor objecto. Atualmente, tensores esparsos no TensorFlow são codificados usando o formato de lista de coordenadas (COO). Este formato de codificação é otimizado para matrizes hiperesparsas, como embeddings.

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

  • values : A 1D tensor com forma [N] contendo todos os valores diferentes de zero.
  • indices : Um tensor de 2D com a forma [N, rank] , contendo os índices dos valores diferentes de zero.
  • dense_shape : Um 1D tensor 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 os valores zero nos values de matriz de um COO escassos, mas estes "zeros explícitas" geralmente não são incluídos quando se refere a valores diferentes de zero em um tensor escassa.

Criando um tf.SparseTensor

Construir tensores esparsas especificando diretamente sua 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 o print() função para imprimir um tensor escassa, ele 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 de entender o conteúdo de um tensor escassa se os diferentes de zero values estão alinhados com seus correspondentes indices . Defina uma função auxiliar para imprimir tensores esparsos de forma 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 esparsas de tensores densas usando tf.sparse.from_dense , e convertê-los de volta para tensores densas 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

Utilizar os utilitários no tf.sparse pacote para manipular tensores esparsas. Ops como tf.math.add que você pode usar para manipulação aritmética dos tensores densas não funcionam com tensores esparsas.

Adicionar 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 tensores multiplicam 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)

Coloque tensores esparsas juntos usando tf.sparse.concat e desmontá-los 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 TensorFlow 2.4 ou acima, use tf.sparse.map_values para operações elemento a elemento em valores diferentes de zero em tensores esparsas.

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.

Da mesma forma, 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)

Usando tf.SparseTensor com outras APIs 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 de tf.keras API suporta esparsas tensores sem caros de fundição ou de conversão ops. A API Keras permite que você passe tensores esparsos como entradas para um modelo Keras. Set sparse=True ao chamar tf.keras.Input ou tf.keras.layers.InputLayer . Você pode passar tensores esparsos entre as camadas Keras e também fazer com que os modelos Keras os retornem como saídas. Se você usar tensores esparsas no tf.keras.layers.Dense camadas no seu modelo, eles vão tensores densas de saída.

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

O tf.data API permite construir dutos de entrada complexos de peças simples, reutilizáveis. A sua estrutura de dados de núcleo é tf.data.Dataset , que representa uma sequência de elementos em que cada elemento é constituído de um ou mais componentes.

Construindo conjuntos de dados com tensores esparsos

Construir a partir de conjuntos de dados tensores esparsos, utilizando os mesmos métodos que são usados para construir os de tf.Tensor matrizes s ou Numpy, tais como tf.data.Dataset.from_tensor_slices . Esta operação preserva a dispersão (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}>

Batching e unbatching datasets com tensores esparsos

Pode lote (combinar elementos consecutivos em um único elemento) e conjuntos de dados unbatch com tensores esparsos usando o Dataset.batch e Dataset.unbatch métodos 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 a elementos do conjunto de dados de lote de formas variadas em tensores esparsas.

Transformando conjuntos de dados com tensores esparsos

Transformar e criar tensores esparsos em conjuntos de dados, utilizando 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.Example

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

tf.function

Os tf.function precomputes decorador TensorFlow gráficos para funções Python, que pode melhorar substancialmente o desempenho do seu código TensorFlow. Tensores esparsas trabalhar de forma transparente tanto com tf.function e funções 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 valores ausentes de valores zero

A maioria dos ops no tf.SparseTensor valores em falta tratar s e valores explícitos de zero de forma idêntica. Isso ocorre por design - um tf.SparseTensor é suposto para agir apenas como um tensor densa.

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 onde 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.

Note-se que alguns ops como tf.sparse.reduce_max não tratar valores ausentes como se fossem zero. Por exemplo, quando você executar o bloco de código abaixo, o resultado esperado é 0 . No entanto, devido a esta excepçã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 densa, a saída é 0 como esperado.

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

Leitura adicional e recursos