| import os |
| import pandas as pd |
| import gradio as gr |
| import torch |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification |
|
|
| |
| |
| |
| |
| BASE_MODEL_NAME = "beomi/kcbert-base" |
| LORA_ADAPTER_DIR = "./lora_adapter" |
|
|
| |
| 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) |
| |
| base_model = AutoModelForSequenceClassification.from_pretrained(BASE_MODEL_NAME, num_labels=2) |
| |
| |
| 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() |
|
|
| |
| |
| |
| def analyze_text(text): |
| if not text or len(text.strip()) == 0: |
| return "텍스트를 입력해주세요.", "대기 중", 0.0, "입력값이 없습니다." |
| |
| |
| 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() |
| |
| 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 |
|
|
| |
| 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) |
| |
| |
| 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, |
| "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" |
| |
| |
| 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 |
|
|
| |
| |
| |
| 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의 연료를 만들다") |
| |
| |
| 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(): |
| |
| 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 파일") |
|
|
| |
| 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차시 학습 모델 데이터 활용 준비 완료] |
| ``` |
| """) |
|
|
| |
| 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() |
|
|