Skip to content

Commit a8c19a2

Browse files
committed
update SliceNdLayer
1 parent e729446 commit a8c19a2

1 file changed

Lines changed: 190 additions & 19 deletions

File tree

returnn/tf/layers/basic.py

Lines changed: 190 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
11791350
class GatherNdLayer(_ConcatInputLayer):
11801351
"""
11811352
Warning: This layer is deprecated, use the more general :class:`GatherLayer` instead.

0 commit comments

Comments
 (0)