Graph & Tabular Machine Learning 11 Aug 2026

Beyond the MLP: Revisiting Deep Learning for Tabular Data

#Deep Learning #Tabular Data #Transformers #ResNet #Gradient Boosted Decision Trees #Benchmarking #Neural Networks #Machine Learning

Beyond the MLP: Revisiting Deep Learning for Tabular Data

For years, the consensus in the data science community has been clear: if your data is tabular, use Gradient Boosted Decision Trees (GBDTs). Whether it's XGBoost, LightGBM, or CatBoost, tree-based models have dominated tabular competitions and production environments due to their robustness and ease of tuning.

But as deep learning has revolutionized vision (CNNs) and language (Transformers), researchers have asked: Why is tabular data different?

In the paper "Revisiting Deep Learning Models for Tabular Data," the authors argue that the perceived inferiority of Deep Learning (DL) on tabular data isn't a failure of the paradigm, but a lack of strong, simple baselines. In this post, we dive into two powerful architectures proposed to challenge the GBDT hegemony: Tabular ResNet and the FT-Transformer.


The Core Challenge: Tabular vs. Grid Data

Unlike images (where pixels have spatial locality) or text (where words have sequential order), tabular data is heterogeneous. A single row might contain an age (integer), a salary (float), and a city (categorical). There is no inherent "distance" between the first and second column.

Standard Multi-Layer Perceptrons (MLPs) often struggle here because they treat the entire row as one flat vector, making it difficult to capture complex, high-order interactions between specific features without becoming prohibitively deep and unstable.


Architecture 1: Tabular ResNet

Stability through Residuals

The Tabular ResNet isn't a reinvented wheel, but a strategic application of the Residual Learning framework. The goal is to enable deeper networks without suffering from the vanishing gradient problem common in standard MLPs.

The Intuition

Instead of forcing every layer to learn a completely new representation, a ResNet block learns a residual mapping. It asks: "What small adjustment do I need to make to the current representation to make it better?"

The Math

The core of the architecture is the ResNetBlock: $$\text{ResNetBlock}(x) = x + \text{Dropout}(\text{Linear}(\text{Dropout}(\text{ReLU}(\text{Linear}(\text{BatchNorm}(x))))))$$

The Workflow

  1. Projection: The input vector $x$ is projected into a higher-dimensional hidden space.
  2. Residual Stack: The data passes through $N$ ResNet blocks. The skip connection (adding $x$ back to the output) ensures that the identity mapping is preserved, allowing gradients to flow freely.
  3. Prediction: A final head (BatchNorm $\rightarrow$ ReLU $\rightarrow$ Linear) maps the representation to the target class.

Architecture 2: FT-Transformer

Treating Features as Tokens

The FT-Transformer (Feature Tokenizer + Transformer) takes a radically different approach. It borrows the "Attention" mechanism from NLP to explicitly model interactions between features.

The Intuition

Instead of treating a row as a single vector, the FT-Transformer treats each feature as a separate token. If you have 20 features, the model sees a "sentence" of 20 tokens. This allows the Multi-Head Self-Attention (MHSA) mechanism to decide which features are most relevant to each other for a specific prediction.

The Workflow

  1. Feature Tokenization:
    • Numerical features are projected into a $d$-dimensional embedding via a linear layer.
    • Categorical features are mapped via an embedding lookup table.
  2. Transformer Processing: The sequence of embeddings is passed through a stack of Transformer layers. Each layer uses self-attention to weigh the importance of every feature relative to every other feature.
  3. Aggregation: The model extracts the final representation (often via a [CLS] token or global average pooling) and passes it through an MLP for the final prediction.

Visualizing the Pipeline

The following diagram illustrates the divergence between the "Flat" approach of ResNet and the "Sequential" approach of the FT-Transformer.

graph TD %% Input Section InputData["Tabular Input Data (Batch, Num_Features)"] --> Split{Architecture Choice} %% ResNet Path Split -->|ResNet Path| ResInputProj["Input Projection (Linear + ReLU)"] ResInputProj --> ResBlock1["ResNet Block 1 (Linear -> ReLU -> Dropout + Skip Connection)"] ResBlock1 --> ResBlockN["ResNet Block N (Linear -> ReLU -> Dropout + Skip Connection)"] ResBlockN --> ResHead["Output Head (Linear)"] ResHead --> FinalOutput["Final Prediction"] %% FT-Transformer Path Split -->|FT-Transformer Path| Tokenizer["Feature Tokenizer (Individual Linear Projections)"] subgraph "FT-Transformer Internal Flow" Tokenizer --> TokenSeq["Feature Token Sequence (Batch, Num_Features, Embed_Dim)"] TokenSeq --> TransformerEnc["Transformer Encoder (Multi-Head Self-Attention + FeedForward)"] TransformerEnc --> GlobalPool["Global Average Pooling (Mean across tokens)"] GlobalPool --> MLPHead["MLP Prediction Head (Linear -> ReLU -> Dropout -> Linear)"] end MLPHead --> FinalOutput %% Styling style InputData fill:#f9f,stroke:#333,stroke-width:2px style FinalOutput fill:#f9f,stroke:#333,stroke-width:2px style Split fill:#fff4dd,stroke:#d4a017,stroke-width:2px style Tokenizer fill:#e1f5fe,stroke:#01579b style TransformerEnc fill:#e1f5fe,stroke:#01579b style ResBlock1 fill:#f1f8e9,stroke:#33691e style ResBlockN fill:#f1f8e9,stroke:#33691e

Production Implementation (PyTorch)

Below is a streamlined implementation of both architectures. We use a synthetic classification dataset to demonstrate the training loop.

PYTHON
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
from sklearn.datasets import make_classification
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score
import numpy as np

# ==========================================
# 1. Tabular ResNet Implementation
# ==========================================

class ResNetBlock(nn.Module):
    def __init__(self, dim, dropout=0.1):
        super().__init__()
        self.block = nn.Sequential(
            nn.Linear(dim, dim),
            nn.ReLU(),
            nn.Dropout(dropout)
        )

    def forward(self, x):
        return x + self.block(x)

class TabularResNet(nn.Module):
    def __init__(self, input_dim, hidden_dim, num_blocks, output_dim, dropout=0.1):
        super().__init__()
        self.input_proj = nn.Linear(input_dim, hidden_dim)
        self.res_blocks = nn.Sequential(
            *[ResNetBlock(hidden_dim, dropout) for _ in range(num_blocks)]
        )
        self.output_head = nn.Linear(hidden_dim, output_dim)

    def forward(self, x):
        x = torch.relu(self.input_proj(x))
        x = self.res_blocks(x)
        return self.output_head(x)

# ==========================================
# 2. FT-Transformer Implementation
# ==========================================

class FeatureTokenizer(nn.Module):
    def __init__(self, num_features, embed_dim):
        super().__init__()
        self.tokenizers = nn.ModuleList([nn.Linear(1, embed_dim) for _ in range(num_features)])

    def forward(self, x):
        # Project each feature independently: (batch, num_features) -> (batch, num_features, embed_dim)
        tokens = [tokenizer(x[:, i].unsqueeze(1)) for i, tokenizer in enumerate(self.tokenizers)]
        return torch.stack(tokens, dim=1)

class FTTransformer(nn.Module):
    def __init__(self, num_features, embed_dim, num_heads, num_layers, output_dim, dropout=0.1):
        super().__init__()
        self.tokenizer = FeatureTokenizer(num_features, embed_dim)
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=embed_dim, nhead=num_heads, dim_feedforward=embed_dim * 4, 
            dropout=dropout, batch_first=True
        )
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
        self.mlp = nn.Sequential(
            nn.Linear(embed_dim, embed_dim),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(embed_dim, output_dim)
        )

    def forward(self, x):
        x = self.tokenizer(x)
        x = self.transformer(x)
        x = torch.mean(x, dim=1) # Global Average Pooling
        return self.mlp(x)

# ==========================================
# 3. Training & Evaluation Pipeline
# ==========================================

def train_and_eval():
    # Data Setup
    X, y = make_classification(n_samples=2000, n_features=20, random_state=42)
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    
    scaler = StandardScaler()
    X_train = torch.FloatTensor(scaler.fit_transform(X_train))
    X_test = torch.FloatTensor(scaler.transform(X_test))
    y_train, y_test = torch.LongTensor(y_train), torch.LongTensor(y_test)
    
    train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=64, shuffle=True)
    test_loader = DataLoader(TensorDataset(X_test, y_test), batch_size=64)

    # Model Initialization
    models = {
        "ResNet": TabularResNet(20, 64, 3, 2),
        "FT-Transformer": FTTransformer(20, 32, 4, 3, 2)
    }

    for name, model in models.items():
        optimizer = optim.Adam(model.parameters(), lr=1e-3)
        criterion = nn.CrossEntropyLoss()
        
        # Simple Training Loop
        model.train()
        for epoch in range(20):
            for bx, by in train_loader:
                optimizer.zero_grad()
                criterion(model(bx), by).backward()
                optimizer.step()
        
        # Evaluation
        model.eval()
        all_preds, all_targets = [], []
        with torch.no_grad():
            for bx, by in test_loader:
                all_preds.extend(torch.argmax(model(bx), dim=1).numpy())
                all_targets.extend(by.numpy())
        
        print(f"{name} -> Accuracy: {accuracy_score(all_targets, all_preds):.4f}")

if __name__ == '__main__':
    train_and_eval()

Summary: Which one should you use?

Feature Tabular ResNet FT-Transformer
Complexity Low (Linear layers) High (Attention mechanisms)
Training Speed Fast Slower (Quadratic with feature count)
Interpretability Low (Black box) Medium (Attention maps)
Best For Large datasets, limited compute Complex feature interactions, smaller/medium datasets

Final Verdict: While GBDTs remain the gold standard for many, the FT-Transformer proves that treating tabular data as a sequence of tokens can unlock performance previously reserved for NLP. If your dataset has complex, non-linear interactions and you have the GPU budget, give the Transformer a try. If you need a stable, deep MLP, ResNet is your best bet.