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, "", "", ""]:
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()