배포
학습된 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, 서버 GPU | 3~5× | 미미 (FP16) / 약간 (INT8) |
| CoreML | Apple 생태계 최적화 | macOS, iOS | 2~3× (Apple) | 거의 없음 |
| TFLite | 모바일 경량화 | Android, 임베디드 | 1.5~2× | 약간 |
| OpenVINO | Intel 하드웨어 최적화 | Intel CPU/iGPU | 2~4× (Intel) | 거의 없음 |
| NCNN | ARM 최적화 경량 프레임워크 | 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 변화
| 항목 | FP32 | FP16 | INT8 |
|---|
| 비트 수 | 32 bit | 16 bit | 8 bit |
| 모델 크기 | 기준 (100%) | 50% | 25% |
| 추론 속도 | 기준 (1×) | ~2× | 34× |
| 메모리 사용량 | 기준 (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 시리즈 스펙 비교
| 디바이스 | GPU | AI 성능 | RAM | 전력 | 가격(참고) | 철도 적합성 |
|---|
| Jetson Orin NX 16GB | 1024 CUDA (Ampere) | 100 TOPS | 16GB LPDDR5 | 25W | ~$499 | 드론 탑재 최적 |
| Jetson Orin NX 8GB | 1024 CUDA (Ampere) | 70 TOPS | 8GB LPDDR5 | 20W | ~$399 | 소형 드론 |
| Jetson Orin Nano 8GB | 1024 CUDA (Ampere) | 40 TOPS | 8GB LPDDR5 | 10W | ~$199 | 초경량 드론 |
| Jetson AGX Orin | 2048 CUDA (Ampere) | 275 TOPS | 32/64GB | 60W | ~$999 | 지상 점검 차량 |
| Jetson Nano 4GB | 128 CUDA (Maxwell) | 0.5 TOPS | 4GB LPDDR4 | 5~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 50126 | CENELEC | 철도 시스템 신뢰성 (유럽) | 12~24개월 |
| EN 50128 | CENELEC | 철도 소프트웨어 안전 (유럽) | 12~18개월 |
| SIL 2/3 | IEC 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") # 드론 카메라
문서 탐색
참고 자료