| import os |
| import torch |
| import torch.nn.functional as F |
| import gradio as gr |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification |
| from peft import PeftModel |
|
|
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| def process_and_infer(user_text): |
| if not user_text.strip(): |
| return "ํ
์คํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.", "0%", [], "์
๋ ฅ๊ฐ์ด ์์ต๋๋ค." |
|
|
| |
| inputs = tokenizer( |
| user_text, |
| return_tensors="pt", |
| truncation=True, |
| max_length=512, |
| padding=True |
| ).to(device) |
|
|
| |
| 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_map = {0: "์ง์ค/์ ๋ขฐํ ์ ์์ (True)", 1: "๊ธฐํ/ํ๊ฒฝ ํ์ ์ ๋ณด (Misinformation)"} |
| result_label = label_map.get(pred_idx, "์ ์ ์์") |
| |
| |
| 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 "โ
์ด์ ์ ๊ธฐ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ ์๋์์ต๋๋ค. ๋ชจ๋ธ ์ฌํ์ต ๋ฐ ํผ๋๋ฐฑ ๊ฒํ ์ ๋ฐ์๋ฉ๋๋ค." |
|
|
| |
| |
| |
| 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() |
| |
| |
| gr.Markdown("### 1. ๋ชจ๋ธ ํ์ ๊ฒฐ๊ณผ") |
| with gr.Row(elem_classes=["result-box"]): |
| out_result = gr.Textbox(label="ํ์ ๊ฒฐ๊ณผ (๋ถ๋ฅ)", interactive=False) |
| out_confidence = gr.Textbox(label="์ ๋ขฐ๋ (์ ์)", interactive=False) |
| |
| |
| gr.Markdown("### 2. ํ๋จ ๊ทผ๊ฑฐ ์ถ์ถ (XAI - ์ํฅ ๋จ์ด ๋ฐ ๊ธฐ์ฌ๋ %)") |
| out_xai_table = gr.Dataframe( |
| headers=["์ํฅ ๋จ์ด (Tokens)", "๊ธฐ์ฌ๋ (%)"], |
| datatype=["str", "str"], |
| interactive=False, |
| row_count=5 |
| ) |
|
|
| |
| 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() |
|
|
| |
| 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() |
|
|