Source code for fynance.models.mlp

#!/usr/bin/env python3
# coding: utf-8

""" Feed-forward multi-layer perceptron model.

Defines :class:`MultiLayerPerceptron`, a configurable MLP built on top
of :class:`~fynance.models._base.BaseNeuralNet`. This is the standard
baseline for tabular and sliding-window features in finance.

For time-ordered sequence input, prefer the recurrent architectures in
:mod:`fynance.models.gru` or :mod:`fynance.models.lstm`. For walk-forward
training, wrap this class with
:class:`~fynance.models.rolling.RollMultiLayerPerceptron`.

Main entry points
-----------------
- :class:`MultiLayerPerceptron` — vanilla MLP with configurable hidden
  layers, activation and dropout.

"""

from __future__ import annotations

# Built-in packages
from typing import Any

# Third-party packages
import pandas as pd
import torch
from numpy.typing import NDArray

# Local packages
from fynance.models._base import BaseNeuralNet

__all__ = ['MultiLayerPerceptron']


[docs] class MultiLayerPerceptron(BaseNeuralNet): r""" Neural network with MultiLayer Perceptron architecture. Refered as vanilla neural network model, with `n` hidden layers s.t n :math:`\geq` 1, with each one a specified number of neurons. Each hidden layer is a ``torch.nn.Linear`` followed by an optional dropout and the configured activation function. The MLP is the standard baseline for tabular and sliding-window features in finance — useful for non-linear regression on engineered features (technical indicators, volatility, sentiment scores). For time-ordered sequence input, prefer :class:`fynance.models.lstm.LongShortTermMemory` or attention-based architectures. Configure the optimizer with :meth:`BaseNeuralNet.set_optimizer` and wrap with :class:`fynance.models.rolling.RollMultiLayerPerceptron` for walk-forward training. Parameters ---------- X, y : array-like or int - If it's an array-like, respectively inputs and outputs data. - If it's an integer, respectively dimension of inputs and outputs. layers : list of int List of number of neurons in each hidden layer. activation : torch.nn.Module Activation function of layers. drop : float, optional Probability of an element to be zeroed. Attributes ---------- criterion : torch.nn.modules.loss A loss function. optimizer : torch.optim An optimizer algorithm. n : int Number of hidden layers. layers : list of int List with the number of neurons for each hidden layer. f : torch.nn.Module Activation function. See Also -------- fynance.models._base.BaseNeuralNet, fynance.models.rolling.RollMultiLayerPerceptron """ def __init__( self, X: NDArray | torch.Tensor | pd.DataFrame | int, y: NDArray | torch.Tensor | pd.DataFrame | int, layers: list[int] = [], activation: type[torch.nn.Module] | None = None, drop: float | None = None, x_type=None, y_type=None, bias: bool = True, activation_kwargs: dict[str, Any] = {}, ): """ Initialize object. """ BaseNeuralNet.__init__(self) if isinstance(X, int) and isinstance(y, int): self.N, self.M = X, y else: self.set_data(X=X, y=y, x_type=x_type, y_type=y_type) self.n_layers = len(layers) + 1 self.layers = self._set_layer_list(layers, bias) self.activation = self._set_activation(activation, **activation_kwargs) self.drop = self._set_dropout(drop) def _set_layer_list(self, layers, bias, input_dim=None, output_dim=None): layers_list = [] # Set input layer input_size = self.N if input_dim is None else input_dim for output_size in layers: # Set hidden layers layers_list += [torch.nn.Linear( input_size, output_size, bias=bias )] input_size = output_size # Set output layer output_size = self.M if output_dim is None else output_dim layers_list += [torch.nn.Linear(input_size, output_size, bias=bias)] return torch.nn.ModuleList(layers_list) def _set_activation(self, activation, n_layers=None, **kwargs): # Set activation functions if isinstance(activation, list): n_layers = len(self.layers) if n_layers is None else n_layers if len(activation) != n_layers: raise ValueError('if you pass a list of activation functions ' 'this one must be of size of layers list + 1') return [a(**kwargs) for a in activation] elif activation is not None: return activation(**kwargs) else: return lambda x: x def _set_dropout(self, drop): # Set dropout parameters if isinstance(drop, list): if len(drop) != self.n_layers: raise ValueError('if you pass a list of drop parameters ' 'this one must be of size of layers list + 1') return [torch.nn.Dropout(p=p) for p in drop] elif drop is not None: return [torch.nn.Dropout(p=drop) for _ in range(self.n_layers)] else: return [lambda x: x for _ in range(self.n_layers)]
[docs] def forward(self, x): """ Forward computation. """ for name, layer in enumerate(self.layers): x = self.drop[name](x) x = layer(x) if isinstance(self.activation, list): x = self.activation[name](x) else: x = self.activation(x) return x