범용 AI 형식의 표준
弘益人間 · 널리 인간을 이롭게 하라
ONNX(Open Neural Network Exchange) 포맷은 2017년 Microsoft와 Facebook(현 Meta)에 의해 머신러닝 모델을 표현하는 개방형 표준으로 만들어졌습니다. ONNX는 PyTorch, TensorFlow, scikit-learn 등의 프레임워크 간 원활한 모델 전송을 가능하게 하는 사실상의 교환 형식이 되었습니다.
ONNX는 Google의 언어 중립적 데이터 직렬화 형식인 Protocol Buffers(protobuf) 위에 구축되었습니다. 이러한 선택은 프로덕션급 형식에 필수적인 효율적인 바이너리 인코딩, 강력한 타이핑, 스키마 진화 기능을 제공합니다.
ONNX 모델은 계층적 구조로 구성된 여러 핵심 구성 요소로 이루어져 있습니다:
전체 모델을 담는 최상위 컨테이너:
message ModelProto {
int64 ir_version = 1; // ONNX IR 버전
repeated OpsetImport opset_import = 8; // 연산자 세트
string producer_name = 2; // 모델을 생성한 프레임워크
string producer_version = 3; // 프레임워크 버전
string domain = 4; // 모델 도메인
int64 model_version = 5; // 모델 버전
string doc_string = 6; // 문서
GraphProto graph = 7; // 계산 그래프
repeated StringStringEntryProto metadata_props = 14;
}
모델의 아키텍처를 정의하는 계산 그래프:
message GraphProto {
repeated NodeProto node = 1; // 계산 노드
string name = 2; // 그래프 이름
repeated TensorProto initializer = 5; // 상수 텐서 (가중치)
string doc_string = 10; // 문서
repeated ValueInfoProto input = 11; // 입력 사양
repeated ValueInfoProto output = 12; // 출력 사양
repeated ValueInfoProto value_info = 13; // 중간 값
}
계산 그래프의 개별 연산:
message NodeProto {
repeated string input = 1; // 입력 텐서 이름
repeated string output = 2; // 출력 텐서 이름
string name = 3; // 노드 이름
string op_type = 4; // 연산 타입 (Conv, Gemm 등)
string domain = 7; // 연산자 도메인
repeated AttributeProto attribute = 5; // 연산 속성
string doc_string = 6; // 문서
}
ONNX는 버전이 지정된 연산자 세트(opset)를 통해 연산자를 정의합니다:
| Opset | 출시 | 주요 기능 |
|---|---|---|
| 1-6 | 2017-2018 | 기본 연산, CNN |
| 7-10 | 2019 | RNN, 제어 흐름 |
| 11-13 | 2020-2021 | Transformer, 동적 형상 |
| 14-16 | 2022-2023 | 양자화, 최적화 |
| 17-18 | 2023-2024 | LLM 연산, 효율성 |
// 합성곱
Conv: 합성곱 연산
- dilations, group, kernel_shape, pads, strides
- 입력: X[N,C_in,H,W], W[C_out,C_in/group,kH,kW]
- 출력: Y[N,C_out,H_out,W_out]
// 행렬 곱셈
Gemm: 일반 행렬 곱셈 (Y = α·AB + β·C)
- alpha, beta, transA, transB
// 활성화 함수
Relu, Sigmoid, Tanh, LeakyRelu, Elu, Selu, Swish
// 정규화
BatchNormalization, LayerNormalization, InstanceNormalization
// 풀링
MaxPool, AveragePool, GlobalMaxPool, GlobalAveragePool
// 어텐션 (Opset 14+)
Attention, MultiHeadAttention
ONNX 텐서는 상수 값과 가중치를 위해 TensorProto 메시지를 사용합니다:
message TensorProto {
repeated int64 dims = 1; // 텐서 차원
int32 data_type = 2; // 요소 데이터 타입
Segment segment = 3; // 큰 텐서용
// 데이터 저장 (다음 중 하나):
repeated float float_data = 4; // FP32 값
repeated int32 int32_data = 5; // INT32 값
bytes raw_data = 9; // 원시 바이너리 데이터
// 외부 데이터 참조
repeated StringStringEntryProto external_data = 13;
DataLocation data_location = 14;
string name = 8; // 텐서 이름
string doc_string = 12; // 문서
}
import torch
import torch.onnx
# 훈련된 모델 로드
model = MyModel()
model.load_state_dict(torch.load('model.pth'))
model.eval()
# 더미 입력 생성
dummy_input = torch.randn(1, 3, 224, 224)
# ONNX로 내보내기
torch.onnx.export(
model,
dummy_input,
"model.onnx",
export_params=True, # 파라미터 내보내기
opset_version=17, # ONNX opset 버전
do_constant_folding=True, # 상수 폴딩 최적화
input_names=['input'], # 입력 이름
output_names=['output'], // 출력 이름
dynamic_axes={ # 동적 축 정의
'input': {0: 'batch_size'},
'output': {0: 'batch_size'}
}
)
import tensorflow as tf
import tf2onnx
# SavedModel 로드
model = tf.saved_model.load('saved_model')
# ONNX로 변환
spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="input"),)
output_path = "model.onnx"
model_proto, _ = tf2onnx.convert.from_keras(
model,
input_signature=spec,
opset=17,
output_path=output_path
)
print(f"ONNX 모델이 {output_path}에 저장되었습니다")
ONNX Runtime은 ONNX 모델을 위한 고성능 추론 엔진입니다:
import onnxruntime as ort
import numpy as np
# 추론 세션 생성
session = ort.InferenceSession("model.onnx")
# 입출력 이름 가져오기
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name
# 입력 준비
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
# 추론 실행
outputs = session.run(
[output_name],
{input_name: input_data}
)
print(f"출력 형상: {outputs[0].shape}")
print(f"예측 결과: {outputs[0]}")
# 세션 옵션 구성
sess_options = ort.SessionOptions()
# 그래프 최적화 활성화
sess_options.graph_optimization_level = \
ort.GraphOptimizationLevel.ORT_ENABLE_ALL
# 스레드 수 설정
sess_options.intra_op_num_threads = 4
sess_options.inter_op_num_threads = 4
# 프로파일링 활성화
sess_options.enable_profiling = True
# 최적화된 세션 생성
session = ort.InferenceSession(
"model.onnx",
sess_options,
providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
)
# 사용 중인 프로바이더 확인
print(f"실행 프로바이더: {session.get_providers()}")
from onnxruntime.quantization import quantize_dynamic, quantize_static
# 동적 양자화 (보정 데이터 불필요)
quantize_dynamic(
model_input="model_fp32.onnx",
model_output="model_int8.onnx",
weight_type=QuantType.QInt8
)
# 정적 양자화 (보정 필요)
from onnxruntime.quantization import CalibrationDataReader
class DataReader(CalibrationDataReader):
def __init__(self, calibration_data):
self.data = calibration_data
self.index = 0
def get_next(self):
if self.index < len(self.data):
result = {"input": self.data[self.index]}
self.index += 1
return result
return None
quantize_static(
model_input="model_fp32.onnx",
model_output="model_int8_static.onnx",
calibration_data_reader=DataReader(calibration_dataset)
)
from onnx import helper, TensorProto
# 사용자 정의 연산자 정의
custom_op = helper.make_node(
'CustomOp',
inputs=['X'],
outputs=['Y'],
domain='ai.onnx.custom',
custom_attribute=42
)
# ONNX Runtime에 구현 등록
import onnxruntime as ort
class CustomOpKernel:
def __init__(self, custom_attribute):
self.custom_attribute = custom_attribute
def compute(self, x):
# 사용자 정의 연산 로직
return x * self.custom_attribute
# 커널 등록
ort.register_custom_ops_library("custom_ops.so")
# Netron 설치
pip install netron
# 브라우저에서 모델 보기
netron model.onnx
# 또는 Python API 사용
import netron
netron.start('model.onnx', port=8080)
import onnx
# 모델 로드
model = onnx.load("model.onnx")
# 모델 유효성 검사
onnx.checker.check_model(model)
print("✓ 모델이 유효합니다")
# 형상 추론
model = onnx.shape_inference.infer_shapes(model)
# 모델 구조 출력
for node in model.graph.node:
print(f"노드: {node.name}")
print(f" 연산: {node.op_type}")
print(f" 입력: {node.input}")
print(f" 출력: {node.output}")
import torch
import onnxruntime as ort
import numpy as np
# 원본 PyTorch 모델
model_pt = load_pytorch_model()
model_pt.eval()
# ONNX 모델
session = ort.InferenceSession("model.onnx")
# 테스트 입력
test_input = np.random.randn(1, 3, 224, 224).astype(np.float32)
# PyTorch 출력
with torch.no_grad():
pt_output = model_pt(torch.from_numpy(test_input)).numpy()
# ONNX 출력
onnx_output = session.run(None, {"input": test_input})[0]
# 비교
diff = np.abs(pt_output - onnx_output)
print(f"최대 차이: {diff.max():.2e}")
print(f"평균 차이: {diff.mean():.2e}")
assert diff.max() < 1e-5, "출력이 크게 다릅니다!"
이 장에서는 ONNX 포맷에 대해 심층적으로 다루었습니다. 주요 내용: