Spaces:
Sleeping
Sleeping
| """ | |
| 03_jsonl_formatter.py | |
| --------------------- | |
| 파인튜닝용 JSONL 데이터 포맷 변환기 | |
| LLM 파인튜닝(SFT)에는 Instruction-Input-Output 구조의 JSONL이 필요합니다. | |
| 이 스크립트는 전처리된 의료 상담 데이터를 모델별 프롬프트 템플릿에 맞게 변환합니다. | |
| 지원 포맷: | |
| 1. Alpaca 포맷 (범용) | |
| 2. ChatML 포맷 (LLaMA 3 / Mistral) | |
| 3. Llama-3 한국어 포맷 (beomi/Llama-3-Open-Ko) | |
| 사용법: | |
| python 03_jsonl_formatter.py --input ./data/processed/consultation_clean.json | |
| --format llama3 | |
| --train_ratio 0.9 | |
| """ | |
| import json | |
| import random | |
| import argparse | |
| from pathlib import Path | |
| from datetime import datetime | |
| # ── 설정 ────────────────────────────────────────────────────────────────────── | |
| JSONL_DIR = Path("./data/jsonl") | |
| # 시스템 프롬프트: 모델에게 역할을 부여 | |
| SYSTEM_PROMPT = """당신은 환자의 증상과 질문을 듣고 의학적 정보를 제공하는 의료 상담 AI입니다. | |
| 정확하고 신뢰할 수 있는 의료 정보를 바탕으로 답변하되, 반드시 전문 의료진 상담을 권장하세요. | |
| 진단이나 처방을 직접 내리지 말고, 가능한 원인과 권장 진료과를 안내하는 방식으로 답변하세요.""" | |
| # ── 프롬프트 템플릿 ─────────────────────────────────────────────────────────── | |
| def format_alpaca(record: dict) -> dict: | |
| """ | |
| Alpaca 포맷 (가장 범용적, 많은 오픈소스 파인튜닝 코드에서 지원) | |
| { | |
| "instruction": "시스템 역할 + 태스크 설명", | |
| "input": "환자 질문", | |
| "output": "의사 답변" | |
| } | |
| """ | |
| return { | |
| "instruction": SYSTEM_PROMPT, | |
| "input": record["input"], | |
| "output": record["output"], | |
| } | |
| def format_chatml(record: dict) -> dict: | |
| """ | |
| ChatML 포맷 (LLaMA 3, Mistral에서 권장) | |
| { | |
| "messages": [ | |
| {"role": "system", "content": "..."}, | |
| {"role": "user", "content": "..."}, | |
| {"role": "assistant", "content": "..."} | |
| ] | |
| } | |
| """ | |
| return { | |
| "messages": [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": record["input"]}, | |
| {"role": "assistant", "content": record["output"]}, | |
| ] | |
| } | |
| def format_llama3(record: dict) -> dict: | |
| """ | |
| LLaMA 3 공식 포맷 (beomi/Llama-3-Open-Ko-8B 파인튜닝 권장) | |
| 특수 토큰: | |
| <|begin_of_text|> | |
| <|start_header_id|>system<|end_header_id|> | |
| ... | |
| <|eot_id|> | |
| """ | |
| text = ( | |
| "<|begin_of_text|>" | |
| "<|start_header_id|>system<|end_header_id|>\n\n" | |
| f"{SYSTEM_PROMPT}" | |
| "<|eot_id|>" | |
| "<|start_header_id|>user<|end_header_id|>\n\n" | |
| f"{record['input']}" | |
| "<|eot_id|>" | |
| "<|start_header_id|>assistant<|end_header_id|>\n\n" | |
| f"{record['output']}" | |
| "<|eot_id|>" | |
| "<|end_of_text|>" | |
| ) | |
| return {"text": text} | |
| # 포맷 함수 매핑 | |
| FORMAT_FUNCTIONS = { | |
| "alpaca": format_alpaca, | |
| "chatml": format_chatml, | |
| "llama3": format_llama3, | |
| } | |
| # ── 메인 변환 클래스 ────────────────────────────────────────────────────────── | |
| class JSONLFormatter: | |
| def __init__(self, format_type: str = "llama3"): | |
| if format_type not in FORMAT_FUNCTIONS: | |
| raise ValueError(f"지원 포맷: {list(FORMAT_FUNCTIONS.keys())}") | |
| self.format_type = format_type | |
| self.format_fn = FORMAT_FUNCTIONS[format_type] | |
| def convert(self, records: list[dict]) -> list[dict]: | |
| """전처리된 records를 파인튜닝 포맷으로 변환""" | |
| formatted = [] | |
| for record in records: | |
| try: | |
| formatted.append(self.format_fn(record)) | |
| except KeyError as e: | |
| print(f"[경고] 필드 누락으로 건너뜀: {e}") | |
| return formatted | |
| def split_train_val( | |
| self, | |
| records: list[dict], | |
| train_ratio: float = 0.9, | |
| seed: int = 42, | |
| ) -> tuple[list, list]: | |
| """학습/검증 데이터 분리""" | |
| random.seed(seed) | |
| shuffled = records.copy() | |
| random.shuffle(shuffled) | |
| split_idx = int(len(shuffled) * train_ratio) | |
| train = shuffled[:split_idx] | |
| val = shuffled[split_idx:] | |
| print(f"\n데이터 분리: 학습 {len(train):,}개 / 검증 {len(val):,}개") | |
| return train, val | |
| def save_jsonl(self, records: list[dict], output_path: str): | |
| """JSONL 형태로 저장 (한 줄 = 하나의 샘플)""" | |
| path = Path(output_path) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(path, "w", encoding="utf-8") as f: | |
| for record in records: | |
| f.write(json.dumps(record, ensure_ascii=False) + "\n") | |
| print(f"[저장] {path} ({len(records):,}개)") | |
| def verify_output(self, jsonl_path: str, n_samples: int = 2): | |
| """저장된 JSONL 검증 및 미리보기""" | |
| print(f"\n{'='*60}") | |
| print(f"JSONL 검증: {jsonl_path}") | |
| print(f"{'='*60}") | |
| path = Path(jsonl_path) | |
| if not path.exists(): | |
| print("[오류] 파일 없음") | |
| return | |
| with open(path, "r", encoding="utf-8") as f: | |
| lines = f.readlines() | |
| print(f"총 줄 수: {len(lines):,}") | |
| for i, line in enumerate(lines[:n_samples]): | |
| record = json.loads(line) | |
| print(f"\n--- 샘플 {i+1} ---") | |
| if self.format_type == "llama3": | |
| # LLaMA3 포맷은 text 필드만 있음 | |
| preview = record["text"][:300].replace("\n", "↵") | |
| print(f"text (앞 300자): {preview}...") | |
| elif self.format_type == "chatml": | |
| for msg in record["messages"]: | |
| role = msg["role"] | |
| content = msg["content"][:100].replace("\n", " ") | |
| print(f"[{role}]: {content}...") | |
| elif self.format_type == "alpaca": | |
| print(f"instruction: {record['instruction'][:80]}...") | |
| print(f"input: {record['input'][:100]}...") | |
| print(f"output: {record['output'][:100]}...") | |
| # ── 데이터셋 카드 생성 (HuggingFace Hub 업로드용) ───────────────────────────── | |
| def create_dataset_card( | |
| output_dir: Path, | |
| train_count: int, | |
| val_count: int, | |
| format_type: str, | |
| ): | |
| """HuggingFace Hub에 업로드할 때 함께 올리는 README""" | |
| card = f"""--- | |
| language: | |
| - ko | |
| task_categories: | |
| - text-generation | |
| - question-answering | |
| domain: | |
| - medical | |
| --- | |
| # 한국어 의료 상담 파인튜닝 데이터셋 | |
| ## 개요 | |
| AI Hub 한국어 의료 상담 데이터를 LLM 파인튜닝(SFT)용으로 변환한 데이터셋입니다. | |
| ## 데이터 통계 | |
| | 분할 | 샘플 수 | | |
| |------|--------| | |
| | train | {train_count:,} | | |
| | validation | {val_count:,} | | |
| | **합계** | **{train_count + val_count:,}** | | |
| ## 포맷 | |
| `{format_type}` 포맷 사용 | |
| ## 원본 데이터 출처 | |
| - AI Hub 한국어 의료 상담 데이터 (https://aihub.or.kr) | |
| - 라이선스: AI Hub 데이터 활용 정책 준수 | |
| ## 생성일 | |
| {datetime.now().strftime("%Y-%m-%d")} | |
| """ | |
| card_path = output_dir / "README.md" | |
| with open(card_path, "w", encoding="utf-8") as f: | |
| f.write(card) | |
| print(f"[데이터셋 카드 생성] {card_path}") | |
| # ── 실행 ────────────────────────────────────────────────────────────────────── | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="파인튜닝용 JSONL 변환") | |
| parser.add_argument("--input", type=str, default="./data/processed/consultation_clean.json") | |
| parser.add_argument("--output_dir", type=str, default="./data/jsonl") | |
| parser.add_argument( | |
| "--format", | |
| choices=["alpaca", "chatml", "llama3"], | |
| default="llama3", | |
| help="파인튜닝 모델에 맞는 포맷 선택", | |
| ) | |
| parser.add_argument("--train_ratio", type=float, default=0.9) | |
| args = parser.parse_args() | |
| # 데이터 로드 | |
| with open(args.input, "r", encoding="utf-8") as f: | |
| records = json.load(f) | |
| if isinstance(records, dict): | |
| records = records.get("data", []) | |
| print(f"[입력 데이터] {len(records):,}개") | |
| # 변환 실행 | |
| formatter = JSONLFormatter(format_type=args.format) | |
| formatted = formatter.convert(records) | |
| # 학습/검증 분리 | |
| train_data, val_data = formatter.split_train_val(formatted, args.train_ratio) | |
| # 저장 | |
| output_dir = Path(args.output_dir) | |
| formatter.save_jsonl(train_data, output_dir / "train.jsonl") | |
| formatter.save_jsonl(val_data, output_dir / "val.jsonl") | |
| # 검증 | |
| formatter.verify_output(output_dir / "train.jsonl") | |
| # 데이터셋 카드 생성 | |
| create_dataset_card(output_dir, len(train_data), len(val_data), args.format) | |
| print(f"\n✅ JSONL 변환 완료! 다음 단계: Colab에서 04_finetune.ipynb 실행") | |