Apply ``torch.Tensor.to`` to tensors in a generic data structure.

    Inspired by:
    https://github.com/pytorch/pytorch/blob/master/torch/utils/data/_utils/collate.py#L31

    Args:
        tensors (tensor, dict, list, namedtuple or tuple): Data structure with tensor values to
            move.
        *args: Arguments passed to ``torch.Tensor.to``.
        **kwargs: Keyword arguments passed to ``torch.Tensor.to``.

    Example use case:
        This is useful as a complementary function to ``collate_tensors``. Following collating,
        it's important to move your tensors to the appropriate device.

    Returns:
        The inputted ``tensors`` with ``torch.Tensor.to`` applied.

    Example:

        >>> import torch
        >>> batch = [
        ...   { 'column_a': torch.randn(5), 'column_b': torch.randn(5) },
        ...   { 'column_a': torch.randn(5), 'column_b': torch.randn(5) },
        ... ]
        >>> tensors_to(batch, torch.device('cpu'))  # doctest: +ELLIPSIS
        [{'column_a': tensor(...}]

def tensors_to(tensors, *args, **kwargs):
    """ Apply ``torch.Tensor.to`` to tensors in a generic data structure.

    Inspired by:
    https://github.com/pytorch/pytorch/blob/master/torch/utils/data/_utils/collate.py#L31

    Args:
        tensors (tensor, dict, list, namedtuple or tuple): Data structure with tensor values to
            move.
        *args: Arguments passed to ``torch.Tensor.to``.
        **kwargs: Keyword arguments passed to ``torch.Tensor.to``.

    Example use case:
        This is useful as a complementary function to ``collate_tensors``. Following collating,
        it's important to move your tensors to the appropriate device.

    Returns:
        The inputted ``tensors`` with ``torch.Tensor.to`` applied.

    Example:

        >>> import torch
        >>> batch = [
        ...   { 'column_a': torch.randn(5), 'column_b': torch.randn(5) },
        ...   { 'column_a': torch.randn(5), 'column_b': torch.randn(5) },
        ... ]
        >>> tensors_to(batch, torch.device('cpu'))  # doctest: +ELLIPSIS
        [{'column_a': tensor(...}]
    """
    if torch.is_tensor(tensors):
        return tensors.to(*args, **kwargs)
    elif isinstance(tensors, dict):
        return {k: tensors_to(v, *args, **kwargs) for k, v in tensors.items()}
    elif hasattr(tensors, '_asdict') and isinstance(tensors, tuple):  # Handle ``namedtuple``
        return tensors.__class__(**tensors_to(tensors._asdict(), *args, **kwargs))
    elif isinstance(tensors, list):
        return [tensors_to(t, *args, **kwargs) for t in tensors]
    elif isinstance(tensors, tuple):
        return tuple([tensors_to(t, *args, **kwargs) for t in tensors])
    else:
        return tensors