하나의 백엔드로 Detection/Classification/Segmentation 지원하기: task별 데이터셋 포맷 분기

UI의 task 드롭다운은 있었지만 백엔드는 항상 detection만 돌리고 있었습니다. 5 사이즈 × 3 task = 15 variant를 task별 데이터셋 포맷(bbox/ImageFolder/polygon)과 메트릭 컬럼 분기로 통합한 기록입니다.

하나의 백엔드로 Detection/Classification/Segmentation 지원하기: task별 데이터셋 포맷 분기

1. 문제 상황: UI는 있는데 백엔드는 모르고 있다

이미지 라벨링 플랫폼의 학습 화면에는 드롭다운이 두 개 있었습니다.

  • Task: detection / classification / segmentation
  • Base Model: yolov8n / s / m / l / x

사용자는 자연스럽게 "아, 이 두 개를 조합해서 15가지 모델 중 하나로 학습되는구나"라고 생각했을 겁니다. 그런데 실제 코드를 들여다보면 백엔드가 받는 요청은 이랬습니다.

# routes/training.py (Before)
def api_train_start():
    d = request.get_json()
    model_name = Path(d.get("model", "yolov8n.pt")).name
    if not model_name.endswith(".pt"):
        return jsonify({"error": "모델 형식은 .pt만 지원합니다"}), 400
    # ... task 파라미터는 어디에도 없음

task는 payload에 있긴 했지만 아무도 읽지 않았습니다. 백엔드는 항상 detection 파이프라인으로 돌렸고, classification이나 segmentation을 고른 사용자는 결과로 detection 모델을 받고 있었습니다. 심지어 에러도 나지 않았습니다. YOLO는 yolov8n.pt가 detection 모델이라는 것을 알고 그대로 학습했으니까요.

그리고 또 하나. 드롭다운에는 ResNet과 EfficientNet 같은 옵션도 있었는데, 이것들은 아예 구현된 적이 없었습니다. 플레이스홀더로 UI에 들어가 있었던 것입니다. 이 프로젝트의 드롭다운은 세 가지 거짓말을 동시에 하고 있었습니다.

UI가 약속한 것 실제 동작
detection / classification / segmentation 선택 항상 detection
yolov8n / s / m / l / x 5개 사이즈 n, s, m, l만 실제 다운로드됨 (x는 없음)
ResNet / EfficientNet 백본 구현 안 됨. 선택 시 조용히 실패

2. 목표

이 세 가지 거짓말을 동시에 고치는 것이 목표였습니다. 최종적으로는 다음이 모두 작동해야 합니다.

  • 5 사이즈 × 3 task = 15개 YOLOv8 variant 전부 지원
  • task별 데이터셋 포맷 자동 변환 (bbox → YOLO txt, polygon → YOLO txt, 이미지 → ImageFolder)
  • task별 메트릭 파싱 분기 (detection: mAP, classification: accuracy)
  • UI에서 ResNet/EfficientNet 옵션 제거 (거짓 옵션 제거)

3. 15개 variant의 이름 체계

Ultralytics YOLOv8은 task 접미사 규칙이 간단합니다.

Task 파일명
Detection yolov8{n,s,m,l,x}.pt
Classification yolov8{n,s,m,l,x}-cls.pt
Segmentation yolov8{n,s,m,l,x}-seg.pt

접미사만 다릅니다. 그래서 백엔드에서 사이즈 + task → 파일명 변환은 문자열 조립만으로 충분합니다.

task_suffix = {
    "detection": "",
    "classification": "-cls",
    "segmentation": "-seg",
}
model_name = f"{model_size}{task_suffix[task]}.pt"
# 예: yolov8m + segmentation → "yolov8m-seg.pt"

프로젝트 루트에 base_models/ 디렉토리를 만들고, 이 15개 파일을 전부 미리 받아 두는 scripts/download_base_models.sh를 추가했습니다. 폐쇄망 배포를 전제로 한 프로젝트라서, 첫 학습에서 Ultralytics가 자동으로 다운받는 기본 동작을 쓸 수 없습니다. 미리 받아 둔 파일을 씁니다.

4. routes/training.py: 파라미터 검증

api_train_start()를 수정해 task와 사이즈를 분리해 검증합니다.

Before

# routes/training.py (Before)
@bp.route("/train/start", methods=["POST"])
@require_login
def api_train_start():
    uid = session["user_id"]
    d = request.get_json()

    model_name = Path(d.get("model", "yolov8n.pt")).name
    if not model_name.endswith(".pt"):
        return jsonify({"error": "모델 형식은 .pt만 지원합니다"}), 400
    # ... task 파라미터 무시

After

# routes/training.py (After)
@bp.route("/train/start", methods=["POST"])
@require_login
def api_train_start():
    uid = session["user_id"]
    d = request.get_json()

    # 모델 사이즈: "yolov8n", "yolov8s", "yolov8m", "yolov8l", "yolov8x"
    model_size = Path(d.get("model", "yolov8n")).name.replace(".pt", "")
    if not model_size.startswith("yolov8") or len(model_size) != 7:
        return jsonify({"error": "모델은 yolov8n/s/m/l/x 중 하나여야 합니다"}), 400

    # 태스크별 베이스 모델 파일명 결정
    task = d.get("task", "detection")
    task_suffix = {
        "detection": "",
        "classification": "-cls",
        "segmentation": "-seg",
    }
    if task not in task_suffix:
        return jsonify({
            "error": "task는 detection/classification/segmentation 중 하나여야 합니다"
        }), 400
    model_name = f"{model_size}{task_suffix[task]}.pt"

    epochs = int(d.get("epochs", 50))
    batch = int(d.get("batch", 16))
    img_size = int(d.get("imgSize", 640))

    # ... 이하 동일

검증 포인트

  1. 사이즈 검증: model_size.startswith("yolov8")len(model_size) == 7을 동시에 확인합니다. yolov8n부터 yolov8x까지 정확히 7글자이므로, 이 두 조건으로 yolov8n/s/m/l/x 5가지만 허용합니다. 실은 이 조건만으로는 yolov8a 같은 가짜 값도 통과하므로, 더 엄격하게 하려면 model_size[-1] in "nsmlx" 체크를 추가할 수 있습니다.
  2. task 검증: task_suffix 딕셔너리에 등록된 세 값만 허용합니다. 알 수 없는 값은 400으로 거절합니다.
  3. 조합: 검증된 두 값을 f-string으로 조립해 최종 파일명을 만듭니다.

task 기록

record 딕셔너리에 task 필드를 추가해 학습 이력에 task 정보를 남겼습니다.

record = {
    "id": train_id,
    "model": model_name,
    "task": task,          # ← 추가
    "status": "running",
    "epochs": epochs,
    "progress": 0,
    "startedAt": _now(),
}

나중에 학습 히스토리에서 "이 학습은 어떤 task였지?"를 알기 위해 필수적입니다. UI에서 이력을 보여 줄 때도 task 뱃지를 달 수 있습니다.

5. submit_training / run_yolo_train에 task 전파

서비스 레이어도 task 파라미터를 받도록 시그니처를 확장했습니다.

# services/training_service.py

def submit_training(uid, train_id, labeled_imgs, model_name, task, epochs, batch, img_size):
    """Submit training job to process pool. Returns immediately."""
    future = _executor.submit(
        run_yolo_train,
        uid, train_id, labeled_imgs, model_name, task, epochs, batch, img_size,
    )
    _active_futures[train_id] = future
    # ...
    return future


def run_yolo_train(uid, train_id, labeled_imgs, model_name, task, epochs, batch, img_size):
    """실제 YOLO 학습을 수행하고 결과를 DB에 반영.

    task: "detection" | "classification" | "segmentation"
    백그라운드 프로세스에서 실행되므로 Flask app context가 필요함.
    """
    from ultralytics import YOLO
    import torch
    # ...

submit_trainingrun_yolo_train 모두 새 파라미터 task를 추가했습니다. 3편에서 다룬 ProcessPoolExecutor 경로를 그대로 따르므로, 워커 프로세스에서도 동일한 파라미터가 전달됩니다.

6. Task별 데이터셋 포맷 분기

가장 큰 변화가 있는 부분입니다. YOLOv8은 task마다 기대하는 디렉토리 구조와 라벨 포맷이 다릅니다.

Task 디렉토리 구조 라벨 포맷
Detection train/images/*.jpg + train/labels/*.txt 각 txt에 class_id cx cy w h (0~1 정규화)
Classification train/<class_name>/*.jpg (ImageFolder) 디렉토리 이름이 곧 클래스
Segmentation train/images/*.jpg + train/labels/*.txt 각 txt에 class_id x1 y1 x2 y2 ... (polygon, 0~1 정규화)

run_yolo_train() 내부에서 이 세 가지를 분기합니다.

Classification: ImageFolder 구조

if task == "classification":
    # ImageFolder 구조: train/<class_name>/*.jpg, val/<class_name>/*.jpg
    for cls_name in classes:
        (train_root / "train" / cls_name).mkdir(parents=True, exist_ok=True)
        (train_root / "val" / cls_name).mkdir(parents=True, exist_ok=True)

    def _copy_classification_data(img_list, split_name):
        for img_info in img_list:
            src_img = get_user_upload_dir(uid) / img_info['name']
            if not src_img.exists():
                continue
            annos = img_info.get('annotations', [])
            if not annos:
                continue
            # 첫 번째 어노테이션의 클래스를 이미지 레이블로 사용
            label = annos[0].get('className')
            if label not in class_map:
                continue
            shutil.copy(src_img, train_root / split_name / label / img_info['name'])

    _copy_classification_data(train_imgs, "train")
    _copy_classification_data(val_imgs, "val")

    # Classification은 data.yaml 대신 디렉토리 경로를 직접 전달
    data_arg = str(train_root)

Classification의 핵심은 이미지를 클래스별 디렉토리로 분류 복사하는 것입니다. YOLOv8 classification은 scikit-learn의 ImageFolder 관습을 따르기 때문에, train/cat/*.jpg, train/dog/*.jpg 같은 구조를 만들고 YOLO에게 train_root 경로를 넘기면 됩니다.

이때 프로젝트 도메인의 한 가지 결정이 있습니다. 원래 이 플랫폼은 "한 이미지에 여러 어노테이션"을 저장합니다(객체 검출 특성). 그런데 classification은 "이미지 한 장 = 한 클래스"입니다. 어느 어노테이션의 클래스를 레이블로 쓸지 결정해야 합니다. 여기서는 첫 번째 어노테이션의 클래스를 사용했습니다.

label = annos[0].get('className')  # ← 첫 어노테이션 채택

이것은 도메인적 타협입니다. "한 이미지에 여러 클래스 어노테이션이 있을 때 어느 것을 대표로 쓸 것인가"에 대한 완벽한 답은 없습니다. 대안으로 "가장 큰 박스의 클래스", "가장 신뢰도 높은 클래스", "사용자가 명시 지정" 등이 있지만, 가장 단순한 선택을 했습니다. UI에서 classification을 고를 때 "각 이미지의 첫 어노테이션이 레이블로 사용됩니다"라는 안내를 띄울 필요가 있습니다.

Detection: bbox → YOLO txt

else:
    # detection, segmentation: YOLO TXT 포맷
    train_img_dir = train_root / "train" / "images"
    train_lbl_dir = train_root / "train" / "labels"
    val_img_dir   = train_root / "val" / "images"
    val_lbl_dir   = train_root / "val" / "labels"
    for d in [train_img_dir, train_lbl_dir, val_img_dir, val_lbl_dir]:
        d.mkdir(parents=True, exist_ok=True)

    def _write_yolo_label(anno_list, out_file, task_type):
        """어노테이션을 YOLO TXT 포맷으로 저장 (좌표는 0-1 정규화)"""
        with open(out_file, "w") as f:
            for a in anno_list:
                cls_id = class_map.get(a['className'], 0)
                atype = a.get('type', 'bbox')

                if task_type == "segmentation" and atype == "polygon":
                    # 폴리곤: class_id x1 y1 x2 y2 ... (정규화)
                    pts = a.get('points', [])
                    if len(pts) < 3:
                        continue
                    coords = " ".join(
                        f"{p['x'] / 100:.6f} {p['y'] / 100:.6f}" for p in pts
                    )
                    f.write(f"{cls_id} {coords}\n")

                elif task_type == "detection" and atype == "bbox":
                    # bbox: class_id cx cy w h (정규화)
                    cx = (a['x'] + a['w'] / 2) / 100
                    cy = (a['y'] + a['h'] / 2) / 100
                    w = a['w'] / 100
                    h = a['h'] / 100
                    f.write(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n")

Detection과 Segmentation은 디렉토리 구조는 공유하지만 라벨 포맷이 다릅니다. 그래서 이미지 복사 함수는 하나로 쓰고, 라벨 저장 함수(_write_yolo_label)만 task 분기하도록 설계했습니다.

Segmentation: polygon 좌표 직렬화

_write_yolo_label 함수 안에서 segmentation 분기 부분이 핵심입니다.

if task_type == "segmentation" and atype == "polygon":
    pts = a.get('points', [])
    if len(pts) < 3:  # 최소 3점 이상이어야 폴리곤 성립
        continue
    coords = " ".join(
        f"{p['x'] / 100:.6f} {p['y'] / 100:.6f}" for p in pts
    )
    f.write(f"{cls_id} {coords}\n")

YOLOv8의 segmentation 라벨 포맷은 이런 모양입니다.

0 0.15 0.22 0.28 0.30 0.35 0.45 0.24 0.50 0.12 0.40

첫 번째 값이 클래스 id, 그 다음부터 x1 y1 x2 y2 ... xn yn 순으로 폴리곤 정점들이 0~1 정규화 좌표로 나열됩니다. 이 프로젝트의 어노테이션은 points: [{x: 15, y: 22}, ...] 형태로 0~100 백분율 좌표로 저장돼 있어서, / 100으로 나누어 0~1 범위로 변환합니다.

len(pts) < 3 체크도 중요합니다. 폴리곤은 최소 3점(삼각형)이 필요하며, 점이 부족한 어노테이션은 유효하지 않으므로 건너뜁니다. 사용자가 실수로 2점만 찍고 저장하는 경우가 있어 방어가 필요합니다.

왜 "detection/segmentation" 공통 분기로 묶었나

처음에는 세 task를 완전 독립 분기로 작성했다가, 중복이 너무 많아 다시 정리했습니다. 관찰한 점은 다음과 같습니다.

  • Detection과 Segmentation은 디렉토리 구조(train/images + train/labels)가 동일하고, 이미지 복사 로직도 완전히 같습니다. 라벨 파일 내용만 다릅니다.
  • Classification은 디렉토리 구조 자체가 다릅니다(ImageFolder).

그래서 코드 구조를 "classification이냐 아니냐" 로 먼저 갈라 놓고, detection/segmentation 공통 분기 안에서 라벨 쓰기 함수 하나가 task_type에 따라 다시 분기하는 2-단계 구조로 만들었습니다. 중복을 최소화하면서 분기의 의미가 명확합니다.

7. data.yaml의 차이

세 task는 YOLO에게 넘기는 "데이터 인자"가 다릅니다.

# detection / segmentation
yaml_path = train_root / "data.yaml"
with open(yaml_path, "w") as f:
    f.write(f"path: {train_root}\n")
    f.write("train: train/images\n")
    f.write("val: val/images\n")
    f.write(f"nc: {len(classes)}\n")
    f.write(f"names: {classes}\n")
data_arg = str(yaml_path)  # → "/.../data.yaml"

# classification
# data.yaml을 쓰지 않고 디렉토리 경로를 직접 전달
data_arg = str(train_root)  # → "/.../train_workspace/<train_id>"

Classification은 data.yaml을 만들지 않습니다. YOLOv8 classification API는 디렉토리 경로를 받으면 자동으로 ImageFolder 구조를 읽어 클래스 목록을 추출하기 때문입니다.

그래서 이후 model.train(data=data_arg, ...) 호출 시점에 data_arg는 task에 따라 파일 경로일 수도 있고 디렉토리 경로일 수도 있습니다. 변수 하나로 두 경우를 모두 받아들입니다.

8. 메트릭 파싱 분기

마지막 조각은 결과 메트릭입니다. YOLO는 학습 완료 후 results.csv를 만드는데, task마다 컬럼 이름이 다릅니다.

Task 주요 메트릭 컬럼
Detection metrics/mAP50(B), metrics/precision(B), metrics/recall(B)
Segmentation metrics/mAP50(M), metrics/precision(M), metrics/recall(M)
Classification metrics/accuracy_top1, metrics/accuracy_top5

(B)는 Bounding box, (M)은 Mask를 의미합니다. 동일한 "mAP50"이지만 대상 객체가 박스인지 마스크인지에 따라 컬럼명이 다릅니다.

CSV 파싱 로직도 task별 분기가 필요합니다.

def _parse_metrics_from_csv(csv_path: Path, task: str) -> dict:
    """task별로 메트릭 컬럼을 파싱"""
    import csv

    if not csv_path.exists():
        return {}

    with open(csv_path, "r") as f:
        reader = csv.DictReader(f)
        rows = list(reader)
    if not rows:
        return {}

    last = rows[-1]  # 마지막 에포크의 값

    if task == "classification":
        return {
            "accuracy_top1": float(last.get("metrics/accuracy_top1", 0)),
            "accuracy_top5": float(last.get("metrics/accuracy_top5", 0)),
        }
    elif task == "segmentation":
        return {
            "map50": float(last.get("metrics/mAP50(M)", 0)),
            "precision": float(last.get("metrics/precision(M)", 0)),
            "recall": float(last.get("metrics/recall(M)", 0)),
        }
    else:  # detection
        return {
            "map50": float(last.get("metrics/mAP50(B)", 0)),
            "precision": float(last.get("metrics/precision(B)", 0)),
            "recall": float(last.get("metrics/recall(B)", 0)),
        }

SSE 진행 모니터링 쪽도 같은 분기를 적용해, task에 따라 올바른 컬럼을 읽도록 수정했습니다. 이 프로젝트의 메트릭 스키마는 이제 task에 따라 다른 키를 갖습니다. UI에서는 어떤 학습인지 확인 후 키를 고릅니다.

9. 프론트엔드: 거짓 옵션 제거

프론트엔드 쪽도 정리했습니다.

  • ResNet/EfficientNet 제거: 구현된 적 없는 옵션을 드롭다운에서 제거
  • yolov8x 추가: 기존에 없던 X-Large 옵션 추가
  • 최소 이미지 1장 → 2장: 5편의 train/val 분리와 맞물려, 최소 2장이 있어야 proper split 성립
 <select id="modelSelect">
-  <option value="resnet50">ResNet-50</option>
-  <option value="efficientnet-b0">EfficientNet-B0</option>
   <option value="yolov8n">YOLOv8n (Nano)</option>
   <option value="yolov8s">YOLOv8s (Small)</option>
   <option value="yolov8m">YOLOv8m (Medium)</option>
   <option value="yolov8l">YOLOv8l (Large)</option>
+  <option value="yolov8x">YOLOv8x (X-Large)</option>
 </select>

UI에서 "거짓 옵션"을 떼어낸 이 변경은 약속된 기능과 실제 기능을 일치시키는 작업 입니다. 코드 변경은 작지만, 제품 신뢰의 측면에서 큰 정리입니다. "UI에 있는 옵션은 전부 동작한다"는 원칙을 회복하는 것입니다.

10. 베이스 모델 15개 다운로드 스크립트

폐쇄망 배포 환경에서는 Ultralytics의 자동 다운로드가 동작하지 않습니다. 그래서 15개 variant를 미리 받아 base_models/에 두는 스크립트를 만들었습니다.

#!/usr/bin/env bash
# scripts/download_base_models.sh
set -euo pipefail

MODELS_DIR="$(dirname "$0")/../base_models"
mkdir -p "$MODELS_DIR"

SIZES=("n" "s" "m" "l" "x")
TASKS=("" "-cls" "-seg")  # detection 은 접미사 없음

for size in "${SIZES[@]}"; do
    for task in "${TASKS[@]}"; do
        file="yolov8${size}${task}.pt"
        url="https://github.com/ultralytics/assets/releases/download/v8.2.0/${file}"
        if [ -f "$MODELS_DIR/$file" ]; then
            echo "  [skip] $file"
            continue
        fi
        echo "  [download] $file"
        curl -L -o "$MODELS_DIR/$file" "$url"
    done
done

echo "Done. 15 base models ready in: $MODELS_DIR"

15개 파일의 총 용량은 상당합니다(수 GB). base_models/README.md에 용량 표를 적어 두어 사용자가 준비할 디스크 공간을 가늠할 수 있게 했습니다.

11. 전체 플로우 요약

사용자가 "segmentation + yolov8m" 을 고르고 학습을 시작하면:

  1. 프론트엔드: {model: "yolov8m", task: "segmentation", ...} 전송
  2. routes/training.py: 사이즈와 task 분리 검증, model_name = "yolov8m-seg.pt" 조립
  3. services/training_service.py: submit_training(..., task="segmentation", ...)
  4. 워커 프로세스 run_yolo_train():
    • detection/segmentation 분기로 진입
    • train/images, train/labels, val/images, val/labels 생성
    • _write_yolo_label(..., task_type="segmentation") → polygon 포맷으로 저장
    • data.yaml 생성, data_arg = str(yaml_path)
    • YOLO("base_models/yolov8m-seg.pt").train(data=data_arg, ...)
  5. 학습 완료 후: _parse_metrics_from_csv(csv_path, task="segmentation")(M) 컬럼 파싱
  6. DB 저장: TrainingRecord.task = "segmentation", map50 등 메트릭 저장
  7. 프론트엔드: 학습 이력 화면에 "segmentation / yolov8m" 뱃지와 메트릭 표시

이 일곱 단계가 모두 task 하나의 파라미터를 관통해 작동합니다. 한 파라미터가 서비스 경계 세 개(라우트 / 서비스 / 워커)를 넘어가며 분기의 축이 되는 구조입니다.

12. 핵심 개념 정리

개념 역할
task 접미사 규칙 {size}{suffix}.pt로 15개 variant 통합
ImageFolder 구조 classification 데이터셋의 표준 레이아웃 (train/<class>/*.jpg)
YOLO bbox 포맷 class_id cx cy w h (정규화 좌표)
YOLO polygon 포맷 class_id x1 y1 x2 y2 ... (정규화 좌표)
data_arg 변수 detection/segmentation은 yaml 파일, classification은 디렉토리 경로
task별 메트릭 컬럼 (B) vs (M) vs accuracy_top1/top5
첫 어노테이션 레이블 채택 classification에서 여러 어노테이션 중 첫 것을 이미지 레이블로

13. 베스트 프랙티스 체크리스트

  • [ ] UI에 있는 옵션이 실제로 백엔드에서 처리되나요?
  • [ ] task와 사이즈를 서버에서 독립적으로 검증하나요?
  • [ ] 검증 실패 시 의미 있는 에러 메시지를 반환하나요?
  • [ ] task별 데이터셋 포맷 변환이 분리된 함수로 구현돼 있나요?
  • [ ] classification 시 이미지 대표 레이블 선택 규칙이 문서화돼 있나요?
  • [ ] data_arg 같은 다형 변수가 명시적으로 주석 처리돼 있나요?
  • [ ] 메트릭 파싱이 task에 따라 올바른 컬럼을 읽나요?
  • [ ] 베이스 모델이 폐쇄망에서도 사용 가능한 위치에 있나요?
  • [ ] UI에서 구현되지 않은 옵션(ResNet 등)이 제거됐나요?

14. FAQ

Q1. classification에서 한 이미지에 여러 어노테이션이 있을 때 "첫 번째"를 고르는 게 맞나요?
A. 완벽한 답은 없습니다. 대안으로 "가장 큰 박스", "가장 신뢰도 높은 클래스", "가장 빈번한 클래스" 등이 있습니다. 이 프로젝트는 단순성을 위해 "첫 번째"를 선택했고, UI에 "classification은 이미지당 하나의 클래스를 사용합니다"라는 안내를 추가할 계획입니다. 장기적으로는 classification용 별도 어노테이션 타입을 만들어 사용자가 명시적으로 이미지 레벨 레이블을 지정하도록 바꿀 여지가 있습니다.

Q2. segmentation에서 bbox 어노테이션을 썼을 때는 어떻게 되나요?
A. _write_yolo_label() 내부에서 atype == "polygon"이 아니면 건너뜁니다. 즉, segmentation 학습을 선택했는데 라벨이 bbox뿐이라면 해당 어노테이션은 학습에 포함되지 않습니다. 장기적으로는 "bbox를 polygon 4점으로 자동 변환" 같은 폴백을 넣을 수 있지만, 현재는 UI에서 task를 먼저 고르고 그에 맞는 어노테이션을 달도록 유도하는 방식입니다.

Q3. yolov8n과 yolov8x는 학습 시간/리소스 차이가 얼마나 나나요?
A. 대략적으로 yolov8n이 가장 작고 빠르며, yolov8x가 10배 이상 큰 모델입니다. yolov8n은 CPU만으로도 학습 가능한 수준이지만, yolov8x는 실용적으로 GPU가 필요합니다. 학습 시간도 데이터 크기에 따라 수 분에서 수 시간까지 차이가 납니다. 작은 데이터셋과 검증 용도에는 yolov8n, 정확도가 중요한 프로덕션에는 yolov8m~l, 연구 목적에는 yolov8x를 권장합니다.

Q4. 사이즈 검증 len(model_size) == 7이 너무 느슨하지 않나요?
A. 맞습니다. yolov8a도 7글자이므로 통과합니다. 더 엄격하게 하려면 model_size[-1] in {"n", "s", "m", "l", "x"} 체크를 추가해야 합니다. 현재는 base_models 디렉토리에 실제 파일이 없으면 어차피 YOLO가 FileNotFoundError를 내기 때문에 방어가 한 겹 더 있는 상태이고, 명시적 validator는 후속 개선 과제입니다.

Q5. 왜 base_models 접미사가 -cls, -seg인데 detection만 빈 문자열인가요?
A. Ultralytics의 네이밍 규칙입니다. detection이 "기본값"으로 취급되기 때문에 접미사가 없습니다. 만약 이 규칙이 혼란스럽다면 내부 설정에서 "detection": "-det" 같이 명시해 볼 수도 있지만, 그 경우 파일명 자체가 Ultralytics의 표준과 달라져 자동 다운로드/로드에서 문제가 생길 수 있습니다. 외부 생태계와 맞추는 것이 가장 편합니다.

15. 참고 자료

  • Ultralytics YOLOv8 Tasks: 검색 키워드 ultralytics yolov8 detection classification segmentation
  • YOLOv8 Classification 데이터셋 가이드: 검색 키워드 yolov8 classification imagefolder dataset
  • YOLOv8 Segmentation 폴리곤 포맷: 검색 키워드 yolov8 segmentation polygon label format
  • Ultralytics YOLO 베이스 모델 릴리스 페이지: 검색 키워드 github ultralytics assets releases yolov8

16. 다음 단계

지금까지는 기능 개선과 리팩토링 이야기였습니다. 다음 편은 보안으로 넘어갑니다. Fail-fast 설정 검증(ALLOWED_ORIGINS 미설정 시 서버 기동 거부)과 Path().name 관용구로 path traversal을 막는 두 가지 보안 패턴을 묶어서 다룹니다. 한 줄짜리 수정이지만 공격 표면에 큰 차이를 만드는 종류의 개선입니다.

🐍 Flask 백엔드 실전 시리즈 (9부작)

  1. 모놀리식 app.py를 Blueprint로 분해하기
  2. JSON 파일 DB에서 PostgreSQL로 마이그레이션
  3. ML 학습 백그라운드 실행: ProcessPoolExecutor
  4. Python/SQLAlchemy N+1 쿼리 잡기
  5. train과 val이 같은 폴더일 때의 조용한 ML 버그
  6. 하나의 백엔드로 Detection/Classification/Segmentation (현재 글)
  7. Fail-fast 설정 검증과 Path Traversal 방어
  8. Rate Limiting을 걷어낸 날: 폐쇄망 보안
  9. 작지만 기억할 만한 네 가지 교훈