본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 9장: 학습-평가-배포 자동화 사이클
2026년 1월 30일·AI / ML·

9장: 학습-평가-배포 자동화 사이클

파인튜닝의 학습, 평가, 배포 전체 과정을 CI/CD 파이프라인으로 자동화하고, 데이터 변경이나 코드 변경 시 자동으로 모델이 업데이트되는 체계를 구축합니다.

17분1,377자9개 섹션
llmtrainingmlopsdata-engineering
공유
fine-tuning9 / 10
12345678910
이전8장: 모델 레지스트리와 버전 관리다음10장: 실전 프로젝트 - 도메인 특화 코드 리뷰 모델 파인튜닝

자동화가 필요한 이유

파인튜닝을 한 번만 수행하는 경우는 드뭅니다. 데이터가 추가되고, 학습 설정이 변경되고, 베이스 모델이 업데이트될 때마다 파인튜닝을 다시 수행해야 합니다. 수동으로 이 과정을 반복하면 실수가 발생하고, 결과의 재현성이 떨어지며, 팀 전체의 생산성이 저하됩니다.

text
수동 파인튜닝 파이프라인의 문제점:
 
  데이터 업데이트
       |
  "누군가" 학습 스크립트 수동 실행
       |
  결과 확인 ("이거 좋은 건가?")
       |
  수동으로 모델 서버에 배포
       |
  문제 발생 시 어떤 버전이었는지 모름
 
  문제:
    - 재현 불가능
    - 실수 발생 가능
    - 배포까지 시간이 오래 걸림
    - 롤백이 어려움

자동화된 파이프라인은 이 모든 과정을 코드로 정의하고, 트리거 조건에 따라 자동으로 실행합니다.

자동화 파이프라인 아키텍처

text
전체 자동화 아키텍처:
 
  트리거
    |-- 데이터 변경 (새 데이터 추가)
    |-- 코드 변경 (학습 설정 수정)
    |-- 스케줄 (주기적 재학습)
    |-- 수동 (버튼 클릭)
       |
  학습 파이프라인
    |-- 데이터 검증
    |-- 전처리
    |-- 모델 학습
    |-- 체크포인트 저장
       |
  평가 파이프라인
    |-- 자동 메트릭 평가
    |-- 벤치마크 테스트
    |-- 회귀 테스트
    |-- 품질 게이트 (통과/실패)
       |
  배포 파이프라인 (평가 통과 시)
    |-- 모델 레지스트리 등록
    |-- 스테이징 배포
    |-- 스모크 테스트
    |-- 프로덕션 배포
    |-- 모니터링 활성화
       |
  알림
    |-- Slack/이메일 통보
    |-- 대시보드 업데이트

GitHub Actions를 활용한 CI/CD

학습 트리거 워크플로우

yaml
# .github/workflows/fine-tuning-pipeline.yml
name: Fine-Tuning Pipeline
 
on:
  # 데이터 변경 시 자동 트리거
  push:
    paths:
      - 'data/**'
      - 'configs/**'
    branches:
      - main
 
  # 수동 트리거
  workflow_dispatch:
    inputs:
      config_file:
        description: 'Training config file'
        required: true
        default: 'configs/experiment_v1.yaml'
      run_evaluation:
        description: 'Run evaluation after training'
        required: true
        default: 'true'
 
  # 주간 스케줄 (매주 일요일 자정)
  schedule:
    - cron: '0 0 * * 0'
 
env:
  MODEL_REGISTRY: s3://my-bucket/model-registry
  HF_TOKEN: ${{ secrets.HF_TOKEN }}
  WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }}

학습 Job 정의

yaml
jobs:
  data-validation:
    runs-on: ubuntu-latest
    outputs:
      data_valid: ${{ steps.validate.outputs.valid }}
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
 
      - name: Install dependencies
        run: pip install -r requirements.txt
 
      - name: Validate training data
        id: validate
        run: |
          python scripts/validate_data.py \
            --input data/train.jsonl \
            --min-examples 100 \
            --max-examples 100000 \
            --check-format \
            --check-duplicates
 
  train:
    needs: data-validation
    if: needs.data-validation.outputs.data_valid == 'true'
    runs-on: [self-hosted, gpu]
    outputs:
      model_path: ${{ steps.train.outputs.model_path }}
      run_id: ${{ steps.train.outputs.run_id }}
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Python environment
        run: |
          python -m venv venv
          source venv/bin/activate
          pip install -r requirements.txt
 
      - name: Run training
        id: train
        run: |
          source venv/bin/activate
          python train.py \
            --config configs/experiment_v1.yaml \
            --output-dir ./output
          echo "model_path=./output/best-adapter" >> "$GITHUB_OUTPUT"
          echo "run_id=$(cat ./output/run_id.txt)" >> "$GITHUB_OUTPUT"
 
      - name: Upload model artifact
        uses: actions/upload-artifact@v4
        with:
          name: lora-adapter
          path: ./output/best-adapter/
          retention-days: 30

평가 Job 정의

yaml
  evaluate:
    needs: train
    runs-on: [self-hosted, gpu]
    outputs:
      eval_passed: ${{ steps.quality-gate.outputs.passed }}
    steps:
      - uses: actions/checkout@v4
 
      - name: Download model artifact
        uses: actions/download-artifact@v4
        with:
          name: lora-adapter
          path: ./adapter/
 
      - name: Run evaluation
        id: evaluate
        run: |
          source venv/bin/activate
          python evaluate.py \
            --model-path ./adapter \
            --test-data data/test.jsonl \
            --output eval_results.json
 
      - name: Quality gate check
        id: quality-gate
        run: |
          source venv/bin/activate
          python scripts/quality_gate.py \
            --results eval_results.json \
            --min-rouge-l 0.70 \
            --min-bertscore 0.80 \
            --max-eval-loss 0.60
 
      - name: Upload evaluation results
        uses: actions/upload-artifact@v4
        with:
          name: eval-results
          path: eval_results.json

배포 Job 정의

yaml
  deploy:
    needs: evaluate
    if: needs.evaluate.outputs.eval_passed == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Download model artifact
        uses: actions/download-artifact@v4
        with:
          name: lora-adapter
          path: ./adapter/
 
      - name: Register model
        run: |
          python scripts/register_model.py \
            --adapter-path ./adapter \
            --registry-uri $MODEL_REGISTRY \
            --stage staging
 
      - name: Deploy to staging
        run: |
          python scripts/deploy.py \
            --environment staging \
            --model-path $MODEL_REGISTRY/latest
 
      - name: Smoke test
        run: |
          python scripts/smoke_test.py \
            --endpoint https://staging-api.example.com/v1/chat
 
      - name: Promote to production
        if: success()
        run: |
          python scripts/deploy.py \
            --environment production \
            --model-path $MODEL_REGISTRY/latest
          python scripts/register_model.py \
            --stage production
 
      - name: Notify
        if: always()
        run: |
          python scripts/notify.py \
            --channel fine-tuning \
            --status ${{ job.status }}

품질 게이트 (Quality Gate)

자동화 파이프라인에서 모델이 프로덕션에 배포되기 전에 반드시 통과해야 하는 기준을 정의합니다.

python
# scripts/quality_gate.py
import json
import sys
from argparse import ArgumentParser
 
 
def check_quality_gate(
    results_path: str,
    min_rouge_l: float = 0.70,
    min_bertscore: float = 0.80,
    max_eval_loss: float = 0.60,
) -> bool:
    """품질 게이트 검사"""
    with open(results_path) as f:
        results = json.load(f)
 
    checks = []
 
    # 1. ROUGE-L 점수 확인
    rouge_l = results.get("rouge_l", 0)
    rouge_pass = rouge_l >= min_rouge_l
    checks.append({
        "name": "ROUGE-L",
        "value": rouge_l,
        "threshold": min_rouge_l,
        "passed": rouge_pass,
    })
 
    # 2. BERTScore 확인
    bertscore = results.get("bertscore_f1", 0)
    bert_pass = bertscore >= min_bertscore
    checks.append({
        "name": "BERTScore F1",
        "value": bertscore,
        "threshold": min_bertscore,
        "passed": bert_pass,
    })
 
    # 3. 검증 손실 확인
    eval_loss = results.get("eval_loss", 1.0)
    loss_pass = eval_loss <= max_eval_loss
    checks.append({
        "name": "Eval Loss",
        "value": eval_loss,
        "threshold": max_eval_loss,
        "passed": loss_pass,
    })
 
    # 결과 출력
    print("품질 게이트 결과:")
    print("=" * 50)
    all_passed = True
    for check in checks:
        status = "PASS" if check["passed"] else "FAIL"
        print(
            "  " + check["name"] + ": "
            + str(round(check["value"], 4))
            + " (기준: " + str(check["threshold"]) + ") "
            + "[" + status + "]"
        )
        if not check["passed"]:
            all_passed = False
 
    overall = "PASS" if all_passed else "FAIL"
    print("=" * 50)
    print("종합: " + overall)
 
    return all_passed
 
 
if __name__ == "__main__":
    parser = ArgumentParser()
    parser.add_argument("--results", required=True)
    parser.add_argument("--min-rouge-l", type=float, default=0.70)
    parser.add_argument("--min-bertscore", type=float, default=0.80)
    parser.add_argument("--max-eval-loss", type=float, default=0.60)
    args = parser.parse_args()
 
    passed = check_quality_gate(
        args.results,
        args.min_rouge_l,
        args.min_bertscore,
        args.max_eval_loss,
    )
 
    if not passed:
        print("품질 게이트 실패. 배포가 중단됩니다.")
        sys.exit(1)
 
    # GitHub Actions 출력
    with open(sys.environ.get("GITHUB_OUTPUT", "/dev/null"), "a") as f:
        f.write("passed=true\n")

회귀 테스트

새 모델이 기존 프로덕션 모델보다 퇴보하지 않았는지 확인합니다.

python
def regression_test(
    new_results: dict,
    prod_results: dict,
    tolerance: float = 0.02
) -> bool:
    """프로덕션 모델 대비 회귀 테스트"""
    regression_detected = False
 
    for metric in ["rouge_l", "bertscore_f1"]:
        new_val = new_results.get(metric, 0)
        prod_val = prod_results.get(metric, 0)
        diff = new_val - prod_val
 
        status = "OK"
        if diff < -tolerance:
            status = "REGRESSION"
            regression_detected = True
        elif diff > tolerance:
            status = "IMPROVED"
 
        print(
            metric + ": " + str(round(prod_val, 4))
            + " -> " + str(round(new_val, 4))
            + " (" + status + ")"
        )
 
    return not regression_detected

데이터 파이프라인 자동화

데이터가 변경될 때 자동으로 전처리와 검증을 수행하는 파이프라인입니다.

python
# scripts/data_pipeline.py
import json
from pathlib import Path
 
 
class DataPipeline:
    """학습 데이터 자동 처리 파이프라인"""
 
    def __init__(self, raw_dir: str, processed_dir: str):
        self.raw_dir = Path(raw_dir)
        self.processed_dir = Path(processed_dir)
 
    def run(self):
        """전체 데이터 파이프라인 실행"""
        print("데이터 파이프라인 시작")
 
        # 1. 원시 데이터 수집
        raw_data = self.collect_raw_data()
        print("원시 데이터: " + str(len(raw_data)) + "개")
 
        # 2. 형식 표준화
        normalized = self.normalize(raw_data)
 
        # 3. 정제 및 필터링
        cleaned = self.clean_and_filter(normalized)
        print("정제 후: " + str(len(cleaned)) + "개")
 
        # 4. 중복 제거
        deduped = self.deduplicate(cleaned)
        print("중복 제거 후: " + str(len(deduped)) + "개")
 
        # 5. 품질 검증
        validated = self.validate_quality(deduped)
        print("검증 통과: " + str(len(validated)) + "개")
 
        # 6. 분할 및 저장
        self.split_and_save(validated)
        print("데이터 파이프라인 완료")
 
    def collect_raw_data(self) -> list:
        all_data = []
        for file in self.raw_dir.glob("*.jsonl"):
            with open(file) as f:
                for line in f:
                    all_data.append(json.loads(line))
        return all_data
 
    def normalize(self, data: list) -> list:
        # 형식 표준화 (2장 참조)
        return data
 
    def clean_and_filter(self, data: list) -> list:
        # 정제 및 필터링 (3장 참조)
        return [d for d in data if self.passes_filter(d)]
 
    def passes_filter(self, example: dict) -> bool:
        messages = example.get("messages", [])
        if not messages:
            return False
        assistant_msgs = [
            m for m in messages if m["role"] == "assistant"
        ]
        if not assistant_msgs:
            return False
        response = assistant_msgs[0]["content"]
        return len(response) >= 50
 
    def deduplicate(self, data: list) -> list:
        seen = set()
        unique = []
        for d in data:
            key = json.dumps(d["messages"], sort_keys=True)
            import hashlib
            h = hashlib.md5(key.encode()).hexdigest()
            if h not in seen:
                seen.add(h)
                unique.append(d)
        return unique
 
    def validate_quality(self, data: list) -> list:
        return data  # 규칙 기반 + LLM 검증 적용
 
    def split_and_save(self, data: list):
        from sklearn.model_selection import train_test_split
 
        train_val, test = train_test_split(
            data, test_size=0.1, random_state=42
        )
        train, val = train_test_split(
            train_val, test_size=0.11, random_state=42
        )
 
        self.processed_dir.mkdir(parents=True, exist_ok=True)
        for split_name, split_data in [
            ("train", train), ("val", val), ("test", test)
        ]:
            path = self.processed_dir / (split_name + ".jsonl")
            with open(path, "w") as f:
                for d in split_data:
                    f.write(json.dumps(d, ensure_ascii=False) + "\n")
            print(
                "  " + split_name + ": "
                + str(len(split_data)) + "개 저장됨"
            )

롤백 전략

프로덕션 모델에 문제가 발생했을 때 이전 버전으로 빠르게 롤백하는 체계입니다.

python
# scripts/rollback.py
class RollbackManager:
    """모델 롤백 관리"""
 
    def __init__(self, registry, deployer):
        self.registry = registry
        self.deployer = deployer
 
    def rollback_to_previous(self, model_name: str):
        """이전 프로덕션 버전으로 롤백"""
        versions = self.registry.list_versions(model_name)
 
        # archived 상태의 가장 최근 버전 찾기
        archived = [
            v for v in versions if v["stage"] == "archived"
        ]
        if not archived:
            raise ValueError("롤백할 이전 버전이 없습니다.")
 
        prev_version = archived[-1]
        current = self.registry.get_production_model(model_name)
 
        print(
            "롤백: v" + str(current["version"])
            + " -> v" + str(prev_version["version"])
        )
 
        # 1. 이전 버전을 프로덕션으로 승격
        self.registry.promote_to_production(
            model_name, prev_version["version"]
        )
 
        # 2. 배포
        self.deployer.deploy(
            model_path=prev_version["model_path"],
            environment="production"
        )
 
        # 3. 현재 버전의 문제 기록
        self.registry.add_note(
            model_name,
            current["version"],
            "프로덕션 롤백됨. 문제 조사 필요."
        )
 
        print("롤백 완료")
Tip

프로덕션 배포 시 Blue-Green 배포 전략을 사용하면 롤백이 더 빠르고 안전합니다. 새 모델을 별도의 서버에 배포하고, 로드 밸런서를 전환하여 트래픽을 옮기는 방식입니다. 문제 발생 시 로드 밸런서만 원래 서버로 되돌리면 됩니다.

알림과 모니터링

파이프라인의 상태를 실시간으로 알리고 모니터링하는 체계입니다.

python
# scripts/notify.py
import json
import os
from urllib.request import urlopen, Request
 
 
def send_slack_notification(
    channel: str,
    status: str,
    details: dict
):
    """Slack 웹훅을 통한 알림 전송"""
    webhook_url = os.environ["SLACK_WEBHOOK_URL"]
 
    color = "#36a64f" if status == "success" else "#ff0000"
    status_text = "성공" if status == "success" else "실패"
 
    blocks = [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": "파인튜닝 파이프라인 " + status_text
            }
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*모델:*\n" + details.get(
                        "model_name", "N/A"
                    )
                },
                {
                    "type": "mrkdwn",
                    "text": "*버전:*\n" + details.get(
                        "version", "N/A"
                    )
                },
            ]
        }
    ]
 
    if "metrics" in details:
        metrics_text = "\n".join(
            k + ": " + str(round(v, 4))
            for k, v in details["metrics"].items()
        )
        blocks.append({
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "*평가 결과:*\n" + metrics_text
            }
        })
 
    payload = json.dumps({
        "channel": channel,
        "attachments": [{"color": color, "blocks": blocks}]
    }).encode()
 
    req = Request(webhook_url, data=payload)
    req.add_header("Content-Type", "application/json")
    urlopen(req)

전체 자동화 체크리스트

text
자동화 파이프라인 체크리스트:
 
  학습 자동화:
    - 트리거 조건 정의 (데이터/코드 변경, 스케줄)
    - 데이터 검증 자동화
    - 재현 가능한 학습 설정
    - GPU 자원 자동 할당/해제
 
  평가 자동화:
    - 자동 메트릭 계산
    - 벤치마크 테스트 자동 실행
    - 품질 게이트 정의
    - 회귀 테스트 자동 실행
 
  배포 자동화:
    - 모델 레지스트리 자동 등록
    - 스테이징 자동 배포
    - 스모크 테스트 자동 실행
    - 프로덕션 배포 (수동 승인 또는 자동)
    - 롤백 스크립트 준비
 
  모니터링:
    - 파이프라인 상태 알림 (Slack/이메일)
    - 학습 메트릭 대시보드
    - 프로덕션 모델 성능 모니터링
    - 비용 추적

정리

이번 장에서는 파인튜닝의 전체 라이프사이클을 자동화하는 CI/CD 파이프라인을 구축했습니다.

  • GitHub Actions를 활용하여 데이터 변경, 코드 변경, 스케줄 기반의 학습 트리거를 설정했습니다.
  • 품질 게이트와 회귀 테스트로 모델 품질을 자동으로 검증합니다.
  • 스테이징 배포, 스모크 테스트, 프로덕션 배포까지 자동화된 흐름을 구축했습니다.
  • 롤백 전략과 알림 체계를 마련하여 운영 안정성을 확보했습니다.

다음 장에서는 지금까지 배운 모든 내용을 통합하여 실전 프로젝트를 진행합니다. 도메인 특화 데이터 수집부터 프로덕션 배포까지, 파인튜닝의 전체 과정을 하나의 프로젝트로 완성합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#llm#training#mlops#data-engineering

관련 글

AI / ML

10장: 실전 프로젝트 - 도메인 특화 코드 리뷰 모델 파인튜닝

코드 리뷰 특화 모델을 데이터 수집부터 프로덕션 배포까지 전 과정을 실습하며, 시리즈에서 배운 모든 기법을 통합 적용합니다.

2026년 2월 1일·20분
AI / ML

8장: 모델 레지스트리와 버전 관리

파인튜닝된 모델을 체계적으로 관리하기 위한 모델 레지스트리 구축, 버전 관리, 메타데이터 추적, 아티팩트 저장 전략을 다룹니다.

2026년 1월 28일·15분
AI / ML

7장: 파인튜닝 모델 평가와 벤치마킹

파인튜닝된 모델의 성능을 자동 메트릭, LLM 평가, 인간 평가를 통해 다각적으로 측정하고 벤치마킹하는 체계적인 방법을 다룹니다.

2026년 1월 26일·16분
이전 글8장: 모델 레지스트리와 버전 관리
다음 글10장: 실전 프로젝트 - 도메인 특화 코드 리뷰 모델 파인튜닝

댓글

목차

약 17분 남음
  • 자동화가 필요한 이유
  • 자동화 파이프라인 아키텍처
  • GitHub Actions를 활용한 CI/CD
    • 학습 트리거 워크플로우
    • 학습 Job 정의
    • 평가 Job 정의
    • 배포 Job 정의
  • 품질 게이트 (Quality Gate)
    • 회귀 테스트
  • 데이터 파이프라인 자동화
  • 롤백 전략
  • 알림과 모니터링
  • 전체 자동화 체크리스트
  • 정리