Feature Extraction
Transformers
Safetensors
muse_glimmer
metaencoder
multimodal-embedding
image-text-retrieval
video-retrieval
document-retrieval
Instructions to use facebook/meta-encoder-v1 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use facebook/meta-encoder-v1 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="facebook/meta-encoder-v1")# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModel processor = AutoProcessor.from_pretrained("facebook/meta-encoder-v1") model = AutoModel.from_pretrained("facebook/meta-encoder-v1", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 19,882 Bytes
d5de844 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | """MetaEncoder v1: encode inputs, retrieve from a corpus, and decide Jev-style questions with one embedding model.
from modeling_metaencoder import MetaEncoder
model = MetaEncoder.from_pretrained("<this repo>")
# 1. encode: one L2-normalised embedding per item
emb = model.encode(["a red bicycle", {"image": img}], instruction="Represent the given image.")
# 2. retrieve: rank a corpus (items, or embeddings from `encode`) against a query
hits = model.retrieve("Which photo shows a bicycle?", corpus, top_k=5)
# 3. decide: Jev-style questions about a state
model.decide({"query": "used iphone 15"},
{"shopping": {"type": "noul", "instructions": "Is the user shopping for a product?"}})
# -> {"questions": {"shopping": {"value": True, "probabilities": {"false": ..., "true": ...}, ...}}}
Built only on the public `transformers` Muse Glimmer classes (`AutoModel` -> `MuseGlimmerModel`, `AutoProcessor`
-> `MuseGlimmerProcessor`). The embedding is the final-norm hidden state of the input's closing `<|eot|>` token,
projected by `embedding_head.safetensors` and L2-normalised.
"""
from __future__ import annotations
import json
import math
import os
import warnings
from collections import defaultdict
from dataclasses import dataclass
from typing import Any, Mapping, Sequence, Union
import torch
import torch.nn.functional as F
from huggingface_hub import hf_hub_download
from PIL import Image
from safetensors.torch import load_file
from transformers import AutoModel, AutoProcessor
MAX_TRAINED_LENGTH = 16384
DEFAULT_BATCH_SIZE = 8
MAX_IMAGE_TOKENS = 4096
VIDEO_FRAMES = 8
TASK_INSTRUCTION = "Read the task definition and select the option it calls for."
SCENARIO_INSTRUCTION = "Answer the question about the scenario by selecting the correct option."
TRAINED_TEMPERATURE = 0.02
# Options of at most this many words are labels whose meaning comes from the task, so their candidates carry the
# task as their instruction (training kept the task only up to MAX_TASK_CHARS).
LABEL_MAX_WORDS = 5
MAX_TASK_CHARS = 600
MAX_OPTIONS = 26
# Trained option order of yes/no ("noul") questions and the Jev answer key of each.
NOUL_OPTIONS = (("No", "false"), ("Yes", "true"))
_MAX_CACHED_CANDIDATES = 4096
Item = Union[str, Image.Image, Mapping[str, Any]]
_ITEM_KEYS = {"text", "instruction", "image", "images", "video"}
@dataclass
class Hit:
"""One retrieved corpus entry."""
index: int # position in the corpus passed in
score: float # cosine similarity to the query, in [-1, 1]
item: Any # the corpus item itself, or None if embeddings were passed
def __repr__(self) -> str:
return f"Hit(index={self.index}, score={self.score:.4f})"
@dataclass(frozen=True)
class Question:
"""A parsed Jev question: what the encoder sees (options) and what the answer reports (keys)."""
name: str
type: str # "choice", "noul" or "score"
instructions: str
options: tuple[str, ...] # option texts in prompt order
keys: tuple[str, ...] # Jev answer key per option: criteria key, "false"/"true", or "0".."n-1"
descriptions: tuple[str, ...] # per option, "" if none
def _sample_frames(frames: Sequence[Any]) -> list[Any]:
if len(frames) <= VIDEO_FRAMES:
return list(frames)
return [frames[round(i * (len(frames) - 1) / (VIDEO_FRAMES - 1))] for i in range(VIDEO_FRAMES)]
def _as_items(inputs: Item | Sequence[Item], instruction: str | None) -> list[dict]:
"""Normalise any accepted input shape into one dict per embedding."""
if isinstance(inputs, (str, Image.Image, Mapping)):
inputs = [inputs]
items = []
for i, raw in enumerate(inputs):
if isinstance(raw, str):
raw = {"text": raw}
elif isinstance(raw, Image.Image):
raw = {"image": raw}
elif not isinstance(raw, Mapping):
raise TypeError(f"item {i} is {type(raw).__name__}; expected str, PIL.Image or a dict")
unknown = set(raw) - _ITEM_KEYS
if unknown:
raise ValueError(f"item {i} has unknown key(s) {sorted(unknown)}; allowed: {sorted(_ITEM_KEYS)}")
images = []
for key in ("image", "images", "video"):
value = raw.get(key)
if value is None:
continue
value = list(value) if isinstance(value, (list, tuple)) else [value]
images.extend(_sample_frames(value) if key == "video" else value)
items.append({
"text": raw.get("text") or "",
"instruction": raw.get("instruction", instruction) or "",
"images": [im.convert("RGB") for im in images if im is not None],
})
return items
def parse_question(name: str, spec: Mapping[str, Any]) -> Question:
"""Validate one Jev question `{"type", "instructions", "criteria"}`.
choice: `criteria` maps each option key to its description (may be empty). noul: optional
`{"true": ..., "false": ...}` descriptions. score: `criteria` is the ordered list of level labels.
"""
if not isinstance(spec, Mapping) or set(spec) - {"type", "instructions", "criteria"}:
raise ValueError(f"{name}: a question is {{'type', 'instructions', 'criteria'}}")
kind, criteria = spec.get("type"), spec.get("criteria")
instructions = " ".join(str(spec.get("instructions") or "").split())
if not instructions:
raise ValueError(f"{name}: instructions must be non-empty")
if kind == "noul":
criteria = criteria or {}
if not isinstance(criteria, Mapping) or set(criteria) - {"true", "false"}:
raise ValueError(f"{name}: noul criteria may only describe 'true' and 'false'")
options, keys = zip(*NOUL_OPTIONS)
descriptions = [criteria.get(k) for k in keys]
elif kind == "choice":
if not isinstance(criteria, Mapping):
raise ValueError(f"{name}: choice criteria must map option keys to descriptions")
options = keys = tuple(str(k).strip() for k in criteria)
descriptions = list(criteria.values())
elif kind == "score":
if isinstance(criteria, (str, Mapping)) or not isinstance(criteria, Sequence):
raise ValueError(f"{name}: score criteria must be the ordered list of level labels")
options = tuple(str(c).strip() for c in criteria)
keys = tuple(str(i) for i in range(len(options)))
descriptions = [""] * len(options)
else:
raise ValueError(f"{name}: type must be choice, noul or score, got {kind!r}")
if not 2 <= len(options) <= MAX_OPTIONS or len(set(options)) != len(options) or not all(options):
raise ValueError(f"{name}: needs 2 to {MAX_OPTIONS} distinct non-empty options, got {list(options)}")
descriptions = tuple("" if d is None or str(d).strip() == o else str(d).strip()
for o, d in zip(options, descriptions))
return Question(name, kind, instructions, tuple(options), tuple(keys), descriptions)
def render_state(state: Any) -> tuple[str, str]:
"""(state text, query instruction): text as is under the typed-decision instruction, JSON as a scenario."""
if state is None or isinstance(state, str):
return (state or "").strip(), TASK_INSTRUCTION
return json.dumps(state, ensure_ascii=False), SCENARIO_INSTRUCTION
def render_options(options: Sequence[str]) -> str:
"""The option list of a decision query: comma-joined labels, or one "- option" line each when sentence-like."""
if any(len(o) > 40 or any(c in o for c in ",.;\n") for o in options):
return "Options:\n" + "\n".join(f"- {o}" for o in options)
return "Options: " + ", ".join(options)
def decision_prompts(state_text: str, question: str, options: Sequence[str], descriptions: Sequence[str],
instruction: str) -> tuple[dict, list[dict]]:
"""The query and one candidate per option of a decision, laid out as the training rows (images excluded)."""
head = f"{state_text}\n\n" if state_text else ""
query = {"text": f"{head}Task: {question.strip()}\n{render_options(options)}", "instruction": instruction}
task = " ".join(question.split())
label_style = all(len(o.split()) <= LABEL_MAX_WORDS for o in options)
cand_instruction = f"Option for the task: {task}" if label_style and len(task) <= MAX_TASK_CHARS else ""
candidates = [{"text": f"{o}\n{d}" if d else o, "instruction": cand_instruction, "images": []}
for o, d in zip(options, descriptions)]
return query, candidates
def answer(question: Question, scores: torch.Tensor, temperature: float) -> dict[str, Any]:
"""The Jev answer of one question from its option cosine scores."""
probs = torch.softmax(scores.double() / temperature, dim=-1)
best = int(probs.argmax())
entropy = -float((probs * probs.clamp_min(1e-300).log()).sum())
if question.type == "score":
value: Any = float(sum(i * p for i, p in enumerate(probs.tolist())))
elif question.type == "noul":
value = question.keys[best] == "true"
else:
value = question.keys[best]
return {"value": value,
"probabilities": dict(zip(question.keys, probs.tolist())),
"confidence": min(1.0, max(0.0, 1.0 - entropy / math.log(len(question.keys)))),
"scores": dict(zip(question.keys, scores.tolist()))}
def fit_temperature(rows: Sequence[tuple[Sequence[float], int]]) -> float:
"""The softmax temperature that minimises the NLL of (option scores, correct index) rows."""
grid = torch.logspace(math.log10(1e-3), 0.0, 301, dtype=torch.float64)
groups: dict[int, list[tuple[Sequence[float], int]]] = defaultdict(list)
for scores, label in rows:
groups[len(scores)].append((scores, label))
nll = torch.zeros_like(grid)
for group in groups.values():
scores = torch.tensor([s for s, _ in group], dtype=torch.float64)
labels = torch.tensor([y for _, y in group])
nll += torch.stack([F.cross_entropy(scores / t, labels, reduction="sum") for t in grid])
return float(grid[int(nll.argmin())])
class MetaEncoder:
"""Embedding model over text, images and video (as a list of frames).
Usage:
model = MetaEncoder.from_pretrained("<repo>")
corpus = model.encode(docs, instruction="Represent the document for retrieval.")
model.retrieve(query, corpus, top_k=5)
model.decide({"post": text}, {"sports": {"type": "noul", "instructions": "Is the post about sports?"}})
"""
def __init__(self, model: torch.nn.Module, processor: Any, head: torch.Tensor):
self.model = model
self.processor = processor
self.head = head
# Softmax temperature of `decide`; `calibrate` refits it on labelled answers.
self.temperature = TRAINED_TEMPERATURE
# Option candidates depend only on the question, so repeated questions reuse their embeddings.
self._candidate_cache: dict[tuple[str, str], torch.Tensor] = {}
# Last-token pooling reads the final column, which is the closing <|eot|> only under left padding.
self.processor.tokenizer.padding_side = "left"
@classmethod
def from_pretrained(cls, path: str, device_map: str | dict = "auto", dtype: torch.dtype = torch.bfloat16,
attn_implementation: str = "sdpa") -> "MetaEncoder":
model = AutoModel.from_pretrained(path, dtype=dtype, device_map=device_map,
attn_implementation=attn_implementation)
model.eval()
head_file = os.path.join(path, "embedding_head.safetensors")
if not os.path.exists(head_file):
head_file = hf_hub_download(path, "embedding_head.safetensors")
head = load_file(head_file)["embedding_head.weight"].to(model.device, dtype)
return cls(model, AutoProcessor.from_pretrained(path), head)
@property
def embedding_dim(self) -> int:
return self.head.shape[0]
def encode(self, inputs: Item | Sequence[Item], instruction: str | None = None,
batch_size: int = DEFAULT_BATCH_SIZE) -> torch.Tensor:
"""Embed items. Returns L2-normalised float32 `(n_items, embedding_dim)`.
An item is a string, a PIL image, or a dict `{"text", "image", "images", "video", "instruction"}` (all
optional). `instruction` describes what the embedding is for (e.g. "Find a caption for the news in the given
photo.") and applies to items that carry none of their own.
"""
return self._encode_items(_as_items(inputs, instruction), batch_size)
def retrieve(self, query: Item, corpus: Sequence[Item] | torch.Tensor, top_k: int | None = None,
query_instruction: str | None = None, corpus_instruction: str | None = None,
batch_size: int = DEFAULT_BATCH_SIZE) -> list[Hit]:
"""Rank `corpus` against `query`, best first.
`corpus` is a list of items, or their embeddings from `encode` (then `corpus_instruction` is unused). Encode
a large corpus once and pass the tensor to search it repeatedly.
"""
q = self.encode(query, instruction=query_instruction, batch_size=1)
is_tensor = isinstance(corpus, torch.Tensor)
c = corpus if is_tensor else self.encode(corpus, instruction=corpus_instruction, batch_size=batch_size)
scores = (q @ c.to(q.device, q.dtype).T)[0]
order = torch.argsort(scores, descending=True)[:top_k]
originals = None if is_tensor else list(corpus)
return [Hit(int(i), float(scores[i]), None if originals is None else originals[int(i)]) for i in order]
def decide(self, state: Any, questions: Mapping[str, Mapping[str, Any]], images: Any = None,
video: Sequence[Image.Image] | None = None) -> dict[str, Any]:
"""Answer Jev questions about `state`.
Args:
state: what the questions are about: a string, or any JSON value (rendered as a JSON scenario).
questions: `{name: {"type": "choice" | "noul" | "score", "instructions": ..., "criteria": ...}}`.
images: a PIL image or a list of them that belong to the state.
video: the state's video as a list of frames.
Returns:
`{"questions": {name: {"value", "probabilities", "confidence", "scores"}}}`. `value` is the chosen
criteria key (choice), a bool (noul) or the expected level index (score); `probabilities` maps each
answer key to softmax(score / temperature); `confidence` is 1 - normalised entropy; `scores` are the
raw cosine similarities.
"""
return self.decide_batch([state], questions, [images], [video])[0]
def decide_batch(self, states: Sequence[Any], questions: Mapping[str, Mapping[str, Any]],
images: Sequence[Any] | None = None, video: Sequence[Sequence[Image.Image] | None] | None = None,
batch_size: int = DEFAULT_BATCH_SIZE) -> list[dict[str, Any]]:
"""`decide` for many states that share the same questions; option embeddings are computed once."""
parsed = [parse_question(name, spec) for name, spec in questions.items()]
if not parsed:
raise ValueError("questions must name at least one question")
n = len(states)
images, video = images or [None] * n, video or [None] * n
if not len(images) == len(video) == n:
raise ValueError("pass one images and one video entry per state")
rendered = [render_state(s) for s in states]
media = [_as_items({"images": im, "video": v}, None)[0]["images"] for im, v in zip(images, video)]
queries, cands = [], []
for q in parsed:
for (text, instruction), state_images in zip(rendered, media):
query, cands_q = decision_prompts(text, q.instructions, q.options, q.descriptions, instruction)
queries.append({**query, "images": state_images})
cands.append(cands_q)
query_embs = self._encode_items(queries, batch_size).view(len(parsed), n, -1)
results: list[dict[str, Any]] = [{"questions": {}} for _ in states]
for q, embs, cands_q in zip(parsed, query_embs, cands):
scores = embs @ self._encode_candidates(cands_q, batch_size).T
for result, s in zip(results, scores):
result["questions"][q.name] = answer(q, s, self.temperature)
return results
def calibrate(self, answers: Sequence[Mapping[str, Any]], labels: Sequence[Any]) -> float:
"""Fit `self.temperature` to labelled answers and return it; later `decide` calls use it.
`answers` are per-question answers from `decide` (`result["questions"][name]`); `labels` are their correct
answer keys (a criteria key, True/False for noul, or a level index for score).
"""
if len(answers) != len(labels) or not answers:
raise ValueError("need one label per answer, and at least one answer")
rows = []
for a, label in zip(answers, labels):
key = ("true" if label else "false") if isinstance(label, bool) else str(label)
rows.append((list(a["scores"].values()), list(a["scores"]).index(key)))
self.temperature = fit_temperature(rows)
return self.temperature
@staticmethod
def similarity(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
"""Cosine similarity matrix `(n_a, n_b)` of unit-norm embeddings."""
return a @ b.T
def _encode_items(self, items: list[dict], batch_size: int) -> torch.Tensor:
if not items:
return torch.empty(0, self.embedding_dim, device=self.model.device)
return torch.cat([self._encode_batch(items[i:i + batch_size]) for i in range(0, len(items), batch_size)])
def _encode_candidates(self, items: list[dict], batch_size: int) -> torch.Tensor:
"""Encode text-only option candidates through the cache."""
cache = self._candidate_cache
keys = [(it["instruction"], it["text"]) for it in items]
missing = list(dict.fromkeys(k for k in keys if k not in cache))
if missing:
if len(cache) + len(missing) > _MAX_CACHED_CANDIDATES:
cache.clear()
embs = self._encode_items([{"instruction": i, "text": t, "images": []} for i, t in missing], batch_size)
cache.update(zip(missing, embs))
return torch.stack([cache[k] for k in keys])
@torch.no_grad()
def _encode_batch(self, items: list[dict]) -> torch.Tensor:
texts, images = [], []
for it in items:
content = [{"type": "image"} for _ in it["images"]]
if it["text"]:
content.append({"type": "text", "text": it["text"]})
messages = [{"role": "system", "content": it["instruction"]}] if it["instruction"] else []
messages.append({"role": "user", "content": content})
texts.append(self.processor.apply_chat_template(messages, tokenize=False))
images.extend(it["images"])
inputs = self.processor(text=texts, images=images or None, padding=True, truncation=False,
add_special_tokens=False, max_image_tokens=MAX_IMAGE_TOKENS, return_tensors="pt")
inputs = {k: v.to(self.model.device) if hasattr(v, "to") else v for k, v in inputs.items()}
if not bool((inputs["attention_mask"][:, -1] == 1).all()):
raise RuntimeError("expected left padding")
longest = int(inputs["attention_mask"].sum(-1).max())
if longest > MAX_TRAINED_LENGTH:
warnings.warn(f"an input has {longest} tokens, longer than the {MAX_TRAINED_LENGTH} seen in training")
hidden = self.model(**inputs, use_cache=False).last_hidden_state[:, -1]
return F.normalize((hidden @ self.head.T).float(), dim=-1)
|