不规则张量

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 Github 上查看源代码 下载笔记本

API 文档: tf.RaggedTensor tf.ragged

设置

!pip install --pre -U tensorflow
import math
import tensorflow as tf
2022-12-14 22:26:04.392854: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory
2022-12-14 22:26:04.392948: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory
2022-12-14 22:26:04.392958: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.

概述

数据有多种形状;张量也应当有多种形状。不规则张量是嵌套的可变长度列表的 TensorFlow 等效项。它们使存储和处理包含非均匀形状的数据变得容易,包括:

  • 可变长度特征,例如电影的演员名单。
  • 成批的可变长度顺序输入,例如句子或视频剪辑。
  • 分层输入,例如细分为节、段落、句子和单词的文本文档。
  • 结构化输入中的各个字段,例如协议缓冲区。

不规则张量的功能

超过一百种 TensorFlow 运算支持不规则张量,包括数学运算(如 tf.addtf.reduce_mean)、数组运算(如 tf.concattf.tile)、字符串操作运算(如 tf.substr)、控制流运算(如 tf.while_looptf.map_fn)等:

digits = tf.ragged.constant([[3, 1, 4, 1], [], [5, 9, 2], [6], []])
words = tf.ragged.constant([["So", "long"], ["thanks", "for", "all", "the", "fish"]])
print(tf.add(digits, 3))
print(tf.reduce_mean(digits, axis=1))
print(tf.concat([digits, [[5, 3]]], axis=0))
print(tf.tile(digits, [1, 2]))
print(tf.strings.substr(words, 0, 2))
print(tf.map_fn(tf.math.square, digits))
<tf.RaggedTensor [[6, 4, 7, 4], [], [8, 12, 5], [9], []]>
tf.Tensor([2.25              nan 5.33333333 6.                nan], shape=(5,), dtype=float64)
<tf.RaggedTensor [[3, 1, 4, 1], [], [5, 9, 2], [6], [], [5, 3]]>
<tf.RaggedTensor [[3, 1, 4, 1, 3, 1, 4, 1], [], [5, 9, 2, 5, 9, 2], [6, 6], []]>
<tf.RaggedTensor [[b'So', b'lo'], [b'th', b'fo', b'al', b'th', b'fi']]>
<tf.RaggedTensor [[9, 1, 16, 1], [], [25, 81, 4], [36], []]>

还有专门针对不规则张量的方法和运算,包括工厂方法、转换方法和值映射运算。有关支持的运算列表,请参阅 tf.ragged 包文档

许多 TensorFlow API 都支持不规则张量,包括 KerasDatasettf.functionSavedModeltf.Example。有关详情,请参阅下面的 TensorFlow API 部分。

与普通张量一样,您可以使用 Python 风格的索引来访问不规则张量的特定切片。有关详情,请参阅下面的索引部分。

print(digits[0])       # First row
tf.Tensor([3 1 4 1], shape=(4,), dtype=int32)
print(digits[:, :2])   # First two values in each row.
<tf.RaggedTensor [[3, 1], [], [5, 9], [6], []]>
print(digits[:, -2:])  # Last two values in each row.
<tf.RaggedTensor [[4, 1], [], [9, 2], [6], []]>

与普通张量一样,您可以使用 Python 算术和比较运算符来执行逐元素运算。有关详情,请参阅下面的重载运算符部分。

print(digits + 3)
<tf.RaggedTensor [[6, 4, 7, 4], [], [8, 12, 5], [9], []]>
print(digits + tf.ragged.constant([[1, 2, 3, 4], [], [5, 6, 7], [8], []]))
<tf.RaggedTensor [[4, 3, 7, 5], [], [10, 15, 9], [14], []]>

如果需要对 RaggedTensor 的值进行逐元素转换,您可以使用 tf.ragged.map_flat_values(它采用一个函数加上一个或多个参数的形式),并应用这个函数来转换 RaggedTensor 的值。

times_two_plus_one = lambda x: x * 2 + 1
print(tf.ragged.map_flat_values(times_two_plus_one, digits))
<tf.RaggedTensor [[7, 3, 9, 3], [], [11, 19, 5], [13], []]>

不规则张量可以转换为嵌套的 Python list 和 NumPy array

digits.to_list()
[[3, 1, 4, 1], [], [5, 9, 2], [6], []]
digits.numpy()
array([array([3, 1, 4, 1], dtype=int32), array([], dtype=int32),
       array([5, 9, 2], dtype=int32), array([6], dtype=int32),
       array([], dtype=int32)], dtype=object)

构造不规则张量

构造不规则张量的最简单方式是使用 tf.ragged.constant,它会构建与给定的嵌套 Python list 或 NumPy array 相对应的 RaggedTensor

sentences = tf.ragged.constant([
    ["Let's", "build", "some", "ragged", "tensors", "!"],
    ["We", "can", "use", "tf.ragged.constant", "."]])
print(sentences)
<tf.RaggedTensor [[b"Let's", b'build', b'some', b'ragged', b'tensors', b'!'],
 [b'We', b'can', b'use', b'tf.ragged.constant', b'.']]>
paragraphs = tf.ragged.constant([
    [['I', 'have', 'a', 'cat'], ['His', 'name', 'is', 'Mat']],
    [['Do', 'you', 'want', 'to', 'come', 'visit'], ["I'm", 'free', 'tomorrow']],
])
print(paragraphs)
<tf.RaggedTensor [[[b'I', b'have', b'a', b'cat'], [b'His', b'name', b'is', b'Mat']],
 [[b'Do', b'you', b'want', b'to', b'come', b'visit'],
  [b"I'm", b'free', b'tomorrow']]]>

还可以通过将扁平的张量与行分区张量进行配对来构造不规则张量,行分区张量使用 tf.RaggedTensor.from_value_rowidstf.RaggedTensor.from_row_lengthstf.RaggedTensor.from_row_splits 等工厂类方法指示如何将值分成各行。

tf.RaggedTensor.from_value_rowids

如果您知道每个值属于哪一行,可以使用 value_rowids 行分区张量构建 RaggedTensor

value_rowids

print(tf.RaggedTensor.from_value_rowids(
    values=[3, 1, 4, 1, 5, 9, 2],
    value_rowids=[0, 0, 0, 0, 2, 2, 3]))
<tf.RaggedTensor [[3, 1, 4, 1], [], [5, 9], [2]]>

tf.RaggedTensor.from_row_lengths

如果知道每行的长度,可以使用 row_lengths 行分区张量:

row_lengths

print(tf.RaggedTensor.from_row_lengths(
    values=[3, 1, 4, 1, 5, 9, 2],
    row_lengths=[4, 0, 2, 1]))
<tf.RaggedTensor [[3, 1, 4, 1], [], [5, 9], [2]]>

tf.RaggedTensor.from_row_splits

如果知道指示每行开始和结束的索引,可以使用 row_splits 行分区张量:

row_splits

print(tf.RaggedTensor.from_row_splits(
    values=[3, 1, 4, 1, 5, 9, 2],
    row_splits=[0, 4, 4, 6, 7]))
<tf.RaggedTensor [[3, 1, 4, 1], [], [5, 9], [2]]>

有关完整的工厂方法列表,请参阅 tf.RaggedTensor 类文档。

注:默认情况下,这些工厂方法会添加断言,说明行分区张量结构良好且与值数量保持一致。如果您能够保证输入的结构良好且一致,可以使用 validate=False 参数跳过此类检查。

可以在不规则张量中存储什么

与普通 Tensor 一样,RaggedTensor 中的所有值必须具有相同的类型;所有值必须处于相同的嵌套深度(张量的):

print(tf.ragged.constant([["Hi"], ["How", "are", "you"]]))  # ok: type=string, rank=2
<tf.RaggedTensor [[b'Hi'], [b'How', b'are', b'you']]>
print(tf.ragged.constant([[[1, 2], [3]], [[4, 5]]]))        # ok: type=int32, rank=3
<tf.RaggedTensor [[[1, 2], [3]], [[4, 5]]]>
try:
  tf.ragged.constant([["one", "two"], [3, 4]])              # bad: multiple types
except ValueError as exception:
  print(exception)
Can't convert Python sequence with mixed types to Tensor.
try:
  tf.ragged.constant(["A", ["B", "C"]])                     # bad: multiple nesting depths
except ValueError as exception:
  print(exception)
all scalar values must have the same nesting depth

示例用例

以下示例演示了如何使用 RaggedTensor,通过为每个句子的开头和结尾使用特殊标记,为一批可变长度查询构造和组合一元与二元嵌入向量。有关本例中使用的运算的更多详细信息,请参阅 tf.ragged 软件包文档。

queries = tf.ragged.constant([['Who', 'is', 'Dan', 'Smith'],
                              ['Pause'],
                              ['Will', 'it', 'rain', 'later', 'today']])

# Create an embedding table.
num_buckets = 1024
embedding_size = 4
embedding_table = tf.Variable(
    tf.random.truncated_normal([num_buckets, embedding_size],
                       stddev=1.0 / math.sqrt(embedding_size)))

# Look up the embedding for each word.
word_buckets = tf.strings.to_hash_bucket_fast(queries, num_buckets)
word_embeddings = tf.nn.embedding_lookup(embedding_table, word_buckets)     # ①

# Add markers to the beginning and end of each sentence.
marker = tf.fill([queries.nrows(), 1], '#')
padded = tf.concat([marker, queries, marker], axis=1)                       # ②

# Build word bigrams and look up embeddings.
bigrams = tf.strings.join([padded[:, :-1], padded[:, 1:]], separator='+')   # ③

bigram_buckets = tf.strings.to_hash_bucket_fast(bigrams, num_buckets)
bigram_embeddings = tf.nn.embedding_lookup(embedding_table, bigram_buckets) # ④

# Find the average embedding for each sentence
all_embeddings = tf.concat([word_embeddings, bigram_embeddings], axis=1)    # ⑤
avg_embedding = tf.reduce_mean(all_embeddings, axis=1)                      # ⑥
print(avg_embedding)
tf.Tensor(
[[ 0.02510674 -0.04737939  0.01799836  0.1717977 ]
 [ 0.4977201   0.38424173 -0.40286708 -0.39982286]
 [ 0.16051094  0.2062596  -0.05239218  0.10338864]], shape=(3, 4), dtype=float32)

ragged_example

不规则维度和均匀维度

不规则维度是切片可能具有不同长度的维度。例如,rt=[[3, 1, 4, 1], [], [5, 9, 2], [6], []] 的内部(列)维度是不规则的,因为列切片 (rt[0, :], ..., rt[4, :]) 具有不同的长度。切片全都具有相同长度的维度称为均匀维度

不规则张量的最外层维度始终是统一维度,因为它只包含一个切片(因此不可能有不同的切片长度)。其余维度可能是不规则维度,也可能是统一维度。例如,我们可以使用形状为 [num_sentences, (num_words), embedding_size] 的不规则张量为一批句子中的每个单词存储单词嵌入向量,其中 (num_words) 周围的括号表示维度是不规则维度。

sent_word_embed

不规则张量可以有多个不规则维度。例如,我们可以使用形状为 [num_documents, (num_paragraphs), (num_sentences), (num_words)] 的张量存储一批结构化文本文档(其中,括号同样用于表示不规则维度)。

tf.Tensor 一样,不规则张量的是其总维数(包括不规则维度和均匀维度)。潜在的不规则张量是一个值,这个值可能是 tf.Tensortf.RaggedTensor

描述 RaggedTensor 的形状时,按照惯例,不规则维度会通过括号进行指示。例如,如上面所见,存储一批句子中每个单词的单词嵌入向量的三维 RaggedTensor 的形状可以写为 [num_sentences, (num_words), embedding_size]

RaggedTensor.shape 特性返回不规则张量的 tf.TensorShape,其中不规则维度的大小为 None

tf.ragged.constant([["Hi"], ["How", "are", "you"]]).shape
TensorShape([2, None])

可以使用方法 tf.RaggedTensor.bounding_shape 查找给定 RaggedTensor 的紧密边界形状:

print(tf.ragged.constant([["Hi"], ["How", "are", "you"]]).bounding_shape())
tf.Tensor([2 3], shape=(2,), dtype=int64)

不规则张量和稀疏张量对比

不规则张量应该被认为是一种稀疏张量。尤其是,稀疏张量是以紧凑的格式对相同数据建模的 tf.Tensor 的高效编码;而不规则张量是对扩展的数据类建模的 tf.Tensor 的扩展。这种区别在定义运算时至关重要:

  • 对稀疏张量或密集张量应用某一运算应当始终获得相同结果。
  • 对不规则张量或稀疏张量应用某一运算可能获得不同结果。

一个说明性的示例是,考虑如何为不规则张量和稀疏张量定义 concatstacktile 之类的数组运算。连接不规则张量时,会将每一行连在一起,形成一个具有组合长度的行:

ragged_concat

ragged_x = tf.ragged.constant([["John"], ["a", "big", "dog"], ["my", "cat"]])
ragged_y = tf.ragged.constant([["fell", "asleep"], ["barked"], ["is", "fuzzy"]])
print(tf.concat([ragged_x, ragged_y], axis=1))
<tf.RaggedTensor [[b'John', b'fell', b'asleep'], [b'a', b'big', b'dog', b'barked'],
 [b'my', b'cat', b'is', b'fuzzy']]>

但连接稀疏张量时,相当于连接相应的密集张量,如以下示例所示(其中 Ø 表示缺失的值):

sparse_concat

sparse_x = ragged_x.to_sparse()
sparse_y = ragged_y.to_sparse()
sparse_result = tf.sparse.concat(sp_inputs=[sparse_x, sparse_y], axis=1)
print(tf.sparse.to_dense(sparse_result, ''))
tf.Tensor(
[[b'John' b'' b'' b'fell' b'asleep']
 [b'a' b'big' b'dog' b'barked' b'']
 [b'my' b'cat' b'' b'is' b'fuzzy']], shape=(3, 5), dtype=string)

另一个说明为什么这种区别非常重要的示例是,考虑一个运算(如 tf.reduce_mean)的“每行平均值”的定义。对于不规则张量,一行的平均值是该行的值总和除以该行的宽度。但对于稀疏张量来说,一行的平均值是该行的值总和除以稀疏张量的总宽度(大于等于最长行的宽度)。

TensorFlow API

Keras

tf.keras 是 TensorFlow 的高级 API,用于构建和训练深度学习模型。通过在 tf.keras.Inputtf.keras.layers.InputLayer 上设置 ragged=True,不规则张量可以作为输入传送到 Keras 模型。不规则张量还可以在 Keras 层之间传递,并由 Keras 模型返回。以下示例显示了一个使用不规则张量训练的小 LSTM 模型。

# Task: predict whether each sentence is a question or not.
sentences = tf.constant(
    ['What makes you think she is a witch?',
     'She turned me into a newt.',
     'A newt?',
     'Well, I got better.'])
is_question = tf.constant([True, False, True, False])

# Preprocess the input strings.
hash_buckets = 1000
words = tf.strings.split(sentences, ' ')
hashed_words = tf.strings.to_hash_bucket_fast(words, hash_buckets)

# Build the Keras model.
keras_model = tf.keras.