Loading your learning…
Loading your learning…
Ranking scores C candidates for each of B examples, so candidate_ids is [B, C]. The model itself doesn't change: flatten the candidate axis into the batch axis, score all B×C pairs in one call, then reshape the results back to [B, C]. Each candidate still gets its own history weights and interest vector. Each history has N positions, with ID 0 reserved for padding. The included model learns D-feature item vectors, scores each history item against its candidate, sums the weighted vectors, and maps that summary plus the candidate to a click logit. Sigmoid converts each logit to a probability. The file includes this entire model; no earlier example is required.
import torchfrom torch import nn class TargetAwareEmbedding(nn.Module): def __init__(self, num_items, dim, padding_idx=0): super().__init__() self.padding_idx = padding_idx self.embedding = nn.Embedding(num_items, dim, padding_idx=padding_idx) self.score = nn.Sequential( nn.Linear(4 * dim, 32), nn.PReLU(), nn.Linear(32, 1) ) def forward(self, history_ids, target_ids): # history_ids: [B, N]; target_ids: [B]. Targets must be real items. valid = history_ids.ne(self.padding_idx) # [B, N] items = self.embedding(history_ids) # [B, N, D] target = self.embedding(target_ids) # [B, D] query = target[:, None, :].expand_as(items) # [B, N, D] features = torch.cat( [items, query, items - query, items * query], dim=-1 ) # [B, N, 4D] weights = self.score(features).squeeze(-1) # [B, N] weights = weights.masked_fill(~valid, 0.0) # No softmax interest = (weights[..., None] * items).sum(dim=1) # [B, D] return interest, target, weights class ClickPredictor(nn.Module): def __init__(self, num_items, dim): super().__init__() self.history = TargetAwareEmbedding(num_items, dim) self.head = nn.Sequential( nn.Linear(2 * dim, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, history_ids, target_ids): interest, target, weights = self.history(history_ids, target_ids) features = torch.cat([interest, target], dim=-1) # [B, 2D] logits = self.head(features).squeeze(-1) # [B] return logits, weights if __name__ == "__main__": torch.manual_seed(0) model = ClickPredictor(num_items=6, dim=8).eval() history_ids = torch.tensor([[1, 2, 0], [3, 4, 5]]) # [B=2, N=3] candidate_ids = torch.tensor([[3, 4], [1, 2]]) # [B=2, C=2] batch, candidates = candidate_ids.shape length = history_ids.shape[1] histories = history_ids[:, None, :].expand(batch, candidates, length) flat_histories = histories.reshape(batch * candidates, length) flat_targets = candidate_ids.reshape(batch * candidates) with torch.no_grad(): flat_logits, flat_weights = model(flat_histories, flat_targets) probabilities = flat_logits.sigmoid().reshape(batch, candidates) weights = flat_weights.reshape(batch, candidates, length) print("Candidate probabilities:", probabilities) # [B, C] print("Candidate-specific history weights:", weights) # [B, C, N] # Use a trained checkpoint for real ranking; this demonstrates batching.history_ids [B, N] holds one history per example; candidate_ids [B, C] holds C candidates for each of those histories. With B=2 and C=2 the model scores four history/candidate pairs, not four history items. C is the ranking axis; N is the axis that pooling sums over.
torch.manual_seed(0) model = ClickPredictor(num_items=6, dim=8).eval() history_ids = torch.tensor([[1, 2, 0], [3, 4, 5]]) # [B=2, N=3] candidate_ids = torch.tensor([[3, 4], [1, 2]]) # [B=2, C=2] batch, candidates = candidate_ids.shape length = history_ids.shape[1] histories = history_ids[:, None, :].expand(batch, candidates, length) flat_histories = histories.reshape(batch * candidates, length)