Skip to content

Commit 62a2d43

Browse files
Add the second set of EinsumDense utility functions for implementing fast gradient norm computation.
PiperOrigin-RevId: 568063831
1 parent 1be6e02 commit 62a2d43

3 files changed

Lines changed: 270 additions & 30 deletions

File tree

tensorflow_privacy/privacy/fast_gradient_clipping/registry_functions/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ py_test(
1919
name = "einsum_utils_test",
2020
srcs = ["einsum_utils_test.py"],
2121
python_version = "PY3",
22+
shard_count = 4,
2223
srcs_version = "PY3",
2324
deps = [":einsum_utils"],
2425
)

tensorflow_privacy/privacy/fast_gradient_clipping/registry_functions/einsum_utils.py

Lines changed: 180 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import enum
1717
import itertools
18+
import os
1819
import re
1920

2021
import numpy as np
@@ -37,6 +38,36 @@ def _is_batch_of_vectors(t: tf.Tensor) -> bool:
3738
return num_nontrivial_indices <= 1
3839

3940

41+
def _is_valid_einsum_equation(
42+
maybe_ab: str,
43+
maybe_bc: str,
44+
maybe_ac: str,
45+
) -> bool:
46+
"""Checks if three input strings form a valid einsum dense equation.
47+
48+
Given three substrings `maybe_ab`, `maybe_bc`, and `maybe_ac`, this function
49+
checks if
50+
```
51+
maybe_ab + ',' + maybe_bc + '->' + maybe_ac
52+
```
53+
is an einsum equation of the form `ab,bc->ac`.
54+
55+
Args:
56+
maybe_ab: The proposed `ab` substring.
57+
maybe_bc: The proposed `bc` substring.
58+
maybe_ac: The proposed `ac` substring.
59+
60+
Returns:
61+
`True` if the three input strings form an einsum equation of the form
62+
`ab,bc->ac` and `False` otherwise.
63+
"""
64+
a_substr = os.path.commonprefix([maybe_ab, maybe_ac])
65+
a_len = len(a_substr)
66+
b_substr = maybe_ab[a_len:]
67+
c_substr = maybe_ac[a_len:]
68+
return maybe_bc == b_substr + c_substr
69+
70+
4071
def _parse_einsum_equation(
4172
equation: str,
4273
) -> tuple[EquationType, tuple[str, str, str]]:
@@ -61,22 +92,33 @@ def _try_match(regex_str):
6192
maybe_match = re.fullmatch(regex_str, equation)
6293
return maybe_match.groups() if maybe_match is not None else None
6394

64-
groups1 = _try_match(r"([a-zA-Z]+),([a-zA-Z]+)->([a-zA-Z]+)")
65-
if groups1 is not None:
66-
return EquationType.NO_ELLIPSES, groups1
67-
groups2 = _try_match(r"\.\.\.([a-zA-Z]+),([a-zA-Z]+)->\.\.\.([a-zA-Z]+)")
68-
if groups2 is not None:
69-
return EquationType.LEFT_ELLIPSES, groups2
70-
groups3 = _try_match(r"([a-zA-Z]+)\.\.\.,([a-zA-Z]+)->([a-zA-Z]+)\.\.\.")
71-
if groups3 is not None:
72-
return EquationType.RIGHT_ELLIPSES, groups3
73-
raise ValueError(
95+
error_message = (
7496
"Invalid Einsum equation string "
7597
+ equation
7698
+ " ."
7799
"Must be one of the forms {ab,bc->ac}, {...ab,bc->...ac}, "
78100
"{ab...,bc->ac...}"
79101
)
102+
case_pairs = [
103+
# equation_type, regex_str
104+
(EquationType.NO_ELLIPSES, r"([a-zA-Z]+),([a-zA-Z]+)->([a-zA-Z]+)"),
105+
(
106+
EquationType.LEFT_ELLIPSES,
107+
r"\.\.\.([a-zA-Z]+),([a-zA-Z]+)->\.\.\.([a-zA-Z]+)",
108+
),
109+
(
110+
EquationType.RIGHT_ELLIPSES,
111+
r"([a-zA-Z]+)\.\.\.,([a-zA-Z]+)->([a-zA-Z]+)\.\.\.",
112+
),
113+
]
114+
for equation_type, regex_str in case_pairs:
115+
groups = _try_match(regex_str)
116+
if groups is not None:
117+
if not _is_valid_einsum_equation(*groups):
118+
raise ValueError(error_message)
119+
return equation_type, groups
120+
# No valid cases found. Raise an error.
121+
raise ValueError(error_message)
80122

81123

82124
def _reshape_einsum_inputs(
@@ -100,13 +142,17 @@ def _reshape_einsum_inputs(
100142
and the number of output columns is the second dimension of the input. The
101143
product of the non-trivial dimensions of the output should be equal to
102144
the product of the dimensions of `input_tensor`.
145+
146+
Raises:
147+
ValueError: If `equation` is not a valid einsum equation in the context of
148+
the `tf.keras.layers.EinsumDense` layer.
103149
"""
104150
# Find the components `ab`, `bc`, and `ac` given that `equation` can only be
105151
# one of the following mutually exclusive forms:
106152
#
107-
# (C1) ab,bc->ac,
108-
# (C2) ...ab,bc->...ac
109-
# (C3) ab...,bc->ac...
153+
# C1. ab,bc->ac,
154+
# C2. ...ab,bc->...ac
155+
# C3. ab...,bc->ac...
110156
#
111157
# NOTE: `a`, `b`, and `c` are (possibly) also substrings.
112158

@@ -115,7 +161,7 @@ def _reshape_einsum_inputs(
115161
input_len = len(input_shape)
116162
equation_type, (ab_str, bc_str, ac_str) = _parse_einsum_equation(equation)
117163
if equation_type == EquationType.LEFT_ELLIPSES:
118-
# In case (C2), the `a` part of this component can be empty, so we have no
164+
# In case C2, the `a` part of this component can be empty, so we have no
119165
# choice but to compare the `c` part of `ac` with the `bc` component.
120166
c_len = 0
121167
for s1, s2 in itertools.zip_longest(reversed(bc_str), reversed(ac_str)):
@@ -135,7 +181,7 @@ def _reshape_einsum_inputs(
135181
else:
136182
break
137183
# Prepare `input_tensor` for reshaping and get the pivot index of the prepped
138-
# tensor. Note that case (C3) requires a transpose to ensure that matrix
184+
# tensor. Note that case C3 requires a transpose to ensure that matrix
139185
# multiplication is performed by the caller.
140186
if equation_type == EquationType.RIGHT_ELLIPSES:
141187
ellipses_idx = len(ab_str)
@@ -178,17 +224,124 @@ def _reshape_einsum_outputs(
178224
A rank-3 `tf.Tensor` whose first dimension is the batch dimension. The
179225
product of the non-trivial dimensions of the output should be equal to
180226
the product of the non-trivial dimensions of `output_tensor`.
227+
228+
Raises:
229+
ValueError: If `equation` is not a valid einsum equation in the context of
230+
the `tf.keras.layers.EinsumDense` layer.
181231
"""
182-
match = re.fullmatch(r"([a-zA-Z.]+),([a-zA-Z.]+)->([a-zA-Z.]+)", equation)
183-
if match is not None:
184-
s1, s2, s3 = match.groups()
185-
else:
186-
raise ValueError(
187-
"Invalid Einsum equation string "
188-
+ equation
189-
+ " ."
190-
"Must be one of the forms {ab,bc->ac}, {...ab,bc->...ac}, "
191-
"{ab...,bc->ac...}"
192-
)
193-
reversed_equation = s3 + "," + s2[::-1] + "->" + s1
232+
# Get the raw components of the reversed equation.
233+
equation_type, (ab_str, bc_str, ac_str) = _parse_einsum_equation(equation)
234+
prefix = "..." if equation_type == EquationType.LEFT_ELLIPSES else ""
235+
suffix = "..." if equation_type == EquationType.RIGHT_ELLIPSES else ""
236+
ellided_ab_str = prefix + ab_str + suffix
237+
ellided_ac_str = prefix + ac_str + suffix
238+
# Swap the `b` and `c` components.
239+
c_str = os.path.commonprefix([bc_str[::-1], ac_str[::-1]])[::-1]
240+
b_len = len(bc_str) - len(c_str)
241+
b_str = bc_str[:b_len]
242+
cb_str = c_str + b_str
243+
reversed_equation = ellided_ac_str + "," + cb_str + "->" + ellided_ab_str
194244
return _reshape_einsum_inputs(output_tensor, reversed_equation)
245+
246+
247+
def _get_einsum_bias_adjoint_reduction_axes(
248+
equation: str,
249+
bias_axes: str,
250+
einsum_rank: int,
251+
) -> list[int]:
252+
"""Computes axes related to the per-example adjoint of the einsum bias op.
253+
254+
To describe the output of this computation, first recall that for each
255+
example the `EinsumDense` layer performs the following transformation:
256+
```
257+
F(W, bias | X) = Einsum(W, X) + Q(bias)
258+
```
259+
where `W` is a tensor of trainable variables, `bias` is a tensor of rank
260+
`len(bias_axes)`, `X` is a batch of inputs, and `Q` is a linear broadcast
261+
operator that roughly corresponds to `Q(bias) ~= tf.broadcast_to(bias, S)` for
262+
`S := tf.shape(Einsum(W, X))`.
263+
264+
It is straightforward to show that the per-example adjoint of `Q` is given by
265+
`Q'(Y) := tf.reduce_sum(Y, axes=R)` where `R` contains the broadcasting
266+
indices. This function returns `R` as an unordered list of `int`s.
267+
268+
Assumptions:
269+
270+
A1. `equation` is one of the following forms:
271+
C1. `ab,bc->ac`
272+
C2. `...ab,bc->...ac`
273+
C3. `ab...,bc->ac...`
274+
275+
A2. The first character in the substring `a` (or `...a` in C2)
276+
in assumption A1 corresponds to the batch dimension.
277+
278+
A3. The characters in `bias_axes` must be subset of the non-batch dimension
279+
characters in the substring `ac` (or `...ac` in C2) in
280+
assumption A1.
281+
282+
A4. `einsum_rank` is the length of the substring `ac` (or `...ac` in C2) in
283+
assumption A1. This includes the batch dimension.
284+
285+
Examples:
286+
287+
1. equation = 'ab,bc->ac', bias_axes = 'c', einsum_rank = 2 -> []
288+
2. equation = 'ab,bce->ace', bias_axes = 'ce', einsum_rank = 3, -> []
289+
3. equation = 'ab,bce->ace', bias_axes = 'c', einsum_rank = 3, -> [2]
290+
4. equation = 'ab,bce->ace', bias_axes = 'e', einsum_rank = 3, -> [1]
291+
5. equation = 'ab,bced->aced', bias_axes = 'ced', einsum_rank = 4 -> []
292+
6. equation = 'ab,bced->aced', bias_axes = 'ce', einsum_rank = 4, -> [3],
293+
7. equation = 'ab,bced->aced', bias_axes = 'c', einsum_rank = 4, -> [2, 3]
294+
8. equation = '...ab,bce->...ace', bias_axes = 'c', einsum_rank = 4
295+
-> [1, 3]
296+
9. equation = '...ab,bce->...ace', bias_axes = 'c', einsum_rank = 10
297+
-> [1, 2, 3, 4, 5, 6, 7, 9]
298+
10. equation = 'ab...,bce->ace...', bias_axes = 'e', einsum_rank = 4
299+
-> [1, 3]
300+
301+
Args:
302+
equation: The einsum equation `string`.
303+
bias_axes: A substring of the output part of `equation` specifying which
304+
axes a bias `tf.Tensor` is added to.
305+
einsum_rank: The rank of the tensor that the per-example adjoint operator is
306+
being applied to.
307+
308+
Returns:
309+
A list of `int` containing axes in the `input` corresponding to
310+
`input_rank`. Each `int` is at most `input_rank-1` and excludes zero.
311+
312+
Raises:
313+
ValueError: If `equation` is not a valid einsum equation in the context of
314+
the `tf.keras.layers.EinsumDense` layer.
315+
"""
316+
reduction_axes = []
317+
bias_char_set = set(bias_axes)
318+
equation_type, (_, _, ac_str) = _parse_einsum_equation(equation)
319+
# Do not allow the bias axes to be the batch axis, since we want the adjoint
320+
# of the bias broadcast op to apply the same operation to all examples in a
321+
# batch.
322+
if equation_type != EquationType.LEFT_ELLIPSES and ac_str[0] in bias_axes:
323+
raise ValueError(f"Bias axis '{bias_axes}' cannot also be the batch axis.")
324+
# If `equation` of the form `...ab,bc->...ac`, i.e., case C2, we do a
325+
# right to left traversal; the other cases do a left to right traversal.
326+
input_indices = range(einsum_rank)
327+
traversal_zip = (
328+
itertools.zip_longest(reversed(input_indices), reversed(ac_str))
329+
if equation_type == EquationType.LEFT_ELLIPSES
330+
else itertools.zip_longest(input_indices, ac_str)
331+
)
332+
# Traverse the output part of `equation` and add an index to the output if
333+
# the corresponding `char` in the `ac` part is NOT in `bias_axes` and the
334+
# index is not zero (batch dimension). Add all indices except index zero in
335+
# the `...` part of the output substring (if present).
336+
for idx, output_char in traversal_zip:
337+
# Exclude the batch dimension (idx == 0), since we want the per-example
338+
# adjoint.
339+
if idx != 0:
340+
if output_char is not None and bias_char_set:
341+
if output_char not in bias_char_set:
342+
reduction_axes.append(idx)
343+
else:
344+
bias_char_set.remove(output_char)
345+
else:
346+
reduction_axes.append(idx)
347+
return reduction_axes

tensorflow_privacy/privacy/fast_gradient_clipping/registry_functions/einsum_utils_test.py

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ def test_is_batch_of_vectors(self, experiment_params):
4545
computed_result = einsum_utils._is_batch_of_vectors(t)
4646
self.assertEqual(computed_result, true_result)
4747

48+
@parameterized.product(
49+
experiment_params=[
50+
(('ab', 'bc', 'ac'), True),
51+
(('ab', 'a', 'b'), False),
52+
(('ab', 'ca', 'bc'), False),
53+
(('b', 'bc', 'c'), True),
54+
(('ab', 'bc', 'bc'), False),
55+
(('abc', 'cde', 'abde'), True),
56+
]
57+
)
58+
def test_is_valid_einsum_equation(self, experiment_params):
59+
inputs, true_result = experiment_params
60+
computed_result = einsum_utils._is_valid_einsum_equation(*inputs)
61+
self.assertEqual(computed_result, true_result)
62+
4863
@parameterized.product(
4964
experiment_params=[
5065
(
@@ -66,7 +81,7 @@ def test_is_batch_of_vectors(self, experiment_params):
6681
)
6782
def test_parse_einsum_equation(self, experiment_params):
6883
equation, true_eqn_type, true_groups = experiment_params
69-
(computed_eqn_type, computed_groups) = einsum_utils._parse_einsum_equation(
84+
computed_eqn_type, computed_groups = einsum_utils._parse_einsum_equation(
7085
equation
7186
)
7287
self.assertEqual(computed_eqn_type, true_eqn_type)
@@ -98,7 +113,7 @@ def test_parse_einsum_equation(self, experiment_params):
98113
]
99114
)
100115
def test_reshape_einsum_inputs(self, experiment_params):
101-
(equation, input_shape, true_permutations, true_parsed_shape) = (
116+
equation, input_shape, true_permutations, true_parsed_shape = (
102117
experiment_params
103118
)
104119
num_entries = int(np.prod(input_shape))
@@ -141,7 +156,7 @@ def test_reshape_einsum_inputs(self, experiment_params):
141156
]
142157
)
143158
def test_reshape_einsum_outputs(self, experiment_params):
144-
(equation, output_shape, true_permutations, true_parsed_shape) = (
159+
equation, output_shape, true_permutations, true_parsed_shape = (
145160
experiment_params
146161
)
147162
num_entries = int(np.prod(output_shape))
@@ -158,6 +173,77 @@ def test_reshape_einsum_outputs(self, experiment_params):
158173
true_parsed_tensor = tf.reshape(true_parsed_tensor, true_parsed_shape)
159174
self.assertAllEqual(computed_parsed_tensor, true_parsed_tensor)
160175

176+
@parameterized.product(
177+
experiment_params=[
178+
# einsum_utils.EquationType.NO_ELLIPSES
179+
('ab,bc->ac', 'c', 2, []),
180+
('ab,bce->ace', 'ce', 3, []),
181+
('ab,bce->ace', 'ec', 3, []),
182+
('ab,bce->ace', 'c', 3, [2]),
183+
('ab,bce->ace', 'e', 3, [1]),
184+
('ab,bced->aced', 'ced', 4, []),
185+
('ab,bced->aced', 'edc', 4, []),
186+
('ab,bced->aced', 'ce', 4, [3]),
187+
('ab,bced->aced', 'ec', 4, [3]),
188+
('ab,bced->aced', 'cd', 4, [2]),
189+
('ab,bced->aced', 'ed', 4, [1]),
190+
('ab,bced->aced', 'c', 4, [2, 3]),
191+
('ab,bced->aced', 'e', 4, [1, 3]),
192+
('ab,bced->aced', 'd', 4, [1, 2]),
193+
# einsum_utils.EquationType.LEFT_ELLIPSES
194+
('...b,bc->...c', 'c', 2, []),
195+
('...b,bce->...ce', 'c', 3, [2]),
196+
('...b,bce->...ce', 'e', 3, [1]),
197+
('...ab,bc->...ac', 'c', 3, [1]),
198+
('...ab,bce->...ace', 'ac', 4, [3]),
199+
('...ab,bce->...ace', 'ae', 4, [2]),
200+
('...ab,bce->...ace', 'ce', 4, [1]),
201+
('...ab,bce->...ace', 'ec', 4, [1]),
202+
('...ab,bce->...ace', 'a', 4, [2, 3]),
203+
('...ab,bce->...ace', 'c', 4, [1, 3]),
204+
('...ab,bce->...ace', 'e', 4, [1, 2]),
205+
('...ab,bce->...ace', 'c', 5, [1, 2, 4]),
206+
('...ab,bce->...ace', 'c', 10, [1, 2, 3, 4, 5, 6, 7, 9]),
207+
# einsum_utils.EquationType.RIGHT_ELLIPSES
208+
('ab...,bc->ac...', 'c', 3, [2]),
209+
('ab...,bce->ace...', 'ce', 4, [3]),
210+
('ab...,bce->ace...', 'ec', 4, [3]),
211+
('ab...,bce->ace...', 'c', 4, [2, 3]),
212+
('ab...,bce->ace...', 'e', 4, [1, 3]),
213+
]
214+
)
215+
def test_get_einsum_bias_adjoint_reduction_axes(self, experiment_params):
216+
equation, bias_axes, einsum_rank, true_reduction_axes = experiment_params
217+
computed_reduction_axes = (
218+
einsum_utils._get_einsum_bias_adjoint_reduction_axes(
219+
equation, bias_axes, einsum_rank
220+
)
221+
)
222+
computed_reduction_axes.sort()
223+
true_reduction_axes.sort()
224+
self.assertAllEqual(computed_reduction_axes, true_reduction_axes)
225+
226+
@parameterized.product(
227+
experiment_params=[
228+
# einsum_utils.EquationType.NO_ELLIPSES
229+
('ab,bc->ac', 'a', 2),
230+
# einsum_utils.EquationType.RIGHT_ELLIPSES
231+
('ab...,bc->ac...', 'a', 3),
232+
('ab...,bc->ac...', 'a', 4),
233+
('ab...,bcde->acde...', 'acd', 4),
234+
]
235+
)
236+
def test_bias_axis_eq_batch_axis_throws_error(self, experiment_params):
237+
equation, bias_axes, einsum_rank = experiment_params
238+
with self.assertRaises(ValueError) as context:
239+
einsum_utils._get_einsum_bias_adjoint_reduction_axes(
240+
equation, bias_axes, einsum_rank
241+
)
242+
self.assertEqual(
243+
f"Bias axis '{bias_axes}' cannot also be the batch axis.",
244+
str(context.exception),
245+
)
246+
161247

162248
if __name__ == '__main__':
163249
tf.test.main()

0 commit comments

Comments
 (0)