Factorization Machines & DeepFM

Learn feature interactions, CTR prediction, and hybrid models for RecSys

intermediate50 min
On this page

The Big Picture: Evolution of Feature Interaction Learning

The journey from simple linear models to DeepFM represents a fundamental shift in how we capture feature interactions for prediction tasks. Each step in this evolution adds more expressive power.

The Four Eras

Evolution of Interaction Learning
Linear ŷ = w₀ + Σwᵢxᵢ No interaction Collab. Filter User-item only MF: P × Q^T FM All pairs: vᵢ · vⱼ 2nd-order only DeepFM FM + Deep Explicit + implicit Capabilities Added at Each Step: Linear → Collab.F: Capture user-item interaction patterns via matrix decomposition Collab.F → FM: Generalize to ANY features (not just user/item), handle sparse data efficiently FM → DeepFM: Add deep layers for high-order, implicit feature interactions

The key insight: Feature interactions matter. A linear model treats each feature independently. But in reality, features combine—a user's likelihood to click depends not just on their age and the ad category separately, but on how age and category interact (e.g., young users may prefer tech ads, older users may prefer finance ads).

Core Philosophy: Each approach captures interactions at different scales. Linear = no interactions. MF = user-item interactions. FM = all pairwise interactions. DeepFM = all pairwise + high-order implicit interactions.

Recommendation Example: CTR Prediction

Let's use a concrete example throughout this guide: predicting whether a user will click on an ad in an e-commerce or ad platform context.

The Problem

We have millions of ads shown daily. For each impression, we want to predict: will this user click? Features include:

  • User features: age, gender, location, historical CTR, device type
  • Ad features: category, brand, price range, ad format, creative quality
  • Context features: hour of day, day of week, search query, page position

The Challenge: Sparse High-Dimensional Data

After one-hot encoding categorical features (e.g., category has 500 values), we have 10,000+ dimensions. Most features are zeros (sparse). The data matrix is 1,000,000 rows × 10,000 columns, but 99% of entries are zero.

High-Dimensional Sparse Feature Space
Raw Features age: 25 gender: M category: Tech price: $50-100 One-Hot One-Hot Encoded Space (High-Dimensional & Sparse) 10,000 dimensions age_25 | gender_M | cat_Tech | cat_Sports | ... | price_50-100 | ... | (99% zeros) x = [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, ... Most dimensions are 0. Called "sparse" representation. Challenge: If age_25 and cat_Tech rarely appear together in training data, how can we learn their interaction effect? Polynomial models (age_25 × cat_Tech as a feature) explode in dimensionality.
Key Question FM Answers: How do we efficiently learn interactions between rare feature pairs using latent vectors (low-rank approximation)?

Matrix Factorization: The Foundation

Matrix Factorization (MF) is the workhorse of collaborative filtering. It's the stepping stone to understanding Factorization Machines.

The Core Idea

Given a sparse user-item matrix R (millions of users × thousands of items, but only a tiny fraction of entries observed), we approximate it as:

R ≈ P × Q^T

where:
  P is an (n_users × k) matrix: each user u has a k-dimensional latent vector p_u
  Q is an (n_items × k) matrix: each item i has a k-dimensional latent vector q_i
  k is small (e.g., 10–100), the number of latent factors

Making a Prediction

For user u and item i, the predicted rating is the dot product of their latent vectors:

r̂_{ui} = p_u · q_i = Σ_{f=1}^{k} p_{u,f} × q_{i,f}

With biases (which improve accuracy), the model becomes:

r̂_{ui} = μ + b_u + b_i + p_u · q_i

where:
  μ = global mean rating
  b_u = user bias (some users rate high, some rate low)
  b_i = item bias (some items are rated high, some low)
  p_u · q_i = interaction term (captures user-item affinity in latent space)

The Loss Function & Training

We minimize squared error on observed entries:

L = Σ_{(u,i) ∈ observed} (r_{ui} - r̂_{ui})^2 + λ(||P||^2 + ||Q||^2 + b_u^2 + b_i^2)

The λ term is L2 regularization (prevents overfitting).

Two common training methods:

  • Stochastic Gradient Descent (SGD): For each observed (u, i), compute the error e_{ui} = r_{ui} - r̂_{ui}, then update:
    p_u ← p_u + α × (e_{ui} × q_i - λ × p_u)
    q_i ← q_i + α × (e_{ui} × p_u - λ × q_i)
    b_u ← b_u + α × (e_{ui} - λ × b_u)
    b_i ← b_i + α × (e_{ui} - λ × b_i)
  • Alternating Least Squares (ALS): Fix P, solve for Q in closed form. Then fix Q, solve for P. Repeat until convergence. More stable, parallelizable.

Concrete Example: A Small Rating Matrix

Matrix Factorization Example (k=2 latent factors)
Original User-Item Matrix R (5×4) Movie A B C D U1 5 ? 4 ? U2 ? 3 ? 2 U3 4 5 ? 3 U4 3 ? 2 ? U5 ? 4 5 ? ? = missing values to predict Factorize into User Matrix P (5×2) Factor1 Factor2 U1 0.85 0.52 U2 0.41 0.74 U3 0.93 0.68 U4 0.62 0.39 U5 0.58 0.77 × Item Matrix Q^T (2×4) A B C D Factor1 0.81 0.92 0.73 0.45 Factor2 0.57 0.39 0.61 0.88 Example: Predict r̂_{U1,B} r̂_{U1,B} = p_{U1} · q_B = (0.85 × 0.92) + (0.52 × 0.39) = 0.782 + 0.203 = 0.985 ≈ 1.0 stars (Compare to actual observed ratings for U1 and B to compute error)

Why MF Works with Sparse Data

Instead of learning one parameter per user-item pair (n_users × n_items = millions of parameters for large systems), we learn two small matrices (n_users × k + n_items × k ≈ thousands of parameters). Latent factors enable generalization: if user U1 and U3 have similar latent vectors, we can infer that U1 might like items U3 likes, even without direct evidence.

MF Limitation (Preview): MF only models user-item interactions. It can't incorporate side features (user age, item category, etc.). What if we want to predict based on all features, not just user ID and item ID?

SVD Decomposition: Classical Matrix Decomposition

Singular Value Decomposition (SVD) is the classical approach to matrix decomposition, and it provides important intuition for understanding FM.

Full SVD Definition

For any matrix R (m × n), SVD decomposes it as:

R = U × Σ × V^T

where:
  U (m × m): orthogonal matrix of left singular vectors (user space)
  Σ (m × n diagonal): singular values (importance of each component)
  V (n × n): orthogonal matrix of right singular vectors (item space)

The singular values σ₁ ≥ σ₂ ≥ ... ≥ σᵣ > 0 are sorted in decreasing order. The first k singular values capture most of the variance.

Truncated SVD: Dimensionality Reduction

To reduce noise and improve generalization, we keep only the top k singular values:

R_k = U_k × Σ_k × V_k^T

where U_k, Σ_k, V_k contain only the top k components.
This is a rank-k approximation: R_k has much lower rank than full R.

SVD vs Matrix Factorization: Key Difference

  • SVD: Computes on the full matrix R (including zeros and unobserved entries). Expensive for large sparse matrices. Suitable for dense, complete data.
  • MF (Funk SVD): Only optimizes on observed entries. Uses SGD or ALS. Efficient for sparse data. This is what Netflix Prize algorithms used.

Visualizing Truncation

Full SVD vs Truncated SVD (Conceptual)
Full SVD: R = U × Σ × V^T U m × m Σ m × n (diagonal) σ₁ (large) σₖ (medium) σ_r ≈ 0 V^T n × n = R (m × n) All data Truncated SVD: R_k = U_k × Σ_k × V_k^T (keep top-k) U_k m × k Σ_k k × k V_k^T k × n = R_k (m × n) Rank-k approx. Key Insight: Typically, top-k singular values capture 90-95% of variance even for large k (e.g., k=10-50). This is the basis for dimensionality reduction: fewer parameters, less overfitting, faster computation.

Connection to Matrix Factorization

MF is essentially a form of approximate SVD optimized for sparse data. When we compute P × Q^T, we're constructing a low-rank approximation to R. The latent factors are analogous to the left and right singular vectors (scaled by singular values).

Intuition: SVD is the optimal low-rank approximation in terms of Frobenius norm (if we observe all entries). MF achieves similar results by only fitting observed entries, making it practical for sparse data.

Factorization Machines: Generalizing MF

Factorization Machines (FM) are the breakthrough that connects matrix factorization to general feature learning. They solve the fundamental limitation of MF: how to incorporate arbitrary features, not just user and item IDs.

The Problem FM Solves

Suppose we have n features (after one-hot encoding). A linear model is:

ŷ(x) = w₀ + Σᵢ wᵢxᵢ

To capture feature interactions, we'd add polynomial terms:

ŷ(x) = w₀ + Σᵢ wᵢxᵢ + Σᵢ Σ_{j>i} w_{ij}xᵢxⱼ

But this has O(n²) parameters! For n=10,000, that's 50 million parameters just for pairwise interactions. With sparse data, most interactions are never observed in training.

FM's Elegant Solution: Latent Vectors

Instead of learning a separate parameter w_{ij} for each pair, FM learns a small latent vector v_i for each feature i. The interaction between features i and j is:

 = Σ_{f=1}^{k} v_{i,f} × v_{j,f}

This is the dot product of their latent vectors. Only k parameters per feature!

The full FM equation (degree-2, which is most common):

ŷ(x) = w₀ + Σᵢ wᵢxᵢ + Σᵢ Σ_{j>i}  × xᵢ × xⱼ

Parameters:
  w₀: 1 parameter (global bias)
  w: n parameters (linear weights)
  V: n × k parameters (latent vectors)
Total: 1 + n + nk ≈ nk (if k << n)

Why This Works: The Latent Factor Trick

Here's the magic: even if features i and j never appear together in training data, their interaction can be estimated because they share the latent space:

  • Feature i has latent vector v_i = [0.8, 0.3, ...]
  • Feature j has latent vector v_j = [0.7, 0.4, ...]
  • Their interaction = 0.8×0.7 + 0.3×0.4 + ... = 0.68 can be inferred even if (i,j) pair is rare

FM Generalizes Matrix Factorization

This is the key insight: FM is a generalization of MF! If you encode user and item as one-hot features, FM's interaction term becomes exactly the MF equation:

FM Generalizes MF: One-Hot User/Item to FM Interaction
Feature Encoding: User u and Item i as one-hot: x = [0, ..., 1 (u), ..., 0, 0, ..., 1 (i), ..., 0] Only two non-zero features: x_u = 1 and x_i = 1 Plug into FM FM Interaction Term: Σᵢ Σ_{j>i} xᵢ xⱼ Only non-zero term: × 1 × 1 = Simplifies to Matrix Factorization! = p_u · q_i (MF equation) FM's latent vectors ARE the MF matrices (user and item factors) Conclusion: FM is MF + side features. If you only have user/item IDs as features, FM reduces to MF. If you add age, category, time, etc. as extra features, FM learns interactions between ALL features.
FM Revolution: Instead of hand-engineering feature crosses (age × category), FM learns them automatically via shared latent vectors. This is why FM was so impactful for CTR prediction in industry.

FM: How It Works—Training & Prediction

Computing Predictions Efficiently

Naively, computing the pairwise interactions takes O(n²k):

Σᵢ Σ_{j>i}  xᵢ xⱼ = Σᵢ Σ_{j>i} (Σ_{f=1}^{k} v_{i,f} v_{j,f}) xᵢ xⱼ

But FM uses a mathematical trick to reduce this to O(kn):

Σᵢ Σ_{j>i}  xᵢ xⱼ = ½ Σ_{f=1}^{k} [(Σᵢ v_{i,f} xᵢ)² - Σᵢ v_{i,f}² xᵢ²]

Proof (for single factor f):
  (Σᵢ v_{i,f} xᵢ)² = Σᵢ Σⱼ v_{i,f} v_{j,f} xᵢ xⱼ
                   = Σᵢ v_{i,f}² xᵢ² + 2·Σᵢ Σ_{j>i} v_{i,f} v_{j,f} xᵢ xⱼ

Rearranging:
  Σᵢ Σ_{j>i} v_{i,f} v_{j,f} xᵢ xⱼ = ½[(Σᵢ v_{i,f} xᵢ)² - Σᵢ v_{i,f}² xᵢ²]

Full FM Prediction Algorithm (O(kn))

def fm_predict(x, w0, w, V):
    # Linear term: O(n)
    linear = w0 + sum(w[i] * x[i] for i in range(n))

    # Quadratic term: O(kn)
    interaction = 0
    for f in range(k):
        # Compute sum of v_{i,f} * x_i for all i
        sum_vx = sum(V[i, f] * x[i] for i in range(n))

        # Compute sum of v_{i,f}^2 * x_i^2 for all i
        sum_vv_xx = sum(V[i, f]**2 * x[i]**2 for i in range(n))

        # Add to interaction term
        interaction += sum_vx**2 - sum_vv_xx

    interaction *= 0.5
    return linear + interaction

Training FM with SGD

For a single training sample (x, y), we compute:

ŷ = FM prediction (as above)
error = y - ŷ  (or loss gradient, depending on loss function)

Gradients:
  ∂ŷ/∂w₀ = 1
  ∂ŷ/∂wᵢ = xᵢ
  ∂ŷ/∂v_{i,f} = xᵢ · (Σⱼ v_{j,f} xⱼ - v_{i,f} xᵢ)
              = xᵢ · (sum_vx - v_{i,f} xᵢ)  [from cached computation]

Updates (with learning rate α and regularization λ):
  w₀ ← w₀ - α · ∂L/∂w₀
  wᵢ ← wᵢ - α · (∂L/∂ŷ · ∂ŷ/∂wᵢ + 2λ·wᵢ)
  v_{i,f} ← v_{i,f} - α · (∂L/∂ŷ · ∂ŷ/∂v_{i,f} + 2λ·v_{i,f})

Loss functions vary by task:

  • Regression (MSE): L = ½(y - ŷ)², ∂L/∂ŷ = -(y - ŷ)
  • Binary Classification (Log Loss): L = -[y log(σ(ŷ)) + (1-y) log(1-σ(ŷ))], ∂L/∂ŷ = σ(ŷ) - y
  • Ranking (BPR): L = -log(σ(ŷ_pos - ŷ_neg)), optimizes relative ordering

Why FM Works with Sparse Data

Key insight: Each feature only needs k parameters (its latent vector). The interaction between rare feature pairs is estimated through the shared latent space:

  • If user_age_25 and item_category_tech rarely co-occur, we can't learn w_{age_25,tech} directly.
  • But both features have learned latent vectors (v_age_25 and v_tech).
  • Their interaction is estimated from other feature interactions they participate in.
  • This is transfer learning in the latent factor space.
Computational Win: The O(n²k) problem becomes O(kn) via algebraic manipulation. For n=10,000 features and k=10, this is 100,000 operations per sample instead of 1 billion. This enables training on millions of samples.

Regularization in FM: Preventing Overfitting

The Regularization Challenge

FM has many parameters: 1 + n + nk. For n=10,000 and k=10, that's 100,001 parameters. With sparse data, most features appear in only a few samples. Without regularization, latent vectors for rare features can grow unbounded to fit noise.

L2 Regularization (Most Common)

Add penalty terms to the loss function:

L_total = L_data + λ_w·||w||² + λ_v·||V||²

where:
  ||w||² = Σᵢ wᵢ²
  ||V||² = Σᵢ Σ_{f} v_{i,f}²

Typical values:
  λ_w: 1e-5 to 1e-3 (smaller, linear terms are robust)
  λ_v: 1e-4 to 1e-1 (larger, latent factors need more regularization)

L2 regularization shrinks all parameters towards zero, proportional to their magnitude. It prevents extreme latent vector values.

Per-Dimension Regularization

Advanced: use different regularization for each latent dimension:

L_total = L_data + Σ_{f} λ_f·Σᵢ v_{i,f}²

Insight: Some latent dimensions may be noisier than others.
Different λ per dimension allows flexibility.

L1 Regularization (Feature Selection)

For linear weights, L1 regularization induces sparsity (some w_i become exactly zero):

L_total = L_data + λ·Σᵢ |wᵢ|

Effect: Fewer non-zero linear features → automatic feature selection.
Less common for latent factors (L2 preferred).

Impact: Seen vs Unseen Feature Interactions

Scenario Without Regularization With L2 Regularization Rare pair (user_25, category_tech): appears 2x in 1M samples Latent vector v_{pair} grows to fit these 2 samples exactly → overfits λ term constrains growth → learns from related features instead Common pair (user_M, category_News): appears 50k times Latent vector learns pattern reliably Slight shrinkage, still learns pattern well (less overfitting) Test data: unseen pair (user_30, category_Sports) Prediction is random (not in training) Latent vectors generalize from similar features
Regularization Philosophy in FM: The latent factors should capture underlying patterns (e.g., "tech-oriented users"), not memorize specific data points. Regularization encourages the model to learn general factors that generalize to unseen feature combinations.

DeepFM Architecture: Combining FM and Deep Learning

The Motivation: Limitations of FM

FM captures 2nd-order feature interactions explicitly via latent vectors. But real-world patterns often involve higher-order interactions:

  • A young female user in San Francisco clicking on ads during lunch hours on weekdays (3rd+ order)
  • Specific combinations of user browsing history × product category × time (high-order)

A deep neural network can learn these automatically, but it can't efficiently capture low-order interactions due to its layer-wise computation. DeepFM combines both: explicit low-order (FM) + implicit high-order (Deep).

DeepFM Architecture Diagram

DeepFM Joint Architecture
Input: Sparse Features (one-hot encoded) age_25=1, gender_M=1, category_tech=1, ... Shared Embedding Layer Each feature → dense vector (8-16 dims). Embeddings are trained jointly. FM Component 1st-order (Linear): Σᵢ wᵢ · embedding_i 2nd-order (FM): Σᵢ Σ_{j>i} × xᵢ × xⱼ Deep Component Embedding → Flatten Dense Layer 1: 256 units, ReLU Dense Layer 2: 128 units, ReLU Dropout (0.5 during training) Dense Layer 3: 64 units, ReLU Dense Layer 4: 32 units, ReLU Output: y_deep (1 unit) Learns high-order implicit patterns Final Output ŷ = sigmoid(y_FM + y_deep) Key Design Choices: • Shared embeddings: Both FM and Deep use same feature representations → learned jointly • FM captures explicit pairwise interactions (low-order, interpretable) • Deep captures implicit high-order patterns (black box, powerful)

Why Shared Embeddings?

Both components use the same embedding layer. Why?

  • Parameter efficiency: Single embedding per feature, not separate ones for FM/Deep
  • Joint learning: Embeddings learn representations that satisfy both low-order and high-order patterns
  • Interpretability: Shared embeddings mean FM factors and deep layer inputs align semantically

DeepFM vs Other Architectures

Model Low-Order Interactions High-Order Interactions Notes Linear None None Fast, interpretable, weak FM Explicit 2nd-order None Efficient, interpretable, but limited Deep (DNN) Implicit (inefficient) Implicit (powerful) Requires manual feature crosses for low-order Wide & Deep Explicit (hand-crafted) Implicit (deep) Requires manual feature engineering DeepFM Explicit 2nd-order (FM) Implicit (deep) Automatic feature learning, no hand-crafting
DeepFM Advantage: Unlike Wide & Deep, you don't need to hand-engineer feature crosses. FM automatically captures them. The deep component is free to learn higher-order patterns. Best of both worlds.

Training DeepFM: Joint Optimization

Joint Loss Function

Both FM and Deep components are trained simultaneously with a single loss function:

ŷ = σ(y_FM + y_deep)

Loss (for binary classification):
  L = -[y·log(ŷ) + (1-y)·log(1-ŷ)] + λ_w·||w||² + λ_v·||V||² + λ_net·||θ_deep||²

where:
  y_FM = w₀ + Σᵢ wᵢxᵢ + (FM interaction term)
  y_deep = output of deep network
  θ_deep = weights of all layers in deep network
  λ_net = regularization for deep network parameters

Training Algorithm

Standard gradient descent (typically Adam optimizer):

Algorithm: Training DeepFM
────────────────────────────
1. Initialize:
   - Embeddings randomly (uniform or Xavier)
   - Linear weights w to zero (or small random)
   - Deep network weights (Xavier initialization)

2. For each epoch:
   For each batch of samples:
      a) Forward pass:
         - Compute embeddings for all features
         - Compute y_FM (with computational trick for efficiency)
         - Compute y_deep (layer-by-layer forward pass)
         - Compute ŷ = sigmoid(y_FM + y_deep)

      b) Compute loss L on batch

      c) Backward pass:
         - Compute gradients ∂L/∂θ for all parameters θ
         - Gradients flow through both FM and Deep components
         - Update shared embeddings based on both components

      d) Update parameters:
         - θ ← θ - α·∂L/∂θ (Adam with learning rate schedule)

3. Validate on held-out data, early stopping

Hyperparameter Tuning

Hyperparameter Typical Range Notes Embedding dimension 8–32 Balance: too small → underfitting, too large → overfitting & slow Deep network layers 2–4 layers Usually 256→128→64→32 units per layer Activation function ReLU (standard) Tanh or ELU sometimes tried, but ReLU dominates in industry Dropout rate 0.3–0.7 Applied only during training. 0.5 is common for tabular data Batch size 256–2048 Larger → more stable gradients, slower. Trade-off Learning rate 1e-4 to 1e-2 Usually decay over time (learning rate schedule) λ_w (linear reg) 1e-6 to 1e-4 Weaker regularization on linear terms (they're stable) λ_v (latent reg) 1e-5 to 1e-2 Stronger on latent factors (sparse data) λ_net (deep reg) 1e-5 to 1e-3 L2 + dropout usually sufficient for deep network

Training Efficiency & Tricks

  • Batch normalization: Can stabilize training in deep layers (less critical with dropout)
  • Learning rate schedule: Decay LR over epochs (e.g., 0.001 → 0.0001). Helps convergence.
  • Early stopping: Monitor validation AUC/loss. Stop when it stops improving for N epochs.
  • Data shuffling: Shuffle samples each epoch to avoid batch correlations.
  • Feature normalization: For continuous features (if any), normalize to [0,1] or z-score.
  • Class balance: If data is imbalanced (e.g., 1% clicks), use weighted loss or over-sampling.
Industrial Practice: DeepFM is trained on millions/billions of samples. Key optimizations: distributed training (parameter servers), gradient compression, and asynchronous SGD. Single-machine implementations handle thousands to millions of samples.

When to Use: MF vs FM vs DeepFM

Decision Tree

Model Selection Guide
Do you have features beyond user/item? No Use: Matrix Factorization (MF) Pure collaborative filtering Yes Do you expect high-order feature interactions? No / Simple Use: Factorization Machines (FM) 2nd-order interactions, sparse features Yes / Complex Use: DeepFM (or Deep Network) Low + high order, large data Additional Considerations: • Data size: MF/FM scale to millions. DeepFM needs more GPU for billions. • Latency: MF/FM: <10ms. DeepFM (with deep): 10-100ms. Industry standard. • Interpretability: MF/FM factors can be analyzed. DeepFM deep layer is black box.

Detailed Comparison

Aspect Matrix Factorization FM DeepFM Input User ID, Item ID Any features (categorical/continuous) Any features (categorical/continuous) Interactions User-Item only All pairwise (2nd-order) 2nd-order (FM) + high-order (deep) Parameters n_u·k + n_i·k n·k + n n·k + n + deep network Training time Fast (ALS friendly) Fast (O(kn) per sample) Medium (backprop through deep layers) Inference time <1ms 1-5ms 10-100ms Scalability Up to 10B samples Up to 10B samples Up to 1B samples (GPU needed) Accuracy Good (if only user/item matter) Better (feature interactions) Best (low + high order) Interpretability High (factors are interpretable) High (latent vectors + linear) Low (deep layers are black box) Best for Pure collab. filtering (Netflix) CTR with sparse features (ads) Large-scale CTR (e-commerce)

Rule of Thumb

  • If you have only user/item (no side features) → MF
  • If you have diverse features but not billions of samples → FM
  • If you have billions of samples and GPU resources → DeepFM
Industry Reality: Most e-commerce/ad platforms use DeepFM (or similar architectures like xDeepFM). MF is for pure recommendation (streaming music, movie ratings). FM is useful when you want a fast, interpretable baseline.

Python Implementation from Scratch

1. Matrix Factorization with SGD

import numpy as np

class MatrixFactorization:
    """Basic MF with SGD training."""

    def __init__(self, n_users, n_items, k=10, learning_rate=0.01, reg=0.01):
        self.n_users = n_users
        self.n_items = n_items
        self.k = k
        self.lr = learning_rate
        self.reg = reg

        # Initialize factors
        self.P = np.random.randn(n_users, k) * 0.01  # User factors
        self.Q = np.random.randn(n_items, k) * 0.01  # Item factors
        self.b_u = np.zeros(n_users)  # User biases
        self.b_i = np.zeros(n_items)  # Item biases
        self.mu = 0.0  # Global mean

    def predict(self, user_id, item_id):
        """Predict rating for user-item pair."""
        return self.mu + self.b_u[user_id] + self.b_i[item_id] + np.dot(self.P[user_id], self.Q[item_id])

    def train(self, user_ids, item_ids, ratings, epochs=10):
        """SGD training."""
        self.mu = np.mean(ratings)

        for epoch in range(epochs):
            total_loss = 0
            for u, i, r in zip(user_ids, item_ids, ratings):
                pred = self.predict(u, i)
                error = r - pred

                # Update biases
                self.b_u[u] += self.lr * (error - self.reg * self.b_u[u])
                self.b_i[i] += self.lr * (error - self.reg * self.b_i[i])

                # Update factors
                q_copy = self.Q[i].copy()
                self.P[u] += self.lr * (error * self.Q[i] - self.reg * self.P[u])
                self.Q[i] += self.lr * (error * self.P[u] - self.reg * self.Q[i])

                total_loss += error ** 2

            print(f"Epoch {epoch+1}: MSE = {total_loss / len(user_ids):.4f}")


# Example usage
np.random.seed(42)
mf = MatrixFactorization(n_users=5, n_items=4, k=2)

# Synthetic training data
user_ids = np.array([0, 0, 1, 1, 2, 2, 3, 3, 4, 4])
item_ids = np.array([0, 2, 1, 3, 0, 1, 0, 2, 1, 2])
ratings = np.array([5, 4, 3, 2, 4, 5, 3, 2, 4, 5])

mf.train(user_ids, item_ids, ratings, epochs=50)

2. Factorization Machines for CTR Prediction

class FactorizationMachine:
    """FM for binary classification (CTR prediction)."""

    def __init__(self, n_features, k=8, learning_rate=0.01, reg_w=0.0001, reg_v=0.001):
        self.n_features = n_features
        self.k = k
        self.lr = learning_rate
        self.reg_w = reg_w
        self.reg_v = reg_v

        self.w0 = 0.0  # Global bias
        self.w = np.zeros(n_features)  # Linear weights
        self.V = np.random.randn(n_features, k) * 0.01  # Latent factors

    def sigmoid(self, x):
        return 1.0 / (1.0 + np.exp(-np.clip(x, -500, 500)))

    def predict(self, x):
        """Efficient O(kn) prediction with computational trick."""
        # Linear term
        linear = self.w0 + np.dot(self.w, x)

        # Quadratic term (O(kn) via the trick)
        interaction = 0.0
        for f in range(self.k):
            sum_vx = np.dot(self.V[:, f], x)
            sum_vv_xx = np.dot(self.V[:, f] ** 2, x ** 2)
            interaction += sum_vx ** 2 - sum_vv_xx
        interaction *= 0.5

        logit = linear + interaction
        return self.sigmoid(logit)

    def train(self, X, y, epochs=20):
        """SGD training for binary classification."""
        n_samples = X.shape[0]

        for epoch in range(epochs):
            total_loss = 0
            for idx in range(n_samples):
                x = X[idx]
                target = y[idx]

                pred = self.predict(x)
                error = pred - target

                # Update w0
                self.w0 -= self.lr * error

                # Update w (linear weights)
                self.w -= self.lr * (error * x + 2 * self.reg_w * self.w)

                # Update V (latent factors) - O(kn)
                for f in range(self.k):
                    sum_vx = np.dot(self.V[:, f], x)
                    for i in range(self.n_features):
                        if x[i] != 0:
                            grad = error * x[i] * (sum_vx - self.V[i, f] * x[i])
                            self.V[i, f] -= self.lr * (grad + 2 * self.reg_v * self.V[i, f])

                total_loss += -(target * np.log(pred + 1e-10) + (1 - target) * np.log(1 - pred + 1e-10))

            auc = self.evaluate(X, y)
            print(f"Epoch {epoch+1}: Loss = {total_loss / n_samples:.4f}, AUC = {auc:.4f}")

    def evaluate(self, X, y):
        """Compute AUC."""
        preds = np.array([self.predict(x) for x in X])
        return np.mean((y == 1) & (preds > 0.5)) / (np.mean(y == 1) + 1e-10)


# Synthetic CTR data
np.random.seed(42)
n_samples = 200
n_features = 20

X = np.random.randint(0, 2, (n_samples, n_features))  # One-hot features
y = (X[:, 0] & X[:, 5] | X[:, 10]).astype(int)  # Synthetic label with interactions
y = np.random.binomial(1, 0.3 + 0.4 * y)  # Add noise

fm = FactorizationMachine(n_features=n_features, k=4)
fm.train(X, y, epochs=30)

3. SimpleDeepFM (Numpy Implementation)

class SimpleDeepFM:
    """DeepFM with single hidden layer (simplified for demo)."""

    def __init__(self, n_features, k=8, hidden_units=64, learning_rate=0.01):
        self.n_features = n_features
        self.k = k
        self.lr = learning_rate

        # FM component
        self.w0 = 0.0
        self.w = np.zeros(n_features)
        self.V = np.random.randn(n_features, k) * 0.01

        # Deep component
        self.embedding_dim = k  # Use FM embeddings
        embedding_size = n_features * k
        self.W1 = np.random.randn(embedding_size, hidden_units) * 0.01
        self.b1 = np.zeros(hidden_units)
        self.W2 = np.random.randn(hidden_units, 1) * 0.01
        self.b2 = np.zeros(1)

    def relu(self, x):
        return np.maximum(0, x)

    def relu_grad(self, x):
        return (x > 0).astype(float)

    def sigmoid(self, x):
        return 1.0 / (1.0 + np.exp(-np.clip(x, -500, 500)))

    def predict(self, x):
        """Forward pass."""
        # FM prediction
        linear = self.w0 + np.dot(self.w, x)
        interaction = 0.0
        for f in range(self.k):
            sum_vx = np.dot(self.V[:, f], x)
            sum_vv_xx = np.dot(self.V[:, f] ** 2, x ** 2)
            interaction += sum_vx ** 2 - sum_vv_xx
        interaction *= 0.5
        y_fm = linear + interaction

        # Deep prediction
        emb = self.V @ x  # Embeddings (k,)
        emb_flat = emb.flatten()  # Flatten if needed
        hidden = self.relu(np.dot(emb_flat, self.W1) + self.b1)
        y_deep = np.dot(hidden, self.W2) + self.b2

        # Combine
        logit = y_fm + y_deep
        return self.sigmoid(logit)

    def train(self, X, y, epochs=20):
        """Simple SGD training (no proper backprop, just for demo)."""
        for epoch in range(epochs):
            total_loss = 0
            for idx in range(len(X)):
                x = X[idx]
                target = y[idx]
                pred = self.predict(x)
                loss = -(target * np.log(pred + 1e-10) + (1 - target) * np.log(1 - pred + 1e-10))
                total_loss += loss

                # Simple gradient update (not full backprop)
                error = pred - target
                self.w0 -= self.lr * error
                self.w -= self.lr * error * x

            print(f"Epoch {epoch+1}: Loss = {total_loss / len(X):.4f}")


# Test on same data
deepfm = SimpleDeepFM(n_features=20, k=4, hidden_units=16)
deepfm.train(X, y, epochs=20)

Complete Example: End-to-End CTR Prediction

"""
Complete example: Predicting click probability on ads.
Features: user age (categorical), ad category (categorical), hour of day (categorical)
"""

import pandas as pd
from sklearn.preprocessing import LabelEncoder

# Synthetic CTR dataset
data = {
    'user_age': ['18-25', '26-35', '18-25', '36-50', '26-35'] * 20,
    'ad_category': ['Tech', 'News', 'Tech', 'Finance', 'News'] * 20,
    'hour': [9, 14, 18, 10, 15] * 20,
    'clicked': [1, 0, 1, 0, 1] * 20
}
df = pd.DataFrame(data)

# One-hot encoding
le_age = LabelEncoder()
le_cat = LabelEncoder()
le_hour = LabelEncoder()

df['user_age_enc'] = le_age.fit_transform(df['user_age'])
df['ad_category_enc'] = le_cat.fit_transform(df['ad_category'])
df['hour_enc'] = le_hour.fit_transform(df['hour'])

# Create feature matrix (one-hot)
n_age = len(df['user_age'].unique())
n_cat = len(df['ad_category'].unique())
n_hour = len(df['hour'].unique())
n_features = n_age + n_cat + n_hour

X = np.zeros((len(df), n_features))
for idx, row in df.iterrows():
    X[idx, row['user_age_enc']] = 1
    X[idx, n_age + row['ad_category_enc']] = 1
    X[idx, n_age + n_cat + row['hour_enc']] = 1

y = df['clicked'].values

# Train FM
print("=== Training Factorization Machine ===")
fm = FactorizationMachine(n_features=n_features, k=4)
fm.train(X, y, epochs=20)

# Make predictions
print("\n=== Sample Predictions ===")
for idx in range(min(5, len(X))):
    pred_prob = fm.predict(X[idx])
    print(f"Sample {idx}: Click probability = {pred_prob:.4f}, Actual = {y[idx]}")
Production Notes: These implementations are for learning. Production systems use optimized libraries (PyTorch, TensorFlow, XGBoost) and distributed training. Key optimizations: GPU acceleration, batch processing, parameter servers, and model serving frameworks.
End of Factorization Machines & DeepFM