Praca z rzadkimi tensorami

Zobacz na TensorFlow.org Uruchom w Google Colab Zobacz na GitHub Pobierz notatnik

Podczas pracy z tensorami, które zawierają wiele wartości zerowych, ważne jest, aby przechowywać je w sposób efektywny czasowo i przestrzennie. Rzadkie tensory umożliwiają wydajne przechowywanie i przetwarzanie tensorów zawierających wiele wartości zerowych. Rzadkie tensory są szeroko stosowane w schematach kodowania, takich jak TF-IDF , jako część wstępnego przetwarzania danych w aplikacjach NLP oraz do wstępnego przetwarzania obrazów z dużą ilością ciemnych pikseli w aplikacjach widzenia komputerowego.

Rzadkie tensory w TensorFlow

TensorFlow reprezentuje rzadkie tensory za pośrednictwem obiektu tf.SparseTensor . Obecnie rzadkie tensory w TensorFlow są kodowane przy użyciu formatu listy współrzędnych (COO). Ten format kodowania jest zoptymalizowany pod kątem bardzo rzadkich macierzy, takich jak osadzania.

Kodowanie COO dla rzadkich tensorów składa się z:

  • values : tensor jednowymiarowy o kształcie [N] zawierający wszystkie wartości niezerowe.
  • indices : tensor 2D o kształcie [N, rank] zawierający indeksy o wartościach niezerowych.
  • dense_shape : tensor 1D z kształtem [rank] , określający kształt tensora.

Wartość niezerowa w kontekście tf.SparseTensor to wartość, która nie jest jawnie zakodowana. Możliwe jest jawne uwzględnienie wartości zerowych w values rzadkiej macierzy COO, ale te „wyraźne zera” na ogół nie są uwzględniane w odniesieniu do wartości niezerowych w tensorze rzadkim.

Tworzenie tf.SparseTensor

Konstruuj rzadkie tensory, określając bezpośrednio ich values , indices i dense_shape .

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

Kiedy używasz funkcji print() do drukowania rzadkiego tensora, pokazuje zawartość trzech tensorów składowych:

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

Łatwiej jest zrozumieć zawartość rzadkiego tensora, jeśli values niezerowe są wyrównane z odpowiadającymi im indices . Zdefiniuj funkcję pomocniczą do ładnego drukowania rzadkich tensorów, tak aby każda wartość niezerowa była pokazywana w osobnym wierszu.

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

Można również skonstruować sparse tensory z gęstych tensorów za pomocą tf.sparse.from_dense i przekonwertować je z powrotem na gęste tensory za pomocą 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)

Manipulowanie rzadkimi tensorami

Użyj narzędzi w pakiecie tf.sparse , aby manipulować rzadkimi tensorami. Operacje takie jak tf.math.add , których można użyć do arytmetycznej manipulacji gęstymi tensorami, nie działają z rzadkimi tensorami.

Dodaj rzadkie tensory o tym samym kształcie, używając 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}>

Użyj tf.sparse.sparse_dense_matmul , aby pomnożyć rzadkie tensory z gęstymi macierzami.

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)

Połącz rzadkie tensory za pomocą tf.sparse.concat i rozłóż je za pomocą 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)

Jeśli używasz TensorFlow 2.4 lub nowszego, użyj tf.sparse.map_values dla operacji elementwise na wartościach niezerowych w rozrzedzonych tensorach.

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)

Zauważ, że tylko wartości niezerowe zostały zmodyfikowane – wartości zerowe pozostają zerowe.

Równoważnie możesz postępować zgodnie z poniższym wzorcem projektowym dla wcześniejszych wersji 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)

Korzystanie z tf.SparseTensor z innymi interfejsami API TensorFlow

Rzadkie tensory działają w przejrzysty sposób z następującymi interfejsami API TensorFlow:

Poniżej przedstawiono przykłady kilku z powyższych interfejsów API.

tf.keras

Podzbiór tf.keras API obsługuje rzadkie tensory bez kosztownych operacji rzutowania lub konwersji. Keras API umożliwia przekazywanie rzadkich tensorów jako danych wejściowych do modelu Keras. Ustaw sparse=True podczas wywoływania tf.keras.Input lub tf.keras.layers.InputLayer . Możesz przekazywać rzadkie tensory między warstwami Keras, a modele Keras zwracają je jako dane wyjściowe. Jeśli użyjesz sparse tensorów w warstwach tf.keras.layers.Dense w swoim modelu, wygenerują one gęste tensory.

Poniższy przykład pokazuje, jak przekazać rozrzedzony tensor jako dane wejściowe do modelu Keras, jeśli używasz tylko warstw obsługujących rzadkie dane wejściowe.

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

Interfejs API tf.data umożliwia tworzenie złożonych potoków wejściowych z prostych elementów wielokrotnego użytku. Jego podstawową strukturą danych jest tf.data.Dataset , która reprezentuje sekwencję elementów, w której każdy element składa się z jednego lub więcej składników.

Budowanie zbiorów danych z rzadkimi tensorami

Twórz zestawy danych z rzadkich tensorów przy użyciu tych samych metod, które są używane do tworzenia ich z tf.Tensor s lub NumPy, takich jak tf.data.Dataset.from_tensor_slices . Ta operacja zachowuje rzadkość (lub rzadką naturę) danych.

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

Grupowanie i rozdzielanie zbiorów danych z rzadkimi tensorami

Możesz wsadowo (łączyć kolejne elementy w jeden element) i rozdzielić zestawy danych z rzadkimi tensorami, używając odpowiednio metod Dataset.batch i 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}>

Możesz również użyć tf.data.experimental.dense_to_sparse_batch do partii elementów zestawu danych o różnych kształtach w rzadkich tensorach.

Przekształcanie zbiorów danych za pomocą rzadkich tensorów

Przekształć i utwórz rzadkie tensory w zestawach danych za pomocą 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.pociąg.Przykład

tf.train.Example to standardowe kodowanie protobuf dla danych TensorFlow. Używając sparse tensorów z tf.train.Example , możesz:

tf.function

Dekorator tf.function wstępnie oblicza wykresy TensorFlow dla funkcji Pythona, co może znacznie poprawić wydajność Twojego kodu TensorFlow. Rzadkie tensory działają przejrzyście zarówno z tf.function , jak i konkretnymi .

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

Rozróżnianie braków danych od wartości zerowych

Większość operacji na tf.SparseTensor s traktuje brakujące wartości i jawne wartości zerowe identycznie. Jest to zgodne z projektem — tf.SparseTensor ma działać jak gęsty tensor.

Istnieje jednak kilka przypadków, w których przydatne może być odróżnienie wartości zerowych od braków danych. W szczególności pozwala to na jeden sposób kodowania brakujących/nieznanych danych w danych treningowych. Rozważmy na przykład przypadek użycia, w którym masz tensor wyników (który może mieć dowolną wartość zmiennoprzecinkową od -Inf do +Inf) z pewnymi brakującymi wynikami. Możesz zakodować ten tensor za pomocą rzadkiego tensora, w którym jawne zera są znanymi punktami zerowymi, ale niejawne wartości zerowe w rzeczywistości reprezentują brakujące dane, a nie zero.

Zauważ, że niektóre operacje, takie jak tf.sparse.reduce_max , nie traktują brakujących wartości tak, jakby były zerowe. Na przykład po uruchomieniu poniższego bloku kodu oczekiwane dane wyjściowe to 0 . Jednak z powodu tego wyjątku wynik to -3 .

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

W przeciwieństwie do tego, gdy zastosujesz tf.math.reduce_max do gęstego tensora, wynik będzie wynosił 0, zgodnie z oczekiwaniami.

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

Dalsze czytanie i zasoby