loss-guided-static-multi / vocab_assignment.py
BorisTM's picture
Add files using upload-large-folder tool
309d3a9 verified
Raw History Blame Contribute Delete
3.25 kB
"""K vocabularies, and a learned assignment of languages to them.
The earlier fusion attempt merged *per token*: two languages could share a row
for one word and keep separate rows for the next. That is cell-level sharing,
not vocabulary sharing, and it is why the readout came out as mush — the merge
decision was never coupled across a language's tokens.
Here the decision lives at the language level. Each language holds one
distribution over K vocabularies, used for every token it emits:
E[l, w] = E_shared[w] + sum_k pi[l, k] * Delta_k[w]
`pi` is L x K — 48 numbers for twelve languages and four vocabularies — so the
grouping is read directly off the parameter instead of being inferred from row
distances. An entropy penalty pushes each language toward one vocabulary, and K
is the explicit axis: K=1 is today's single shared vocabulary, K=L with a hard
assignment is one per language, and where quality stops improving is the answer
to how many a multilingual encoder actually needs.
Offsets start at zero so the model is numerically identical to the shared one at
step zero. The assignment logits carry the small random asymmetry instead: with
identical offsets and a uniform assignment every expert would receive the same
gradient and the K vocabularies would collapse back into one by symmetry.
"""
from __future__ import annotations
import numpy as np
import torch
from torch import nn
class VocabAssignment(nn.Module):
def __init__(self, index_path: str, vocab_size: int, dim: int,
n_languages: int, n_vocabs: int = 4, seed: int = 20260727) -> None:
super().__init__()
data = np.load(index_path)
tokens = np.unique(data["token_index"].astype(np.int64))
self.n_tokens = int(tokens.size)
self.n_vocabs = int(n_vocabs)
slot = torch.full((vocab_size,), -1, dtype=torch.long)
slot[torch.tensor(tokens)] = torch.arange(self.n_tokens)
self.register_buffer("slot", slot, persistent=False)
self.offsets = nn.Parameter(torch.zeros(self.n_vocabs, self.n_tokens, dim))
generator = torch.Generator().manual_seed(seed)
self.logits = nn.Parameter(
torch.randn(n_languages, self.n_vocabs, generator=generator) * 0.01)
def assignment(self) -> torch.Tensor:
return torch.softmax(self.logits, dim=-1)
def entropy(self) -> torch.Tensor:
"""Mean assignment entropy; penalising it drives each language to one slot."""
pi = self.assignment()
return -(pi * torch.log(pi.clamp_min(1e-9))).sum(-1).mean()
def forward(self, token_ids: torch.Tensor, lang_per_token: torch.Tensor,
shared: torch.Tensor) -> torch.Tensor:
index = self.slot[token_ids]
has = index >= 0
if not bool(has.any()):
return shared
safe = index.clamp(min=0)
# (K, N, dim) gathered offsets weighted by each token's language row of pi
gathered = self.offsets[:, safe]
weights = self.assignment()[lang_per_token]
delta = torch.einsum("nk,knd->nd", weights, gathered)
return shared + delta * has.unsqueeze(1)
def extra_repr(self) -> str:
return f"tokens={self.n_tokens}, vocabs={self.n_vocabs}"