@@ -854,52 +854,79 @@ class SliceNdLayer(_ConcatInputLayer):
854854 e.g. ``x[start:start + size]``.
855855 This layers allows a different start slice point for each batch,
856856 in contrast to :class:`SliceLayer`, and the start is variable.
857- See also :class:`GatherNdLayer`.
857+ In case start has more than 1 axis we loop over them.
858+ E.g. if start: [B,T] we loop over T and slice for each batch normally.
859+ See also :class:`GatherLayer`.
858860 :class:`PrefixInTimeLayer` can recover the original shape (by zero-padding).
859861 """
860862 layer_class = "slice_nd"
861863 recurrent = True
862864
863865 def __init__ (self , start , size , min_size = None , ** kwargs ):
864866 """
865- :param LayerBase start:
867+ We expect the input to have at least one more axis than start and the rest of the axis in front are the same.
868+ In case the input has no extra time axis compared to start, we assume slice_nd is pulled out of a rec layer
869+ but the input has stood the same.
870+
871+ :other LayerBase input_data: shape [B,T0,..,Tn,D] or [B,T0,..,Tn-1,D]
872+ :param LayerBase start: shape [B,T0,..,Tn-1]
873+ :other LayerBase output: shape [B,T0,..,Tn',D]
866874 :param int|None size: if None, it uses the max possible size, and it becomes a dynamic axis
867875 :param int|None min_size: if size is None, but we want to have a min-size, set this
868876 """
869877 super (SliceNdLayer , self ).__init__ (** kwargs )
870- from returnn .tf .util .basic import slice_nd , where_bc , expand_multiple_dims , DimensionTag
878+ from returnn .tf .util .basic import slice_nd , DimensionTag
879+ assert start .output .have_batch_axis () and self .input_data .have_batch_axis ()
880+ self .start = start
881+
871882 x = self .input_data .copy_as_batch_major ()
872- assert x .time_dim_axis == 1 , "currently only time-axis==1 supported"
883+ start = start .output .copy_as_batch_major ()
884+
885+ # make sure axis of start are in input
886+ is_equal_opts = dict (ignore_feature_dim = True , allow_same_spatial_dim = True , broadcast_matches = True )
887+ for start_axis in range (start .batch_ndim ):
888+ assert x .get_dim_tag (start_axis ).is_equal (start .get_dim_tag (start_axis ), ** is_equal_opts )
889+
890+ # Handle the case when layer is pulled out of rec loop but the input hasn't change
891+ if self .optimized_out_of_loop_and_unchanged_input (x , start ):
892+ # add an axis after the last start axis and tile the input Tn-1 times: [B,T0,..,Tn-1,D] -> [B,T0,..,Tn-1,Tn-1,D]
893+ tag = start .get_dim_tag (- 1 )
894+ x = x .copy_add_dim_by_tag (tag , True , start .batch_ndim ) # tiles the input
895+
896+ start = start .get_placeholder_as_batch_major ()
873897 seq_lens = x .get_sequence_lengths () if x .is_time_axis_dynamic () else None
874- self .start = start
875- assert start .output .have_batch_axis () and start .output .batch_shape == (None ,)
876- start = start .output .get_placeholder_as_batch_major ()
898+ slice_axis = len (list (start .shape )) - 1 # slice_axis w/o batch
877899 if size is None :
878900 if seq_lens is None :
879- size = tf .maximum (tf .reduce_max (x .batch_shape [1 ] - start ), 0 )
901+ size = tf .maximum (tf .reduce_max (x .batch_shape [slice_axis ] - start ), 0 )
880902 else :
881903 size = tf .maximum (tf .reduce_max (seq_lens - start ), 0 )
882904 if min_size is not None :
883905 size = tf .maximum (size , min_size )
884906 self .size = size
885- start = tf .expand_dims (start , axis = 1 ) # (B, T)
886907 slices = slice_nd (x .placeholder , start = tf .cast (start , tf .int32 ), size = size ) # (B,size, ...)
887- if seq_lens is not None :
888- mask = tf .greater_equal (tf .range (size )[None , :] + start , seq_lens [:, None ]) # (B,T)
889- mask = expand_multiple_dims (mask , list (range (2 , x .batch_ndim )))
890- slices = where_bc (mask , tf .zeros_like (slices ), slices )
908+
891909 self .output .size_placeholder = x .size_placeholder .copy ()
892910 if isinstance (size , tf .Tensor ):
893- self .output .size_placeholder [0 ] = tf . maximum ( seq_lens - tf . reshape ( start , tf . shape ( seq_lens )), 0 )
911+ self .output .size_placeholder [slice_axis ] = size
894912 tag = DimensionTag (
895913 description = "sliced-time:%s" % self .get_absolute_name (),
896914 kind = DimensionTag .Types .Spatial )
897- tag .set_tag_on_size_tensor (self .output .size_placeholder [0 ])
915+ tag .set_tag_on_size_tensor (self .output .size_placeholder [slice_axis ])
898916 else :
899917 assert isinstance (size , int )
900- self .output .size_placeholder .pop (0 , None ) # static time axis
918+ self .output .size_placeholder .pop (slice_axis , None ) # static time axis
919+
901920 self .output .placeholder = slices
902921
922+ @classmethod
923+ def optimized_out_of_loop_and_unchanged_input (cls , input_data , start ):
924+ """
925+ :rtype: bool
926+ The idea is to check that the axis after the last common axis is a feature axis instead of spatial.
927+ """
928+ return input_data .get_dim_tag (start .batch_ndim ) == input_data .get_dim_tag (input_data .get_feature_batch_axes ()[0 ])
929+
903930 def get_dep_layers (self ):
904931 """
905932 :rtype: list[LayerBase]
@@ -916,10 +943,17 @@ def get_out_data_from_opts(cls, name, sources=(), start=None, size=None, **kwarg
916943 :rtype: Data
917944 """
918945 input_data = get_concat_sources_data_template (sources ).copy_as_batch_major ()
919- if start :
920- input_data .beam = SearchBeam .get_combined_beam (input_data .beam , start .output .beam )
946+ start = start .output .copy_as_batch_major ()
947+ input_data .beam = SearchBeam .get_combined_beam (input_data .beam , start .beam )
948+
921949 in_shape = list (input_data .shape )
922- shape = [size ] + in_shape [1 :] # (B, size, ...) (w/o batch)
950+ start_shape = list (start .shape )
951+ slice_axis = len (start_shape ) + 1 # w/o B
952+
953+ if cls .optimized_out_of_loop_and_unchanged_input (input_data , start ):
954+ slice_axis -= 1
955+ shape = start_shape [:] + [size ] + in_shape [slice_axis :] # (B, T1, .., Tn-1, size, ...) (w/o batch)
956+
923957 out_type = input_data .get_kwargs ()
924958 out_type ["name" ] = "%s_output" % name
925959 out_type ["shape" ] = shape
@@ -1176,6 +1210,143 @@ def transform_config_dict(cls, d, network, get_layer):
11761210 d ["position" ] = get_layer (d ["position" ])
11771211
11781212
1213+ class SliceNdLayer2 (_ConcatInputLayer ):
1214+ """
1215+ This takes out a slice-range from some axis,
1216+ e.g. ``x[start:start + size]``.
1217+ This layers allows a different start slice point for each batch,
1218+ in contrast to :class:`SliceLayer`, and the start is variable.
1219+ See also :class:`GatherNdLayer`.
1220+ :class:`PrefixInTimeLayer` can recover the original shape (by zero-padding).
1221+
1222+ Gathers slices on a specified axis from the input layer using indices from a ``position`` layer.
1223+ If the input is a layer of the shape ``[B,T1,T2,F1]``, and start of shape ``[B,T1]``, and size is S,
1224+ this will yield output of the shape ``[B,T1,S,F1]`` where
1225+
1226+ ``output[b,t1,0:S,f1] = input[b,position[b,t1],T2:T2+S,f1]``
1227+
1228+ (if ``D`` is the axis to gather from).
1229+ In general, all shared axes of the input and the positions will be considered as batch-axes.
1230+
1231+ The ``position`` argument can also be an ``int``.
1232+ In this case, this simply gives ``input[position]`` one the specified ``axis``.
1233+
1234+ It's basically a wrapper around ``tf.gather``.
1235+ It provides the same functionality as the deprecated ``GatherNdLayer``, but is more generic.
1236+ See also :class:`GatherNdLayer`.
1237+ """
1238+ layer_class = "slice_nd2"
1239+ recurrent = True
1240+
1241+ def __init__ (self , start , size , min_size = None , ** kwargs ):
1242+ """
1243+ We expect the input to have at least one more axis than start and the rest of the axis in front are the same.
1244+ In case the input has no extra time axis compared to start, we assume slice_nd is pulled out of a rec layer
1245+ but the input has stood the same.
1246+
1247+ :other LayerBase input_data: shape [B,T0,..,Tn,D] or [B,T0,..,Tn-1,D]
1248+ :param LayerBase start: shape [B,T0,..,Tn-1]
1249+ :other LayerBase output: shape [B,T0,..,Tn',D]
1250+ :param int|None size: if None, it uses the max possible size, and it becomes a dynamic axis
1251+ :param int|None min_size: if size is None, but we want to have a min-size, set this
1252+ """
1253+ super (SliceNdLayer2 , self ).__init__ (** kwargs )
1254+ from returnn .tf .util .basic import slice_nd , DimensionTag
1255+ assert start .output .have_batch_axis () and self .input_data .have_batch_axis ()
1256+ self .start = start
1257+
1258+ input_data = self .input_data .copy_as_batch_major ()
1259+ start = start .output .copy_as_batch_major ()
1260+
1261+ # make sure axis of start are in input
1262+ is_equal_opts = dict (ignore_feature_dim = True , allow_same_spatial_dim = True , broadcast_matches = True )
1263+ for start_axis in range (start .batch_ndim ):
1264+ assert input_data .get_dim_tag (start_axis ).is_equal (start .get_dim_tag (start_axis ), ** is_equal_opts )
1265+
1266+ # Handle the case when layer is pulled out of rec loop but the input hasn't change
1267+ if self .optimized_out_of_loop_and_unchanged_input (input_data , start ):
1268+ # add an axis after the last start axis and tile the input Tn-1 times: [B,T0,..,Tn-1,D] -> [B,T0,..,Tn-1,Tn-1,D]
1269+ tag = start .get_dim_tag (- 1 )
1270+ input_data = input_data .copy_add_dim_by_tag (tag , True , start .batch_ndim ) # tiles the input
1271+
1272+ start = start .get_placeholder_as_batch_major ()
1273+ seq_lens = input_data .get_sequence_lengths () if input_data .is_time_axis_dynamic () else None
1274+ slice_axis = len (list (start .shape )) - 1 # slice_axis w/o batch
1275+ if size is None :
1276+ if seq_lens is None :
1277+ size = tf .maximum (tf .reduce_max (input_data .batch_shape [slice_axis ] - start ), 0 )
1278+ else :
1279+ size = tf .maximum (tf .reduce_max (seq_lens - start ), 0 )
1280+ if min_size is not None :
1281+ size = tf .maximum (size , min_size )
1282+ self .size = size
1283+
1284+ self .output .size_placeholder = input_data .size_placeholder .copy ()
1285+ if isinstance (size , tf .Tensor ): # size was None in the beginning
1286+ self .output .size_placeholder [slice_axis ] = size
1287+ tag = DimensionTag (
1288+ description = "sliced-time:%s" % self .get_absolute_name (),
1289+ kind = DimensionTag .Types .Spatial )
1290+ tag .set_tag_on_size_tensor (self .output .size_placeholder [slice_axis ])
1291+ else :
1292+ assert isinstance (size , int )
1293+ self .output .size_placeholder .pop (slice_axis , None ) # static time axis
1294+
1295+ slices = slice_nd (input_data .placeholder , start = tf .cast (start , tf .int32 ), size = size ) # (B,size, ...)
1296+ self .output .placeholder = slices
1297+
1298+ @classmethod
1299+ def optimized_out_of_loop_and_unchanged_input (cls , input_data , start ):
1300+ """
1301+ :rtype: bool
1302+ The idea is to check that the axis after the last common axis is a feature axis instead of spatial.
1303+ """
1304+ return input_data .get_dim_tag (start .batch_ndim ) == input_data .get_dim_tag (input_data .get_feature_batch_axes ()[0 ])
1305+
1306+ def get_dep_layers (self ):
1307+ """
1308+ :rtype: list[LayerBase]
1309+ """
1310+ return super (SliceNdLayer2 , self ).get_dep_layers () + [self .start ]
1311+
1312+ @classmethod
1313+ def get_out_data_from_opts (cls , name , sources , start , size = None , ** kwargs ):
1314+ """
1315+ :param str name:
1316+ :param list[LayerBase] sources:
1317+ :param LayerBase|None start:
1318+ :param int|None size:
1319+ :rtype: Data
1320+ """
1321+ input_data = get_concat_sources_data_template (sources ).copy_as_batch_major ()
1322+ start = start .output .copy_as_batch_major ()
1323+ input_data .beam = SearchBeam .get_combined_beam (input_data .beam , start .beam )
1324+
1325+ in_shape = list (input_data .shape )
1326+ start_shape = list (start .shape )
1327+ slice_axis = len (start_shape ) + 1 # w/o B
1328+
1329+ if cls .optimized_out_of_loop_and_unchanged_input (input_data , start ):
1330+ slice_axis -= 1
1331+ shape = start_shape [:] + [size ] + in_shape [slice_axis :] # (B, T1, .., Tn-1, size, ...) (w/o batch)
1332+
1333+ out_type = input_data .get_kwargs ()
1334+ out_type ["name" ] = "%s_output" % name
1335+ out_type ["shape" ] = shape
1336+ out_type ["batch_dim_axis" ] = 0
1337+ return Data (** out_type )
1338+
1339+ @classmethod
1340+ def transform_config_dict (cls , d , network , get_layer ):
1341+ """
1342+ :param dict[str] d:
1343+ :param returnn.tf.network.TFNetwork network:
1344+ :param get_layer:
1345+ """
1346+ super (SliceNdLayer2 , cls ).transform_config_dict (d , network = network , get_layer = get_layer )
1347+ d ["start" ] = get_layer (d ["start" ])
1348+
1349+
11791350class GatherNdLayer (_ConcatInputLayer ):
11801351 """
11811352 Warning: This layer is deprecated, use the more general :class:`GatherLayer` instead.
0 commit comments