PyTorch, TensorFlow 및 그 너머
弘益人間 · 널리 인간을 이롭게 하라
현대 AI 개발에는 여러 프레임워크가 관여합니다. 연구자는 실험을 위해 PyTorch를 사용할 수 있지만 프로덕션 시스템은 TensorFlow Serving을 실행합니다. 크로스 프레임워크 변환은 원활한 모델 전송을 가능하게 하여 팀이 ML 라이프사이클의 각 단계에 가장 적합한 도구를 활용할 수 있게 합니다.
ONNX는 대부분의 변환을 위한 중앙 허브 역할을 합니다:
변환 그래프:
PyTorch ←→ ONNX ←→ TensorFlow
↓ ↓ ↓
TorchScript ↓ SavedModel
↓ ↓ ↓
Mobile TFLite TF.js
↓ ↓ ↓
└──────→ CoreML ←──┘
ONNX → TensorRT (NVIDIA)
ONNX → OpenVINO (Intel)
ONNX → ONNX.js (웹)
import torch
import torch.onnx
# PyTorch 모델
class ResNetBlock(torch.nn.Module):
def __init__(self, channels):
super().__init__()
self.conv1 = torch.nn.Conv2d(channels, channels, 3, padding=1)
self.bn1 = torch.nn.BatchNorm2d(channels)
self.conv2 = torch.nn.Conv2d(channels, channels, 3, padding=1)
self.bn2 = torch.nn.BatchNorm2d(channels)
def forward(self, x):
residual = x
out = torch.nn.functional.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
return torch.nn.functional.relu(out + residual)
model = ResNetBlock(64)
dummy_input = torch.randn(1, 64, 56, 56)
# ONNX로 내보내기
torch.onnx.export(
model,
dummy_input,
"resnet_block.onnx",
export_params=True, # 파라미터 내보내기
opset_version=17, # ONNX opset 버전
do_constant_folding=True, # 상수 폴딩 최적화
input_names=['input'], # 입력 이름
output_names=['output'], # 출력 이름
dynamic_axes={ # 동적 축
'input': {0: 'batch'},
'output': {0: 'batch'}
}
)
import onnx
from onnx_tf.backend import prepare
# ONNX 모델 로드
onnx_model = onnx.load("resnet_block.onnx")
# TensorFlow로 변환
tf_rep = prepare(onnx_model)
# SavedModel로 내보내기
tf_rep.export_graph("resnet_block_tf")
# 또는 Python에서 직접 사용
import numpy as np
output = tf_rep.run(np.random.randn(1, 64, 56, 56).astype(np.float32))
print(output)
import tensorflow as tf
import tf2onnx
# TensorFlow 모델 생성
model = tf.keras.Sequential([
tf.keras.layers.Conv2D(64, 3, padding='same', input_shape=(224, 224, 3)),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.ReLU(),
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dense(1000)
])
# ONNX로 변환
spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="input"),)
model_proto, _ = tf2onnx.convert.from_keras(
model,
input_signature=spec,
opset=17,
output_path="tf_model.onnx"
)
import onnx
import torch
from onnx2pytorch import ConvertModel
# ONNX 모델 로드
onnx_model = onnx.load("tf_model.onnx")
# PyTorch로 변환
pytorch_model = ConvertModel(onnx_model)
# 모델 사용
test_input = torch.randn(1, 3, 224, 224)
output = pytorch_model(test_input)
print(f"출력 형상: {output.shape}")
모든 연산이 프레임워크 간에 직접적인 대응 관계를 갖는 것은 아닙니다:
| PyTorch | ONNX | TensorFlow | 참고사항 |
|---|---|---|---|
| F.interpolate | Resize | tf.image.resize | 정렬 방식 차이 |
| torch.einsum | Einsum | tf.einsum | Opset 12+ |
| torch.nn.GELU | Gelu | tf.nn.gelu | 근사 방법 차이 |
import torch
import math
# 변환 전: 지원되지 않는 연산 교체
class ModelWithCompatibleOps(torch.nn.Module):
def forward(self, x):
# 대신: x = torch.special.erf(x)
# 지원되는 연산 사용:
x = 0.5 * (1.0 + torch.erf(x / math.sqrt(2.0))) # GELU 근사
return x
# 또는 사용자 정의 ONNX 연산자 사용
@torch.onnx.symbolic_helper.parse_args('v', 'f')
def custom_op(g, input, scale):
return g.op("CustomNamespace::CustomOp", input, scale_f=scale)
PyTorch는 NCHW(배치, 채널, 높이, 너비)를 사용하는 반면 TensorFlow는 NHWC를 선호합니다:
import torch
import numpy as np
# PyTorch: NCHW
pytorch_tensor = torch.randn(1, 3, 224, 224)
# TensorFlow로 변환: NHWC
tf_tensor = pytorch_tensor.permute(0, 2, 3, 1) # [1, 224, 224, 3]
# TensorFlow 처리 후 다시 변환
pytorch_result = tf_output.permute(0, 3, 1, 2) # [1, 3, H, W]
# ONNX에서 자동 변환
torch.onnx.export(
model,
dummy_input,
"model.onnx",
input_names=['input'],
output_names=['output']
)
# ONNX가 자동으로 레이아웃 변환 처리
변환된 모델의 수치 정확도를 보장하기 위해 항상 검증하세요:
import torch
import numpy as np
import onnxruntime as ort
class ModelValidator:
"""모델 변환 검증 도구"""
def __init__(self, original_model, onnx_path):
self.pt_model = original_model
self.ort_session = ort.InferenceSession(onnx_path)
def validate(self, test_inputs, tolerance=1e-5):
results = []
for i, test_input in enumerate(test_inputs):
# PyTorch 추론
self.pt_model.eval()
with torch.no_grad():
pt_output = self.pt_model(test_input).numpy()
# ONNX 추론
ort_input = {
self.ort_session.get_inputs()[0].name: test_input.numpy()
}
ort_output = self.ort_session.run(None, ort_input)[0]
# 비교
diff = np.abs(pt_output - ort_output)
max_diff = diff.max()
mean_diff = diff.mean()
passed = max_diff < tolerance
results.append({
'test_id': i,
'passed': passed,
'max_diff': max_diff,
'mean_diff': mean_diff
})
print(f"테스트 {i}: {'✓ 통과' if passed else '✗ 실패'}")
print(f" 최대 차이: {max_diff:.2e}, 평균 차이: {mean_diff:.2e}")
return results
# 사용 예
validator = ModelValidator(pytorch_model, "model.onnx")
test_data = [torch.randn(1, 3, 224, 224) for _ in range(10)]
results = validator.validate(test_data)
import tensorflow as tf
# SavedModel에서
converter = tf.lite.TFLiteConverter.from_saved_model('model_tf')
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()
with open('model.tflite', 'wb') as f:
f.write(tflite_model)
# ONNX에서 (TensorFlow 경유)
# 1. ONNX → TensorFlow
# 2. TensorFlow → TFLite (위와 같음)
import coremltools as ct
# ONNX에서
from onnx_coreml import convert
# 변환
coreml_model = convert(
model='model.onnx',
minimum_ios_deployment_target='13',
preprocessing_args={
'image_scale': 1.0/255.0,
'red_bias': -0.485/0.229,
'green_bias': -0.456/0.224,
'blue_bias': -0.406/0.225
}
)
# 저장
coreml_model.save('model.mlmodel')
# PyTorch에서 (ONNX 경유)
# 1. PyTorch → ONNX
# 2. ONNX → CoreML (위와 같음)
from pathlib import Path
import torch
import onnx
import tensorflow as tf
class ModelConverter:
"""다중 형식 모델 변환기"""
def __init__(self, output_dir="converted_models"):
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True)
def pytorch_to_all(self, model, dummy_input, name="model"):
"""PyTorch 모델을 여러 형식으로 변환"""
results = {}
# PyTorch → ONNX
onnx_path = self.output_dir / f"{name}.onnx"
torch.onnx.export(model, dummy_input, onnx_path, opset_version=17)
results['onnx'] = onnx_path
# ONNX → TensorFlow
try:
from onnx_tf.backend import prepare
onnx_model = onnx.load(str(onnx_path))
tf_path = self.output_dir / f"{name}_tf"
tf_rep = prepare(onnx_model)
tf_rep.export_graph(str(tf_path))
results['tensorflow'] = tf_path
except Exception as e:
print(f"TensorFlow 변환 실패: {e}")
# ONNX → TFLite
try:
converter = tf.lite.TFLiteConverter.from_saved_model(str(tf_path))
tflite_model = converter.convert()
tflite_path = self.output_dir / f"{name}.tflite"
with open(tflite_path, 'wb') as f:
f.write(tflite_model)
results['tflite'] = tflite_path
except Exception as e:
print(f"TFLite 변환 실패: {e}")
return results
# 사용 예
converter = ModelConverter()
results = converter.pytorch_to_all(
pytorch_model,
torch.randn(1, 3, 224, 224),
name="resnet50"
)
print(f"변환된 형식: {list(results.keys())}")