7

I want to assign some values to slices of an input tensor in one of my model in TensorFlow 2.x (I am using 2.2 but ready to accept a solution for 2.1). A non-working template of what I am trying to do is:

import tensorflow as tf
from tensorflow.keras.models import Model

class AddToEven(Model):
    def call(self, inputs):
        outputs = inputs
        outputs[:, ::2] += inputs[:, ::2]
        return outputs

of course when building this (AddToEven().build(tf.TensorShape([None, None]))) I get the following error:

TypeError: 'Tensor' object does not support item assignment

I can achieve this simple example via the following:

class AddToEvenScatter(Model):
    def call(self, inputs):
        batch_size = tf.shape(inputs)[0]
        n = tf.shape(inputs)[-1]
        update_indices = tf.range(0, n, delta=2)[:, None]
        scatter_nd_perm = [1, 0]
        inputs_reshaped = tf.transpose(inputs, scatter_nd_perm)
        outputs = tf.tensor_scatter_nd_add(
            inputs_reshaped,
            indices=update_indices,
            updates=inputs_reshaped[::2],
        )
        outputs = tf.transpose(outputs, scatter_nd_perm)
        return outputs

(you can sanity-check with:

model = AddToEvenScatter()
model.build(tf.TensorShape([None, None]))
model(tf.ones([1, 10]))

)

But as you can see it's very complicated to write. And this is only for a static number of updates (here 1) on a 1D (+ batch size) tensor.

What I want to do is a bit more involved and I think writing it with tensor_scatter_nd_add is going to be a nightmare.

A lot of the current QAs on the topic cover the case for variables but not tensors (see e.g. this or this). It is mentionned here that indeed pytorch supports this, so I am surprised to see no response from any tf members on that topic recently. This answer doesn't really help me, because I will need some kind of mask generation which is going to be awful as well.

The question is thus: how can I do slice assignment efficiently (computation-wise, memory-wise and code-wise) w/o tensor_scatter_nd_add? The trick is that I want this to be as dynamical as possible, meaning that the shape of the inputs could be variable.

(For anyone curious I am trying to translate this code in tf).

This question was originally posted in a GitHub issue.

Zaccharie Ramzi
  • 2,106
  • 1
  • 18
  • 37
  • For lack of a better solution, I created a module for this using `tensor_scatter_nd_update`. In the long run hopefully I will not have to resort to this. But in the meantime, if anyone wants to use this you can check it out [here](https://github.com/zaccharieramzi/tf-slice-assign). – Zaccharie Ramzi Jun 04 '20 at 15:35

2 Answers2

2

Here is another solution based on binary mask.

"""Solution based on binary mask.
- We just add this mask to inputs, instead of multiplying."""
class AddToEven(tf.keras.Model):
    def __init__(self):
        super(AddToEven, self).__init__()        

    def build(self, inputshape):
        self.built = True # Actually nothing to build with, becuase we don't have any variables or weights here.

    @tf.function
    def call(self, inputs):
        w = inputs.get_shape()[-1]

        # 1-d mask generation for w-axis (activate even indices only)        
        m_w = tf.range(w)  # [0, 1, 2,... w-1]
        m_w = ((m_w%2)==0) # [True, False, True ,...] with dtype=tf.bool

        # Apply 1-d mask to 2-d input
        m_w = tf.expand_dims(m_w, axis=0) # just extend dimension as to be (1, W)
        m_w = tf.cast(m_w, dtype=inputs.dtype) # in advance, we need to convert dtype

        # Here, we just add this (1, W) mask to (H,W) input magically.
        outputs = inputs + m_w # This add operation is allowed in both TF and numpy!
        return tf.reshape(outputs, inputs.get_shape())

Sanity-check here.

# sanity-check as model
model = AddToEven()
model.build(tf.TensorShape([None, None]))
z = model(tf.zeros([2,4]))
print(z)

Result (with TF 2.1) is like this.

tf.Tensor(
[[1. 0. 1. 0.]
 [1. 0. 1. 0.]], shape=(2, 4), dtype=float32)

-------- Below is the previous answer --------

You need to create tf.Variable in build() method. It also allows dynamic size by shape=(None,). In the code below, I specified the input shape as (None, None).

class AddToEven(tf.keras.Model):
    def __init__(self):
        super(AddToEven, self).__init__()

    def build(self, inputshape):
        self.v = tf.Variable(initial_value=tf.zeros((0,0)), shape=(None, None), trainable=False, dtype=tf.float32)

    @tf.function
    def call(self, inputs):
        self.v.assign(inputs)
        self.v[:, ::2].assign(self.v[:, ::2] + 1)
        return self.v.value()

I tested this code with TF 2.1.0 and TF1.15

# test
add_to_even = AddToEven()
z = add_to_even(tf.zeros((2,4)))
print(z)

Result:

tf.Tensor(
[[1. 0. 1. 0.]
 [1. 0. 1. 0.]], shape=(2, 4), dtype=float32)

P.S. There are some other ways, such as using tf.numpy_function(), or generating mask function.

EyesBear
  • 1,376
  • 11
  • 21
  • I accept this answer as it provides a way to solve the question asked. I think the main drawback I see if I am not mistaken is that for every slice assignment in your model you need to create a different buffer `tf.Variable`. – Zaccharie Ramzi Jun 04 '20 at 22:53
  • How would you go about generating the mask here? – Zaccharie Ramzi Jun 04 '20 at 22:55
  • @ZaccharieRamzi this what my solution said you – Marco Cerliani Jun 05 '20 at 07:17
  • Well no because your solution didn't use a variable shape – Zaccharie Ramzi Jun 05 '20 at 15:11
  • @Zaccharie Ramzi I updated answer with new solution based on (adding) mask. In this method, the model doesn't have any variables nor weights to keep. So, if you print model.summary(), it would display as empty model. – EyesBear Jun 06 '20 at 09:07
  • @ZaccharieRamzi I remain of the idea that you can achieve the same solution following my answer – Marco Cerliani Jun 06 '20 at 10:02
  • @March Cerliaini Let me explain. The question was how to build static graph that allows dynamic shape. Your solution will work ONLY IF using eager_mode. In TF, static graph is activated by explicitly adding @tf.function() or implicitly model.fit(). In your solution, tf.Variable will get a fixed shape (eg. (3,4)) when the model is created. Then, we can't feed dynamic shapes like (5,10). In my solution 1), I defined tf.Variable with (None, None) shape that allows any 2-D inputs. Solution 2) also behaves dynamically without tf.Variable. Your idea was good, but these made differences. – EyesBear Jun 06 '20 at 10:18
1

It seem to produce no errors with this:

import tensorflow as tf
from tensorflow.keras.models import Model

class AddToEven(Model):
    def call(self, inputs):
        outputs = inputs
        outputs = outputs[:, ::2] + 1
        return outputs

# tf.Tensor.__iadd__ does not seem to exist, but tf.Tensor.__add__ does. 
Innat
  • 16,113
  • 6
  • 53
  • 101
Bobby Ocean
  • 3,120
  • 1
  • 8
  • 15
  • Sure, but that's not what I want to do. Here you are outputting the slice of even positions in inputs with 1 added. So your output for `tf.zeros(1, 10)` is going to be `[1, 1, ,1 ,1 1]`. What I want is `[1, 0, 1, 0, 1, 0, 1, 0, 1, 0]`, i.e. the input tensor with even positions incremented by one. – Zaccharie Ramzi May 30 '20 at 09:22
  • I don't understand, you wrote "A non-working template of what I am trying to do is:" and then showed a TypeError. I fixed the error without changing any of the functionality that your original class provided? – Bobby Ocean May 30 '20 at 16:00
  • No no the functionality is not the same that's what I am saying. I want my function to go from [0, 0,0,0] to [1,0,1,0]. In numpy or pytorch this what my function would do, but in tensorflow it doesn't appear to be as straight forward. – Zaccharie Ramzi May 30 '20 at 16:47
  • One sanity check is that the output's shape should be the same as the input shape. – Zaccharie Ramzi May 30 '20 at 16:48
  • I am not disagreeing that possibly the code you provided is not the code that you wanted. I am simply pointing out that you provided code, said this is the code I want working but has a TypeError, and I fixed the TypeError and now the code works exactly as you requested. It sounds like you didn't just want the code working, you are also uncertain about the algorithm itself and unsure how to get the output you are after. That was not clear in your post, I thought you were wondering why you got a TypeError. :-) – Bobby Ocean May 30 '20 at 16:55
  • Oh no no I see the confusion but the code I provided in the first snippet is just a first draft of the attempt. The working version is right after but I am looking for something slicker. The TypeError was just there to show that the first draft approach is indeed not working. – Zaccharie Ramzi May 30 '20 at 17:58
  • Yes, the fact that the first draft doesn't even have the correct algorithm your looking for, was not clear at all. – Bobby Ocean May 30 '20 at 20:01
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/215002/discussion-between-zaccharie-ramzi-and-bobby-ocean). – Zaccharie Ramzi May 30 '20 at 21:21