5장: 크로스 프레임워크 변환

PyTorch, TensorFlow 및 그 너머

弘益人間 · 널리 인간을 이롭게 하라

5.1 크로스 프레임워크 변환의 필요성

현대 AI 개발에는 여러 프레임워크가 관여합니다. 연구자는 실험을 위해 PyTorch를 사용할 수 있지만 프로덕션 시스템은 TensorFlow Serving을 실행합니다. 크로스 프레임워크 변환은 원활한 모델 전송을 가능하게 하여 팀이 ML 라이프사이클의 각 단계에 가장 적합한 도구를 활용할 수 있게 합니다.

일반적인 변환 시나리오

5.2 변환 경로

ONNX는 대부분의 변환을 위한 중앙 허브 역할을 합니다:

변환 그래프:

PyTorch ←→ ONNX ←→ TensorFlow
   ↓          ↓         ↓
TorchScript  ↓      SavedModel
   ↓          ↓         ↓
Mobile     TFLite   TF.js
   ↓          ↓         ↓
   └──────→ CoreML ←──┘

ONNX → TensorRT (NVIDIA)
ONNX → OpenVINO (Intel)
ONNX → ONNX.js (웹)

5.3 PyTorch → ONNX → TensorFlow

단계 1: PyTorch에서 ONNX로

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'}
    }
)

단계 2: ONNX에서 TensorFlow로

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)

5.4 TensorFlow → ONNX → PyTorch

TensorFlow에서 ONNX로

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"
)

ONNX에서 PyTorch로

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}")

5.5 변환 문제 처리

연산자 호환성

모든 연산이 프레임워크 간에 직접적인 대응 관계를 갖는 것은 아닙니다:

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)

5.6 데이터 레이아웃 차이

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가 자동으로 레이아웃 변환 처리

5.7 검증 프레임워크

변환된 모델의 수치 정확도를 보장하기 위해 항상 검증하세요:

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)

5.8 모바일 형식으로 변환

TensorFlow Lite로

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 (위와 같음)

CoreML로

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 (위와 같음)

5.9 자동 변환 파이프라인

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())}")

5.10 모범 사례

변환 체크리스트

  1. 모델 단순화: 훈련 전용 코드 제거 (eval 모드의 dropout)
  2. 표준 연산 사용: 가능한 한 사용자 정의 연산 피하기
  3. 광범위한 테스트: 다양한 입력으로 검증
  4. 연산자 지원 확인: 모든 연산이 대상 형식에서 지원되는지 확인
  5. 동적 형상 처리: ONNX 내보내기에서 동적 축 지정
  6. 수치 정밀도 모니터링: fp32 vs fp16 차이 추적
  7. 변환 문서화: 변환 단계와 매개변수 기록
  8. 버전 관리: 원본과 변환된 모델의 버전 유지

요약

복습 문제

  1. 크로스 프레임워크 변환에 ONNX가 중간 형식으로 자주 사용되는 이유는?
  2. PyTorch 모델을 ONNX로 변환한 다음 TensorFlow로 변환하는 코드를 작성하세요.
  3. NCHW와 NHWC 데이터 레이아웃의 차이점은 무엇인가요?
  4. 변환된 모델의 수치 정확도를 어떻게 검증하나요?
  5. 세 가지 일반적인 연산자 호환성 문제와 해결책을 설명하세요.
  6. PyTorch 모델을 TFLite로 변환하는 파이프라인을 작성하세요.
  7. 모바일 배포를 위해 TFLite 대신 CoreML을 언제 사용하나요?
  8. 모델을 변환하기 전에 취해야 할 전처리 단계는 무엇인가요?
  9. 변환 중 사용자 정의 연산을 어떻게 처리하나요?
  10. 변환된 모델 버전을 유지하기 위한 모범 사례를 설명하세요.
弘益人間 (홍익인간) · 널리 인간을 이롭게 하라