如何定义具有动态形状的新张量以支持自定义层中的批处理

Posted

技术标签:

【中文标题】如何定义具有动态形状的新张量以支持自定义层中的批处理【英文标题】:How to define a new Tensor with a dynamic shape to support batching in a custom layer 【发布时间】:2022-01-22 00:32:49 【问题描述】:

我正在尝试实现一个自定义层,它将标记化的单词序列预处理成一个矩阵,其中预定义的元素数量等于词汇表的大小。本质上,我正在尝试实现一个“词袋”层。这是我能想到的最接近的:

    def get_encoder(vocab_size=args.vocab_size):
       encoder = TextVectorization(max_tokens=vocab_size)
       encoder.adapt(train_dataset.map(lambda text, label: text))
       return encoder

    class BagOfWords(tf.keras.layers.Layer):
        def __init__(self, vocab_size=args.small_vocab_size, batch_size=args.batch_size):
            super(BagOfWords, self).__init__()
            self.vocab_size = vocab_size
            self.batch_size = batch_size

        def build(self, input_shape):
            super().build(input_shape)

        def call(self, inputs):
            if inputs.shape[-1] == None:
                return tf.constant(np.zeros([self.batch_size, self.vocab_size])) # 32 is the batch size
            outputs = tf.zeros([self.batch_size, self.vocab_size])
            if inputs.shape[-1] != None:
                for i in range(inputs.shape[0]):
                    for ii in range(inputs.shape[-1]):
                        ouput_idx = inputs[i][ii]
                        outputs[i][ouput_idx] = outputs[i][ouput_idx] + 1
            return outputs

    model = keras.models.Sequential()
    model.add(encoder)
    model.add(bag_of_words)
    model.add(keras.layers.Dense(64, activation='relu'))
    model.add(keras.layers.Dense(1, activation='sigmoid'))

在模型上调用 fit() 时出现错误并不奇怪:“不兼容的形状:[8,1] 与 [32,1]”。这发生在最后一步,当批大小小于 32 时。

我的问题是:抛开性能不谈,如何为我的词袋矩阵定义输出张量,使其具有动态形状以进行批处理并使我的代码正常工作?

编辑 1 在评论之后,我意识到代码确实不起作用,因为它永远不会进入“else”分支。 我对其进行了一些编辑,使其仅使用 tf 函数:

 class BagOfWords(tf.keras.layers.Layer):
        def __init__(self, vocab_size=args.small_vocab_size, batch_size=args.batch_size):
            super(BagOfWords, self).__init__()
            self.vocab_size = vocab_size
            self.batch_size = batch_size
            self.outputs = tf.Variable(tf.zeros([batch_size, vocab_size]))

        def build(self, input_shape):
            super().build(input_shape)

        def call(self, inputs):
            if tf.shape(inputs)[-1] == None:
                return tf.zeros([self.batch_size, self.vocab_size])
            self.outputs.assign(tf.zeros([self.batch_size, self.vocab_size]))
            for i in range(tf.shape(inputs)[0]):
                for ii in range(tf.shape(inputs)[-1]):
                    output_idx = inputs[i][ii]
                    if output_idx >= tf.constant(self.vocab_size, dtype=tf.int64):
                        output_idx = tf.constant(1, dtype=tf.int64)
                    self.outputs[i][output_idx].assign(self.outputs[i][output_idx] + 1)                        
            return outputs

但这并没有帮助:AttributeError: 'Tensor' object has no attribute 'assign'。

【问题讨论】:

无论批量大小如何,您的代码都不起作用。张量项分配不会那样工作。 @AloneTogether 感谢您的回答。很奇怪,因为我仔细检查了它是否有效。不管我的代码是否有效,您能指出您将如何实现这种层吗? 【参考方案1】:

如果我错了,请纠正我,但我认为使用TextVectorization 层的output_mode="multi_hot" 就足以完成您想做的事情。根据docs,multi_hot的输出方式:

每批次输出一个 int 数组,大小为 vocab_size 或 max_tokens,在批次项中映射到该索引的标记至少存在一次的所有元素中包含 1

所以可以这么简单:

import tensorflow as tf

def get_encoder():
    encoder = tf.keras.layers.TextVectorization(output_mode="multi_hot")
    encoder.adapt(train_dataset.map(lambda text, label: text))
    return encoder

texts  = [
          'All my cats in a row',
          'When my cat sits down, she looks like a Furby toy!',
          'The cat from outer space',
          'Sunshine loves to sit like this for some reason.']

labels = [[1], [0], [1], [1]]
train_dataset = tf.data.Dataset.from_tensor_slices((texts, labels))

model = tf.keras.Sequential()
model.add(get_encoder())
model.add(tf.keras.layers.Dense(64, activation='relu'))
model.add(tf.keras.layers.Dense(1, activation='sigmoid'))
model.compile(optimizer='adam', loss = tf.keras.losses.BinaryCrossentropy())
model.fit(train_dataset.batch(2), epochs=2)

这就是您的文本的编码方式:

import tensorflow as tf

texts  = ['All my cats in a row',
          'When my cat sits down, she looks like a Furby toy!',
          'The cat from outer space',
          'Sunshine loves to sit like this for some reason.']
encoder = get_encoder()
inputs = encoder(texts)
print(inputs)
tf.Tensor(
[[0. 1. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 1. 0. 0.
  0. 0. 1. 1.]
 [0. 1. 1. 1. 1. 1. 1. 0. 0. 0. 0. 0. 0. 1. 0. 1. 0. 0. 0. 0. 1. 0. 1. 0.
  0. 1. 0. 0.]
 [0. 0. 0. 1. 0. 0. 0. 0. 0. 1. 0. 1. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 1.
  0. 0. 0. 0.]
 [0. 0. 1. 0. 0. 0. 0. 1. 1. 0. 1. 0. 1. 0. 1. 0. 0. 1. 0. 1. 0. 0. 0. 0.
  1. 0. 0. 0.]], shape=(4, 28), dtype=float32)

因此,正如您在自定义层中尝试的那样,序列中存在的单词标记为 1,不存在的单词标记为 0。

【讨论】:

非常感谢您的建议!它会起作用,但我首先实现该层的原因是使用 tf.所以问题是:如何在支持动态形状和通过索引寻址元素的情况下从头开始实现这样的自定义层? 会尽快回复您。【参考方案2】:

这是一个不使用任何额外预处理层的词袋自定义 keras 层的示例:

import tensorflow as tf

class BagOfWords(tf.keras.layers.Layer):
   def __init__(self, vocabulary_size):
       super(BagOfWords, self).__init__()
       self.vocabulary_size = vocabulary_size

   def call(self, inputs):  
       batch_size = tf.shape(inputs)[0]
       outputs = tf.TensorArray(dtype=tf.float32, size=0, dynamic_size=True)
       for i in range(batch_size):
         string = inputs[i]
         string_length = tf.shape(tf.where(tf.math.not_equal(string, b'')))[0]
         string = string[:string_length]
         string_array = tf.TensorArray(dtype=tf.float32, size=0, dynamic_size=True)
         for s in string:
           string_array = string_array.write(string_array.size(), tf.where(tf.equal(s, self.vocabulary_size), 1.0, 0.0))
         outputs = outputs.write(i, tf.cast(tf.reduce_any(tf.cast(string_array.stack(), dtype=tf.bool), axis=0), dtype=tf.float32))
       return outputs.stack()

这里是手动预处理步骤和模型:

labels = [[1], [0], [1], [0]]

texts  = ['All my cats in a row',
          'When my cat sits down, she looks like a Furby toy!',
          'The cat from the outer space',
          'Sunshine loves to sit like this for some reason.']

DEFAULT_STRIP_REGEX = r'[!"#$%&()\*\+,-\./:;<=>?@\[\\\]^_`|~\']'
tensor_of_strings = tf.constant(texts)
tensor_of_strings = tf.strings.lower(tensor_of_strings)
tensor_of_strings = tf.strings.regex_replace(tensor_of_strings, DEFAULT_STRIP_REGEX, "")
split_strings = tf.strings.split(tensor_of_strings).to_tensor()
flattened_split_strings = tf.reshape(split_strings, (split_strings.shape[0] * split_strings.shape[1]))
unique_words, _ = tf.unique(flattened_split_strings)
unique_words = tf.random.shuffle(unique_words)

bag_of_words = BagOfWords(vocabulary_size = unique_words)
train_dataset = tf.data.Dataset.from_tensor_slices((split_strings, labels))
model = tf.keras.Sequential()
model.add(bag_of_words)
model.add(tf.keras.layers.Dense(64, activation='relu'))
model.add(tf.keras.layers.Dense(1, activation='sigmoid'))
model.compile(optimizer='adam', loss = tf.keras.losses.BinaryCrossentropy())
model.fit(train_dataset.batch(2), epochs=2)
Epoch 1/2
4/4 [==============================] - 2s 7ms/step - loss: 0.7081
Epoch 2/2
4/4 [==============================] - 0s 6ms/step - loss: 0.7008
<keras.callbacks.History at 0x7f5ba844bad0>

这就是 4 个编码句子的样子:

print(bag_of_words(split_strings))
tf.Tensor(
[[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 1. 0. 0. 0. 1. 0. 0. 0. 0. 0.
  1. 1. 1. 0.]
 [1. 1. 1. 0. 0. 1. 0. 0. 0. 0. 0. 1. 0. 1. 0. 0. 1. 1. 0. 0. 0. 1. 0. 0.
  0. 1. 1. 0.]
 [0. 0. 1. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 1. 0. 1. 0.
  0. 0. 0. 0.]
 [0. 1. 0. 1. 1. 0. 0. 1. 1. 1. 0. 0. 1. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0.
  0. 0. 0. 1.]], shape=(4, 28), dtype=float32)

【讨论】:

以上是关于如何定义具有动态形状的新张量以支持自定义层中的批处理的主要内容,如果未能解决你的问题,请参考以下文章

具有特定形状的 Android 自定义视图

如何在颤动中绘制自定义动态高度曲线形状?

如何自定义相机视图并捕获叠加层中的部分?

如何在 Swift 中创建具有自定义形状的 UIButton?

自定义形状的动态数据

具有自定义形状的自定义 ImageView