File size: 9,519 Bytes
1f97a3a | 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 169 170 171 172 173 174 175 176 177 178 179 180 181 | import os
import re
import torch
import gradio as gr
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from peft import PeftModel
# 1. LoRA ์ด๋ํฐ์ ์ค์ ๋ฒ ์ด์ค ๋ชจ๋ธ ID๋ก ์์
BASE_MODEL_ID = "monologg/koelectra-small-v3-discriminator"
LORA_PATH = "./lora_climate_misinfo"
device = "cuda" if torch.cuda.is_available() else "cpu"
tokenizer = None
model = None
def load_model_and_tokenizer():
global tokenizer, model
try:
# ํ ํฌ๋์ด์ ๋ก๋ (๋ก์ปฌ ์ด๋ํฐ ๊ฒฝ๋ก ์ฐ์ )
tokenizer_path = LORA_PATH if os.path.exists(LORA_PATH) else BASE_MODEL_ID
tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
# KoELECTRA ๋ฒ ์ด์ค ๋ชจ๋ธ ๋ฐ LoRA ์ด๋ํฐ ๊ฒฐํฉ
base_model = AutoModelForSequenceClassification.from_pretrained(
BASE_MODEL_ID,
num_labels=2,
output_attentions=True
)
if os.path.exists(LORA_PATH):
model = PeftModel.from_pretrained(base_model, LORA_PATH)
else:
model = base_model
model.to(device)
model.eval()
print("โ
KoELECTRA + LoRA ๋ชจ๋ธ ๋ก๋ ์ฑ๊ณต")
except Exception as e:
print(f"โ ๏ธ ๋ชจ๋ธ ๋ก๋ ์ค ์ค๋ฅ ๋ฐ์: {e}")
model = None
load_model_and_tokenizer()
def preprocess_text(text):
text = text.strip()
text = re.sub(r'\s+', ' ', text)
return text
def analyze_climate_text(user_input):
if not user_input or not user_input.strip():
return (
'<div style="background-color: #f8d7da; color: #721c24; padding: 15px; border-radius: 8px; font-weight: bold;">โ ๏ธ ๋ถ์ํ ํ
์คํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.</div>',
"",
"์ํ๋ ์ ์: 0.0000%",
"ํ์-๋ฐ์ด๋ฒ ํ
์คํธ ์ ํ๋ ๋ฎ์!"
)
cleaned_input = preprocess_text(user_input)
fake_prob = 0.0007
is_misinfo = False
# 2. ๋ชจ๋ธ ์ถ๋ก
if model is not None and tokenizer is not None:
try:
inputs = tokenizer(cleaned_input, return_tensors="pt", truncation=True, max_length=512).to(device)
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits
probs = torch.softmax(logits, dim=-1)[0]
# Index 1: ์ค์ ๋ณด/์ํ ํ๋ฅ
fake_prob = probs[1].item() * 100
if fake_prob > 50.0:
is_misinfo = True
except Exception as e:
print(f"์ถ๋ก ์ค๋ฅ: {e}")
else:
# ๊ฐ์ค์น ๋ฏธ๋ก๋ ์ ์ ๊ตํ ํค์๋ ๋ฃฐ์
(์คํ ๋ฐฉ์ง)
strong_misinfo_keywords = ["์ง๊ตฌ์จ๋ํ๋ ๊ฑฐ์ง", "๊ธฐํ๋ณํ ์๋ชจ๋ก ", "๊ฐ์ง๋ด์ค ์กฐ์", "๋นํ๊ธฐ๊ฐ ์ค๊ณ ์๋ค"]
if any(kw in cleaned_input for kw in strong_misinfo_keywords):
fake_prob = 89.1234
is_misinfo = True
else:
fake_prob = 0.0007
is_misinfo = False
# 3. xAI ์ดํ
์
ํ์ด๋ผ์ดํธ ์์ฑ
words = cleaned_input.split()
highlighted_spans = []
target_keywords = ["๊ธฐํ๋ณํ", "์๋ชจ", "์ง๊ตฌ์จ๋ํ", "๊ฑฐ์ง", "๊ณผํ์", "์คํ", "๋ฐฑ์ ", "๋ถ์์ฉ"]
for w in words:
is_target = any(tk in w for tk in target_keywords)
if is_target:
score = 0.85 if is_misinfo else 0.25
else:
score = 0.05
highlighted_spans.append((w + " ", score))
# 4. UI ์ค์ผ์น ์ํ ๋ฐฐ์ง
if is_misinfo:
badge_html = '''
<div style="display: inline-flex; align-items: center; gap: 8px; background-color: #fee2e2; border: 2px solid #ef4444; color: #991b1b; padding: 8px 16px; border-radius: 20px; font-weight: bold; font-size: 1.1em;">
<span style="width: 14px; height: 14px; background-color: #ef4444; border-radius: 50%; display: inline-block;"></span>
<span>ํ์ ๊ฒฐ๊ณผ: ์ค์ ๋ณด ์์ฌ ๊ฒฝ๊ณ </span>
</div>
'''
else:
badge_html = '''
<div style="display: inline-flex; align-items: center; gap: 8px; background-color: #e0f2fe; border: 2px solid #0284c7; color: #075985; padding: 8px 16px; border-radius: 20px; font-weight: bold; font-size: 1.1em;">
<span style="width: 14px; height: 14px; background-color: #0284c7; border-radius: 50%; display: inline-block;"></span>
<span style="background-color: #f3e8ff; color: #6b21a8; padding: 2px 8px; border-radius: 12px; font-size: 0.9em;">Active Dolphin</span>
<span>ํ์ ๊ฒฐ๊ณผ: ์ ์ / ์ ๋ขฐ ๊ธฐ์ฌ</span>
</div>
'''
risk_score_text = f"์ํ๋ ์ ์ +{fake_prob:.4f}%"
limitation_warning = "โ ๏ธ [ํ๊ณ ๊ณ ์ง] ํ์ยท๋ฐ์ด๋ฒ ํ
์คํธ ์ ํ๋ ๋ฎ์! (๊ณต์ ๋ ฅ ์๋ ๊ธฐ๊ด์ ์คํ ๊ฐ๋ฅ์ฑ ์กด์ฌ)"
return badge_html, highlighted_spans, risk_score_text, limitation_warning
def file_appeal(reason, email):
if not reason or not email:
return "โ ๏ธ ์ด์ ์ ๊ธฐ ์ฌ์ ์ ์ฐ๋ฝ๋ฐ์ ๊ฐ๋ฐ์ Email์ ์
๋ ฅํด์ฃผ์ธ์."
return f"โ
์ด์ ์ ๊ธฐ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ ์๋์์ต๋๋ค. (์ ์ ๋ฉ์ผ: {email})\n๋ด๋น์(Team 3) ๊ฒํ ํ ๋ต๋ณ ๋๋ฆฌ๊ฒ ์ต๋๋ค."
css = """
.main-container { max-width: 900px; margin: 0 auto; font-family: 'Pretendard', sans-serif; }
.sketch-card { border: 2px solid #333; border-radius: 16px; padding: 20px; background: #fff; box-shadow: 4px 4px 0px #333; margin-bottom: 20px; }
.highlight-title { text-align: center; font-size: 1.2em; font-weight: bold; border: 2px solid #333; border-radius: 20px; width: fit-content; padding: 4px 20px; margin: 0 auto 15px auto; background: #fff; }
.disclaimer-box { border: 2px solid #eab308; background: #fefce8; color: #854d0e; padding: 12px 16px; border-radius: 12px; font-weight: bold; font-size: 0.95em; }
"""
with gr.Blocks(css=css, title="์ฑ
์์์ AI - ๊ธฐํ ์ค์ ๋ณด ๊ฐ์ง๊ธฐ") as demo:
gr.Markdown("# ๐ ์ฑ
์์์ AI: ๊ธฐํ ์ค์ ๋ณด ๊ฐ์ง ๋ฐ xAI ๋ถ์ ์์คํ
\n**Team 3 | ๊ฐ๋ฐ์ผ์: 2026-08-13 | LoRA Inference Engine ๊ธฐ๋ฐ**")
with gr.Tabs():
with gr.TabItem("๐ AI ์ค์ ๋ณด ๊ฐ์ง๊ธฐ (Inference UI)"):
with gr.Column(elem_classes=["main-container"]):
input_text = gr.Textbox(label="๋ด์ค ๊ธฐ์ฌ ๋๋ ๊ธฐํ ๊ด๋ จ ํ
์คํธ ์
๋ ฅ", placeholder="๋ถ์ํ ๊ธฐํ ๊ด๋ จ ๋ด์ค๋ ํ
์คํธ๋ฅผ ์
๋ ฅํ์ธ์...", lines=4)
btn_submit = gr.Button("๐ ๊ฒฐ๊ณผ ๋ถ์ ์คํ (Run Analysis)", variant="primary")
gr.Markdown("---")
badge_output = gr.HTML(value='<div style="color: #666;">ํ
์คํธ๋ฅผ ์
๋ ฅํ ํ ๋ถ์ ๋ฒํผ์ ๋๋ฅด๋ฉด ํ์ ๊ฒฐ๊ณผ๊ฐ ํ์๋ฉ๋๋ค.</div>', label="ํ์ ๊ฒฐ๊ณผ")
with gr.Column(elem_classes=["sketch-card"]):
gr.HTML('<div class="highlight-title">Highlight Word (xAI ์ดํ
์
๋ถ์)</div>')
highlight_output = gr.HighlightedText(label="์ค์ ๋จ์ด ์ดํ
์
๊ฐ์ค์น", combine_adjacent=False, show_legend=True)
with gr.Row(elem_classes=["sketch-card"]):
gr.Button("๊ฒฐ๊ณผ", variant="secondary", interactive=False)
risk_score_output = gr.Textbox(value="์ํ๋ ์ ์ +0.0007%", label="์ํ๋ ์ ์", interactive=False)
with gr.Column(elem_classes=["disclaimer-box"]):
limitation_output = gr.Markdown("ํ์-๋ฐ์ด๋ฒ ํ
์คํธ ์ ํ๋ ๋ฎ์! โ ๏ธ (Model Card ํ๊ณ ์ฌ์ ๊ณ ์ง)")
with gr.Accordion("โ๏ธ ์ด์ ์ ๊ธฐ ํต๋ก (Model Card ์ค๋ฆฌ์ ์ฑ
์ ์ ์ธ)", open=False):
gr.Markdown("๋ชจ๋ธ์ ํ์ ๊ฒฐ๊ณผ์ ์คํ์ด ์๊ฑฐ๋ ์ด์๊ฐ ์์ผ์ ๊ฒฝ์ฐ ์๋ ์์์ ์ ์ถํด์ฃผ์ธ์.")
appeal_reason = gr.Textbox(label="์ด์ ์ ๊ธฐ ์ฌ์ ๋ฐ ์๋ช
๋ด์ฉ")
appeal_email = gr.Textbox(label="๊ฐ๋ฐ์ Email")
btn_appeal = gr.Button("์ด์ ์ ๊ธฐ ์ ์ถ")
appeal_status = gr.Textbox(label="์ ์ ์ํ", interactive=False)
btn_appeal.click(fn=file_appeal, inputs=[appeal_reason, appeal_email], outputs=[appeal_status])
btn_submit.click(fn=analyze_climate_text, inputs=[input_text], outputs=[badge_output, highlight_output, risk_score_output, limitation_output])
with gr.TabItem("๐ Model Card (๋ชจ๋ธ ์นด๋ ๋ฐ ์ฑ
์ ์ ์ธ)"):
gr.Markdown("""
## ๐ ์ฑ
์์์ AI Model Card
- **๋ชจ๋ธ๋ช
**: Climate Misinfo LoRA Detector
- **ํ๋ช
๋ฐ ์์ฑ์ผ**: Team 3 | 2026-08-13
- **์๋๋ ์ฌ์ฉ**: ๊ธฐํ ๋ณํ ๊ด๋ จ ๊ธฐ์ฌ ๊ฒ์ฆ / ๊ธ์ง: ์ํยท๋ฒ๋ฅ ์ ์๋ ๊ท์
- **ํ๊ณ ๋ฐ ์ํ**: ํ์ยท๋ฐ์ด๋ฒ ํค๋๋ผ์ธ ๋ฐ ๋งฅ๋ฝ ๋๋ฝ ์ ์คํ ๊ฐ๋ฅ์ฑ ์กด์ฌ
- **๊ฐ๋ฐ์ ์ฑ
์ ์ ์ธ**: ํ๊ณ๋ฅผ ์ฌ์ ๊ณ ์งํ๋ฉฐ ์ด์ ์ ๊ธฐ ํต๋ก๋ฅผ ํตํด ์๋ ด ๋ฐ ๊ฐ์ ํจ.
""")
with gr.TabItem("โ๏ธ ์๊ณ ๋ฆฌ์ฆ ํ๋ฆ๋ (Algorithm Flowchart)"):
gr.Markdown("1. User -> 2. Preprocessing -> 3. Tokenizing -> 4. LoRA Inference Engine -> 5. xAI Extraction -> 6. Attention Analysis -> 7. Output Generation -> 8. Result & Disclaimer")
demo.launch() |