Skip to content

Commit 0094b39

Browse files
Add some Tensorflow graph traversal utility functions.
PiperOrigin-RevId: 517108819
1 parent 52806ba commit 0094b39

2 files changed

Lines changed: 113 additions & 2 deletions

File tree

tensorflow_privacy/privacy/fast_gradient_clipping/gradient_clipping_utils.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222

2323
PackedTensors = Union[tf.Tensor, Iterable[tf.Tensor], Dict[Text, tf.Tensor]]
2424

25-
GeneratorFunction = Optional[Callable[[Any, Tuple, Dict], Tuple[Any, Any]]]
25+
GeneratorFunction = Callable[[Any, Tuple, Dict], Tuple[Any, Any]]
26+
27+
LayerFunction = Callable[[tf.keras.layers.Layer], None]
2628

2729

2830
def has_internal_compute_graph(input_object: Any):
@@ -52,7 +54,7 @@ def _get_internal_layers(
5254
def model_forward_pass(
5355
input_model: tf.keras.Model,
5456
inputs: PackedTensors,
55-
generator_fn: GeneratorFunction = None,
57+
generator_fn: Optional[GeneratorFunction] = None,
5658
) -> Tuple[PackedTensors, List[Any]]:
5759
"""Does a forward pass of a model and returns useful intermediates.
5860
@@ -211,6 +213,55 @@ def add_noise(g):
211213
return tf.nest.map_structure(add_noise, clipped_grads)
212214

213215

216+
def depth_first_backward_pass(
217+
outputs: PackedTensors, layer_function: Optional[LayerFunction] = None
218+
):
219+
"""Performs a depth-first traversal on a given set of model outputs.
220+
221+
This function is simplified version of
222+
`tf.keras.engine.functional._build_map()` that allows additional side-effects
223+
performed by an (optional) layer function.
224+
225+
NOTE: The behavior, name, and implementation details of this function may
226+
change in future versions. Users should avoid using it outside of this module.
227+
228+
Args:
229+
outputs: A `PackedTensor` that should be generated by calling a
230+
`tf.keras.Model` on a set of non-eager inputs.
231+
layer_function: A callable that consumes a `tf.keras.layers.Layer`. This
232+
callable is applied to every layer in the DAG that generates `outputs`.
233+
"""
234+
235+
# Helper function that performs the traversal.
236+
finished_nodes = set()
237+
nodes_in_progress = set()
238+
239+
def graph_crawler(tensor: tf.Tensor):
240+
layer, node_index, _ = tensor._keras_history # pylint: disable=protected-access
241+
node = layer._inbound_nodes[node_index] # pylint: disable=protected-access
242+
# Avoid duplicating work on shared subgraphs.
243+
if node in finished_nodes:
244+
return
245+
# Check if we encountered a cycle.
246+
if node in nodes_in_progress:
247+
raise ValueError(
248+
f'Tensor {tensor} from layer "{layer.name}" is part of a cycle.'
249+
)
250+
# Apply side-effects and go to the next node (pre-order traversal).
251+
if layer_function is not None:
252+
layer_function(layer)
253+
nodes_in_progress.add(node)
254+
if not node.is_input:
255+
for tensor in node.keras_inputs:
256+
graph_crawler(tensor)
257+
finished_nodes.add(node)
258+
nodes_in_progress.remove(node)
259+
260+
# Traverse over the outputs.
261+
for output in tf.nest.flatten(outputs):
262+
graph_crawler(output)
263+
264+
214265
def generate_model_outputs_using_core_keras_layers(
215266
input_model: tf.keras.Model,
216267
) -> PackedTensors:

tensorflow_privacy/privacy/fast_gradient_clipping/gradient_clipping_utils_test.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,65 @@ def test_outputs_are_consistent(
7575
self.assertAllClose(computed_outputs, true_outputs)
7676

7777

78+
class DepthFirstBackwardPassTest(tf.test.TestCase, parameterized.TestCase):
79+
80+
@parameterized.product(
81+
depth=[1, 2],
82+
input_packing_type=[None, tuple, list, dict],
83+
output_packing_type=[None, tuple, list, dict],
84+
)
85+
def test_layer_function(self, depth, input_packing_type, output_packing_type):
86+
num_dims = 3
87+
num_units = 5
88+
num_inputs = 1 if input_packing_type is None else 2
89+
num_outputs = 1 if output_packing_type is None else 2
90+
sample_inputs = [tf.keras.Input((num_dims,)) for i in range(num_inputs)]
91+
temp_sum = tf.stack(sample_inputs, axis=0)
92+
sample_sum = [
93+
tf.multiply(temp_sum, float(i + 1.0)) for i in range(num_outputs)
94+
]
95+
sample_outputs = sample_sum
96+
for _ in range(depth):
97+
sample_outputs = [
98+
tf.keras.layers.Dense(num_units)(t) for t in sample_outputs
99+
]
100+
101+
# Pack inputs.
102+
if input_packing_type is None:
103+
inputs = sample_inputs[0]
104+
elif input_packing_type is not dict:
105+
inputs = input_packing_type(sample_inputs)
106+
else:
107+
inputs = {}
108+
keys = [str(i) for i in range(len(sample_inputs))]
109+
for k, v in zip(keys, sample_inputs):
110+
inputs[k] = v
111+
112+
# Pack outputs.
113+
if output_packing_type is None:
114+
outputs = sample_outputs[0]
115+
elif output_packing_type is not dict:
116+
outputs = output_packing_type(sample_outputs)
117+
else:
118+
outputs = {}
119+
keys = [str(i) for i in range(len(sample_outputs))]
120+
for k, v in zip(keys, sample_outputs):
121+
outputs[k] = v
122+
123+
# Append the trainable layers into a list.
124+
layer_list = []
125+
126+
def layer_function(layer):
127+
if layer.trainable_variables:
128+
layer_list.append(layer)
129+
130+
# Run the traversal and verify the outputs that are relevant to
131+
# the above layer function.
132+
gradient_clipping_utils.depth_first_backward_pass(outputs, layer_function)
133+
self.assertLen(layer_list, num_outputs * depth)
134+
for l in layer_list:
135+
self.assertIsInstance(l, tf.keras.layers.Dense)
136+
137+
78138
if __name__ == '__main__':
79139
tf.test.main()

0 commit comments

Comments
 (0)