multimodalart HF Staff commited on
Commit
0cfebfa
·
verified ·
1 Parent(s): f3cf3aa

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -1,13 +1,31 @@
1
  ---
2
- title: Uncha Hyperbolic Vlm
3
- emoji: 📊
4
- colorFrom: gray
5
- colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: UNCHA Hyperbolic VLM
3
+ emoji: 🔮
4
+ colorFrom: pink
5
+ colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.20.0
 
8
  app_file: app.py
9
+ short_description: Hyperbolic vision-language zero-shot classification
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 30m
12
  ---
13
 
14
+ # UNCHA: Uncertainty-guided Compositional Hyperbolic Alignment
15
+
16
+ This Space demonstrates **zero-shot image classification** using the UNCHA model,
17
+ a hyperbolic vision-language model that embeds images and text in Lorentz (hyperbolic)
18
+ space to capture hierarchical part-whole relationships.
19
+
20
+ ## How it works
21
+
22
+ 1. Upload an image (or use an example)
23
+ 2. Provide a comma-separated list of candidate labels
24
+ 3. The model encodes the image and each label's text prompts into hyperbolic space
25
+ 4. Classification scores are computed via the Lorentzian inner product
26
+
27
+ ## References
28
+
29
+ - [Paper (arXiv)](https://arxiv.org/abs/2603.22042)
30
+ - [GitHub](https://github.com/jeeit17/UNCHA)
31
+ - [Model weights](https://huggingface.co/hayeonkim/uncha)
app.py ADDED
@@ -0,0 +1,485 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UNCHA: Uncertainty-guided Compositional Hyperbolic Alignment
3
+ Zero-shot image classification demo using hyperbolic (Lorentz) embeddings.
4
+
5
+ Paper: https://arxiv.org/abs/2603.22042
6
+ Code: https://github.com/jeeit17/UNCHA
7
+ """
8
+
9
+ import os
10
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
11
+
12
+ import spaces # MUST come before torch / any CUDA-touching import
13
+
14
+ import gzip
15
+ import html
16
+ import math
17
+ import re
18
+ from collections import OrderedDict
19
+ from pathlib import Path
20
+
21
+ import numpy as np
22
+ import torch
23
+ from torch import nn
24
+ from torch.nn import functional as F
25
+ import timm
26
+ import gradio as gr
27
+ from PIL import Image
28
+
29
+ import torchvision.transforms as T
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Tokenizer (adapted from UNCHA/CLIP BPE tokenizer)
33
+ # ---------------------------------------------------------------------------
34
+
35
+ class Tokenizer:
36
+ """Byte-Pair Encoding tokenizer compatible with CLIP / UNCHA checkpoints."""
37
+
38
+ def __init__(self, bpe_path: str | Path | None = None):
39
+ bs = (
40
+ list(range(ord("!"), ord("~") + 1))
41
+ + list(range(ord("\xa1"), ord("\xac") + 1))
42
+ + list(range(ord("\xae"), ord("\xff") + 1))
43
+ )
44
+ self.byte_encoder = {b: chr(b) for b in bs}
45
+ n = 0
46
+ for b in range(2**8):
47
+ if b not in self.byte_encoder:
48
+ self.byte_encoder[b] = chr(2**8 + n)
49
+ n += 1
50
+
51
+ if bpe_path is None:
52
+ bpe_path = Path(__file__).resolve().parent / "bpe_simple_vocab_16e6.txt.gz"
53
+ merges = gzip.open(bpe_path).read().decode("utf-8").split("\n")
54
+ merges = merges[1 : 49152 - 256 - 2 + 1]
55
+ merges = [tuple(merge.split()) for merge in merges]
56
+ vocab = list(self.byte_encoder.values())
57
+ vocab = vocab + [v + "</w>" for v in vocab]
58
+ for merge in merges:
59
+ vocab.append("".join(merge))
60
+ vocab.extend(["<|startoftext|>", ""])
61
+
62
+ self.encoder = dict(zip(vocab, range(len(vocab))))
63
+ self.bpe_ranks = dict(zip(merges, range(len(merges))))
64
+ self.cache = {"<|startoftext|>": "<|startoftext|>", "": ""}
65
+ self.pat = re.compile(
66
+ r"""<\|startoftext\|>|<\|endoftext\|>|'s|'t|'re|'ve|'m|'ll|'d|[\p{L}]+|[\p{N}]|[^\s\p{L}\p{N}]+""",
67
+ re.IGNORECASE,
68
+ )
69
+
70
+ def __call__(self, text):
71
+ import ftfy
72
+
73
+ text_list = [text] if isinstance(text, str) else text
74
+ token_tensors = []
75
+ for text in text_list:
76
+ bpe_tokens = []
77
+ text = ftfy.fix_text(text)
78
+ text = html.unescape(html.unescape(text))
79
+ text = re.sub(r"\s+", " ", text)
80
+ text = text.strip().lower()
81
+ for token in re.findall(self.pat, text):
82
+ token = "".join(self.byte_encoder[b] for b in token.encode("utf-8"))
83
+ bpe_tokens.extend(
84
+ self.encoder[bpe_token]
85
+ for bpe_token in self.bpe(token).split(" ")
86
+ )
87
+ sot = self.encoder["<|startoftext|>"]
88
+ eot = self.encoder[""]
89
+ bpe_tokens = [sot, *bpe_tokens, eot]
90
+ token_tensors.append(torch.IntTensor(bpe_tokens))
91
+ return token_tensors
92
+
93
+ @staticmethod
94
+ def get_pairs(word):
95
+ pairs = set()
96
+ prev_char = word[0]
97
+ for char in word[1:]:
98
+ pairs.add((prev_char, char))
99
+ prev_char = char
100
+ return pairs
101
+
102
+ def bpe(self, token):
103
+ if token in self.cache:
104
+ return self.cache[token]
105
+ word = tuple(token[:-1]) + (token[-1] + "</w>",)
106
+ pairs = self.get_pairs(word)
107
+ if not pairs:
108
+ return token + "</w>"
109
+ while True:
110
+ bigram = min(pairs, key=lambda pair: self.bpe_ranks.get(pair, float("inf")))
111
+ if bigram not in self.bpe_ranks:
112
+ break
113
+ first, second = bigram
114
+ new_word = []
115
+ i = 0
116
+ while i < len(word):
117
+ try:
118
+ j = word.index(first, i)
119
+ new_word.extend(word[i:j])
120
+ i = j
121
+ except ValueError:
122
+ new_word.extend(word[i:])
123
+ break
124
+ if word[i] == first and i < len(word) - 1 and word[i + 1] == second:
125
+ new_word.append(first + second)
126
+ i += 2
127
+ else:
128
+ new_word.append(word[i])
129
+ i += 1
130
+ new_word = tuple(new_word)
131
+ word = new_word
132
+ if len(word) == 1:
133
+ break
134
+ else:
135
+ pairs = self.get_pairs(word)
136
+ word = " ".join(word)
137
+ self.cache[token] = word
138
+ return word
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Lorentz model hyperbolic operations (adapted from UNCHA/meru)
143
+ # ---------------------------------------------------------------------------
144
+
145
+ def pairwise_inner(x, y, curv=1.0):
146
+ x_time = torch.sqrt(1 / curv + torch.sum(x**2, dim=-1, keepdim=True))
147
+ y_time = torch.sqrt(1 / curv + torch.sum(y**2, dim=-1, keepdim=True))
148
+ return x @ y.T - x_time @ y_time.T
149
+
150
+
151
+ def exp_map0(x, curv=1.0, eps=1e-8):
152
+ rc_xnorm = curv**0.5 * torch.norm(x, dim=-1, keepdim=True)
153
+ sinh_input = torch.clamp(rc_xnorm, min=eps, max=math.asinh(2**15))
154
+ return torch.sinh(sinh_input) * x / torch.clamp(rc_xnorm, min=eps)
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # Text encoder (adapted from UNCHA TransformerTextEncoder)
159
+ # ---------------------------------------------------------------------------
160
+
161
+ class _TransformerBlock(nn.Module):
162
+ def __init__(self, d_model, n_head):
163
+ super().__init__()
164
+ self.attn = nn.MultiheadAttention(d_model, n_head, batch_first=True)
165
+ self.ln_1 = nn.LayerNorm(d_model)
166
+ self.mlp = nn.Sequential(
167
+ OrderedDict([
168
+ ("c_fc", nn.Linear(d_model, d_model * 4)),
169
+ ("gelu", nn.GELU()),
170
+ ("c_proj", nn.Linear(d_model * 4, d_model)),
171
+ ])
172
+ )
173
+ self.ln_2 = nn.LayerNorm(d_model)
174
+
175
+ def forward(self, x, attn_mask=None):
176
+ lx = self.ln_1(x)
177
+ ax = self.attn(lx, lx, lx, need_weights=False, attn_mask=attn_mask)[0]
178
+ x = x + ax
179
+ x = x + self.mlp(self.ln_2(x))
180
+ return x
181
+
182
+
183
+ class TransformerTextEncoder(nn.Module):
184
+ def __init__(self, arch="L12_W512", vocab_size=49408, context_length=77):
185
+ super().__init__()
186
+ self.vocab_size = vocab_size
187
+ self.context_length = context_length
188
+ self.layers = int(re.search(r"L(\d+)", arch).group(1))
189
+ self.width = int(re.search(r"W(\d+)", arch).group(1))
190
+ _attn = re.search(r"A(\d+)", arch)
191
+ self.heads = int(_attn.group(1)) if _attn else self.width // 64
192
+
193
+ self.token_embed = nn.Embedding(vocab_size, self.width)
194
+ self.posit_embed = nn.Parameter(torch.empty(context_length, self.width))
195
+ _resblocks = [_TransformerBlock(self.width, self.heads) for _ in range(self.layers)]
196
+ self.resblocks = nn.ModuleList(_resblocks)
197
+ self.ln_final = nn.LayerNorm(self.width)
198
+
199
+ attn_mask = torch.triu(
200
+ torch.full((context_length, context_length), float("-inf")), diagonal=1
201
+ )
202
+ self.register_buffer("attn_mask", attn_mask.bool())
203
+
204
+ nn.init.normal_(self.token_embed.weight, std=0.02)
205
+ nn.init.normal_(self.posit_embed.data, std=0.01)
206
+ out_proj_std = (2 * self.width * self.layers) ** -0.5
207
+ for block in self.resblocks:
208
+ nn.init.normal_(block.attn.in_proj_weight, std=self.width**-0.5)
209
+ nn.init.normal_(block.attn.out_proj.weight, std=out_proj_std)
210
+ nn.init.normal_(block.mlp[0].weight, std=(2 * self.width) ** -0.5)
211
+ nn.init.normal_(block.mlp[2].weight, std=out_proj_std)
212
+
213
+
214
+ def build_timm_vit(arch="vit_base_patch16_224", global_pool="token",
215
+ use_sincos2d_pos=True):
216
+ model = timm.create_model(
217
+ arch, num_classes=0, global_pool=global_pool,
218
+ class_token=global_pool == "token", norm_layer=nn.LayerNorm,
219
+ )
220
+ model.width = model.embed_dim
221
+ if use_sincos2d_pos:
222
+ h, w = model.patch_embed.grid_size
223
+ grid_w = torch.arange(w, dtype=torch.float32)
224
+ grid_h = torch.arange(h, dtype=torch.float32)
225
+ grid_w, grid_h = torch.meshgrid(grid_w, grid_h)
226
+ pos_dim = model.embed_dim // 4
227
+ omega = torch.arange(pos_dim, dtype=torch.float32) / pos_dim
228
+ omega = 1.0 / (10000.0**omega)
229
+ out_w = torch.einsum("m,d->md", [grid_w.flatten(), omega])
230
+ out_h = torch.einsum("m,d->md", [grid_h.flatten(), omega])
231
+ pos_emb = torch.cat(
232
+ [torch.sin(out_w), torch.cos(out_w), torch.sin(out_h), torch.cos(out_h)],
233
+ dim=1,
234
+ )[None, :, :]
235
+ if global_pool == "token":
236
+ pe_token = torch.zeros([1, 1, model.embed_dim], dtype=torch.float32)
237
+ pos_emb = torch.cat([pe_token, pos_emb], dim=1)
238
+ model.pos_embed.data.copy_(pos_emb)
239
+ model.pos_embed.requires_grad = False
240
+ return model
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # UNCHA model (inference-only)
245
+ # ---------------------------------------------------------------------------
246
+
247
+ class UNCHAModel(nn.Module):
248
+ """Inference-only UNCHA model: hyperbolic image-text alignment."""
249
+
250
+ def __init__(self, embed_dim=512, visual_arch="vit_base_patch16_224",
251
+ text_arch="L12_W512", vocab_size=49408, context_length=77):
252
+ super().__init__()
253
+ self.visual = build_timm_vit(arch=visual_arch)
254
+ self.textual = TransformerTextEncoder(
255
+ arch=text_arch, vocab_size=vocab_size, context_length=context_length
256
+ )
257
+ self.embed_dim = embed_dim
258
+ self.visual_proj = nn.Linear(self.visual.width, embed_dim, bias=False)
259
+ self.textual_proj = nn.Linear(self.textual.width, embed_dim, bias=False)
260
+ self.logit_scale = nn.Parameter(torch.tensor(1 / 0.07).log())
261
+ self.curv = nn.Parameter(torch.tensor(1.0).log())
262
+ self.visual_alpha = nn.Parameter(torch.tensor(embed_dim**-0.5).log())
263
+ self.textual_alpha = nn.Parameter(torch.tensor(embed_dim**-0.5).log())
264
+ self.tokenizer = Tokenizer()
265
+ self.register_buffer("pixel_mean", torch.tensor((0.485, 0.456, 0.406)).view(-1, 1, 1))
266
+ self.register_buffer("pixel_std", torch.tensor((0.229, 0.224, 0.225)).view(-1, 1, 1))
267
+
268
+ @property
269
+ def device(self):
270
+ return self.logit_scale.device
271
+
272
+ def encode_image(self, images, project=True):
273
+ images = (images - self.pixel_mean) / self.pixel_std
274
+ feats = self.visual(images)
275
+ feats = self.visual_proj(feats)
276
+ if project:
277
+ feats = feats * self.visual_alpha.exp()
278
+ with torch.autocast(self.device.type, dtype=torch.float32):
279
+ feats = exp_map0(feats, self.curv.exp())
280
+ return feats
281
+
282
+ def encode_text(self, tokens, project=True):
283
+ context_len = self.textual.context_length
284
+ batch_size = len(tokens)
285
+ padded = torch.zeros((batch_size, context_len), dtype=torch.long)
286
+ for idx, inst in enumerate(tokens):
287
+ L_ = min(inst.shape[0], context_len)
288
+ if inst.shape[0] > context_len:
289
+ inst = inst[:context_len]
290
+ padded[idx, :L_] = inst[:L_]
291
+ padded = padded.to(self.device)
292
+ feats = self.textual(padded)
293
+ eos = padded.argmax(dim=-1)
294
+ batch_idx = torch.arange(batch_size, device=self.device)
295
+ feats = feats[batch_idx, eos]
296
+ feats = self.textual_proj(feats)
297
+ if project:
298
+ feats = feats * self.textual_alpha.exp()
299
+ with torch.autocast(self.device.type, dtype=torch.float32):
300
+ feats = exp_map0(feats, self.curv.exp())
301
+ return feats
302
+
303
+
304
+ # ---------------------------------------------------------------------------
305
+ # Image preprocessing (matching the evaluation pipeline)
306
+ # ---------------------------------------------------------------------------
307
+
308
+ IMAGE_TRANSFORM = T.Compose([
309
+ T.Resize(224, T.InterpolationMode.BICUBIC),
310
+ T.CenterCrop(224),
311
+ T.ToTensor(),
312
+ ])
313
+
314
+ # ---------------------------------------------------------------------------
315
+ # Load model at module scope
316
+ # ---------------------------------------------------------------------------
317
+
318
+ CHECKPOINT_REPO = "hayeonkim/uncha"
319
+ CHECKPOINT_FILE = "uncha_vit_b.pth"
320
+
321
+ print("Loading UNCHA model...")
322
+ model = UNCHAModel(
323
+ embed_dim=512,
324
+ visual_arch="vit_base_patch16_224",
325
+ text_arch="L12_W512",
326
+ vocab_size=49408,
327
+ context_length=77,
328
+ )
329
+
330
+ # Download and load checkpoint
331
+ from huggingface_hub import hf_hub_download
332
+ _ckpt_path = hf_hub_download(CHECKPOINT_REPO, CHECKPOINT_FILE)
333
+ _ckpt = torch.load(_ckpt_path, map_location="cpu", weights_only=False)
334
+ _sd = _ckpt["model"]
335
+
336
+ # The checkpoint may contain min_radius_head keys from the text encoder that
337
+ # our inference model doesn't have — filter them out.
338
+ _model_sd = model.state_dict()
339
+ _filtered_sd = {}
340
+ for k, v in _sd.items():
341
+ if k in _model_sd:
342
+ _filtered_sd[k] = v
343
+ else:
344
+ print(f" Skipping checkpoint key not in model: {k}")
345
+
346
+ _missing, _unexpected = model.load_state_dict(_filtered_sd, strict=False)
347
+ if _missing:
348
+ print(f" Missing keys: {_missing}")
349
+ if _unexpected:
350
+ print(f" Unexpected keys: {_unexpected}")
351
+
352
+ model = model.eval().to("cuda")
353
+ print(f"Model loaded. curv={model.curv.exp().item():.4f}, "
354
+ f"visual_alpha={model.visual_alpha.exp().item():.4f}, "
355
+ f"textual_alpha={model.textual_alpha.exp().item():.4f}")
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # Inference
359
+ # ---------------------------------------------------------------------------
360
+
361
+ PROMPT_TEMPLATES = [
362
+ "a photo of a {}.",
363
+ "a blurry photo of a {}.",
364
+ "a black and white photo of a {}.",
365
+ "a low contrast photo of a {}.",
366
+ "a high contrast photo of a {}.",
367
+ "a bad photo of a {}.",
368
+ "a good photo of a {}.",
369
+ "a photo of a small {}.",
370
+ "a photo of a big {}.",
371
+ "a photo of the {}.",
372
+ ]
373
+
374
+ @spaces.GPU(duration=60)
375
+ def classify(image: Image.Image, candidate_labels: str) -> dict:
376
+ """Zero-shot image classification using UNCHA hyperbolic vision-language model.
377
+
378
+ Args:
379
+ image: Input image to classify.
380
+ candidate_labels: Comma-separated list of candidate class labels.
381
+
382
+ Returns:
383
+ Dictionary mapping each label to its probability score.
384
+ """
385
+ if image is None:
386
+ return {}
387
+ if image.mode != "RGB":
388
+ image = image.convert("RGB")
389
+
390
+ # Parse labels
391
+ labels = [l.strip() for l in candidate_labels.split(",") if l.strip()]
392
+ if not labels:
393
+ return {}
394
+
395
+ # Preprocess image
396
+ img_tensor = IMAGE_TRANSFORM(image).unsqueeze(0).to(model.device)
397
+
398
+ # Encode image into hyperbolic space
399
+ with torch.inference_mode():
400
+ img_feats = model.encode_image(img_tensor, project=True) # (1, D)
401
+
402
+ # Encode text prompts for each label (prompt ensemble in tangent space)
403
+ with torch.inference_mode():
404
+ all_class_feats = []
405
+ for label in labels:
406
+ prompts = [pt.format(label) for pt in PROMPT_TEMPLATES]
407
+ tokens = model.tokenizer(prompts)
408
+ text_feats = model.encode_text(tokens, project=False) # (N_prompts, D)
409
+ # Ensemble in tangent space, then project to hyperboloid
410
+ text_feats = text_feats.mean(dim=0) # (D,)
411
+ text_feats = text_feats * model.textual_alpha.exp()
412
+ text_feats = exp_map0(text_feats.unsqueeze(0), model.curv.exp()) # (1, D)
413
+ all_class_feats.append(text_feats.squeeze(0))
414
+
415
+ classifier = torch.stack(all_class_feats, dim=0) # (num_classes, D)
416
+
417
+ # Lorentzian pairwise inner product as classification scores
418
+ scores = pairwise_inner(img_feats, classifier, model.curv.exp()) # (1, num_classes)
419
+ scores = scores.squeeze(0) # (num_classes,)
420
+
421
+ # Convert to probabilities via softmax
422
+ probs = F.softmax(scores * model.logit_scale.exp(), dim=-1)
423
+
424
+ # Build label->prob dict, sorted by probability
425
+ result = {label: float(probs[i]) for i, label in enumerate(labels)}
426
+ result = dict(sorted(result.items(), key=lambda x: x[1], reverse=True))
427
+ return result
428
+
429
+
430
+ # ---------------------------------------------------------------------------
431
+ # Gradio UI
432
+ # ---------------------------------------------------------------------------
433
+
434
+ CSS = """
435
+ #col-container { max-width: 1100px; margin: 0 auto; }
436
+ .dark .gradio-container { color: var(--body-text-color); }
437
+ """
438
+
439
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
440
+ with gr.Column(elem_id="col-container"):
441
+ gr.Markdown(
442
+ """
443
+ # UNCHA: Uncertainty-guided Compositional Hyperbolic Alignment
444
+
445
+ Zero-shot image classification using a hyperbolic vision-language model.
446
+ Upload an image and provide candidate labels — the model will compute
447
+ similarity scores using Lorentzian (hyperbolic) geometry.
448
+
449
+ [Paper](https://arxiv.org/abs/2603.22042) | [Code](https://github.com/jeeit17/UNCHA) | [Model](https://huggingface.co/hayeonkim/uncha)
450
+ """
451
+ )
452
+
453
+ with gr.Row():
454
+ with gr.Column(scale=1):
455
+ image_input = gr.Image(
456
+ type="pil", label="Input Image",
457
+ sources=["upload", "clipboard"],
458
+ )
459
+ labels_input = gr.Textbox(
460
+ label="Candidate Labels (comma-separated)",
461
+ placeholder="cat, dog, bird, car, person",
462
+ value="cat, dog, bird, car, person",
463
+ )
464
+ run_btn = gr.Button("Classify", variant="primary")
465
+
466
+ with gr.Column(scale=1):
467
+ output_labels = gr.Label(
468
+ label="Classification Scores",
469
+ num_top_classes=10,
470
+ )
471
+
472
+ gr.Examples(
473
+ examples=[
474
+ ["examples/sample1.jpg", "dog, cat, animal, pet, mammal"],
475
+ ["examples/sample2.jpg", "building, landscape, nature, city, water"],
476
+ ["examples/sample3.jpg", "person, food, vehicle, animal, plant"],
477
+ ],
478
+ inputs=[image_input, labels_input],
479
+ outputs=output_labels,
480
+ fn=classify,
481
+ cache_examples=True,
482
+ cache_mode="lazy",
483
+ )
484
+
485
+ demo.launch(mcp_server=True)
bpe_simple_vocab_16e6.txt.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:924691ac288e54409236115652ad4aa250f48203de50a9e4722a6ecd48d6804a
3
+ size 1356917
examples/sample1.jpg ADDED
examples/sample2.jpg ADDED
examples/sample3.jpg ADDED
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ timm
2
+ ftfy
3
+ regex