josefchen's picture
Add gr.Examples, mode atlas browser, compare-siblings tab
dd6d94a verified
Raw
History Blame
18.1 kB
"""Epicure Explorer: chef-facing operators over the three sibling embeddings.
Six tabs:
- Basket pairings: pick 1+ ingredients, get neighbours and closest modes of the basket centroid.
- Supervised SLERP: rotate a multi-ingredient seed toward one or more supervised poles.
- Emergent SLERP: rotate a seed toward one or more emergent factor-mode poles.
- Arithmetic: Mikolov-style 'centroid(positives) - centroid(negatives)' nearest neighbours.
- Mode atlas: browse all GMM modes per sibling with kind filter and label search.
- Compare siblings: run the same query across cooc/core/chem in three columns.
All three siblings (Cooc, Core, Chem) load on startup from public HF model repos.
Paper: https://arxiv.org/abs/2605.22391
"""
from __future__ import annotations
import os
import sys
import numpy as np
import gradio as gr
try:
from epicure import Epicure
except ImportError:
from huggingface_hub import hf_hub_download
epicure_py = hf_hub_download("Kaikaku/epicure-cooc", "epicure.py")
sys.path.insert(0, os.path.dirname(epicure_py))
from epicure import Epicure
MODELS = {
"cooc": Epicure.from_pretrained("Kaikaku/epicure-cooc"),
"core": Epicure.from_pretrained("Kaikaku/epicure-core"),
"chem": Epicure.from_pretrained("Kaikaku/epicure-chem"),
}
ALL_INGREDIENTS = sorted(MODELS["cooc"].vocab.keys())
# ===== math helpers =====
def _unit(v: np.ndarray, eps: float = 1e-9) -> np.ndarray:
n = np.linalg.norm(v); return v / max(n, eps)
def _basket_centroid(m: Epicure, names: list[str]) -> np.ndarray | None:
valid = [n for n in (names or []) if n in m.vocab]
if not valid:
return None
idxs = [m.vocab[n] for n in valid]
return _unit(m.E[idxs].mean(axis=0))
def _stack_directions(m: Epicure, keys: list[str], use_factor_pole: bool = False) -> np.ndarray | None:
poles = []
for k in keys or []:
if use_factor_pole:
for mode in m.modes:
if mode.mode_id == k:
poles.append(_unit(mode.pole)); break
else:
if k in m.supervised_poles:
poles.append(_unit(m.supervised_poles[k]))
if not poles:
return None
return _unit(np.stack(poles, axis=0).sum(axis=0))
def _topk(m: Epicure, q: np.ndarray, k: int, exclude: list[str]) -> list[tuple[str, float]]:
sims = m.E @ q
for name in exclude or []:
if name in m.vocab:
sims[m.vocab[name]] = -np.inf
order = np.argsort(-sims)
return [(m.itos[int(i)], float(sims[i])) for i in order[:k]]
def _supervised_choices(sibling: str) -> list[str]:
return sorted(MODELS[sibling].supervised_poles.keys())
def _factor_mode_choices(sibling: str) -> list[tuple[str, str]]:
return [(f"{m.label} ({m.mode_id})", m.mode_id) for m in MODELS[sibling].modes if m.kind == "factor"]
def _slerp(m: Epicure, v: np.ndarray, d: np.ndarray, theta_deg: float) -> np.ndarray:
d_perp = d - (d @ v) * v
n_perp = np.linalg.norm(d_perp)
if n_perp < 1e-9:
return v
d_perp = d_perp / n_perp
th = np.deg2rad(float(theta_deg))
return _unit(np.cos(th) * v + np.sin(th) * d_perp)
# ===== tab handlers =====
def basket_pairings(sibling: str, basket: list[str], k: int):
m = MODELS[sibling]
centroid = _basket_centroid(m, basket)
if centroid is None:
return [], []
nb = _topk(m, centroid, k=k, exclude=basket or [])
scored = [(mode.mode_id, mode.label, mode.kind, float(_unit(mode.pole) @ centroid)) for mode in m.modes]
scored.sort(key=lambda x: -x[3])
return (
[[name, f"{sim:.4f}"] for name, sim in nb],
[[mid, label, kind, f"{sim:.4f}"] for mid, label, kind, sim in scored[:k]],
)
def supervised_slerp_multi(sibling: str, basket: list[str], directions: list[str], theta: float, k: int):
m = MODELS[sibling]
v = _basket_centroid(m, basket)
d = _stack_directions(m, directions, use_factor_pole=False)
if v is None:
return []
if d is None:
return [[n, f"{s:.4f}"] for n, s in _topk(m, v, k, basket)]
q = _slerp(m, v, d, theta)
return [[name, f"{sim:.4f}"] for name, sim in _topk(m, q, k, basket)]
def emergent_slerp_multi(sibling: str, basket: list[str], mode_labels: list[str], theta: float, k: int):
m = MODELS[sibling]
label_to_id = {f"{mode.label} ({mode.mode_id})": mode.mode_id for mode in m.modes if mode.kind == "factor"}
mode_ids = [label_to_id[lab] for lab in (mode_labels or []) if lab in label_to_id]
v = _basket_centroid(m, basket)
d = _stack_directions(m, mode_ids, use_factor_pole=True)
if v is None:
return []
if d is None:
return [[n, f"{s:.4f}"] for n, s in _topk(m, v, k, basket)]
q = _slerp(m, v, d, theta)
return [[name, f"{sim:.4f}"] for name, sim in _topk(m, q, k, basket)]
def arithmetic(sibling: str, positives: list[str], negatives: list[str], k: int):
m = MODELS[sibling]
pos = _basket_centroid(m, positives)
if pos is None:
return []
neg = _basket_centroid(m, negatives) if negatives else None
q = _unit(pos - neg) if neg is not None else pos
return [[name, f"{sim:.4f}"] for name, sim in _topk(m, q, k, (positives or []) + (negatives or []))]
def browse_modes(sibling: str, kind_filter: str, query: str):
m = MODELS[sibling]
rows = []
q = (query or "").strip().lower()
for mode in m.modes:
if kind_filter != "all" and mode.kind != kind_filter:
continue
if q and q not in mode.label.lower() and q not in mode.property.lower():
continue
rows.append([
mode.mode_id,
mode.kind,
mode.property,
mode.label,
mode.n_members,
", ".join(mode.members[:12]),
])
rows.sort(key=lambda r: (r[1], -r[4]))
return rows
def compare_siblings(basket: list[str], directions: list[str], theta: float, k: int):
out = []
for sib in ["cooc", "core", "chem"]:
m = MODELS[sib]
v = _basket_centroid(m, basket)
if v is None:
out.append([]); continue
# Direction set can use any pole key; we intersect with this sibling's supervised_poles
valid_dirs = [d for d in (directions or []) if d in m.supervised_poles]
if valid_dirs:
d_vec = _stack_directions(m, valid_dirs)
q = _slerp(m, v, d_vec, theta) if d_vec is not None else v
else:
q = v
hits = _topk(m, q, k=k, exclude=basket)
out.append([[name, f"{sim:.4f}"] for name, sim in hits])
return out[0], out[1], out[2]
# ===== UI =====
with gr.Blocks(title="Epicure Explorer") as demo:
gr.Markdown(
"""# Epicure Explorer
Chef-facing operators over the three Epicure sibling embeddings (Cooc, Core, Chem),
from [arXiv:2605.22391](https://arxiv.org/abs/2605.22391).
- **Cooc** walks recipe co-occurrence only. Neighbours are recipe companions.
- **Core** blends typed FlavorDB compound walks with injected I-I walks. Concentrated geometry, tightest modes.
- **Chem** walks typed FlavorDB compound metapaths only. Strongest supervised-direction recovery; neighbours are flavour-profile peers.
Pick a sibling, then explore. Each tab has a few worked examples just below the form: click any row to populate the inputs.
"""
)
sibling = gr.Radio(choices=["cooc", "core", "chem"], value="chem", label="Sibling embedding")
# ---------- Tab 1: Basket pairings ----------
with gr.Tab("Basket pairings"):
gr.Markdown(
"Pick one or more ingredients. The tool averages their unit vectors and returns nearest "
"neighbours plus closest modes of that centroid. Useful for 'what should I add to what I have'."
)
basket = gr.Dropdown(
choices=ALL_INGREDIENTS, value=["chicken","lemon","garlic"],
label="Ingredient basket (pick 1+)", multiselect=True, max_choices=10,
)
k_pair = gr.Slider(1, 15, value=8, step=1, label="K")
pair_btn = gr.Button("Find pairings", variant="primary")
with gr.Row():
nb_table = gr.Dataframe(headers=["Neighbour","Cosine"], label="Top-K nearest neighbours", interactive=False)
mode_table = gr.Dataframe(headers=["Mode id","Label","Kind","Cosine"], label="Closest modes", interactive=False)
pair_btn.click(basket_pairings, inputs=[sibling, basket, k_pair], outputs=[nb_table, mode_table])
gr.Examples(
examples=[
["chem", ["chicken","lemon","garlic"], 8],
["core", ["miso","ginger","sesame_oil"], 8],
["chem", ["tomato","basil","mozzarella_cheese"], 8],
["cooc", ["chocolate","strawberry","cream"], 8],
["chem", ["cumin","coriander","turmeric"], 8],
["core", ["soy_sauce","ginger","scallion"], 8],
["chem", ["red_wine","beef","rosemary"], 8],
["core", ["coconut_milk","lemongrass","fish_sauce"], 8],
],
inputs=[sibling, basket, k_pair],
label="Try one of these baskets",
)
# ---------- Tab 2: Supervised SLERP ----------
with gr.Tab("Supervised SLERP"):
gr.Markdown(
"Rotate the (possibly multi-ingredient) seed toward one or more supervised direction poles. "
"Multiple directions are summed and L2-normalised before rotation, matching the paper's "
"multi-constraint queries (e.g. 'chicken + processed + Western_Atlantic')."
)
sup_basket = gr.Dropdown(
choices=ALL_INGREDIENTS, value=["rice"],
label="Seed basket (pick 1+)", multiselect=True, max_choices=10,
)
sup_dirs = gr.Dropdown(
choices=_supervised_choices("chem"), value=["cuisine:South_Asian"],
label="Supervised directions (pick 1+; summed before rotation)",
multiselect=True, max_choices=5,
)
sup_theta = gr.Slider(0, 90, value=30, step=5, label="Rotation angle (deg)")
sup_k = gr.Slider(1, 15, value=8, step=1, label="K")
sup_btn = gr.Button("Rotate", variant="primary")
sup_table = gr.Dataframe(headers=["Ingredient","Cosine"], label="Top-K rotated-query neighbours")
sup_btn.click(supervised_slerp_multi, inputs=[sibling, sup_basket, sup_dirs, sup_theta, sup_k], outputs=sup_table)
sibling.change(
lambda s: gr.Dropdown(choices=_supervised_choices(s), value=[]),
inputs=sibling, outputs=sup_dirs,
)
gr.Examples(
examples=[
["chem", ["rice"], ["cuisine:South_Asian"], 30, 8],
["chem", ["corn"], ["cuisine:Latin_American"], 30, 8],
["core", ["chicken"], ["cuisine:Mediterranean"], 45, 8],
["core", ["tomato","basil"], ["cuisine:Southeast_Asian"], 45, 8],
["chem", ["beef"], ["cuisine:East_Asian"], 60, 8],
["cooc", ["chocolate"], ["cuisine:Latin_American"], 30, 8],
],
inputs=[sibling, sup_basket, sup_dirs, sup_theta, sup_k],
label="Try one of these rotations",
)
# ---------- Tab 3: Emergent SLERP ----------
with gr.Tab("Emergent SLERP"):
gr.Markdown(
"Rotate the seed basket toward one or more emergent factor-mode poles discovered "
"by multi-seed-stable FastICA + GMM. Stack mode targets to combine culinary axes."
)
em_basket = gr.Dropdown(
choices=ALL_INGREDIENTS, value=["chocolate"],
label="Seed basket (pick 1+)", multiselect=True, max_choices=10,
)
factor_opts = _factor_mode_choices("chem")
em_modes = gr.Dropdown(
choices=[label for label, _ in factor_opts],
value=[factor_opts[0][0]] if factor_opts else [],
label="Factor modes (pick 1+; summed before rotation)",
multiselect=True, max_choices=5,
)
em_theta = gr.Slider(0, 90, value=30, step=5, label="Rotation angle (deg)")
em_k = gr.Slider(1, 15, value=8, step=1, label="K")
em_btn = gr.Button("Rotate", variant="primary")
em_table = gr.Dataframe(headers=["Ingredient","Cosine"], label="Top-K rotated-query neighbours")
em_btn.click(emergent_slerp_multi, inputs=[sibling, em_basket, em_modes, em_theta, em_k], outputs=em_table)
sibling.change(
lambda s: gr.Dropdown(choices=[label for label, _ in _factor_mode_choices(s)], value=[]),
inputs=sibling, outputs=em_modes,
)
# ---------- Tab 4: Arithmetic ----------
with gr.Tab("Arithmetic"):
gr.Markdown(
"Classic Mikolov-style vector arithmetic: `centroid(positives) - centroid(negatives)`, "
"then top-K nearest neighbours. The killer demo is `miso - salt` on Core (returns the "
"Japanese fermented-umami pantry minus the salty component): mirin, kombu, wakame, sake, dashi."
)
pos_box = gr.Dropdown(
choices=ALL_INGREDIENTS, value=["miso"],
label="Positives (added)", multiselect=True, max_choices=10,
)
neg_box = gr.Dropdown(
choices=ALL_INGREDIENTS, value=["salt"],
label="Negatives (subtracted)", multiselect=True, max_choices=10,
)
ar_k = gr.Slider(1, 15, value=8, step=1, label="K")
ar_btn = gr.Button("Compute", variant="primary")
ar_table = gr.Dataframe(headers=["Ingredient","Cosine"], label="Top-K nearest to result vector")
ar_btn.click(arithmetic, inputs=[sibling, pos_box, neg_box, ar_k], outputs=ar_table)
gr.Examples(
examples=[
["core", ["miso"], ["salt"], 8],
["core", ["chicken","tofu"], ["beef"], 8],
["cooc", ["basil","cumin"], ["parsley"], 8],
["chem", ["chocolate"], ["sugar"], 8],
["chem", ["wine"], ["beer"], 8],
["core", ["bread"], ["flour"], 8],
["core", ["coffee"], ["milk"], 8],
["chem", ["mozzarella_cheese"], ["milk"], 8],
],
inputs=[sibling, pos_box, neg_box, ar_k],
label="Try one of these arithmetic queries",
)
# ---------- Tab 5: Mode atlas browser ----------
with gr.Tab("Mode atlas"):
gr.Markdown(
"Browse the GMM mode atlas of the selected sibling. Cooc has 150 modes across 41 properties; "
"Core 193 / 44; Chem 200 / 43. `factor` modes are the emergent FastICA factor poles; "
"`continuous` modes are quartile partitions of NOVA / sensory / USDA scores; "
"`binary` modes are food-group buckets. Search by label or property substring."
)
atlas_kind = gr.Radio(
choices=["all","factor","continuous","binary"], value="all", label="Mode kind"
)
atlas_search = gr.Textbox(
label="Search labels / properties", placeholder="e.g. South Asian, baking, fiber",
value="",
)
atlas_btn = gr.Button("Browse modes", variant="primary")
atlas_table = gr.Dataframe(
headers=["mode_id","kind","property","label","n_members","top members"],
label="Modes (sorted by kind, then size descending)",
wrap=True, interactive=False,
)
atlas_btn.click(browse_modes, inputs=[sibling, atlas_kind, atlas_search], outputs=atlas_table)
# ---------- Tab 6: Compare siblings ----------
with gr.Tab("Compare siblings"):
gr.Markdown(
"Run the same query across all three siblings in one shot. This is the spectrum-of-models "
"view the paper is built around: Cooc shows recipe companions, Chem shows chemistry peers, "
"Core sits in between. Leave the direction empty for pure basket pairings."
)
cmp_basket = gr.Dropdown(
choices=ALL_INGREDIENTS, value=["chicken"],
label="Seed basket (pick 1+)", multiselect=True, max_choices=10,
)
cmp_dirs = gr.Dropdown(
choices=_supervised_choices("chem"), value=[],
label="Optional: supervised directions (leave empty for pure pairings)",
multiselect=True, max_choices=5,
)
cmp_theta = gr.Slider(0, 90, value=30, step=5, label="Rotation angle (deg; ignored if no directions)")
cmp_k = gr.Slider(1, 15, value=8, step=1, label="K")
cmp_btn = gr.Button("Compare across siblings", variant="primary")
with gr.Row():
cmp_cooc = gr.Dataframe(headers=["Cooc neighbour","Cosine"], label="Cooc (recipe-context)")
cmp_core = gr.Dataframe(headers=["Core neighbour","Cosine"], label="Core (blended)")
cmp_chem = gr.Dataframe(headers=["Chem neighbour","Cosine"], label="Chem (chemistry)")
cmp_btn.click(
compare_siblings,
inputs=[cmp_basket, cmp_dirs, cmp_theta, cmp_k],
outputs=[cmp_cooc, cmp_core, cmp_chem],
)
gr.Examples(
examples=[
[["chicken"], [], 0, 8],
[["basil"], [], 0, 8],
[["miso"], [], 0, 8],
[["rice"], ["cuisine:South_Asian"], 30, 8],
[["corn"], ["cuisine:Latin_American"], 30, 8],
[["chicken","onion"], ["cuisine:Mediterranean"], 45, 8],
],
inputs=[cmp_basket, cmp_dirs, cmp_theta, cmp_k],
label="Try one of these side-by-side comparisons",
)
gr.Markdown(
"""---
**Cite:** Radzikowski and Chen, 2026, *Epicure: Navigating the Emergent Geometry of Food Ingredient Embeddings*, [arXiv:2605.22391](https://arxiv.org/abs/2605.22391).
Models: [epicure-cooc](https://huggingface.co/Kaikaku/epicure-cooc) | [epicure-core](https://huggingface.co/Kaikaku/epicure-core) | [epicure-chem](https://huggingface.co/Kaikaku/epicure-chem). Dataset: [epicure-corpus-resources](https://huggingface.co/datasets/Kaikaku/epicure-corpus-resources).
"""
)
if __name__ == "__main__":
demo.launch()