Spaces:
Runtime error
Runtime error
| import streamlit as st | |
| import torch | |
| import requests | |
| import os | |
| import json | |
| from transformers import AutoImageProcessor, AutoModelForImageClassification, AutoConfig | |
| from peft import PeftModel, PeftConfig | |
| from PIL import Image | |
| import torch.nn.functional as F | |
| import io | |
| # 1. ํ์ด์ง ๊ธฐ๋ณธ ์ค์ (๊ฐ์ฅ ๋จผ์ ํธ์ถํด์ผ ํจ) | |
| st.set_page_config(page_title="Pokemon Classifier", page_icon="๐พ", layout="wide", initial_sidebar_state="collapsed") | |
| # 2. ์ปค์คํ CSS ์ ์ฉ (๋ ์๋น์ค์ค๋ฌ์ด ๋๋์ ์ํด) | |
| st.markdown(""" | |
| <style> | |
| .main-header { | |
| text-align: center; | |
| padding: 2rem 0 3rem 0; | |
| } | |
| .main-header h1 { | |
| font-size: 3.5rem; | |
| color: #ffcb05; | |
| -webkit-text-stroke: 2px #3c5aa6; | |
| text-shadow: 4px 4px 0px #3c5aa6; | |
| margin-bottom: 0.5rem; | |
| } | |
| .main-header p { | |
| font-size: 1.2rem; | |
| color: #555; | |
| font-weight: 500; | |
| } | |
| /* ์นด๋ ๋๋์ ์ปจํ ์ด๋๋ฅผ ์ํด ์ฌ๋ฐฑ ์กฐ์ */ | |
| div[data-testid="stVerticalBlock"] { | |
| gap: 1.2rem; | |
| } | |
| </style> | |
| <div class="main-header"> | |
| <h1>Pokemon Classifier</h1> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # 3. ๋ง๋ฅ ๋ชจ๋ธ ๋ก๋ ํจ์ (์บ์ฑ & ์๋ฌ ๋ฐฉ์ด ์ ์ฉ) | |
| def load_model(model_path): | |
| try: | |
| # ํ๊น ํ์ด์ค ๋ฆฌํฌ์งํ ๋ฆฌ ๋๋ ๋ก์ปฌ ๊ฒฝ๋ก์์ PEFT ์ค์ ํ์ผ ํ์ธ | |
| try: | |
| config = PeftConfig.from_pretrained(model_path) | |
| is_peft = True | |
| except: | |
| is_peft = False | |
| if is_peft: | |
| base_model_name = config.base_model_name_or_path | |
| try: | |
| processor = AutoImageProcessor.from_pretrained(model_path) | |
| except: | |
| processor = AutoImageProcessor.from_pretrained(base_model_name) | |
| # 1. ํฌ์ผ๋ชฌ ๋ถ๋ฅ๋ ๋ฌด์กฐ๊ฑด 150๊ฐ์ ํด๋์ค์ ๋๋ค. | |
| num_labels = 150 | |
| # 2. ๋ผ๋ฒจ ๋งคํ ๋์ ๋๋ฆฌ ๊ตฌ์ฑ (์ง์ ์์ฑํ์ฌ ์ถฉ๋ ๋ฐฉ์ง) | |
| try: | |
| # ๋ก์ปฌ์ ์ ์ฅ๋ Full FT ๋ชจ๋ธ์ config๊ฐ ์๋ค๋ฉด ๊ฐ์ฅ ์๋ฒฝํ ํฌ์ผ๋ชฌ ์ด๋ฆ ๋์ ๋๋ฆฌ ์ฌ์ฉ | |
| config_path = "./saved_model/best_vit_full/config.json" | |
| with open(config_path, "r", encoding="utf-8") as f: | |
| full_config = json.load(f) | |
| id2label = {int(k): v for k, v in full_config["id2label"].items()} | |
| label2id = full_config["label2id"] | |
| except Exception: | |
| try: | |
| # ๋ก์ปฌ ํ์ผ์ด ์์ผ๋ฉด ๋ฐฐํฌ๋ ํ๊น ํ์ด์ค ์ ์ฅ์์์ ์ฌ๋ฐ๋ฅธ ๋งคํ์ ๊ฐ์ ๋ก ๊ฐ์ ธ์ต๋๋ค. | |
| reference_config = AutoConfig.from_pretrained("gyann/pokemon-vit-full") | |
| id2label = {int(k): v for k, v in reference_config.id2label.items()} | |
| label2id = reference_config.label2id | |
| except Exception: | |
| # ์ตํ์ ์๋จ์ผ๋ก ๋๋ฏธ ์์ฑ | |
| id2label = {i: f"LABEL_{i}" for i in range(num_labels)} | |
| label2id = {f"LABEL_{i}": i for i in range(num_labels)} | |
| # 3. Base ๋ชจ๋ธ์ ๋ก๋ํ ๋ ๋ฐ๋์ num_labels๋ฅผ ๋ช ์ํด์ผ classifier ํค๋ ์ฌ์ด์ฆ๊ฐ 150์ผ๋ก ์ด๊ธฐํ๋ฉ๋๋ค. | |
| base_model = AutoModelForImageClassification.from_pretrained( | |
| base_model_name, | |
| num_labels=num_labels, | |
| id2label=id2label, | |
| label2id=label2id, | |
| ignore_mismatched_sizes=True | |
| ) | |
| # 4. 150 ์ฌ์ด์ฆ๋ก ๋ง์ถฐ์ง Base ๋ชจ๋ธ์ LoRA ๊ฐ์ค์น ๊ฒฐํฉ | |
| model = PeftModel.from_pretrained(base_model, model_path) | |
| else: | |
| processor = AutoImageProcessor.from_pretrained(model_path) | |
| model = AutoModelForImageClassification.from_pretrained(model_path) | |
| # ๋น-PEFT ๋ชจ๋ธ๋ config์ ์ด๋ฆ์ด ์๋ ๊ฒฝ์ฐ(Label_15 ๋ฑ)๋ฅผ ๋๋นํด ๋งคํ์ ๋ฎ์ด์์๋๋ค. | |
| if getattr(model.config, "id2label", {}).get(0, "") == "LABEL_0" or getattr(model.config, "id2label", {}).get("0", "") == "LABEL_0": | |
| try: | |
| reference_config = AutoConfig.from_pretrained("gyann/pokemon-vit-full") | |
| model.config.id2label = {int(k): v for k, v in reference_config.id2label.items()} | |
| model.config.label2id = reference_config.label2id | |
| except Exception: | |
| pass | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| model.to(device) | |
| model.eval() | |
| return processor, model, device, None | |
| except Exception as e: | |
| return None, None, None, str(e) | |
| # 4. PokeAPI ๋ฐ์ดํฐ ํธ์ถ ํจ์ (์ด๋ฏธ์ง, ํ์ , ํค, ๋ชธ๋ฌด๊ฒ) | |
| def get_pokemon_data(pokemon_name): | |
| try: | |
| name_lower = pokemon_name.lower().replace(" ", "-").replace(".", "").replace("'", "") | |
| url = f"https://pokeapi.co/api/v2/pokemon/{name_lower}" | |
| response = requests.get(url) | |
| if response.status_code == 200: | |
| data = response.json() | |
| return { | |
| "artwork_url": data["sprites"]["other"]["official-artwork"]["front_default"], | |
| "types": [t["type"]["name"] for t in data["types"]], | |
| "height": data["height"] / 10.0, # meters | |
| "weight": data["weight"] / 10.0 # kg | |
| } | |
| return None | |
| except: | |
| return None | |
| MODEL_PATHS = { | |
| "ViT Full Fine-tuning": "gyann/pokemon-vit-full", | |
| "ViT + LoRA": "gyann/pokemon-vit-lora", | |
| "ViT + QLoRA (4-bit)": "gyann/pokemon-vit-qlora", | |
| "ResNet50": "gyann/pokemon-resnet50", | |
| "ConvNeXt": "gyann/pokemon-convnext", | |
| "Swin Transformer": "gyann/pokemon-swin" | |
| } | |
| # 5. ์ค์ ๋ฐ ์ ๋ ฅ ์์ญ (๊น๋ํ ์ปจํ ์ด๋ UI) | |
| with st.container(border=True): | |
| st.markdown("### โ๏ธ ๋ถ์ ์ค์ ") | |
| setting_col1, setting_col2 = st.columns([1, 2]) | |
| with setting_col1: | |
| compare_models = st.toggle("๋ชจ๋ธ ๋น๊ต ๋ชจ๋ ํ์ฑํ", value=False) | |
| st.caption(f"๐ ํ์ฌ ๊ฐ์ ์ฅ์น: **{'GPU (CUDA)' if torch.cuda.is_available() else 'CPU'}**") | |
| with setting_col2: | |
| if compare_models: | |
| col_a, col_b = st.columns(2) | |
| with col_a: | |
| model_name_a = st.selectbox("Model A", list(MODEL_PATHS.keys()), index=0) | |
| with col_b: | |
| model_name_b = st.selectbox("Model B", list(MODEL_PATHS.keys()), index=1) | |
| with st.spinner("๋ชจ๋ธ A ๋ก๋ฉ ์ค..."): | |
| processor_a, model_a, device_a, err_a = load_model(MODEL_PATHS[model_name_a]) | |
| with st.spinner("๋ชจ๋ธ B ๋ก๋ฉ ์ค..."): | |
| processor_b, model_b, device_b, err_b = load_model(MODEL_PATHS[model_name_b]) | |
| if err_a: st.error(f"Model A ๋ก๋ ์คํจ: {err_a}") | |
| if err_b: st.error(f"Model B ๋ก๋ ์คํจ: {err_b}") | |
| active_models = [ | |
| ("Model A", model_name_a, processor_a, model_a, device_a), | |
| ("Model B", model_name_b, processor_b, model_b, device_b) | |
| ] | |
| else: | |
| model_name = st.selectbox("๋ถ์์ ์ฌ์ฉํ ๋ชจ๋ธ", list(MODEL_PATHS.keys())) | |
| with st.spinner("๋ชจ๋ธ ๋ก๋ฉ ์ค..."): | |
| processor, model, device, err = load_model(MODEL_PATHS[model_name]) | |
| if err: st.error(f"๋ชจ๋ธ ๋ก๋ ์คํจ: {err}") | |
| active_models = [("Result", model_name, processor, model, device)] | |
| with st.container(border=True): | |
| st.markdown("### ๐ผ๏ธ ์ด๋ฏธ์ง ์ ํ") | |
| EXAMPLE_IMAGES = { | |
| "์ง์ ํ์ผ ์ ๋ก๋ํ๊ธฐ": None, | |
| "์์: ํผ์นด์ธ (Pikachu)": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png", | |
| "์์: ์ด์ํด์จ (Bulbasaur)": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/1.png", | |
| "์์: ๊ผฌ๋ถ๊ธฐ (Squirtle)": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/7.png", | |
| "์์: ํ์ด๋ฆฌ (Charmander)": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/4.png" | |
| } | |
| # ๋ผ๋์ค ๋ฒํผ์ ๊ฐ๋ก๋ก ๋ฐฐ์นํ์ฌ ๋ชจ๋ํ๊ฒ | |
| selected_example = st.radio("ํ ์คํธ ๋ฐฉ์์ ์ ํํ์ธ์", list(EXAMPLE_IMAGES.keys()), horizontal=True, label_visibility="collapsed") | |
| image_to_process = None | |
| if selected_example == "์ง์ ํ์ผ ์ ๋ก๋ํ๊ธฐ": | |
| uploaded_file = st.file_uploader("ํฌ์ผ๋ชฌ ์ด๋ฏธ์ง๋ฅผ ๋๋๊ทธ ์ค ๋๋กญํ์ธ์", type=["jpg", "jpeg", "png"], label_visibility="collapsed") | |
| if uploaded_file is not None: | |
| image_to_process = Image.open(uploaded_file).convert("RGB") | |
| else: | |
| example_url = EXAMPLE_IMAGES[selected_example] | |
| try: | |
| response = requests.get(example_url) | |
| if response.status_code == 200: | |
| image_to_process = Image.open(io.BytesIO(response.content)).convert("RGB") | |
| except Exception as e: | |
| st.error(f"์์ ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {e}") | |
| # 6. ์ถ๋ก ๋ฐ ๊ฒฐ๊ณผ ์ถ๋ ฅ ์ปดํฌ๋ํธ | |
| def predict_and_display(name_prefix, model_name, processor, model, device, image): | |
| if model is None: | |
| st.warning(f"๋ชจ๋ธ({model_name})์ด ์ ์์ ์ผ๋ก ๋ก๋๋์ง ์์์ต๋๋ค.") | |
| return | |
| inputs = processor(images=image, return_tensors="pt").to(device) | |
| with torch.no_grad(): | |
| outputs = model(**inputs) | |
| logits = outputs.logits | |
| probs = F.softmax(logits, dim=-1)[0] | |
| top5_prob, top5_catid = torch.topk(probs, 5) | |
| cat_id_0 = top5_catid[0].item() | |
| top1_label = model.config.id2label.get(cat_id_0, model.config.id2label.get(str(cat_id_0), "Unknown")) | |
| top1_score = top5_prob[0].item() | |
| poke_data = get_pokemon_data(top1_label) | |
| st.markdown(f"<h5 style='text-align: center; color: #777; margin-bottom: 0;'>{name_prefix}: {model_name}</h5>", unsafe_allow_html=True) | |
| # ๋ฉํธ๋ฆญ | |
| st.metric(label="๐ Top-1 Prediction", value=top1_label.title(), delta=f"{top1_score*100:.1f}% Confidence", delta_color="normal") | |
| tab1, tab2, tab3 = st.tabs(["๐ Overview", "๐ Stats", "๐ Top-5 Details"]) | |
| with tab1: | |
| if poke_data and poke_data["artwork_url"]: | |
| st.image(poke_data["artwork_url"], use_container_width=True) | |
| else: | |
| st.info("๊ณต์ ์ผ๋ฌ์คํธ๊ฐ ์์ต๋๋ค.") | |
| with tab2: | |
| if poke_data: | |
| st.markdown(f"**์์ฑ(Types)**: {', '.join(poke_data['types']).title()}") | |
| st.markdown(f"**์ ์ฅ(Height)**: {poke_data['height']} m") | |
| st.markdown(f"**์ฒด์ค(Weight)**: {poke_data['weight']} kg") | |
| else: | |
| st.warning("๋๊ฐ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.") | |
| with tab3: | |
| for i in range(top5_prob.size(0)): | |
| c_id = top5_catid[i].item() | |
| lbl = model.config.id2label.get(c_id, model.config.id2label.get(str(c_id), "Unknown")) | |
| scr = top5_prob[i].item() | |
| st.caption(f"**{i+1}. {lbl.title()}**") | |
| st.progress(scr, text=f"{scr * 100:.1f}%") | |
| # 7. ๋ฉ์ธ ์คํ ๋ถ | |
| if image_to_process is not None: | |
| st.markdown("---") | |
| with st.container(): | |
| if compare_models: | |
| col_img, col_res = st.columns([1, 2.5]) | |
| with col_img: | |
| st.markdown("### ๐ท ์ ๋ ฅ ์ด๋ฏธ์ง") | |
| st.image(image_to_process, use_container_width=True) | |
| with col_res: | |
| st.markdown("### โจ ๋ถ์ ๊ฒฐ๊ณผ") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| with st.container(border=True): | |
| m_prefix, m_name, m_proc, m_model, m_dev = active_models[0] | |
| with st.spinner('๋ถ์ ์ค...'): | |
| predict_and_display(m_prefix, m_name, m_proc, m_model, m_dev, image_to_process) | |
| with col2: | |
| with st.container(border=True): | |
| m_prefix, m_name, m_proc, m_model, m_dev = active_models[1] | |
| with st.spinner('๋ถ์ ์ค...'): | |
| predict_and_display(m_prefix, m_name, m_proc, m_model, m_dev, image_to_process) | |
| else: | |
| col1, col2 = st.columns([1, 1.5]) | |
| with col1: | |
| st.markdown("### ๐ท ์ ๋ ฅ ์ด๋ฏธ์ง") | |
| st.image(image_to_process, use_container_width=True) | |
| with col2: | |
| st.markdown("### โจ ๋ถ์ ๊ฒฐ๊ณผ") | |
| with st.container(border=True): | |
| m_prefix, m_name, m_proc, m_model, m_dev = active_models[0] | |
| with st.spinner('๋ถ์ ์ค...'): | |
| predict_and_display(m_prefix, m_name, m_proc, m_model, m_dev, image_to_process) | |
| # 8. ๋ชจ๋ธ ์ํคํ ์ฒ ์ ๋ณด UI (์ตํ๋จ ๋ฐฐ์น) | |
| ARCHITECTURE_INFO = { | |
| "ViT Full Fine-tuning": { | |
| "title": "Vision Transformer (ViT) - Full FT", | |
| "desc": "์ด๋ฏธ์ง๋ฅผ 16x16 ํฝ์ ํจ์น(Patch)๋ก ๋๋์ด ์ฒ๋ฆฌํฉ๋๋ค. ์ด๋ฏธ์ง ์ ์ฒด์ **์ ์ญ์ ๋ฌธ๋งฅ(Global Context)** ์ ํ์ ํ๋ ๋ฐ ๋ฐ์ด๋๋ฉฐ, ๋ชจ๋ ๊ฐ์ค์น๋ฅผ ์ฌํ์ตํ์ฌ ์ต๊ณ ์ฑ๋ฅ์ ๋์ถํ์ง๋ง ์ฐ์ฐ ๋น์ฉ์ด ๊ฐ์ฅ ํฝ๋๋ค." | |
| }, | |
| "ViT + LoRA": { | |
| "title": "ViT with LoRA (Low-Rank Adaptation)", | |
| "desc": "๊ฑฐ๋ํ ์๋ณธ ๋ชจ๋ธ์ ์ผ๋ ค๋๊ณ (Freeze), ํต์ฌ ์ฐ์ฐ์ธต์ ์์ฃผ ์์ **ํ์ต ๊ฐ๋ฅํ ์ฐํ๋ก(์ ๋ญํฌ ํ๋ ฌ)** ๋ฅผ ๋ง๋ถ์ ๋๋ค. ๋จ 1%์ ํ๋ผ๋ฏธํฐ๋ง ํ์ตํ์ฌ ๋น์ฉ์ ๊ทน์ ์ผ๋ก ๋ฎ์ถ๋ฉด์๋ Full FT์ ์ ์ฌํ ์ฑ๋ฅ์ ๋ ๋๋ค." | |
| }, | |
| "ViT + QLoRA (4-bit)": { | |
| "title": "ViT with QLoRA (Quantized LoRA)", | |
| "desc": "LoRA์์ ํ ๋ฐ ๋ ๋์๊ฐ, ์๋ณธ ๋ชจ๋ธ์ **4-bit ์ ๋ฐ๋** ๋ก ์์ถ(Quantization)ํ์ฌ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฌํฉ๋๋ค. VRAM์ด ๋งค์ฐ ๋ถ์กฑํ ํ๊ฒฝ์์๋ ๋๊ท๋ชจ ๋ชจ๋ธ์ ํ๋ํ ์ ์๊ฒ ํ๋ ์ต์ ํ ๊ธฐ๋ฒ์ ๋๋ค." | |
| }, | |
| "ResNet50": { | |
| "title": "ResNet50 (Baseline CNN)", | |
| "desc": "ํฉ์ฑ๊ณฑ(Convolution) ํํฐ๋ฅผ ๊ฒน์ณ ์ด๋ฏธ์ง์ **๊ตญ์์ ํจํด(Local Feature)** ์ ์ฐพ์๋ด๋ ์ ํต์ ๊ฐ์์ ๋๋ค. ์์ฐจ ์ฐ๊ฒฐ(Residual Connection)๋ก ๊น์ ์ ๊ฒฝ๋ง์ ํ์ต ์์ ์ฑ์ ๋ณด์ฅํฉ๋๋ค." | |
| }, | |
| "ConvNeXt": { | |
| "title": "ConvNeXt (Modernized CNN)", | |
| "desc": "ํธ๋์คํฌ๋จธ์ ์ค๊ณ ์ฒ ํ(ํฐ ์ปค๋, LayerNorm, GELU ๋ฑ)์ ์ญ์ผ๋ก CNN์ ๋์ ํ **'๋ชจ๋ CNN'** ์ ๋๋ค. CNN์ ์ง์ญ์ ๊ท๋ฉ์ ํธํฅ์ ์ ์งํ๋ฉด์๋ ํธ๋์คํฌ๋จธ๊ธ ์ฑ๋ฅ์ ๋ ๋๋ค." | |
| }, | |
| "Swin Transformer": { | |
| "title": "Swin Transformer (Hierarchical ViT)", | |
| "desc": "CNN์ฒ๋ผ ์์ ์์ญ(Window)๋ถํฐ ์ ์ ๋์ ์์ญ์ผ๋ก **๊ณ์ธต์ (Hierarchical)** ์ผ๋ก ๋ณํฉํ๋ฉฐ ํ์ตํ๋ ํธ๋์คํฌ๋จธ์ ๋๋ค. ViT๊ฐ ๋์น๊ธฐ ์ฌ์ด ๋ฏธ์ธํ ๋ํ ์ผ ๊ตฌ๋ถ์ ๊ฐํฉ๋๋ค." | |
| } | |
| } | |
| st.markdown("---") | |
| st.markdown("#### ๐ง ์ ํ๋ ์ํคํ ์ฒ ์์๋ณด๊ธฐ") | |
| if compare_models: | |
| col_info1, col_info2 = st.columns(2) | |
| with col_info1: | |
| with st.expander(f"{model_name_a} ๊ตฌ์กฐ", expanded=False): | |
| info = ARCHITECTURE_INFO.get(model_name_a, {"title": model_name_a, "desc": "์ค๋ช ์ด ์ค๋น๋์ง ์์์ต๋๋ค."}) | |
| st.markdown(f"**{info['title']}**\n\n{info['desc']}") | |
| with col_info2: | |
| with st.expander(f"{model_name_b} ๊ตฌ์กฐ", expanded=False): | |
| info = ARCHITECTURE_INFO.get(model_name_b, {"title": model_name_b, "desc": "์ค๋ช ์ด ์ค๋น๋์ง ์์์ต๋๋ค."}) | |
| st.markdown(f"**{info['title']}**\n\n{info['desc']}") | |
| else: | |
| with st.expander(f"{model_name} ๊ตฌ์กฐ", expanded=False): | |
| info = ARCHITECTURE_INFO.get(model_name, {"title": model_name, "desc": "์ค๋ช ์ด ์ค๋น๋์ง ์์์ต๋๋ค."}) | |
| st.markdown(f"**{info['title']}**\n\n{info['desc']}") |