Source code for texar.tf.utils.shapes

# Copyright 2018 The Texar Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Utility functions related to tensor shapes.
"""

# pylint: disable=no-name-in-module, protected-access, no-member, invalid-name

import numpy as np

import tensorflow as tf
from tensorflow.python.util import nest
from tensorflow.python.ops import rnn
from tensorflow.python.framework import ops

__all__ = [
    "transpose_batch_time",
    "get_batch_size",
    "get_rank",
    "mask_sequences",
    "_mask_sequences_tensor",
    "_mask_sequences_py",
    "reduce_with_weights",
    "flatten",
    "shape_list",
    "pad_and_concat",
    "varlength_concat",
    "varlength_concat_py",
    "varlength_roll"
]


[docs]def transpose_batch_time(inputs): """Transposes inputs between time-major and batch-major. Args: inputs: A Tensor of shape `[batch_size, max_time, ...]` (batch-major) or `[max_time, batch_size, ...]` (time-major), or a (possibly nested) tuple of such elements. Returns: A (possibly nested tuple of) Tensor with transposed batch and time dimensions of inputs. """ flat_input = nest.flatten(inputs) flat_input = [ops.convert_to_tensor(input_) for input_ in flat_input] # pylint: disable=protected-access flat_input = [rnn._transpose_batch_time(input_) for input_ in flat_input] return nest.pack_sequence_as(structure=inputs, flat_sequence=flat_input)
[docs]def get_batch_size(tensor): """Returns a unit `Tensor` representing the batch size, i.e., the size of the 1st dimension of :attr:`tensor`. """ return tf.shape(tensor)[0]
[docs]def get_rank(tensor): """Returns the tensor rank as a python `int`. The input tensor can also be a python array. Args: tensor: A Tensor or python array. Returns: A python `int` representing the rank of :attr:`tensor`. Returns `None` if the rank cannot be determined. """ if tf.contrib.framework.is_tensor(tensor): shape = tensor.shape try: rank = len(shape.as_list()) except ValueError: # when `shape==TensorShape(None)` rank = None else: array = np.asarray(tensor) rank = array.ndim return rank
[docs]def mask_sequences(sequence, sequence_length, dtype=None, time_major=False, tensor_rank=2): """Masks out sequence entries that are beyond the respective sequence lengths. Masks along the time dimension. :attr:`sequence` and :attr:`sequence_length` can either be python arrays or Tensors, respectively. If both are python arrays (or None), the return will be a python array as well. Args: sequence: A Tensor or python array of sequence values. If `time_major==False` (default), this must be a Tensor of shape `[batch_size, max_time, ...]`. The batch and time dimension is exchanged if `time_major==True`. sequence_length: A Tensor or python array of shape `[batch_size]`. Time steps beyond the respective sequence lengths will be made zero. dtype (dtype): Type of :attr:`sequence`. If `None`, infer from :attr:`sequence` automatically. time_major (bool): The shape format of the inputs. If `True`, :attr:`sequence` must have shape `[max_time, batch_size, ...]`. If `False` (default), :attr:`sequence` must have shape `[batch_size, max_time, ...]`. tensor_rank (int): The number of dimensions of :attr:`sequence`. Default is 2, i.e., :attr:`sequence` is a 2D Tensor consisting of batch and time dimensions. Ignored if both :attr:`sequence` and :attr:`sequence_length` are python arrays. Returns: The masked sequence, i.e., a Tensor or python array of the same shape as :attr:`sequence` but with masked-out entries (set to zero). If both :attr:`sequence` and :attr:`sequence_length` are python arrays, the returned value is a python array as well. """ is_tensor = tf.contrib.framework.is_tensor if is_tensor(sequence) or is_tensor(sequence_length): return _mask_sequences_tensor( sequence, sequence_length, dtype, time_major, tensor_rank) else: return _mask_sequences_py( sequence, sequence_length, dtype, time_major)
def _mask_sequences_tensor(sequence, sequence_length, dtype=None, time_major=False, tensor_rank=2): """Masks out sequence entries that are beyond the respective sequence lengths. Masks along the time dimension. Args: sequence: A Tensor of sequence values. If `time_major=False` (default), this must be a Tensor of shape: `[batch_size, max_time, d_2, ..., d_rank]`, where the rank of the Tensor is specified with :attr:`tensor_rank`. If `time_major=True`, this must be a Tensor of shape: `[max_time, batch_size, d_2, ..., d_rank].` sequence_length: A Tensor of shape `[batch_size]`. Time steps beyond the respective sequence lengths will be made zero. dtype (dtype): Type of :attr:`sequence`. If `None`, infer from :attr:`sequence` automatically. time_major (bool): The shape format of the inputs. If `True`, :attr:`sequence` must have shape `[max_time, batch_size, d_2, ..., d_rank]`. If `False` (default), :attr:`sequence` must have shape `[batch_size, max_time, d_2, ..., d_rank]`. tensor_rank (int): The number of dimensions of :attr:`sequence`. Default is 2, i.e., :attr:`sequence` is a 2D Tensor consisting of batch and time dimensions. Returns: The masked sequence, i.e., a Tensor of the same shape as :attr:`sequence` but with masked-out entries (set to zero). """ if tensor_rank is None: tensor_rank = 2 if tensor_rank < 2: raise ValueError( "tensor_rank must be > 2. Got tensor_rank = {}".format(tensor_rank)) if time_major: sequence = rnn._transpose_batch_time(sequence) max_time = tf.cast(tf.shape(sequence)[1], tf.int32) if dtype is None: dtype = sequence.dtype mask = tf.sequence_mask( tf.cast(sequence_length, tf.int32), max_time, dtype=dtype) for _ in range(2, tensor_rank): mask = tf.expand_dims(mask, axis=-1) sequence = sequence * mask if time_major: sequence = rnn._transpose_batch_time(sequence) return sequence def _mask_sequences_py(sequence, sequence_length, dtype=None, time_major=False): """Masks out sequence entries that are beyond the respective sequence lengths. Masks along the time dimension. This is the numpy version of :func:`texar.tf.utils.mask_sequences`. Args: sequence: An python array of sequence values. If `time_major=False` (default), this must be an array of shape: `[batch_size, max_time, ...]` If `time_major=True`, this must be a Tensor of shape: `[max_time, batch_size, ...].` sequence_length: An array of shape `[batch_size]`. Time steps beyond the respective sequence lengths will be made zero. dtype (dtype): Type of :attr:`sequence`. If `None`, infer from :attr:`sequence` automatically. time_major (bool): The shape format of the inputs. If `True`, :attr:`sequence` must have shape `[max_time, batch_size, ...]`. If `False` (default), :attr:`sequence` must have shape `[batch_size, max_time, ...]`. Returns: The masked sequence, i.e., an array of the same shape as :attr:`sequence` but with masked-out entries (set to zero). """ sequence = np.array(sequence) sequence_length = np.array(sequence_length) rank = sequence.ndim if rank < 2: raise ValueError("`sequence` must be 2D or higher order.") batch_size = sequence.shape[0] max_time = sequence.shape[1] dtype = dtype or sequence.dtype if time_major: sequence = np.transpose(sequence, axes=[1, 0, 2]) steps = np.tile(np.arange(max_time), [batch_size, 1]) mask = np.asarray(steps < sequence_length[:, None], dtype=dtype) for _ in range(2, rank): mask = np.expand_dims(mask, -1) sequence = sequence * mask if time_major: sequence = np.transpose(sequence, axes=[1, 0, 2]) return sequence
[docs]def reduce_with_weights(tensor, weights=None, average_across_batch=True, average_across_remaining=False, sum_over_batch=False, sum_over_remaining=True, tensor_rank=None): """Weights and reduces tensor. Args: tensor: A Tensor to weight and reduce, of shape `[batch_size, ...]`. weights (optional): A Tensor of the same shape and dtype with :attr:`tensor`. For example, this is can be a 0-1 tensor for masking values of :attr:`tensor``. average_across_batch (bool): If set, average the tensor across the batch dimension. Must not set `average_across_batch`' and `sum_over_batch` at the same time. average_across_remaining (bool): If set, average the tensor across the remaining dimensions. Must not set `average_across_remaining`' and `sum_over_remaining` at the same time. If :attr:`weights` is given, this is a weighted average. sum_over_batch (bool): If set, sum the tensor across the batch dimension. Must not set `average_across_batch` and `sum_over_batch` at the same time. sum_over_remaining (bool): If set, sum the tensor across the remaining dimension. Must not set `average_across_remaining` and `sum_over_remaining` at the same time. If :attr:`weights` is given, this is a weighted sum. tensor_rank (int, optional): The number of dimensions of :attr:`tensor`. If not given, inferred from :attr:`tensor` automatically. Returns: A Tensor. Example: .. code-block:: python x = tf.constant([[10, 10, 2, 2], [20, 2, 2, 2]]) mask = tf.constant([[1, 1, 0, 0], [1, 0, 0, 0]]) z = reduce_with_weights(x, weights=mask) # z == 20 # (all 2 in x are masked) """ if tensor_rank is None: tensor_rank = get_rank(tensor) if tensor_rank is None: raise ValueError('Unable to infer the rank of `tensor`. ' 'Please set `tensor_rank` explicitly.') if weights is not None: tensor = tensor * weights if tensor_rank > 1: if average_across_remaining and sum_over_remaining: raise ValueError("Only one of `average_across_remaining` and " "`sum_over_remaining` can be set.") if average_across_remaining: if weights is None: tensor = tf.reduce_mean(tensor, axis=np.arange(1, tensor_rank)) else: tensor = tf.reduce_sum(tensor, axis=np.arange(1, tensor_rank)) weights = tf.reduce_sum(weights, axis=np.arange(1, tensor_rank)) tensor = tensor / weights elif sum_over_remaining: tensor = tf.reduce_sum(tensor, axis=np.arange(1, tensor_rank)) if average_across_batch and sum_over_batch: raise ValueError("Only one of `average_across_batch` and " "`sum_over_batch` can be set.") if sum_over_batch: tensor = tf.reduce_sum(tensor, axis=[0]) elif average_across_batch: tensor = tf.reduce_mean(tensor, axis=[0]) return tensor
[docs]def flatten(tensor, preserve_dims, flattened_dim=None): """Flattens a tensor whiling keeping several leading dimensions. :attr:`preserve_dims` must < tensor's rank Args: tensor: A Tensor to flatten. preserve_dims (int): The number of leading dimensions to preserve. flatterned_dim (int, optional): The size of the resulting flattened dimension. If not given, infer automatically, which can cause a statically unknown dimension size. Returns: A Tensor with rank :attr:`perserve_dims` + 1. Example: .. code-block:: python x = tf.ones(shape=[d_1, d_2, d_3, d_4]) y = flatten(x, 2) # y.shape == [d_1, d_2, d_3 * d_4] """ if flattened_dim is None: flattened_dim = -1 shape = tf.concat([tf.shape(tensor)[:preserve_dims], [flattened_dim]], axis=0) tensor_ = tf.reshape(tensor, shape) return tensor_
[docs]def shape_list(x): r"""Returns **static** shape of the input Tensor whenever possible. Args: x: A Tensor. Returns: - If the rank of `x` is unknown, returns the dynamic shape ``tf.shape(x)`` - Otherwise, returns a list of dims, each of which is either an `int` whenever it can be statically determined, or a scalar Tensor otherwise. """ x = tf.convert_to_tensor(x) # If unknown rank, return dynamic shape if x.get_shape().dims is None: return tf.shape(x) static = x.get_shape().as_list() shape = tf.shape(x) ret = [] for i, dim in enumerate(static): if dim is None: dim = shape[i] ret.append(dim) return ret
[docs]def pad_and_concat(values, axis, rank=None, pad_axis=None, pad_constant_values=0): """Concats tensors along one dimension. Pads each of other dimensions of the tensors to the corresponding maximum size if necessary. Args: values: A list of Tensors of the same rank. axis (int): A Python int. Dimension along which to concatenate. rank (int, optional): Rank of the tensors. If `None`, inferred automatically from :attr:`values`. pad_axis (int or list, optional): A Python int or a list of int. Dimensions to pad. Paddings are only added to the end of corresponding dimensions. If `None`, all dimensions except the :attr:`axis` dimension are padded. pad_constant_values: The scalar pad value to use. Must be same type as the tensors. Returns: A `Tensor` resulting from padding and concatenation of the input tensors. Raises: ValueError: If :attr:`rank` is `None` and cannot be inferred from :attr:`values`. Example: .. code-block:: python a = tf.ones([1, 2]) b = tf.ones([2, 3]) c = pad_and_concat([a,b], 0) # c.shape == [3, 3] # c == [[1, 1, 0], # [1, 1, 1], # [1, 1, 1]] d = pad_and_concat([a,b], 1) # d.shape == [2, 5] # d == [[1, 1, 1, 1, 1] # [0, 0, 1, 1, 1]] """ if rank is None: for value in values: rank = get_rank(value) if rank is not None: break if rank is None: raise ValueError('Cannot determine the rank of the tensors') def _pad_to_size(value, axis_, size): """Pads the :attr:`axis_` of a tensor :attr:`value` to the given :attr:`size`. Only pads to the end. Args: value: A Tensor. axis_: A Python int. size: A scalar int Tensor or Python int. """ paddings = np.zeros([rank, 2], dtype=np.int32) paddings[axis_, 1] = 1 paddings = paddings * (size - tf.shape(value)[axis_]) return tf.pad(value, paddings, mode='CONSTANT', constant_values=pad_constant_values) if pad_axis is None: pad_axis = [r for r in range(rank) if r != axis] pad_axis = pad_axis if isinstance(pad_axis, (list, tuple)) else [pad_axis] for pa in pad_axis: max_dim_size = tf.reduce_max([tf.shape(v)[pa] for v in values]) for i, v in enumerate(values): values[i] = _pad_to_size(v, pa, max_dim_size) return tf.concat(values, axis)
[docs]def varlength_concat(x, y, x_length, dtype=None, tensor_rank=None): """Concatenates rows of `x` and `y` where each row of `x` has a variable length. Both `x` and `y` are of numeric dtypes, such as `tf.int32` and `tf.float32`, with mask value `0`. The two tensors must be of the same dtype. Args: x: A tensor of shape `[batch_size, x_dim_2, other_dims]`. y: A tensor of shape `[batch_size, y_dim_2, other_dims]`. All dimensions except the 2nd dimension must be the same with those of `x`. x_length: A 1D int tensor of shape `[batch_size]` containing the length of each `x` row. Elements beyond the respective lengths will be made zero. dtype: Type of :attr:`x`. If `None`, inferred from :attr:`x` automatically. tensor_rank (int, optional): The number of dimensions of :attr:`x`. If not given, inferred from :attr:`x` automatically. Returns: A Tensor of shape `[batch_size, x_dim_2 + y_dim_2, other_dims]`. Example: .. code-block:: python x = tf.constant([[1, 1, 0, 0], [1, 1, 1, 0]]) x_length = [2, 3] y = tf.constant([[2, 2, 0], [2, 2, 2]]) out = varlength_concat(x, y, x_length) # out = [[1, 1, 2, 2, 0, 0, 0] # [1, 1, 1, 2, 2, 2, 0]] """ x = tf.convert_to_tensor(x) y = tf.convert_to_tensor(y) x_length = tf.convert_to_tensor(x_length) if tensor_rank is None: tensor_rank = get_rank(x) or get_rank(y) if tensor_rank is None: raise ValueError('Unable to infer the rank of `x`. ' 'Please set `tensor_rank` explicitly.') x_masked = mask_sequences(x, x_length, dtype=dtype, tensor_rank=tensor_rank) zeros_y = tf.zeros_like(y) x_aug = tf.concat([x_masked, zeros_y], axis=1) zeros_x = tf.zeros_like(x) y_aug = tf.concat([zeros_x, y], axis=1) # Now, x_aug.shape == y_aug.shape max_length_x = tf.shape(x)[1] batch_size = tf.shape(x)[0] initial_index = tf.constant(0, dtype=tf.int32) initial_outputs_ta = tf.TensorArray( dtype=dtype or x.dtype, size=0, dynamic_size=True) def _cond(index, _): return tf.less(index, batch_size) def _body(index, outputs_ta): y_aug_i_rolled = tf.roll( input=y_aug[index], shift=x_length[index] - max_length_x, # shift to left axis=0) xy = x_aug[index] + y_aug_i_rolled return [index + 1, outputs_ta.write(index, xy)] res = tf.while_loop(_cond, _body, [initial_index, initial_outputs_ta]) return res[1].stack()
[docs]def varlength_concat_py(x, y, x_length, dtype=None): """Concatenates rows of `x` and `y` where each row of `x` has a variable length. The function has the same semantic as :func:`varlength_concat`, except that this function is for numpy arrays instead of TF tensors. Both `x` and `y` are of numeric dtypes, such as `int32` and `float32`, with mask value `0`. The two arrays must be of the same dtype. Args: x: A array of shape `[batch_size, x_dim_2, other_dims]`. y: A array of shape `[batch_size, y_dim_2, other_dims]`. All dimensions except the 2nd dimension must be the same with those of `x`. x_length: A 1D int array of shape `[batch_size]` containing the length of each `x` row. Elements beyond the respective lengths will be made zero. dtype: Type of :attr:`x`. If `None`, inferred from :attr:`x` automatically. Returns: An array of shape `[batch_size, x_dim_2 + y_dim_2, other_dims]`. Example: .. code-block:: python x = np.asarray([[1, 1, 0, 0], [1, 1, 1, 0]]) x_length = [2, 3] y = np.asarray([[2, 2, 0], [2, 2, 2]]) out = varlength_concat_py(x, y, x_length) # out = [[1, 1, 2, 2, 0, 0, 0] # [1, 1, 1, 2, 2, 2, 0]] """ x = np.asarray(x, dtype=dtype) y = np.asarray(y, dtype=dtype) x_masked = mask_sequences(x, x_length, dtype=dtype) zeros_y = np.zeros_like(y) x_aug = np.concatenate([x_masked, zeros_y], axis=1) zeros_x = np.zeros_like(x) y_aug = np.concatenate([zeros_x, y], axis=1) # Now, x_aug.shape == y_aug.shape max_length_x = x.shape[1] batch_size = x.shape[0] for index in np.arange(batch_size): y_aug_i_rolled = np.roll( a=y_aug[index], shift=x_length[index] - max_length_x, axis=0) x_aug[index] += y_aug_i_rolled return x_aug
[docs]def varlength_roll(input, shift, axis=1, dtype=None): """Rolls the elements of *each row* of a tensor along an axis for variable steps. This is a `tf.while_loop` wrapper of :tf_main:`tf.roll <roll>`. Note the different definition of :attr:`shift` and :attr:`axis` here compared to :tf_main:`tf.roll <roll>`. Args: input: A tensor of shape `[batch_size, other_dims]` where `other_dims` can be multiple dimensions. shift: A 1D int tensor of shape `[batch_size]` containing the steps for which each row in the batch are rolled. Positive shifts will roll towards larger indices, while negative shifts will roll towards smaller indices. axis: A scalar int tensor > 0. The dimension that the roll should occur. dtype: Type of :attr:`input`. If `None`, inferred from :attr:`input` automatically. Returns: A Tensor of the same shape/dtype as :attr:`input`. Example: .. code-block:: python x = tf.constant([[0, 0, 1, 0], [0, 1, 1, 1]]) shift = [-2, -1] out = varlength_roll(x, shift) # out = [[1, 0, 0, 0] # [1, 1, 1, 0]] .. code-block:: python x = tf.constant([[1, 2, 3, 4], [5, 6, 7, 8]]) shift = [1, -1] out = varlength_roll(x, shift) # out = [[4, 1, 2, 3] # [6, 7, 8, 5]] """ x = tf.convert_to_tensor(input) # x = input shift = tf.convert_to_tensor(shift) batch_size = tf.shape(x)[0] initial_index = tf.constant(0, dtype=tf.int32) initial_outputs_ta = tf.TensorArray( dtype=dtype or x.dtype, size=0, dynamic_size=True) def _cond(index, _): return tf.less(index, batch_size) def _body(index, outputs_ta): x_i_rolled = tf.roll( input=x[index], shift=shift[index], axis=axis - 1) return [index + 1, outputs_ta.write(index, x_i_rolled)] res = tf.while_loop(_cond, _body, [initial_index, initial_outputs_ta]) return res[1].stack()