배포

학습된 YOLO 모델을 실제 철도 드론 시스템에 배포하는 과정이다. 목표 플랫폼에 맞는 형식으로 내보내고, 경량화 기법을 적용하여 실시간 추론 요건을 충족시킨다.

핵심 요약

항목내용
권장 배포 형식TensorRT (Jetson), ONNX (범용), OpenVINO (Intel)
목표 추론 속도30ms 이하 (실시간 경보 기준)
경량화 방법FP16 양자화 (기본), INT8 (최고 성능)
드론 탑재 권장Jetson Orin NX 16GB + TensorRT INT8
지상 시스템 권장ONNX Runtime + GPU 또는 OpenVINO + Intel CPU

문서 탐색


모델 내보내기 형식

형식특징대상 플랫폼속도 향상정확도 손실
PyTorch (.pt)원본 형식, 수정 용이학습 환경기준 (1×)없음
ONNX범용 호환, 다양한 런타임 지원CPU/GPU 범용1.2~1.5×거의 없음
TensorRT (.engine)NVIDIA 최적화, 최고 GPU 성능Jetson, 서버 GPU3~5×미미 (FP16) / 약간 (INT8)
CoreMLApple 생태계 최적화macOS, iOS2~3× (Apple)거의 없음
TFLite모바일 경량화Android, 임베디드1.5~2×약간
OpenVINOIntel 하드웨어 최적화Intel CPU/iGPU2~4× (Intel)거의 없음
NCNNARM 최적화 경량 프레임워크Raspberry Pi, 모바일2~3× (ARM)거의 없음

ONNX 내보내기

ONNX는 가장 범용적인 내보내기 형식으로, 다양한 하드웨어와 프레임워크에서 실행 가능하다.

from ultralytics import YOLO
 
model = YOLO("best.pt")
 
# ONNX 내보내기
model.export(
    format="onnx",
    imgsz=640,
    half=False,          # FP32 (호환성 우선)
    dynamic=False,       # 고정 입력 크기 (배포 안정성)
    simplify=True,       # ONNX 그래프 단순화 (속도 향상)
    opset=17,            # ONNX opset 버전
)
# 출력: best.onnx
 
# ONNX Runtime으로 추론
import onnxruntime as ort
import numpy as np
from PIL import Image
 
session = ort.InferenceSession(
    "best.onnx",
    providers=["CUDAExecutionProvider", "CPUExecutionProvider"]
)
 
# 입력 전처리
img = Image.open("railway_image.jpg").resize((640, 640))
img_array = np.array(img).transpose(2, 0, 1)[np.newaxis] / 255.0
img_tensor = img_array.astype(np.float32)
 
# 추론
input_name = session.get_inputs()[0].name
outputs = session.run(None, {input_name: img_tensor})
print(f"출력 형태: {outputs[0].shape}")

TensorRT 내보내기

TensorRT는 NVIDIA GPU에서 최고 성능을 발휘하는 추론 엔진이다. Jetson 시리즈 배포의 표준이다.

from ultralytics import YOLO
 
model = YOLO("best.pt")
 
# TensorRT FP16 내보내기 (권장)
model.export(
    format="engine",
    imgsz=640,
    half=True,           # FP16 양자화 (속도 2× 향상, 정확도 손실 미미)
    device=0,            # GPU 장치 번호
    workspace=4,         # TensorRT 작업 메모리 (GB)
    batch=1,             # 추론 배치 크기
)
# 출력: best.engine
 
# TensorRT INT8 내보내기 (최고 성능, 캘리브레이션 필요)
model.export(
    format="engine",
    imgsz=640,
    int8=True,           # INT8 양자화
    data="railway.yaml", # 캘리브레이션 데이터셋
    device=0,
    workspace=8,
)
# 출력: best.engine (INT8)
 
# TensorRT 엔진으로 추론
model_trt = YOLO("best.engine")
results = model_trt.predict(
    source="railway_video.mp4",
    stream=True,
    conf=0.30,
    iou=0.5,
    device=0,
)
 
for r in results:
    print(f"탐지 수: {len(r.boxes)}, 추론 시간: {r.speed['inference']:.1f}ms")

모델 경량화 기법

기법방법효과정확도 영향구현 난이도
FP16 양자화32비트 → 16비트 부동소수점속도 2×, 메모리 50% 절감거의 없음 (< 0.5% mAP)하 (옵션 1개)
INT8 양자화32비트 → 8비트 정수속도 3~4×, 메모리 75% 절감약간 (1~3% mAP)중 (캘리브레이션)
구조적 가지치기불필요한 채널/레이어 제거파라미터 50~80% 감소중간 (재학습 필요)
지식 증류큰 모델 → 작은 모델 학습작은 모델에 큰 모델 성능낮음 (증류 잘 되면)
채널 축소각 레이어 채널 수 감소연산량 감소중간
동적 양자화런타임 동적 정밀도 조정속도 1.5×, 메모리 절감최소

양자화 상세

FP32 → FP16 → INT8 변화

항목FP32FP16INT8
비트 수32 bit16 bit8 bit
모델 크기기준 (100%)50%25%
추론 속도기준 (1×)~2×3
메모리 사용량기준 (100%)50%25%
mAP 변화기준-0.1~0.5%-1~3%
캘리브레이션 필요불필요불필요필요 (대표 데이터)

INT8 캘리브레이션 데이터 준비

# INT8 캘리브레이션을 위한 대표 데이터셋 준비
# 학습 데이터의 일부 (권장: 500~1000장)를 사용
 
import os
import shutil
import random
 
source_dir = "./railway_train/images"
calib_dir = "./calib_dataset/images"
os.makedirs(calib_dir, exist_ok=True)
 
# 500장 무작위 선택
all_images = os.listdir(source_dir)
calib_images = random.sample(all_images, min(500, len(all_images)))
 
for img in calib_images:
    shutil.copy(
        os.path.join(source_dir, img),
        os.path.join(calib_dir, img)
    )
 
# calib.yaml 작성
calib_yaml = f"""
path: ./calib_dataset
train: images
val: images
nc: 15
names: ['rail', 'rail_joint', 'sleeper_wood', ...]
"""
 
# INT8 내보내기
from ultralytics import YOLO
model = YOLO("best.pt")
model.export(
    format="engine",
    int8=True,
    data="calib.yaml",
    device=0,
    imgsz=640,
)

엣지 디바이스 배포

Jetson 시리즈 스펙 비교

디바이스GPUAI 성능RAM전력가격(참고)철도 적합성
Jetson Orin NX 16GB1024 CUDA (Ampere)100 TOPS16GB LPDDR525W~$499드론 탑재 최적
Jetson Orin NX 8GB1024 CUDA (Ampere)70 TOPS8GB LPDDR520W~$399소형 드론
Jetson Orin Nano 8GB1024 CUDA (Ampere)40 TOPS8GB LPDDR510W~$199초경량 드론
Jetson AGX Orin2048 CUDA (Ampere)275 TOPS32/64GB60W~$999지상 점검 차량
Jetson Nano 4GB128 CUDA (Maxwell)0.5 TOPS4GB LPDDR45~10W~$99레거시, 비권장

전력 소비와 비행 시간

드론 탑재 엣지 디바이스 선택 시 전력 소비는 비행 시간에 직접 영향을 미친다.

배터리 용량 예시: 6S 10,000mAh (22.2V)

Jetson Orin NX 16GB (25W):
  - 전류 소비: 25W / 22.2V ≈ 1.1A
  - 드론 자체 소비: ~50A (중형 드론 기준)
  - 영향: 비행 시간 약 2% 감소 → 실용적

Jetson AGX Orin (60W):
  - 전류 소비: 60W / 22.2V ≈ 2.7A
  - 영향: 비행 시간 약 5% 감소 → 허용 범위

철도 인증 시스템

철도 현장 배포를 위해서는 국내외 철도 안전 인증이 필요하다.

인증기관적용 범위획득 기간
KR 인증 (한국철도)국가철도공단국내 철도 설비6~12개월
EN 50126CENELEC철도 시스템 신뢰성 (유럽)12~24개월
EN 50128CENELEC철도 소프트웨어 안전 (유럽)12~18개월
SIL 2/3IEC 61508기능 안전 무결성 수준12~24개월

Jetson Orin NX 배포 체크리스트

# 1. JetPack 버전 확인 (최신 버전 권장)
cat /etc/nv_tegra_release
 
# 2. TensorRT 버전 확인
python3 -c "import tensorrt; print(tensorrt.__version__)"
 
# 3. CUDA 버전 확인
nvcc --version
 
# 4. 모델 성능 벤치마크
python3 -c "
from ultralytics import YOLO
import time
 
model = YOLO('best.engine')
img = 'railway_test.jpg'
 
# Warm-up
for _ in range(5):
    model.predict(img, verbose=False)
 
# 벤치마크
times = []
for _ in range(100):
    start = time.perf_counter()
    results = model.predict(img, verbose=False, conf=0.3)
    times.append((time.perf_counter() - start) * 1000)
 
import numpy as np
print(f'평균 추론 시간: {np.mean(times):.1f}ms')
print(f'P95 추론 시간: {np.percentile(times, 95):.1f}ms')
print(f'FPS: {1000/np.mean(times):.1f}')
"
 
# 5. 전력 모니터링
tegrastats --interval 1000

실전 배포 서비스 구조

import cv2
import threading
import queue
from ultralytics import YOLO
 
class RailwayDetectionService:
    def __init__(self, model_path, conf=0.30, alert_classes=None):
        self.model = YOLO(model_path)
        self.conf = conf
        # 경보 대상 클래스 (지장물, 레일 균열 등 안전 critical)
        self.alert_classes = alert_classes or [0, 1, 6]
        self.frame_queue = queue.Queue(maxsize=10)
        self.result_queue = queue.Queue(maxsize=10)
 
    def inference_worker(self):
        while True:
            frame = self.frame_queue.get()
            if frame is None:
                break
            results = self.model.predict(frame, conf=self.conf, verbose=False)
            self.result_queue.put(results[0])
 
    def run(self, source=0):
        cap = cv2.VideoCapture(source)
        thread = threading.Thread(target=self.inference_worker, daemon=True)
        thread.start()
 
        while cap.isOpened():
            ret, frame = cap.read()
            if not ret:
                break
 
            # 큐가 가득 차면 프레임 건너뜀 (실시간성 유지)
            if not self.frame_queue.full():
                self.frame_queue.put(frame.copy())
 
            # 결과 처리
            if not self.result_queue.empty():
                result = self.result_queue.get()
                self._process_result(result)
 
        self.frame_queue.put(None)
        cap.release()
 
    def _process_result(self, result):
        for box in result.boxes:
            cls_id = int(box.cls[0])
            conf = float(box.conf[0])
            if cls_id in self.alert_classes and conf >= self.conf:
                class_name = self.model.names[cls_id]
                print(f"[경보] {class_name} 탐지! 신뢰도: {conf:.2f}")
                # 실제 구현: 경보 발송, 로그 기록, 디지털 트윈 업데이트
 
# 서비스 실행
service = RailwayDetectionService(
    model_path="best.engine",
    conf=0.30,
    alert_classes=[0, 1, 6],  # foreign_object, rail_break, person
)
service.run(source="/dev/video0")  # 드론 카메라

문서 탐색


참고 자료