Yanqing0327 commited on
Commit
ef01023
·
verified ·
1 Parent(s): 31d4f53

Add caption text decoder (encoder + decoder = full OpenVision2 generative model)

Browse files
README.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ pipeline_tag: image-to-text
3
+ tags:
4
+ - openvision2
5
+ - image-captioning
6
+ - vision-encoder
7
+ library_name: open_clip
8
+ ---
9
+
10
+ # OpenVision2 · L/14 @336 — vision encoder + caption decoder
11
+
12
+ This repo now contains the **full OpenVision2 generative model**: the **vision encoder**
13
+ (originally released here) **plus the caption text decoder** it was jointly trained with.
14
+ Together they map an image -> a descriptive caption. The encoder files are unchanged;
15
+ only the decoder (and this card) were added.
16
+
17
+ ## Files
18
+ | file | role |
19
+ |---|---|
20
+ | `open_clip_pytorch_model.bin`, `open_clip_config.json` | L/14 vision encoder (open_clip format) |
21
+ | `caption_decoder.safetensors` | caption text decoder weights |
22
+ | `text_decoder_config.json` | decoder architecture config |
23
+ | `modeling_openvision2_decoder.py` | standalone PyTorch decoder + `generate()` |
24
+ | `bert_base_vocab_bos_eos.txt` | tokenizer vocab (`[PAD]=0 [bos]=1 [eos]=2`) |
25
+ | `caption_example.py` | end-to-end image -> caption demo |
26
+
27
+ ## Decoder architecture
28
+ A **concat / prefix-LM** autoregressive transformer (not a CoCa cross-attention decoder):
29
+ ViT patch tokens are linearly projected and **prepended as a bidirectional prefix**, and
30
+ text is generated **causally** while attending to all image tokens.
31
+
32
+ `12 layers · width 768 · 12 heads · mlp 3072 · vocab 32000 ·
33
+ pre-LN · gelu(tanh) · LayerNorm eps 1e-6 · no positional embedding on the text stream.`
34
+ It consumes the encoder's pre-final-norm patch tokens (open_clip `output_tokens=True`).
35
+
36
+ ## Usage
37
+ Needs the patched `open_clip` providing `create_vision_encoder_and_transforms`
38
+ (https://github.com/UCSC-VLAA/OpenVision), plus `torch`, `safetensors`, `pillow`.
39
+ See `caption_example.py`:
40
+
41
+ ```bash
42
+ python caption_example.py --image your.jpg
43
+ ```
44
+
45
+ ## Notes
46
+ - Encoder and decoder are a **matched pair** exported from the same training checkpoint.
47
+ - Captions are LLaVA-style **dense** descriptions (multi-sentence, detailed).
bert_base_vocab_bos_eos.txt ADDED
The diff for this file is too large to render. See raw diff
 
caption_decoder.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0241150fd5f5327e9fa5048a005f8ed82912a29b894b8c2be6151a37226558aa
3
+ size 539991872
caption_example.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ r'''
2
+ OpenVision2 ViT-H/14 @224 — image -> caption demo.
3
+
4
+ Combines the released vision encoder (this repo, open_clip format) with the caption
5
+ text decoder (this repo). Requires the patched open_clip that ships
6
+ `create_vision_encoder_and_transforms` (from the OpenVision repo:
7
+ https://github.com/UCSC-VLAA/OpenVision), plus torch / safetensors / pillow.
8
+
9
+ python caption_example.py --image path/to/img.jpg
10
+ '''
11
+ import argparse, json
12
+ import numpy as np, torch
13
+ from PIL import Image
14
+ from huggingface_hub import hf_hub_download
15
+ from open_clip.factory import create_vision_encoder_and_transforms # patched open_clip
16
+ from modeling_openvision2_decoder import OpenVision2TextDecoder, OpenVision2TextDecoderConfig
17
+ from safetensors.torch import load_file
18
+
19
+ REPO = "UCSC-VLAA/openvision2-vit-large-patch14-336-vision-only"
20
+ IMAGENET_MEAN = np.array([0.485, 0.456, 0.406]) * 255
21
+ IMAGENET_STD = np.array([0.229, 0.224, 0.225]) * 255
22
+
23
+
24
+ def preprocess(path, res=224):
25
+ im = Image.open(path).convert("RGB")
26
+ w, h = im.size; s = res / min(w, h)
27
+ im = im.resize((round(w * s), round(h * s)), Image.BILINEAR)
28
+ w, h = im.size; l, t = (w - res) // 2, (h - res) // 2
29
+ im = im.crop((l, t, l + res, t + res))
30
+ x = (np.asarray(im, np.float32) - IMAGENET_MEAN) / IMAGENET_STD
31
+ return torch.tensor(x.transpose(2, 0, 1)[None], dtype=torch.float32) # [1,C,H,W]
32
+
33
+
34
+ def load_decoder(cache_dir=None):
35
+ cfg = json.load(open(hf_hub_download(REPO, "text_decoder_config.json", cache_dir=cache_dir)))
36
+ dec = OpenVision2TextDecoder(OpenVision2TextDecoderConfig(
37
+ width=cfg["width"], depth=cfg["depth"], num_heads=cfg["num_heads"], mlp_dim=cfg["mlp_dim"],
38
+ vocab_size=cfg["vocab_size"], vision_width=cfg["vision_width"]))
39
+ dec.load_state_dict(load_file(hf_hub_download(REPO, "caption_decoder.safetensors", cache_dir=cache_dir)))
40
+ dec.eval()
41
+ vocab = [l.rstrip("\n") for l in open(hf_hub_download(REPO, "bert_base_vocab_bos_eos.txt", cache_dir=cache_dir))]
42
+ return dec, vocab
43
+
44
+
45
+ def detok(ids, vocab):
46
+ w = []
47
+ for i in ids:
48
+ if i == 2: break # eos
49
+ if i in (0, 1, 2): continue # pad / bos / eos
50
+ tk = vocab[i] if 0 <= i < len(vocab) else "[UNK]"
51
+ if tk.startswith("##"): w[-1] = (w[-1] + tk[2:]) if w else tk[2:]
52
+ else: w.append(tk)
53
+ return " ".join(w)
54
+
55
+
56
+ def main():
57
+ ap = argparse.ArgumentParser()
58
+ ap.add_argument("--image", required=True)
59
+ ap.add_argument("--max_len", type=int, default=64)
60
+ ap.add_argument("--cache_dir", default=None)
61
+ args = ap.parse_args()
62
+
63
+ enc = create_vision_encoder_and_transforms(model_name=f"hf-hub:{REPO}", cache_dir=args.cache_dir).eval()
64
+ dec, vocab = load_decoder(args.cache_dir)
65
+ with torch.no_grad():
66
+ _, tokens = enc(preprocess(args.image)) # [1, N, 1280]
67
+ ids = dec.generate(tokens, max_len=args.max_len, bos_id=1, eos_id=2)[0].tolist()
68
+ print(detok(ids, vocab))
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
modeling_openvision2_decoder.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PyTorch reference implementation of the OpenVision2 caption decoder.
2
+
3
+ The OpenVision2 generative model is: a ViT vision encoder (released separately as
4
+ the `*-vision-only` open_clip checkpoint) whose patch tokens condition a small
5
+ autoregressive text decoder trained with a captioning loss.
6
+
7
+ This decoder is a **prefix-LM / concat-fusion** transformer (NOT a CoCa-style
8
+ cross-attention decoder):
9
+
10
+ text_embeds = Embed(text_tokens) # no positional embedding
11
+ image_embeds = image_projection(vit_patch_tokens) # Linear, no bias
12
+ x = concat([image_embeds, text_embeds], 1) # [B, N_img + L, width]
13
+ x = prefix_lm_transformer(x) # image=bidirectional prefix,
14
+ # text=causal, text->image=full,
15
+ # image-/->text
16
+ logits = lm_head( ln_final( x[:, N_img:] ) ) # over text positions
17
+
18
+ Faithful details that matter for numerical parity with the JAX model:
19
+ * LayerNorm eps = 1e-6 (flax default), pre-LN blocks.
20
+ * MLP activation = gelu(tanh approximation).
21
+ * attention scale = head_dim ** -0.5.
22
+ * NO positional embedding on the text stream.
23
+ * vision patch tokens are the pre-final-norm tokens, cls excluded
24
+ (open_clip vision model with output_tokens=True returns exactly these).
25
+ """
26
+ from dataclasses import dataclass
27
+ from typing import Optional
28
+ import torch
29
+ import torch.nn as nn
30
+ import torch.nn.functional as F
31
+
32
+
33
+ @dataclass
34
+ class OpenVision2TextDecoderConfig:
35
+ width: int = 1024 # decoder hidden width (H decoder -> 1024)
36
+ depth: int = 24 # number of transformer blocks
37
+ num_heads: int = 16
38
+ mlp_dim: int = 4096
39
+ vocab_size: int = 32000
40
+ vision_width: int = 1280 # ViT token dim feeding image_projection (H ViT -> 1280)
41
+ layer_norm_eps: float = 1e-6
42
+ pad_id: int = 0
43
+ bos_id: int = 1
44
+ eos_id: int = 2
45
+
46
+
47
+ class _Mlp(nn.Module):
48
+ def __init__(self, width: int, mlp_dim: int):
49
+ super().__init__()
50
+ self.c_fc = nn.Linear(width, mlp_dim)
51
+ self.c_proj = nn.Linear(mlp_dim, width)
52
+
53
+ def forward(self, x):
54
+ return self.c_proj(F.gelu(self.c_fc(x), approximate="tanh"))
55
+
56
+
57
+ class _Attention(nn.Module):
58
+ """MHA with a fused in_proj (q,k,v) and an explicit boolean attend-mask."""
59
+
60
+ def __init__(self, width: int, num_heads: int):
61
+ super().__init__()
62
+ assert width % num_heads == 0
63
+ self.num_heads = num_heads
64
+ self.head_dim = width // num_heads
65
+ self.scale = self.head_dim ** -0.5
66
+ self.in_proj_weight = nn.Parameter(torch.empty(3 * width, width))
67
+ self.in_proj_bias = nn.Parameter(torch.zeros(3 * width))
68
+ self.out_proj = nn.Linear(width, width)
69
+
70
+ def forward(self, x, attend_mask):
71
+ # attend_mask: [L, L] bool, True = keep, False = -inf
72
+ B, L, D = x.shape
73
+ qkv = F.linear(x, self.in_proj_weight, self.in_proj_bias)
74
+ q, k, v = qkv.chunk(3, dim=-1)
75
+ H, hd = self.num_heads, self.head_dim
76
+ q = q.view(B, L, H, hd).transpose(1, 2) # [B,H,L,hd]
77
+ k = k.view(B, L, H, hd).transpose(1, 2)
78
+ v = v.view(B, L, H, hd).transpose(1, 2)
79
+ attn = (q @ k.transpose(-2, -1)) * self.scale # [B,H,L,L]
80
+ attn = attn.masked_fill(~attend_mask[None, None], float("-inf"))
81
+ attn = attn.softmax(dim=-1)
82
+ out = attn @ v # [B,H,L,hd]
83
+ out = out.transpose(1, 2).reshape(B, L, D)
84
+ return self.out_proj(out)
85
+
86
+
87
+ class _Block(nn.Module):
88
+ def __init__(self, cfg: OpenVision2TextDecoderConfig):
89
+ super().__init__()
90
+ self.ln_1 = nn.LayerNorm(cfg.width, eps=cfg.layer_norm_eps)
91
+ self.attn = _Attention(cfg.width, cfg.num_heads)
92
+ self.ln_2 = nn.LayerNorm(cfg.width, eps=cfg.layer_norm_eps)
93
+ self.mlp = _Mlp(cfg.width, cfg.mlp_dim)
94
+
95
+ def forward(self, x, attend_mask):
96
+ x = x + self.attn(self.ln_1(x), attend_mask)
97
+ x = x + self.mlp(self.ln_2(x))
98
+ return x
99
+
100
+
101
+ class OpenVision2TextDecoder(nn.Module):
102
+ def __init__(self, cfg: OpenVision2TextDecoderConfig):
103
+ super().__init__()
104
+ self.cfg = cfg
105
+ self.token_embedding = nn.Embedding(cfg.vocab_size, cfg.width)
106
+ self.image_projection = nn.Linear(cfg.vision_width, cfg.width, bias=False)
107
+ self.blocks = nn.ModuleList([_Block(cfg) for _ in range(cfg.depth)])
108
+ self.ln_final = nn.LayerNorm(cfg.width, eps=cfg.layer_norm_eps)
109
+ self.lm_head = nn.Linear(cfg.width, cfg.vocab_size, bias=False)
110
+
111
+ @staticmethod
112
+ def _prefix_lm_mask(li: int, lt: int, device) -> torch.Tensor:
113
+ """[l, l] bool attend-mask. image(:li) bidirectional prefix; text(li:)
114
+ causal + attends all image; image does NOT attend text."""
115
+ l = li + lt
116
+ mask = torch.zeros(l, l, dtype=torch.bool, device=device)
117
+ mask[:li, :li] = True # image <-> image
118
+ mask[li:, :li] = True # text -> image
119
+ text_causal = torch.tril(torch.ones(lt, lt, dtype=torch.bool, device=device))
120
+ mask[li:, li:] = text_causal # text causal self
121
+ return mask
122
+
123
+ def forward(self, image_tokens: torch.Tensor, text_tokens: torch.Tensor) -> torch.Tensor:
124
+ """image_tokens: [B, N_img, vision_width]; text_tokens: [B, L] (input side).
125
+ Returns logits [B, L, vocab] (next-token logits at each text position)."""
126
+ text_embeds = self.token_embedding(text_tokens) # [B, L, width]
127
+ image_embeds = self.image_projection(image_tokens) # [B, N_img, width]
128
+ li, lt = image_embeds.shape[1], text_embeds.shape[1]
129
+ x = torch.cat([image_embeds, text_embeds], dim=1) # [B, li+lt, width]
130
+ mask = self._prefix_lm_mask(li, lt, x.device)
131
+ for blk in self.blocks:
132
+ x = blk(x, mask)
133
+ x = x[:, li:] # text positions
134
+ x = self.ln_final(x)
135
+ return self.lm_head(x)
136
+
137
+ @torch.no_grad()
138
+ def generate(self, image_tokens: torch.Tensor, max_len: int = 64,
139
+ bos_id: Optional[int] = None, eos_id: Optional[int] = None) -> torch.Tensor:
140
+ """Greedy autoregressive caption generation. image_tokens: [B, N, vision_width]."""
141
+ bos_id = self.cfg.bos_id if bos_id is None else bos_id
142
+ eos_id = self.cfg.eos_id if eos_id is None else eos_id
143
+ B = image_tokens.shape[0]
144
+ device = image_tokens.device
145
+ seq = torch.full((B, 1), bos_id, dtype=torch.long, device=device)
146
+ done = torch.zeros(B, dtype=torch.bool, device=device)
147
+ for _ in range(max_len):
148
+ logits = self.forward(image_tokens, seq) # [B, cur_len, vocab]
149
+ nxt = logits[:, -1].argmax(dim=-1) # [B]
150
+ nxt = torch.where(done, torch.full_like(nxt, self.cfg.pad_id), nxt)
151
+ seq = torch.cat([seq, nxt[:, None]], dim=1)
152
+ done = done | (nxt == eos_id)
153
+ if bool(done.all()):
154
+ break
155
+ return seq
text_decoder_config.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architecture": "OpenVision2TextDecoder",
3
+ "fusion_style": "concat",
4
+ "width": 768,
5
+ "depth": 12,
6
+ "num_heads": 12,
7
+ "mlp_dim": 3072,
8
+ "vocab_size": 32000,
9
+ "vision_width": 1024,
10
+ "layer_norm_eps": 1e-06,
11
+ "pad_id": 0,
12
+ "bos_id": 1,
13
+ "eos_id": 2,
14
+ "weights_file": "caption_decoder.safetensors",
15
+ "modeling_file": "modeling_openvision2_decoder.py",
16
+ "tokenizer": "bert wordpiece (bert_base_vocab_bos_eos.txt), lowercase, [PAD]=0 [bos]=1 [eos]=2"
17
+ }