스프린트미션

규칙1

goodella 2026. 2. 12. 15:40

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