1515
1616import enum
1717import itertools
18+ import os
1819import re
1920
2021import 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+
4071def _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
82124def _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
0 commit comments