Repository: ludwig-ai/ludwig

Issue:
Migrate time series features to PyTorch



Discussion:

Resolving patch:
diff --git a/ludwig/encoders/sequence_encoders.py b/ludwig/encoders/sequence_encoders.py
--- a/ludwig/encoders/sequence_encoders.py
+++ b/ludwig/encoders/sequence_encoders.py
@@ -1843,7 +1843,7 @@ def __init__(
         if self.reduce_output is not None:
             logger.debug('  FCStack')
             self.fc_stack = FCStack(
-                self.conv1d_stack.output_shape[0],
+                self.conv1d_stack.output_shape[-1],
                 layers=fc_layers,
                 num_layers=num_fc_layers,
                 default_fc_size=fc_size,
diff --git a/ludwig/features/timeseries_feature.py b/ludwig/features/timeseries_feature.py
--- a/ludwig/features/timeseries_feature.py
+++ b/ludwig/features/timeseries_feature.py
@@ -155,15 +155,20 @@ class TimeseriesInputFeature(TimeseriesFeatureMixin, SequenceInputFeature):
     max_sequence_length = None
 
     def __init__(self, feature, encoder_obj=None):
+        # add required sequence encoder parameters for time series
+        feature['embedding_size'] = 1
+        feature['should_embed'] = False
+
+        # initialize encoder for time series
         super().__init__(feature, encoder_obj=encoder_obj)
 
-    def call(self, inputs, training=None, mask=None):
-        assert isinstance(inputs, tf.Tensor)
-        assert inputs.dtype == tf.float16 or inputs.dtype == tf.float32 or \
-               inputs.dtype == tf.float64
+    def forward(self, inputs, training=None, mask=None):
+        assert isinstance(inputs, torch.Tensor)
+        assert inputs.dtype == torch.float16 or inputs.dtype == torch.float32 \
+               or inputs.dtype == torch.float64
         assert len(inputs.shape) == 2
 
-        inputs_exp = tf.cast(inputs, dtype=tf.float32)
+        inputs_exp = inputs.type(torch.float32)
         encoder_output = self.encoder_obj(
             inputs_exp, training=training, mask=mask
         )
@@ -183,8 +188,6 @@ def update_config_with_metadata(
     ):
         input_feature['max_sequence_length'] = feature_metadata[
             'max_timeseries_length']
-        input_feature['embedding_size'] = 1
-        input_feature['should_embed'] = False
 
     @staticmethod
     def populate_defaults(input_feature):


Test patch:
diff --git a/tests/integration_tests/test_timeseries_feature.py b/tests/integration_tests/test_timeseries_feature.py
new file mode 100644
--- /dev/null
+++ b/tests/integration_tests/test_timeseries_feature.py
@@ -0,0 +1,49 @@
+import pytest
+
+import torch
+
+from ludwig.features.timeseries_feature import TimeseriesInputFeature
+from tests.integration_tests.utils import timeseries_feature
+
+BATCH_SIZE = 2
+SEQ_SIZE = 10
+DEFAULT_FC_SIZE = 4
+
+
+@pytest.mark.parametrize(
+    'enc_encoder',
+    [
+        'stacked_cnn', 'parallel_cnn', 'stacked_parallel_cnn', 'rnn', 'cnnrnn',
+        'passthrough'
+    ]
+)
+def test_timeseries_feature(enc_encoder):
+    # synthetic time series tensor
+    timeseries_tensor = torch.randn([BATCH_SIZE, SEQ_SIZE],
+                                    dtype=torch.float32)
+
+    # generate feature config
+    timeseries_feature_config = timeseries_feature(
+        encoder=enc_encoder,
+        max_len=SEQ_SIZE,
+        fc_layers=[{'fc_size': DEFAULT_FC_SIZE}],
+        # simulated parameters determined by pre-processing
+        max_sequence_length=SEQ_SIZE,
+    )
+
+    # instantiate input feature object
+    timeseries_input_feature = TimeseriesInputFeature(timeseries_feature_config)
+
+    # pass synthetic tensor through input feature
+    encoder_output = timeseries_input_feature(timeseries_tensor)
+
+    # confirm correctness of the encoder output
+    assert isinstance(encoder_output, dict)
+    assert 'encoder_output' in encoder_output
+    assert isinstance(encoder_output['encoder_output'], torch.Tensor)
+    if enc_encoder == 'passthrough':
+        assert encoder_output['encoder_output'].shape \
+               == (BATCH_SIZE, SEQ_SIZE, 1)
+    else:
+        assert encoder_output['encoder_output'].shape \
+               == (BATCH_SIZE, DEFAULT_FC_SIZE)