Beyond the MLP: Revisiting Deep Learning for Tabular Data
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
- Projection: The input vector $x$ is projected into a higher-dimensional hidden space.
- 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.
- 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
- Feature Tokenization:
- Numerical features are projected into a $d$-dimensional embedding via a linear layer.
- Categorical features are mapped via an embedding lookup table.
- 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.
- 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.
Production Implementation (PyTorch)
Below is a streamlined implementation of both architectures. We use a synthetic classification dataset to demonstrate the training loop.
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.