Sequential Recommendations

Session-based recommendations with GRU4Rec and Transformer4Rec

advanced50 min
On this page

The Big Picture: From Static to Sequential

Traditional collaborative filtering treats users as fixed feature vectors. But behavior is temporal. Watching Action → Adventure → Sci-Fi tells you the next preference is likely Sci-Fi sequel, not a random genre. Sequential recommendation models capture this order-dependent signal.

Session-Based vs Long-Term History

Two paradigms dominate:

  • Session-based: Model a single browsing/shopping session with no user ID. Example: e-commerce add-to-cart history within one visit.
  • User history-based: Model the full sequence of interactions for one user across months. Example: watching history on a streaming platform.

Session-based is stateless and privacy-preserving; history-based requires user tracking but captures longer patterns.

Why Order Matters: The Timeline View

Static Profile vs Sequence Modeling
Static Profile (Collaborative Filtering) User Action: 0.8 Comedy: 0.3 SciFi: 0.7 → Averages. No sequence. Sequential Modeling (RNN / Transformer) Time t=1: Action Time t=2: Adventure Time t=3: SciFi Predict: SciFi 2

Four Approaches to Sequential Modeling

RNN-Based (GRU4Rec)

Hidden state passes temporal information forward. Efficient on limited data. Suited for session-based scenarios. See RNN guide.

FastStateful

Attention-Based (SASRec)

Self-attention directly models item-to-item relationships. Parallelizable. Captures long-range dependencies. Better than RNN for longer sequences.

ParallelizableInterpretable

Transformer-Based (Transformer4Rec)

Stacked attention blocks with rich feature integration. Pre-training with BERT-style MLM. Industrial scale. See Transformer guide.

Pre-trainableFeature-rich

Graph-Based (GCN/GAT)

Model items as nodes, sequences as temporal edges. Capture cross-session item correlations. Best for discovery tasks. See Graph embeddings.

Cross-sessionScalable
Key Insight: Sequential models leverage the order of interactions to predict future behavior. Static profiles ignore the trajectory; sequential models embrace it.

Problem Formulation

The Core Task

Given a sequence of item interactions [i₁, i₂, ..., i_T], predict the next item i_{T+1}.

Inputs & Outputs

Component Type Example
Input Sequence List of item IDs [movie_101, movie_45, movie_203]
Optional Features Category, price, embedding [(Action, $15), (Comedy, $12), (SciFi, $18)]
Output Probability distribution P(movie_50) = 0.35, P(movie_72) = 0.12, ...
Target True next item (label) movie_50

Two Formulations

  • Session-Based: No user ID. Model [i₁, i₂, ..., i_T] from a single browsing session. Stateless, privacy-friendly.
  • User History-Based: Require user ID. Model chronological interactions across multiple sessions. Captures long-term preference drift.

Evaluation Metrics

Metric Formula Interpretation
HR@K (Hit Rate) # correct in top-K / total test samples Fraction of times true item is in top-K. Simple, popular.
NDCG@K (Normalized DCG) Σ log₂(1 + rank) / ideal DCG Rank-weighted hit. Rewards ranking true item high.
MRR (Mean Reciprocal Rank) Average of 1/rank for true item How soon true item appears in ranking.
Recall@K True positives / all positives Coverage; especially useful for multi-item evaluation.

Sequence Data Example

Sample Dataset: User Sessions
Session ID Item Sequence Target (Next) Length S001 [101, 45, 203] 50 3 S002 [304, 88, 156, 22] 99 4 S003 [7, 18] 150 2 S004 [445, 67, 89, 123, 200] 67 5

Pipeline: From Sequence to Top-K Predictions

Inference Flow
Input Sequence [i₁, i₂, ..., i_T] Embed RNN/Attention hidden state Score Scoring Layer logits for all items Rank Top-K Items ranked by score Evaluate HR@K NDCG@K
Training vs Evaluation: During training, use all items in vocabulary as negatives. During evaluation, rank against all items and measure HR@10, NDCG@10, etc. This is expensive but standard for sequential recs.

GRU4Rec: Gated Recurrent Units for Recommendations

GRU4Rec (Hidasi et al., 2015) applies Gated Recurrent Units to session-based recommendation. It replaced static collaborative filtering with a temporal model that captures item-to-item transitions. For GRU internals, see our RNN learning guide.

GRU Architecture Recap

A GRU cell has two gates:

  • Update Gate (z_t): Controls how much prior state to keep.
  • Reset Gate (r_t): Controls interaction with candidate state.
  • Candidate Hidden State (h̃_t): Candidate new information.
  • Hidden State (h_t): Convex blend of old and candidate.
z_t = σ(W_z [h_{t-1}, x_t] + b_z)         # Update gate
r_t = σ(W_r [h_{t-1}, x_t] + b_r)         # Reset gate
h̃_t = tanh(W [r_t ⊙ h_{t-1}, x_t] + b)   # Candidate
h_t = (1 - z_t) ⊙ h_{t-1} + z_t ⊙ h̃_t    # New hidden state

GRU4Rec Pipeline

GRU4Rec: Item Embedding → GRU → Output Layer
Input Sequence [i_1, i_2, i_3] Item 1 Item 2 Item 3 Embedding Layer e₁ ∈ ℝ^D e₂ ∈ ℝ^D e₃ ∈ ℝ^D GRU Cells GRU h₁ GRU h₂ GRU h₃ Output Layer Linear(h₃) logits ∈ ℝ^|V| Softmax P(next item) Probability

Key Innovations in GRU4Rec

Component Detail
Item Embedding D-dimensional embedding for each item. Learned end-to-end. Captures item similarity in embedding space.
GRU Recurrence Hidden state h_t summarizes items [i_1...i_t]. Next item prediction uses h_T (final hidden state).
Output Layer Linear projection: logits = W·h_T + b. Softmax gives probability over all |V| items in vocabulary.
Session Parallel Training Batch multiple independent sessions together. Advance all GRU cells synchronously. Much more efficient than sequential processing.
Ranking Loss BPR-max or TOP-1: Focus on rank of true item, not calibrated probability. Better for ranking tasks.
Unrolled GRU: For T timesteps, unroll GRU T times. Input x_t is embedding of item i_t. Hidden state h_t flows left-to-right, carrying temporal context.

GRU4Rec Training Details

Training GRU4Rec efficiently requires understanding session-parallel mini-batching and ranking-aware loss functions.

Session-Parallel Mini-Batching

Unlike standard RNN training (BPTT), GRU4Rec trains multiple independent sessions in parallel:

  • No truncation: Process entire session length without BPTT windows.
  • Session matrix: Rows = items in batch, columns = sessions.
  • Synchronous advance: All sessions step forward at once.
Session-Parallel Mini-Batch Structure
Mini-Batch: 3 Sessions, Variable Lengths 101 45 203 50 Session 1 (len=4) 304 88 pad pad Session 2 (len=2, padded) 7 18 150 99 Session 3 (len=4+) GRU Processing (Parallel) Step t=1: Embed [101, 304, 7] → GRU cells Step t=2: Embed [45, 88, 18] → GRU cells (using h from step 1) Step t=3: Embed [203, pad, 150] → GRU cells Step t=4: Embed [50, pad, 99] → GRU cells; compute loss at this step

Ranking Loss: BPR-max

GRU4Rec uses a ranking-aware loss, not cross-entropy. BPR-max penalizes incorrect rankings, not misaligned probabilities:

BPR-max Loss:
  s_pos = score(h_t, i_{t+1})           # Score of true next item
  s_neg = max over all negative items   # Worst negative sample
  L = -log(σ(s_pos - s_neg))            # σ is sigmoid

  Intuition: Maximize margin between positive and hardest negative.
  The "max" focuses on the hardest negatives to beat.

Loss Details

Loss Formula Use Case
Cross-Entropy -log(P(i_{t+1})) using softmax Calibrated probabilities; slower (softmax over all items).
BPR -log(σ(s_pos - s_neg)) Ranking-focused. s_neg is random negative sample.
BPR-max -log(σ(s_pos - max(s_neg))) Hard negative mining. Focuses on ranking challenge.
TOP-1 σ(-s_pos) + Σ σ(s_neg) Alternative ranking loss; empirically similar to BPR-max.

Training Procedure

Algorithm: GRU4Rec Training

For each epoch:
  For each mini-batch of sessions B:
    1. Pad sessions in B to max length in B
    2. Initialize GRU hidden state h_0 = 0

    For each timestep t in max_length:
      3. Embed items at position t: X_t = Embed(items[t])
      4. Forward GRU: h_t = GRU(X_t, h_{t-1})
      5. Score all items: logits = W @ h_t + b
      6. Identify true next items (labels)
      7. Sample negatives for each session
      8. Compute BPR-max loss
      9. Backward pass (BPTT)
      10. Update params (Adam)

    11. Update h_0 = h_T for next batch (reset per session)

Data Augmentation: Sub-Sequence Sampling

Boost training data by sampling sub-sequences from longer sessions:

  • Session [1, 2, 3, 4, 5] becomes training samples:
  • [1] → 2, [2] → 3, [3] → 4, [4] → 5 (standard)
  • [1, 2] → 3, [2, 3] → 4, [3, 4] → 5 (augmentation)

Increases data 3-5x. Helps with generalization, especially for short sessions.

Efficiency Win: Session-parallel batching lets you train on full sequences without truncation, unlike standard BPTT. All sessions in a batch advance together, massively parallelizing the computation.

SASRec: Self-Attention for Sequential Recommendation

SASRec (Kang et al., 2018) replaces GRU with multi-head self-attention. Key advantage: parallelizable training and explicit item-to-item interaction modeling. For attention internals, see our Transformer guide.

Why Attention Over RNN?

  • Parallelization: All timesteps computed in parallel (attention is O(T²) but not sequential).
  • Long-range dependencies: Attention directly connects distant items; RNN compresses through hidden state.
  • Interpretability: Attention weights show which past items influenced the prediction.
  • Gradient flow: No vanishing gradient problem over long sequences.

SASRec Architecture

SASRec: Transformer Blocks with Causal Masking
Item Sequence [i_1, i_2, i_3, i_4] i_1 i_2 i_3 i_4 Embed + Positional Encoding e_1+p_1 e_2+p_2 e_3+p_3 e_4+p_4 Multi-Head Self-Attention (Causal) Attention( Q, K, V ) Mask: attend t ≤ current only Add & Norm (residual connection) Position-wise Feed-Forward Linear → ReLU hidden_dim = 4 * d Linear → Dropout back to d_model Repeat L times (e.g., L=2) Output: Linear + Softmax Linear(h_T) → logits Softmax → P(next)

Causal Masking

Position t can only attend to positions ≤ t. This prevents the model from "cheating" by looking at future items during training:

Attention Mask (T=4):
[1, 0, 0, 0]      # Position 1 attends only to itself
[1, 1, 0, 0]      # Position 2 attends to 1, 2
[1, 1, 1, 0]      # Position 3 attends to 1, 2, 3
[1, 1, 1, 1]      # Position 4 attends to 1, 2, 3, 4

Set masked positions to -∞ before softmax, they become ~0 after.

SASRec vs GRU4Rec

Aspect GRU4Rec SASRec
Architecture RNN (GRU) hidden state Transformer blocks
Parallelization Sequential (limited) Fully parallel
Long-Range Compressed via h_t Direct attention
Training Speed ~5 steps/sec per GPU ~20 steps/sec per GPU
Latency (Inference) ~10-50ms ~50-200ms (QKV comp)
Loss BPR-max Cross-entropy or sampled softmax
Transformer Foundation: SASRec is a simplified Transformer with causal masking. See transformer_guide.html for multi-head attention math and layer-norm details.

Transformer4Rec: Industrial-Scale Feature Integration

Transformer4Rec extends SASRec for industry use: integrates rich item features (category, price, text), supports pre-training with BERT-style masking, and leverages Hugging Face Transformers.

Feature-Rich Architecture

Instead of embedding just item IDs, concatenate multiple feature embeddings:

Transformer4Rec: Multi-Feature Input
Item = ID + Category + Price + Text Item ID 101 Category Action Price: $15 ID emb Cat emb Price emb Concat d = sum of feature dims Item ID 45 Category Comedy Price: $10 ID emb Cat emb Price emb Concat Feed to Transformer Blocks Multi-Head Attention + FFN (causal) Process all feature-rich embeddings in parallel

BERT-Style Pre-Training

Transformer4Rec supports masked item prediction (similar to BERT's MLM). During pre-training on unlabeled interaction data:

  • Randomly mask 15% of items in a sequence.
  • Task: predict masked item from surrounding context.
  • Fine-tune on downstream next-item prediction task.

Pre-training improves cold-start and generalization. See BERT guide for MLM details.

Training: Causal Language Modeling Loss

Causal LM Loss for next-item prediction:
  L = Σ_t cross_entropy(logits_t, i_{t+1})

  At position t, predict next item i_{t+1} from context [i_1...i_t].
  Compute loss only on non-padded positions.
  Softmax over all items (or sampled softmax for large vocabularies).

Transformer4Rec vs SASRec

Aspect SASRec Transformer4Rec
Features Item ID only ID + Category + Price + Text + Custom
Pre-training None BERT-style MLM on interaction data
Backbone Custom Transformer Hugging Face (GPT-2, XLNet, ALBERT)
Scalability Good (< 1M items) Better (distributed training)
Industry Use Research Production (e.g., streaming platforms)
Feature Engineering: Transformer4Rec shines when you have rich item metadata. Encoding text descriptions as embeddings (using pre-trained models) further boosts performance on cold-start items.

BERT4Rec: Bidirectional Encoding for Recommendations

BERT4Rec (Sun et al., 2019) uses bidirectional attention instead of causal. Unlike SASRec (left-to-right), BERT4Rec can attend to future items during training. Trade-off: better training performance but requires a different decoding strategy at inference.

Unidirectional vs Bidirectional

Attention Masking: Causal (SASRec) vs Bidirectional (BERT4Rec)
SASRec: Causal (Left-to-Right) Position can attend only to positions ≤ itself Pos 1→ [1, 0, 0, 0] Pos 2→ [1, 1, 0, 0] Pos 3→ [1, 1, 1, 0] Pos 4→ [1, 1, 1, 1] Result: sequential dependencies BERT4Rec: Bidirectional Position can attend to all positions (except masked) Pos 1→ [1, 1, 1, 1] Pos 2→ [1, 1, 1, 1] Pos 3→ [1, 1, 1, 1] Pos 4→ [1, 1, 1, 1] Result: full context awareness BERT4Rec Training: Cloze Task Randomly mask 15% of items. Predict masked item from surrounding context. Example: [item1, item2, [MASK], item4] → Predict item3 Advantage: bidirectional context; Disadvantage: no causal structure at inference

Training Procedure: Masked Item Prediction

BERT4Rec Training Algorithm:

For each training sequence [i_1, i_2, i_3, i_4]:
  1. Randomly select 15% of positions (e.g., position 3)
  2. Replace with special [MASK] token
  3. Corrupted sequence: [i_1, i_2, [MASK], i_4]
  4. Feed to bidirectional Transformer
  5. Predict: position 3 → should be i_3
  6. Loss = cross_entropy(logits_3, i_3)
  7. Backprop and update

Repeat for multiple epochs with different masking.

At inference (next-item prediction):
  - Can't use [MASK] since we don't know the next item yet!
  - Use last hidden state directly, or fine-tune with causal mask
  - Some implementations retrain decoder with causal masking

BERT4Rec vs SASRec: When to Use

Aspect SASRec BERT4Rec
Attention Causal (left-to-right) Bidirectional
Training Task Next-item prediction Masked item prediction (Cloze)
Training Efficiency Good (direct task) Very good (self-supervised)
Inference Direct (output layer on h_T) Requires re-decoding or fine-tuning
Cold-Start Moderate Better (bidirectional pre-training)
Use Case Real-time, low-latency systems Offline training, pre-training focused
The Cloze Task: BERT's masked language modeling translates to recommendations as: hide a random item, predict it from left and right context. This is why BERT4Rec works well for training but needs adaptation for inference.

Handling Sequences in Practice

Sequence Truncation & Padding

Real sequences vary in length. Decisions to make:

  • Max sequence length: Typically 50–200 items. Longer sequences truncated to most recent N items.
  • Left-truncation: Keep i_{T-N+1} to i_T, discard older items.
  • Padding: If sequence shorter than max, left-pad with zeros (or a [PAD] token).

Preprocessing Pipeline

Data Preprocessing for Sequential Recommendation
Raw Logs Timestamps, User, Item Session 30-min window or user ID Truncate Keep last N=50 items Pad Left Add zeros to length 50 Embed Item IDs → Vectors Ready for Model Example: Session [101, 45, 203] Original: [101, 45, 203] (len=3) Padded: [0, 0, ..., 0, 101, 45, 203] (len=50) Embedded: [e₀, e₀, ..., e₀, e_101, e_45, e_203] where e₀ is zero vector Mask: [0, 0, ..., 0, 1, 1, 1] (1 = valid position)

Item Embeddings: Learned + Pre-Trained

Approach 1: Learned Embeddings

  • Initialize random D-dimensional vectors for each item.
  • Update via backprop during training.
  • Pro: tailored to your data. Con: cold-start for new items.

Approach 2: Pre-Trained + Side Info

  • Use content embeddings (e.g., text BERT embeddings for movie descriptions).
  • Concatenate with learned ID embedding.
  • Pro: better cold-start. Con: higher dimensional, slower inference.

Temporal Encoding: Gap Between Items

Encode time gaps between interactions:

Time Gap Encoding:
  gap_t = t_i - t_{i-1}  # seconds, minutes, or days

  Option 1: Sinusoidal (from Transformer)
    pos_enc(gap_t) = [sin(gap_t/10000^0), cos(...), sin(...), ...]

  Option 2: Learned embedding
    time_emb = Embedding(quantize(gap_t))  # e.g., buckets: [0, 1h), [1h, 1d), etc.

  Concatenate with item embedding to form augmented feature.
  Helps model distinguish between rapid clicks vs long waits.

Negative Sampling

For ranking loss (BPR-max), sample negatives uniformly or by popularity:

  • Uniform sampling: Random items from vocabulary. Simple, but many easy negatives.
  • Popularity-based (POP-weighted): Sample with probability ∝ item frequency. Focuses on harder negatives.
  • Hard negatives: Items similar to the positive (via embeddings). Most challenging.

Cold-Start Handling

New users with few interactions:

  • Content-based fallback: Recommend popular items or items similar to browsed categories.
  • Transfer learning: Use pre-trained embeddings from large datasets.
  • User features: If available (age, location), use as auxiliary input.
Padding Strategy Matters: Left-padding (prepend zeros) preserves positional encoding. Right-padding (append zeros) is standard for attention masking. Choose based on your positional encoding scheme.

Evaluation & Metrics

Standard Metrics for Sequential Recommendation

Metric Formula Pros / Cons
HR@K
(Hit Rate)
|{ground truth in top-K}| / |test| Pros: Simple, interpretable. Cons: ignores rank position.
NDCG@K
(Normalized DCG)
DCG@K / IDCG@K where
DCG = Σ_{i=1}^K log₂(1+i)^{-1} × rel_i
Pros: Position-aware. Cons: complex, less intuitive.
MRR@K
(Mean Reciprocal Rank)
avg(1/rank of first hit) Pros: Measures ranking quality. Cons: only rewards first hit.
Recall@K |{true items in top-K}| / |all true items| Pros: Coverage metric. Cons: assumes multiple true items.

Evaluation Protocol: Leave-One-Out

Standard approach for session-based recommendation:

  • For each session, hold out the last item as ground truth.
  • Use items [i_1, ..., i_{T-1}] as input.
  • Model predicts top-K items.
  • Check if i_T is in top-K.
  • Repeat for all test sessions, compute HR@K, NDCG@K, MRR.

Ranking Evaluation: Full vs Sampled

Type Method Cost / Accuracy Trade-off
Full Ranking Rank against all |V| items Expensive (O(|V|)), but accurate metrics.
Sampled Ranking Rank against 100–1000 samples Fast (O(sample size)), approximate metrics.
Approximate Indexing Use FAISS or approximate NN Trade-off: near-exact metrics, O(log|V|) lookup.

Benchmark: GRU4Rec vs SASRec vs BERT4Rec on MovieLens

Comparative Results (Illustrative)
HR@10 on MovieLens-1M, 1-session evaluation 0% 50% 55% GRU4Rec 64% SASRec 67% BERT4Rec 70% Transformer Note: Benchmark results vary by dataset, hyperparameters, and feature richness. Transformer4Rec with rich features generally wins.
Be Careful with Metrics: Small differences in HR@10 can be noise. Use statistical significance testing (t-test or bootstrap) when comparing models. Also report wall-clock latency for real-time serving.

When to Use Which Model

Comprehensive Comparison

Dimension GRU4Rec SASRec BERT4Rec Transformer4Rec
Inference Latency ~10–50ms ~50–200ms ~100–300ms ~200–500ms
Feature Support ID only ID + optional metadata ID only (pre-train) ID + rich features
Cold-Start (New Items) Poor (random init) Moderate (can leverage features) Good (pre-trained) Very Good (content + pre-train)
Training Speed ~50 steps/sec ~200 steps/sec ~150 steps/sec ~100 steps/sec
Accuracy (Relative) 100% (baseline) ~115% ~120% ~125%
Implementation Effort Low (simple RNN) Medium (attention) Medium (masking logic) High (HF integration)
Production Readiness High (mature) High (proven) Medium (research) High (industry-proven)

Decision Flowchart

Model Selection Decision Tree
Sequential Rec Task Real-time inference needed? (< 100ms) YES GRU4Rec • Fast (RNN) • Mature NO Rich item features available? YES Transformer4Rec • Feature integration • Industrial scale NO Long history? (many items per session) YES SASRec • Parallelizable • Long-range deps NO BERT4Rec • Self-supervised • Pre-training

Rule-of-Thumb Selections

Choose GRU4Rec When:

  • Real-time (< 50ms latency) needed
  • Limited computational budget
  • Session-based (no user ID)
  • Production stability is priority

Choose SASRec When:

  • Longer sequences (100+ items)
  • Can tolerate 50–100ms latency
  • Need better accuracy
  • GPU training available

Choose BERT4Rec When:

  • Offline pre-training is OK
  • Need strong cold-start
  • Accuracy more important than latency
  • Research-focused projects

Choose Transformer4Rec When:

  • Rich item metadata available
  • Industry-scale system
  • Pre-training infrastructure ready
  • Maximum accuracy needed
Hybrid Approach: Train GRU4Rec for real-time serving, train SASRec offline for batch recommendations. Use SASRec's embeddings to warm-start GRU4Rec for faster convergence.

Python Implementation: GRU4Rec & SASRec from Scratch

Build core models with NumPy and Pandas. This demonstrates the internal mechanics and is suitable for educational purposes and small-scale testing.

Synthetic Dataset

import numpy as np
import pandas as pd
from collections import defaultdict

# Generate synthetic sessions
np.random.seed(42)
num_sessions = 500
num_items = 100
max_seq_len = 10

sessions = []
for s_id in range(num_sessions):
    # Random session length
    seq_len = np.random.randint(2, max_seq_len + 1)
    # Sample items without replacement
    seq = np.random.choice(num_items, seq_len, replace=False).tolist()
    sessions.append(seq)

print(f"Sessions: {len(sessions)}, Items: {num_items}")
print(f"Example session: {sessions[0]}")

# Split: 80% train, 20% test
train_sessions = sessions[:400]
test_sessions = sessions[400:]

print(f"Train: {len(train_sessions)}, Test: {len(test_sessions)}")

GRU4Rec Implementation (Simplified)

class GRU4Rec:
    def __init__(self, num_items, embedding_dim=32, hidden_dim=64, lr=0.01):
        self.num_items = num_items
        self.embedding_dim = embedding_dim
        self.hidden_dim = hidden_dim
        self.lr = lr

        # Initialize embeddings and weights
        self.item_embed = np.random.randn(num_items, embedding_dim) * 0.01
        self.W_z = np.random.randn(embedding_dim + hidden_dim, hidden_dim) * 0.01  # Update gate
        self.W_r = np.random.randn(embedding_dim + hidden_dim, hidden_dim) * 0.01  # Reset gate
        self.W = np.random.randn(embedding_dim + hidden_dim, hidden_dim) * 0.01    # Candidate
        self.W_out = np.random.randn(hidden_dim, num_items) * 0.01                 # Output
        self.b_z = np.zeros(hidden_dim)
        self.b_r = np.zeros(hidden_dim)
        self.b = np.zeros(hidden_dim)
        self.b_out = np.zeros(num_items)

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

    def tanh(self, x):
        return np.tanh(x)

    def gru_forward(self, x_t, h_prev):
        """Single GRU step: x_t (embedding), h_prev (hidden state)"""
        combined = np.concatenate([h_prev, x_t])

        z_t = self.sigmoid(combined @ self.W_z + self.b_z)      # Update gate
        r_t = self.sigmoid(combined @ self.W_r + self.b_r)      # Reset gate
        h_tilde = self.tanh(np.concatenate([r_t * h_prev, x_t]) @ self.W + self.b)
        h_t = (1 - z_t) * h_prev + z_t * h_tilde
        return h_t, z_t, r_t, h_tilde

    def forward(self, session):
        """Forward pass for a single session"""
        h = np.zeros(self.hidden_dim)
        h_list = [h]

        for item_id in session:
            x_t = self.item_embed[item_id]
            h, _, _, _ = self.gru_forward(x_t, h)
            h_list.append(h)

        # Output at final step
        logits = h @ self.W_out + self.b_out
        return logits, h, h_list

    def train_step(self, session):
        """Train on one session (simplified: ignore mask for now)"""
        if len(session) < 2:
            return 0

        # Forward pass
        logits, h_final, h_list = self.forward(session[:-1])

        # Compute ranking loss (simplified BPR-max)
        true_item = session[-1]
        pos_score = logits[true_item]

        # Sample negatives
        neg_items = np.random.choice([i for i in range(self.num_items) if i != true_item], 5)
        neg_scores = logits[neg_items]
        max_neg = np.max(neg_scores)

        # BPR loss (simplified): -log(sigmoid(pos - max_neg))
        loss = -np.log(self.sigmoid(pos_score - max_neg) + 1e-10)

        # Backward pass (simplified gradient update)
        grad_logits = np.zeros(self.num_items)
        grad_logits[true_item] = -1 / (self.sigmoid(pos_score - max_neg) + 1e-10)
        grad_logits[neg_items] = 1 / (self.sigmoid(pos_score - max_neg) + 1e-10) * (1 / len(neg_items))

        grad_h = grad_logits @ self.W_out.T
        self.W_out -= self.lr * np.outer(h_final, grad_logits)
        self.b_out -= self.lr * grad_logits

        return loss

    def evaluate(self, test_sessions, k=10):
        """Evaluate HR@K"""
        hits = 0
        for session in test_sessions:
            if len(session) < 2:
                continue
            logits, _, _ = self.forward(session[:-1])
            top_k = np.argsort(-logits)[:k]
            if session[-1] in top_k:
                hits += 1

        hr = hits / len(test_sessions) if test_sessions else 0
        return hr

# Training loop
model = GRU4Rec(num_items=num_items, embedding_dim=32, hidden_dim=64)
for epoch in range(10):
    total_loss = 0
    for session in train_sessions:
        loss = model.train_step(session)
        total_loss += loss

    hr = model.evaluate(test_sessions, k=10)
    print(f"Epoch {epoch+1}: Loss={total_loss/len(train_sessions):.4f}, HR@10={hr:.4f}")

SASRec-like Implementation (Attention Layer)

class SimpleAttention:
    def __init__(self, d_model=32, num_heads=2, dropout=0.1):
        self.d_model = d_model
        self.num_heads = num_heads
        self.head_dim = d_model // num_heads
        self.dropout = dropout

        self.W_q = np.random.randn(d_model, d_model) * 0.01
        self.W_k = np.random.randn(d_model, d_model) * 0.01
        self.W_v = np.random.randn(d_model, d_model) * 0.01
        self.W_o = np.random.randn(d_model, d_model) * 0.01

    def scaled_dot_product_attention(self, Q, K, V, mask=None):
        """
        Q, K, V: (seq_len, d_model)
        mask: (seq_len, seq_len) causal mask
        """
        scores = (Q @ K.T) / np.sqrt(self.head_dim)

        if mask is not None:
            scores[mask == 0] = -1e9

        attn = self.softmax(scores)
        out = attn @ V
        return out, attn

    def softmax(self, x):
        exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
        return exp_x / np.sum(exp_x, axis=-1, keepdims=True)

    def forward(self, X, mask=None):
        """
        X: (seq_len, d_model) - sequence of embeddings
        mask: (seq_len, seq_len) - causal mask (lower triangular)
        """
        Q = X @ self.W_q
        K = X @ self.W_k
        V = X @ self.W_v

        attn_out, attn_weights = self.scaled_dot_product_attention(Q, K, V, mask)
        output = attn_out @ self.W_o
        return output, attn_weights


class SASRecSimplified:
    def __init__(self, num_items, d_model=32, num_heads=2, lr=0.01):
        self.num_items = num_items
        self.d_model = d_model
        self.num_heads = num_heads
        self.lr = lr

        self.item_embed = np.random.randn(num_items, d_model) * 0.01
        self.pos_embed = np.random.randn(100, d_model) * 0.01  # Max seq len
        self.attention = SimpleAttention(d_model, num_heads)
        self.W_out = np.random.randn(d_model, num_items) * 0.01
        self.b_out = np.zeros(num_items)

    def forward(self, session):
        """Forward pass with attention"""
        seq_len = len(session)

        # Embed and add positional encoding
        X = np.zeros((seq_len, self.d_model))
        for i, item_id in enumerate(session):
            X[i] = self.item_embed[item_id] + self.pos_embed[i]

        # Causal mask
        mask = np.tril(np.ones((seq_len, seq_len)))

        # Attention
        attn_out, _ = self.attention.forward(X, mask)

        # Output at last position
        h_final = attn_out[-1]
        logits = h_final @ self.W_out + self.b_out
        return logits, attn_out

    def evaluate(self, test_sessions, k=10):
        """Evaluate HR@K"""
        hits = 0
        for session in test_sessions:
            if len(session) < 2:
                continue
            logits, _ = self.forward(session[:-1])
            top_k = np.argsort(-logits)[:k]
            if session[-1] in top_k:
                hits += 1

        hr = hits / len(test_sessions) if test_sessions else 0
        return hr

# Quick eval
sasrec = SASRecSimplified(num_items=num_items, d_model=32)
hr_sasrec = sasrec.evaluate(test_sessions, k=10)
print(f"SASRec HR@10: {hr_sasrec:.4f}")

Evaluation Comparison

print("\n=== Model Comparison ===")
print(f"GRU4Rec HR@10:    {model.evaluate(test_sessions, k=10):.4f}")
print(f"SASRec HR@10:     {sasrec.evaluate(test_sessions, k=10):.4f}")

# Timing
import time

start = time.time()
for _ in range(100):
    gru_logits, _, _ = model.forward(test_sessions[0][:-1])
gru_time = (time.time() - start) / 100 * 1000

start = time.time()
for _ in range(100):
    sasrec_logits, _ = sasrec.forward(test_sessions[0][:-1])
sasrec_time = (time.time() - start) / 100 * 1000

print(f"\nInference Time (ms):")
print(f"GRU4Rec:  {gru_time:.2f}ms")
print(f"SASRec:   {sasrec_time:.2f}ms")
print(f"Ratio (SASRec/GRU4Rec): {sasrec_time/gru_time:.2f}x")
For Production: Use libraries like RecBole, TensorFlow/PyTorch for efficiency. This NumPy code is for understanding. Real implementations include GPU support, distributed training, and extensive optimizations.
Next Steps: Extend with:
  • Batch training with mini-batches
  • Negative sampling strategies
  • Temporal features (time gaps)
  • Content embeddings (side information)
  • Multi-head attention (full implementation)
End of Sequential Recommendations