resonate-api / app /core /models.py
sandy898's picture
Initial backend deployment
d597de7
Raw
History Blame Contribute Delete
5.46 kB
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
MAX_SLATE_SIZE = 160
EMBED_DIM = 256
class TriModalTwoTower(nn.Module):
def __init__(self, num_items, plot_embs, gnn_embs, scalars):
super().__init__()
self.register_buffer("plot_feats", torch.tensor(np.vstack([np.zeros((1, 1024)), plot_embs]), dtype=torch.float32))
self.register_buffer("gnn_feats", torch.tensor(np.vstack([np.zeros((1, 256)), gnn_embs]), dtype=torch.float32))
self.register_buffer("scalar_feats", torch.tensor(np.vstack([np.zeros((1, 10)), scalars]), dtype=torch.float32))
self.plot_proj = nn.Linear(1024, 256)
self.gnn_proj = nn.Linear(256, 128)
self.scalar_proj = nn.Linear(10, 64)
self.content_fusion = nn.Sequential(
nn.Linear(256 + 128 + 64, 512), nn.LayerNorm(512), nn.GELU(), nn.Dropout(0.1),
nn.Linear(512, EMBED_DIM), nn.LayerNorm(EMBED_DIM)
)
self.cf_emb = nn.Embedding(num_items + 1, EMBED_DIM, padding_idx=0)
self.cf_norm = nn.LayerNorm(EMBED_DIM)
self.id_dropout = nn.Dropout(0.3)
self.rating_emb = nn.Embedding(6, EMBED_DIM, padding_idx=0)
self.long_attention = nn.MultiheadAttention(embed_dim=EMBED_DIM, num_heads=4, batch_first=True)
self.long_proj = nn.Sequential(nn.Linear(EMBED_DIM, EMBED_DIM), nn.LayerNorm(EMBED_DIM))
self.short_gru = nn.GRU(EMBED_DIM, EMBED_DIM, batch_first=True)
self.short_proj = nn.Sequential(nn.Linear(EMBED_DIM, EMBED_DIM), nn.LayerNorm(EMBED_DIM))
self.gate = nn.Sequential(nn.Linear(EMBED_DIM * 2, EMBED_DIM), nn.GELU(), nn.Linear(EMBED_DIM, 1), nn.Sigmoid())
def get_item_representations(self, item_ids, is_target=False):
p = F.relu(self.plot_proj(self.plot_feats[item_ids]))
g = F.relu(self.gnn_proj(self.gnn_feats[item_ids]))
s = F.relu(self.scalar_proj(self.scalar_feats[item_ids]))
content_vec = self.content_fusion(torch.cat([p, g, s], dim=-1))
cf_vec = self.cf_norm(self.cf_emb(item_ids))
if not is_target: cf_vec = self.id_dropout(cf_vec)
return content_vec + cf_vec
def forward_user(self, long_items, long_ratings, short_items, short_ratings):
long_embs = self.get_item_representations(long_items, is_target=False) + self.rating_emb(long_ratings)
l_pad_mask = (long_items == 0)
attn_out, _ = self.long_attention(long_embs, long_embs, long_embs, key_padding_mask=l_pad_mask)
mask_f = (~l_pad_mask).unsqueeze(-1).float()
long_vec = self.long_proj((attn_out * mask_f).sum(dim=1) / mask_f.sum(dim=1).clamp(min=1e-6))
short_embs = self.get_item_representations(short_items, is_target=False) + self.rating_emb(short_ratings)
lengths = (~(short_items == 0)).sum(dim=1).clamp(min=1).cpu()
packed = nn.utils.rnn.pack_padded_sequence(short_embs, lengths, batch_first=True, enforce_sorted=False)
_, hidden = self.short_gru(packed)
short_vec = self.short_proj(hidden.squeeze(0))
alpha = self.gate(torch.cat([long_vec, short_vec], dim=-1))
return F.normalize(alpha * short_vec + (1.0 - alpha) * long_vec, p=2, dim=1)
def forward_item(self, item_ids):
return F.normalize(self.get_item_representations(item_ids, is_target=True), p=2, dim=1)
class SemanticSlateRanker(nn.Module):
def __init__(self, continuous_dim, num_items, num_franchises, pretrained_text_embs, d_model=128, dropout=0.1):
super().__init__()
self.item_emb = nn.Embedding(num_items, pretrained_text_embs.shape[1], padding_idx=0)
self.item_emb.weight.data.copy_(torch.from_numpy(pretrained_text_embs).float())
self.item_emb.weight.requires_grad = False
self.franchise_emb = nn.Embedding(num_franchises, 16, padding_idx=0)
self.pos_encoder = nn.Embedding(MAX_SLATE_SIZE, d_model)
self.item_proj = nn.Sequential(nn.Linear(pretrained_text_embs.shape[1], d_model), nn.LayerNorm(d_model), nn.GELU())
# continuous_dim (11) + franchise_emb (16) = 27
self.tab_proj = nn.Sequential(
nn.Linear(continuous_dim + 16, d_model), nn.LayerNorm(d_model), nn.GELU(),
nn.Linear(d_model, d_model), nn.LayerNorm(d_model), nn.GELU()
)
self.fusion_proj = nn.Sequential(nn.Linear(d_model * 2, d_model), nn.LayerNorm(d_model), nn.GELU())
encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=4, dim_feedforward=d_model*2, dropout=dropout, batch_first=True, norm_first=True)
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=2, enable_nested_tensor=False)
self.scorer = nn.Sequential(nn.Linear(d_model, 64), nn.GELU(), nn.Linear(64, 1))
def forward(self, tab, f_ids, item_ids, valid_mask):
f_embs = self.franchise_emb(f_ids)
tab_encoded = self.tab_proj(torch.cat([tab, f_embs], dim=-1))
c_emb = self.item_proj(self.item_emb(item_ids))
fused = self.fusion_proj(torch.cat([tab_encoded, c_emb], dim=-1))
seq_length = tab.size(1)
positions = torch.arange(seq_length, device=tab.device).unsqueeze(0).expand(tab.size(0), -1)
contextualized = self.transformer(fused + self.pos_encoder(positions), src_key_padding_mask=~valid_mask)
return self.scorer(contextualized).squeeze(-1)