ProCreations's picture
download
raw
15.3 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import Dict, Optional, Sequence, Tuple
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from .base import RecommenderBase
def bpr_loss(pos: torch.Tensor, neg: torch.Tensor) -> torch.Tensor:
return -torch.log(torch.sigmoid(pos - neg) + 1e-8).mean()
def _normalized_adj(
num_q: int,
num_a: int,
interactions: Sequence[Tuple[int, int]],
device: torch.device,
) -> torch.Tensor:
"""
Build the symmetric normalized adjacency matrix for a bipartite graph.
Nodes are ordered as [queries, agents].
"""
n_nodes = num_q + num_a
if n_nodes == 0:
raise ValueError("Cannot build adjacency with zero nodes.")
if not interactions:
indices = torch.empty((2, 0), dtype=torch.long, device=device)
values = torch.empty((0,), dtype=torch.float32, device=device)
return torch.sparse_coo_tensor(indices, values, (n_nodes, n_nodes), device=device)
rows = []
cols = []
for qi, ai in interactions:
rows.extend([qi, num_q + ai])
cols.extend([num_q + ai, qi])
indices = torch.tensor([rows, cols], dtype=torch.long, device=device)
data = torch.ones(len(rows), dtype=torch.float32, device=device)
adj = torch.sparse_coo_tensor(indices, data, (n_nodes, n_nodes), device=device)
deg = torch.sparse.sum(adj, dim=1).to_dense()
deg_inv_sqrt = torch.pow(deg + 1e-8, -0.5)
norm_values = data * deg_inv_sqrt[indices[0]] * deg_inv_sqrt[indices[1]]
norm_adj = torch.sparse_coo_tensor(indices, norm_values, (n_nodes, n_nodes), device=device)
return norm_adj.coalesce()
class GraphRecommenderBase(RecommenderBase):
def __init__(
self,
num_q: int,
num_a: int,
embed_dim: int,
interactions: Sequence[Tuple[int, int]],
*,
agent_content: Optional[torch.Tensor] = None,
agent_tool_indices_padded: Optional[torch.LongTensor] = None,
agent_tool_mask: Optional[torch.FloatTensor] = None,
agent_llm_idx: Optional[torch.LongTensor] = None,
num_tools: int = 0,
num_llm_ids: int = 0,
use_query_id_emb: bool = True,
use_tool_id_emb: bool = True,
use_llm_id_emb: bool = True,
use_agent_content: bool = True,
device: Optional[torch.device] = None,
) -> None:
super().__init__()
self.num_q = int(num_q)
self.num_a = int(num_a)
self.embed_dim = int(embed_dim)
self.use_query_id_emb = bool(use_query_id_emb)
self.use_tool_id_emb = bool(use_tool_id_emb) and num_tools > 0
self.use_llm_id_emb = bool(use_llm_id_emb) and num_llm_ids > 0
self.use_agent_content = bool(use_agent_content) and agent_content is not None
self.emb_q = nn.Embedding(num_q, embed_dim)
self.emb_a = nn.Embedding(num_a, embed_dim)
if self.use_agent_content:
self.content_proj = nn.Linear(agent_content.size(1), embed_dim)
self.register_buffer("agent_content", agent_content.float())
else:
self.content_proj = None
self.register_buffer("agent_content", torch.zeros((num_a, 0), dtype=torch.float32))
if self.use_tool_id_emb:
self.emb_tool = nn.Embedding(num_tools, embed_dim)
self.register_buffer(
"agent_tool_indices",
(agent_tool_indices_padded if agent_tool_indices_padded is not None else torch.zeros((num_a, 1), dtype=torch.long)),
)
self.register_buffer(
"agent_tool_mask",
(agent_tool_mask if agent_tool_mask is not None else torch.zeros((num_a, 1), dtype=torch.float32)),
)
else:
self.emb_tool = None
self.register_buffer("agent_tool_indices", torch.zeros((num_a, 1), dtype=torch.long))
self.register_buffer("agent_tool_mask", torch.zeros((num_a, 1), dtype=torch.float32))
if self.use_llm_id_emb:
self.emb_llm = nn.Embedding(num_llm_ids, embed_dim)
self.register_buffer(
"agent_llm_idx",
agent_llm_idx if agent_llm_idx is not None else torch.zeros((num_a,), dtype=torch.long),
)
else:
self.emb_llm = None
self.register_buffer("agent_llm_idx", torch.zeros((num_a,), dtype=torch.long))
self.register_buffer("norm_adj", _normalized_adj(num_q, num_a, interactions, device=device or torch.device("cpu")))
self.reset_parameters()
def reset_parameters(self) -> None:
nn.init.xavier_uniform_(self.emb_q.weight)
nn.init.xavier_uniform_(self.emb_a.weight)
if self.emb_tool is not None:
nn.init.xavier_uniform_(self.emb_tool.weight)
if self.emb_llm is not None:
nn.init.xavier_uniform_(self.emb_llm.weight)
if self.content_proj is not None:
nn.init.xavier_uniform_(self.content_proj.weight)
nn.init.zeros_(self.content_proj.bias)
def _initial_embeddings(self) -> Tuple[torch.Tensor, torch.Tensor]:
q0 = self.emb_q.weight
a0 = self.emb_a.weight
if not self.use_query_id_emb:
q0 = torch.zeros_like(q0)
if self.use_agent_content and self.content_proj is not None:
a0 = a0 + self.content_proj(self.agent_content)
if self.use_tool_id_emb and self.emb_tool is not None:
mask = self.agent_tool_mask
counts = mask.sum(dim=1).to(torch.long) # (Na,)
valid_idx = mask.bool()
flat_idx = self.agent_tool_indices[valid_idx] # (N_valid,)
if flat_idx.numel() > 0:
offsets = torch.cat(
[
torch.zeros(1, device=counts.device, dtype=torch.long),
counts.cumsum(0)[:-1],
]
)
tool_mean = F.embedding_bag(
flat_idx,
self.emb_tool.weight,
offsets,
mode="mean",
)
else:
tool_mean = torch.zeros_like(a0)
a0 = a0 + tool_mean
if self.use_llm_id_emb and self.emb_llm is not None:
a0 = a0 + self.emb_llm(self.agent_llm_idx)
return q0, a0
def _final_embeddings(self) -> Tuple[torch.Tensor, torch.Tensor]:
raise NotImplementedError
def forward(
self,
q_idx: torch.LongTensor,
pos_idx: torch.LongTensor,
neg_idx: torch.LongTensor,
) -> Tuple[torch.Tensor, torch.Tensor]:
q_emb, a_emb = self._final_embeddings()
qv = q_emb[q_idx.long()]
pos_v = a_emb[pos_idx.long()]
neg_v = a_emb[neg_idx.long()]
pos = (qv * pos_v).sum(dim=-1)
neg = (qv * neg_v).sum(dim=-1)
return pos, neg
# --- RecommenderBase ---
def export_agent_embeddings(self) -> np.ndarray:
with torch.no_grad():
_, a_emb = self._final_embeddings()
return a_emb.detach().cpu().numpy().astype(np.float32)
def export_query_embeddings(self, q_indices: Sequence[int]) -> np.ndarray:
with torch.no_grad():
q_emb, _ = self._final_embeddings()
q_indices = list(q_indices)
return q_emb[q_indices].detach().cpu().numpy().astype(np.float32)
def export_agent_bias(self) -> Optional[np.ndarray]:
return None
def extra_state_dict(self) -> Dict[str, object]:
return {
"embed_dim": self.embed_dim,
"use_query_id_emb": self.use_query_id_emb,
"use_tool_id_emb": self.use_tool_id_emb,
"use_llm_id_emb": self.use_llm_id_emb,
"use_agent_content": self.use_agent_content,
}
class LightGCNRecommender(GraphRecommenderBase):
def __init__(
self,
*args,
num_layers: int = 2,
**kwargs,
) -> None:
self.num_layers = int(num_layers)
super().__init__(*args, **kwargs)
def _propagate(self, all_emb: torch.Tensor) -> torch.Tensor:
embs = [all_emb]
h = all_emb
for _ in range(self.num_layers):
h = torch.sparse.mm(self.norm_adj, h)
embs.append(h)
return torch.stack(embs, dim=0).mean(dim=0)
def _final_embeddings(self) -> Tuple[torch.Tensor, torch.Tensor]:
q0, a0 = self._initial_embeddings()
h0 = torch.cat([q0, a0], dim=0)
h = self._propagate(h0)
return h[: self.num_q], h[self.num_q :]
def extra_state_dict(self) -> Dict[str, object]:
base = super().extra_state_dict()
base.update({"num_layers": self.num_layers})
return base
class NGCFRecommender(GraphRecommenderBase):
def __init__(
self,
*args,
num_layers: int = 2,
dropout: float = 0.1,
act: str = "leakyrelu",
**kwargs,
) -> None:
self.num_layers = int(num_layers)
self.dropout = float(dropout)
self.act = act
super().__init__(*args, **kwargs)
self.W1 = nn.ModuleList([nn.Linear(self.embed_dim, self.embed_dim) for _ in range(self.num_layers)])
self.W2 = nn.ModuleList([nn.Linear(self.embed_dim, self.embed_dim) for _ in range(self.num_layers)])
self.activation = nn.LeakyReLU() if act == "leakyrelu" else nn.ReLU()
for w1, w2 in zip(self.W1, self.W2):
nn.init.xavier_uniform_(w1.weight)
nn.init.zeros_(w1.bias)
nn.init.xavier_uniform_(w2.weight)
nn.init.zeros_(w2.bias)
def _final_embeddings(self) -> Tuple[torch.Tensor, torch.Tensor]:
q0, a0 = self._initial_embeddings()
h = torch.cat([q0, a0], dim=0)
all_layers = [h]
for i in range(self.num_layers):
neigh = torch.sparse.mm(self.norm_adj, h)
bi = torch.sparse.mm(self.norm_adj, h * all_layers[0])
h = self.activation(self.W1[i](neigh) + self.W2[i](bi))
h = F.dropout(h, p=self.dropout, training=self.training)
all_layers.append(h)
out = torch.stack(all_layers, dim=0).mean(dim=0)
return out[: self.num_q], out[self.num_q :]
def extra_state_dict(self) -> Dict[str, object]:
base = super().extra_state_dict()
base.update({"num_layers": self.num_layers, "dropout": self.dropout, "act": self.act})
return base
class KGATRecommender(GraphRecommenderBase):
def __init__(
self,
*args,
num_layers: int = 2,
att_dropout: float = 0.1,
**kwargs,
) -> None:
self.num_layers = int(num_layers)
self.att_dropout = float(att_dropout)
super().__init__(*args, **kwargs)
self.att_weight = nn.Parameter(torch.randn(self.embed_dim))
nn.init.xavier_uniform_(self.att_weight.view(1, -1))
# Precompute edge indices for attention
self.register_buffer("edge_index", self.norm_adj.indices())
def _agg_attention(self, h: torch.Tensor) -> torch.Tensor:
src = self.edge_index[0]
dst = self.edge_index[1]
h_src = h[src]
h_dst = h[dst]
score = (h_src * h_dst).mul(self.att_weight).sum(dim=-1)
att = torch.sigmoid(score)
att = F.dropout(att, p=self.att_dropout, training=self.training)
msg = h_dst * att.unsqueeze(-1)
out = torch.zeros_like(h)
out.index_add_(0, src, msg)
denom = torch.zeros((h.size(0),), device=h.device)
denom.index_add_(0, src, att + 1e-8)
out = out / (denom.unsqueeze(-1) + 1e-8)
return out
def _final_embeddings(self) -> Tuple[torch.Tensor, torch.Tensor]:
q0, a0 = self._initial_embeddings()
h = torch.cat([q0, a0], dim=0)
embs = [h]
for _ in range(self.num_layers):
h = self._agg_attention(h)
embs.append(h)
out = torch.stack(embs, dim=0).mean(dim=0)
return out[: self.num_q], out[self.num_q :]
def extra_state_dict(self) -> Dict[str, object]:
base = super().extra_state_dict()
base.update({"num_layers": self.num_layers, "att_dropout": self.att_dropout})
return base
class SimGCLRecommender(LightGCNRecommender):
def __init__(
self,
*args,
cl_weight: float = 0.1,
perturb_eps: float = 0.1,
temperature: float = 0.2,
**kwargs,
) -> None:
self.cl_weight = float(cl_weight)
self.perturb_eps = float(perturb_eps)
self.temperature = float(temperature)
super().__init__(*args, **kwargs)
def _propagate(self, all_emb: torch.Tensor) -> torch.Tensor:
return super()._propagate(all_emb)
def _cl_view(self, base: torch.Tensor) -> torch.Tensor:
noise = F.normalize(torch.randn_like(base), dim=-1)
return base + self.perturb_eps * noise
def contrastive_loss(self, z1: torch.Tensor, z2: torch.Tensor, idx: torch.Tensor) -> torch.Tensor:
# Only compute logits for the participating indices to keep memory usage bounded.
idx = idx.unique()
z1 = F.normalize(z1[idx], dim=-1)
z2 = F.normalize(z2[idx], dim=-1)
logits = z1 @ z2.T / self.temperature
labels = torch.arange(idx.numel(), device=logits.device)
return F.cross_entropy(logits, labels)
def forward(
self,
q_idx: torch.LongTensor,
pos_idx: torch.LongTensor,
neg_idx: torch.LongTensor,
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
q0, a0 = self._initial_embeddings()
base = torch.cat([q0, a0], dim=0)
h_clean = self._propagate(base)
q_clean, a_clean = h_clean[: self.num_q], h_clean[self.num_q :]
pos = (q_clean[q_idx.long()] * a_clean[pos_idx.long()]).sum(dim=-1)
neg = (q_clean[q_idx.long()] * a_clean[neg_idx.long()]).sum(dim=-1)
if self.cl_weight <= 0:
return pos, neg, torch.tensor(0.0, device=pos.device)
view1 = self._propagate(self._cl_view(base))
view2 = self._propagate(self._cl_view(base))
q_v1, a_v1 = view1[: self.num_q], view1[self.num_q :]
q_v2, a_v2 = view2[: self.num_q], view2[self.num_q :]
cl_q = self.contrastive_loss(q_v1, q_v2, q_idx.to(pos.device))
agent_idx = torch.cat([pos_idx, neg_idx]).to(pos.device)
cl_a = self.contrastive_loss(a_v1, a_v2, agent_idx)
cl_loss = 0.5 * (cl_q + cl_a)
return pos, neg, cl_loss
def export_agent_embeddings(self) -> np.ndarray:
# Override to avoid contrastive noise during export.
with torch.no_grad():
return super().export_agent_embeddings()
def export_query_embeddings(self, q_indices: Sequence[int]) -> np.ndarray:
with torch.no_grad():
return super().export_query_embeddings(q_indices)
def extra_state_dict(self) -> Dict[str, object]:
base = super().extra_state_dict()
base.update(
{
"num_layers": self.num_layers,
"cl_weight": self.cl_weight,
"perturb_eps": self.perturb_eps,
"temperature": self.temperature,
}
)
return base

Xet Storage Details

Size:
15.3 kB
·
Xet hash:
f1d779281c338a1e21c1dd585ec4ee861691a22570edbc4fad425ef2fefc07e1

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.