עבודה עם טנסורים דלילים

הצג באתר TensorFlow.org הפעל בגוגל קולאב הצג ב-GitHub הורד מחברת

כשעובדים עם טנזורים המכילים הרבה ערכים אפס, חשוב לאחסן אותם בצורה יעילה במרחב ובזמן. טנזורים דלילים מאפשרים אחסון ועיבוד יעיל של טנזורים המכילים הרבה ערכים אפסיים. טנזורים דלילים נמצאים בשימוש נרחב בסכימות קידוד כמו TF-IDF כחלק מעיבוד מוקדם של נתונים ביישומי NLP ועבור עיבוד מקדים של תמונות עם הרבה פיקסלים כהים ביישומי ראייה ממוחשבת.

טנסורים דלילים ב- TensorFlow

TensorFlow מייצג טנזורים דלילים דרך האובייקט tf.SparseTensor . נכון לעכשיו, טנזורים דלילים ב- TensorFlow מקודדים באמצעות פורמט רשימת הקואורדינטות (COO). פורמט קידוד זה מותאם למטריצות דלילות במיוחד כגון הטבעות.

קידוד ה-COO עבור טנזורים דלילים מורכב מ:

  • values : טנזור 1D עם צורה [N] המכיל את כל הערכים שאינם אפס.
  • indices : טנזור דו-ממדי עם צורה [N, rank] , המכיל את המדדים של הערכים שאינם אפס.
  • dense_shape : טנסור 1D עם צורה [rank] , המציין את צורת הטנזור.

ערך שאינו אפס בהקשר של tf.SparseTensor הוא ערך שאינו מקודד במפורש. ניתן לכלול במפורש ערכי אפס values של מטריצת COO דלילה, אך ה"אפסים המפורשים" הללו בדרך כלל אינם נכללים כאשר מתייחסים לערכים שאינם מאפסים בטנזור דליל.

יצירת tf.SparseTensor

בנו טנסורים דלילים על ידי ציון ישיר של values , indices dense_shape .

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

כאשר אתה משתמש בפונקציה print() כדי להדפיס טנזור דליל, הוא מציג את התוכן של שלושת הטנסורים המרכיבים:

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

קל יותר להבין את התוכן של טנזור דליל אם values שאינם אפס מיושרים עם indices המתאימים להם. הגדר פונקציית עוזר להדפסת טנסורים דלילים בצורה יפה כך שכל ערך שאינו אפס יוצג בקו משלו.

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

אתה יכול גם לבנות טנסורים דלילים מטנסורים צפופים על ידי שימוש ב- tf.sparse.from_dense , ולהמיר אותם בחזרה לטנסורים צפופים על ידי שימוש ב- 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)

מניפולציה של טנזורים דלילים

השתמש בכלי השירות בחבילת tf.sparse כדי לתפעל טנסור דל. אופציות כמו tf.math.add שאתה יכול להשתמש בהן למניפולציה אריתמטית של טנסורים צפופים לא עובדות עם טנסורים דלילים.

הוסף טנסור דל מאותה צורה באמצעות 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}>

השתמש tf.sparse.sparse_dense_matmul כדי להכפיל טנזורים דלילים עם מטריצות צפופות.

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)

חבר טנסור דל יחד באמצעות tf.sparse.concat אותם באמצעות 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)

אם אתה משתמש ב-TensorFlow 2.4 ומעלה, השתמש ב- tf.sparse.map_values עבור פעולות אלמנטריות על ערכים שאינם מאפס בטנסורים דלילים.

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)

שים לב שרק הערכים שאינם אפס שונו - ערכי האפס נשארים אפס.

באופן שווה, אתה יכול לעקוב אחר דפוס העיצוב שלהלן עבור גרסאות קודמות של 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)

שימוש ב- tf.SparseTensor עם ממשקי API אחרים של TensorFlow

טנזורים דלילים עובדים בשקיפות עם ממשקי API אלה של TensorFlow:

דוגמאות מוצגות להלן עבור כמה ממשקי ה-API שלעיל.

tf.keras

תת-קבוצה של ה-API של tf.keras תומכת בטנזורים דלילים ללא פעולות ליהוק או המרה יקרות. ה-API של Keras מאפשר לך להעביר טנזורים דלילים ככניסות לדגם Keras. הגדר sparse=True בעת קריאה tf.keras.Input או tf.keras.layers.InputLayer . אתה יכול להעביר טנזורים דלילים בין שכבות Keras, וגם לאפשר לדגמי Keras להחזיר אותם כפלטים. אם אתה משתמש בטנסורים דלילים בשכבות tf.keras.layers.Dense בדגם שלך, הם יוציאו טנסורים צפופים.

הדוגמה להלן מראה לך כיצד להעביר טנזור דליל כקלט למודל Keras אם אתה משתמש רק בשכבות התומכות בקלט דליל.

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

ה-API של tf.data מאפשר לך לבנות צינורות קלט מורכבים מחלקים פשוטים הניתנים לשימוש חוזר. מבנה הנתונים הליבה שלו הוא tf.data.Dataset , המייצג רצף של אלמנטים שבהם כל אלמנט מורכב ממרכיב אחד או יותר.

בניית מערכי נתונים עם טנזורים דלילים

בנו מערכי נתונים מטנזורים דלילים באותן שיטות המשמשות לבנייתם tf.Tensor s או NumPy, כגון tf.data.Dataset.from_tensor_slices . פעולה זו משמרת את הדלילות (או האופי הדליל) של הנתונים.

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

אצווה וביטול אצווה של מערכי נתונים עם טנזורים דלילים

ניתן לבצע אצווה (לשלב רכיבים עוקבים לרכיב בודד) ולבטל מערכי נתונים עם טנזורים דלילים באמצעות שיטות Dataset.batch ו- 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}>

אתה יכול גם להשתמש ב- tf.data.experimental.dense_to_sparse_batch כדי לקבץ רכיבי מערך נתונים בצורות שונות לטנזורים דלילים.

שינוי מערכי נתונים עם טנזורים דלילים

הפוך וצור טנזורים דלילים במערכים נתונים באמצעות 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.דוגמה

tf.train.Example הוא קידוד פרוטובוף סטנדרטי עבור נתוני TensorFlow. בעת שימוש בטנסור דל עם tf.train.Example , אתה יכול:

tf.function

מעצב tf.function מחשב מראש גרפים של TensorFlow עבור פונקציות Python, מה שיכול לשפר באופן משמעותי את הביצועים של קוד TensorFlow שלך. טנזורים דלילים עובדים בשקיפות עם פונקציות tf.function וקונקרטיות .

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

הבחנה בין ערכים חסרים לאפס ערכים

רוב האופציות ב- tf.SparseTensor מתייחסות לערכים חסרים ולערכים אפס מפורשים באופן זהה. זה בתכנון - tf.SparseTensor אמור לפעול בדיוק כמו טנזור צפוף.

עם זאת, ישנם כמה מקרים שבהם זה יכול להיות שימושי להבחין בין ערכים אפס לערכים חסרים. בפרט, זה מאפשר דרך אחת לקודד נתונים חסרים/לא ידועים בנתוני האימון שלך. לדוגמה, שקול מקרה שימוש שבו יש לך טנסור של ציונים (שיכול להיות לו כל ערך של נקודה צפה מ-Inf עד +Inf), עם כמה ציונים חסרים. אתה יכול לקודד טנזור זה באמצעות טנזור דל שבו האפסים המפורשים הם ציוני אפס ידועים, אך ערכי האפס המרומזים למעשה מייצגים נתונים חסרים ולא אפס.

שימו לב שחלק מהאופציות כמו tf.sparse.reduce_max לא מתייחסות לערכים החסרים כאילו הם אפס. לדוגמה, כאשר אתה מפעיל את בלוק הקוד למטה, הפלט הצפוי הוא 0 . עם זאת, בגלל חריג זה, הפלט הוא -3 .

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

לעומת זאת, כאשר אתה מחיל את tf.math.reduce_max על טנסור צפוף, הפלט הוא 0 כצפוי.

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

קריאה נוספת ומשאבים