WEGO LIMO 객체 인식 알고리즘 분석 — YOLO11n으로 교통 표지판과 신호등 학습하기

1. 개요

이번 글에서는 WEGO LIMO 자율주행 로봇에서 사용할 수 있는 객체 인식 모델을 학습하기 위한 YOLO11n 기반 Python 코드를 자세히 살펴보겠습니다.

사용한 소스는 Jupyter Notebook 형태의 다음 파일입니다.

train_yolo11n_object_detection_docker.ipynb

이 프로그램은 Ultralytics의 YOLO11n 사전 학습 모델을 이용하여 WEGO LIMO 주행 환경에서 필요한 교통 관련 객체를 학습합니다.

학습 대상은 총 8개 클래스입니다.

nc: 8

names:
  - speed_up
  - slow_down
  - turn_right
  - Crosswalk
  - Red_light
  - Green_light
  - Yellow_light
  - Crossing_gate

즉 카메라 영상에서 다음과 같은 객체를 찾는 것이 모델의 목적입니다.

  1. speed_up : 속도 증가 표지
  2. slow_down : 감속 표지
  3. turn_right : 우회전 표지
  4. Crosswalk : 횡단보도
  5. Red_light : 빨간 신호등
  6. Green_light : 초록 신호등
  7. Yellow_light : 노란 신호등
  8. Crossing_gate : 차단기

중요한 점은 이 Notebook 자체가 LIMO를 직접 주행시키는 프로그램은 아니라는 것입니다.

이 프로그램의 역할은

카메라 이미지
    ↓
YOLO11 객체 탐지 모델 학습
    ↓
best.pt 생성
    ↓
추후 실시간 객체 인식 프로그램에서 사용

까지입니다.

실제 LIMO에서 빨간불을 보고 정지하거나, 횡단보도에서 감속하거나, 우회전 표지를 보고 회전하는 기능은 학습된 best.pt 모델을 ROS 2 카메라 노드 및 주행 제어 노드와 연결하여 별도로 구현하게 됩니다.

2. 전체 프로그램 동작 구조

Notebook의 전체 실행 과정은 다음과 같이 구성되어 있습니다.

1. Docker 작업 환경 설정
        ↓
2. Ultralytics YOLO 설치 확인
        ↓
3. PyTorch / CUDA / GPU 확인
        ↓
4. YOLO 데이터셋 탐색 및 압축 해제
        ↓
5. 이미지와 라벨 개수 검사
        ↓
6. 8개 클래스 dataset YAML 생성
        ↓
7. YOLO11n 사전 학습 모델 로드
        ↓
8. Transfer Learning 수행
        ↓
9. 200 Epoch 학습
        ↓
10. best.pt / last.pt 저장
        ↓
11. best.pt 검증
        ↓
12. Precision / Recall / mAP 계산

YOLO 학습에 필요한 환경 준비부터 모델 저장과 검증까지 하나의 Notebook에서 자동으로 처리할 수 있도록 구성되어 있습니다.

3. Docker 환경을 사용하는 이유

이 코드는 원래 Google Colab 기반 코드를 Docker와 Jupyter 환경에서 사용할 수 있도록 수정한 구조입니다.

기존 Colab에서는 보통 다음과 같은 코드가 사용됩니다.

from google.colab import drive
drive.mount('/content/drive')

하지만 실제 로봇 개발 환경이나 로컬 GPU PC에서는 Google Drive를 사용하는 방식이 불편합니다.

따라서 이 Notebook에서는 기본 작업 경로를 다음과 같이 변경했습니다.

/workspace

권장 폴더 구조는 다음과 같습니다.

/workspace/
├── dataset.zip
├── dataset/
│   └── yolo_images/
│       ├── images/
│       │   ├── train/
│       │   └── val/
│       └── labels/
│           ├── train/
│           └── val/
└── runs/

Docker 실행 시 호스트 디렉터리를 /workspace로 마운트하면 컨테이너가 종료되어도 데이터셋과 학습 결과가 호스트에 그대로 남습니다.

이는 로봇 비전 모델을 반복적으로 학습할 때 매우 유용합니다.

4. 기본 환경 설정

첫 번째 코드에서는 학습에 사용할 주요 경로와 파라미터를 정의합니다.

import os
import sys
import subprocess
from pathlib import Path

여기서 Path를 사용하는 이유는 문자열로 경로를 관리하는 것보다 Linux와 Docker 환경에서 파일 경로를 안정적으로 처리할 수 있기 때문입니다.

작업 디렉터리는 다음과 같이 설정합니다.

WORKSPACE = Path(
    os.getenv("WORKSPACE_DIR", "/workspace")
).resolve()

환경변수 WORKSPACE_DIR가 존재하면 해당 경로를 사용하고, 없으면 기본값으로 /workspace를 사용합니다.

같은 방식으로 데이터셋 ZIP 파일과 학습 결과 디렉터리를 정의합니다.

DATASET_ZIP = Path(
    os.getenv(
        "DATASET_ZIP",
        str(WORKSPACE / "dataset.zip")
    )
).resolve()

RUNS_DIR = Path(
    os.getenv(
        "RUNS_DIR",
        str(WORKSPACE / "runs")
    )
).resolve()

따라서 기본 경로는 다음과 같습니다.

데이터셋 ZIP
/workspace/dataset.zip

학습 결과
/workspace/runs

5. YOLO11n 학습 파라미터

학습과 관련된 기본 설정은 다음과 같습니다.

MODEL_NAME = os.getenv(
    "YOLO_MODEL",
    "yolo11n.pt"
)

EPOCHS = int(
    os.getenv("EPOCHS", "200")
)

IMGSZ = int(
    os.getenv("IMGSZ", "640")
)

BATCH = int(
    os.getenv("BATCH", "16")
)

WORKERS = int(
    os.getenv("WORKERS", "4")
)

현재 Notebook에서 사용하는 기본값은 다음과 같습니다.

MODEL   = yolo11n.pt
EPOCHS  = 200
IMGSZ   = 640
BATCH   = 16
WORKERS = 4

각 값의 의미를 살펴보겠습니다.

1) MODEL_NAME

MODEL_NAME = "yolo11n.pt"

YOLO11의 Nano 모델을 사용한다는 의미입니다.

Nano 계열은 모델 크기가 작고 추론 속도가 빠르기 때문에 임베디드 시스템이나 이동 로봇처럼 실시간 처리가 중요한 환경에서 검토하기 좋은 모델입니다.

2) EPOCHS

EPOCHS = 200

전체 학습 데이터를 200번 반복하여 학습합니다.

1 Epoch는 모든 학습 데이터를 한 번 사용했다는 의미입니다.

3) IMGSZ

IMGSZ = 640

학습 입력 이미지 크기를 640 기준으로 사용합니다.

YOLO가 학습 과정에서 입력 이미지를 처리하기 위한 기본 해상도입니다.

4) BATCH

BATCH = 16

한 번의 학습 연산에서 최대 16개의 이미지를 묶어 처리합니다.

GPU 메모리가 부족할 경우 이 값을

16 → 8 → 4

와 같이 낮추면 됩니다.

5) WORKERS

WORKERS = 4

데이터를 GPU에 공급하기 위해 사용하는 DataLoader 작업 프로세스 수입니다.

6. 환경변수를 사용한 설정의 장점

코드를 직접 수정하지 않고 Docker 실행 시 학습 조건을 변경할 수 있습니다.

예를 들어 다음과 같이 사용할 수 있습니다.

-e EPOCHS=300
-e BATCH=8
-e DEVICE=0

그러면 Python 파일을 수정하지 않아도

Epoch = 300
Batch = 8
GPU 0 사용

으로 변경할 수 있습니다.

여러 PC와 GPU에서 같은 소스를 사용해야 하는 개발 환경에서는 좋은 구성 방식입니다.

7. Ultralytics 설치 확인

다음 부분에서는 YOLO를 제공하는 ultralytics 패키지가 설치되어 있는지 확인합니다.

try:
    import ultralytics

    print(
        "Ultralytics already installed:",
        ultralytics.__version__
    )

except ImportError:

    subprocess.check_call([
        sys.executable,
        "-m",
        "pip",
        "install",
        "--no-cache-dir",
        "ultralytics"
    ])

이미 설치되어 있다면 그대로 사용합니다.

설치되어 있지 않은 경우 자동으로

pip install ultralytics

와 동일한 작업을 수행합니다.

실제 Notebook 실행 결과에서는

Ultralytics 8.4.133

버전이 사용되었습니다.

마지막으로 YOLO 클래스를 가져옵니다.

from ultralytics import YOLO

이후 모델 생성, 학습, 검증 모두 이 YOLO 클래스를 사용합니다.

8. GPU와 CUDA 확인

딥러닝 학습에서는 GPU 사용 여부가 매우 중요합니다.

Notebook은 다음 코드를 이용해 CUDA 상태를 확인합니다.

import torch

print("PyTorch      :", torch.__version__)
print("CUDA runtime :", torch.version.cuda)
print("CUDA usable  :", torch.cuda.is_available())

특히 중요한 코드는 다음 부분입니다.

torch.cuda.is_available()

결과가

True

이면 NVIDIA GPU를 사용할 수 있습니다.

결과가

False

이면 CPU로 학습합니다.

이번 Notebook의 실제 실행 환경은 다음과 같이 기록되어 있습니다.

Python       : 3.10.12
PyTorch      : 2.11.0+cu128
CUDA runtime : 12.8
CUDA usable  : True
GPU count    : 1
GPU          : NVIDIA GeForce RTX 5050 Laptop GPU

따라서 실제 학습은 GPU에서 수행되었습니다.

9. nvidia-smi까지 검사하는 이유

코드에서는 PyTorch의 CUDA 확인만 하는 것이 아니라

shutil.which("nvidia-smi")

를 이용해 nvidia-smi 명령이 존재하는지도 확인합니다.

존재할 경우 다음 명령을 실행합니다.

subprocess.run(["nvidia-smi"])

이를 통해 다음 항목을 확인할 수 있습니다.

  1. NVIDIA 드라이버 버전
  2. GPU 모델
  3. GPU 메모리
  4. GPU 사용률
  5. CUDA 드라이버 지원 상태

Docker 환경에서는 호스트 PC에서는 GPU가 정상이어도 컨테이너 안으로 GPU가 전달되지 않는 문제가 발생할 수 있습니다.

따라서

torch.cuda.is_available()

와

nvidia-smi

를 함께 검사하는 방법은 GPU Docker 환경 문제를 확인하는 데 유용합니다.

10. YOLO 데이터셋 구조 검사

객체 탐지 학습에는 이미지와 해당 이미지의 객체 위치를 기록한 라벨 파일이 필요합니다.

코드는 먼저 다음 함수를 정의합니다.

def is_yolo_dataset_root(path: Path) -> bool:
    return (
        (path / "images" / "train").is_dir()
        and
        (path / "images" / "val").is_dir()
        and
        (path / "labels" / "train").is_dir()
        and
        (path / "labels" / "val").is_dir()
    )

다음 4개의 폴더가 모두 존재해야 정상적인 YOLO 데이터셋으로 판단합니다.

images/train
images/val
labels/train
labels/val

즉 기본 구조는 다음과 같습니다.

yolo_images/
├── images/
│   ├── train/
│   └── val/
└── labels/
    ├── train/
    └── val/

11. 데이터셋 자동 검색 알고리즘

데이터셋 경로를 고정하지 않고 자동 탐색하도록 만든 것도 이 코드의 특징입니다.

먼저 다음 경로를 확인합니다.

preferred = [
    base / "dataset" / "yolo_images",
    base / "yolo_images",
    base / "dataset",
]

즉 순서대로

/workspace/dataset/yolo_images
/workspace/yolo_images
/workspace/dataset

을 확인합니다.

그래도 찾지 못하면 다음 코드가 실행됩니다.

for images_train in base.rglob("images/train"):

/workspace 아래 전체 폴더를 검색하여 images/train을 찾습니다.

그리고 해당 폴더 주변에

images/train
images/val
labels/train
labels/val

이 모두 존재하는지 다시 검사합니다.

따라서 ZIP 파일 내부에 상위 디렉터리가 하나 더 들어가는 등 구조가 약간 달라져도 데이터셋을 찾을 가능성이 높습니다.

실제 실행에서는 다음 데이터셋이 발견되었습니다.

/workspace/dataset/yolo_images

12. dataset.zip 자동 압축 해제

데이터셋 폴더가 발견되지 않았지만

/workspace/dataset.zip

파일이 존재하는 경우 자동으로 압축을 해제합니다.

if dataset_root is None and DATASET_ZIP.is_file():

    with zipfile.ZipFile(
        DATASET_ZIP,
        "r"
    ) as zf:

        zf.extractall(WORKSPACE)

압축을 해제한 후 다시 데이터셋을 검색합니다.

즉 사용자는 경우에 따라 dataset.zip만 /workspace에 넣고 Notebook을 실행할 수도 있습니다.

13. 데이터셋을 찾지 못한 경우

정상적인 데이터셋이 발견되지 않으면 프로그램은 그대로 학습을 진행하지 않습니다.

raise FileNotFoundError(...)

를 발생시켜 중단합니다.

딥러닝 학습은 데이터 경로 오류가 발생하면 불필요하게 시간을 낭비할 수 있기 때문에 학습을 시작하기 전에 이러한 검증을 수행하는 것이 중요합니다.

14. 이미지와 라벨 파일 개수 검사

데이터셋을 찾은 다음 이미지와 라벨 개수를 계산합니다.

지원하는 이미지 확장자는 다음과 같습니다.

image_suffixes = {
    ".jpg",
    ".jpeg",
    ".png",
    ".bmp",
    ".webp"
}

그리고

count_files()

함수를 이용해 각각의 파일 개수를 조사합니다.

실제 Notebook 실행 결과는 다음과 같습니다.

train_images : 45
val_images   : 13
train_labels : 44
val_labels   : 13

여기에서 중요한 부분이 하나 있습니다.

학습 이미지는 45개인데 학습 라벨 파일은 44개입니다.

그래서 프로그램에서도 다음 경고가 출력되었습니다.

WARNING: train 이미지 수와 라벨 수가 다릅니다.

Ultralytics의 실제 데이터셋 스캔에서는

44 images, 1 backgrounds, 0 corrupt

로 처리되었습니다.

즉 45개의 학습 이미지 가운데 1장은 객체 라벨이 없는 Background 이미지로 처리된 상황입니다.

이것이 의도적으로 객체가 없는 이미지를 넣은 것이라면 문제가 아닐 수 있습니다.

반대로 객체가 존재하는 이미지인데 라벨 TXT 파일을 빠뜨린 것이라면 반드시 확인해야 합니다.

15. YOLO 라벨 파일의 의미

YOLO 객체 탐지 라벨은 일반적으로 이미지마다 같은 이름의 TXT 파일을 사용합니다.

예를 들어

images/train/image001.jpg

에 대한 라벨은

labels/train/image001.txt

가 됩니다.

라벨 한 줄은 일반적으로 다음 형태입니다.

class_id center_x center_y width height

예를 들어

4 0.512 0.421 0.164 0.238

이라면 첫 번째 숫자 4는 현재 클래스 정의에서

Red_light

를 의미합니다.

나머지 값은 Bounding Box의 중심 위치와 폭, 높이를 이미지 크기에 대해 정규화한 값입니다.

16. 가장 중요한 8개 클래스 설정

이제 이번 WEGO LIMO 객체 인식 모델의 핵심 부분입니다.

dataset_config = {
    "path": str(dataset_root),

    "train": "images/train",
    "val": "images/val",
    "test": "",

    "nc": 8,

    "names": [
        "speed_up",
        "slow_down",
        "turn_right",
        "Crosswalk",
        "Red_light",
        "Green_light",
        "Yellow_light",
        "Crossing_gate",
    ],
}

nc는 Number of Classes의 의미입니다.

nc: 8

이므로 모델의 출력 클래스가 8개라는 의미입니다.

클래스 번호는 배열의 순서에 따라 결정됩니다.

0 = speed_up
1 = slow_down
2 = turn_right
3 = Crosswalk
4 = Red_light
5 = Green_light
6 = Yellow_light
7 = Crossing_gate

이 번호는 YOLO TXT 라벨의 첫 번째 숫자와 정확하게 일치해야 합니다.

예를 들어

0 0.5 0.4 0.2 0.3

은 speed_up이고

7 0.5 0.4 0.2 0.3

은 Crossing_gate입니다.

클래스 순서를 나중에 임의로 변경하면 기존 라벨의 의미가 모두 달라지므로 주의해야 합니다.

17. 8개 객체의 자율주행 의미

각 클래스는 향후 LIMO 자율주행 로직과 다음과 같이 연결할 수 있습니다.

1) speed_up

Class ID = 0

속도 증가와 관련된 표지를 검출하기 위한 클래스입니다.

실제 주행 프로그램에서는 인식 결과를 받아 허용 범위 내에서 목표 선속도를 높이는 조건으로 사용할 수 있습니다.

2) slow_down

Class ID = 1

감속이 필요한 상황을 나타냅니다.

객체 인식 결과를 속도 제어 노드로 전달하여 LIMO의 선속도를 낮추는 로직과 연결할 수 있습니다.

3) turn_right

Class ID = 2

우회전 관련 표지를 인식합니다.

향후 차선 인식 또는 경로 계획 알고리즘과 결합해 우회전 상태로 전환하는 조건으로 사용할 수 있습니다.

4) Crosswalk

Class ID = 3

횡단보도를 인식합니다.

횡단보도 진입 전 감속 또는 정지 판단에 사용할 수 있습니다.

5) Red_light

Class ID = 4

빨간 신호를 인식합니다.

실제 제어 시스템에서는 높은 우선순위의 정지 조건으로 연결할 수 있습니다.

6) Green_light

Class ID = 5

초록 신호를 인식합니다.

정지 상태에서 출발 허용 조건이나 정상 주행 조건으로 사용할 수 있습니다.

7) Yellow_light

Class ID = 6

노란 신호를 인식합니다.

주행 상황에 따라 감속이나 정지 준비 상태로 연결할 수 있습니다.

8) Crossing_gate

Class ID = 7

차단기를 인식합니다.

닫힌 차단기 또는 차단기 접근 상황을 검출하여 정지 조건을 구성하는 데 사용할 수 있습니다.

다만 이러한 주행 동작은 현재 Notebook에 구현된 기능은 아닙니다.

현재 코드는 객체 탐지 모델의 학습과 검증까지만 담당합니다.

18. dataset YAML 파일 자동 생성

설정한 데이터는 다음 파일에 저장됩니다.

DATA_YAML = WORKSPACE / "limo_pro_dataset.yaml"

실제로 생성되는 내용은 다음과 같습니다.

path: /workspace/dataset/yolo_images
train: images/train
val: images/val
test: ''
nc: 8
names:
- speed_up
- slow_down
- turn_right
- Crosswalk
- Red_light
- Green_light
- Yellow_light
- Crossing_gate

파일 위치는

/workspace/limo_pro_dataset.yaml

입니다.

Notebook 첫 설명에는 coco8.yaml을 생성한다고 적힌 문장이 남아 있지만, 실제 현재 코드에서 생성하여 학습에 사용하는 파일은 limo_pro_dataset.yaml입니다.

따라서 실제 동작을 이해할 때는 코드의 DATA_YAML 값을 기준으로 보는 것이 맞습니다.

19. GPU와 CPU를 자동 선택하는 방법

학습 직전에 다음 함수가 정의되어 있습니다.

def parse_device(value: str):

    value = value.strip()

    if value.lower() in {"cpu", "mps"}:
        return value

    if "," in value:
        return [
            int(x.strip())
            for x in value.split(",")
        ]

    try:
        return int(value)

    except ValueError:
        return value

이를 통해 다양한 Device 설정을 처리할 수 있습니다.

예를 들어

DEVICE=0

이면 첫 번째 GPU,

DEVICE=cpu

이면 CPU,

DEVICE=0,1

이면 여러 GPU를 의미합니다.

기본 설정은 다음과 같습니다.

default_device = (
    "0"
    if torch.cuda.is_available()
    else "cpu"
)

GPU를 찾으면 자동으로 GPU 0을 사용하고 GPU가 없으면 CPU를 사용합니다.

실제 실행에서는

Training device: 0

이 출력되어 첫 번째 NVIDIA GPU에서 학습했습니다.

20. YOLO11n 모델 불러오기

모델은 단 한 줄로 생성됩니다.

model = YOLO(MODEL_NAME)

기본 MODEL_NAME이

yolo11n.pt

이므로 Ultralytics에서 제공하는 사전 학습 YOLO11n 모델을 불러옵니다.

처음부터 랜덤한 가중치를 학습하는 것이 아니라 이미 일반적인 객체 특징을 학습한 모델을 WEGO LIMO용 8개 객체에 맞게 다시 학습합니다.

이를 Transfer Learning이라고 합니다.

21. 80개 클래스 모델을 8개 클래스 모델로 변경

YOLO 사전 학습 모델은 일반적으로 COCO 데이터셋의 80개 객체 클래스를 기준으로 만들어져 있습니다.

하지만 현재 프로젝트에서는 8개의 클래스만 필요합니다.

학습 시작 시 실제 로그에서도 다음 메시지가 나타납니다.

Overriding model.yaml nc=80 with nc=8

즉 기존

80 Classes

출력 구조를

8 Classes

출력 구조로 변경한 것입니다.

사전 학습 가중치 중 재사용 가능한 부분도 그대로 전달됩니다.

실행 로그에는 다음과 같이 기록되었습니다.

Transferred 448/499 items from pretrained weights

이것이 Transfer Learning의 핵심입니다.

22. YOLO11n 네트워크 구조

학습 과정에서 출력된 YOLO11n 구조를 보면 주요 모듈로

Conv
C3k2
SPPF
C2PSA
Upsample
Concat
Detect

등이 사용됩니다.

큰 흐름으로 보면 다음과 같이 이해할 수 있습니다.

입력 이미지
     ↓
Conv
     ↓
C3k2
     ↓
특징 추출 Backbone
     ↓
SPPF
     ↓
C2PSA
     ↓
Upsample + Concat
     ↓
다중 크기 Feature 결합
     ↓
Detect
     ↓
Bounding Box + Class

Conv 계층은 이미지에서 기본적인 특징을 추출합니다.

C3k2 계층은 여러 특징을 효율적으로 학습하는 YOLO11의 주요 블록입니다.

SPPF는 서로 다른 공간 크기의 정보를 결합하는 역할을 합니다.

C2PSA는 중요한 특징에 더 집중할 수 있도록 특징 표현을 강화하는 구조입니다.

이후 Upsample과 Concat을 이용하여 서로 다른 크기의 특징을 합칩니다.

이 과정 덕분에 작은 신호등부터 비교적 큰 횡단보도까지 다양한 크기의 객체를 탐지할 수 있습니다.

최종 Detect 계층은

어디에 객체가 있는가?
무슨 객체인가?
얼마나 확신하는가?

를 계산합니다.

23. YOLO11n 모델 크기

실행 결과에서 학습 모델은 다음과 같이 표시되었습니다.

YOLO11n summary
182 layers
2,591,400 parameters
6.5 GFLOPs

학습 종료 후 Fused 모델은

101 layers
2,583,712 parameters
6.4 GFLOPs

로 표시되었습니다.

YOLO11n은 수백만 개 수준의 파라미터를 가지는 비교적 작은 모델이기 때문에 대규모 모델보다 실시간 로봇 비전에 적용하기 유리한 편입니다.

24. 핵심 학습 코드

실제 학습은 다음 코드에서 수행됩니다.

train_results = model.train(
    data=str(DATA_YAML),
    epochs=EPOCHS,
    imgsz=IMGSZ,
    batch=BATCH,
    workers=WORKERS,
    device=DEVICE,
    project=str(RUNS_DIR),
    name="yolo11n_custom",
    exist_ok=True,
    pretrained=True,
    verbose=True,
)

각 옵션을 살펴보겠습니다.

1) data

data=str(DATA_YAML)

앞에서 생성한

/workspace/limo_pro_dataset.yaml

을 사용합니다.

2) epochs

epochs=200

200 Epoch 동안 학습합니다.

3) imgsz

imgsz=640

640 크기의 입력 영상을 사용합니다.

4) batch

batch=16

최대 16장의 이미지를 하나의 Batch로 처리합니다.

5) workers

workers=4

4개의 DataLoader Worker를 사용합니다.

6) device

device=DEVICE

자동 선택되거나 환경변수로 지정한 GPU 또는 CPU를 사용합니다.

7) pretrained

pretrained=True

사전 학습 가중치를 사용하여 Transfer Learning을 수행합니다.

8) project

project=str(RUNS_DIR)

학습 결과의 최상위 저장 경로입니다.

기본값은

/workspace/runs

입니다.

9) name

name="yolo11n_custom"

이번 학습 프로젝트 이름입니다.

결과적으로 출력 경로는

/workspace/runs/yolo11n_custom

이 됩니다.

10) exist_ok

exist_ok=True

같은 이름의 폴더가 이미 존재해도 사용할 수 있도록 합니다.

25. 실제 학습에서 사용된 Optimizer

코드에서는 Optimizer를 직접 지정하지 않았습니다.

따라서 Ultralytics가

optimizer=auto

방식으로 자동 결정합니다.

실제 실행에서는 다음 설정이 선택되었습니다.

AdamW
lr = 0.000833
momentum = 0.9

즉 현재 결과는 AdamW Optimizer를 사용하여 학습된 결과입니다.

26. 학습 과정에서 확인해야 할 Loss

YOLO 학습 과정에서는 대표적으로 다음 값이 출력됩니다.

box_loss
cls_loss
dfl_loss

box_loss는 예측한 Bounding Box 위치가 실제 객체 위치와 얼마나 차이가 나는지를 나타냅니다.

cls_loss는 객체 클래스 분류 오류와 관련된 손실입니다.

예를 들어 Red_light를 Yellow_light로 판단하거나 speed_up을 다른 클래스로 판단하면 분류 손실에 영향을 줍니다.

dfl_loss는 Bounding Box 위치를 보다 정밀하게 학습하는 Distribution Focal Loss 계열 값입니다.

일반적으로 학습이 정상적으로 진행된다면 전체적인 Loss 추세가 감소하는 방향을 보여야 합니다.

27. 200 Epoch 최종 결과

실제 200번째 Epoch에서는 다음 값이 기록되었습니다.

box_loss = 0.3341
cls_loss = 1.057
dfl_loss = 0.8249

그리고 해당 Epoch의 검증 결과는

Precision = 0.886
Recall    = 0.951
mAP50     = 0.995
mAP50-95  = 0.826

으로 나타났습니다.

하지만 최종적으로 사용할 모델은 마지막 Epoch 모델이 아니라 학습 과정에서 가장 좋은 성능을 보인 best.pt입니다.

28. best.pt와 last.pt의 차이

학습이 끝나면 다음 2개의 주요 모델이 생성됩니다.

best.pt
last.pt

코드에서는 다음과 같이 경로를 생성합니다.

best_pt = (
    Path(train_results.save_dir)
    / "weights"
    / "best.pt"
)

last_pt = (
    Path(train_results.save_dir)
    / "weights"
    / "last.pt"
)

last.pt는 마지막 Epoch의 모델입니다.

best.pt는 학습 과정에서 검증 성능이 가장 우수하다고 판단된 모델입니다.

실제 객체 인식 프로그램에서는 일반적으로

best.pt

를 사용하는 것이 적절합니다.

이번 실행에서는 두 파일 모두 정상적으로 생성되었습니다.

/workspace/runs/yolo11n_custom/weights/best.pt

/workspace/runs/yolo11n_custom/weights/last.pt

파일 크기는 학습 로그 기준 약

5.5 MB

였습니다.

29. best.pt 재검증 과정

학습 후에는 단순히 파일이 존재하는지만 확인하지 않고 다시 모델을 로딩합니다.

best_model = YOLO(str(best_pt))

그리고 다음 명령을 실행합니다.

metrics = best_model.val(
    data=str(DATA_YAML),
    imgsz=IMGSZ,
    device=DEVICE,
    project=str(RUNS_DIR),
    name="yolo11n_custom_val",
    exist_ok=True,
)

즉 가장 좋은 모델을 다시 로드해 검증 데이터 전체에 대해 평가합니다.

검증 결과는

/workspace/runs/yolo11n_custom_val

에 저장됩니다.

30. Precision의 의미

최종 검증 결과에서 전체 Precision은

0.972

였습니다.

Precision은 쉽게 말하면

모델이 객체라고 검출한 것 중
실제로 맞는 객체의 비율

을 의미합니다.

Precision이 높을수록 잘못된 객체를 해당 클래스로 인식하는 False Positive가 상대적으로 적다는 의미입니다.

31. Recall의 의미

전체 Recall은

0.953

이었습니다.

Recall은

실제로 존재하는 객체 중
모델이 찾아낸 객체의 비율

을 의미합니다.

Recall이 낮다면 실제 신호등이나 표지가 있는데도 모델이 놓치는 경우가 많다는 뜻입니다.

자율주행에서는 객체를 놓치는 False Negative가 안전에 직접 영향을 줄 수 있으므로 Recall 역시 중요합니다.

32. mAP50의 의미

전체

mAP50 = 0.995

가 기록되었습니다.

mAP는 Mean Average Precision의 약자입니다.

YOLO 객체 탐지 성능을 평가할 때 대표적으로 사용하는 지표입니다.

mAP50은 IoU 기준 0.5에서 객체 탐지 성능을 평가합니다.

현재 검증셋에서는 매우 높은 값이 나왔습니다.

하지만 여기서 데이터셋 규모를 반드시 같이 봐야 합니다.

현재 검증 데이터는 총 13장, 객체 인스턴스도 13개뿐입니다.

따라서 0.995라는 숫자만으로 실제 주행 환경에서도 99.5% 수준의 신뢰도를 보장한다고 해석해서는 안 됩니다.

33. mAP50-95의 의미

전체

mAP50-95 = 0.856

이 기록되었습니다.

mAP50-95는 IoU 기준을

0.50
0.55
0.60
...
0.95

까지 변화시키면서 평균을 계산하기 때문에 mAP50보다 훨씬 엄격한 성능 지표입니다.

따라서

mAP50     = 0.995
mAP50-95  = 0.856

이라는 차이는 객체 자체는 비교적 잘 찾고 있지만 Bounding Box를 매우 정밀하게 맞추는 조건까지 적용하면 성능이 낮아진다는 의미로 해석할 수 있습니다.

34. 클래스별 실제 검증 결과

best.pt를 다시 검증한 결과는 다음과 같습니다.

1) speed_up

Images     = 1
Instances  = 1
Precision  = 0.954
Recall     = 1.000
mAP50      = 0.995
mAP50-95   = 0.995

2) slow_down

Images     = 3
Instances  = 3
Precision  = 0.966
Recall     = 1.000
mAP50      = 0.995
mAP50-95   = 0.758

3) turn_right

Images     = 3
Instances  = 3
Precision  = 0.965
Recall     = 1.000
mAP50      = 0.995
mAP50-95   = 0.830

4) Crosswalk

Images     = 3
Instances  = 3
Precision  = 1.000
Recall     = 0.716
mAP50      = 0.995
mAP50-95   = 0.763

Crosswalk은 Precision은 높지만 Recall이 다른 클래스보다 낮게 나타났습니다.

현재 3개의 객체만으로 평가했기 때문에 데이터가 더 필요합니다.

5) Red_light

Images     = 2
Instances  = 2
Precision  = 0.993
Recall     = 1.000
mAP50      = 0.995
mAP50-95   = 0.895

6) Green_light

현재 최종 검증 출력에 클래스별 결과가 나타나지 않습니다.

이는 검증셋에 Green_light 객체가 포함되지 않았기 때문으로 볼 수 있습니다.

따라서 현재 실행 결과만 가지고 Green Light의 실제 일반화 성능을 판단할 수 없습니다.

7) Yellow_light

Yellow_light 역시 최종 검증 결과에 클래스별 지표가 출력되지 않습니다.

검증 데이터에 해당 인스턴스가 없기 때문에 현재 결과로는 Yellow Light의 성능을 평가할 수 없습니다.

8) Crossing_gate

Images     = 1
Instances  = 1
Precision  = 0.956
Recall     = 1.000
mAP50      = 0.995
mAP50-95   = 0.895

35. 전체 최종 검증 성능

현재 Notebook에서 best.pt를 별도로 검증한 최종 결과는 다음과 같습니다.

Images     = 13
Instances  = 13

Precision  = 0.972
Recall     = 0.953
mAP50      = 0.995
mAP50-95   = 0.856

수치 자체는 상당히 높습니다.

그러나 검증 객체가 13개밖에 없고 일부 클래스는 검증 데이터가 전혀 없다는 점을 반드시 고려해야 합니다.

36. 현재 데이터셋에서 가장 중요한 문제

현재 데이터셋에서 가장 먼저 개선해야 할 부분은 데이터 양입니다.

학습 데이터는

45 images

이고 검증 데이터는

13 images

입니다.

8개 클래스를 학습하기에는 매우 작은 규모입니다.

특히 검증셋에서는

speed_up       1
slow_down      3
turn_right     3
Crosswalk      3
Red_light      2
Green_light    0
Yellow_light   0
Crossing_gate  1

의 분포를 보입니다.

따라서 현재 높은 mAP는 제한된 검증 환경에서 나온 값으로 보는 것이 안전합니다.

실제 LIMO 주행에서는 조명, 거리, 각도, 배경 변화가 훨씬 커집니다.

37. 실제 LIMO 적용을 위해 필요한 데이터

실차 환경에서 객체 인식을 안정적으로 사용하려면 같은 물체를 다양한 조건에서 촬영해야 합니다.

예를 들어 Red Light만 학습한다고 하더라도 다음 데이터를 포함하는 것이 좋습니다.

정면에서 촬영
좌측에서 촬영
우측에서 촬영
가까운 거리
중간 거리
먼 거리
밝은 환경
어두운 환경
역광
그림자
부분 가림
주행 중 Motion Blur
다른 객체와 동시에 존재

특히 LIMO 카메라가 실제로 장착되는 높이와 각도에서 데이터를 취득하는 것이 중요합니다.

인터넷 이미지보다 실제 로봇 카메라로 수집한 이미지가 실전 성능 개선에 더 직접적인 영향을 줄 수 있습니다.

38. 데이터 클래스 균형도 중요하다

8개의 클래스가 모두 비슷한 수량을 가지는 것이 좋습니다.

예를 들어

speed_up       500
slow_down      500
turn_right     500
Crosswalk      500
Red_light      500
Green_light    500
Yellow_light   500
Crossing_gate  500

처럼 균형을 맞추는 방식이 이상적입니다.

정확히 같은 숫자일 필요는 없지만 특정 클래스만 지나치게 많거나 적은 것은 피하는 것이 좋습니다.

특히 현재처럼 Green_light, Yellow_light가 검증셋에서 0개인 상황은 모델 평가 측면에서 개선해야 합니다.

39. 학습 속도

학습 로그에서는 GPU를 사용하여 200 Epoch 학습을 수행했습니다.

학습 중 GPU 메모리는 대략

2.16 GB

수준으로 표시되었습니다.

이번 데이터셋 자체가 매우 작기 때문에 한 Epoch에서 처리해야 할 Batch 수도 많지 않습니다.

학습 로그에서는 전체 200 Epoch가

0.034 hours

로 기록되어 있습니다.

대략 2분 정도에 해당하지만 이것은 현재처럼 데이터셋이 매우 작은 경우의 실행 결과입니다.

이미지 수가 수천 장으로 증가하면 학습 시간도 크게 증가합니다.

40. 추론 속도

별도 검증 단계에서는 이미지 한 장당 대략 다음 시간이 기록되었습니다.

preprocess   0.2 ms
inference    3.8 ms
postprocess  0.4 ms

해당 수치는 RTX 5050 Laptop GPU 기반 실행 환경의 결과입니다.

학습 직후 내부 검증에서는 더 낮은 추론 시간도 기록되어 있으므로 실제 로봇에서의 FPS를 판단할 때는 반드시 LIMO에 장착할 실제 연산 장치에서 다시 측정해야 합니다.

Desktop 또는 Laptop GPU에서 빠르다고 해서 Jetson이나 다른 임베디드 장치에서 같은 속도가 보장되는 것은 아닙니다.

41. 학습 결과 저장 구조

학습 결과는 기본적으로 다음 위치에 저장됩니다.

/workspace/runs/yolo11n_custom/

주요 파일은 다음과 같습니다.

/workspace/runs/yolo11n_custom/
└── weights/
    ├── best.pt
    └── last.pt

그리고 별도 검증 결과는

/workspace/runs/yolo11n_custom_val/

에 저장됩니다.

Docker 환경에서 /workspace를 호스트 폴더와 Volume Mount했다면 컨테이너를 삭제해도 이 파일을 유지할 수 있습니다.

42. 실시간 객체 인식에서는 best.pt를 사용한다

학습이 끝났다면 다음 단계는 LIMO 카메라 영상에 best.pt를 적용하는 것입니다.

개념적으로는 다음 구조가 됩니다.

LIMO Camera
     ↓
ROS 2 Camera Topic
     ↓
OpenCV Image
     ↓
YOLO11n best.pt
     ↓
Object Detection
     ↓
Class ID
Confidence
Bounding Box
     ↓
Driving Decision Node
     ↓
LIMO Control

예를 들어 YOLO가

class_id = 4

를 검출했다면 클래스 정의상

Red_light

입니다.

ROS 2 주행 판단 노드는 이를 받아 정지 조건으로 사용할 수 있습니다.

43. 실제 자율주행에서는 Confidence Threshold가 필요하다

YOLO 결과를 바로 차량 제어에 연결하면 위험할 수 있습니다.

예를 들어

Red_light
confidence = 0.31

인 결과 하나만으로 정지시키면 오검출 때문에 차량이 계속 멈출 수 있습니다.

따라서 실제 프로그램에서는 보통

if confidence > threshold:

와 같은 조건이 필요합니다.

예를 들면

Confidence > 0.70

또는

Confidence > 0.80

인 객체만 제어 판단에 사용할 수 있습니다.

다만 적절한 Threshold는 실제 주행 시험을 통해 결정해야 합니다.

44. 한 프레임만 보고 판단하면 안 되는 이유

자율주행에서는 한 프레임의 YOLO 결과만으로 차량 상태를 바꾸는 것보다 여러 프레임을 확인하는 것이 안정적입니다.

예를 들어

Frame 1 : Red_light 0.91
Frame 2 : Red_light 0.94
Frame 3 : Red_light 0.95

처럼 연속으로 검출된 경우 정지 판단을 확정할 수 있습니다.

반면

Frame 1 : Red_light 0.32
Frame 2 : 없음
Frame 3 : 없음

이라면 일시적인 오검출일 가능성이 있습니다.

따라서 최종 시스템에서는

YOLO 객체 탐지
+
Confidence 조건
+
연속 프레임 조건
+
거리 또는 Bounding Box 크기
+
차량 현재 상태

를 함께 사용하는 것이 적절합니다.

45. 8개 클래스를 주행 상태 머신과 연결하는 방법

향후 ROS 2 프로그램에서는 다음과 같은 상태 구조를 구성할 수 있습니다.

NORMAL_DRIVE
    ↓
speed_up
    ↓
SPEED_UP

NORMAL_DRIVE
    ↓
slow_down
    ↓
SLOW_DOWN

NORMAL_DRIVE
    ↓
turn_right
    ↓
TURN_RIGHT

NORMAL_DRIVE
    ↓
Crosswalk
    ↓
CROSSWALK_SLOW

NORMAL_DRIVE
    ↓
Red_light
    ↓
STOP

STOP
    ↓
Green_light
    ↓
NORMAL_DRIVE

NORMAL_DRIVE
    ↓
Yellow_light
    ↓
SLOW_OR_STOP

NORMAL_DRIVE
    ↓
Crossing_gate
    ↓
STOP

이렇게 하면 딥러닝 모델과 차량 제어 알고리즘을 분리할 수 있습니다.

객체 인식 노드는

무엇을 보았는가?

만 판단하고

주행 제어 노드는

그 결과에 따라 어떻게 움직일 것인가?

를 담당하는 구조가 좋습니다.

46. ROS 2에서 권장되는 구조

WEGO LIMO에서 이 모델을 사용할 경우 전체 구조는 다음과 같이 설계할 수 있습니다.

Camera Driver
      ↓
/camera/image_raw
      ↓
YOLO11 Detection Node
      ↓
/object_detection
      ↓
Driving Decision Node
      ↓
/cmd_vel
      ↓
LIMO Base

YOLO 노드는 객체 인식에 집중합니다.

Driving Decision Node는

Red_light
Crosswalk
speed_up
slow_down
turn_right

등의 인식 결과와 주행 상태를 결합하여 차량의 속도와 방향을 결정합니다.

이렇게 노드를 분리하면 학습 모델을 바꾸더라도 차량 제어 프로그램 전체를 다시 수정할 필요가 줄어듭니다.

Leave a Comment