audeep/backend/models/ops.py
def window_features(inputs: tf.Tensor,
                    window_width: int,
                    window_overlap: int,
                    name: str = "window_features") -> tf.Tensor:
    """
    Performs windowing of the features in a tensor.
    
    Given an input tensor of shape [batch_size, num_features], the features of the tensor are split into windows with 
    width `window_width` and overlap `window_overlap`. The resulting tensors are then stacked along a new dimension,
    resulting in an output tensor of shape [num_windows, batch_size, window_width].
    
    Parameters
    ----------
    inputs: tf.Tensor
        The tensor of which features should be windowed, with shape [batch_size, num_features]
    window_width: int
        The number of features per window
    window_overlap: int
        The overlap between windows
    name: str, optional
        A name for the operation (default "window_features")

    Returns
    -------
    tf.Tensor
        A tensor containing the windowed features of the input tensor, with shape [num_windows, batch_size, window_width]
    """
    if rank(inputs) != 2:
        raise ValueError("rank of inputs must be 2 ([batch_size, num_features]), is: {}".format(rank(inputs)))

    log = logging.getLogger(__name__)
    num_features = inputs.shape.as_list()[1]  # must be known at graph-construction time

    with tf.variable_scope(scope=name):
        window_step = window_width - window_overlap
        num_windows = (num_features - window_width) // window_step + 1
        covered_features = window_width + (num_windows - 1) * window_step

        if covered_features != num_features:
            log.warning(
                "the last %d of %d features will be dropped for windows with width %d and overlap %d",
                num_features - covered_features, num_features, window_width, window_overlap)

        windows = [(window * window_step, window * window_step + window_width)
                   for window in range(num_windows)]
        window_tensors = [inputs[:, window_start:window_end] for window_start, window_end in windows]

        return tf.stack(window_tensors, axis=0)  # shape: [num_windows, batch_size, window_width]
audeep/backend/decorators.py
def scoped_subgraph_initializers(klass):
    """
    Decorator which creates initializers and properties for @scoped_subgraph annotated methods.
    
    For each @scoped_subgraph decorated method with name <NAME>, this decorator performs the following steps. An 
    initialization method named init_<NAME> is created, which calls the decorated method and caches the return value
    in a field named _cache_<NAME>. The @scoped_subgraph method itself is renamed to _<NAME>_initializer, and a 
    property with name <NAME> is added, returning the cached value
    
    Parameters
    ----------
    klass: class
        The class to decorate
    Returns
    -------
    class
        The decorated class
    """
    if not inspect.isclass(klass):
        raise AttributeError("scoped_subgraph_initializers decorator may only be used on classes")

    original_methods = klass.__dict__.copy()

    for name, method in original_methods.items():
        if hasattr(method, "_scoped_subgraph"):
            def init_fn(fn_self, method=method):
                attribute = "_cache_" + method.__name__

                if not hasattr(fn_self, attribute):
                    with tf.variable_scope(method.scope or method.__name__, *method.scope_args,
                                           **method.scope_kwargs) as scope:
                        setattr(fn_self, attribute, method(fn_self))
                        setattr(fn_self, (method.scope or method.__name__) + "_scope", scope)

            @property
            @functools.wraps(method)
            def property_fn(fn_self, method=method):
                attribute = "_cache_" + method.__name__

                if not hasattr(fn_self, attribute):
                    raise AttributeError(
                        "scoped subgraph defined by function %s has not been initialized yet" % method.__name__)

                return getattr(fn_self, attribute)

            setattr(klass, "init_" + name, init_fn)
            setattr(klass, "_" + name + "_initializer", method)
            setattr(klass, name, property_fn)

    return klass
audeep/backend/models/ops.py
def linear(input: tf.Tensor,
           output_size: int,
           weight_initializer: Optional[Initializer] = None,
           bias_initializer: Optional[Initializer] = None,
           name: str = "linear") -> tf.Tensor:
    """
    Apply a linear transformation to a tensor.
    
    Parameters
    ----------
    input: tf.Tensor
        The tensor which should be linearly transformed
    output_size: int
        The desired output size of the linear transformation
    weight_initializer: tf.Initializer, optional
        A custom initializer for the weight matrix of the linear transformation
    bias_initializer: tf.Initializer, optional
        A custom initializer for the bias vector of the linear transformation
    name: str, optional
        A name for the operation (default "linear")

    Returns
    -------
    tf.Tensor
        The linearly transformed input tensor
    """
    shape = input.get_shape().as_list()

    with tf.variable_scope(name):
        weights = tf.get_variable(name="weights",
                                  shape=[shape[-1], output_size],
                                  dtype=tf.float32,
                                  initializer=weight_initializer)

        bias = tf.get_variable(name="bias",
                               shape=[output_size],
                               initializer=bias_initializer)

        return tf.matmul(input, weights) + bias
audeep/backend/models/ops.py
def flatten_time(inputs: tf.Tensor):
    """
    Flattens the time dimension of a tensor representing time-series data.
    
    This operation reshapes the input tensor in such a way that time steps in the original tensor are converted to
    individual batches, so that the same operation can be applied to all time steps.
    
    Parameters
    ----------
    inputs: tf.Tensor
        The tensor which should be flattened, of shape [max_time, batch_size, num_features]

    Returns
    -------
    tf.Tensor
        The input tensor with flattened time dimension, of shape [max_time * batch_size, num_features]
    """
    max_time, batch_size, _ = tf.unstack(tf.shape(inputs))
    num_features = inputs.shape[2].value

    return tf.reshape(inputs, shape=[max_time * batch_size, num_features], name="flatten_time")
audeep/backend/models/rnn_base.py
class StandardRNN(_RNNBase):
    """
    Default implementation of the `_RNNBase` interface.
    
    This class uses a straightforward, possibly bidirectional, RNN to process the input sequence. If desired, an input
    noise probability can be specified. If this probability is greater than zero, the features of each step in the
    input sequence will be set to zero with that probability.
    """

    def __init__(self,
                 architecture: RNNArchitecture,
                 inputs: tf.Tensor,
                 initial_state: Optional[tf.Tensor],
                 keep_prob: Optional[tf.Tensor],
                 input_noise: Optional[tf.Tensor]):
        """
        Create and initialize a StandardRNN with the specified parameters.
        
        Parameters
        ----------
        architecture: RNNArchitecture
            The RNN architecture.
        inputs: tf.Tensor
            Input sequences to this RNN of shape [max_step, batch_size, num_features]
        initial_state: tf.Tensor, optional
            Initial hidden states of the RNN cells of shape [batch_size, state_size]
        keep_prob: tf.Tensor, optional
            Probability to keep hidden activations
        input_noise: tf.Tensor, optional
            Probability to replace steps in the input sequence with zeros
        """
        super().__init__(architecture, inputs, initial_state, keep_prob)

        self.input_noise = input_noise

        # initialize subgraphs
        self.init_sequence_length()
        self.init_initial_states_tuple()
        self.init_noisy_inputs()
        self.init_rnn_output_and_state()

    @property
    def output_size(self) -> int:
        """
        The number of features in the output sequence of this RNN.
        
        If the RNN is unidirectional, the output sequence will contain one feature for each unit in the last RNN layer.
        If it is bidirectional, the output sequence will contain two features for each unit in the last RNN layer, one
        for the forward RNN, and one for the backward RNN.
        
        Returns
        -------
        int
            The number of features in the output sequence of this RNN.
            
        See Also
        --------
        _RNNBase.output_size
        """
        return self.num_units * (2 if self.bidirectional else 1)

    @scoped_subgraph
    def noisy_inputs(self) -> tf.Tensor:
        """
        Return the input sequence, with noise added according to the `input_noise` parameter.
        
        If the `input_noise` parameter is not set, this method simply returns the input sequence. Otherwise, return a 
        tensor in which each time step of the input sequence is randomly set to zeros with probability given by the
        `input_noise` parameter.
        
        Returns
        -------
        tf.Tensor
            The input sequence, with noise added according to the `input_noise` parameter
        """
        if self.input_noise is None:
            return self.inputs

        # drop entire time steps with probability self.noise
        randoms = tf.random_uniform([self.max_step, self.batch_size], minval=0, maxval=1)
        randoms = tf.stack([randoms] * self.num_features, axis=2)

        result = tf.where(randoms > self.input_noise, self.inputs, tf.zeros_like(self.inputs))

        return result

    @scoped_subgraph
    def rnn_output_and_state(self):
        cells = self._create_cells()

        if self.bidirectional:
            initial_states_fw, initial_states_bw = self.initial_states_tuple

            output, (final_state_fw,
                     final_state_bw) = tf.nn.bidirectional_dynamic_rnn(cell_fw=cells[0],
                                                                       cell_bw=cells[1],
                                                                       inputs=self.noisy_inputs,
                                                                       sequence_length=self.sequence_length,
                                                                       initial_state_fw=initial_states_fw,
                                                                       initial_state_bw=initial_states_bw,
                                                                       dtype=tf.float32,
                                                                       swap_memory=True,
                                                                       time_major=True)

            output = tf.concat(output, axis=2)

            # LSTM states are tuples -> need to be concatenated
            # axis is one because states have shape [batch_size, units]
            if self.cell_type == CellType.LSTM:
                final_state_fw = [tf.concat(lstm_tuple, axis=1) for lstm_tuple in final_state_fw]
                final_state_bw = [tf.concat(lstm_tuple, axis=1) for lstm_tuple in final_state_bw]

            final_state = [tf.concat(x, axis=1) for x in zip(final_state_fw, final_state_bw)]
        else:
            initial_states = self.initial_states_tuple

            output, final_state = tf.nn.dynamic_rnn(cell=cells[0],
                                                    inputs=self.noisy_inputs,
                                                    sequence_length=self.sequence_length,
                                                    initial_state=initial_states,
                                                    dtype=tf.float32,
                                                    swap_memory=True,
                                                    time_major=True)

            # LSTM states are tuples -> need to be concatenated
            # axis is one because states have shape [batch_size, units]
            if self.cell_type == CellType.LSTM:
                final_state = [tf.concat(lstm_tuple, axis=1) for lstm_tuple in final_state]

        final_state = tf.concat(final_state, axis=1)

        return output, final_state
audeep/backend/models/rnn_base.py
class FeedPreviousRNN(_RNNBase):
    """
    An RNN in which, at each time step, the previous output of the RNN is used as input with a given probability.
    
    Since the output and the input of such an RNN must have equal shape, this class implicitly adds a linear output 
    projection layer with hyperbolic tangent activation and the correct size on top of the RNN outputs.
    """

    def __init__(self,
                 architecture: RNNArchitecture,
                 inputs: tf.Tensor,
                 initial_state: Optional[tf.Tensor],
                 keep_prob: Optional[tf.Tensor],
                 feed_previous_prob: Optional[tf.Tensor]):
        """
        Create and initialize a FeedPreviousRNN with the specified parameters.
        
        Parameters
        ----------
        architecture: RNNArchitecture
            The RNN architecture.
        inputs: tf.Tensor
            Input sequences to this RNN of shape [max_step, batch_size, num_features]
        initial_state: tf.Tensor, optional
            Initial hidden states of the RNN cells of shape [batch_size, state_size]
        keep_prob: tf.Tensor, optional
            Probability to keep hidden activations
        feed_previous_prob: tf.Tensor, optional
            Probability at each time step to feed the output of the previous time step as input to the RNN
        """
        super().__init__(architecture, inputs, initial_state, keep_prob)

        self.feed_previous_prob = feed_previous_prob

        # initialize subgraphs
        self.init_sequence_length()
        self.init_initial_states_tuple()
        self.init_rnn_output_and_state()

    @property
    def output_size(self):
        """
        The number of features in the output sequence of this RNN.
        
        Since an output projection layer is implicitly added on top of the RNN output, the number of output features is
        equal to the number of input features if the network is unidirectional, and equal to twice the number of input
        features if the network is bidirectional.
        
        Returns
        -------
        int
            The number of features in the output sequence of this RNN
        """
        return self.num_features * (2 if self.bidirectional else 1)

    def _feed_previous_rnn(self,
                           cell,
                           initial_state,
                           reverse: bool) -> (tf.Tensor, Any):
        """
        Implements a unidirectional RNN with stochastic feeding of previous RNN outputs.
        
        Since the required functionality is not directly supported by the `tf.contrib.rnn` module, this method
        implements a custom loop function used with the `raw_rnn` function.
        
        Parameters
        ----------
        cell
            The RNN cell to use
        initial_state
            A possibly nested tuple of initial states for the RNN cells
        reverse: bool
            Whether to reverse the input sequence

        Returns
        -------
        output: tf.Tensor
            The output sequence of the RNN after applying the output projection
        final_state: Any
            A possible nested tuple of final states of the RNN cells, with the same structure as the `initial_state` 
            tuple
        """
        # input sequence
        if reverse:
            input_sequence = tf.reverse(self.inputs, axis=[0])
        else:
            input_sequence = self.inputs

        # output projection
        weights = tf.get_variable(name="weights",
                                  shape=[self.num_units, self.num_features],
                                  dtype=tf.float32)
        bias = tf.get_variable(name="bias",
                               shape=[self.num_features])

        # feed mask
        if self.feed_previous_prob is None:
            feed_mask = tf.fill(dims=[self.max_step, self.batch_size], value=False)
        else:
            feed_mask = tf.random_uniform([self.max_step, self.batch_size], minval=0, maxval=1)
            feed_mask = feed_mask < self.feed_previous_prob

        def loop_fn_initial():
            initial_elements_finished = (0 >= self.sequence_length)

            initial_input = tf.zeros([self.batch_size, self.num_features], dtype=tf.float32)
            initial_cell_state = initial_state
            initial_cell_output = None
            initial_loop_state = None  # we don't need to pass any additional information

            return (initial_elements_finished,
                    initial_input,
                    initial_cell_state,
                    initial_cell_output,
                    initial_loop_state)

        def loop_fn_transition(time, cell_output, cell_state, previous_loop_state):
            elements_finished = (time >= self.sequence_length)

            next_input = tf.where(feed_mask[time - 1],
                                  x=tf.tanh(tf.matmul(cell_output, weights) + bias),
                                  y=input_sequence[time - 1])
            loop_state = None

            return (elements_finished,
                    next_input,
                    cell_state,
                    cell_output,
                    loop_state)

        def loop_fn(time, previous_output, previous_state, previous_loop_state):
            if previous_state is None:  # time == 0
                assert previous_output is None and previous_state is None
                return loop_fn_initial()
            else:
                return loop_fn_transition(time, previous_output, previous_state, previous_loop_state)

        outputs_ta, final_state, _ = tf.nn.raw_rnn(cell=cell,
                                                   loop_fn=loop_fn,
                                                   swap_memory=True)
        outputs = outputs_ta.stack()
        outputs = flatten_time(outputs)
        outputs = tf.tanh(tf.matmul(outputs, weights) + bias)
        outputs = restore_time(outputs,
                               max_time=self.max_step,
                               batch_size=self.batch_size,
                               num_features=self.num_features)
        outputs.set_shape([self.inputs.shape[0], self.inputs.shape[1], self.num_features])

        return outputs, final_state

    @scoped_subgraph
    def rnn_output_and_state(self):
        cells = self._create_cells()

        if self.bidirectional:
            initial_states_fw, initial_states_bw = self.initial_states_tuple

            with tf.variable_scope("fw"):
                output_fw, final_state_fw = self._feed_previous_rnn(cell=cells[0],
                                                                    initial_state=initial_states_fw,
                                                                    reverse=False)

            with tf.variable_scope("bw"):
                output_bw, final_state_bw = self._feed_previous_rnn(cell=cells[1],
                                                                    initial_state=initial_states_bw,
                                                                    reverse=True)

            output = tf.concat([output_fw, output_bw], axis=2)

            # LSTM states are tuples -> need to be concatenated
            # axis is one because states have shape [batch_size, units]
            if self.cell_type == CellType.LSTM:
                final_state_fw = [tf.concat(lstm_tuple, axis=1) for lstm_tuple in final_state_fw]
                final_state_bw = [tf.concat(lstm_tuple, axis=1) for lstm_tuple in final_state_bw]

            final_state = [tf.concat(x, axis=1) for x in zip(final_state_fw, final_state_bw)]
        else:
            initial_states = self.initial_states_tuple

            output, final_state = self._feed_previous_rnn(cell=cells[0],
                                                          initial_state=initial_states,
                                                          reverse=False)

            # LSTM states are tuples -> need to be concatenated
            # axis is one because states have shape [batch_size, units]
            if self.cell_type == CellType.LSTM:
                final_state = [tf.concat(lstm_tuple, axis=1) for lstm_tuple in final_state]

        final_state = tf.concat(final_state, axis=1)

        return output, final_state
audeep/backend/models/summaries.py
def variable_summaries(tensor: tf.Tensor):
def scalar_summaries(tensor: tf.Tensor,
                     name: str = "value"):
def reconstruction_summaries(reconstruction: tf.Tensor,
                             targets: tf.Tensor,
                             name: str = "targets_vs_reconstruction"):
def image_summaries(images: tf.Tensor,
                    max_outputs: int = 3,
                    name: str = "image"):
audeep/backend/models/ops.py
def restore_time(inputs: tf.Tensor,
                 max_time: tf.Tensor,
                 batch_size: tf.Tensor,
                 num_features: int) -> tf.Tensor:
    """
    Restores the time dimension of tensor representing time-series data in which the time dimension has been flattened.
    
    Parameters
    ----------
    inputs: tf.Tensor
        The tensor of which the time dimension should be restored, of shape [max_time * batch_size, num_features]
    max_time: tf.Tensor
        Tensor containing the desired number of time steps
    batch_size: tf.Tensor
        Tensor containing the batch size
    num_features: int
        The number of features in the input tensor. Required since the reshaping operation will otherwise lose the
        static shape information

    Returns
    -------
    tf.Tensor
        The input tensor with restored time dimension, of shape [max_time, batch_size, num_features]
    """
    return tf.reshape(inputs, shape=[max_time, batch_size, num_features], name="restore_time")
audeep/backend/decorators.py
@doublewrap
def scoped_subgraph(scope: str = None, *scope_args, **scope_kwargs):
    """
    A decorator for methods which specify subgraphs of a Tensorflow graph. The code of the decorated method will only be
    executed once, so that operations are not added to the graph multiple times. Furthermore, the operations are 
    enclosed in a Tensorflow variable_scope with the arguments passed to the decorator.
    
    This decorator should be used in conjunction with the scoped_subgraph_initializers decorator, which takes care of
    creating initialization methods for the subgraphs.
    
    Parameters
    ----------
    scope: str
        The name of the scope. Defaults to the name of the decorated method
    scope_args: positional arguments
        Positional arguments to pass to variable_scope
    scope_kwargs: keyword arguments
        Keyword arguments to pass to variable_scope
        
    Returns
    -------
    method
        A method which can be used to decorate methods
    """

    def wrap_function(fn):
        fn._scoped_subgraph = True
        fn.scope = scope
        fn.scope_args = scope_args
        fn.scope_kwargs = scope_kwargs

        return fn

    return wrap_function
audeep/backend/models/rnn_base.py
class RNNArchitecture:
    """
    Architecture of a RNN.
    """

    def __init__(self,
                 num_layers: int,
                 num_units: int,
                 bidirectional: bool,
                 cell_type: CellType):
        """
        Creates and initializes a new RNNArchitecture with the specified parameters.
        
        Parameters
        ----------
        num_layers: int
            The number of layers in the RNN
        num_units: int
            The number of RNN cells per layer
        bidirectional: bool
            Indicates whether the RNN is bidirectional
        cell_type: CellType
            The type of RNN cell to use
        """
        self.num_layers = num_layers
        self.num_units = num_units
        self.bidirectional = bidirectional
        self.cell_type = cell_type

    @property
    def state_size(self) -> int:
        """
        Returns the state size of an RNN with this architecture.
        
        Returns
        -------
        int
            The state size of an RNN with this architecture
        """
        size = self.num_units * self.num_layers

        if self.cell_type == CellType.LSTM:
            size *= 2

        if self.bidirectional:
            size *= 2

        return size


Question: from typing import Optional
from audeep.backend.decorators import scoped_subgraph_initializers, scoped_subgraph
from audeep.backend.models import summaries
from audeep.backend.models.ops import flatten_time, window_features, linear, restore_time
from audeep.backend.models.rnn_base import RNNArchitecture, StandardRNN, FeedPreviousRNN
import tensorflow as tf
        return tf.shape(self.inputs)[1]

    @property
    def num_features(self) -> int:
        """
        Returns the number of features in each time step of the input sequences.

        This property is known at graph-construction time.

        Returns
        -------
        int
            The number of features in each time step of the input sequence
        """
        return self.inputs.shape.as_list()[2]

    @scoped_subgraph
    def targets(self) -> tf.Tensor:
        """
        Returns the reconstruction target sequences.
        
        The target sequences are simply the input sequences reversed along the frequency axis.
        
        Returns
        -------
        tf.Tensor
            The target sequences, of shape [max_time, batch_size, num_features]
        """
        return self.inputs[:, :, ::-1]

    @scoped_subgraph
    def encoder_inputs(self) -> tf.Tensor:
        """
        Returns the input sequences for the encoder.
        
        The encoder input sequences are built by splitting the input spectrograms into windows of width 
        `frequency_window_width` and overlap `frequency_window_overlap` along the frequency axis. These windows are then
        fed in order to the encoder RNN.
        
        Returns
        -------
        tf.Tensor
            The input sequences for the encoder
        """
        # shape: [max_time * batch_size, num_features]
        inputs_flat = flatten_time(self.inputs)

        # shape: [num_windows, max_time * batch_size, window_width]
        return window_features(inputs=inputs_flat,
                               window_width=self.frequency_window_width,
                               window_overlap=self.frequency_window_overlap)

    @scoped_subgraph
    def encoder(self) -> StandardRNN:
        """
        Creates the encoder RNN.

        The encoder RNN receives the windowed frequency vectors as input, and is initialized with zero initial states.

        Returns
        -------
        StandardRNN
            The encoder RNN
        """
        return StandardRNN(architecture=self.encoder_architecture,
                           inputs=self.encoder_inputs,
                           initial_state=None,
                           keep_prob=self.keep_prob,
                           input_noise=self.encoder_noise)

    @scoped_subgraph
    def representation(self) -> tf.Tensor:
        """
        Computes the hidden representation of the input sequences.

        The hidden representation of an input sequence is computed by applying a linear transformation with hyperbolic
        tangent activation to the final state of the encoder. The output size of the linear transformation matches the
        state vector size of the decoder.

        Returns
        -------
        tf.Tensor
            The hidden representation of the input sequences, of shape [batch_size, decoder_state_size]
        """

        # shape: [max_time * batch_size, encoder.state_size]
        internal_rep = tf.tanh(linear(self.encoder.final_state, self.decoder_architecture.state_size))

        # shape: [max_time, batch_size, encoder.state_size]
        rep = tf.reshape(internal_rep,
                         [self.max_time, self.batch_size, self.encoder_architecture.state_size])
        # shape: [batch_size, max_time, encoder.state_size]
        rep = tf.transpose(rep, perm=[1, 0, 2])

        tf.add_to_collection("representation", rep)
        summaries.variable_summaries(rep)

        return internal_rep

    @scoped_subgraph
    def decoder_inputs(self) -> tf.Tensor:
        """
        Computes the decoder RNN input sequences.

        At each time step, the input to the decoder RNN is the expected output at the previous frequency step. Thus, the
        decoder input sequences are the encoder input sequences shifted by one step along the frequency axis.

        Returns
        -------
        tf.Tensor
            The decoder RNN input sequences, of shape [num_windows, batch_size*max_time, window_width]
        """
        num_windows = self.encoder_inputs.shape.as_list()[0]
        decoder_inputs = self.encoder_inputs[::-1]
        decoder_inputs = decoder_inputs[:num_windows - 1]
        decoder_inputs = tf.pad(decoder_inputs, paddings=[[1, 0], [0, 0], [0, 0]], mode="constant")

        return decoder_inputs

    @scoped_subgraph

Answer:     def decoder(self) -> FeedPreviousRNN: