Download app.py from kdhs/junseok: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/spaces/kdhs/junseok/resolve/main/app.py
- Command line
-
hf download hf://spaces/kdhs/junseok/app.py
-
curl -L -o app.py https://huggingface.co/spaces/kdhs/junseok/resolve/main/app.py
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() | |