Skip to content

keras.layers.RandomGrayscale can't be put in as part of image augmentation layer #21268

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
AlundorZhu opened this issue May 9, 2025 · 4 comments · Fixed by #21312
Closed
Assignees
Labels
keras-team-review-pending Pending review by a Keras team member. type:Bug

Comments

@AlundorZhu
Copy link

Hi all,
When I build the model with RandomGrayscale in my image augmentation layers it gives the following error:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[35], line 8
      5 x = keras.layers.Dropout(0.2)(x)
      6 output = keras.layers.Dense(1, activation=None)(x)
----> 8 model = keras.Model(inputs=input, outputs=output)
     10 model.compile(
     11     optimizer='adam',
     12     loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
     13     metrics=['accuracy'],
     14 )

File ~/Work/Research/.venv-metal/lib/python3.12/site-packages/keras/src/utils/tracking.py:26, in no_automatic_dependency_tracking.<locals>.wrapper(*args, **kwargs)
     23 @wraps(fn)
     24 def wrapper(*args, **kwargs):
     25     with DotNotTrackScope():
---> 26         return fn(*args, **kwargs)

File ~/Work/Research/.venv-metal/lib/python3.12/site-packages/keras/src/models/functional.py:135, in Functional.__init__(self, inputs, outputs, name, **kwargs)
    132 if not all(is_input_keras_tensor(t) for t in flat_inputs):
    133     inputs, outputs = clone_graph_nodes(inputs, outputs)
--> 135 Function.__init__(self, inputs, outputs, name=name)
    137 if trainable is not None:
    138     self.trainable = trainable

File ~/Work/Research/.venv-metal/lib/python3.12/site-packages/keras/src/ops/function.py:77, in Function.__init__(self, inputs, outputs, name)
     74 if backend() == "tensorflow":
     75     self._self_setattr_tracking = _self_setattr_tracking
---> 77 (nodes, nodes_by_depth, operations, operations_by_depth) = map_graph(
     78     self._inputs, self._outputs
     79 )
     80 self._nodes = nodes
     81 self._nodes_by_depth = nodes_by_depth

File ~/Work/Research/.venv-metal/lib/python3.12/site-packages/keras/src/ops/function.py:232, in map_graph(inputs, outputs)
    216 """Validates a graph's topology and gather its operations and nodes.
    217 
    218 Args:
   (...)
    228         instances.
    229 """
    230 # "depth" is number of operations between output Node and the Node.
    231 # Nodes are ordered from inputs -> outputs.
--> 232 nodes_in_decreasing_depth, operation_indices = _build_map(inputs, outputs)
    233 network_nodes = {
    234     make_node_key(node.operation, node.operation._inbound_nodes.index(node))
    235     for node in nodes_in_decreasing_depth
    236 }
    238 nodes_depths = {}  # dict {node: depth value}

File ~/Work/Research/.venv-metal/lib/python3.12/site-packages/keras/src/ops/function.py:363, in _build_map(inputs, outputs)
    361 operation_indices = {}  # operation -> in traversal order.
    362 for output in tree.flatten(outputs):
--> 363     _build_map_helper(
    364         inputs,
    365         output,
    366         finished_nodes,
    367         nodes_in_progress,
    368         nodes_in_decreasing_depth,
    369         operation_indices,
    370     )
    371 return nodes_in_decreasing_depth, operation_indices

File ~/Work/Research/.venv-metal/lib/python3.12/site-packages/keras/src/ops/function.py:412, in _build_map_helper(inputs, tensor, finished_nodes, nodes_in_progress, nodes_in_decreasing_depth, operation_indices)
    410 if not node.is_input and tensor not in tree.flatten(inputs):
    411     for tensor in node.input_tensors:
--> 412         _build_map_helper(
    413             inputs,
    414             tensor,
    415             finished_nodes,
    416             nodes_in_progress,
    417             nodes_in_decreasing_depth,
    418             operation_indices,
    419         )
    421 finished_nodes.add(node)
    422 nodes_in_progress.remove(node)

File ~/Work/Research/.venv-metal/lib/python3.12/site-packages/keras/src/ops/function.py:412, in _build_map_helper(inputs, tensor, finished_nodes, nodes_in_progress, nodes_in_decreasing_depth, operation_indices)
    410 if not node.is_input and tensor not in tree.flatten(inputs):
    411     for tensor in node.input_tensors:
--> 412         _build_map_helper(
    413             inputs,
    414             tensor,
    415             finished_nodes,
    416             nodes_in_progress,
    417             nodes_in_decreasing_depth,
    418             operation_indices,
    419         )
    421 finished_nodes.add(node)
    422 nodes_in_progress.remove(node)

    [... skipping similar frames: _build_map_helper at line 412 (1 times)]

File ~/Work/Research/.venv-metal/lib/python3.12/site-packages/keras/src/ops/function.py:412, in _build_map_helper(inputs, tensor, finished_nodes, nodes_in_progress, nodes_in_decreasing_depth, operation_indices)
    410 if not node.is_input and tensor not in tree.flatten(inputs):
    411     for tensor in node.input_tensors:
--> 412         _build_map_helper(
    413             inputs,
    414             tensor,
    415             finished_nodes,
    416             nodes_in_progress,
    417             nodes_in_decreasing_depth,
    418             operation_indices,
    419         )
    421 finished_nodes.add(node)
    422 nodes_in_progress.remove(node)

File ~/Work/Research/.venv-metal/lib/python3.12/site-packages/keras/src/ops/function.py:399, in _build_map_helper(inputs, tensor, finished_nodes, nodes_in_progress, nodes_in_decreasing_depth, operation_indices)
    397 # Prevent cycles.
    398 if node in nodes_in_progress:
--> 399     raise ValueError(
    400         f"Tensor {tensor} from operation '{operation.name}' is part of a "
    401         "cycle."
    402     )
    404 # Store the traversal order for operation sorting.
    405 if operation not in operation_indices:

ValueError: Tensor <KerasTensor shape=(None, 224, 224, 3), dtype=float32, sparse=False, name=keras_tensor_1089> from operation 'random_grayscale_4' is part of a cycle.

The code I use to build the model:

data_augmentation_layers = [
    layers.RandomFlip("horizontal"),
    layers.RandomRotation(0.02),
    layers.RandomShear(x_factor=0.1, y_factor=0.1),
    layers.RandomTranslation(0.1, 0.1),
    layers.RandomGrayscale(0.2)
]


def data_augmentation(images):
    for layer in data_augmentation_layers:
        images = layer(images)
    return images


IMG_SHAPE = (224, 224, 3)

base_model = keras.applications.MobileNetV3Large(
    include_top=False,
    weights="imagenet",
    input_shape=IMG_SHAPE,
    pooling="avg",
    classifier_activation=None
)

preprocess_input = keras.applications.mobilenet_v3.preprocess_input

input = keras.Input(IMG_SHAPE)
x = data_augmentation(input)
x = preprocess_input(x)
x = base_model(x)
x = keras.layers.Dropout(0.2)(x)
output = keras.layers.Dense(1, activation=None)(x)

model = keras.Model(inputs=input, outputs=output)

model.compile(
    optimizer='adam',
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy'],
)

Best,

@pctablet505 pctablet505 added the keras-team-review-pending Pending review by a Keras team member. label May 9, 2025
@pctablet505
Copy link
Collaborator

The below code works for me. After fixing some bug for passing single unbatched image in RandomGrayscale.call
#21277

class DataAugmentation(keras.Layer):
    def __init__(self, name=None):
        super().__init__(name='Data Augmentation')
        # Store layers as attributes to ensure they are tracked by Keras
        self.random_flip = layers.RandomFlip("horizontal")
        self.random_rotation = layers.RandomRotation(0.02)
        self.random_shear = layers.RandomShear(x_factor=0.1, y_factor=0.1)
        self.random_translation = layers.RandomTranslation(0.1, 0.1)
        self.random_grayscale = layers.RandomGrayscale(0.2)

    def call(self, images, training=None):
        x = images
        # Pass the training argument to each layer
        x = self.random_flip(x, training=training)
        x = self.random_rotation(x, training=training)
        x = self.random_shear(x, training=training)
        x = self.random_translation(x, training=training)
        x = self.random_grayscale(x, training=training)
        return x



IMG_SHAPE = (224, 224, 3)

base_model = keras.applications.MobileNetV3Large(
    include_top=False,
    weights=None,
    input_shape=IMG_SHAPE,
    pooling="avg",
    classifier_activation=None
)

preprocess_input = keras.applications.mobilenet_v3.preprocess_input

input = keras.Input(IMG_SHAPE)
x = DataAugmentation()(input)
x = preprocess_input(x)
x = base_model(x)
x = keras.layers.Dropout(0.2)(x)
output = keras.layers.Dense(1, activation=None)(x)

model = keras.Model(inputs=input, outputs=output)

model.compile(
    optimizer='adam',
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy'],
)

print(model)
print(model.summary())

@AlundorZhu
Copy link
Author

Thank you! it works!

Also, final bit of change to make model.fit work:

def __init__(self, name=None):
        # Change the layer name to remove the space
        super().__init__(name='DataAugmentation')

@pctablet505
Copy link
Collaborator

import os
os.environ["KERAS_BACKEND"] = "torch"
import keras
from keras import layers


data_augmentation_layers = [
    layers.RandomFlip("horizontal"),
    layers.RandomRotation(0.02),
    layers.RandomShear(x_factor=0.1, y_factor=0.1),
    layers.RandomTranslation(0.1, 0.1),
    layers.RandomGrayscale(0.2)
]


def data_augmentation(images):
    for layer in data_augmentation_layers:
        images = layer(images)
    return images


IMG_SHAPE = (224, 224, 3)

input = keras.Input(IMG_SHAPE)
x = data_augmentation(input)

output = x

model = keras.Model(inputs=input, outputs=output)

model.compile(
    optimizer='adam',
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy'],
)

print(model)
print(model.summary())

this is giving error

/usr/local/bin/python3 /Users/hellorahul/PyCharmMiscProject/script.py 
Traceback (most recent call last):
  File "/Users/hellorahul/PyCharmMiscProject/script.py", line 54, in <module>
    model = keras.Model(inputs=input, outputs=output)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/hellorahul/Projects/keras/keras/src/utils/tracking.py", line 26, in wrapper
    return fn(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^
  File "/Users/hellorahul/Projects/keras/keras/src/models/functional.py", line 136, in __init__
    Function.__init__(self, inputs, outputs, name=name)
  File "/Users/hellorahul/Projects/keras/keras/src/ops/function.py", line 77, in __init__
    (nodes, nodes_by_depth, operations, operations_by_depth) = map_graph(
                                                               ^^^^^^^^^^
  File "/Users/hellorahul/Projects/keras/keras/src/ops/function.py", line 238, in map_graph
    nodes_in_decreasing_depth, operation_indices = _build_map(inputs, outputs)
                                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/hellorahul/Projects/keras/keras/src/ops/function.py", line 369, in _build_map
    _build_map_helper(
  File "/Users/hellorahul/Projects/keras/keras/src/ops/function.py", line 418, in _build_map_helper
    _build_map_helper(
  File "/Users/hellorahul/Projects/keras/keras/src/ops/function.py", line 405, in _build_map_helper
    raise ValueError(
ValueError: Tensor <KerasTensor shape=(None, 224, 224, 3), dtype=float32, sparse=False, ragged=False, name=keras_tensor_4> from operation 'random_grayscale' is part of a cycle.

Process finished with exit code 1

whereas if we use the Subclassing based technique, it works

import os
# os.environ["KERAS_BACKEND"] = "torch"
import keras
from keras import layers



#%%

class DataAugmentation(keras.Layer):
    def __init__(self, name=None):
        super().__init__(name='Data Augmentation')
        # Store layers as attributes to ensure they are tracked by Keras
        self.random_flip = layers.RandomFlip("horizontal")
        self.random_rotation = layers.RandomRotation(0.02)
        self.random_shear = layers.RandomShear(x_factor=0.1, y_factor=0.1)
        self.random_translation = layers.RandomTranslation(0.1, 0.1)
        self.random_grayscale = layers.RandomGrayscale(0.2)

    def call(self, images, training=None):
        x = images
        # Pass the training argument to each layer
        x = self.random_flip(x, training=training)
        x = self.random_rotation(x, training=training)
        x = self.random_shear(x, training=training)
        x = self.random_translation(x, training=training)
        x = self.random_grayscale(x, training=training)
        return x



# data_augmentation_layers = [
#     layers.RandomFlip("horizontal"),
#     layers.RandomRotation(0.02),
#     layers.RandomShear(x_factor=0.1, y_factor=0.1),
#     layers.RandomTranslation(0.1, 0.1),
#     layers.RandomGrayscale(0.2)
# ]
#
#
# def data_augmentation(images):
#     for layer in data_augmentation_layers:
#         images = layer(images)
#     return images


IMG_SHAPE = (224, 224, 3)

input = keras.Input(IMG_SHAPE)
x = DataAugmentation()(input)

output = x

model = keras.Model(inputs=input, outputs=output)

model.compile(
    optimizer='adam',
    loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy'],
)

print(model)
print(model.summary())

Output:

/usr/local/bin/python3 /Users/hellorahul/PyCharmMiscProject/script.py 
<Functional name=functional, built=True>
Model: "functional"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                    ┃ Output Shape           ┃       Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ input_layer (InputLayer)        │ (None, 224, 224, 3)    │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ Data Augmentation               │ (None, 224, 224, 3)    │             0 │
│ (DataAugmentation)              │                        │               │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 0 (0.00 B)
 Trainable params: 0 (0.00 B)
 Non-trainable params: 0 (0.00 B)

@pctablet505
Copy link
Collaborator

Why is it that other keras image preprocessing layers not causing cycle, whereas random_grayscale causing cycle?
I tried multiple things like mimicking the codeflow from random_flip, random_contrast, etc in random_grayscale, but it still detects a cycle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
keras-team-review-pending Pending review by a Keras team member. type:Bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants