From 5fb72c706e87b3b7bf4fcd9701298b250cc98d89 Mon Sep 17 00:00:00 2001 From: lucifer4073 Date: Sun, 6 Jul 2025 16:51:57 +0530 Subject: [PATCH 1/7] tcn network added --- aeon/networks/__init__.py | 2 + aeon/networks/_tcn.py | 326 ++++++++++++++++++++++++++++++++ aeon/networks/tests/test_tcn.py | 212 +++++++++++++++++++++ 3 files changed, 540 insertions(+) create mode 100644 aeon/networks/_tcn.py create mode 100644 aeon/networks/tests/test_tcn.py diff --git a/aeon/networks/__init__.py b/aeon/networks/__init__.py index d774abe102..61e669283c 100644 --- a/aeon/networks/__init__.py +++ b/aeon/networks/__init__.py @@ -19,6 +19,7 @@ "AEBiGRUNetwork", "DisjointCNNNetwork", "RecurrentNetwork", + "TemporalConvolutionalNetwork", ] from aeon.networks._ae_abgru import AEAttentionBiGRUNetwork from aeon.networks._ae_bgru import AEBiGRUNetwork @@ -36,4 +37,5 @@ from aeon.networks._mlp import MLPNetwork from aeon.networks._resnet import ResNetNetwork from aeon.networks._rnn import RecurrentNetwork +from aeon.networks._tcn import TemporalConvolutionalNetwork from aeon.networks.base import BaseDeepLearningNetwork diff --git a/aeon/networks/_tcn.py b/aeon/networks/_tcn.py new file mode 100644 index 0000000000..08f4ff9341 --- /dev/null +++ b/aeon/networks/_tcn.py @@ -0,0 +1,326 @@ +"""Implementation of Temporal Convolutional Network (TCN). + +Based on the paper "An Empirical Evaluation of Generic Convolutional and +Recurrent Networks for Sequence Modeling" by Bai et al. (2018). +""" + +__maintainer__ = [] + +from aeon.networks.base import BaseDeepLearningNetwork + + +class TemporalConvolutionalNetwork(BaseDeepLearningNetwork): + """Temporal Convolutional Network (TCN) for sequence modeling. + + A generic convolutional architecture for sequence modeling that combines: + - Dilated convolutions for exponentially large receptive fields + - Residual connections for training stability + + The TCN can take sequences of any length and map them to output sequences + of the same length, making it suitable for autoregressive prediction tasks. + + Parameters + ---------- + num_inputs : int + Number of input channels/features in the input sequence. + num_channels : list of int + List specifying the number of output channels for each layer. + The length determines the depth of the network. + kernel_size : int, default=2 + Size of the convolutional kernel. Larger kernels can capture + more local context but require more parameters. + dropout : float, default=0.2 + Dropout rate applied after each convolutional layer for regularization. + + Notes + ----- + The receptive field size grows exponentially with network depth due to + dilated convolutions with dilation factors of 2^i for layer i. + + References + ---------- + Bai, S., Kolter, J. Z., & Koltun, V. (2018). An empirical evaluation of + generic convolutional and recurrent networks for sequence modeling. + arXiv preprint arXiv:1803.01271. + """ + + _config = { + "python_dependencies": ["tensorflow"], + "python_version": "<3.13", + "structure": "encoder", + } + + def __init__( + self, + num_inputs: int, + num_channels: list, + kernel_size: int = 2, + dropout: float = 0.2, + ): + """Initialize the TCN architecture. + + Parameters + ---------- + num_inputs : int + Number of input channels/features. + num_channels : list of int + Number of output channels for each temporal block. + kernel_size : int, default=2 + Size of convolutional kernels. + dropout : float, default=0.2 + Dropout rate for regularization. + """ + super().__init__() + self.num_inputs = num_inputs + self.num_channels = num_channels + self.kernel_size = kernel_size + self.dropout = dropout + + def _conv1d_with_variable_padding( + self, + x, + filters: int, + kernel_size: int, + padding_value: int, + stride: int = 1, + dilation_rate: int = 1, + ): + """Apply 1D convolution with variable padding for causal convolutions. + + Parameters + ---------- + x : tf.Tensor + Input tensor of shape (batch_size, channels, sequence_length). + filters : int + Number of output filters. + kernel_size : int + Size of the convolutional kernel. + padding_value : int + Amount of padding to apply. + stride : int, default=1 + Stride of the convolution. + dilation_rate : int, default=1 + Dilation rate for dilated convolutions. + + Returns + ------- + tf.Tensor + Output tensor after convolution. + """ + import tensorflow as tf + + # Transpose to Keras format (batch, sequence, channels) + x_keras_format = tf.keras.layers.Permute((2, 1))(x) + + # Apply padding in sequence dimension + padded_x = tf.keras.layers.ZeroPadding1D(padding=padding_value)(x_keras_format) + + # Create and apply convolution layer + conv_layer = tf.keras.layers.Conv1D( + filters=filters, + kernel_size=kernel_size, + strides=stride, + dilation_rate=dilation_rate, + padding="valid", + ) + + # Apply convolution + out = conv_layer(padded_x) + + # Transpose back to PyTorch format (batch, channels, sequence) + return tf.keras.layers.Permute((2, 1))(out) + + def _chomp_1d(self, x, chomp_size: int): + """Remove padding from the end of sequences to maintain causality. + + This operation ensures that the output at time t only depends on + inputs from times 0 to t, preventing information leakage from future. + + Parameters + ---------- + x : tf.Tensor + Input tensor of shape (batch_size, channels, sequence_length). + chomp_size : int + Number of time steps to remove from the end. + + Returns + ------- + tf.Tensor + Chomped tensor with reduced sequence length. + """ + return x[:, :, :-chomp_size] + + def _temporal_block( + self, + x, + n_inputs: int, + n_outputs: int, + kernel_size: int, + stride: int, + dilation: int, + padding: int, + dropout: float = 0.2, + training: bool = None, + ): + """Create a temporal block with dilated causal convolutions. + + Each temporal block consists of: + 1. Two dilated causal convolutions + 2. ReLU activations and dropout for regularization + 3. Residual connection with optional 1x1 convolution for dimension + matching + + Parameters + ---------- + x : tf.Tensor + Input tensor of shape (batch_size, channels, sequence_length). + n_inputs : int + Number of input channels. + n_outputs : int + Number of output channels. + kernel_size : int + Size of convolutional kernels. + stride : int + Stride of convolutions (typically 1). + dilation : int + Dilation factor for dilated convolutions. + padding : int + Padding size to be chomped off. + dropout : float, default=0.2 + Dropout rate for regularization. + training : bool, optional + Whether the model is in training mode. + + Returns + ------- + tf.Tensor + Output tensor of shape (batch_size, n_outputs, sequence_length). + """ + import tensorflow as tf + + # First convolution block + out = self._conv1d_with_variable_padding( + x, n_outputs, kernel_size, padding, stride, dilation + ) + out = self._chomp_1d(out, padding) + out = tf.keras.layers.ReLU()(out) + out = tf.keras.layers.Dropout(dropout)(out, training=training) + + # Second convolution block + out = self._conv1d_with_variable_padding( + out, n_outputs, kernel_size, padding, stride, dilation + ) + out = self._chomp_1d(out, padding) + out = tf.keras.layers.ReLU()(out) + out = tf.keras.layers.Dropout(dropout)(out, training=training) + + # Residual connection with optional dimension matching + if n_inputs != n_outputs: + res = self._conv1d_with_variable_padding(x, n_outputs, 1, 0, 1, 1) + else: + res = x + + # Add residual and apply final ReLU + result = tf.keras.layers.Add()([out, res]) + return tf.keras.layers.ReLU()(result) + + def _temporal_conv_net( + self, + x, + num_inputs: int, + num_channels: list, + kernel_size: int = 2, + dropout: float = 0.2, + training: bool = None, + ): + """Apply the complete Temporal Convolutional Network. + + Stacks multiple temporal blocks with exponentially increasing dilation + factors to achieve a large receptive field efficiently. + + Parameters + ---------- + x : tf.Tensor + Input tensor of shape (batch_size, channels, sequence_length). + num_inputs : int + Number of input channels. + num_channels : list of int + Number of output channels for each temporal block. + kernel_size : int, default=2 + Size of convolutional kernels. + dropout : float, default=0.2 + Dropout rate for regularization. + training : bool, optional + Whether the model is in training mode. + + Returns + ------- + tf.Tensor + Output tensor after applying all temporal blocks. + """ + num_levels = len(num_channels) + for i in range(num_levels): + dilation_size = 2**i + in_channels = num_inputs if i == 0 else num_channels[i - 1] + out_channels = num_channels[i] + padding = (kernel_size - 1) * dilation_size + + x = self._temporal_block( + x, + n_inputs=in_channels, + n_outputs=out_channels, + kernel_size=kernel_size, + stride=1, + dilation=dilation_size, + padding=padding, + dropout=dropout, + training=training, + ) + + return x + + def build_network(self, input_shape: tuple, **kwargs) -> tuple: + """Build the complete TCN architecture. + + Constructs a series of temporal blocks with exponentially increasing + dilation factors to achieve a large receptive field efficiently. + + Parameters + ---------- + input_shape : tuple + Shape of input data (sequence_length, num_features). + **kwargs + Additional keyword arguments (unused). + + Returns + ------- + tuple + A tuple containing (input_layer, output_tensor) representing + the complete network architecture. + + Notes + ----- + The dilation factor for layer i is 2^i, which ensures exponential + growth of the receptive field while maintaining computational + efficiency. + """ + import tensorflow as tf + + # Create input layer + input_layer = tf.keras.layers.Input(shape=input_shape) + + # Transpose input to match the expected format (batch, channels, seq) + x = input_layer + + # Apply TCN using the private function + x = self._temporal_conv_net( + x, + num_inputs=self.num_inputs, + num_channels=self.num_channels, + kernel_size=self.kernel_size, + dropout=self.dropout, + ) + + output = x + + return input_layer, output diff --git a/aeon/networks/tests/test_tcn.py b/aeon/networks/tests/test_tcn.py new file mode 100644 index 0000000000..97500f5557 --- /dev/null +++ b/aeon/networks/tests/test_tcn.py @@ -0,0 +1,212 @@ +"""Tests for the TemporalConvolutionalNetwork.""" + +import pytest + +from aeon.networks import TemporalConvolutionalNetwork +from aeon.utils.validation._dependencies import _check_soft_dependencies + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +def test_tcn_network_basic(): + """Test basic TCN network creation and build_network functionality.""" + import tensorflow as tf + + input_shape = (100, 5) + num_inputs = 5 + num_channels = [32, 64] + + tcn_network = TemporalConvolutionalNetwork( + num_inputs=num_inputs, num_channels=num_channels + ) + input_layer, output_layer = tcn_network.build_network(input_shape) + + # Check that layers are created correctly + assert hasattr(input_layer, "shape"), "Input layer should have a shape attribute" + assert hasattr(output_layer, "shape"), "Output layer should have a shape attribute" + assert input_layer.dtype == tf.float32 + assert output_layer.dtype == tf.float32 + + # Create a model to test the network structure + model = tf.keras.Model(inputs=input_layer, outputs=output_layer) + assert model is not None, "Model should be created successfully" + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +@pytest.mark.parametrize("num_channels", [[32], [32, 64], [16, 32, 64], [64, 32, 16]]) +def test_tcn_network_different_channels(num_channels): + """Test TCN network with different channel configurations.""" + import tensorflow as tf + + input_shape = (50, 3) + num_inputs = 3 + + tcn_network = TemporalConvolutionalNetwork( + num_inputs=num_inputs, num_channels=num_channels + ) + input_layer, output_layer = tcn_network.build_network(input_shape) + + # Create a model and verify it works + model = tf.keras.Model(inputs=input_layer, outputs=output_layer) + assert model is not None + + # Test with dummy data + import numpy as np + + dummy_input = np.random.random((8,) + input_shape) + output = model(dummy_input) + assert output is not None, "Model should produce output" + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +@pytest.mark.parametrize("kernel_size", [2, 3, 5]) +def test_tcn_network_kernel_sizes(kernel_size): + """Test TCN network with different kernel sizes.""" + import tensorflow as tf + + input_shape = (80, 4) + num_inputs = 4 + num_channels = [32, 64] + + tcn_network = TemporalConvolutionalNetwork( + num_inputs=num_inputs, + num_channels=num_channels, + kernel_size=kernel_size, + ) + input_layer, output_layer = tcn_network.build_network(input_shape) + + # Verify network builds successfully + model = tf.keras.Model(inputs=input_layer, outputs=output_layer) + assert model is not None + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +@pytest.mark.parametrize("dropout", [0.0, 0.1, 0.3, 0.5]) +def test_tcn_network_dropout_rates(dropout): + """Test TCN network with different dropout rates.""" + import tensorflow as tf + + input_shape = (60, 2) + num_inputs = 2 + num_channels = [16, 32] + + tcn_network = TemporalConvolutionalNetwork( + num_inputs=num_inputs, num_channels=num_channels, dropout=dropout + ) + input_layer, output_layer = tcn_network.build_network(input_shape) + + # Verify network builds successfully + model = tf.keras.Model(inputs=input_layer, outputs=output_layer) + assert model is not None + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +def test_tcn_network_output_shape(): + """Test TCN network output shapes.""" + import numpy as np + import tensorflow as tf + + input_shape = (40, 6) + batch_size = 16 + num_inputs = 6 + num_channels = [32, 64] + + tcn_network = TemporalConvolutionalNetwork( + num_inputs=num_inputs, num_channels=num_channels + ) + input_layer, output_layer = tcn_network.build_network(input_shape) + model = tf.keras.Model(inputs=input_layer, outputs=output_layer) + + # Create dummy input and test output shape + dummy_input = np.random.random((batch_size,) + input_shape) + output = model(dummy_input) + + # Output should maintain sequence length and have final channel dimension + expected_shape = (batch_size, num_channels[-1], input_shape[1]) + assert ( + output.shape == expected_shape + ), f"Expected shape {expected_shape}, got {output.shape}" + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +def test_tcn_network_config(): + """Test TCN network configuration attributes.""" + tcn_network = TemporalConvolutionalNetwork(num_inputs=3, num_channels=[16, 32]) + + # Check _config attributes + assert "python_dependencies" in tcn_network._config + assert "tensorflow" in tcn_network._config["python_dependencies"] + assert "python_version" in tcn_network._config + assert "structure" in tcn_network._config + assert tcn_network._config["structure"] == "encoder" + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +def test_tcn_network_parameter_initialization(): + """Test TCN network parameter initialization.""" + num_inputs = 4 + num_channels = [32, 64, 128] + kernel_size = 3 + dropout = 0.2 + + tcn_network = TemporalConvolutionalNetwork( + num_inputs=num_inputs, + num_channels=num_channels, + kernel_size=kernel_size, + dropout=dropout, + ) + + # Check that parameters are set correctly + assert tcn_network.num_inputs == num_inputs + assert tcn_network.num_channels == num_channels + assert tcn_network.kernel_size == kernel_size + assert tcn_network.dropout == dropout + + +@pytest.mark.skipif( + not _check_soft_dependencies(["tensorflow"], severity="none"), + reason="Tensorflow soft dependency unavailable.", +) +def test_tcn_network_single_layer(): + """Test TCN network with single temporal block.""" + import tensorflow as tf + + input_shape = (30, 2) + num_inputs = 2 + num_channels = [16] # Single layer + + tcn_network = TemporalConvolutionalNetwork( + num_inputs=num_inputs, num_channels=num_channels + ) + input_layer, output_layer = tcn_network.build_network(input_shape) + + # Verify single layer network works + model = tf.keras.Model(inputs=input_layer, outputs=output_layer) + assert model is not None + + # Test with dummy data + import numpy as np + + dummy_input = np.random.random((4,) + input_shape) + output = model(dummy_input) + assert output.shape == (4, 16, 2) From 3434757d2403729f4b07f67a960c41cc8250a4ff Mon Sep 17 00:00:00 2001 From: lucifer4073 Date: Sun, 6 Jul 2025 16:52:47 +0530 Subject: [PATCH 2/7] tcn_net pytest added --- .github/workflows/pr_pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_pytest.yml b/.github/workflows/pr_pytest.yml index 69323e47c5..368425d136 100644 --- a/.github/workflows/pr_pytest.yml +++ b/.github/workflows/pr_pytest.yml @@ -3,7 +3,7 @@ name: PR pytest on: push: branches: - - main + - tcn_net pull_request: paths: - "aeon/**" From c602e39cb5edb82537cd697096536f9b9733fb38 Mon Sep 17 00:00:00 2001 From: lucifer4073 Date: Sun, 6 Jul 2025 17:29:00 +0530 Subject: [PATCH 3/7] tcn_network updated with default params --- aeon/networks/_tcn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aeon/networks/_tcn.py b/aeon/networks/_tcn.py index 08f4ff9341..900b9e47c1 100644 --- a/aeon/networks/_tcn.py +++ b/aeon/networks/_tcn.py @@ -52,8 +52,8 @@ class TemporalConvolutionalNetwork(BaseDeepLearningNetwork): def __init__( self, - num_inputs: int, - num_channels: list, + num_inputs: int = 1, + num_channels: list = [16] * 3, kernel_size: int = 2, dropout: float = 0.2, ): From 2f3c98b9008d8a7504f24dcd1d22760261a4e1b3 Mon Sep 17 00:00:00 2001 From: lucifer4073 Date: Mon, 7 Jul 2025 23:43:22 +0530 Subject: [PATCH 4/7] tcn reshaped --- aeon/networks/_tcn.py | 23 ++++++++++++++++++++--- aeon/networks/tests/test_tcn.py | 4 ++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/aeon/networks/_tcn.py b/aeon/networks/_tcn.py index 900b9e47c1..987b0f2b68 100644 --- a/aeon/networks/_tcn.py +++ b/aeon/networks/_tcn.py @@ -42,6 +42,21 @@ class TemporalConvolutionalNetwork(BaseDeepLearningNetwork): Bai, S., Kolter, J. Z., & Koltun, V. (2018). An empirical evaluation of generic convolutional and recurrent networks for sequence modeling. arXiv preprint arXiv:1803.01271. + + Examples + -------- + >>> from aeon.networks._tcn import TemporalConvolutionalNetwork + >>> from aeon.testing.data_generation import make_example_3d_numpy + >>> import tensorflow as tf + >>> X, y = make_example_3d_numpy(n_cases=8, n_channels=4, n_timepoints=150, + ... return_y=True, regression_target=True, + ... random_state=42) + >>> network = TemporalConvolutionalNetwork(num_inputs=4, num_channels=[8, 8]) + >>> input_layer, output = network.build_network(input_shape=(4, 150)) + >>> model = tf.keras.Model(inputs=input_layer, outputs=output) + >>> model.compile(optimizer="adam", loss="mse") + >>> model.fit(X, y, epochs=2, batch_size=2, verbose=0) # doctest: +SKIP + """ _config = { @@ -53,7 +68,7 @@ class TemporalConvolutionalNetwork(BaseDeepLearningNetwork): def __init__( self, num_inputs: int = 1, - num_channels: list = [16] * 3, + num_channels: list = [16] * 3, # change to n_filters kernel_size: int = 2, dropout: float = 0.2, ): @@ -321,6 +336,8 @@ def build_network(self, input_shape: tuple, **kwargs) -> tuple: dropout=self.dropout, ) - output = x - + x = tf.keras.layers.Dense(input_shape[0])(x[:, -1, :]) + output = tf.keras.layers.Lambda( + lambda x: tf.reduce_mean(x, axis=1, keepdims=True), output_shape=(1,) + )(x) return input_layer, output diff --git a/aeon/networks/tests/test_tcn.py b/aeon/networks/tests/test_tcn.py index 97500f5557..b78424a73a 100644 --- a/aeon/networks/tests/test_tcn.py +++ b/aeon/networks/tests/test_tcn.py @@ -136,7 +136,7 @@ def test_tcn_network_output_shape(): output = model(dummy_input) # Output should maintain sequence length and have final channel dimension - expected_shape = (batch_size, num_channels[-1], input_shape[1]) + expected_shape = (batch_size, 1) assert ( output.shape == expected_shape ), f"Expected shape {expected_shape}, got {output.shape}" @@ -209,4 +209,4 @@ def test_tcn_network_single_layer(): dummy_input = np.random.random((4,) + input_shape) output = model(dummy_input) - assert output.shape == (4, 16, 2) + assert output.shape == (4, 1) From f6447b180c4b725d889dbed5b80fe948945bbd6c Mon Sep 17 00:00:00 2001 From: lucifer4073 Date: Tue, 8 Jul 2025 13:34:47 +0530 Subject: [PATCH 5/7] tcn changed --- aeon/networks/_tcn.py | 22 ++++++++++----------- aeon/networks/tests/test_tcn.py | 34 ++++++++++++++++----------------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/aeon/networks/_tcn.py b/aeon/networks/_tcn.py index 987b0f2b68..62ed404f1b 100644 --- a/aeon/networks/_tcn.py +++ b/aeon/networks/_tcn.py @@ -68,7 +68,7 @@ class TemporalConvolutionalNetwork(BaseDeepLearningNetwork): def __init__( self, num_inputs: int = 1, - num_channels: list = [16] * 3, # change to n_filters + n_filters: list = [16] * 3, # changed from num_channels kernel_size: int = 2, dropout: float = 0.2, ): @@ -78,7 +78,7 @@ def __init__( ---------- num_inputs : int Number of input channels/features. - num_channels : list of int + n_filters : list of int Number of output channels for each temporal block. kernel_size : int, default=2 Size of convolutional kernels. @@ -87,7 +87,7 @@ def __init__( """ super().__init__() self.num_inputs = num_inputs - self.num_channels = num_channels + self.n_filters = n_filters self.kernel_size = kernel_size self.dropout = dropout @@ -243,7 +243,7 @@ def _temporal_conv_net( self, x, num_inputs: int, - num_channels: list, + n_filters: list, # changed from num_channels kernel_size: int = 2, dropout: float = 0.2, training: bool = None, @@ -259,7 +259,7 @@ def _temporal_conv_net( Input tensor of shape (batch_size, channels, sequence_length). num_inputs : int Number of input channels. - num_channels : list of int + n_filters : list of int Number of output channels for each temporal block. kernel_size : int, default=2 Size of convolutional kernels. @@ -273,11 +273,11 @@ def _temporal_conv_net( tf.Tensor Output tensor after applying all temporal blocks. """ - num_levels = len(num_channels) + num_levels = len(n_filters) for i in range(num_levels): dilation_size = 2**i - in_channels = num_inputs if i == 0 else num_channels[i - 1] - out_channels = num_channels[i] + in_channels = num_inputs if i == 0 else n_filters[i - 1] + out_channels = n_filters[i] padding = (kernel_size - 1) * dilation_size x = self._temporal_block( @@ -331,13 +331,11 @@ def build_network(self, input_shape: tuple, **kwargs) -> tuple: x = self._temporal_conv_net( x, num_inputs=self.num_inputs, - num_channels=self.num_channels, + n_filters=self.n_filters, kernel_size=self.kernel_size, dropout=self.dropout, ) x = tf.keras.layers.Dense(input_shape[0])(x[:, -1, :]) - output = tf.keras.layers.Lambda( - lambda x: tf.reduce_mean(x, axis=1, keepdims=True), output_shape=(1,) - )(x) + output = tf.keras.layers.Dense(1)(x) return input_layer, output diff --git a/aeon/networks/tests/test_tcn.py b/aeon/networks/tests/test_tcn.py index b78424a73a..47c1d38615 100644 --- a/aeon/networks/tests/test_tcn.py +++ b/aeon/networks/tests/test_tcn.py @@ -16,10 +16,10 @@ def test_tcn_network_basic(): input_shape = (100, 5) num_inputs = 5 - num_channels = [32, 64] + n_filters = [32, 64] tcn_network = TemporalConvolutionalNetwork( - num_inputs=num_inputs, num_channels=num_channels + num_inputs=num_inputs, n_filters=n_filters ) input_layer, output_layer = tcn_network.build_network(input_shape) @@ -38,8 +38,8 @@ def test_tcn_network_basic(): not _check_soft_dependencies(["tensorflow"], severity="none"), reason="Tensorflow soft dependency unavailable.", ) -@pytest.mark.parametrize("num_channels", [[32], [32, 64], [16, 32, 64], [64, 32, 16]]) -def test_tcn_network_different_channels(num_channels): +@pytest.mark.parametrize("n_filters", [[32], [32, 64], [16, 32, 64], [64, 32, 16]]) +def test_tcn_network_different_channels(n_filters): """Test TCN network with different channel configurations.""" import tensorflow as tf @@ -47,7 +47,7 @@ def test_tcn_network_different_channels(num_channels): num_inputs = 3 tcn_network = TemporalConvolutionalNetwork( - num_inputs=num_inputs, num_channels=num_channels + num_inputs=num_inputs, n_filters=n_filters ) input_layer, output_layer = tcn_network.build_network(input_shape) @@ -74,11 +74,11 @@ def test_tcn_network_kernel_sizes(kernel_size): input_shape = (80, 4) num_inputs = 4 - num_channels = [32, 64] + n_filters = [32, 64] tcn_network = TemporalConvolutionalNetwork( num_inputs=num_inputs, - num_channels=num_channels, + n_filters=n_filters, kernel_size=kernel_size, ) input_layer, output_layer = tcn_network.build_network(input_shape) @@ -99,10 +99,10 @@ def test_tcn_network_dropout_rates(dropout): input_shape = (60, 2) num_inputs = 2 - num_channels = [16, 32] + n_filters = [16, 32] tcn_network = TemporalConvolutionalNetwork( - num_inputs=num_inputs, num_channels=num_channels, dropout=dropout + num_inputs=num_inputs, n_filters=n_filters, dropout=dropout ) input_layer, output_layer = tcn_network.build_network(input_shape) @@ -123,10 +123,10 @@ def test_tcn_network_output_shape(): input_shape = (40, 6) batch_size = 16 num_inputs = 6 - num_channels = [32, 64] + n_filters = [32, 64] tcn_network = TemporalConvolutionalNetwork( - num_inputs=num_inputs, num_channels=num_channels + num_inputs=num_inputs, n_filters=n_filters ) input_layer, output_layer = tcn_network.build_network(input_shape) model = tf.keras.Model(inputs=input_layer, outputs=output_layer) @@ -148,7 +148,7 @@ def test_tcn_network_output_shape(): ) def test_tcn_network_config(): """Test TCN network configuration attributes.""" - tcn_network = TemporalConvolutionalNetwork(num_inputs=3, num_channels=[16, 32]) + tcn_network = TemporalConvolutionalNetwork(num_inputs=3, n_filters=[16, 32]) # Check _config attributes assert "python_dependencies" in tcn_network._config @@ -165,20 +165,20 @@ def test_tcn_network_config(): def test_tcn_network_parameter_initialization(): """Test TCN network parameter initialization.""" num_inputs = 4 - num_channels = [32, 64, 128] + n_filters = [32, 64, 128] kernel_size = 3 dropout = 0.2 tcn_network = TemporalConvolutionalNetwork( num_inputs=num_inputs, - num_channels=num_channels, + n_filters=n_filters, kernel_size=kernel_size, dropout=dropout, ) # Check that parameters are set correctly assert tcn_network.num_inputs == num_inputs - assert tcn_network.num_channels == num_channels + assert tcn_network.n_filters == n_filters assert tcn_network.kernel_size == kernel_size assert tcn_network.dropout == dropout @@ -193,10 +193,10 @@ def test_tcn_network_single_layer(): input_shape = (30, 2) num_inputs = 2 - num_channels = [16] # Single layer + n_filters = [16] # Single layer tcn_network = TemporalConvolutionalNetwork( - num_inputs=num_inputs, num_channels=num_channels + num_inputs=num_inputs, n_filters=n_filters ) input_layer, output_layer = tcn_network.build_network(input_shape) From 7bacdac1a9df547330c08691ab89d4b0ece3a23d Mon Sep 17 00:00:00 2001 From: lucifer4073 Date: Tue, 8 Jul 2025 20:28:53 +0530 Subject: [PATCH 6/7] tcn updated --- aeon/networks/__init__.py | 4 +- aeon/networks/_tcn.py | 138 ++++++++++++++++---------------- aeon/networks/tests/test_tcn.py | 62 +++++--------- 3 files changed, 92 insertions(+), 112 deletions(-) diff --git a/aeon/networks/__init__.py b/aeon/networks/__init__.py index 61e669283c..aed37be7e7 100644 --- a/aeon/networks/__init__.py +++ b/aeon/networks/__init__.py @@ -19,7 +19,7 @@ "AEBiGRUNetwork", "DisjointCNNNetwork", "RecurrentNetwork", - "TemporalConvolutionalNetwork", + "TCNNetwork", ] from aeon.networks._ae_abgru import AEAttentionBiGRUNetwork from aeon.networks._ae_bgru import AEBiGRUNetwork @@ -37,5 +37,5 @@ from aeon.networks._mlp import MLPNetwork from aeon.networks._resnet import ResNetNetwork from aeon.networks._rnn import RecurrentNetwork -from aeon.networks._tcn import TemporalConvolutionalNetwork +from aeon.networks._tcn import TCNNetwork from aeon.networks.base import BaseDeepLearningNetwork diff --git a/aeon/networks/_tcn.py b/aeon/networks/_tcn.py index 62ed404f1b..5400a557b7 100644 --- a/aeon/networks/_tcn.py +++ b/aeon/networks/_tcn.py @@ -1,15 +1,11 @@ -"""Implementation of Temporal Convolutional Network (TCN). - -Based on the paper "An Empirical Evaluation of Generic Convolutional and -Recurrent Networks for Sequence Modeling" by Bai et al. (2018). -""" +"""Implementation of Temporal Convolutional Network (TCN).""" __maintainer__ = [] from aeon.networks.base import BaseDeepLearningNetwork -class TemporalConvolutionalNetwork(BaseDeepLearningNetwork): +class TCNNetwork(BaseDeepLearningNetwork): """Temporal Convolutional Network (TCN) for sequence modeling. A generic convolutional architecture for sequence modeling that combines: @@ -21,9 +17,7 @@ class TemporalConvolutionalNetwork(BaseDeepLearningNetwork): Parameters ---------- - num_inputs : int - Number of input channels/features in the input sequence. - num_channels : list of int + n_blocks : list of int List specifying the number of output channels for each layer. The length determines the depth of the network. kernel_size : int, default=2 @@ -39,19 +33,19 @@ class TemporalConvolutionalNetwork(BaseDeepLearningNetwork): References ---------- - Bai, S., Kolter, J. Z., & Koltun, V. (2018). An empirical evaluation of + .. [1] Bai, S., Kolter, J. Z., & Koltun, V. (2018). An empirical evaluation of generic convolutional and recurrent networks for sequence modeling. arXiv preprint arXiv:1803.01271. Examples -------- - >>> from aeon.networks._tcn import TemporalConvolutionalNetwork + >>> from aeon.networks._tcn import TCNNetwork >>> from aeon.testing.data_generation import make_example_3d_numpy >>> import tensorflow as tf >>> X, y = make_example_3d_numpy(n_cases=8, n_channels=4, n_timepoints=150, ... return_y=True, regression_target=True, ... random_state=42) - >>> network = TemporalConvolutionalNetwork(num_inputs=4, num_channels=[8, 8]) + >>> network = TCNNetwork(num_channels=[8, 8]) >>> input_layer, output = network.build_network(input_shape=(4, 150)) >>> model = tf.keras.Model(inputs=input_layer, outputs=output) >>> model.compile(optimizer="adam", loss="mse") @@ -67,8 +61,7 @@ class TemporalConvolutionalNetwork(BaseDeepLearningNetwork): def __init__( self, - num_inputs: int = 1, - n_filters: list = [16] * 3, # changed from num_channels + n_blocks: list = [16] * 3, kernel_size: int = 2, dropout: float = 0.2, ): @@ -78,7 +71,7 @@ def __init__( ---------- num_inputs : int Number of input channels/features. - n_filters : list of int + n_blocks : list of int Number of output channels for each temporal block. kernel_size : int, default=2 Size of convolutional kernels. @@ -86,33 +79,32 @@ def __init__( Dropout rate for regularization. """ super().__init__() - self.num_inputs = num_inputs - self.n_filters = n_filters + self.n_blocks = n_blocks self.kernel_size = kernel_size self.dropout = dropout def _conv1d_with_variable_padding( self, - x, - filters: int, + input_tensor, + n_filters: int, kernel_size: int, padding_value: int, - stride: int = 1, + strides: int = 1, dilation_rate: int = 1, ): """Apply 1D convolution with variable padding for causal convolutions. Parameters ---------- - x : tf.Tensor + input_tensor : tf.Tensor Input tensor of shape (batch_size, channels, sequence_length). - filters : int + n_filters : int Number of output filters. kernel_size : int Size of the convolutional kernel. padding_value : int Amount of padding to apply. - stride : int, default=1 + strides : int, default=1 Stride of the convolution. dilation_rate : int, default=1 Dilation rate for dilated convolutions. @@ -125,16 +117,16 @@ def _conv1d_with_variable_padding( import tensorflow as tf # Transpose to Keras format (batch, sequence, channels) - x_keras_format = tf.keras.layers.Permute((2, 1))(x) + x_keras_format = tf.keras.layers.Permute((2, 1))(input_tensor) # Apply padding in sequence dimension padded_x = tf.keras.layers.ZeroPadding1D(padding=padding_value)(x_keras_format) # Create and apply convolution layer conv_layer = tf.keras.layers.Conv1D( - filters=filters, + filters=n_filters, kernel_size=kernel_size, - strides=stride, + strides=strides, dilation_rate=dilation_rate, padding="valid", ) @@ -145,7 +137,7 @@ def _conv1d_with_variable_padding( # Transpose back to PyTorch format (batch, channels, sequence) return tf.keras.layers.Permute((2, 1))(out) - def _chomp_1d(self, x, chomp_size: int): + def _chomp(self, input_tensor, chomp_size: int): """Remove padding from the end of sequences to maintain causality. This operation ensures that the output at time t only depends on @@ -153,7 +145,7 @@ def _chomp_1d(self, x, chomp_size: int): Parameters ---------- - x : tf.Tensor + input_tensor : tf.Tensor Input tensor of shape (batch_size, channels, sequence_length). chomp_size : int Number of time steps to remove from the end. @@ -163,17 +155,17 @@ def _chomp_1d(self, x, chomp_size: int): tf.Tensor Chomped tensor with reduced sequence length. """ - return x[:, :, :-chomp_size] + return input_tensor[:, :, :-chomp_size] def _temporal_block( self, - x, + input_tensor, n_inputs: int, - n_outputs: int, + n_filters: int, kernel_size: int, - stride: int, - dilation: int, - padding: int, + strides: int, + dilation_rate: int, + padding_value: int, dropout: float = 0.2, training: bool = None, ): @@ -187,19 +179,19 @@ def _temporal_block( Parameters ---------- - x : tf.Tensor + input_tensor : tf.Tensor Input tensor of shape (batch_size, channels, sequence_length). n_inputs : int Number of input channels. - n_outputs : int - Number of output channels. + n_filters : int + Number of output filters. kernel_size : int Size of convolutional kernels. - stride : int + strides : int Stride of convolutions (typically 1). - dilation : int + dilation_rate : int Dilation factor for dilated convolutions. - padding : int + padding_value : int Padding size to be chomped off. dropout : float, default=0.2 Dropout rate for regularization. @@ -209,31 +201,38 @@ def _temporal_block( Returns ------- tf.Tensor - Output tensor of shape (batch_size, n_outputs, sequence_length). + Output tensor of shape (batch_size, n_filters, sequence_length). """ import tensorflow as tf # First convolution block out = self._conv1d_with_variable_padding( - x, n_outputs, kernel_size, padding, stride, dilation + input_tensor, n_filters, kernel_size, padding_value, strides, dilation_rate ) - out = self._chomp_1d(out, padding) + out = self._chomp(out, padding_value) out = tf.keras.layers.ReLU()(out) out = tf.keras.layers.Dropout(dropout)(out, training=training) # Second convolution block out = self._conv1d_with_variable_padding( - out, n_outputs, kernel_size, padding, stride, dilation + out, n_filters, kernel_size, padding_value, strides, dilation_rate ) - out = self._chomp_1d(out, padding) + out = self._chomp(out, padding_value) out = tf.keras.layers.ReLU()(out) out = tf.keras.layers.Dropout(dropout)(out, training=training) # Residual connection with optional dimension matching - if n_inputs != n_outputs: - res = self._conv1d_with_variable_padding(x, n_outputs, 1, 0, 1, 1) + if n_inputs != n_filters: + res = self._conv1d_with_variable_padding( + input_tensor=input_tensor, + n_filters=n_filters, + kernel_size=1, + padding_value=0, + strides=1, + dilation_rate=1, + ) else: - res = x + res = input_tensor # Add residual and apply final ReLU result = tf.keras.layers.Add()([out, res]) @@ -241,9 +240,9 @@ def _temporal_block( def _temporal_conv_net( self, - x, - num_inputs: int, - n_filters: list, # changed from num_channels + input_tensor, + n_inputs: int, + n_blocks: list, kernel_size: int = 2, dropout: float = 0.2, training: bool = None, @@ -255,11 +254,11 @@ def _temporal_conv_net( Parameters ---------- - x : tf.Tensor + input_tensor : tf.Tensor Input tensor of shape (batch_size, channels, sequence_length). - num_inputs : int + n_inputs : int Number of input channels. - n_filters : list of int + n_blocks : list of int Number of output channels for each temporal block. kernel_size : int, default=2 Size of convolutional kernels. @@ -273,26 +272,26 @@ def _temporal_conv_net( tf.Tensor Output tensor after applying all temporal blocks. """ - num_levels = len(n_filters) + num_levels = len(n_blocks) for i in range(num_levels): - dilation_size = 2**i - in_channels = num_inputs if i == 0 else n_filters[i - 1] - out_channels = n_filters[i] - padding = (kernel_size - 1) * dilation_size + dilation_rate = 2**i + in_channels = n_inputs if i == 0 else n_blocks[i - 1] + out_channels = n_blocks[i] + padding_value = (kernel_size - 1) * dilation_rate - x = self._temporal_block( - x, + input_tensor = self._temporal_block( + input_tensor, n_inputs=in_channels, - n_outputs=out_channels, + n_filters=out_channels, kernel_size=kernel_size, - stride=1, - dilation=dilation_size, - padding=padding, + strides=1, + dilation_rate=dilation_rate, + padding_value=padding_value, dropout=dropout, training=training, ) - return x + return input_tensor def build_network(self, input_shape: tuple, **kwargs) -> tuple: """Build the complete TCN architecture. @@ -303,7 +302,7 @@ def build_network(self, input_shape: tuple, **kwargs) -> tuple: Parameters ---------- input_shape : tuple - Shape of input data (sequence_length, num_features). + Shape of input data (n_channels, n_timepoints). **kwargs Additional keyword arguments (unused). @@ -326,12 +325,13 @@ def build_network(self, input_shape: tuple, **kwargs) -> tuple: # Transpose input to match the expected format (batch, channels, seq) x = input_layer + n_inputs = input_shape[0] # Apply TCN using the private function x = self._temporal_conv_net( x, - num_inputs=self.num_inputs, - n_filters=self.n_filters, + n_inputs=n_inputs, + n_blocks=self.n_blocks, kernel_size=self.kernel_size, dropout=self.dropout, ) diff --git a/aeon/networks/tests/test_tcn.py b/aeon/networks/tests/test_tcn.py index 47c1d38615..94495e3c41 100644 --- a/aeon/networks/tests/test_tcn.py +++ b/aeon/networks/tests/test_tcn.py @@ -1,8 +1,8 @@ -"""Tests for the TemporalConvolutionalNetwork.""" +"""Tests for the TCNNetwork.""" import pytest -from aeon.networks import TemporalConvolutionalNetwork +from aeon.networks import TCNNetwork from aeon.utils.validation._dependencies import _check_soft_dependencies @@ -15,12 +15,9 @@ def test_tcn_network_basic(): import tensorflow as tf input_shape = (100, 5) - num_inputs = 5 - n_filters = [32, 64] + n_blocks = [32, 64] - tcn_network = TemporalConvolutionalNetwork( - num_inputs=num_inputs, n_filters=n_filters - ) + tcn_network = TCNNetwork(n_blocks=n_blocks) input_layer, output_layer = tcn_network.build_network(input_shape) # Check that layers are created correctly @@ -38,17 +35,14 @@ def test_tcn_network_basic(): not _check_soft_dependencies(["tensorflow"], severity="none"), reason="Tensorflow soft dependency unavailable.", ) -@pytest.mark.parametrize("n_filters", [[32], [32, 64], [16, 32, 64], [64, 32, 16]]) -def test_tcn_network_different_channels(n_filters): +@pytest.mark.parametrize("n_blocks", [[32], [32, 64], [16, 32, 64], [64, 32, 16]]) +def test_tcn_network_different_channels(n_blocks): """Test TCN network with different channel configurations.""" import tensorflow as tf input_shape = (50, 3) - num_inputs = 3 - tcn_network = TemporalConvolutionalNetwork( - num_inputs=num_inputs, n_filters=n_filters - ) + tcn_network = TCNNetwork(n_blocks=n_blocks) input_layer, output_layer = tcn_network.build_network(input_shape) # Create a model and verify it works @@ -73,12 +67,10 @@ def test_tcn_network_kernel_sizes(kernel_size): import tensorflow as tf input_shape = (80, 4) - num_inputs = 4 - n_filters = [32, 64] + n_blocks = [32, 64] - tcn_network = TemporalConvolutionalNetwork( - num_inputs=num_inputs, - n_filters=n_filters, + tcn_network = TCNNetwork( + n_blocks=n_blocks, kernel_size=kernel_size, ) input_layer, output_layer = tcn_network.build_network(input_shape) @@ -98,12 +90,9 @@ def test_tcn_network_dropout_rates(dropout): import tensorflow as tf input_shape = (60, 2) - num_inputs = 2 - n_filters = [16, 32] + n_blocks = [16, 32] - tcn_network = TemporalConvolutionalNetwork( - num_inputs=num_inputs, n_filters=n_filters, dropout=dropout - ) + tcn_network = TCNNetwork(n_blocks=n_blocks, dropout=dropout) input_layer, output_layer = tcn_network.build_network(input_shape) # Verify network builds successfully @@ -122,12 +111,9 @@ def test_tcn_network_output_shape(): input_shape = (40, 6) batch_size = 16 - num_inputs = 6 - n_filters = [32, 64] + n_blocks = [32, 64] - tcn_network = TemporalConvolutionalNetwork( - num_inputs=num_inputs, n_filters=n_filters - ) + tcn_network = TCNNetwork(n_blocks=n_blocks) input_layer, output_layer = tcn_network.build_network(input_shape) model = tf.keras.Model(inputs=input_layer, outputs=output_layer) @@ -148,7 +134,7 @@ def test_tcn_network_output_shape(): ) def test_tcn_network_config(): """Test TCN network configuration attributes.""" - tcn_network = TemporalConvolutionalNetwork(num_inputs=3, n_filters=[16, 32]) + tcn_network = TCNNetwork(n_blocks=[16, 32]) # Check _config attributes assert "python_dependencies" in tcn_network._config @@ -164,21 +150,18 @@ def test_tcn_network_config(): ) def test_tcn_network_parameter_initialization(): """Test TCN network parameter initialization.""" - num_inputs = 4 - n_filters = [32, 64, 128] + n_blocks = [32, 64, 128] kernel_size = 3 dropout = 0.2 - tcn_network = TemporalConvolutionalNetwork( - num_inputs=num_inputs, - n_filters=n_filters, + tcn_network = TCNNetwork( + n_blocks=n_blocks, kernel_size=kernel_size, dropout=dropout, ) # Check that parameters are set correctly - assert tcn_network.num_inputs == num_inputs - assert tcn_network.n_filters == n_filters + assert tcn_network.n_blocks == n_blocks assert tcn_network.kernel_size == kernel_size assert tcn_network.dropout == dropout @@ -192,12 +175,9 @@ def test_tcn_network_single_layer(): import tensorflow as tf input_shape = (30, 2) - num_inputs = 2 - n_filters = [16] # Single layer + n_blocks = [16] # Single layer - tcn_network = TemporalConvolutionalNetwork( - num_inputs=num_inputs, n_filters=n_filters - ) + tcn_network = TCNNetwork(n_blocks=n_blocks) input_layer, output_layer = tcn_network.build_network(input_shape) # Verify single layer network works From 08dadeca78beba0b1c2a6685e003950366603e58 Mon Sep 17 00:00:00 2001 From: lucifer4073 Date: Tue, 8 Jul 2025 20:49:27 +0530 Subject: [PATCH 7/7] doctest corrected --- aeon/networks/_tcn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aeon/networks/_tcn.py b/aeon/networks/_tcn.py index 5400a557b7..834b5865e7 100644 --- a/aeon/networks/_tcn.py +++ b/aeon/networks/_tcn.py @@ -45,7 +45,7 @@ class TCNNetwork(BaseDeepLearningNetwork): >>> X, y = make_example_3d_numpy(n_cases=8, n_channels=4, n_timepoints=150, ... return_y=True, regression_target=True, ... random_state=42) - >>> network = TCNNetwork(num_channels=[8, 8]) + >>> network = TCNNetwork(n_blocks=[8, 8]) >>> input_layer, output = network.build_network(input_shape=(4, 150)) >>> model = tf.keras.Model(inputs=input_layer, outputs=output) >>> model.compile(optimizer="adam", loss="mse")