딥러닝 모델 최적화 완벽 가이드
프로덕션 환경을 위한 모델 경량화와 속도 개선
연구 환경에서는 정확도가 최우선이지만, 프로덕션 환경에서는 모델 크기, 추론 속도, 메모리 사용량이 중요한 제약 조건이 됩니다. 이 글에서는 모델 성능을 유지하면서 효율성을 극대화하는 다양한 최적화 기법을 다룹니다.
모델 최적화가 필요한 이유
딥러닝 모델이 실제 서비스에 배포될 때 직면하는 현실적인 문제들을 살펴보겠습니다.
프로덕션 환경의 제약
| 환경 | 연구/개발 | 프로덕션 |
|---|---|---|
| 목표 | 최고 정확도 | 정확도 + 속도 + 비용 |
| 하드웨어 | 고성능 GPU | CPU, 모바일, 엣지 디바이스 |
| 응답 시간 | 수 초 ~ 수 분 | 밀리초 단위 |
| 배치 크기 | 큰 배치 (32~256) | 단일 또는 작은 배치 (1~8) |
| 모델 크기 | 제한 없음 | 수십 MB 이내 |
| 메모리 | 16GB+ VRAM | 수백 MB ~ 수 GB |
최적화로 얻을 수 있는 이점
최적화 효과 예시
양자화 (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)
프루닝은 중요도가 낮은 가중치나 뉴런을 제거하여 모델을 경량화하는 기법입니다. 나무를 가지치기하듯 불필요한 부분을 제거한다는 의미에서 프루닝이라고 합니다.
프루닝의 종류
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)
지식 증류는 크고 복잡한 교사 모델의 지식을 작고 빠른 학생 모델에게 전달하는 기법입니다. 모델 압축의 가장 우아한 방법 중 하나입니다.
지식 증류의 원리
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)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")모바일 배포
모바일 기기는 제한된 메모리와 연산 능력을 가지므로 특별한 최적화가 필요합니다.
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")성능 측정과 비교
최적화 효과를 정량적으로 측정하고 비교하는 것이 중요합니다.
종합 벤치마크 코드
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%+ | ⭐⭐⭐ |
실제 사례 비교
# 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% 손실)'AI_학습' 카테고리의 다른 글
| Colab 모델 저장과 배포 완벽 가이드: 학습한 모델을 저장하고 실서비스에 적용하기까지 (0) | 2026.01.06 |
|---|---|
| Google Colab으로 시작하는 머신러닝: GPU 환경 설정부터 회귀분석 실습까지 (0) | 2026.01.06 |