Skip to content

Commit 5eb93b5

Browse files
SliceNdLayer now uses GatherLayer to get the slices (#635)
1 parent c246f26 commit 5eb93b5

2 files changed

Lines changed: 149 additions & 35 deletions

File tree

returnn/tf/layers/basic.py

Lines changed: 101 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -842,9 +842,13 @@ def get_out_data_from_opts(
842842

843843
class SliceNdLayer(_ConcatInputLayer):
844844
"""
845-
This takes out a slice-range from some axis,
845+
This takes out a slice-range from the time axis,
846846
e.g. ``x[start:start + size]``.
847-
This layers allows a different start slice point for each batch,
847+
If the input is of shape (B,T,F) and start is of shape (B,),
848+
then the output will be of shape (B,size,F).
849+
If the input is of shape (B,T,F) and start is of shape (B,T),
850+
then the output will be of shape (B,T,size,F).
851+
This layer allows a different start slice point for each batch,
848852
in contrast to :class:`SliceLayer`, and the start is variable.
849853
See also :class:`GatherNdLayer`.
850854
:class:`PrefixInTimeLayer` can recover the original shape (by zero-padding).
@@ -854,44 +858,93 @@ class SliceNdLayer(_ConcatInputLayer):
854858

855859
def __init__(self, start, size, min_size=None, **kwargs):
856860
"""
857-
:param LayerBase start:
861+
:param LayerBase start: (B,...)
858862
:param int|None size: if None, it uses the max possible size, and it becomes a dynamic axis
859-
:param int|None min_size: if size is None, but we want to have a min-size, set this
863+
:param int|None min_size: if size is None, but we want to have a min-size
860864
"""
861865
super(SliceNdLayer, self).__init__(**kwargs)
862-
from returnn.tf.util.basic import slice_nd, where_bc, expand_multiple_dims, DimensionTag
866+
from returnn.tf.util.basic import where_bc, expand_multiple_dims
863867
x = self.input_data.copy_as_batch_major()
864-
assert x.time_dim_axis == 1, "currently only time-axis==1 supported"
865-
seq_lens = x.get_sequence_lengths() if x.is_time_axis_dynamic() else None
868+
seq_lens = x.get_sequence_lengths() if x.is_time_axis_dynamic() else None # (B,) or None
866869
self.start = start
867-
assert start.output.have_batch_axis() and start.output.batch_shape == (None,)
868-
start = start.output.get_placeholder_as_batch_major()
870+
start_data = start.output.copy_as_batch_major() # e.g. (B,) or (B,T)
871+
start_t = start_data.placeholder
869872
if size is None:
873+
if min_size is None:
874+
min_size = 0
870875
if seq_lens is None:
871-
size = tf.maximum(tf.reduce_max(x.batch_shape[1] - start), 0)
876+
assert isinstance(x.batch_shape[x.time_dim_axis], int)
877+
size = tf.maximum(tf.reduce_max(x.batch_shape[x.time_dim_axis] - start_t), min_size) # scalar
872878
else:
873-
size = tf.maximum(tf.reduce_max(seq_lens - start), 0)
874-
if min_size is not None:
875-
size = tf.maximum(size, min_size)
876-
self.size = size
877-
start = tf.expand_dims(start, axis=1) # (B, T)
878-
slices = slice_nd(x.placeholder, start=tf.cast(start, tf.int32), size=size) # (B,size, ...)
879+
# make seq_lens compatible with start_t
880+
seq_lens = expand_multiple_dims( # e.g. (B,) or (B,1)
881+
x=seq_lens,
882+
axes=[-1] * (len(start_t.shape) - len(seq_lens.shape)))
883+
size = tf.maximum(tf.reduce_max(seq_lens - start_t), min_size) # scalar
884+
# for each start index in start_data, we want to gather a slice
885+
# therefore, the output's first axes are the same as the ones from start_data
886+
# and the next axis will therefore be the slice axis
887+
slice_tag = self.output.dim_tags[start_data.batch_ndim]
888+
assert slice_tag.description.startswith("sliced-time:")
889+
if not isinstance(size, int):
890+
# in this case, size is not known before runtime and becomes dynamic and we need to set dyn_size
891+
if seq_lens is None:
892+
dyn_size = tf.maximum(x.batch_shape[x.time_dim_axis] - start_t, min_size) # (B,) or (B,T)
893+
else:
894+
dyn_size = tf.maximum(seq_lens - start_t, min_size) # (B,) or (B,T)
895+
dyn_size_ext = Data(
896+
name=("%s:dyn_size" % slice_tag.description),
897+
dtype=Data.size_dtype,
898+
placeholder=dyn_size,
899+
dim_tags=start_data.dim_tags,
900+
batch=slice_tag.batch,
901+
beam=slice_tag.batch.beam if slice_tag.batch else self.output.beam,
902+
control_flow_ctx=slice_tag.control_flow_ctx)
903+
slice_tag.dyn_size_ext = dyn_size_ext
904+
gather_positions_data = start_data.copy_template(name="%s_gather_positions" % self.name)
905+
gather_positions_data = gather_positions_data.copy_add_dim_by_tag(
906+
slice_tag,
907+
unbroadcast=True,
908+
axis=start_data.batch_ndim)
909+
# [start+0, start+1, ...]
910+
gather_positions = tf.expand_dims(start_t, -1) + tf.range(0, size) # e.g. (B, size) or (B, T, size)
879911
if seq_lens is not None:
880-
mask = tf.greater_equal(tf.range(size)[None, :] + start, seq_lens[:, None]) # (B,T)
881-
mask = expand_multiple_dims(mask, list(range(2, x.batch_ndim)))
882-
slices = where_bc(mask, tf.zeros_like(slices), slices)
883-
size_placeholder = x.size_placeholder.copy()
884-
if isinstance(size, tf.Tensor):
885-
size_placeholder[0] = tf.maximum(seq_lens - tf.reshape(start, tf.shape(seq_lens)), 0)
886-
tag = DimensionTag(
887-
description="sliced-time:%s" % self.get_absolute_name(),
888-
kind=DimensionTag.Types.Spatial, batch=self.output.batch)
889-
tag.set_tag_on_size_tensor(size_placeholder[0])
912+
# broadcast from (B,) to the shape of the indices
913+
seq_lens = expand_multiple_dims( # e.g. (B,1) or (B,1,1)
914+
x=seq_lens,
915+
axes=[-1] * (len(gather_positions.shape) - len(seq_lens.shape)))
916+
pad_mask = tf.logical_or( # shape like gather_positions
917+
tf.greater(gather_positions, seq_lens - 1),
918+
tf.less(gather_positions, 0))
919+
gather_positions = tf.clip_by_value(gather_positions, 0, seq_lens - 1)
890920
else:
891-
assert isinstance(size, int)
892-
size_placeholder.pop(0, None) # static time axis
893-
self.output.size_placeholder = size_placeholder
894-
self.output.placeholder = slices
921+
pad_mask = tf.logical_or( # shape like gather_positions
922+
tf.greater(gather_positions, x.batch_shape[1] - 1),
923+
tf.less(gather_positions, 0))
924+
gather_positions = tf.clip_by_value(gather_positions, 0, x.batch_shape[1] - 1)
925+
gather_positions_data.placeholder = gather_positions
926+
position = InternalLayer(
927+
network=self.network,
928+
name="%s_internal" % gather_positions_data.name,
929+
output=gather_positions_data)
930+
gather_layer = GatherLayer(
931+
name="%s_gather" % self.name,
932+
network=self.network,
933+
output=self.output,
934+
sources=self.sources,
935+
position=position,
936+
axis=x.get_time_dim_tag())
937+
placeholder = gather_layer.output.placeholder
938+
# In principle, the padded frames are being ignored
939+
# (unless get_padding_info_dict_ref et al are used).
940+
# However, you can still end up with gradients for them
941+
# in unexpected ways.
942+
# Due to our gather implementation,
943+
# the gradient flow would go into wrong frames
944+
# and might lead to unexpected behavior.
945+
# So to be on the safe side, we do the masking here.
946+
pad_mask = expand_multiple_dims(pad_mask, [-1] * (len(placeholder.shape) - len(pad_mask.shape)))
947+
self.output.placeholder = where_bc(pad_mask, tf.zeros_like(placeholder), placeholder)
895948

896949
def get_dep_layers(self):
897950
"""
@@ -909,11 +962,24 @@ def get_out_data_from_opts(cls, name, sources=(), start=None, size=None, **kwarg
909962
:rtype: Data
910963
"""
911964
from ..util.data import DimensionTag
912-
input_data = get_concat_sources_data_template(sources).copy_as_batch_spatial_major()
913-
if start:
914-
input_data.beam = SearchBeam.get_combined_beam(input_data.beam, start.output.beam)
915-
new_dim_tag = DimensionTag(kind=DimensionTag.Types.Spatial, description="%s:slice_nd" % name, dimension=size)
916-
return input_data.copy_template_replace_dim_tag(axis=1, new_dim_tag=new_dim_tag, name="%s_output" % name)
965+
start_data = start.output.copy_as_batch_major()
966+
input_data = sources[0].output.copy_as_batch_major()
967+
gather_positions_data = start_data.copy_template(name="%s_gather_positions" % name)
968+
# size might be None here in which case we set the dyn_size in __init__
969+
tag = DimensionTag(
970+
kind=DimensionTag.Types.Spatial,
971+
description="sliced-time:%s" % name,
972+
dimension=size)
973+
gather_positions_data = gather_positions_data.copy_add_dim_by_tag(tag, unbroadcast=True, axis=start_data.batch_ndim)
974+
position = InternalLayer(
975+
network=sources[0].network,
976+
name="%s_internal" % gather_positions_data.name,
977+
output=gather_positions_data)
978+
return GatherLayer.get_out_data_from_opts(
979+
name="%s_gather" % name,
980+
sources=sources,
981+
position=position,
982+
axis=input_data.get_time_dim_tag())
917983

918984
@classmethod
919985
def transform_config_dict(cls, d, network, get_layer):

tests/test_TFNetworkLayer.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2662,6 +2662,54 @@ def test_SliceNdLayer_dyn_size():
26622662
numpy.testing.assert_equal(orig_seq[t], out[b, t])
26632663

26642664

2665+
def test_SliceNdLayer_multidimensional_start():
2666+
with make_scope() as session:
2667+
n_out = 5
2668+
n_batch = 3
2669+
max_seq_len = 10
2670+
config = Config({
2671+
"debug_print_layer_output_template": True,
2672+
"extern_data": {
2673+
"data": {"dim": n_out},
2674+
"classes": {"dim": n_out, "sparse": True}
2675+
}})
2676+
net = TFNetwork(config=config, train_flag=True)
2677+
net.construct_from_dict({
2678+
"output": {
2679+
"class": "rec", "from": "data:data", "unit": {
2680+
"start": {"class": "copy", "from": "prev:choice"},
2681+
"slices": {"class": "slice_nd", "from": "base:data:data", "start": "start", "size": None},
2682+
"output": {"class": "reduce", "from": "slices", "mode": "max", "axes": "dyn:-1"},
2683+
"prob": {"class": "softmax", "from": "data:source", "target": "classes", "loss": "ce"},
2684+
'choice': {
2685+
'class': 'choice', 'target': "classes", 'beam_size': 3, 'from': "prob", "input_type": "prob",
2686+
"initial_output": 0,}}}})
2687+
session.run(tf_compat.v1.global_variables_initializer())
2688+
output_layer = net.layers["output"]
2689+
starts = output_layer.cell.output_layers_net.layers["start"].output.get_placeholder_as_batch_major()
2690+
segments = output_layer.cell.output_layers_net.layers["slices"].output.get_placeholder_as_batch_major()
2691+
feed = make_feed_dict(net.extern_data.data.values(), n_batch=n_batch, n_time=max_seq_len, same_time=True)
2692+
starts = session.run(starts, feed_dict=feed)
2693+
segments = session.run(segments, feed_dict=feed)
2694+
seq_lens = feed[net.extern_data.data["data"].size_placeholder[0]]
2695+
input_data = feed[net.extern_data.data["data"].placeholder]
2696+
max_size = numpy.amax(seq_lens[:, None] - starts)
2697+
max_size = max(max_size, 0)
2698+
assert segments.shape == (n_batch, max_seq_len, max_size, n_out)
2699+
for b in range(n_batch):
2700+
for t in range(max_seq_len):
2701+
s = starts[b, t]
2702+
orig_seq = input_data[b, s:]
2703+
if len(orig_seq) < max_size:
2704+
orig_seq = numpy.pad(orig_seq, [(0, max_size - len(orig_seq)), (0, 0)], "constant")
2705+
elif len(orig_seq) > max_size:
2706+
orig_seq = orig_seq[:max_size]
2707+
assert orig_seq.shape == (max_size, n_out)
2708+
orig_seq = numpy.where((numpy.arange(s, s + max_size) >= seq_lens[b])[:, None], 0.0, orig_seq)
2709+
for t2 in range(max_size):
2710+
numpy.testing.assert_equal(orig_seq[t2], segments[b, t, t2])
2711+
2712+
26652713
def test_WindowLayer_output_placeholder():
26662714
with make_scope() as session:
26672715
net = TFNetwork(extern_data=ExternData())

0 commit comments

Comments
 (0)