การทำงานกับเทนเซอร์แบบกระจัดกระจาย

ดูบน TensorFlow.org ทำงานใน Google Colab ดูบน GitHub ดาวน์โหลดโน๊ตบุ๊ค

เมื่อทำงานกับเทนเซอร์ที่มีค่าศูนย์จำนวนมาก สิ่งสำคัญคือต้องจัดเก็บค่าเหล่านี้ไว้ในลักษณะที่ประหยัดพื้นที่และเวลา เมตริกซ์แบบกระจายช่วยให้สามารถจัดเก็บและประมวลผลเทนเซอร์ที่มีค่าศูนย์จำนวนมากได้อย่างมีประสิทธิภาพ เมตริกซ์แบบกระจายมีการใช้กันอย่างแพร่หลายในรูปแบบการเข้ารหัส เช่น TF-IDF โดยเป็นส่วนหนึ่งของการประมวลผลข้อมูลล่วงหน้าในแอปพลิเคชัน NLP และสำหรับการประมวลผลภาพล่วงหน้าที่มีพิกเซลมืดจำนวนมากในแอปพลิเคชันการมองเห็นด้วยคอมพิวเตอร์

เมตริกซ์แบบกระจายใน TensorFlow

TensorFlow แสดงถึงเทนเซอร์แบบเบาบางผ่านอ็อบเจ็กต์ tf.SparseTensor ปัจจุบัน เมตริกซ์แบบกระจายใน TensorFlow ได้รับการเข้ารหัสโดยใช้รูปแบบรายการพิกัด (COO) รูปแบบการเข้ารหัสนี้ได้รับการปรับให้เหมาะสมสำหรับเมทริกซ์ที่มีการกระจายมากเกินไป เช่น การฝัง

การเข้ารหัส COO สำหรับเมตริกซ์แบบกระจายประกอบด้วย:

  • values : เทนเซอร์ 1D ที่มีรูปร่าง [N] ที่มีค่าที่ไม่ใช่ศูนย์ทั้งหมด
  • indices : เทนเซอร์ 2 มิติที่มีรูปร่าง [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 เพื่อจัดการเทนเซอร์แบบเบาบาง Ops เช่น 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)

ใส่ sparse tensor เข้าด้วยกันโดยใช้ 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)
ตัวยึดตำแหน่ง23

การใช้ tf.SparseTensor กับ TensorFlow API อื่นๆ

เมตริกซ์แบบกระจายทำงานอย่างโปร่งใสกับ TensorFlow API เหล่านี้:

ตัวอย่างแสดงไว้ด้านล่างสำหรับ API บางส่วนข้างต้น

tf.keras

ชุดย่อยของ tf.keras API รองรับเทนเซอร์แบบเบาบางโดยไม่ต้องแคสต์หรือการแปลงค่า ops ที่มีราคาแพง Keras API ให้คุณส่งผ่านเทนเซอร์แบบเบาบางเป็นอินพุตไปยังโมเดล Keras Set 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

tf.data API ช่วยให้คุณสร้างไพพ์ไลน์อินพุตที่ซับซ้อนจากชิ้นส่วนที่เรียบง่ายและนำกลับมาใช้ใหม่ได้ โครงสร้างข้อมูลหลักของมันคือ tf.data.Dataset ซึ่งแสดงถึงลำดับขององค์ประกอบที่แต่ละองค์ประกอบประกอบด้วยส่วนประกอบตั้งแต่หนึ่งองค์ประกอบขึ้นไป

การสร้างชุดข้อมูลที่มีเทนเซอร์เบาบาง

สร้างชุดข้อมูลจากเทนเซอร์แบบกระจายโดยใช้วิธีการเดียวกับที่ใช้สร้างจาก tf.Tensor หรือ NumPy เช่น tf.data.Dataset.from_tensor_slices op นี้จะรักษาข้อมูลที่กระจัดกระจาย (หรือลักษณะเบาบาง) ของข้อมูล

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}>
ตัวยึดตำแหน่ง33

tf.train.ตัวอย่าง

tf.train.Example คือการเข้ารหัส protobuf มาตรฐานสำหรับข้อมูล 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 ล่วงหน้าสำหรับฟังก์ชัน 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)

แยกแยะค่าที่หายไปจากค่าศูนย์

ops ส่วนใหญ่บน tf.SparseTensor จะจัดการกับค่าที่หายไปและค่าศูนย์ที่ชัดเจนเหมือนกัน นี่คือการออกแบบ — tf.SparseTensor ควรจะทำหน้าที่เหมือนกับเทนเซอร์ที่มีความหนาแน่นสูง

อย่างไรก็ตาม มีบางกรณีที่การแยกค่าศูนย์ออกจากค่าที่ขาดหายไปอาจเป็นประโยชน์ โดยเฉพาะอย่างยิ่ง วิธีนี้ช่วยให้มีวิธีหนึ่งในการเข้ารหัสข้อมูลที่ขาดหายไป/ไม่รู้จักในข้อมูลการฝึกของคุณ ตัวอย่างเช่น พิจารณากรณีการใช้งานที่คุณมีเทนเซอร์ของคะแนน (ซึ่งสามารถมีค่าทศนิยมจาก -Inf ถึง +Inf) โดยมีคะแนนหายไปบางส่วน คุณสามารถเข้ารหัสเทนเซอร์นี้ได้โดยใช้เทนเซอร์แบบเบาบาง โดยที่ค่าศูนย์ที่ชัดเจนนั้นเรียกว่าคะแนนเป็นศูนย์ แต่ค่าศูนย์โดยนัยจะแสดงข้อมูลที่ขาดหายไปจริง ๆ ไม่ใช่ศูนย์

โปรดทราบว่า ops บางอย่างเช่น 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)
ตัวยึดตำแหน่ง39

อ่านเพิ่มเติมและทรัพยากร