AI_학습

딥러닝 모델 최적화 완벽 가이드: 프로덕션 환경을 위한 모델 경량화와 속도 개선

JohnnyDeveloper 2026. 1. 6. 16:59

딥러닝 모델 최적화 완벽 가이드

프로덕션 환경을 위한 모델 경량화와 속도 개선

연구 환경에서는 정확도가 최우선이지만, 프로덕션 환경에서는 모델 크기, 추론 속도, 메모리 사용량이 중요한 제약 조건이 됩니다. 이 글에서는 모델 성능을 유지하면서 효율성을 극대화하는 다양한 최적화 기법을 다룹니다.

모델 최적화가 필요한 이유

딥러닝 모델이 실제 서비스에 배포될 때 직면하는 현실적인 문제들을 살펴보겠습니다.

프로덕션 환경의 제약

환경 연구/개발 프로덕션
목표 최고 정확도 정확도 + 속도 + 비용
하드웨어 고성능 GPU CPU, 모바일, 엣지 디바이스
응답 시간 수 초 ~ 수 분 밀리초 단위
배치 크기 큰 배치 (32~256) 단일 또는 작은 배치 (1~8)
모델 크기 제한 없음 수십 MB 이내
메모리 16GB+ VRAM 수백 MB ~ 수 GB

최적화로 얻을 수 있는 이점

최적화 효과 예시

모델 크기
75%↓
추론 속도
3~4x
정확도 손실
<1%
graph TB A["원본 모델"] --> B{"최적화 기법"} B --> C["양자화"] B --> D["프루닝"] B --> E["지식 증류"] B --> F["구조 최적화"] C --> G["최적화된 모델"] D --> G E --> G F --> G G --> H["더 작은 크기"] G --> I["더 빠른 속도"] G --> J["더 낮은 비용"] style A fill:#fef2f2 style G fill:#f0fdf4 style H fill:#dbeafe style I fill:#dbeafe style J fill:#dbeafe

양자화 (Quantization)

양자화는 모델의 가중치와 활성화를 낮은 정밀도로 표현하여 메모리와 계산량을 줄이는 기법입니다. 가장 효과적이고 널리 사용되는 최적화 방법입니다.

양자화의 원리

딥러닝 모델은 일반적으로 32비트 부동소수점(FP32)으로 표현됩니다. 양자화는 이를 16비트(FP16), 8비트 정수(INT8), 심지어 4비트까지 줄일 수 있습니다.

# 데이터 타입별 메모리 사용량 FP32 (Float32): 32bit = 4bytes FP16 (Float16): 16bit = 2bytes # 50% 감소 INT8: 8bit = 1byte # 75% 감소 INT4: 4bit = 0.5byte # 87.5% 감소 # 100MB 모델의 경우 FP32: 100MB INT8: 25MB # 4배 작음

PyTorch 동적 양자화

가장 간단한 형태의 양자화로, 추론 시점에 가중치를 INT8로 변환합니다.

import torch import torch.quantization # 원본 모델 model = YourModel() model.eval() # 동적 양자화 적용 quantized_model = torch.quantization.quantize_dynamic( model, {torch.nn.Linear, torch.nn.LSTM}, # 양자화할 레이어 타입 dtype=torch.qint8 ) # 모델 크기 비교 def get_model_size(model): torch.save(model.state_dict(), "temp.pt") size = os.path.getsize("temp.pt") / (1024 * 1024) os.remove("temp.pt") return size original_size = get_model_size(model) quantized_size = get_model_size(quantized_model) print(f"Original: {original_size:.2f} MB") print(f"Quantized: {quantized_size:.2f} MB") print(f"Reduction: {(1 - quantized_size/original_size)*100:.1f}%")

PyTorch 정적 양자화 (QAT)

Quantization-Aware Training은 학습 과정에서 양자화를 고려하여 정확도 손실을 최소화합니다.

import torch.quantization # 1. 모델 준비 model = YourModel() model.train() # 2. 양자화 설정 추가 model.qconfig = torch.quantization.get_default_qat_qconfig('fbgemm') # 3. Fake Quantization 삽입 model_prepared = torch.quantization.prepare_qat(model) # 4. 일반적인 학습 수행 for epoch in range(num_epochs): for batch in train_loader: optimizer.zero_grad() output = model_prepared(batch) loss = criterion(output, target) loss.backward() optimizer.step() # 5. 실제 양자화로 변환 model_prepared.eval() quantized_model = torch.quantization.convert(model_prepared)

TensorFlow 양자화

import tensorflow as tf # Post-Training Quantization converter = tf.lite.TFLiteConverter.from_keras_model(model) # INT8 양자화 converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.target_spec.supported_types = [tf.int8] # 양자화 모델 생성 tflite_quant_model = converter.convert() # 파일 저장 with open('quantized_model.tflite', 'wb') as f: f.write(tflite_quant_model) # Float16 양자화 (더 나은 정확도) converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.target_spec.supported_types = [tf.float16] tflite_fp16_model = converter.convert()

양자화 방식 비교

방식 정확도 속도 향상 구현 난이도 적용 시점
동적 양자화 높음 2x 쉬움 추론 시
정적 양자화 중간 3x 중간 추론 전
QAT 가장 높음 3~4x 어려움 학습 시

프루닝 (Pruning)

프루닝은 중요도가 낮은 가중치나 뉴런을 제거하여 모델을 경량화하는 기법입니다. 나무를 가지치기하듯 불필요한 부분을 제거한다는 의미에서 프루닝이라고 합니다.

프루닝의 종류

graph TB A["프루닝 기법"] --> B["비구조적 프루닝"] A --> C["구조적 프루닝"] B --> D["개별 가중치 제거"] B --> E["희소 행렬 생성"] C --> F["전체 뉴런 제거"] C --> G["필터/채널 제거"] D --> H["높은 압축률"] F --> I["하드웨어 친화적"] style A fill:#e3f2fd style B fill:#fff3e0 style C fill:#f3e5f5

PyTorch 프루닝

import torch import torch.nn.utils.prune as prune # 모델 정의 model = YourModel() # 특정 레이어의 가중치 프루닝 (크기 기반) # 가장 작은 가중치 30%를 0으로 설정 prune.l1_unstructured( module=model.conv1, name='weight', amount=0.3 ) # 여러 레이어에 프루닝 적용 parameters_to_prune = ( (model.conv1, 'weight'), (model.conv2, 'weight'), (model.fc1, 'weight'), ) prune.global_unstructured( parameters_to_prune, pruning_method=prune.L1Unstructured, amount=0.4, ) # 프루닝 상태 확인 print(list(model.conv1.named_buffers())) # [('weight_mask', tensor([[...]]])] # 프루닝 영구 적용 prune.remove(model.conv1, 'weight')

반복적 프루닝

한 번에 많은 가중치를 제거하면 정확도가 크게 떨어지므로, 점진적으로 프루닝하고 재학습하는 과정을 반복합니다.

def iterative_pruning( model, train_loader, val_loader, initial_sparsity=0.0, final_sparsity=0.8, num_iterations=10 ): sparsity_schedule = torch.linspace( initial_sparsity, final_sparsity, num_iterations ) for i, target_sparsity in enumerate(sparsity_schedule): print(f"Iteration {i+1}: Target sparsity {target_sparsity:.2f}") # 프루닝 적용 for name, module in model.named_modules(): if isinstance(module, torch.nn.Conv2d): prune.l1_unstructured( module, name='weight', amount=target_sparsity ) # 미세 조정 (Fine-tuning) for epoch in range(5): train_epoch(model, train_loader, optimizer) # 검증 accuracy = evaluate(model, val_loader) print(f"Accuracy: {accuracy:.2f}%") # 프루닝 영구 적용 for name, module in model.named_modules(): if isinstance(module, torch.nn.Conv2d): prune.remove(module, 'weight') return model
장점
  • 모델 크기 대폭 감소 (50~90%)
  • 메모리 사용량 감소
  • 에너지 효율 향상
단점
  • 재학습 시간 필요
  • 하드웨어 지원 필요 (희소 행렬)
  • 과도한 프루닝 시 정확도 하락

지식 증류 (Knowledge Distillation)

지식 증류는 크고 복잡한 교사 모델의 지식을 작고 빠른 학생 모델에게 전달하는 기법입니다. 모델 압축의 가장 우아한 방법 중 하나입니다.

지식 증류의 원리

graph LR A["큰 교사 모델"] --> B["Soft Labels"] C["작은 학생 모델"] --> D["예측"] B --> E["증류 손실"] D --> E F["실제 레이블"] --> G["분류 손실"] D --> G E --> H["총 손실"] G --> H H --> C style A fill:#fef2f2 style C fill:#f0fdf4 style H fill:#eff6ff

PyTorch 구현

import torch import torch.nn as nn import torch.nn.functional as F class DistillationLoss(nn.Module): def __init__(self, alpha=0.5, temperature=3.0): super().__init__() self.alpha = alpha self.temperature = temperature self.kl_div = nn.KLDivLoss(reduction='batchmean') self.ce_loss = nn.CrossEntropyLoss() def forward(self, student_logits, teacher_logits, labels): # Soft target loss (증류 손실) soft_targets = F.softmax( teacher_logits / self.temperature, dim=1 ) soft_prob = F.log_softmax( student_logits / self.temperature, dim=1 ) distillation_loss = self.kl_div(soft_prob, soft_targets) distillation_loss *= (self.temperature ** 2) # Hard target loss (분류 손실) student_loss = self.ce_loss(student_logits, labels) # 가중 합산 total_loss = ( self.alpha * distillation_loss + (1 - self.alpha) * student_loss ) return total_loss # 학습 루프 teacher_model.eval() # 교사 모델은 평가 모드 student_model.train() criterion = DistillationLoss(alpha=0.7, temperature=3.0) for epoch in range(num_epochs): for images, labels in train_loader: # 교사 모델 예측 (gradient 계산 안 함) with torch.no_grad(): teacher_logits = teacher_model(images) # 학생 모델 예측 student_logits = student_model(images) # 손실 계산 loss = criterion(student_logits, teacher_logits, labels) # 역전파 optimizer.zero_grad() loss.backward() optimizer.step()

다양한 증류 전략

# 1. Response-based: 최종 출력만 사용 loss = distill_response(student_output, teacher_output) # 2. Feature-based: 중간 특징 맵 활용 teacher_features = teacher_model.get_features(x) student_features = student_model.get_features(x) loss = mse_loss(student_features, teacher_features) # 3. Relation-based: 샘플 간 관계 학습 def relation_loss(teacher_batch, student_batch): # 배치 내 샘플 간 유사도 행렬 teacher_sim = torch.matmul(teacher_batch, teacher_batch.T) student_sim = torch.matmul(student_batch, student_batch.T) return F.mse_loss(student_sim, teacher_sim)
지식 증류의 효과 BERT-large (340M 파라미터)를 DistilBERT (66M 파라미터)로 증류하면 크기는 40%로 줄어들지만 성능은 97%를 유지합니다.

ONNX 변환

ONNX는 프레임워크에 독립적인 모델 포맷으로, 다양한 런타임에서 최적화된 추론을 가능하게 합니다.

PyTorch to ONNX

import torch import onnx import onnxruntime as ort # PyTorch 모델 준비 model = YourModel() model.eval() # 더미 입력 (실제 입력 크기와 동일해야 함) dummy_input = torch.randn(1, 3, 224, 224) # ONNX로 변환 torch.onnx.export( model, dummy_input, "model.onnx", export_params=True, opset_version=13, do_constant_folding=True, # 상수 연산 최적화 input_names=['input'], output_names=['output'], dynamic_axes={ 'input': {0: 'batch_size'}, 'output': {0: 'batch_size'} } ) # ONNX 모델 검증 onnx_model = onnx.load("model.onnx") onnx.checker.check_model(onnx_model) print("ONNX model is valid!")

ONNX Runtime 추론

import numpy as np import onnxruntime as ort import time # ONNX Runtime 세션 생성 session = ort.InferenceSession( "model.onnx", providers=['CUDAExecutionProvider', 'CPUExecutionProvider'] ) # 입력 데이터 준비 input_name = session.get_inputs()[0].name input_data = np.random.randn(1, 3, 224, 224).astype(np.float32) # 추론 실행 outputs = session.run(None, {input_name: input_data}) # 속도 벤치마크 def benchmark(session, input_data, num_runs=100): # 워밍업 for _ in range(10): session.run(None, {input_name: input_data}) # 측정 start = time.time() for _ in range(num_runs): session.run(None, {input_name: input_data}) end = time.time() avg_time = (end - start) / num_runs * 1000 return avg_time avg_latency = benchmark(session, input_data) print(f"Average latency: {avg_latency:.2f} ms")

ONNX 최적화

from onnxruntime.transformers import optimizer # 그래프 최적화 optimized_model = optimizer.optimize_model( "model.onnx", model_type='bert', # 모델 타입 지정 num_heads=12, hidden_size=768, optimization_options=optimizer.FusionOptions('bert') ) optimized_model.save_model_to_file("model_optimized.onnx")

TensorRT 최적화

TensorRT는 NVIDIA GPU에서 최고의 추론 성능을 제공하는 고성능 딥러닝 추론 엔진입니다.

PyTorch to TensorRT

import torch import torch_tensorrt # PyTorch 모델 로드 model = torch.jit.load("model_scripted.pt") model.eval() # TensorRT로 컴파일 trt_model = torch_tensorrt.compile( model, inputs=[ torch_tensorrt.Input( min_shape=[1, 3, 224, 224], opt_shape=[8, 3, 224, 224], max_shape=[16, 3, 224, 224], dtype=torch.float32 ) ], enabled_precisions={torch.float16}, # FP16 사용 workspace_size=1 << 30 # 1GB ) # 추론 input_data = torch.randn(8, 3, 224, 224).cuda() output = trt_model(input_data) # TensorRT 엔진 저장 torch.jit.save(trt_model, "model_trt.ts")
TensorRT 성능 TensorRT는 레이어 융합, 정밀도 보정, 커널 자동 튜닝 등을 통해 PyTorch 대비 2~5배의 속도 향상을 제공합니다.

모바일 배포

모바일 기기는 제한된 메모리와 연산 능력을 가지므로 특별한 최적화가 필요합니다.

TensorFlow Lite

import tensorflow as tf # Keras 모델을 TFLite로 변환 converter = tf.lite.TFLiteConverter.from_keras_model(model) # 최적화 옵션 converter.optimizations = [tf.lite.Optimize.DEFAULT] # INT8 양자화를 위한 대표 데이터셋 def representative_dataset(): for i in range(100): data = np.random.rand(1, 224, 224, 3).astype(np.float32) yield [data] converter.representative_dataset = representative_dataset converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] converter.inference_input_type = tf.int8 converter.inference_output_type = tf.int8 # 변환 tflite_model = converter.convert() # 저장 with open('model_mobile.tflite', 'wb') as f: f.write(tflite_model) # TFLite 추론 interpreter = tf.lite.Interpreter(model_path="model_mobile.tflite") interpreter.allocate_tensors() input_details = interpreter.get_input_details() output_details = interpreter.get_output_details() # 추론 실행 input_data = np.array(np.random.random_sample(input_shape), dtype=np.float32) interpreter.set_tensor(input_details[0]['index'], input_data) interpreter.invoke() output_data = interpreter.get_tensor(output_details[0]['index'])

PyTorch Mobile

import torch from torch.utils.mobile_optimizer import optimize_for_mobile # 모델을 TorchScript로 변환 model.eval() example_input = torch.rand(1, 3, 224, 224) traced_script_module = torch.jit.trace(model, example_input) # 모바일 최적화 optimized_model = optimize_for_mobile(traced_script_module) # 저장 optimized_model._save_for_lite_interpreter("model_mobile.ptl") # 양자화까지 적용 from torch.quantization import quantize_dynamic quantized_model = quantize_dynamic( model, {torch.nn.Linear}, dtype=torch.qint8 ) traced_quantized = torch.jit.trace( quantized_model, example_input ) optimized_quantized = optimize_for_mobile(traced_quantized) optimized_quantized._save_for_lite_interpreter("model_quantized.ptl")
모바일 프레임워크 비교 특징 TensorFlow Lite PyTorch Mobile 플랫폼 Android, iOS, 임베디드 Android, iOS 모델 크기 매우 작음 중간 추론 속도 빠름 빠름 하드웨어 가속 GPU, DSP, NPU 지원 GPU 지원 생태계 성숙함 성장 중

성능 측정과 비교

최적화 효과를 정량적으로 측정하고 비교하는 것이 중요합니다.

종합 벤치마크 코드

import time import torch import numpy as np from sklearn.metrics import accuracy_score def benchmark_model( model, test_loader, device='cuda', num_warmup=10, num_runs=100 ): model.eval() model = model.to(device) # 워밍업 with torch.no_grad(): for i, (images, _) in enumerate(test_loader): if i >= num_warmup: break images = images.to(device) _ = model(images) # 속도 측정 latencies = [] with torch.no_grad(): for i, (images, _) in enumerate(test_loader): if i >= num_runs: break images = images.to(device) start = time.perf_counter() _ = model(images) torch.cuda.synchronize() # GPU 동기화 end = time.perf_counter() latencies.append((end - start) * 1000) # 정확도 측정 all_preds = [] all_labels = [] with torch.no_grad(): for images, labels in test_loader: images = images.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) accuracy = accuracy_score(all_labels, all_preds) * 100 # 모델 크기 torch.save(model.state_dict(), "temp_model.pth") model_size = os.path.getsize("temp_model.pth") / (1024 ** 2) os.remove("temp_model.pth") results = { 'avg_latency': np.mean(latencies), 'std_latency': np.std(latencies), 'p50_latency': np.percentile(latencies, 50), 'p95_latency': np.percentile(latencies, 95), 'p99_latency': np.percentile(latencies, 99), 'throughput': 1000 / np.mean(latencies), 'accuracy': accuracy, 'model_size_mb': model_size } return results # 사용 예시 original_results = benchmark_model(original_model, test_loader) quantized_results = benchmark_model(quantized_model, test_loader) print("Original Model:") print(f" Latency: {original_results['avg_latency']:.2f} ms") print(f" Accuracy: {original_results['accuracy']:.2f}%") print(f" Size: {original_results['model_size_mb']:.2f} MB") print("\nQuantized Model:") print(f" Latency: {quantized_results['avg_latency']:.2f} ms") print(f" Accuracy: {quantized_results['accuracy']:.2f}%") print(f" Size: {quantized_results['model_size_mb']:.2f} MB") speedup = original_results['avg_latency'] / quantized_results['avg_latency'] print(f"\nSpeedup: {speedup:.2f}x")

최적화 기법 비교 표

기법 크기 감소 속도 향상 정확도 유지 구현 복잡도
FP16 양자화 50% 1.5~2x 99%+
INT8 양자화 75% 2~4x 98~99% ⭐⭐
프루닝 50~90% 1.2~2x 97~99% ⭐⭐⭐
지식 증류 60~90% 3~5x 95~98% ⭐⭐⭐⭐
ONNX 0~10% 1.2~2x 100% ⭐⭐
TensorRT 0~20% 2~5x 99%+ ⭐⭐⭐
최적화 권장 순서 1. ONNX 변환 (프레임워크 독립성) 2. FP16 양자화 (정확도 손실 거의 없음) 3. INT8 양자화 (더 큰 압축) 4. 프루닝 (추가 최적화) 5. TensorRT/모바일 배포 (프로덕션)

실제 사례 비교

# ResNet-50 최적화 결과 예시 Original Model: 크기: 98MB 속도: 15ms 정확도: 76.1% FP16 Quantization: 크기: 49MB (50% 감소) 속도: 9ms (1.67x 빠름) 정확도: 76.0% (0.1% 손실) INT8 Quantization: 크기: 25MB (75% 감소) 속도: 5ms (3x 빠름) 정확도: 75.5% (0.6% 손실) TensorRT + INT8: 크기: 23MB (77% 감소) 속도: 3ms (5x 빠름) 정확도: 75.6% (0.5% 손실)