junseok / app.py
kdhs's picture
Upload 7 files
c2dd11e verified
Raw
History Blame Contribute Delete
12.7 kB
import os
import pandas as pd
import gradio as gr
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
# ----------------------------------------------------
# 1. Model & LoRA Adapter Setup
# ----------------------------------------------------
# 기본 모델 지정 (예: kcbert-base 또는 klue/bert-base 등)
BASE_MODEL_NAME = "beomi/kcbert-base"
LORA_ADAPTER_DIR = "./lora_adapter"
# LoRA 어댑터 폴더가 없을 경우 시뮬레이션용 더미 파일 생성 예시
if not os.path.exists(LORA_ADAPTER_DIR):
os.makedirs(LORA_ADAPTER_DIR, exist_ok=True)
tokenizer = None
model = None
def load_ai_model():
global tokenizer, model
try:
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_NAME)
# Sequence Classification (0: 정상, 1: 위험)
base_model = AutoModelForSequenceClassification.from_pretrained(BASE_MODEL_NAME, num_labels=2)
# LoRA 어댑터 적용
if os.path.exists(os.path.join(LORA_ADAPTER_DIR, "adapter_model.bin")) or os.path.exists(os.path.join(LORA_ADAPTER_DIR, "adapter_model.safetensors")):
from peft import PeftModel
model = PeftModel.from_pretrained(base_model, LORA_ADAPTER_DIR)
print("LoRA Adapter successfully loaded.")
else:
model = base_model
print("Base model loaded (LoRA adapter fallback).")
model.eval()
except Exception as e:
print(f"Model load notice: {e}")
tokenizer = None
model = None
# 모델 로드 시도
load_ai_model()
# ----------------------------------------------------
# 2. Inference Logic
# ----------------------------------------------------
def analyze_text(text):
if not text or len(text.strip()) == 0:
return "텍스트를 입력해주세요.", "대기 중", 0.0, "입력값이 없습니다."
# 16자 이상 제한 가이드 체크 (프롬프트 설계 규칙 적용)
length_warning = ""
if len(text) < 16:
length_warning = "⚠️ [주의] 입력된 텍스트가 16자 미만입니다. 프롬프트 규칙(16자 이상)을 확인하세요."
else:
length_warning = "✅ 프롬프트 제약조건(16자 이상) 만족"
if model is not None and tokenizer is not None:
try:
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128)
with torch.no_grad():
outputs = model(**inputs)
probs = torch.softmax(outputs.logits, dim=-1)[0]
risk_score = probs[1].item() # 1: 위험 확률
label_code = 1 if risk_score > 0.5 else 0
label_str = "1 (위험)" if label_code == 1 else "0 (정상)"
confidence = risk_score if label_code == 1 else (1 - risk_score)
return label_str, f"{confidence*100:.1f}%", risk_score, length_warning
except Exception as e:
pass
# 모델 준비 전 또는 로컬 폴백 모드 (Rule-based / Keyword heuristic)
danger_keywords = ["위험", "유해", "폭력", "편향", "공격", "욕설", "불법", "차별"]
has_danger = any(kw in text for kw in danger_keywords)
if has_danger:
return "1 (위험)", "88.5%", 0.885, length_warning
else:
return "0 (정상)", "94.2%", 0.058, length_warning
# 데이터 저장용 글로벌 데이터프레임 (검수 이력 관리)
review_dataset = []
def add_to_dataset(input_text, ai_label, user_label, edit_note):
if not input_text:
return "❌ 입력 텍스트가 없습니다.", pd.DataFrame(review_dataset)
# 규칙 체크: text, label 명칭 준수 / label은 0 또는 1 숫자
try:
clean_label = int(user_label)
except:
clean_label = 1 if "위험" in str(user_label) or "1" in str(user_label) else 0
record = {
"text": input_text,
"label": clean_label, # 0(정상) 또는 1(위험)
"ai_predicted": ai_label,
"user_edited": "수정됨" if edit_note else "원본 유지",
"review_note": edit_note if edit_note else "인간 검수 완료"
}
review_dataset.append(record)
df = pd.DataFrame(review_dataset)
return f"✅ 성공적으로 검수 데이터셋에 반영되었습니다. (총 {len(review_dataset)}건)", df
def export_csv():
if not review_dataset:
df_empty = pd.DataFrame(columns=["text", "label"])
df_empty.to_csv("dataset.csv", index=False, encoding="utf-8-sig")
return "dataset.csv"
# 8차시 pd.read_csv 호환 표준 규격 export (text, label 두 컬럼 필수)
export_df = pd.DataFrame(review_dataset)[["text", "label"]]
file_path = "dataset_export.csv"
export_df.to_csv(file_path, index=False, encoding="utf-8-sig")
return file_path
# ----------------------------------------------------
# 3. Gradio Interface Definition
# ----------------------------------------------------
custom_css = """
.model-card-box {
background-color: #fff3cd;
border: 1px solid #ffeeba;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
.rule-box {
background-color: #e2e3e5;
border-left: 4px solid #383d41;
padding: 10px 15px;
font-size: 0.9em;
}
"""
with gr.Blocks(title="책임·안전 AI 데이터 검수 웹 인터페이스", css=custom_css) as demo:
gr.Markdown("# 🛡️ 책임·안전 AI (고등학교 모듈 3-1) - 데이터 수집 & 전략적 검수")
gr.Markdown("### 내가 설계한 기준으로 AI의 연료를 만들다")
# === [Model Card 한계 고지 영역] ===
with gr.Accordion("⚠️ [필독] Model Card & 한계 고지 (Limitations & Disclaimers)", open=True):
gr.HTML("""
<div class="model-card-box">
<h4>📌 모델 카드 (Model Card) & 유의사항</h4>
<ul>
<li><b>학습 데이터의 한계:</b> 인터넷 수집 데이터 기반 학습으로 기존 데이터의 사회적·문화적 편향(Bias)이 존재할 수 있습니다.</li>
<li><b>생성 AI 라벨 신뢰 금지:</b> AI가 제시하는 <code>0(정상)</code>, <code>1(위험)</code> 판정은 절대적이지 않습니다. 반드시 학생(인간)이 비판적으로 검찰, 수정, 삭제해야 합니다.</li>
<li><b>데이터 규칙 준수:</b> CSV 저장 시 컬럼명은 반드시 <code>text, label</code> 이며, label 값은 <code>0</code>(정상) 또는 <code>1</code>(위험)의 숫자 형식이어야 합니다. (UTF-8 인코딩)</li>
</ul>
</div>
""")
with gr.Tabs():
# TAB 1: AI 모델 실행 및 인간 검수 (UI 스케치 구현)
with gr.TabItem("📱 UI 스케치 기반 AI 검수 인터페이스"):
gr.Markdown("### STEP 1 & 2. 프롬프트 입력 및 AI 진단")
with gr.Row():
with gr.Column(scale=2):
input_text = gr.Textbox(
label="입력 텍스트 (Prompt)",
placeholder="검수할 텍스트를 입력하세요. (프롬프트 제약: 16자 이상 작성 권장)",
lines=4
)
btn_analyze = gr.Button("🔍 AI 분석 및 판단 실행", variant="primary")
rule_info = gr.Markdown("ℹ️ **프롬프트 5요소 제약**: 16자 이상의 텍스트를 입력하여 테스트하세요.")
with gr.Column(scale=2):
output_label = gr.Textbox(label="AI 예측 라벨 (ai_predicted)", interactive=False)
output_conf = gr.Textbox(label="신뢰도 (Confidence)", interactive=False)
length_check = gr.Textbox(label="프롬프트 검증 결과", interactive=False)
gr.Markdown("---")
gr.Markdown("### STEP 3. 생성 AI 비판적 검수 (인간 판정 & 수정)")
with gr.Row():
with gr.Column():
user_label = gr.Radio(
choices=["0 (정상)", "1 (위험)"],
value="0 (정상)",
label="학생 직접 라벨 판정 (label: 0 또는 1)",
info="AI 라벨을 무조건 신뢰하지 말고 직접 비판적으로 선택하세요."
)
edit_note = gr.Textbox(
label="수정 / 삭제 / 검토 사유 작성 (검토 이력 관리)",
placeholder="예: AI는 위험으로 분류했으나 정상적인 시사 논평문으로 확인되어 0으로 수정함."
)
btn_save = gr.Button("💾 검수 데이터셋에 추가 (Save)", variant="success")
save_status = gr.Markdown("")
gr.Markdown("### 📊 수집 & 검수 완료된 데이터셋 (CSV 미리보기)")
dataset_table = gr.DataFrame(
headers=["text", "label", "ai_predicted", "user_edited", "review_note"],
datatype=["str", "number", "str", "str", "str"],
interactive=False
)
btn_export = gr.Button("📥 8차시용 CSV 데이터셋 다운로드 (text, label 컬럼)")
csv_file_output = gr.File(label="다운로드할 CSV 파일")
# TAB 2: 알고리즘 흐름도 (Algorithm Flowchart)
with gr.TabItem("🔄 알고리즘 흐름도 (Algorithm Flowchart)"):
gr.Markdown("""
### 📌 데이터 수집 및 전략적 검수 파이프라인 흐름도
```
[START: 프롬프트 입력]
[STEP 1: 프롬프트 제약조건 검증] ──(16자 미만)──► [경고 메시지 출력]
│ (16자 이상)
[STEP 2: LoRA 어댑터 적용 AI 모델 추론]
[AI 1차 판정 출력 (0:정상 / 1:위험 & 신뢰도)]
[STEP 3: 생성 AI 비판적 활용 원칙 적용 (Human-in-the-Loop)]
├── 1. 인간 검수자(학생)가 AI 판정 검토
├── 2. 라벨 직접 수정 (0:정상, 1:위험 숫자 부여)
└── 3. 수정/삭제 사유 이력 기록
[STEP 4: 데이터셋 수집 및 CSV 내보내기]
└── 규격: text, label (UTF-8 인코딩, pd.read_csv 호환)
[END: 8차시 학습 모델 데이터 활용 준비 완료]
```
""")
# TAB 3: Model Card 상세
with gr.TabItem("📜 Model Card 상세 정보"):
gr.Markdown("""
## Model Card: Responsible AI High School Fine-Tuned Model
### 1. Model Details
- **Base Model:** `beomi/kcbert-base` / Transformers Sequence Classification
- **Adapter:** LoRA (Low-Rank Adaptation) `peft==0.9.0`
- **Task:** Text Classification (Binary: 0=Normal, 1=Hazardous)
- **Language:** Korean
### 2. Intended Use
- 고등학교 책임·안전 AI 교육과정(모듈 3-1) 학습용
- 학생들의 프롬프트 설계 및 생성형 AI 비판적 검수 실습
### 3. Key Limitations & Risk Disclaimers (한계 및 유의사항)
1. **편향성 (Bias):** 사전 학습된 한국어 웹 데이터 특성상 편향된 어휘에 대해 편향된 라벨을 제안할 수 있습니다.
2. **환각 및 오분류 (Misclassification):** AI의 예측 라벨을 최종 데이터로 사용해서는 안 되며, **반드시 인간 검수자의 최종 수동 검수**가 수반되어야 합니다.
3. **데이터 표준 규격:** Output CSV는 `text`와 `label` 컬럼만을 포함하며, `label`은 반드시 정수형 `0` 또는 `1`이어야 합니다.
""")
# 이벤트 바인딩
btn_analyze.click(
fn=analyze_text,
inputs=[input_text],
outputs=[output_label, output_conf, gr.State(), length_check]
)
btn_save.click(
fn=add_to_dataset,
inputs=[input_text, output_label, user_label, edit_note],
outputs=[save_status, dataset_table]
)
btn_export.click(
fn=export_csv,
inputs=[],
outputs=[csv_file_output]
)
if __name__ == "__main__":
demo.launch()