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
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.
Attention-Based (SASRec)
Self-attention directly models item-to-item relationships. Parallelizable. Captures long-range dependencies. Better than RNN for longer sequences.
Transformer-Based (Transformer4Rec)
Stacked attention blocks with rich feature integration. Pre-training with BERT-style MLM. Industrial scale. See Transformer guide.
Graph-Based (GCN/GAT)
Model items as nodes, sequences as temporal edges. Capture cross-session item correlations. Best for discovery tasks. See Graph embeddings.
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
Pipeline: From Sequence to Top-K Predictions
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 stateGRU4Rec Pipeline
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. |
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.
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.
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
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 |
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:
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) |
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
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 |
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
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.
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
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
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
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")- Batch training with mini-batches
- Negative sampling strategies
- Temporal features (time gaps)
- Content embeddings (side information)
- Multi-head attention (full implementation)