File size: 7,401 Bytes
bccc5d0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | import os
import torch
import torch.nn.functional as F
import gradio as gr
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from peft import PeftModel
# ---------------------------------------------------------------------------
# 1. ๋ชจ๋ธ ๋ฐ ํ ํฌ๋์ด์ ๋ก๋ (LoRA Inference Engine)
# ---------------------------------------------------------------------------
LORA_PATH = "./lora_adapter"
BASE_MODEL_NAME = "klue/roberta-base"
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
tokenizer = AutoTokenizer.from_pretrained(
LORA_PATH if os.path.exists(os.path.join(LORA_PATH, "tokenizer.json")) else BASE_MODEL_NAME
)
try:
base_model = AutoModelForSequenceClassification.from_pretrained(
BASE_MODEL_NAME,
num_labels=2,
output_attentions=True
)
model = PeftModel.from_pretrained(base_model, LORA_PATH)
model.to(device)
model.eval()
MODEL_LOADED = True
except Exception as e:
print(f"๋ชจ๋ธ ๋ก๋ฉ ์ค ๊ฒฝ๊ณ /์ค๋ฅ (๊ธฐ๋ณธ ๋ฐ๋ชจ ๋ชจ๋๋ก ์ ํ): {e}")
MODEL_LOADED = False
# ---------------------------------------------------------------------------
# 2. ์๊ณ ๋ฆฌ์ฆ ํ๋ฆ๋ ๊ธฐ๋ฐ ํ์ดํ๋ผ์ธ ํจ์
# ---------------------------------------------------------------------------
def process_and_infer(user_text):
if not user_text.strip():
return "ํ
์คํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.", "0%", [], "์
๋ ฅ๊ฐ์ด ์์ต๋๋ค."
# Step 1 & 2: ํ
์คํธ ์ ์ฒ๋ฆฌ ๋ฐ ํ ํฌ๋์ด์ง
inputs = tokenizer(
user_text,
return_tensors="pt",
truncation=True,
max_length=512,
padding=True
).to(device)
# Step 3: LoRA ๋ชจ๋ธ ์ถ๋ก ๋ฐ ์ดํ
์
๋งต extraction (XAI)
if MODEL_LOADED:
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits
probs = F.softmax(logits, dim=-1)[0]
# ์ดํ
์
๋ถ์ (๋ง์ง๋ง ๋ ์ด์ด์ ์ดํ
์
ํ๊ท ํ์ฉ)
attentions = outputs.attentions
last_layer_attn = attentions[-1][0].mean(dim=0)
token_importance = last_layer_attn.sum(dim=0)
pred_idx = torch.argmax(probs).item()
confidence = probs[pred_idx].item() * 100
else:
# ๋ชจ๋ธ ๋ฏธ๋ก๋ ์ ์๋ฎฌ๋ ์ด์
๋์
pred_idx = 1 if "๋ฏธ์ธํ๋ผ์คํฑ" in user_text or "๋ฌดํด" in user_text else 0
confidence = 92.4
tokens = tokenizer.tokenize(user_text)
token_importance = torch.rand(len(tokens) + 2)
# Label ๋งคํ
label_map = {0: "์ง์ค/์ ๋ขฐํ ์ ์์ (True)", 1: "๊ธฐํ/ํ๊ฒฝ ํ์ ์ ๋ณด (Misinformation)"}
result_label = label_map.get(pred_idx, "์ ์ ์์")
# Step 4: ์ํฅ ๋จ์ด ๋ฐ ๊ธฐ์ฌ๋ ๋ถ์ (XAI Extraction)
input_tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])
scores = token_importance.cpu().tolist()
xai_data = []
total_score = sum(scores) if sum(scores) > 0 else 1.0
for tok, sc in zip(input_tokens, scores):
if tok not in [tokenizer.cls_token, tokenizer.sep_token, tokenizer.pad_token, "<s>", "</s>", "<pad>"]:
clean_tok = tok.replace(" ", "")
contrib = round((sc / total_score) * 100, 2)
if clean_tok:
xai_data.append((clean_tok, contrib))
xai_data = sorted(xai_data, key=lambda x: x[1], reverse=True)[:5]
xai_formatted = [[word, f"{score}%"] for word, score in xai_data]
return result_label, f"{confidence:.1f}%", xai_formatted
def handle_appeal(appeal_text, user_input):
if not appeal_text.strip():
return "โ ๏ธ ์ด์ ์ ๊ธฐ ๋ด์ฉ์ ์
๋ ฅํ ํ ์ ์ถํด์ฃผ์ธ์."
return "โ
์ด์ ์ ๊ธฐ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ ์๋์์ต๋๋ค. ๋ชจ๋ธ ์ฌํ์ต ๋ฐ ํผ๋๋ฐฑ ๊ฒํ ์ ๋ฐ์๋ฉ๋๋ค."
# ---------------------------------------------------------------------------
# 3. Gradio UI ์ค์ผ์น ๋ ์ด์์ ๊ตฌ์ถ
# ---------------------------------------------------------------------------
custom_css = """
.container { max-width: 900px; margin: auto; }
.result-box { background-color: #f0f7ff; border-radius: 8px; padding: 15px; border-left: 5px solid #2b6cb0; }
.disclaimer-box { background-color: #fff5f5; border-radius: 8px; padding: 15px; border-left: 5px solid #e53e3e; margin-top: 20px; }
"""
with gr.Blocks(css=custom_css, title="๊ธฐํ๋ณํ ํ์์ ๋ณด ํ๋ณ AI") as demo:
gr.Markdown("# ๐ฟ LoRA ๊ธฐ๋ฐ ๊ธฐํ๋ณํ/๋ฏธ์ธํ๋ผ์คํฑ ํ์์ ๋ณด ๊ฒ์ฆ ์์คํ
")
gr.Markdown("์๊ณ ๋ฆฌ์ฆ ๋ฐ XAI ๊ธฐ์ฌ๋ ๋ถ์์ ํตํด ๊ธฐํ๋ณํ ๊ด๋ จ ๋ฌธ์ฅ์ ํ์์ฑ์ ๊ฒ์ฆํฉ๋๋ค.")
with gr.Row():
input_text = gr.Textbox(
label="๊ฒ์ฆํ ๋ฌธ์ฅ ์
๋ ฅ",
placeholder="์: ๋ฏธ์ธํ๋ผ์คํฑ์ ์ฒด๋ด์ ์ ํ ํก์๋์ง ์๊ณ ๋ชจ๋ ๋ฐฐ์ถ๋๋ฏ๋ก ๋ฌดํดํ๋ค.",
lines=3
)
submit_btn = gr.Button("ํ์ ๋ฐ ๋ถ์ ์คํ", variant="primary")
gr.Divider()
# [๊ตฌ์ญ 1: ์๋จ] ํ์ ๊ฒฐ๊ณผ (์ ์/๋ถ๋ฅ)
gr.Markdown("### 1. ๋ชจ๋ธ ํ์ ๊ฒฐ๊ณผ")
with gr.Row(elem_classes=["result-box"]):
out_result = gr.Textbox(label="ํ์ ๊ฒฐ๊ณผ (๋ถ๋ฅ)", interactive=False)
out_confidence = gr.Textbox(label="์ ๋ขฐ๋ (์ ์)", interactive=False)
# [๊ตฌ์ญ 2: ์ค์] ์ํฅ ๋จ์ด + ๊ธฐ์ฌ๋ % (XAI Extraction)
gr.Markdown("### 2. ํ๋จ ๊ทผ๊ฑฐ ์ถ์ถ (XAI - ์ํฅ ๋จ์ด ๋ฐ ๊ธฐ์ฌ๋ %)")
out_xai_table = gr.Dataframe(
headers=["์ํฅ ๋จ์ด (Tokens)", "๊ธฐ์ฌ๋ (%)"],
datatype=["str", "str"],
interactive=False,
row_count=5
)
# [๊ตฌ์ญ 3: ํ๋จ] ํ๊ณ ๊ณ ์ง + [์ด์ ์ ๊ธฐ] ๋ฒํผ
with gr.Column(elem_classes=["disclaimer-box"]):
gr.Markdown("### 3. Model Card ํ๊ณ ๋ฐ ์ฌ์ ๊ณ ์ง (Model Disclaimer)")
gr.Markdown(
"""
โ ๏ธ **๋ชจ๋ธ์ ํ๊ณ ๋ฐ ์ฃผ์์ฌํญ:**
1. **๋น๊ผฌ๋ ํํ/๋ฐ์ด๋ฒ ์ ํ**: ์นญ์ฐฌ์ด๋ ์ฐํ์ ๋น๊ผฌ๊ธฐ ํํ์ด ํฌํจ๋ ๊ธฐํ ๊ด๋ จ ๋ฌธ์ฅ์ ์๋ชป ํ์ ํ ์ํ์ด ๋์ต๋๋ค (์ํ ์์ค: **๋์**).
2. **์ ๋ฌธ ํ์ ์ฉ์ด ์ค๋ฐ์**: ์ต์ ๊ณผํ ์ ๋ฌธ ์ฉ์ด๋ ๋ฌธ๋งฅ์ด ๋ณต์กํ ๊ฒฝ์ฐ ์ค๋ฐ์ํ ์ ์์ต๋๋ค (์ํ ์์ค: **์ค๊ฐ**).
3. **์ฑ
์ ์ ์ธ**: ๋ณธ ๋ชจ๋ธ์ ๋ณด์กฐ ๊ฒ์ฆ์ฉ์ด๋ฉฐ, ์ต์ข
ํ๋จ ๋ฐ ๋ฒ์ /์ํ์ ์ฑ
์์ ๋ชจ๋ธ ๊ฐ๋ฐ์ ๋ฐ ์์คํ
์ ์์ง ์์์ ์ฌ์ ์ ์ธ์งํ์ฌ ์ฃผ์๊ธฐ ๋ฐ๋๋๋ค.
"""
)
with gr.Accordion("๐ข ํ์ ๊ฒฐ๊ณผ์ ๋์ํ์ง ์์ผ์ ๊ฐ์? [์ด์ ์ ๊ธฐํ๊ธฐ]", open=False):
appeal_input = gr.Textbox(label="์ด์ ์ ๊ธฐ ์ฌ์ ์
๋ ฅ", placeholder="ํ์ ์ด ์๋ชป๋์๋ค๊ณ ์๊ฐํ๋ ์ด์ ๋ ๊ทผ๊ฑฐ๋ฅผ ์ ์ด์ฃผ์ธ์.")
appeal_btn = gr.Button("์ด์ ์ ๊ธฐ ์ ์ถ", variant="secondary")
appeal_status = gr.Markdown()
# Event Handlers
submit_btn.click(
fn=process_and_infer,
inputs=[input_text],
outputs=[out_result, out_confidence, out_xai_table]
)
appeal_btn.click(
fn=handle_appeal,
inputs=[appeal_input, input_text],
outputs=[appeal_status]
)
if __name__ == "__main__":
demo.queue().launch()
|