🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

모델 직렬화와 저장 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 7. · 13 Views

모델 직렬화와 저장 완벽 가이드

PyTorch 모델을 저장하고 불러오는 다양한 방법을 알아봅니다. state_dict부터 ONNX, TorchScript까지 실무에서 꼭 알아야 할 모델 직렬화 기법을 초보자도 이해할 수 있도록 설명합니다.


목차

  1. PyTorch_모델_저장_state_dict
  2. 전체_모델_저장_vs_가중치만
  3. ONNX_포맷_변환
  4. TorchScript_활용
  5. 모델_버전_관리
  6. 체크포인트_전략

1. PyTorch 모델 저장 state dict

김개발 씨는 일주일 동안 공들여 학습시킨 딥러닝 모델이 있습니다. 그런데 컴퓨터를 재부팅하고 나니 모델이 사라졌습니다.

"아, 저장을 안 했구나..." 허탈한 마음으로 선배에게 물었습니다. "모델은 어떻게 저장해야 하나요?"

state_dict는 PyTorch 모델의 학습된 가중치를 담고 있는 Python 딕셔너리입니다. 마치 게임의 세이브 파일처럼, 모델이 학습한 모든 정보를 파일로 저장할 수 있게 해줍니다.

이것을 제대로 활용하면 언제든지 학습된 모델을 복원할 수 있습니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

# 간단한 신경망 정의
class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(10, 64)
        self.fc2 = nn.Linear(64, 2)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        return self.fc2(x)

model = SimpleNet()
# 핵심: state_dict로 가중치만 저장
torch.save(model.state_dict(), 'model_weights.pth')

# 모델 불러오기
new_model = SimpleNet()
new_model.load_state_dict(torch.load('model_weights.pth'))

김개발 씨는 입사 2개월 차 주니어 ML 엔지니어입니다. 처음으로 혼자 힘으로 모델을 학습시켰는데, 저장하는 방법을 몰라서 낭패를 봤습니다.

선배 개발자 박시니어 씨가 다가와 말했습니다. "PyTorch에서 모델을 저장하는 가장 기본적인 방법이 state_dict야.

이것만 알아도 대부분의 상황에서 문제없어." 그렇다면 state_dict란 정확히 무엇일까요? 쉽게 비유하자면, state_dict는 마치 레시피의 정확한 계량값과 같습니다.

요리사가 완벽한 맛을 찾아낸 후 그 비율을 기록해 두듯이, 모델이 학습을 통해 찾아낸 최적의 가중치 값들을 기록해 두는 것입니다. 레시피만 있으면 언제든 같은 맛을 재현할 수 있듯이, state_dict만 있으면 언제든 같은 성능의 모델을 복원할 수 있습니다.

state_dict가 없던 시절에는 어땠을까요? 개발자들은 모델을 저장하기 위해 복잡한 방법을 동원해야 했습니다.

각 레이어의 가중치를 일일이 추출하고, 별도의 파일로 저장한 뒤, 다시 불러올 때도 하나하나 수동으로 할당해야 했습니다. 실수하기도 쉬웠고, 코드도 길어졌습니다.

바로 이런 불편함을 해결하기 위해 PyTorch는 state_dict라는 우아한 해결책을 제공합니다. state_dict는 Python의 OrderedDict 형태로, 각 레이어의 이름을 키로, 해당 레이어의 가중치 텐서를 값으로 가지고 있습니다.

모델의 모든 학습 가능한 파라미터가 이 딕셔너리 안에 담깁니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 SimpleNet이라는 간단한 신경망을 정의합니다. 두 개의 완전연결층으로 구성되어 있습니다.

모델을 생성한 후, torch.save 함수에 model.state_dict()를 전달하면 가중치가 파일로 저장됩니다. 불러올 때는 먼저 동일한 구조의 모델 인스턴스를 생성해야 합니다.

그 다음 load_state_dict 메서드를 호출하여 저장된 가중치를 불러옵니다. 이때 torch.load로 먼저 파일을 읽어들입니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 이미지 분류 서비스를 개발한다고 가정해봅시다.

개발 환경에서 모델을 학습시킨 후 state_dict로 저장합니다. 그러면 프로덕션 서버에서 동일한 모델 구조를 정의하고 저장된 가중치만 불러오면 됩니다.

학습에 사용한 무거운 라이브러리 없이도 추론이 가능합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 모델 구조를 변경한 후 이전 가중치를 불러오려는 것입니다. 레이어 이름이나 크기가 달라지면 에러가 발생합니다.

따라서 가중치를 저장할 때는 모델 구조에 대한 정보도 함께 문서화해 두는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 바로 코드를 작성했습니다. "이제 다시는 모델을 잃어버리지 않을 거예요!" state_dict를 제대로 이해하면 모델 저장의 기본기를 확실히 다질 수 있습니다.

여러분도 오늘 학습시킨 모델이 있다면 바로 저장해 보세요.

실전 팁

💡 - 파일 확장자는 관례적으로 .pth 또는 .pt를 사용합니다

  • 저장 전 model.eval()로 평가 모드로 전환하는 습관을 들이세요
  • GPU에서 학습한 모델을 CPU에서 불러올 때는 map_location 인자를 사용하세요

2. 전체 모델 저장 vs 가중치만

김개발 씨가 팀 채팅방에 질문을 올렸습니다. "모델 전체를 저장하는 방법도 있던데, 그냥 전체를 저장하면 더 편하지 않나요?" 곧바로 박시니어 씨의 답변이 달렸습니다.

"편할 수는 있는데, 그게 항상 좋은 건 아니야."

PyTorch는 두 가지 저장 방식을 제공합니다. 가중치만 저장하는 방식과 모델 전체를 저장하는 방식입니다.

마치 요리 레시피만 저장하느냐, 완성된 요리를 통째로 냉동하느냐의 차이와 같습니다. 각각의 장단점을 알아야 상황에 맞게 선택할 수 있습니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

class MyModel(nn.Module):
    def __init__(self, input_size):
        super().__init__()
        self.layer = nn.Linear(input_size, 10)

    def forward(self, x):
        return self.layer(x)

model = MyModel(input_size=100)

# 방법 1: 가중치만 저장 (권장)
torch.save(model.state_dict(), 'weights_only.pth')

# 방법 2: 모델 전체 저장
torch.save(model, 'entire_model.pth')

# 불러오기 비교
# 가중치만: 모델 클래스 정의 필요
loaded_model = MyModel(input_size=100)
loaded_model.load_state_dict(torch.load('weights_only.pth'))

# 전체 모델: 클래스 정의 없이 바로 사용 (단, 같은 환경 필요)
loaded_entire = torch.load('entire_model.pth')

김개발 씨는 두 가지 저장 방식이 있다는 것을 알게 되었습니다. 처음에는 전체 모델을 저장하는 것이 더 편해 보였습니다.

클래스 정의 없이 바로 불러와서 사용할 수 있으니까요. 하지만 박시니어 씨는 고개를 저었습니다.

"단기적으로는 편해 보이지만, 나중에 문제가 생길 수 있어." 그렇다면 두 방식의 차이는 정확히 무엇일까요? 가중치만 저장하는 방식은 마치 요리 레시피의 정확한 계량값만 기록하는 것과 같습니다.

레시피 책이 있다면 언제든 그 계량값으로 같은 요리를 만들 수 있습니다. 여기서 레시피 책은 모델 클래스 정의에 해당합니다.

반면 전체 모델 저장은 완성된 요리를 통째로 냉동하는 것과 같습니다. 해동만 하면 바로 먹을 수 있어 편리합니다.

하지만 문제가 있습니다. 냉동 요리는 특정 냉장고에서만 제대로 해동될 수 있습니다.

전체 모델을 저장하면 어떤 문제가 생길까요? PyTorch는 전체 모델을 저장할 때 Python의 pickle 모듈을 사용합니다.

이 과정에서 모델 클래스가 정의된 파일의 경로와 구조가 함께 저장됩니다. 만약 나중에 코드 구조가 변경되거나, 다른 컴퓨터에서 불러오려 한다면 문제가 발생할 수 있습니다.

실제로 김개발 씨의 동료가 이런 경험을 했습니다. 개발 서버에서 저장한 모델을 프로덕션 서버에서 불러오려 했더니 "ModuleNotFoundError"가 발생했습니다.

개발 서버의 디렉토리 구조와 프로덕션 서버의 구조가 달랐기 때문입니다. 결국 코드를 수정하는 데 반나절을 썼습니다.

그렇다면 언제 전체 모델 저장을 사용할까요? 빠른 프로토타이핑이나 실험 단계에서는 전체 모델 저장이 유용할 수 있습니다.

같은 환경에서 계속 작업하고, 빠르게 결과를 확인해야 할 때 말입니다. 하지만 모델을 배포하거나 장기간 보관해야 한다면 가중치만 저장하는 것이 안전합니다.

위의 코드를 살펴보면 두 방식의 차이가 명확합니다. 가중치만 저장할 때는 model.state_dict()를 전달합니다.

불러올 때는 먼저 동일한 모델 클래스를 정의하고 인스턴스를 생성한 후 load_state_dict로 가중치를 주입합니다. 전체 모델 저장은 model 객체를 그대로 전달합니다.

불러올 때도 torch.load만 호출하면 됩니다. 간단해 보이지만, 앞서 말한 환경 의존성 문제가 있습니다.

PyTorch 공식 문서에서도 가중치만 저장하는 방식을 권장합니다. "저장된 모델의 이식성과 유연성을 위해 state_dict 저장을 권장합니다"라고 명시되어 있습니다.

이것이 커뮤니티의 표준 관행이기도 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

설명을 들은 김개발 씨는 이해했습니다. "아, 그래서 선배들이 항상 state_dict를 쓰라고 했군요!" 두 방식의 차이를 이해하면 상황에 맞는 올바른 선택을 할 수 있습니다.

특별한 이유가 없다면 가중치만 저장하는 습관을 들이세요.

실전 팁

💡 - 배포용 모델은 반드시 가중치만 저장하세요

  • 전체 모델 저장 시 pickle 보안 취약점에 주의하세요
  • 모델 클래스 정의 코드도 함께 버전 관리하는 것이 좋습니다

3. ONNX 포맷 변환

김개발 씨에게 새로운 미션이 주어졌습니다. "PyTorch로 만든 모델을 TensorFlow 팀에 전달해야 해요." 당황한 김개발 씨에게 박시니어 씨가 말했습니다.

"ONNX를 사용하면 돼. 딥러닝 세계의 공용어 같은 거야."

ONNX(Open Neural Network Exchange)는 서로 다른 딥러닝 프레임워크 간에 모델을 교환하기 위한 표준 포맷입니다. 마치 여러 나라 사람들이 영어로 소통하듯이, PyTorch, TensorFlow, ONNX Runtime 등 다양한 환경에서 하나의 모델을 공유할 수 있게 해줍니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

class ImageClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(3, 16, 3, padding=1)
        self.fc = nn.Linear(16 * 32 * 32, 10)

    def forward(self, x):
        x = torch.relu(self.conv(x))
        x = x.view(x.size(0), -1)
        return self.fc(x)

model = ImageClassifier()
model.eval()

# 더미 입력 생성 (배치 크기, 채널, 높이, 너비)
dummy_input = torch.randn(1, 3, 32, 32)

# ONNX로 변환
torch.onnx.export(
    model,
    dummy_input,
    "model.onnx",
    input_names=['image'],
    output_names=['prediction'],
    dynamic_axes={'image': {0: 'batch_size'}}  # 동적 배치 크기
)

김개발 씨는 난감한 상황에 처했습니다. ML 팀은 PyTorch를 사용하는데, 백엔드 팀은 TensorFlow Serving을 운영하고 있었습니다.

두 팀이 협업하려면 모델을 공유할 방법이 필요했습니다. 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다.

"ONNX는 딥러닝 모델의 '표준 설계도'라고 생각하면 돼." 그렇다면 ONNX는 어떻게 작동할까요? 쉽게 비유하자면, ONNX는 마치 건축 설계의 국제 표준 도면과 같습니다.

한국 건축가가 그린 도면을 미국 건축가도 이해할 수 있듯이, PyTorch로 만든 모델을 ONNX로 변환하면 TensorFlow나 다른 프레임워크에서도 이해하고 실행할 수 있습니다. ONNX가 없던 시절에는 어땠을까요?

개발자들은 프레임워크마다 모델을 따로 구현해야 했습니다. PyTorch 버전, TensorFlow 버전, 때로는 Caffe 버전까지.

같은 모델인데 여러 번 작성해야 하니 시간도 오래 걸리고, 버전 간 결과가 미묘하게 달라지는 문제도 있었습니다. ONNX의 등장으로 이런 문제가 해결되었습니다.

2017년 마이크로소프트와 페이스북이 공동으로 ONNX를 발표했습니다. 이후 구글, 아마존 등 주요 기업들이 참여하면서 사실상 표준이 되었습니다.

지금은 대부분의 딥러닝 프레임워크가 ONNX를 지원합니다. 위의 코드를 살펴보겠습니다.

먼저 ImageClassifier라는 간단한 CNN 모델을 정의합니다. ONNX로 변환하기 전에 반드시 **model.eval()**을 호출해야 합니다.

이것은 배치 정규화나 드롭아웃 같은 레이어가 추론 모드로 동작하게 합니다. dummy_input은 매우 중요합니다.

ONNX 변환은 이 더미 입력을 모델에 통과시키면서 연산 그래프를 추적합니다. 입력의 형태가 실제 서비스에서 사용할 형태와 같아야 합니다.

torch.onnx.export 함수에서 input_names와 output_names는 변환된 모델의 입출력 이름을 지정합니다. dynamic_axes는 특정 차원을 동적으로 만들어, 배치 크기가 달라져도 모델을 사용할 수 있게 합니다.

실제 현업에서 ONNX는 어떻게 활용될까요? 가장 흔한 사용 사례는 ONNX Runtime과의 조합입니다.

ONNX Runtime은 마이크로소프트가 개발한 고성능 추론 엔진입니다. PyTorch보다 추론 속도가 빠른 경우가 많아, 프로덕션 환경에서 선호됩니다.

또한 엣지 디바이스 배포에도 ONNX가 유용합니다. 모바일이나 임베디드 환경에서는 PyTorch 전체를 설치하기 어렵습니다.

ONNX로 변환하면 가벼운 런타임만으로 모델을 실행할 수 있습니다. 하지만 주의할 점도 있습니다.

모든 PyTorch 연산이 ONNX로 변환되는 것은 아닙니다. 특히 커스텀 연산이나 동적 제어 흐름이 있는 모델은 변환이 실패할 수 있습니다.

변환 후에는 반드시 결과를 검증해야 합니다. 박시니어 씨가 덧붙였습니다.

"변환된 ONNX 모델의 출력과 원본 PyTorch 모델의 출력을 비교해 봐. 숫자가 약간 다를 수 있지만, 오차가 크다면 문제가 있는 거야." 다시 김개발 씨의 이야기로 돌아가 봅시다.

ONNX로 모델을 변환하니 백엔드 팀과의 협업이 한결 수월해졌습니다. "프레임워크가 달라도 모델을 공유할 수 있다니, 정말 편리하네요!" ONNX를 활용하면 프레임워크의 경계를 넘어 유연하게 모델을 배포할 수 있습니다.

실전 팁

💡 - 변환 전 반드시 model.eval()을 호출하세요

  • onnx.checker.check_model()로 변환된 모델의 유효성을 검사하세요
  • 복잡한 모델은 opset_version을 조정해가며 변환을 시도해 보세요

4. TorchScript 활용

김개발 씨가 또 다른 고민에 빠졌습니다. "C++ 서버에서 모델을 돌려야 하는데, Python 없이도 가능한가요?" 박시니어 씨가 대답했습니다.

"TorchScript를 사용하면 Python 인터프리터 없이도 모델을 실행할 수 있어."

TorchScript는 PyTorch 모델을 Python 독립적인 형태로 직렬화하는 방법입니다. 마치 번역가가 한국어 소설을 영어로 번역하면 한국어를 모르는 사람도 읽을 수 있듯이, TorchScript로 변환하면 Python을 모르는 C++ 환경에서도 모델을 실행할 수 있습니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

class TransformerBlock(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.attention = nn.MultiheadAttention(dim, num_heads=4)
        self.norm = nn.LayerNorm(dim)

    def forward(self, x):
        attn_out, _ = self.attention(x, x, x)
        return self.norm(x + attn_out)

model = TransformerBlock(dim=64)
model.eval()

# 방법 1: Tracing - 예제 입력으로 그래프 추적
example_input = torch.randn(10, 1, 64)
traced_model = torch.jit.trace(model, example_input)
traced_model.save("traced_model.pt")

# 방법 2: Scripting - 코드 자체를 분석
scripted_model = torch.jit.script(model)
scripted_model.save("scripted_model.pt")

# 불러오기 (Python 또는 C++에서)
loaded = torch.jit.load("traced_model.pt")
output = loaded(example_input)

김개발 씨의 회사에서는 고성능이 필요한 서비스를 C++로 개발하고 있었습니다. Python의 GIL(Global Interpreter Lock) 때문에 멀티스레딩 성능에 한계가 있었기 때문입니다.

그런데 ML 모델은 Python으로 개발했으니, 어떻게 연동해야 할까요? 박시니어 씨가 설명했습니다.

"TorchScript는 PyTorch가 제공하는 '중간 언어'야. Python 코드를 이 중간 언어로 변환하면, C++에서도 실행할 수 있지." TorchScript는 어떻게 작동할까요?

쉽게 비유하자면, TorchScript는 마치 악보와 같습니다. 피아니스트가 연주하는 모습을 보고 악보를 만들 수도 있고(tracing), 작곡가가 직접 악보를 쓸 수도 있습니다(scripting).

어떤 방식이든 악보만 있으면 다른 피아니스트도 같은 곡을 연주할 수 있습니다. TorchScript에는 두 가지 변환 방법이 있습니다.

첫 번째는 Tracing입니다. 예제 입력을 모델에 통과시키면서 실행되는 연산들을 기록합니다.

마치 누군가의 행동을 관찰하고 따라하는 것과 같습니다. 장점은 간단하다는 것이고, 단점은 제어 흐름(if문, for문)을 제대로 캡처하지 못한다는 것입니다.

두 번째는 Scripting입니다. 코드 자체를 분석하여 TorchScript IR(Intermediate Representation)로 변환합니다.

Python 코드를 읽고 이해하는 것과 같습니다. 제어 흐름도 제대로 처리되지만, 모든 Python 문법을 지원하지는 않습니다.

위의 코드를 살펴보겠습니다. TransformerBlock이라는 어텐션 기반 모델을 정의했습니다.

Tracing을 사용할 때는 torch.jit.trace에 모델과 예제 입력을 전달합니다. 이 예제 입력이 모델을 통과하면서 연산 그래프가 기록됩니다.

Scripting을 사용할 때는 torch.jit.script에 모델만 전달합니다. PyTorch가 코드를 분석하여 변환합니다.

둘 다 .save() 메서드로 파일에 저장할 수 있습니다. 불러올 때는 torch.jit.load를 사용합니다.

Python에서도 되고, LibTorch(C++ PyTorch)에서도 같은 파일을 불러올 수 있습니다. 언제 Tracing을 쓰고, 언제 Scripting을 써야 할까요?

모델에 조건문이나 반복문이 없다면 Tracing이 더 간단합니다. 하지만 입력에 따라 다른 경로를 타는 모델이라면 Scripting을 사용해야 합니다.

실무에서는 두 방법을 섞어 쓰기도 합니다. 실제 현업에서 TorchScript는 다양하게 활용됩니다.

게임 회사에서는 C++ 게임 엔진에 ML 모델을 통합할 때 TorchScript를 사용합니다. 금융 회사에서는 저지연(low-latency)이 중요한 트레이딩 시스템에 활용합니다.

모바일 앱에서도 PyTorch Mobile과 함께 TorchScript 모델을 사용할 수 있습니다. 주의할 점도 있습니다.

TorchScript는 Python의 모든 기능을 지원하지 않습니다. 예를 들어 동적 타입, 일부 표준 라이브러리, 커스텀 Python 객체 등은 변환이 안 될 수 있습니다.

변환 전에 모델 코드를 TorchScript 호환되게 수정해야 할 수도 있습니다. 다시 김개발 씨의 이야기입니다.

TorchScript로 모델을 변환한 후, C++ 팀에 파일을 전달했습니다. C++ 팀은 LibTorch를 사용해 모델을 로드하고 추론을 수행했습니다.

Python 없이도 말이죠. TorchScript를 활용하면 PyTorch의 유연성과 C++의 성능을 동시에 누릴 수 있습니다.

실전 팁

💡 - 간단한 모델은 tracing, 복잡한 제어 흐름이 있으면 scripting을 사용하세요

  • @torch.jit.script 데코레이터로 특정 함수만 스크립트로 변환할 수 있습니다
  • 변환 후 torch.jit.optimize_for_inference()로 추가 최적화가 가능합니다

5. 모델 버전 관리

김개발 씨가 한숨을 쉬었습니다. "어제 저장한 모델이 어디 있더라?

v2였나 v3였나..." 모델 파일이 수십 개로 늘어나면서 관리가 점점 어려워지고 있었습니다. 박시니어 씨가 다가와 말했습니다.

"체계적인 버전 관리가 필요해 보이네."

모델 버전 관리는 학습된 모델과 그 메타데이터를 체계적으로 관리하는 방법입니다. 마치 도서관이 책을 분류하고 카탈로그를 만들듯이, 모델도 버전, 학습 정보, 성능 지표와 함께 저장해야 나중에 쉽게 찾고 비교할 수 있습니다.

다음 코드를 살펴봅시다.

import torch
import json
from datetime import datetime
from pathlib import Path

def save_model_with_version(model, optimizer, metrics, base_path="models"):
    # 버전 정보 생성
    version = datetime.now().strftime("%Y%m%d_%H%M%S")
    model_dir = Path(base_path) / f"v_{version}"
    model_dir.mkdir(parents=True, exist_ok=True)

    # 체크포인트 저장
    checkpoint = {
        'model_state_dict': model.state_dict(),
        'optimizer_state_dict': optimizer.state_dict(),
        'metrics': metrics,
        'timestamp': version,
        'pytorch_version': torch.__version__
    }
    torch.save(checkpoint, model_dir / "checkpoint.pth")

    # 메타데이터 JSON 저장
    metadata = {
        'version': version,
        'accuracy': metrics.get('accuracy'),
        'loss': metrics.get('loss'),
        'epochs': metrics.get('epochs')
    }
    with open(model_dir / "metadata.json", 'w') as f:
        json.dump(metadata, f, indent=2)

    return model_dir

김개발 씨의 모델 폴더는 엉망이었습니다. model_final.pth, model_final_v2.pth, model_really_final.pth...

어느 것이 가장 성능이 좋았는지, 어떤 하이퍼파라미터로 학습했는지 전혀 알 수 없었습니다. 박시니어 씨가 김개발 씨의 폴더를 보며 웃었습니다.

"누구나 처음에는 이렇게 해. 하지만 프로젝트가 커지면 반드시 체계가 필요해." 모델 버전 관리는 왜 중요할까요?

쉽게 비유하자면, 모델 버전 관리는 마치 와인 저장고의 라벨링 시스템과 같습니다. 좋은 와인 저장고는 각 병에 빈티지, 포도 품종, 생산지, 시음 노트를 기록해 둡니다.

나중에 특정 와인을 찾거나 비교할 때 이 정보가 결정적입니다. 체계적인 버전 관리가 없으면 어떤 문제가 생길까요?

가장 흔한 문제는 재현 불가능성입니다. "지난달에 학습한 모델이 더 좋았는데, 그때 어떤 설정이었지?" 기록이 없으면 답할 수 없습니다.

또한 롤백 불가능도 문제입니다. 새 모델이 오히려 성능이 떨어질 때, 이전 버전으로 돌아가야 하는데 어떤 것이 이전 버전인지 모릅니다.

위의 코드는 간단하지만 효과적인 버전 관리 시스템입니다. 먼저 타임스탬프를 기반으로 버전 이름을 생성합니다.

"v_20231215_143022" 같은 형식이죠. 각 버전마다 별도의 디렉토리를 만들어 파일들을 정리합니다.

checkpoint에는 모델 가중치뿐만 아니라 옵티마이저 상태, 성능 지표, 타임스탬프, PyTorch 버전까지 함께 저장합니다. 옵티마이저 상태를 저장하면 학습을 이어서 할 수 있습니다.

metadata.json은 빠른 조회를 위한 것입니다. 전체 체크포인트 파일을 열지 않고도 각 버전의 성능을 한눈에 볼 수 있습니다.

실무에서는 더 정교한 도구들을 사용합니다. MLflow는 가장 인기 있는 ML 실험 관리 도구입니다.

모델 버전 관리뿐 아니라 하이퍼파라미터 추적, 실험 비교, 모델 레지스트리 기능까지 제공합니다. Weights & Biases도 비슷한 기능을 클라우드 기반으로 제공합니다.

DVC(Data Version Control)는 Git과 함께 사용하는 데이터 및 모델 버전 관리 도구입니다. Git이 코드를 추적하듯이 DVC는 큰 파일들을 추적합니다.

좋은 버전 관리의 핵심 원칙은 무엇일까요? 첫째, 모든 것을 기록하세요.

가중치, 하이퍼파라미터, 데이터셋 버전, 무작위 시드, 환경 정보까지. 재현 가능성의 핵심입니다.

둘째, 일관된 네이밍 규칙을 사용하세요. 타임스탬프나 시맨틱 버저닝(v1.2.3)처럼 명확한 규칙을 정하고 지키세요.

셋째, 자동화하세요. 수동으로 하면 실수가 생깁니다.

학습 스크립트에 버전 관리 로직을 포함시키세요. 주의할 점도 있습니다.

모든 버전을 무한정 저장하면 스토리지가 금방 차게 됩니다. 보관 정책을 정해두세요.

예를 들어 "최근 10개 버전과 베스트 5개 버전만 유지"처럼요. 또한 메타데이터와 실제 체크포인트가 불일치하지 않도록 주의하세요.

다시 김개발 씨의 이야기입니다. 버전 관리 시스템을 도입한 후, 김개발 씨는 한 달 전 학습한 모델의 정확도가 91.5%였다는 것을 바로 확인할 수 있었습니다.

"이제 어떤 모델이 최고인지 한눈에 알 수 있네요!" 체계적인 버전 관리는 ML 프로젝트의 품질을 한 단계 높여줍니다.

실전 팁

💡 - 실험 초기에 버전 관리 시스템을 도입하세요, 나중에 하면 이미 늦습니다

  • 팀 프로젝트라면 MLflow나 W&B 같은 도구 도입을 고려하세요
  • Git LFS나 DVC를 사용해 모델 파일도 Git으로 추적할 수 있습니다

6. 체크포인트 전략

김개발 씨가 밤새 학습을 돌려놓고 출근했습니다. 그런데 화면을 보니 "CUDA out of memory" 에러와 함께 프로세스가 죽어 있었습니다.

8시간 동안의 학습이 물거품이 되었습니다. 박시니어 씨가 위로했습니다.

"체크포인트를 저장해뒀어야 했는데..."

체크포인트는 학습 중간중간에 모델 상태를 저장하는 것입니다. 마치 게임의 세이브 포인트처럼, 문제가 생겨도 처음부터 다시 시작할 필요 없이 마지막 저장 지점부터 이어갈 수 있습니다.

장시간 학습에서 체크포인트는 필수입니다.

다음 코드를 살펴봅시다.

import torch
from pathlib import Path

class CheckpointManager:
    def __init__(self, save_dir, max_to_keep=5):
        self.save_dir = Path(save_dir)
        self.save_dir.mkdir(parents=True, exist_ok=True)
        self.max_to_keep = max_to_keep
        self.checkpoints = []

    def save(self, model, optimizer, epoch, loss, is_best=False):
        checkpoint = {
            'epoch': epoch,
            'model_state_dict': model.state_dict(),
            'optimizer_state_dict': optimizer.state_dict(),
            'loss': loss
        }

        # 정기 체크포인트 저장
        path = self.save_dir / f"checkpoint_epoch_{epoch}.pth"
        torch.save(checkpoint, path)
        self.checkpoints.append(path)

        # 오래된 체크포인트 삭제
        while len(self.checkpoints) > self.max_to_keep:
            old = self.checkpoints.pop(0)
            old.unlink(missing_ok=True)

        # 베스트 모델 별도 저장
        if is_best:
            torch.save(checkpoint, self.save_dir / "best_model.pth")

    def load_latest(self):
        if self.checkpoints:
            return torch.load(self.checkpoints[-1])
        return None

김개발 씨의 악몽 같은 경험은 ML 엔지니어라면 누구나 한 번쯤 겪어봤을 것입니다. 며칠간의 학습이 갑작스러운 에러로 사라지는 것만큼 허탈한 일도 없습니다.

박시니어 씨가 말했습니다. "딥러닝 학습은 마라톤이야.

중간에 물도 마시고 쉬어야 해. 체크포인트가 바로 그 쉼터 역할을 하는 거지." 체크포인트는 왜 중요할까요?

쉽게 비유하자면, 체크포인트는 마치 등산할 때 중간 캠프를 설치하는 것과 같습니다. 정상을 향해 한 번에 올라가다 폭풍을 만나면 처음부터 다시 시작해야 합니다.

하지만 중간 캠프가 있다면 그곳에서 다시 출발할 수 있습니다. 체크포인트가 없으면 어떤 문제가 생길까요?

첫째, 시스템 장애입니다. 정전, 메모리 부족, GPU 오류 등 예상치 못한 문제가 발생할 수 있습니다.

둘째, 조기 종료가 필요할 때입니다. 학습 곡선을 보다가 "여기서 멈추는 게 나을 것 같아"라고 판단할 수 있습니다.

체크포인트가 있어야 그 지점의 모델을 사용할 수 있습니다. 위의 코드는 실무에서 사용할 수 있는 체크포인트 매니저입니다.

CheckpointManager 클래스는 체크포인트 저장과 관리를 담당합니다. max_to_keep 파라미터로 최대 보관 개수를 제한합니다.

모든 체크포인트를 저장하면 디스크가 금방 차기 때문입니다. save 메서드는 모델 가중치, 옵티마이저 상태, 에포크, 손실값을 함께 저장합니다.

옵티마이저 상태가 중요한 이유는 모멘텀이나 학습률 스케줄러 상태를 보존하기 위해서입니다. is_best 플래그가 True일 때는 별도로 베스트 모델을 저장합니다.

검증 손실이 최저일 때 이 플래그를 True로 설정하면 됩니다. 오래된 체크포인트는 자동 삭제됩니다.

리스트에서 가장 오래된 것을 꺼내고, 해당 파일을 삭제합니다. 이렇게 하면 디스크 공간을 효율적으로 사용할 수 있습니다.

좋은 체크포인트 전략의 핵심 원칙은 무엇일까요? 첫째, 정기 저장입니다.

매 에포크 또는 일정 스텝마다 저장하세요. 너무 자주 저장하면 I/O 오버헤드가 생기고, 너무 드물면 복구 지점이 멀어집니다.

보통 1-5 에포크마다가 적당합니다. 둘째, 베스트 모델 별도 보관입니다.

학습이 끝난 후 사용할 모델은 보통 손실이 가장 낮았던 시점의 모델입니다. 이것은 따로 보관하세요.

셋째, 충분한 정보 저장입니다. 나중에 학습을 이어가려면 모델 가중치뿐 아니라 옵티마이저 상태, 학습률 스케줄러 상태, 현재 에포크 등이 필요합니다.

실무에서 자주 사용되는 패턴도 있습니다. Early Stopping과의 연계가 대표적입니다.

검증 손실이 N 에포크 동안 개선되지 않으면 학습을 중단하고, 그때까지의 베스트 모델을 사용합니다. 분산 학습에서의 체크포인트도 고려해야 합니다.

여러 GPU나 여러 머신에서 학습할 때는 메인 프로세스에서만 체크포인트를 저장해야 합니다. 그렇지 않으면 파일이 충돌할 수 있습니다.

주의할 점도 있습니다. 체크포인트 파일은 생각보다 큽니다.

대형 모델의 경우 수 GB에 달할 수 있습니다. 디스크 용량을 미리 확보하고, 보관 정책을 명확히 하세요.

또한 체크포인트를 불러올 때 GPU 메모리 매핑에 주의하세요. map_location 인자를 적절히 사용해야 합니다.

다시 김개발 씨의 이야기입니다. 체크포인트 시스템을 도입한 후, 며칠 뒤 또 에러가 발생했습니다.

하지만 이번에는 달랐습니다. "7에포크까지 저장되어 있네!

여기서부터 다시 시작하면 되겠다." 체크포인트는 단순한 편의 기능이 아닙니다. 장시간 학습에서는 필수 안전장치입니다.

실전 팁

💡 - 클라우드 환경에서는 체크포인트를 S3 같은 외부 스토리지에도 백업하세요

  • 체크포인트 저장 시간이 너무 길다면 비동기 저장을 고려하세요
  • 학습 재개 시 무작위 시드도 복원해야 완벽한 재현이 가능합니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#PyTorch#ModelSerialization#ONNX#TorchScript#Checkpoint#MLOps#MLOps,Model Serialization

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.