Rule 1: Functional Core & Stateful Shell
[핵심 요약 (한글)]
- 의도: 실용적인 재현성을 위해 함수형 프로그래밍(Pure Logic)과 상태 관리(State Management)를 엄격히 분리하는 하이브리드 아키텍처를 채택
1. 함수형 코어 (Stateless Logic - 상태 없는 로직)
1)범위 (Scope):
- 메트릭(Metrics), 데이터 변환(Data Transforms), 손실 함수 수식(Loss Math), 모델의 구성 블록(Model Building Blocks).
- 예시:
- Metrics: [분류] Accuracy, Precision, Recall, F1-Score / [회귀] MAE, MSE
- Data Transforms: Image Normalization, Resize
2)제약 사항 (Constraint):
- 순수 함수(Pure functions)여야 함.
- 클래스 멤버 변수(self)에 의존하지 않고, 오직 입력 인자(input)에만 의존하여 동작해야 함
# [좋은 예] 입력 인자(input)에만 의존하는 함수
# self가 아예 없으므로, 오직 입력(in_channels, out_channels)에 의해서만 값이 결정됨.
# 언제, 어디서 호출하든 무조건 똑같은 구조의 레이어를 반환함 (Pure).
def double_conv(in_channels, out_channels):
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, padding=1),
# ... 생략
)
# [나쁜 예] self(상태)에 의존하는 함수
class BadModelBuilder:
def __init__(self, config):
self.config = config # [State] 설정값을 기억함
def make_layer(self):
# [문제] 함수 인자가 없는데도, self.config를 몰래 가져다 씀.
# 나중에 누가 self.config를 바꾸면, 이 함수의 결과도 변해버림 (Side Effect).
return nn.Conv2d(self.config['in_ch'], self.config['out_ch'], 3)
3)위치 (Placement):
- 기본적으로 모듈 레벨 함수(Module-level functions) 사용을 권장한다. (위 2) 제약사항 설명 밑에 있는 double_conv 예시 참조)
- @staticmethod는 오직 클래스 내부에서만 쓰이는 헬퍼 함수일 때만 제한적으로 사용한다.
class CheckpointManager: def save(self, path): # 메인 로직: 경로 유효성을 검사하고 저장 수행 if self._is_valid_path(path): print("저장 중...") # [Exception] 허용되는 경우: # 1. self를 안 씀 (순수 로직) -> staticmethod 적합 # 2. 하지만 '파일 경로 검사'는 이 클래스 밖에서는 쓸모가 없음. # 3. 굳이 밖으로 빼서 API를 어지럽히느니, 내부에 숨겨둠 (Helper). @staticmethod def _is_valid_path(path): return path.endswith(".pt") and len(path) > 0
4)무작위성 예외 (Randomness Exception):
- 규칙: 데이터 증강(Data Augmentation), 드롭아웃(Dropout) 등 확률적 연산(Stochastic ops)은 torch.manual_seed()로 관리되는 전역 상태(Global State)에 의존하는 것을 허용
- 근거: 엄격한 함수형 프로그래밍을 따르기 위해 모든 확률적 함수에 난수 생성기(Generator)를 인자로 전달하는 것은 구현 복잡도를 과도하게 높이고 비효율적이기 때문임. (실용성을 위한 타협).
# 1. PyTorch 내장 레이어
# 설명: Dropout은 내부적으로 torch.manual_seed()로 설정된 전역 상태를 공유함.
dropout = nn.Dropout(p=0.5)
# output = dropout(x) # <- 실행할 때마다 결과가 달라짐 (Not Pure Function)
# 2. 커스텀 데이터 증강 함수
def random_flip(image):
"""
[Impure Function] 전역 난수(torch.rand)에 의존하는 함수.
이 함수는 '순수 함수'가 아님. 입력값(image)이 동일해도,
함수 인자로 받지 않은 외부 전역 상태(torch.rand)에 따라
결과가 뒤집히거나 그대로일 수 있음
엄격한 재현성을 위해서는 seed를 인자로 받아야 하지만,
코드 복잡도를 낮추기 위해 데이터 증강 및 드롭아웃에 한해서는
이러한 전역 의존성을 허용함.
"""
# 전역 난수 생성기를 사용 (인자로 시드를 받지 않음 -> 예외 허용)
if torch.rand(1).item() > 0.5:
return torch.flip(image, dims=[3])
return image
2. 상태 기반 쉘 (State Management - 상태 관리)
- 범위 (Scope):
- nn.Module(모델): Weight를 가지고 있어서 학습될수록 값이 계속 변함
- Trainer(학습기): 현재 ‘몇 번째 에폭(Epoch)인지, 학습률(Learning Rate)이 얼마인지, 최고 점수(Best Score)가 얼마였는지’ 등을 기억해야 함
- DataLoader(데이터 로더): 현재 데이터셋의 몇 번째 배치를 꺼내고 있는지(배치 인덱스), 다음에 꺼낼 순서는 어떻게 섞었는지(셔플링 순서) 등을 기억해야 함
- 책임 (Responsibility):
- 전역 시드 (Global Seed): 프로그램 진입점(Entry point)에서 set_seed()를 딱 한 번만 호출
- 데이터로더 (DataLoader): 멀티프로세스 환경(num_workers > 0)에서 랜덤 데이터 로드 시, 데이터 중복 생성을 막기 위해 반드시 워커 ID(worker ID)를 활용하는 worker_init_fn과 별도의 generator를 설정하여 사용해야 함
- 모드 (Mode): .train()과 .eval() 상태 전환을 명확히 처리한다.
import torch
import torch.nn as nn
import numpy as np
import random
import os
# =========================================================
# [Stateful Shell] Infrastructure & Reproducibility
# Scope: Model, Trainer, DataLoader
# =========================================================
# 1. Entry Point Seed (Global State Responsibility)
def set_seed(seed: int = 42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
# [Performance Note]
# 속도를 위해 benchmark는 True(기본값) 유지.
# 완벽한 재현성이 필요할 때만 False로 변경
# torch.backends.cudnn.benchmark = False
# 2. DataLoader Worker Seeding (Essential for Multi-process)
def seed_worker(worker_id):
# base_seed(torch.initial_seed)에 worker_id를 더해 충돌 방지
worker_seed = (torch.initial_seed() + worker_id) % 2**32
np.random.seed(worker_seed)
random.seed(worker_seed)
class Trainer:
def __init__(self, model):
self.model = model # [Stateful] 모델(Weight) 관리
self.epoch = 0 # [Stateful] 학습 상태 관리
def prepare_data(self, dataset, batch_size):
# Generator: DataLoader의 Shuffle 순서 제어 (독립적 난수)
g = torch.Generator()
g.manual_seed(42)
return torch.utils.data.DataLoader(
dataset,
batch_size=batch_size,
num_workers=4,
worker_init_fn=seed_worker, # 워커별 난수 격리
generator=g # 셔플 순서 고정
)
def train_step(self, batch):
# [Rule 2] Mode: 상태 전환 명시
self.model.train()
# ... 학습 로직 ...
def validation_step(self, batch):
# [Rule 2] Mode: 상태 전환 명시
self.model.eval()
# ... 평가 로직 ...
3. 로드맵 & 한계 (고급 재현성을 위한 참고사항)
- 성능 타협 (Performance Trade-off): cudnn.benchmark는 학습 속도를 위해 True(기본값)로 유지한다.
- 환경 변수 (Environment): PYTHONHASHSEED (해시시드, 파이썬 딕셔너리 순서 설정 옵션)와 CUBLAS_WORKSPACE_CONFIG (GPU연산 재현성 설정)는 성능 저하 우려가 있으므로 기본적으로 설정하지 않는다(고정하지 않는다).
'스프린트미션' 카테고리의 다른 글
| 위클리페이퍼 7 (0) | 2026.03.03 |
|---|---|
| 위클리 페이퍼 #6 (0) | 2026.02.22 |
| 규칙3 (0) | 2026.02.12 |
| 규칙2 (0) | 2026.02.12 |
| [AI] 위클리페이퍼 #5 (0) | 2026.02.09 |