| import os
|
| import sys
|
|
|
| import faiss
|
| import streamlit as st
|
| import timm
|
| import torch
|
| import torch.nn as nn
|
| import torch.nn.functional as F
|
| from PIL import Image
|
| from transformers import (
|
| AutoModelForCausalLM,
|
| AutoTokenizer,
|
| CLIPTextModelWithProjection,
|
| CLIPTokenizer,
|
| )
|
|
|
| sys.path.append(os.path.dirname(__file__))
|
| from multimodal_w8a8_smoothquant import SmoothQuantWrapper
|
|
|
|
|
| MODEL_NAME = "convnextv2_nano.fcmae_ft_in1k"
|
| LLM_NAME = "Qwen/Qwen1.5-0.5B"
|
| CLIP_MODEL_ID = "openai/clip-vit-base-patch32"
|
| VISION_CKPT = "./models/hf_w8a8_smoothquant/smoothquant_w8a8.pth"
|
| PROJ_CKPT = "./checkpoints/vlm_projection/projection_head_epoch_1.pth"
|
| FAISS_INDEX_PATH = "./data/faiss_db/general_clip.index"
|
| FAISS_META_PATH = "./data/faiss_db/general_metadata.txt"
|
|
|
| DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
|
|
| st.set_page_config(page_title="General VLM Explorer", page_icon="VLM", layout="wide")
|
|
|
|
|
|
|
| @st.cache_resource(show_spinner="Loading Models...")
|
| def load_models():
|
|
|
| vision_encoder = timm.create_model(MODEL_NAME, pretrained=False)
|
| in_features = vision_encoder.head.fc.in_features
|
| vision_encoder.head.fc = nn.Linear(in_features, 512)
|
|
|
| for name, module in dict(vision_encoder.named_modules()).items():
|
| if isinstance(module, (nn.Conv2d, nn.Linear)) and "head" not in name:
|
| dummy_scale = torch.ones(
|
| module.in_channels if isinstance(module, nn.Conv2d) else module.in_features
|
| )
|
| sq_layer = SmoothQuantWrapper(module, dummy_scale)
|
| parts = name.split(".")
|
| parent = vision_encoder
|
| for part in parts[:-1]:
|
| parent = getattr(parent, part)
|
| setattr(parent, parts[-1], sq_layer)
|
|
|
| if os.path.exists(VISION_CKPT):
|
| ckpt = torch.load(VISION_CKPT, map_location="cpu")
|
| vision_encoder.load_state_dict(
|
| ckpt["model_state_dict"] if "model_state_dict" in ckpt else ckpt, strict=False
|
| )
|
| vision_encoder.head.fc = nn.Identity()
|
| vision_encoder = vision_encoder.bfloat16().to(DEVICE).eval()
|
|
|
|
|
| llm = (
|
| AutoModelForCausalLM.from_pretrained(LLM_NAME, torch_dtype=torch.bfloat16).to(DEVICE).eval()
|
| )
|
| tokenizer = AutoTokenizer.from_pretrained(LLM_NAME)
|
| if tokenizer.pad_token is None:
|
| tokenizer.pad_token = tokenizer.eos_token
|
|
|
|
|
| projection_head = nn.Linear(in_features, llm.config.hidden_size).to(DEVICE).bfloat16()
|
| if os.path.exists(PROJ_CKPT):
|
| projection_head.load_state_dict(torch.load(PROJ_CKPT, map_location=DEVICE))
|
|
|
|
|
| _dummy = timm.create_model(MODEL_NAME, pretrained=False)
|
| data_config = timm.data.resolve_model_data_config(_dummy)
|
| image_transform = timm.data.create_transform(**data_config, is_training=False)
|
|
|
|
|
| clip_tokenizer = CLIPTokenizer.from_pretrained(CLIP_MODEL_ID)
|
| clip_text_model = (
|
| CLIPTextModelWithProjection.from_pretrained(CLIP_MODEL_ID).bfloat16().to(DEVICE).eval()
|
| )
|
|
|
|
|
| index = faiss.read_index(FAISS_INDEX_PATH) if os.path.exists(FAISS_INDEX_PATH) else None
|
| metadata = []
|
| if os.path.exists(FAISS_META_PATH):
|
| with open(FAISS_META_PATH) as f:
|
| metadata = [line.strip() for line in f.readlines()]
|
|
|
| return (
|
| vision_encoder,
|
| llm,
|
| tokenizer,
|
| projection_head,
|
| image_transform,
|
| clip_tokenizer,
|
| clip_text_model,
|
| index,
|
| metadata,
|
| )
|
|
|
|
|
| (
|
| vision_encoder,
|
| llm,
|
| tokenizer,
|
| projection_head,
|
| image_transform,
|
| clip_tokenizer,
|
| clip_text_model,
|
| index,
|
| metadata,
|
| ) = load_models()
|
|
|
|
|
| st.title("Omni-Modal VLM Explorer")
|
| st.markdown(
|
| "W8A8 ConvNeXt-Nano์ Qwen 0.5B๋ฅผ ๊ฒฐํฉํ ๊ฒฝ๋ VLM์
๋๋ค. ์ด๋ฏธ์ง๋ฅผ ์ค๋ช
ํ๊ฑฐ๋ ์ง๋ฌธ์ ๋ต๋ณํ๊ณ , ํ
์คํธ๋ก ์ด๋ฏธ์ง๋ฅผ ๊ฒ์ํ ์ ์์ต๋๋ค."
|
| )
|
|
|
| tab1, tab2, tab3 = st.tabs(["Image-to-Text", "Text-to-Image", "Text-to-Text"])
|
|
|
|
|
| def generate_with_image(image, prompt, max_new_tokens=100, temperature=0.7):
|
| pixel_values = image_transform(image).unsqueeze(0).to(DEVICE, dtype=torch.bfloat16)
|
| encoded = tokenizer(prompt, return_tensors="pt").to(DEVICE)
|
|
|
| with torch.no_grad():
|
| vision_features = vision_encoder(pixel_values)
|
| image_embeds = projection_head(vision_features).unsqueeze(1)
|
| text_embeds = llm.get_input_embeddings()(encoded["input_ids"])
|
| inputs_embeds = torch.cat([image_embeds, text_embeds], dim=1)
|
|
|
| attention_mask = encoded["attention_mask"]
|
| image_attention_mask = torch.ones(
|
| attention_mask.shape[0], 1, dtype=attention_mask.dtype, device=DEVICE
|
| )
|
| attention_mask = torch.cat([image_attention_mask, attention_mask], dim=1)
|
|
|
| outputs = llm.generate(
|
| inputs_embeds=inputs_embeds,
|
| attention_mask=attention_mask,
|
| max_new_tokens=max_new_tokens,
|
| temperature=temperature,
|
| do_sample=True,
|
| pad_token_id=tokenizer.eos_token_id,
|
| )
|
|
|
| return tokenizer.decode(outputs[0], skip_special_tokens=True)
|
|
|
|
|
| with tab1:
|
| st.subheader("์ด๋ฏธ์ง ์ค๋ช
๋ฐ ์ง๋ฌธ ์๋ต")
|
| col1, col2 = st.columns([1, 1])
|
|
|
| with col1:
|
| uploaded_file = st.file_uploader("์ด๋ฏธ์ง๋ฅผ ์
๋ก๋ํ์ธ์", type=["jpg", "png", "jpeg"])
|
| if uploaded_file is not None:
|
| image = Image.open(uploaded_file).convert("RGB")
|
| st.image(image, caption="์
๋ก๋๋ ์ด๋ฏธ์ง", use_container_width=True)
|
|
|
| with col2:
|
| mode = st.radio("๋ชจ๋ ์ ํ", ["์บก์
๋", "VQA"], horizontal=True)
|
| if mode == "์บก์
๋":
|
| prompt = st.text_input("์์ฒญ", value="์ด ์ด๋ฏธ์ง๋ฅผ ์์ธํ ์ค๋ช
ํด ์ฃผ์ญ์์ค:")
|
| else:
|
| prompt = st.text_input("์ง๋ฌธ", value="์ด ์ด๋ฏธ์ง์์ ๊ฐ์ฅ ๋์ ๋๋ ๋ฌผ์ฒด๋ ๋ฌด์์
๋๊น?")
|
|
|
| if st.button("๋ถ์ ์์") and uploaded_file is not None:
|
| with st.spinner("์ด๋ฏธ์ง ์๋ฒ ๋ฉ์ ๊ณ์ฐ ์ค์
๋๋ค."):
|
| full_prompt = f"<image>\n{prompt}\n"
|
| response = generate_with_image(image, full_prompt, max_new_tokens=120)
|
| st.write(response)
|
|
|
| with tab2:
|
| st.subheader("ํ
์คํธ ๊ธฐ๋ฐ ์ด๋ฏธ์ง ๊ฒ์")
|
| query = st.text_input("๊ฒ์ํ ์ด๋ฏธ์ง์ ํน์ง์ ์์ด๋ก ์
๋ ฅํ์ธ์")
|
| if st.button("๊ฒ์") and query:
|
| if index is None:
|
| st.error("FAISS ์ธ๋ฑ์ค๊ฐ ๋ก๋๋์ง ์์์ต๋๋ค.")
|
| else:
|
| with st.spinner("ํ
์คํธ ์๋ฒ ๋ฉ์ ๊ณ์ฐ ์ค์
๋๋ค."):
|
| inputs = clip_tokenizer(query, return_tensors="pt", padding=True).to(DEVICE)
|
| with torch.no_grad():
|
| t_feat = (
|
| F.normalize(clip_text_model(**inputs).text_embeds, p=2, dim=-1)
|
| .cpu()
|
| .float()
|
| .numpy()
|
| )
|
|
|
| D, indices = index.search(t_feat, 4)
|
|
|
| cols = st.columns(4)
|
| for c_idx, col in enumerate(cols):
|
| match_idx = indices[0][c_idx]
|
| if match_idx < len(metadata):
|
| img_path = metadata[match_idx]
|
| try:
|
| res_img = Image.open(img_path)
|
| col.image(
|
| res_img,
|
| caption=f"์ ์ฌ๋: {D[0][c_idx]:.4f}",
|
| use_container_width=True,
|
| )
|
| except Exception:
|
| col.write("์ด๋ฏธ์ง๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.")
|
|
|
| with tab3:
|
| st.subheader("ํ
์คํธ ์ ์ฉ ๋ํ")
|
| user_text = st.text_area("์
๋ ฅ", height=120)
|
| if st.button("์๋ต ์์ฑ") and user_text:
|
| with st.spinner("๋ชจ๋ธ์ด ์๋ต์ ์์ฑ ์ค์
๋๋ค."):
|
| encoded = tokenizer(user_text, return_tensors="pt").to(DEVICE)
|
| with torch.no_grad():
|
| outputs = llm.generate(
|
| input_ids=encoded["input_ids"],
|
| attention_mask=encoded["attention_mask"],
|
| max_new_tokens=160,
|
| temperature=0.7,
|
| do_sample=True,
|
| pad_token_id=tokenizer.eos_token_id,
|
| )
|
| response = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| st.write(response)
|
|
|
| st.sidebar.markdown("### Architecture Analytics")
|
| st.sidebar.markdown("""
|
| - **Vision:** ConvNeXt-Nano (W8A8 PTQ)
|
| - **Vision Size:** 14.9 MB
|
| - **LLM:** Qwen1.5-0.5B (BFloat16)
|
| - **FAISS Index:** 1,000 Conceptual Captions
|
| - **Speed:** ~100 FPS (Vision)
|
| """)
|
|
|