Travailler avec des tenseurs clairsemés

Voir sur TensorFlow.org Exécuter dans Google Colab Afficher sur GitHub Télécharger le cahier

Lorsque vous travaillez avec des tenseurs contenant de nombreuses valeurs nulles, il est important de les stocker de manière efficace en termes d'espace et de temps. Les tenseurs clairsemés permettent un stockage et un traitement efficaces des tenseurs qui contiennent beaucoup de valeurs nulles. Les tenseurs clairsemés sont largement utilisés dans les schémas de codage tels que TF-IDF dans le cadre du prétraitement des données dans les applications NLP et pour le prétraitement des images avec beaucoup de pixels sombres dans les applications de vision par ordinateur.

Tenseurs clairsemés dans TensorFlow

TensorFlow représente des tenseurs clairsemés via l'objet tf.SparseTensor . Actuellement, les tenseurs clairsemés dans TensorFlow sont encodés à l'aide du format de liste de coordonnées (COO). Ce format d'encodage est optimisé pour les matrices hyper clairsemées telles que les représentations vectorielles continues.

Le codage COO pour les tenseurs creux est composé de :

  • values : Un tenseur 1D de forme [N] contenant toutes les valeurs non nulles.
  • indices : Un tenseur 2D de forme [N, rank] , contenant les indices des valeurs non nulles.
  • dense_shape : Un tenseur 1D avec shape [rank] , spécifiant la forme du tenseur.

Une valeur différente de zéro dans le contexte d'un tf.SparseTensor est une valeur qui n'est pas explicitement encodée. Il est possible d'inclure explicitement des valeurs nulles dans les values d'une matrice creuse COO, mais ces "zéros explicites" ne sont généralement pas inclus lorsqu'ils se réfèrent à des valeurs non nulles dans un tenseur clairsemé.

Création d'un tf.SparseTensor

Construisez des tenseurs creux en spécifiant directement leurs values , indices et dense_shape .

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

Lorsque vous utilisez la fonction print() pour imprimer un tenseur creux, elle affiche le contenu des trois composants tenseurs :

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

Il est plus facile de comprendre le contenu d'un tenseur creux si les values non nulles sont alignées avec leurs indices correspondants. Définissez une fonction d'assistance pour imprimer joliment des tenseurs clairsemés de sorte que chaque valeur différente de zéro soit affichée sur sa propre ligne.

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

Vous pouvez également construire des tenseurs clairsemés à partir de tenseurs denses en utilisant tf.sparse.from_dense et les reconvertir en tenseurs denses en utilisant 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)

Manipulation de tenseurs creux

Utilisez les utilitaires du package tf.sparse pour manipuler les tenseurs creux. Les opérations comme tf.math.add que vous pouvez utiliser pour la manipulation arithmétique des tenseurs denses ne fonctionnent pas avec les tenseurs clairsemés.

Ajoutez des tenseurs clairsemés de la même forme en utilisant 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}>

Utilisez tf.sparse.sparse_dense_matmul pour multiplier les tenseurs clairsemés avec des matrices denses.

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)

Assemblez des tenseurs clairsemés en utilisant tf.sparse.concat et séparez-les en utilisant 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 vous utilisez TensorFlow 2.4 ou une version ultérieure, utilisez tf.sparse.map_values pour les opérations élément par élément sur des valeurs non nulles dans des tenseurs clairsemés.

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)

Notez que seules les valeurs non nulles ont été modifiées – les valeurs nulles restent nulles.

De manière équivalente, vous pouvez suivre le modèle de conception ci-dessous pour les versions antérieures 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)

Utilisation de tf.SparseTensor avec d'autres API TensorFlow

Les Tensors clairsemés fonctionnent de manière transparente avec ces API TensorFlow :

Des exemples sont présentés ci-dessous pour quelques-unes des API ci-dessus.

tf.keras

Un sous-ensemble de l'API tf.keras prend en charge les tenseurs clairsemés sans opérations de diffusion ou de conversion coûteuses. L'API Keras vous permet de transmettre des tenseurs clairsemés en tant qu'entrées à un modèle Keras. Définissez sparse=True lors de l'appel tf.keras.Input ou tf.keras.layers.InputLayer . Vous pouvez passer des tenseurs clairsemés entre les couches Keras, et également faire en sorte que les modèles Keras les renvoient en tant que sorties. Si vous utilisez des tenseurs clairsemés dans les couches tf.keras.layers.Dense de votre modèle, ils produiront des tenseurs denses.

L'exemple ci-dessous vous montre comment transmettre un tenseur clairsemé comme entrée à un modèle Keras si vous utilisez uniquement des couches qui prennent en charge les entrées clairsemées.

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

L'API tf.data vous permet de créer des pipelines d'entrée complexes à partir de pièces simples et réutilisables. Sa structure de données de base est tf.data.Dataset , qui représente une séquence d'éléments dans laquelle chaque élément est constitué d'un ou plusieurs composants.

Construire des ensembles de données avec des tenseurs clairsemés

Créez des ensembles de données à partir de tenseurs clairsemés en utilisant les mêmes méthodes que celles utilisées pour les créer à partir de tf.Tensor s ou NumPy, tels que tf.data.Dataset.from_tensor_slices . Cette opération préserve la parcimonie (ou la nature parcimonieuse) des données.

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

Regrouper et dégrouper des ensembles de données avec des tenseurs clairsemés

Vous pouvez regrouper (combiner des éléments consécutifs en un seul élément) et dégrouper des ensembles de données avec des tenseurs clairsemés à l'aide des méthodes Dataset.batch et Dataset.unbatch respectivement.

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

Vous pouvez également utiliser tf.data.experimental.dense_to_sparse_batch pour grouper des éléments d'ensemble de données de différentes formes dans des tenseurs clairsemés.

Transformer des ensembles de données avec des tenseurs clairsemés

Transformez et créez des tenseurs clairsemés dans des ensembles de données à l'aide 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.Exemple

tf.train.Example est un encodage protobuf standard pour les données TensorFlow. Lorsque vous utilisez des tenseurs creux avec tf.train.Example , vous pouvez :

tf.function

Le décorateur tf.function précalcule les graphiques TensorFlow pour les fonctions Python, ce qui peut considérablement améliorer les performances de votre code TensorFlow. Les tenseurs creux fonctionnent de manière transparente avec tf.function et les fonctions concrètes .

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

Distinguer les valeurs manquantes des valeurs nulles

La plupart des opérations sur tf.SparseTensor s traitent les valeurs manquantes et les valeurs zéro explicites de la même manière. C'est par conception - un tf.SparseTensor est censé agir comme un tenseur dense.

Cependant, il existe quelques cas où il peut être utile de distinguer les valeurs nulles des valeurs manquantes. En particulier, cela permet d'encoder les données manquantes/inconnues dans vos données d'entraînement. Par exemple, considérez un cas d'utilisation où vous avez un tenseur de scores (qui peut avoir n'importe quelle valeur à virgule flottante de -Inf à +Inf), avec quelques scores manquants. Vous pouvez encoder ce tenseur à l'aide d'un tenseur clairsemé où les zéros explicites sont des scores nuls connus, mais les valeurs nulles implicites représentent en fait des données manquantes et non nulles.

Notez que certaines opérations comme tf.sparse.reduce_max ne traitent pas les valeurs manquantes comme si elles étaient nulles. Par exemple, lorsque vous exécutez le bloc de code ci-dessous, la sortie attendue est 0 . Cependant, à cause de cette exception, la sortie est -3 .

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

En revanche, lorsque vous appliquez tf.math.reduce_max à un tenseur dense, la sortie est 0 comme prévu.

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

Lectures complémentaires et ressources