1모델 경량화 개요

제조 현장에서는 밀리초 단위의 추론 시간이 필수입니다. 학습된 모델을 실시간 검사에 적합하도록 최적화하는 과정을 알아봅니다.

모델 최적화 목표
제조 검사 요구사항
추론 시간: < 100ms (라인 택타임 준수)
처리량: > 10 FPS (연속 검사)
정확도: 원본 대비 99% 이상 유지
메모리: < 1GB (엣지 디바이스)
최적화 기법 조합
기법 속도 향상 정확도 손실 구현 난이도
ONNX 변환1.2-1.5x0%쉬움
TensorRT2-5x0-1%중간
FP16 변환1.5-2x0.1-0.5%쉬움
INT8 양자화2-4x0.5-2%중간
프루닝1.3-2x1-3%어려움
지식 증류1.5-3x1-5%어려움
권장 조합: ONNX → TensorRT (FP16) = 3-8배 속도 향상

2ONNX 변환

ONNX(Open Neural Network Exchange)는 프레임워크 간 모델 교환을 위한 표준 포맷입니다. PyTorch에서 TensorRT, OpenVINO 등으로 배포할 때 중간 포맷으로 활용합니다.

import torch import onnx import onnxruntime as ort def export_to_onnx(model, save_path: str, input_size=(1, 3, 224, 224)): """PyTorch 모델을 ONNX로 변환""" model.eval() # 더미 입력 생성 dummy_input = torch.randn(input_size) # ONNX 내보내기 torch.onnx.export( model, dummy_input, save_path, export_params=True, opset_version=17, do_constant_folding=True, input_names=['input'], output_names=['output'], dynamic_axes={ 'input': {0: 'batch_size'}, 'output': {0: 'batch_size'} } ) # 검증 onnx_model = onnx.load(save_path) onnx.checker.check_model(onnx_model) print(f"ONNX 모델 저장: {save_path}") class ONNXInference: """ONNX Runtime 추론 엔진""" def __init__(self, onnx_path: str, device='cuda'): providers = ['CUDAExecutionProvider'] if device == 'cuda' else ['CPUExecutionProvider'] self.session = ort.InferenceSession(onnx_path, providers=providers) self.input_name = self.session.get_inputs()[0].name def predict(self, image: np.ndarray) -> np.ndarray: """추론 실행""" outputs = self.session.run(None, {self.input_name: image}) return outputs[0] # 사용 예시 model = DefectClassifier(num_classes=5) model.load_state_dict(torch.load('best_model.pth')['model_state_dict']) export_to_onnx(model, 'model.onnx') # ONNX 추론 테스트 engine = ONNXInference('model.onnx', device='cuda') result = engine.predict(test_image)

3TensorRT 최적화

TensorRT는 NVIDIA GPU에서 최적의 추론 성능을 제공하는 딥러닝 추론 최적화 엔진입니다. 레이어 퓨전, 커널 자동 튜닝, 정밀도 최적화를 통해 2-5배 속도 향상을 달성합니다.

import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit class TensorRTEngine: """TensorRT 추론 엔진""" def __init__(self, engine_path: str): self.logger = trt.Logger(trt.Logger.WARNING) with open(engine_path, 'rb') as f: self.engine = trt.Runtime(self.logger).deserialize_cuda_engine(f.read()) self.context = self.engine.create_execution_context() # 메모리 할당 self.inputs, self.outputs, self.bindings, self.stream = self._allocate_buffers() def _allocate_buffers(self): inputs, outputs, bindings = [], [], [] stream = cuda.Stream() for binding in self.engine: size = trt.volume(self.engine.get_binding_shape(binding)) dtype = trt.nptype(self.engine.get_binding_dtype(binding)) host_mem = cuda.pagelocked_empty(size, dtype) device_mem = cuda.mem_alloc(host_mem.nbytes) bindings.append(int(device_mem)) if self.engine.binding_is_input(binding): inputs.append({'host': host_mem, 'device': device_mem}) else: outputs.append({'host': host_mem, 'device': device_mem}) return inputs, outputs, bindings, stream def infer(self, image: np.ndarray) -> np.ndarray: """TensorRT 추론""" # 입력 복사 np.copyto(self.inputs[0]['host'], image.ravel()) cuda.memcpy_htod_async(self.inputs[0]['device'], self.inputs[0]['host'], self.stream) # 추론 실행 self.context.execute_async_v2(bindings=self.bindings, stream_handle=self.stream.handle) # 출력 복사 cuda.memcpy_dtoh_async(self.outputs[0]['host'], self.outputs[0]['device'], self.stream) self.stream.synchronize() return self.outputs[0]['host'].copy() # ONNX → TensorRT 변환 def build_tensorrt_engine(onnx_path: str, engine_path: str, precision='fp16'): """ONNX를 TensorRT 엔진으로 변환""" logger = trt.Logger(trt.Logger.WARNING) builder = trt.Builder(logger) network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser = trt.OnnxParser(network, logger) # ONNX 파싱 with open(onnx_path, 'rb') as f: if not parser.parse(f.read()): for error in range(parser.num_errors): print(parser.get_error(error)) raise RuntimeError("ONNX 파싱 실패") # 빌더 설정 config = builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30) # 1GB if precision == 'fp16': config.set_flag(trt.BuilderFlag.FP16) # 엔진 빌드 engine = builder.build_serialized_network(network, config) with open(engine_path, 'wb') as f: f.write(engine) print(f"TensorRT 엔진 저장: {engine_path}")

4양자화 (Quantization)

모델의 가중치와 활성화를 낮은 정밀도(FP16, INT8)로 변환하여 메모리와 연산량을 줄이는 기법입니다.

16 FP16 (Half Precision)

32비트 → 16비트. 메모리 50% 절감, 속도 1.5-2배. 정확도 손실 거의 없음

8 INT8 (Integer)

32비트 → 8비트. 메모리 75% 절감, 속도 2-4배. 캘리브레이션 필요

# PyTorch 동적 양자화 import torch.quantization as quant def quantize_model_dynamic(model): """동적 양자화 (FC 레이어 대상)""" model.eval() quantized_model = torch.quantization.quantize_dynamic( model, {torch.nn.Linear}, dtype=torch.qint8 ) return quantized_model # TensorRT INT8 캘리브레이션 class ImageCalibrator(trt.IInt8EntropyCalibrator2): """INT8 양자화용 캘리브레이션 데이터 제공""" def __init__(self, dataloader, cache_file='calibration.cache'): super().__init__() self.dataloader = iter(dataloader) self.cache_file = cache_file self.batch_size = dataloader.batch_size self.current_batch = 0 # GPU 메모리 할당 batch = next(iter(dataloader))[0] self.device_input = cuda.mem_alloc(batch.numpy().nbytes) def get_batch_size(self): return self.batch_size def get_batch(self, names): try: batch = next(self.dataloader)[0] cuda.memcpy_htod(self.device_input, batch.numpy()) return [int(self.device_input)] except StopIteration: return None def read_calibration_cache(self): if os.path.exists(self.cache_file): with open(self.cache_file, 'rb') as f: return f.read() return None def write_calibration_cache(self, cache): with open(self.cache_file, 'wb') as f: f.write(cache)

INT8 주의: 제조 결함 검출에서 INT8 양자화는 미세 결함 검출 성능에 영향을 줄 수 있습니다. 반드시 캘리브레이션 후 검증 데이터셋에서 성능을 확인하세요.

5배포 파이프라인

모델을 현장 검사 시스템에 배포하는 전체 파이프라인입니다.

Model Deployment Pipeline
학습 서버
- PyTorch 모델 학습
- ONNX 변환
- TensorRT 최적화
모델 레지스트리
- 버전 관리 (MLflow, DVC)
- 성능 메트릭 기록
- A/B 테스트 설정
엣지 디바이스
- 모델 다운로드
- 런타임 초기화 (TensorRT/ONNX)
- 추론 서비스 시작
검사 애플리케이션
- 카메라 이미지 수신
- 전처리 → 추론 → 후처리
- 결과 MES/품질 시스템 전송
import time import logging class ProductionInferenceServer: """생산 환경 추론 서버""" def __init__(self, model_path: str, config: dict): self.config = config self.logger = logging.getLogger(__name__) # 모델 로드 self.engine = TensorRTEngine(model_path) # 전처리 파이프라인 self.preprocessor = ImagePreprocessor( target_size=config['input_size'] ) # 성능 모니터링 self.inference_times = [] self.total_inferences = 0 def predict(self, image: np.ndarray) -> dict: """추론 실행""" start_time = time.time() # 전처리 processed = self.preprocessor.preprocess(image) # 추론 output = self.engine.infer(processed) probs = self._softmax(output) # 결과 해석 pred_class = int(np.argmax(probs)) confidence = float(probs[pred_class]) # 성능 기록 inference_time = (time.time() - start_time) * 1000 self.inference_times.append(inference_time) self.total_inferences += 1 return { 'class': self.config['class_names'][pred_class], 'class_id': pred_class, 'confidence': confidence, 'is_defect': pred_class != 0, 'inference_time_ms': inference_time, 'all_probs': probs.tolist() } def get_metrics(self) -> dict: """성능 메트릭 반환""" if not self.inference_times: return {} times = np.array(self.inference_times[-1000:]) # 최근 1000건 return { 'total_inferences': self.total_inferences, 'avg_inference_ms': float(np.mean(times)), 'p95_inference_ms': float(np.percentile(times, 95)), 'p99_inference_ms': float(np.percentile(times, 99)), 'fps': 1000 / np.mean(times) } @staticmethod def _softmax(x): e_x = np.exp(x - np.max(x)) return e_x / e_x.sum()

6성능 벤치마크

다양한 최적화 조합의 실제 성능 비교입니다.

모델프레임워크정밀도추론 시간메모리정확도
EfficientNet-B0PyTorchFP3215ms180MB98.5%
EfficientNet-B0ONNXFP3212ms150MB98.5%
EfficientNet-B0TensorRTFP325ms120MB98.5%
EfficientNet-B0TensorRTFP163ms65MB98.4%
EfficientNet-B0TensorRTINT82ms35MB97.8%

권장 설정: 제조 품질 검사에서는 TensorRT FP16이 최적 균형점입니다. INT8은 속도가 더 빠르지만 미세 결함 검출에서 성능 저하가 있을 수 있습니다.