Repository: ludwig-ai/ludwig

Issue:
[tf-legacy] input_features type mismatch in combiners
**Describe the bug**
On the `tf-legacy` branch, combiners expect `input_features` to be a list, but `build_inputs()` outputs an `OrderedDict`. In particular, this causes the `TabTransformerCombiner` to break.

**To Reproduce**
Attempt to train a model using the `titanic` dataset. Sample config:

```
input_features:
  - name: Pclass
    type: category
    column: Pclass
  - name: Sex
    type: category
    column: Sex
  - name: Age
    type: numerical
    column: Age
  - name: SibSp
    type: numerical
    column: SibSp
  - name: Parch
    type: numerical
    column: Parch
  - name: Ticket
    type: category
    column: Ticket
  - name: Fare
    type: numerical
    column: Fare
  - name: Cabin
    type: category
    column: Cabin
  - name: Embarked
    type: category
    column: Embarked
output_features:
  - name: Survived
    type: category
    column: Survived
combiner:
  type: tabtransformer
  num_layers: 1
  hidden_size: 256
  num_heads: 8
  transformer_fc_size: 256
  dropout: 0.1
  num_fc_layers: 0
  fc_size: 256
  use_bias: true
  weights_initializer: glorot_uniform
  bias_initializer: zeros
  fc_activation: relu
  fc_dropout: 0
  fc_residual: false
  reduce_output: concat
```

**Expected behavior**
Training succeeds.



Discussion:

Resolving patch:
diff --git a/ludwig/combiners/combiners.py b/ludwig/combiners/combiners.py
--- a/ludwig/combiners/combiners.py
+++ b/ludwig/combiners/combiners.py
@@ -79,7 +79,7 @@ class Meta:
 class ConcatCombiner(tf.keras.Model):
     def __init__(
             self,
-            input_features: Optional[List] = None,
+            input_features: Optional[Dict] = None,
             config: ConcatCombinerConfig = None,
             **kwargs
     ):
@@ -507,7 +507,7 @@ class Meta:
 class TransformerCombiner(tf.keras.Model):
     def __init__(
             self,
-            input_features: Optional[List] = None,
+            input_features: Optional[Dict] = None,
             config: TransformerCombinerConfig = None,
             **kwargs
     ):
@@ -638,7 +638,7 @@ class Meta:
 class TabTransformerCombiner(tf.keras.Model):
     def __init__(
             self,
-            input_features: Optional[List] = None,
+            input_features: Optional[Dict] = None,
             config: TabTransformerCombinerConfig = None,
             **kwargs
     ):
@@ -655,8 +655,9 @@ def __init__(
 
         self.embed_input_feature_name = config.embed_input_feature_name
         if self.embed_input_feature_name:
-            vocab = [i_f for i_f in input_features
-                     if i_f[TYPE] != NUMERICAL or i_f[TYPE] != BINARY]
+            vocab = [i_f
+                     for i_f in input_features
+                     if input_features[i_f].type != NUMERICAL or input_features[i_f].type != BINARY]
             if self.embed_input_feature_name == 'add':
                 self.embed_i_f_name_layer = Embed(vocab, config.hidden_size,
                                                   force_embedding_size=True)
@@ -688,9 +689,9 @@ def __init__(
 
         logger.debug('  Projectors')
         self.projectors = [Dense(projector_size) for i_f in input_features
-                           if i_f[TYPE] != NUMERICAL and i_f[TYPE] != BINARY]
-        self.skip_features = [i_f[NAME] for i_f in input_features
-                              if i_f[TYPE] == NUMERICAL or i_f[TYPE] == BINARY]
+                           if input_features[i_f].type != NUMERICAL and input_features[i_f].type != BINARY]
+        self.skip_features = [i_f for i_f in input_features
+                              if input_features[i_f].type == NUMERICAL or input_features[i_f].type == BINARY]
 
         logger.debug('  TransformerStack')
         self.transformer_stack = TransformerStack(


Test patch:
diff --git a/tests/integration_tests/test_combiners.py b/tests/integration_tests/test_combiners.py
--- a/tests/integration_tests/test_combiners.py
+++ b/tests/integration_tests/test_combiners.py
@@ -20,6 +20,7 @@
     TransformerCombinerConfig,
     sequence_encoder_registry,
 )
+from ludwig.models.ecd import build_inputs
 from ludwig.utils.schema_utils import load_config
 
 logger = logging.getLogger(__name__)
@@ -382,12 +383,12 @@ def test_tabtransformer_combiner(encoder_outputs):
 
     input_features_def = [
         {'name': 'feature_1', 'type': 'numerical'},
-        {'name': 'feature_2', 'type': 'category'}
+        {'name': 'feature_2', 'type': 'category', 'vocab': ['a', 'b', 'c']}
     ]
 
     # setup combiner to test
     combiner = TabTransformerCombiner(
-        input_features=input_features_def,
+        input_features=build_inputs(input_features_def),
         config=load_config(TabTransformerCombinerConfig)
     )
 
@@ -399,7 +400,7 @@ def test_tabtransformer_combiner(encoder_outputs):
 
     # setup combiner to test
     combiner = TabTransformerCombiner(
-        input_features=input_features_def,
+        input_features=build_inputs(input_features_def),
         config=load_config(
             TabTransformerCombinerConfig,
             embed_input_feature_name=56
@@ -414,7 +415,7 @@ def test_tabtransformer_combiner(encoder_outputs):
 
     # setup combiner to test
     combiner = TabTransformerCombiner(
-        input_features=input_features_def,
+        input_features=build_inputs(input_features_def),
         config=load_config(
             TabTransformerCombinerConfig,
             embed_input_feature_name='add'