2026gd / app.py
kdhs's picture
Update app.py
1168fe0 verified
Raw
History Blame Contribute Delete
9.53 kB
import gradio as gr
import torch
import torch.nn.functional as F
import os
import zipfile
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from peft import PeftModel
# ==========================================
# 1. Model Loading
# ==========================================
# [์ˆ˜์ •] LoRA ์–ด๋Œ‘ํ„ฐ(adapter_config.json)๋ฅผ ํ™•์ธํ•ด๋ณด๋ฉด ์‹ค์ œ base_model_name_or_path๋Š”
# "monologg/koelectra-small-v3-discriminator" ์ด๋ฉฐ, task_type๋„ "SEQ_CLS"(๋ถ„๋ฅ˜)์ž…๋‹ˆ๋‹ค.
# Qwen ์ƒ์„ฑํ˜• LLM์ด ์•„๋‹ˆ๋ผ KoELECTRA ๊ธฐ๋ฐ˜ "์ด์ง„ ๋ถ„๋ฅ˜(์ •์ƒ/์˜ค์ •๋ณด)" ๋ชจ๋ธ์ž…๋‹ˆ๋‹ค.
BASE_MODEL = "monologg/koelectra-small-v3-discriminator"
LORA_WEIGHTS = ""
NUM_LABELS = 2 # safetensors์˜ classifier.out_proj shape์ด (2, 256) -> ํด๋ž˜์Šค 2๊ฐœ
LABEL_MAP = {
0: "์‹ ๋ขฐ ๊ฐ€๋Šฅ (๊ธฐํ›„ ์˜ค์ •๋ณด๋กœ ํŒ๋‹จ๋˜์ง€ ์•Š์Œ)",
1: "๊ธฐํ›„ ์˜ค์ •๋ณด ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ (์ฃผ์˜ ํ•„์š”)"
}
print("Loading model and tokenizer...")
try:
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
base_model = AutoModelForSequenceClassification.from_pretrained(
BASE_MODEL,
num_labels=NUM_LABELS,
torch_dtype=torch.float32,
)
model = PeftModel.from_pretrained(base_model, LORA_WEIGHTS)
model.eval()
MODEL_LOADED = True
print("LoRA Model successfully loaded!")
except Exception as e:
print(f"Warning: Model loading failed ({e}). Running in Simulation Mode.")
MODEL_LOADED = False
# ==========================================
# 2. Pipeline Algorithm & Inference Engine
# ==========================================
def run_pipeline(user_input):
"""
1. Text Preprocessing
2. Tokenizing
3. LoRA Model Inference (๋ถ„๋ฅ˜)
4. XAI / Decision Guide Extraction
5. Attention / Keyword Map Analysis
6. Output Generation & Limitations
"""
if not user_input.strip():
return "์ž…๋ ฅ ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", "N/A", "N/A", "N/A"
# Step 1: Preprocessing
cleaned_input = user_input.strip()
if MODEL_LOADED:
# Step 2: Tokenizing
inputs = tokenizer(cleaned_input, return_tensors="pt", truncation=True, max_length=512)
# Step 3: LoRA Model Inference (+ attention ์ถœ๋ ฅ ์š”์ฒญ)
with torch.no_grad():
outputs = model(**inputs, output_attentions=True)
logits = outputs.logits
probs = F.softmax(logits, dim=-1)[0]
pred_id = int(torch.argmax(probs).item())
confidence = float(probs[pred_id].item())
generated_text = (
f"[๋ถ„์„ ๊ฒฐ๊ณผ]\n"
f"ํŒ๋‹จ: {LABEL_MAP.get(pred_id, f'ํด๋ž˜์Šค {pred_id}')}\n"
f"์‹ ๋ขฐ๋„(ํ™•๋ฅ ): {confidence * 100:.1f}%\n\n"
f"ํด๋ž˜์Šค๋ณ„ ํ™•๋ฅ  -> ์ •์ƒ: {probs[0]*100:.1f}% / ์˜ค์ •๋ณด ์˜์‹ฌ: {probs[1]*100:.1f}%"
)
# Step 4: Decision Guide (xAI) - ์‹ค์ œ ํ™•๋ฅ ๊ฐ’ ๊ธฐ๋ฐ˜
decision_guide = (
"๐Ÿ” **xAI ํŒ๋‹จ ๊ฐ€์ด๋“œ ์ถ”์ถœ**\n"
f"- ๋ชจ๋ธ ์˜ˆ์ธก ํด๋ž˜์Šค: {pred_id} ({LABEL_MAP.get(pred_id, '')})\n"
f"- ์˜ˆ์ธก ์‹ ๋ขฐ๋„: {confidence * 100:.1f}%\n"
f"- ํด๋ž˜์Šค ํ™•๋ฅ  ๋ถ„ํฌ: ์ •์ƒ {probs[0]*100:.1f}% / ์˜ค์ •๋ณด ์˜์‹ฌ {probs[1]*100:.1f}%"
)
# Step 5: Attention Map Analysis - ์‹ค์ œ ์–ดํ…์…˜ ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ (CLS -> ๊ฐ ํ† ํฐ)
try:
# ๋งˆ์ง€๋ง‰ ๋ ˆ์ด์–ด์˜ ๋ชจ๋“  ํ—ค๋“œ ํ‰๊ท , [CLS] ํ† ํฐ์ด ๊ฐ ํ† ํฐ์— ์ค€ ๊ฐ€์ค‘์น˜ ์‚ฌ์šฉ
last_layer_attn = outputs.attentions[-1][0] # (num_heads, seq_len, seq_len)
cls_attn = last_layer_attn.mean(dim=0)[0] # (seq_len,) CLS -> ๊ฐ ํ† ํฐ
tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])
scored = [
(tok, float(w)) for tok, w in zip(tokens, cls_attn.tolist())
if tok not in tokenizer.all_special_tokens
]
scored.sort(key=lambda x: x[1], reverse=True)
top_k = scored[:5]
lines = "\n".join(f"{i+1}. '{tok}' -> ๊ฐ€์ค‘์น˜ {w:.3f}" for i, (tok, w) in enumerate(top_k))
attention_analysis = "๐Ÿ“Š **์–ดํ…์…˜ ๋งต ํ•ต์‹ฌ ๊ฐ€์ค‘์น˜ (Attention Weights)**\n" + (lines or "ํ‘œ์‹œํ•  ํ† ํฐ์ด ์—†์Šต๋‹ˆ๋‹ค.")
except Exception as e:
attention_analysis = f"๐Ÿ“Š ์–ดํ…์…˜ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}"
else:
# ๋ชจ๋ธ ๋กœ๋”ฉ ์ž์ฒด๊ฐ€ ์‹คํŒจํ–ˆ์„ ๋•Œ๋งŒ ์‚ฌ์šฉ๋˜๋Š” ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ถœ๋ ฅ
generated_text = (
"[์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๊ฒฐ๊ณผ - ๋ชจ๋ธ ๋ฏธ๋กœ๋”ฉ]\n"
"ํ˜„์žฌ ์„œ๋ฒ„์— LoRA ๊ฐ€์ค‘์น˜ ๋˜๋Š” base ๋ชจ๋ธ์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ•ด ์˜ˆ์‹œ ์ถœ๋ ฅ์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.\n"
"requirements.txt ๋ฐ ๋ชจ๋ธ ๊ฒฝ๋กœ ์„ค์ •์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”."
)
decision_guide = "๐Ÿ” ๋ชจ๋ธ์ด ๋กœ๋”ฉ๋˜์ง€ ์•Š์•„ xAI ๊ฐ€์ด๋“œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
attention_analysis = "๐Ÿ“Š ๋ชจ๋ธ์ด ๋กœ๋”ฉ๋˜์ง€ ์•Š์•„ ์–ดํ…์…˜ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
# Step 6: Disclaimer & Limitations
disclaimer = (
"โš ๏ธ **Model Card ํ•œ๊ณ„ ๊ณ ์ง€ ๋ฐ ์‚ฌ์ „ ์•ˆ๋‚ด (Disclaimer)**\n"
"โ€ข **์ฑ…์ž„ ์„ ์–ธ**: ๋ณธ ๋ชจ๋ธ(Team 3, 2026)์€ ๊ธฐํ›„๋ณ€ํ™” ์˜ค์ •๋ณด ํƒ์ง€ ๋ชฉ์ ์œผ๋กœ ๊ฐœ๋ฐœ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.\n"
"โ€ข **์˜๋„๋œ ์‚ฌ์šฉ**: ์ผ๋ฐ˜์ ์ธ ๊ธฐํ›„ ์ •๋ณด ํƒ์ƒ‰ ๋ฐ ๊ต์–‘ ํŒ๋‹จ ์ฐธ๊ณ ์šฉ.\n"
"โ€ข **์‚ฌ์šฉ ๊ธˆ์ง€**: ์˜ํ•™์  ํŒ๋‹จ, ๋ฒ•์  ๊ทœ์ œ ๊ทผ๊ฑฐ, ์ž๋™ ์ฐจ๋‹จ/์ œ์žฌ ์‹œ์Šคํ…œ์˜ ๋‹จ๋… ๊ทผ๊ฑฐ๋กœ ์‚ฌ์šฉ ๋ถˆ๊ฐ€.\n"
"โ€ข **์•ฝ์  ๋ฐ ํ•œ๊ณ„**: ๊ณต์‹ ๋ ฅ ์žˆ๋Š” ๊ธฐ๊ด€์˜ ํ•™์ˆ  ์ž๋ฃŒ๊ฐ€ ์•„๋‹Œ ์†Œ์…œ ๋ฏธ๋””์–ด ํŠน์œ ์˜ ์‹ ์กฐ์–ด, ๊ทน๋‹จ์  ๋น„์œ , ํ…์ŠคํŠธ ํ˜•ํƒœ๊ฐ€ ์™œ๊ณก๋œ ์กฐ๊ฑด์—์„œ๋Š” ์˜คํƒ๋ฅ ์ด ์ƒ์Šนํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.\n"
"โ€ข **์ด์˜ ์ œ๊ธฐ**: ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ์ด์˜ ์‹ ์ฒญ ๋ฐ ์˜ค๋ฅ˜ ์ œ๋ณด๋Š” ๊ฐœ๋ฐœํŒ€(Team 3) ํ†ต๋กœ๋ฅผ ์ด์šฉํ•ด ์ฃผ์„ธ์š”."
)
return generated_text, decision_guide, attention_analysis, disclaimer
# ==========================================
# 3. Gradio Web Interface
# ==========================================
theme = gr.themes.Soft(
primary_hue="teal",
secondary_hue="slate",
)
custom_css = """
.model-card-box {
background-color: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
.disclaimer-box {
background-color: #fffbe2;
border-left: 4px solid #f59e0b;
padding: 12px;
border-radius: 4px;
margin-top: 10px;
}
"""
with gr.Blocks(theme=theme, css=custom_css, title="๊ธฐํ›„๋ณ€ํ™” ์˜ค์ •๋ณด ํƒ์ง€ AI ๋ชจ๋ธ") as demo:
gr.Markdown(
"""
# ๐ŸŒ ๊ธฐํ›„๋ณ€ํ™” ์˜ค์ •๋ณด ํƒ์ง€ AI ๋ชจ๋ธ (Climate Misinfo Detector)
**Team 3 | ์ฑ…์ž„์•ˆ์ „ AI ํ”„๋กœ์ ํŠธ (High School Module 3-9)**
LoRA ์–ด๋Œ‘ํ„ฐ ๊ฐ€์ค‘์น˜๊ฐ€ ์ ์šฉ๋œ KoELECTRA ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜ AI ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์›น ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค.
"""
)
with gr.Row():
# Left Column
with gr.Column(scale=1):
gr.Markdown("### ๐Ÿ“ฅ ์‚ฌ์šฉ์ž ์ž…๋ ฅ (Input)")
user_input = gr.Textbox(
lines=5,
placeholder="๊ฒ€์ฆํ•˜๊ณ  ์‹ถ์€ ๊ธฐํ›„๋ณ€ํ™” ๊ด€๋ จ ๋ฌธ์žฅ์ด๋‚˜ ์ฃผ์žฅ์„ ์ž…๋ ฅํ•˜์„ธ์š”...\n ์˜ˆ: ์ง€๊ตฌ์˜จ๋‚œํ™”๋Š” ์ธ๊ฐ„ ํ™œ๋™ ๋•Œ๋ฌธ์ด ์•„๋‹ˆ๋ผ ์ž์—ฐ์ ์ธ ์ฃผ๊ธฐ์ผ ๋ฟ์ด๋‹ค.",
label="์ž…๋ ฅ ํ…์ŠคํŠธ (Text Preprocessing & Tokenizing)"
)
submit_btn = gr.Button("๐Ÿš€ AI ์ถ”๋ก  ๋ฐ ๋ถ„์„ ์‹คํ–‰", variant="primary")
gr.Markdown("### ๐Ÿ“„ Model Card ๊ฐœ์š”")
gr.Markdown(
"""
<div class="model-card-box">
<b>โ€ข ๋ชจ๋ธ๋ช…:</b> Team 3 LoRA Climate Misinfo LLM (KoELECTRA-small-v3 ๊ธฐ๋ฐ˜)<br>
<b>โ€ข ํ•™์Šต ๋ฐ์ดํ„ฐ:</b> ๊ธฐํ›„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (2024~2026, 200๊ฑด ๊ฒ€์ˆ˜์™„๋ฃŒ)<br>
<b>โ€ข ์„ฑ๋Šฅ ์ง€ํ‘œ:</b> Acc 85%, F1 0.82 (TP:100, TN:100, FP:10, FN:20)<br>
<b>โ€ข ์œค๋ฆฌ ์ฒดํฌ:</b> ์˜ํ•™/๋ฒ•๋ฅ  ์ž๋™ ํŒ๋‹จ ๊ธˆ์ง€
</div>
"""
)
# Right Column
with gr.Column(scale=1):
gr.Markdown("### ๐Ÿ“ค ์ถ”๋ก  ๋ฐ XAI ๋ถ„์„ ๊ฒฐ๊ณผ (Output)")
output_text = gr.Textbox(
label="์ตœ์ข… ํŒ๋ณ„ ๊ฒฐ๊ณผ (Output Generation)",
lines=6,
interactive=False
)
with gr.Tabs():
with gr.TabItem("๐Ÿ” ํŒ๋‹จ ๊ฐ€์ด๋“œ (xAI)"):
xai_output = gr.Markdown("๋ถ„์„ ์‹คํ–‰ ํ›„ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.")
with gr.TabItem("๐Ÿ“Š ์–ดํ…์…˜ ๋งต ๋ถ„์„"):
attention_output = gr.Markdown("๋ถ„์„ ์‹คํ–‰ ํ›„ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.")
gr.Markdown("### โš ๏ธ ํ•œ๊ณ„ ๊ณ ์ง€ ๋ฐ ์ฑ…์ž„ ์„ ์–ธ (Model Card Section 5 & 6)")
disclaimer_output = gr.Markdown(
"""
<div class="disclaimer-box">
<b>โ€ข ํ•œ๊ณ„ ์‚ฌ์ „ ๊ณ ์ง€:</b> ๋ณธ ๋ชจ๋ธ์€ ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ์˜คํ”ˆ ์ƒํ™ฉ ๋ฐ ์™œ๊ณก ๋ฌธ๋งฅ์—์„œ ์˜คํƒ ์œ„ํ—˜์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.<br>
<b>โ€ข ์ด์˜ ์ œ๊ธฐ ํ†ต๋กœ:</b> ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋ฌธ์˜ ๋ฐ ํ”ผ๋“œ๋ฐฑ์€ ๊ณต์‹ ๋ชจ๋“ˆ 3-9 ์ด์˜์ œ๊ธฐ ์ฐฝ๊ตฌ๋ฅผ ํ†ตํ•ด ์ ‘์ˆ˜ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
</div>
""",
elem_classes=["disclaimer-box"]
)
submit_btn.click(
fn=run_pipeline,
inputs=[user_input],
outputs=[output_text, xai_output, attention_output, disclaimer_output]
)
if __name__ == "__main__":
demo.launch()