Skip to content
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

Extract Nested Layers from Encapsulated Model #16355

Closed
innat opened this issue Apr 3, 2022 · 10 comments
Closed

Extract Nested Layers from Encapsulated Model #16355

innat opened this issue Apr 3, 2022 · 10 comments
Assignees

Comments

@innat
Copy link

innat commented Apr 3, 2022

System information.

  • Have I written custom code (as opposed to using a stock example script provided in Keras): No
  • OS Platform and Distribution (e.g., Linux Ubuntu 16.04): Windows 10
  • TensorFlow installed from (source or binary): binary
  • TensorFlow version (use command below):
  • Python version: 3.7
  • Bazel version (if compiling from source):
  • GPU model and memory:
  • Exact command to reproduce:

Describe the problem and current behavior

Encapsulated Model - It can be expressed as a whole model but act as a single layer. Please see the description below for a better understanding.

Case 1

Let's say, at first you initiate a pretrained model from keras. applications. And next, place it to Sequential API to create a final model. Later, you successfully train the model. And then, you want to get the intermediate feature maps from that keras. applications model.

IMG_SHAPE = (224, 224, 3)
pretrained_model = keras.applications.EfficientNetB0(
        weights='imagenet', 
        include_top=False, 
        input_shape=IMG_SHAPE,
        pooling='avg'
    )
pretrained_model.trainable = False

model = keras.Sequential([
    pretrained_model,
    keras.layers.Dense(512, activation='relu'),
    keras.layers.Dense(3, activation='softmax'),
])

for l in model.layers: print(l.name, l.output_shape)
efficientnetb0 (None, 1280)
dense_5 (None, 512)
dense_6 (None, 3)
nested_layers = tf.keras.models.Model(
        [model.inputs], 
        [model.layers[0].get_layer('top_activation').output, model.output]
    )

It gives

ValueError: Graph disconnected: cannot obtain value for tensor KerasTensor(type_spec=TensorSpec(shape=(None, 224, 224, 3), dtype=tf.float32, name='input_1'), name='input_1', description="created by layer 'input_1'") at layer "rescaling". The following previous layers were accessed without issue: []

[continue]

@innat
Copy link
Author

innat commented Apr 3, 2022

Case 2

It's not only limited to Sequential API but also some cases with Functional API. See another example below.

inputs = keras.Input(shape=IMG_SHAPE)
x = pretrained_model(inputs)
x = keras.layers.Dense(512, activation='relu')(x)
outputs = keras.layers.Dense(3, activation='softmax')(x)
model = keras.Model(inputs, outputs)

for l in model.layers:
    print(l.name, l.output_shape)
input_7 [(None, 224, 224, 3)]
efficientnetb0 (None, 1280)
dense_7 (None, 512)
dense_8 (None, 3)
nested_layers = tf.keras.models.Model(
        [model.inputs], 
        [model.layers[1].get_layer('top_activation').output, model.output]
    )

It also gives

ValueError: Graph disconnected: cannot obtain value for tensor KerasTensor(type_spec=TensorSpec(shape=(None, 224, 224, 3), dtype=tf.float32, name='input_6'), name='input_6', description="created by layer 'input_6'") at layer "rescaling_3". The following previous layers were accessed without issue: []

[continue]

@innat
Copy link
Author

innat commented Apr 3, 2022

The Current Workaround

Currently, we need to flatten the base model (above pretrained_model). It means, disclosing the target base model in the following manner.

inputs = keras.Input(shape=IMG_SHAPE)
pretrained_model = keras.applications.EfficientNetB0(
        weights='imagenet', 
        include_top=False, 
        input_tensor=inputs,
        pooling='avg'
    )
pretrained_model.trainable = False

x = keras.layers.Dense(512, activation='relu')(pretrained_model.output)
outputs = keras.layers.Dense(3, activation='softmax')(x)
model = keras.Model(inputs, outputs)
nested_layers = tf.keras.models.Model(
        [model.inputs], 
        [model.get_layer('top_activation').output, model.output]
    )
# OK 

Describe the expected behavior.

The case 1 and case 2, described above should be done like the above-mentioned current workaround.


  • Do you want to contribute a PR? (yes/no): Yes, but need some design guidance.
  • If yes, please read this page for instructions
  • Briefly describe your candidate solution(if contributing): It's mentioned in the current workaround section. Please see.

Standalone code to reproduce the issue.

It's already provided in the above description. Please see.

[End]

@sushreebarsa
Copy link
Contributor

@gadagashwini I was able to replicate the issue on colab using TF v2.8.0 , please find the gist.Thanks!

@gadagashwini gadagashwini added the keras-team-review-pending Pending review by a Keras team member. label Apr 11, 2022
@fchollet
Copy link
Member

I think the error message could be better, but the behavior here is logical.

When you create the EfficientNet model, you create a first graph of layers linking pretrained_model.inputs to pretrained_model.get_layer('top_activation').output. Then you create a second graph built on top of your own Input object.

Does that make sense so far?

Because the top layer has been called twice, pretrained_model.get_layer('top_activation').output is now undefined -- there are actually 2 outputs, one of the first call and one for the second call. The one for the second call is tracked at the level of the output nodes of pretrained_model. When you retrieve pretrained_model.get_layer('top_activation').output you are getting the first output, but you were expecting the second one. This is why your graph is disconnected.

To access the second output, the one you're looking for, you should be able to do:

nested_layers = keras.Model(
        [model.inputs],   # This is the Input() you've created yourself
        [pretrained_model.inbound_nodes[0].output_tensors,  # This is output created when you called `pretrained_model()` on your own `Input()`
         model.output]  # This is the output of the final `Dense()` layer you created
)

Let me know if this is clear.

@fchollet fchollet removed keras-team-review-pending Pending review by a Keras team member. type:bug/performance labels Apr 13, 2022
@innat
Copy link
Author

innat commented Apr 14, 2022

@fchollet yes, that makes sense. Thanks for your details answer. I also understood that it's not a bug, I should mention that earlier. The thing is, I saw many practitioners face such issues after training their models and later trying to compute grad-cam. It's one of the common keras-tagged questions on the stack. The fact is the modeling approach (model above) is so typical that people usually go with it and later face such issues (with nested_layers above).


To access the second output, the one you're looking for, you should be able to do:

About this workaround, in case 1/2,

nested_layers = keras.Model([
            model.inputs
        ],[
            pretrained_model.inbound_nodes[0].output_tensors,  
            model.output
        ]  
)

This inbound_nodes gives the last layer output of the pretrained model (None, 1280). Is there any way we can get an intermediate 2D layer (i.e. top_activation)? Also, when I did

pretrained_model.inbound_nodes?

Type:        property
String form: <property object at 0x7ff9f6f22d70>
Docstring:   Deprecated, do NOT use! 
Only for compatibility with external Keras.

Is it safe to use for now and later?

@innat
Copy link
Author

innat commented Jan 3, 2023

cc @gadagashwini

@innat
Copy link
Author

innat commented Jan 27, 2023

@lucasdavid any thoughst on this? I saw some utility from here, looks promising.

@lucasdavid
Copy link
Contributor

As far as I am aware, in a previous version of Keras:
Using a model as a layer of another model would call Layer.__call__ for every sub-component of the inner model, creating nodes for each intermediate signal (see v2.0.0/engine/topology.py#L573).
The new nodes were stacked in the _inbound_nodes property of the inner model's layers, and we could extract them with this:

nested_layers = keras.Model(
        [model.inputs],
        [model.get_layer("inner_model").get_layer("post_relu").get_output_at(1),
         model.output]  # This is the output of the final `Dense()` layer you created
)

This solution is better discussed in #34977#issuecomment-571878943.

In the current version:
Calling Model.__call__ will not call Layer.__call__ for its composing layers, but treat the intermediate operations of the inner model as a single encapsulated function.
A new node will be stacked in the _inbound_nodes list of the inner model itself (see base_layer.py#L2589), but the intermediate signals will not be available.
In other words, calling inner_model.get_layer("post_relu").get_output_at(1) raises an error, as <layer post_relu>._inbound_nodes is a list of a single element. Calling inner_model.get_output_at(1) does not raise an error, but it is not the output you need...

About the utility you mentioned: I kept it in my own lib (keras-explainable), in case the user has an older version of keras, but I have skipped the testing of this functionality on master.
As of today, I believe avoiding the nesting of the layers of interest (e.g. GAP) is the only viable solution.

@lucasdavid
Copy link
Contributor

@innat has this problem been fixed? How does the new API behave?

@innat
Copy link
Author

innat commented Sep 20, 2023

Hi @lucasdavid, I didn't test this with the new API yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants