import inspect import paddle class Sequential(paddle.nn.LayerDict): """A sequence of modules with potentially inferring shape on construction. If layers are passed with names, these can be referenced with dot notation. Arguments --------- input_shape : iterable A list or tuple of ints or None, representing the expected shape of an input tensor. None represents a variable-length dimension. If no ``input_shape`` is passed, no shape inference will be performed. *layers, **named_layers The inputs are treated as a list of layers to be applied in sequence. The output shape of each layer is used to infer the shape of the following layer. If a tuple is returned, only the shape of the first element is used to determine input shape of the next layer (e.g. RNN returns output, hidden). Example ------- >>> inputs = paddle.rand(10, 40, 50) >>> model = Sequential(input_shape=inputs.shape) >>> model.append(Linear, n_neurons=100, layer_name="layer1") >>> model.append(Linear, n_neurons=200, layer_name="layer2") >>> outputs = model(inputs) >>> outputs.shape paddle.shape([10, 40, 200]) >>> outputs = model.layer1(inputs) >>> outputs.shape paddle.shape([10, 40, 100]) """ def __init__(self, *layers, input_shape=None, **named_layers): super().__init__() # Make sure either layers or input_shape is passed if not layers and input_shape is None and not named_layers: raise ValueError("Must pass either layers or input shape") # Keep track of what layers need "lengths" passed self.length_layers = [] # Replace None dimensions with arbitrary value self.input_shape = input_shape if input_shape and None in input_shape: self.input_shape = list(input_shape) for i, dim in enumerate(self.input_shape): # To reduce size of dummy tensors, use 1 for batch dim if i == 0 and dim is None: dim = 1 # Use 64 as nice round arbitrary value, big enough that # halving this dimension a few times doesn't reach 1 self.input_shape[i] = dim or 256 # Append non-named layers for layer in layers: self.append(layer) # Append named layers for name, layer in named_layers.items(): self.append(layer, layer_name=name) def append(self, layer, *args, layer_name=None, **kwargs): """Add a layer to the list of layers, inferring shape if necessary. Arguments --------- layer : A paddle.nn.Module class or object If the layer is a class, it should accept an argument called ``input_shape`` which will be inferred and passed. If the layer is a module object, it is added as-is. layer_name : str The name of the layer, for reference. If the name is in use, ``_{count}`` will be appended. *args, **kwargs These are passed to the layer if it is constructed. """ # Compute layer_name if layer_name is None: layer_name = str(len(self)) elif layer_name in self: index = 0 while f"{layer_name}_{index}" in self: index += 1 layer_name = f"{layer_name}_{index}" # Check if it needs to be constructed with input shape if self.input_shape: argspec = inspect.getfullargspec(layer) if "input_shape" in argspec.args + argspec.kwonlyargs: input_shape = self.get_output_shape() layer = layer(*args, input_shape=input_shape, **kwargs) # Finally, append the layer. try: self[layer_name] = layer # self.add_module(layer_name, layer) except TypeError: raise ValueError( "Must pass `input_shape` at initialization and use " "modules that take `input_shape` to infer shape when " "using `append()`.") def get_output_shape(self): """Returns expected shape of the output. Computed by passing dummy input constructed with the ``self.input_shape`` attribute. """ with paddle.no_grad(): dummy_input = paddle.zeros(self.input_shape) dummy_output = self(dummy_input) return dummy_output.shape def forward(self, x): """Applies layers in sequence, passing only the first element of tuples. Arguments --------- x : paddle.Tensor The input tensor to run through the network. """ for layer in self.values(): x = layer(x) if isinstance(x, tuple): x = x[0] return x