کار با سنسورهای پراکنده

مشاهده در TensorFlow.org در Google Colab اجرا شود در GitHub مشاهده کنید دانلود دفترچه یادداشت

هنگام کار با تانسورهایی که حاوی مقادیر زیادی صفر هستند، مهم است که آنها را به روشی کارآمد از نظر مکان و زمان ذخیره کنید. تانسورهای پراکنده ذخیره و پردازش کارآمد تانسورهایی را که حاوی مقادیر زیادی صفر هستند را امکان پذیر می کند. تانسورهای پراکنده به طور گسترده در طرح های رمزگذاری مانند TF-IDF به عنوان بخشی از پیش پردازش داده در برنامه های کاربردی NLP و برای پیش پردازش تصاویر با تعداد زیادی پیکسل تیره در برنامه های بینایی کامپیوتر استفاده می شود.

تانسورهای پراکنده در TensorFlow

TensorFlow تانسورهای پراکنده را از طریق شی tf.SparseTensor می دهد. در حال حاضر، تانسورهای پراکنده در TensorFlow با استفاده از فرمت فهرست مختصات (COO) کدگذاری می‌شوند. این قالب کدگذاری برای ماتریس های بسیار پراکنده مانند جاسازی ها بهینه شده است.

کدگذاری COO برای تانسورهای پراکنده شامل موارد زیر است:

  • values : یک تانسور 1 بعدی با شکل [N] که حاوی همه مقادیر غیر صفر است.
  • indices ها: یک تانسور دوبعدی با شکل [N, rank] ، حاوی شاخص های مقادیر غیر صفر.
  • dense_shape : یک تانسور 1 بعدی با شکل [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.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

تانسورهای Sparse با این API های TensorFlow به طور شفاف کار می کنند:

نمونه هایی در زیر برای تعدادی از API های بالا نشان داده شده است.

tf.keras

زیر مجموعه ای از tf.keras API از تانسورهای پراکنده بدون عملیات ریخته گری یا تبدیل گران قیمت پشتیبانی می کند. Keras API به شما امکان می دهد تانسورهای پراکنده را به عنوان ورودی به مدل Keras ارسال کنید. هنگام فراخوانی tf.keras.Input یا tf.keras.layers.InputLayer sparse=True را تنظیم کنید. می‌توانید تانسورهای پراکنده را بین لایه‌های 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

tf.data API شما را قادر می سازد خطوط لوله ورودی پیچیده ای را از قطعات ساده و قابل استفاده مجدد بسازید. ساختار داده اصلی آن 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 تانسورهای پراکنده را در Datasets تغییر دهید و ایجاد کنید.

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 یک پروتوباف استاندارد برای داده های TensorFlow است. هنگام استفاده از تانسورهای پراکنده با tf.train.Example ، می توانید:

  • داده‌های طول متغیر را با استفاده از tf.SparseTensor در tf.io.VarLenFeature . با این حال، باید به جای آن از tf.io.RaggedFeature استفاده کنید.

  • داده‌های پراکنده دلخواه را در tf.SparseTensor با استفاده از tf.io.SparseFeature که از سه کلید ویژگی جداگانه برای ذخیره indices ، values و dense_shape استفاده می‌کند.

tf.function

دکوراتور tf.function نمودارهای TensorFlow را برای توابع پایتون از قبل محاسبه می کند، که می تواند عملکرد کد 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)

مطالعه بیشتر و منابع