CAFF / caff /model.py
MrDhifallah's picture
Upload folder using huggingface_hub
634ebe8 verified
Raw
History Blame Contribute Delete
7.92 kB
"""
caff/model.py
=============
Full CAFFModel β€” assembles encoder, CSV, per-hop scorers into a
single trainable module.
Per paper Β§8.4, learnable capacity must be < 12M params (~3.5%
of the 340M-param frozen backbone). This is asserted at __init__.
Architecture summary (paper Algorithm 1):
for β„“ = 1 to L:
C_β„“ ← BFS(G, S(Q), β„“) # Eq. 1
C_β„“ ← FreqCap(C_β„“, K_r) # Eq. 13
g_β„“ ← Οƒ(P_β„“ z_{β„“-1} + b^g_β„“) # Eq. 16
Ξ”_β„“ ← U_β„“ diag(g_β„“) V_β„“^T # Eq. 17
W^ctx_β„“ ← W_0 + A_β„“ B_β„“^T + Ξ”_β„“ # Eq. 18
for (h, r, t) ∈ C_β„“:
s ← Οƒ(q^T W^ctx_β„“ e_r + v^T(q βŠ™ e_r) + Ξ²_β„“) # Eq. 19
if s β‰₯ ΞΈ: S_β„“ ← S_β„“ βˆͺ {(h,r,t)}
z_β„“ ← CSV(S_β„“) # Eq. 14
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
import torch
import torch.nn as nn
from .config import AblationFlags, CAFFConfig
from .csv import CSV
from .encoders import RelationEmbeddingCache
from .scorer import HopScorer
logger = logging.getLogger(__name__)
@dataclass
class HopOutput:
"""Output of scoring a single hop."""
logits: torch.Tensor # pre-sigmoid scores, shape (N,)
scores: torch.Tensor # sigmoid-activated, shape (N,)
retained_mask: torch.Tensor # bool, shape (N,) β€” True if score >= ΞΈ
z_next: torch.Tensor # CSV computed from retained set, shape (d,)
class CAFFModel(nn.Module):
"""Full CAFF filtering model (paper Β§6).
Parameters
----------
config : CAFFConfig
Hyperparameters from paper Β§8.4.
relation_cache : RelationEmbeddingCache
Pre-encoded frozen relation embeddings.
ablation : AblationFlags
Component toggles for ablation study (paper Β§10.1).
"""
def __init__(
self,
config: CAFFConfig,
relation_cache: RelationEmbeddingCache,
ablation: AblationFlags | None = None,
) -> None:
super().__init__()
self.config = config
self.ablation = ablation or AblationFlags()
self.relation_cache = relation_cache
d = config.d
rho = config.rho
L = config.L
# Shared parameters
self.W0 = nn.Parameter(torch.empty(d, d))
nn.init.xavier_uniform_(self.W0)
self.v = nn.Parameter(torch.zeros(d)) # element-wise interaction
nn.init.normal_(self.v, mean=0.0, std=0.01)
# Per-hop scorers (one HopScorer per β„“ ∈ {1, ..., L})
self.hop_scorers = nn.ModuleList([
HopScorer(
d=d,
rho=rho,
gate_activation=self.ablation.gate_activation,
use_dbm=self.ablation.use_dbm,
)
for _ in range(L)
])
# CSV is parameter-free, but holds a reference to the cache
self.csv = CSV(relation_cache, pool=self.ablation.csv_pool)
# Sanity check: paper Β§8.4 says learnable params < 12M
n_trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
logger.info(
f"CAFFModel initialized: {n_trainable / 1e6:.2f}M trainable params "
f"(paper budget: <12M)"
)
if n_trainable > 12_000_000:
logger.warning(
f"Trainable param count {n_trainable / 1e6:.2f}M exceeds "
f"paper's <12M budget. Check d, rho, L."
)
@property
def device(self) -> torch.device:
return next(self.parameters()).device
def get_hop_W_ctx(
self,
hop_idx: int,
z_prev: torch.Tensor,
) -> torch.Tensor:
"""Compute W^ctx_β„“ for a batch of items at hop β„“ (Eq. 18).
Parameters
----------
hop_idx : int in {0, 1, ..., L-1}
z_prev : Tensor of shape (B, d)
CSV from hop β„“-1 (or zeros at hop 0 / when set is empty).
If ablation.use_csv is False, z_prev is forcibly zeroed.
Returns
-------
W_ctx : Tensor of shape (B, d, d)
"""
if not self.ablation.use_csv:
# Ablation Β§10.1: w/o CSV β†’ z_{β„“-1} ≑ 0 β†’ reduces to DepthBilinear
z_prev = torch.zeros_like(z_prev)
return self.hop_scorers[hop_idx].compute_W_ctx(self.W0, z_prev)
def score_hop_candidates(
self,
hop_idx: int,
W_ctx_single: torch.Tensor,
q: torch.Tensor,
relation_names: list[str],
return_logits: bool = False,
) -> torch.Tensor:
"""Score all candidates of a single (query, hop) β€” Eq. 19.
Smart Engineering S3: W_ctx_single is computed ONCE per
(query, hop), then this method scores all N_β„“ candidates
against it in a single matmul.
Parameters
----------
hop_idx : int
W_ctx_single : Tensor of shape (d, d)
Pre-computed W^ctx for one item.
q : Tensor of shape (d,)
β„“2-normalized query embedding.
relation_names : list of str, length N
Relation names of the candidate triples.
return_logits : bool
If True, return pre-sigmoid logits (for BCEWithLogitsLoss).
Returns
-------
Tensor of shape (N,).
"""
E_r = self.relation_cache.get_batch(relation_names) # (N, d)
scorer = self.hop_scorers[hop_idx]
if return_logits:
return scorer.score_logits(W_ctx_single, self.v, q, E_r)
return scorer.score_candidates(W_ctx_single, self.v, q, E_r)
@torch.no_grad()
def filter_query(
self,
q: torch.Tensor,
candidate_sets: list[list[tuple[str, str, str]]],
) -> list[list[tuple[str, str, str]]]:
"""Apply CAFF filtering to a single query β€” Algorithm 1 (paper).
Parameters
----------
q : Tensor of shape (d,)
β„“2-normalized query embedding.
candidate_sets : list of length L
candidate_sets[β„“] is the BFS-extracted (and FreqCapped)
list of (h, r, t) tuples at hop β„“+1.
Returns
-------
retained_sets : list of length L
retained_sets[β„“] is S_{β„“+1} = {(h,r,t) : s_β„“(...) β‰₯ ΞΈ}.
"""
L = self.config.L
theta = self.config.theta
d = self.config.d
# Initialize: z_0 = 0, S_0 = βˆ… (Algorithm 1, line 2)
z_prev = torch.zeros(1, d, device=self.device) # (1, d) β€” single query
retained_sets: list[list[tuple[str, str, str]]] = []
for hop_idx in range(L):
candidates = candidate_sets[hop_idx]
if len(candidates) == 0:
retained_sets.append([])
# z stays the same β€” empty retained set carries no info
continue
# Eq. 18: compute W^ctx for this hop given z_prev
W_ctx = self.get_hop_W_ctx(hop_idx, z_prev).squeeze(0) # (d, d)
# Eq. 19: score all candidates
relation_names = [r for (_, r, _) in candidates]
scores = self.score_hop_candidates(
hop_idx, W_ctx, q.squeeze(0) if q.dim() > 1 else q, relation_names
)
# Eq. 2: retain if score >= ΞΈ
retained = [
triple for triple, s in zip(candidates, scores.tolist())
if s >= theta
]
retained_sets.append(retained)
# Eq. 14: update z for next hop
retained_relations = [r for (_, r, _) in retained]
z_prev = self.csv([retained_relations]) # (1, d)
return retained_sets