정규화(Regularization) — 드롭아웃(Dropout), 가중치 감쇠(Weight Decay), 배치 정규화(BatchNorm)

모델(model)이 학습 데이터(training data)에서는 99%, 테스트 데이터(test data)에서는 60%를 얻었습니다. 학습한 것이 아니라 외운 것입니다. 정규화(regularization)는 일반화(generalization)를 강제하기 위해 복잡도(complexity)에 부과하는 세금입니다.

유형: Build

언어: Python

선수 지식: Lesson 03.06 옵티마이저(Optimizers)

소요 시간: 약 75분

학습 목표

  • 역스케일링(inverted scaling)을 사용하는 드롭아웃(dropout), L2 가중치 감쇠(L2 weight decay), 배치 정규화(batch normalization), 층 정규화(layer normalization), RMSNorm을 처음부터 구현합니다.
  • 학습-테스트 정확도 격차(train-test accuracy gap)를 측정하고 정규화 실험(regularization experiment)으로 과적합(overfitting)을 진단합니다.
  • 트랜스포머(transformer)가 BatchNorm 대신 LayerNorm을 쓰는 이유와 현대 LLM(modern LLM)이 RMSNorm을 선호하는 이유를 설명합니다.
  • 과적합 심각도(severity)에 따라 적절한 정규화 기법(regularization technique) 조합을 적용합니다.

문제

파라미터(parameter)가 충분한 신경망(neural network)은 어떤 데이터셋(dataset)도 외울 수 있습니다. 이것은 가설이 아닙니다. Zhang et al.(2017)은 표준 네트워크(standard network)를 무작위 레이블(random label)이 붙은 ImageNet에서 학습시켜 거의 0에 가까운 학습 손실(near-zero training loss)에 도달하게 했습니다. 배울 패턴(pattern)이 전혀 없는데도 백만 개의 무작위 입력-출력 쌍(random input-output pair)을 외운 것입니다. 학습 손실은 완벽했지만 테스트 정확도(test accuracy)는 0이었습니다.

이것이 과적합(overfitting) 문제입니다. 모델이 커질수록 심해집니다. GPT-3는 1,750억 개 파라미터를 갖고 있고 학습 집합(training set)은 약 5,000억 토큰(token)입니다. 이 정도 용량(capacity)이라면 학습 데이터의 상당 부분을 말 그대로(verbatim) 외울 수 있습니다. 정규화가 없다면 일반화 가능한 패턴(generalizable pattern) 대신 학습 예제(training example)를 그대로 토해낼 위험이 커집니다.

학습 성능(training performance)과 테스트 성능(test performance)의 차이를 과적합 격차(overfitting gap)라고 부릅니다. 이 lesson의 모든 기법은 이 격차를 서로 다른 각도에서 공격합니다. 드롭아웃(Dropout)은 네트워크가 특정 뉴런(neuron) 하나에 의존하지 못하게 만듭니다. 가중치 감쇠(Weight decay)는 특정 가중치(weight)가 지나치게 커지는 것을 막습니다. BatchNorm은 손실 지형(loss landscape)을 매끄럽게 만들어 옵티마이저(optimizer)가 더 평평하고 일반화 가능한 최소점(minima)을 찾도록 돕습니다. LayerNorm은 배치 크기(batch size)가 작거나 시퀀스 길이(sequence length)가 변하는 등 BatchNorm이 실패하는 상황에서 같은 역할을 대신합니다. RMSNorm은 평균(mean) 계산을 없애 약 10% 더 빠르게 동작합니다. 각각의 기법은 단순하지만, 함께 사용하면 그저 외우기만 하는 모델과 제대로 일반화하는 모델을 가르는 결정적인 차이를 만듭니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.신경망(neural network)에서 과적합(overfitting)은 무엇인가요?

2.드롭아웃(dropout)은 신경망(neural network)을 어떻게 정규화(regularize)하나요?

0/2 답변 완료

개념

과적합 스펙트럼(The Overfitting Spectrum)

모든 모델은 과소적합(underfitting, 패턴을 잡기에는 너무 단순함)과 과적합(overfitting, 너무 복잡해서 잡음까지 잡음) 사이 어딘가에 있습니다. 좋은 지점(sweet spot)은 중간이고, 정규화는 과적합 쪽에 있는 모델을 그 지점으로 밀어냅니다.

graph LR
    Under["Underfitting<br/>Train: 60%<br/>Test: 58%<br/>Model too simple"] --> Good["Good Fit<br/>Train: 95%<br/>Test: 92%<br/>Generalizes well"]
    Good --> Over["Overfitting<br/>Train: 99.9%<br/>Test: 65%<br/>Memorized noise"]

    Dropout["Dropout"] -->|"Pushes left"| Over
    WD["Weight Decay"] -->|"Pushes left"| Over
    BN["BatchNorm"] -->|"Pushes left"| Over
    Aug["Data Augmentation"] -->|"Pushes left"| Over

드롭아웃(Dropout)

드롭아웃(dropout)은 가장 단순한 정규화 기법이면서 해석도 우아합니다. 학습(training) 중 각 뉴런 출력(neuron output)을 확률 p로 0으로 만듭니다.

output = activation(z) * mask    where mask[i] ~ Bernoulli(1 - p)

p = 0.5라면 매 순전파(forward pass)마다 뉴런 절반이 0이 됩니다. 네트워크는 어떤 뉴런이 살아 있을지 예측할 수 없으므로 중복 표현(redundant representation)을 학습해야 합니다. 특정 뉴런 조합에 의존하는 공적응(co-adaptation)을 막습니다.

앙상블(ensemble) 해석도 있습니다. N개 뉴런을 가진 네트워크에 dropout을 적용하면 켜지고 꺼지는 뉴런 조합에 따라 2^N개의 가능한 서브네트워크(subnetwork)가 생깁니다. dropout 학습은 서로 다른 미니배치(mini-batch)에서 이 2^N개 서브네트워크를 동시에 근사적으로 학습하는 것과 같습니다. 테스트 시점에는 모든 뉴런을 사용하고 dropout 없이 예측합니다. 이는 하나의 모델에서 거대한 앙상블의 평균 예측을 얻는 것과 비슷합니다.

실무에서는 학습 시점에 스케일을 보정해 두는 역드롭아웃(inverted dropout)을 사용합니다.

During training:  output = activation(z) * mask / (1 - p)
During testing:   output = activation(z)

이렇게 하면 테스트 코드(test code)가 드롭아웃 스케일링(dropout scaling)을 따로 알 필요가 없어 깔끔해집니다. 기본 비율(default rate)은 트랜스포머에서 p=0.1, 다층 퍼셉트론(MLP)에서 p=0.5, 합성곱 신경망(CNN)에서 p=0.2-0.3 정도를 사용합니다. 드롭아웃 비율이 클수록 정규화는 강해지지만 과소적합(underfitting) 위험도 함께 커집니다.

가중치 감쇠(Weight Decay, L2 Regularization)

손실(loss)에 모든 가중치 크기(weight magnitude)의 제곱을 더합니다.

total_loss = task_loss + (lambda / 2) * sum(w_i^2)

정규화 항(regularization term)의 기울기(gradient)는 lambda * w입니다. 즉 매 학습 단계(step)마다 각 가중치는 자기 크기에 비례한 만큼 0 쪽으로 줄어듭니다(shrink). 큰 가중치일수록 더 큰 벌점을 받게 되므로, 모델은 특정 가중치 하나가 모든 것을 지배하지 않는 해(solution)로 밀려갑니다.

이 방식이 일반화에 도움이 되는 이유는, 과적합된 모델일수록 학습 데이터의 잡음(noise)을 증폭하는 큰 가중치를 갖기 쉽기 때문입니다. 가중치 감쇠(weight decay)는 가중치를 작게 유지해 모델의 유효 용량(effective capacity)을 제한하고, 외워 둔 특이점(memorized quirk)이 아니라 견고하고 일반화 가능한 특징(robust, generalizable feature)에 의존하도록 유도합니다.

lambda 하이퍼파라미터(hyperparameter)는 정규화 강도를 조절합니다. 흔히 쓰이는 값은 다음과 같습니다.

  • 트랜스포머에서 AdamW를 쓸 때: 0.01
  • CNN에서 SGD를 쓸 때: 1e-4
  • 심하게 과적합된 모델: 0.1

Lesson 06에서 다뤘듯, SGD에서는 L2 정규화(L2 regularization)와 가중치 감쇠(weight decay)가 수학적으로 동등(equivalent)하지만 Adam에서는 그렇지 않습니다. Adam 계열 옵티마이저(optimizer)를 사용할 때는 분리된 가중치 감쇠(decoupled weight decay)를 제공하는 AdamW를 항상 사용합니다.

배치 정규화(Batch Normalization)

BatchNorm은 다음 층으로 넘기기 전에 각 층 출력(layer output)을 미니배치(mini-batch) 전체 기준으로 정규화(normalize)합니다.

mu = (1/B) * sum(x_i)
sigma^2 = (1/B) * sum((x_i - mu)^2)
x_hat = (x_i - mu) / sqrt(sigma^2 + eps)
y = gamma * x_hat + beta

gammabeta는 학습 가능한 파라미터(learnable parameter)로, 필요하다면 네트워크가 정규화를 되돌릴 수 있게 해 줍니다. 이 두 파라미터가 없다면 모든 층 출력을 평균 0, 분산 1로 강제하게 되는데, 이것이 항상 네트워크가 원하는 표현은 아니기 때문입니다.

학습(training) 중에는 현재 미니배치의 평균(mean)과 분산(variance)을 사용하고, 추론(inference) 중에는 학습 중 누적해 둔 실행 평균(running average)을 사용합니다. 보통 모멘텀(momentum) 0.1의 지수 이동 평균(exponential moving average)을 사용하며, 이는 "기존 값의 90%에 새 값의 10%를 더한다"는 뜻입니다. 그래서 model.train()model.eval()의 차이가 중요합니다.

BatchNorm이 왜 작동하는지는 여전히 논쟁이 있는 주제입니다. 원 논문은 "내부 공변량 변화(internal covariate shift)"를 줄이기 때문이라고 설명했지만, Santurkar et al.(2018)은 이 설명이 틀렸음을 보였습니다. 실제 이유는 BatchNorm이 손실 지형(loss landscape)을 더 매끄럽게 만들기 때문입니다. 기울기(gradient)가 더 예측 가능해지고 립시츠 상수(Lipschitz constant)가 작아지며, 옵티마이저(optimizer)가 더 큰 단계(step)를 안전하게 밟을 수 있게 됩니다. 그래서 BatchNorm을 사용하면 더 높은 학습률(learning rate)을 쓰면서도 더 빠르게 수렴할 수 있습니다.

BatchNorm에는 근본적인 한계가 있습니다. BatchNorm은 배치 통계(batch statistics)에 의존하기 때문에 배치 크기(batch size)가 1이면 평균과 분산이 의미가 없어집니다. 배치가 작을 때(< 32)는 통계가 잡음(noise)이 많아져 오히려 성능을 해칠 수 있습니다. 이는 메모리 제약으로 배치 크기를 키우기 어려운 객체 탐지(object detection)나, 시퀀스 길이(sequence length)가 들쭉날쭉한 언어 모델링(language modeling)에서 특히 중요합니다.

층 정규화(Layer Normalization)

LayerNorm은 배치 차원이 아니라 특징 차원(feature dimension)을 기준으로 정규화합니다. 단일 샘플(sample) 안에서 계산이 이루어집니다.

mu = (1/D) * sum(x_j)
sigma^2 = (1/D) * sum((x_j - mu)^2)
x_hat = (x_j - mu) / sqrt(sigma^2 + eps)
y = gamma * x_hat + beta

여기서 D는 특징 차원(feature dimension)입니다. 각 샘플은 독립적으로 정규화되므로 배치 크기(batch size)에 의존하지 않습니다. 이 때문에 트랜스포머(transformer)는 BatchNorm 대신 LayerNorm을 사용합니다. 시퀀스 길이(sequence length)가 변하거나 생성(generation) 중에 배치 크기가 1이어도 학습과 추론의 계산이 동일합니다.

트랜스포머에서 LayerNorm은 각 셀프 어텐션 블록(self-attention block)과 피드 포워드 블록(feed-forward block) 뒤(Post-LN), 또는 그 앞(Pre-LN)에 적용됩니다. Pre-LN 방식이 학습 안정성이 더 좋습니다.

RMSNorm

RMSNorm은 평균 빼기(mean subtraction)를 뺀 LayerNorm입니다. Zhang & Sennrich(2019)가 제안했습니다.

rms = sqrt((1/D) * sum(x_j^2))
y = gamma * x / rms

끝입니다. 평균 계산도 없고 beta parameter도 없습니다. 관찰은 간단합니다. LayerNorm의 재중심화(re-centering, mean subtraction)는 모델 성능에 거의 기여하지 않지만 계산 비용은 듭니다. 이를 제거하면 거의 같은 정확도를 약 10% 낮은 오버헤드로 얻을 수 있습니다.

LLaMA, LLaMA 2, LLaMA 3, Mistral을 비롯해 대부분의 현대 LLM(modern LLM)은 LayerNorm 대신 RMSNorm을 사용합니다. 수십억 개 파라미터와 수조 개 토큰(token) 규모에서는 이 10% 절감이 매우 의미 있는 차이를 만듭니다.

정규화 비교(Normalization Comparison)

graph TD
    subgraph "Batch Normalization"
        BN_D["각 feature에 대해<br/>BATCH 방향으로 normalize"]
        BN_S["Batch: [x1, x2, x3, x4]<br/>Feature 1: [x1f1, x2f1, x3f1, x4f1] normalize"]
        BN_P["batch > 32 필요<br/>train/eval 계산 다름<br/>CNN에서 사용"]
    end
    subgraph "Layer Normalization"
        LN_D["각 sample에 대해<br/>FEATURE 방향으로 normalize"]
        LN_S["Sample x1: [f1, f2, f3, f4] normalize"]
        LN_P["batch-independent<br/>train/eval 동일<br/>Transformers에서 사용"]
    end
    subgraph "RMS Normalization"
        RN_D["LayerNorm과 비슷하지만<br/>mean subtraction 생략"]
        RN_S["RMS로만 나눔<br/>centering 없음"]
        RN_P["LayerNorm보다 약 10% 빠름<br/>정확도 유사<br/>LLaMA, Mistral에서 사용"]
    end

정규화로서의 데이터 증강(Data Augmentation)

모델 수정이 아니라 데이터 수정입니다. 레이블을 보존하면서(label-preserving) 학습 입력을 변환합니다.

  • 이미지(Images): 무작위 잘라내기(random crop), 좌우 뒤집기(flip), 회전(rotation), 색상 흔들기(color jitter), 컷아웃(cutout)
  • 텍스트(Text): 동의어 치환(synonym replacement), 역번역(back-translation), 무작위 단어 삭제(random deletion)
  • 오디오(Audio): 시간 늘리기(time stretch), 음높이 변경(pitch shift), 잡음 추가(noise addition)

효과는 정규화와 같습니다. 학습 집합의 유효 크기(effective size)를 키워 모델이 특정 예제를 외우기 어렵게 만듭니다. 원본 형태의 이미지를 한 번만 보는 모델은 외울 수 있습니다. 각 이미지를 50개의 증강 버전으로 보는 모델은 불변 구조(invariant structure)를 학습하도록 강제됩니다.

조기 종료(Early Stopping)

가장 단순한 정규화 기법입니다. 검증 손실(validation loss)이 증가하기 시작하는 시점에 학습을 멈추는 방식으로, 그 시점에는 아직 모델이 과적합되지 않은 상태입니다. 실무에서는 매 에폭(epoch)마다 검증 손실을 추적하면서 가장 좋은 모델을 저장해 두고, 인내 기간(patience window)을 함께 둡니다. 보통 5~20 에폭 정도를 두며, 이 기간 동안 검증 손실이 개선되지 않으면 학습을 멈추고 저장해 둔 최적 모델(best model)을 불러옵니다.

언제 무엇을 적용할까

flowchart TD
    Gap{"Train-test<br/>accuracy gap?"} -->|"> 10%"| Heavy["강한 regularization"]
    Gap -->|"5-10%"| Medium["중간 regularization"]
    Gap -->|"< 5%"| Light["가벼운 regularization"]

    Heavy --> D5["Dropout p=0.3-0.5"]
    Heavy --> WD2["Weight decay 0.01-0.1"]
    Heavy --> Aug["Aggressive data augmentation"]
    Heavy --> ES["Early stopping"]

    Medium --> D3["Dropout p=0.1-0.2"]
    Medium --> WD1["Weight decay 0.001-0.01"]
    Medium --> Norm["BatchNorm or LayerNorm"]

    Light --> D1["Dropout p=0.05-0.1"]
    Light --> WD0["Weight decay 1e-4"]

만들어 보기

Step 1: 드롭아웃(Dropout) — 학습 모드와 평가 모드(Train and Eval Mode)

import random
import math


class Dropout:
    def __init__(self, p=0.5):
        self.p = p
        self.training = True
        self.mask = None

    def forward(self, x):
        if not self.training:
            return list(x)
        self.mask = []
        output = []
        for val in x:
            if random.random() < self.p:
                self.mask.append(0)
                output.append(0.0)
            else:
                self.mask.append(1)
                output.append(val / (1 - self.p))
        return output

    def backward(self, grad_output):
        grads = []
        for g, m in zip(grad_output, self.mask):
            if m == 0:
                grads.append(0.0)
            else:
                grads.append(g / (1 - self.p))
        return grads

Step 2: L2 가중치 감쇠(L2 Weight Decay)

def l2_regularization(weights, lambda_reg):
    penalty = 0.0
    for w in weights:
        penalty += w * w
    return lambda_reg * 0.5 * penalty

def l2_gradient(weights, lambda_reg):
    return [lambda_reg * w for w in weights]

Step 3: 배치 정규화(Batch Normalization)

class BatchNorm:
    def __init__(self, num_features, momentum=0.1, eps=1e-5):
        self.gamma = [1.0] * num_features
        self.beta = [0.0] * num_features
        self.eps = eps
        self.momentum = momentum
        self.running_mean = [0.0] * num_features
        self.running_var = [1.0] * num_features
        self.training = True
        self.num_features = num_features

    def forward(self, batch):
        batch_size = len(batch)
        if self.training:
            mean = [0.0] * self.num_features
            for sample in batch:
                for j in range(self.num_features):
                    mean[j] += sample[j]
            mean = [m / batch_size for m in mean]

            var = [0.0] * self.num_features
            for sample in batch:
                for j in range(self.num_features):
                    var[j] += (sample[j] - mean[j]) ** 2
            var = [v / batch_size for v in var]

            for j in range(self.num_features):
                self.running_mean[j] = (1 - self.momentum) * self.running_mean[j] + self.momentum * mean[j]
                self.running_var[j] = (1 - self.momentum) * self.running_var[j] + self.momentum * var[j]
        else:
            mean = list(self.running_mean)
            var = list(self.running_var)

        self.x_hat = []
        output = []
        for sample in batch:
            normalized = []
            out_sample = []
            for j in range(self.num_features):
                x_h = (sample[j] - mean[j]) / math.sqrt(var[j] + self.eps)
                normalized.append(x_h)
                out_sample.append(self.gamma[j] * x_h + self.beta[j])
            self.x_hat.append(normalized)
            output.append(out_sample)
        return output

Step 4: 층 정규화(Layer Normalization)

class LayerNorm:
    def __init__(self, num_features, eps=1e-5):
        self.gamma = [1.0] * num_features
        self.beta = [0.0] * num_features
        self.eps = eps
        self.num_features = num_features

    def forward(self, x):
        mean = sum(x) / len(x)
        var = sum((xi - mean) ** 2 for xi in x) / len(x)

        self.x_hat = []
        output = []
        for j in range(self.num_features):
            x_h = (x[j] - mean) / math.sqrt(var + self.eps)
            self.x_hat.append(x_h)
            output.append(self.gamma[j] * x_h + self.beta[j])
        return output

Step 5: RMSNorm

class RMSNorm:
    def __init__(self, num_features, eps=1e-6):
        self.gamma = [1.0] * num_features
        self.eps = eps
        self.num_features = num_features

    def forward(self, x):
        rms = math.sqrt(sum(xi * xi for xi in x) / len(x) + self.eps)
        output = []
        for j in range(self.num_features):
            output.append(self.gamma[j] * x[j] / rms)
        return output

Step 6: 정규화 적용/미적용 학습 비교(Training With and Without Regularization)

def sigmoid(x):
    x = max(-500, min(500, x))
    return 1.0 / (1.0 + math.exp(-x))


def make_circle_data(n=200, seed=42):
    random.seed(seed)
    data = []
    for _ in range(n):
        x = random.uniform(-2, 2)
        y = random.uniform(-2, 2)
        label = 1.0 if x * x + y * y < 1.5 else 0.0
        data.append(([x, y], label))
    return data


class RegularizedNetwork:
    def __init__(self, hidden_size=16, lr=0.05, dropout_p=0.0, weight_decay=0.0):
        random.seed(0)
        self.hidden_size = hidden_size
        self.lr = lr
        self.dropout_p = dropout_p
        self.weight_decay = weight_decay
        self.dropout = Dropout(p=dropout_p) if dropout_p > 0 else None

        self.w1 = [[random.gauss(0, 0.5) for _ in range(2)] for _ in range(hidden_size)]
        self.b1 = [0.0] * hidden_size
        self.w2 = [random.gauss(0, 0.5) for _ in range(hidden_size)]
        self.b2 = 0.0

    def forward(self, x, training=True):
        self.x = x
        self.z1 = []
        self.h = []
        for i in range(self.hidden_size):
            z = self.w1[i][0] * x[0] + self.w1[i][1] * x[1] + self.b1[i]
            self.z1.append(z)
            self.h.append(max(0.0, z))

        if self.dropout and training:
            self.dropout.training = True
            self.h = self.dropout.forward(self.h)
        elif self.dropout:
            self.dropout.training = False
            self.h = self.dropout.forward(self.h)

        self.z2 = sum(self.w2[i] * self.h[i] for i in range(self.hidden_size)) + self.b2
        self.out = sigmoid(self.z2)
        return self.out

    def backward(self, target):
        eps = 1e-15
        p = max(eps, min(1 - eps, self.out))
        d_loss = -(target / p) + (1 - target) / (1 - p)
        d_sigmoid = self.out * (1 - self.out)
        d_out = d_loss * d_sigmoid

        for i in range(self.hidden_size):
            d_relu = 1.0 if self.z1[i] > 0 else 0.0
            d_h = d_out * self.w2[i] * d_relu
            self.w2[i] -= self.lr * (d_out * self.h[i] + self.weight_decay * self.w2[i])
            for j in range(2):
                self.w1[i][j] -= self.lr * (d_h * self.x[j] + self.weight_decay * self.w1[i][j])
            self.b1[i] -= self.lr * d_h
        self.b2 -= self.lr * d_out

    def evaluate(self, data):
        correct = 0
        total_loss = 0.0
        for x, y in data:
            pred = self.forward(x, training=False)
            eps = 1e-15
            p = max(eps, min(1 - eps, pred))
            total_loss += -(y * math.log(p) + (1 - y) * math.log(1 - p))
            if (pred >= 0.5) == (y >= 0.5):
                correct += 1
        return total_loss / len(data), correct / len(data) * 100

    def train_model(self, train_data, test_data, epochs=300):
        history = []
        for epoch in range(epochs):
            total_loss = 0.0
            correct = 0
            for x, y in train_data:
                pred = self.forward(x, training=True)
                self.backward(y)
                eps = 1e-15
                p = max(eps, min(1 - eps, pred))
                total_loss += -(y * math.log(p) + (1 - y) * math.log(1 - p))
                if (pred >= 0.5) == (y >= 0.5):
                    correct += 1
            train_loss = total_loss / len(train_data)
            train_acc = correct / len(train_data) * 100
            test_loss, test_acc = self.evaluate(test_data)
            history.append((train_loss, train_acc, test_loss, test_acc))
            if epoch % 75 == 0 or epoch == epochs - 1:
                gap = train_acc - test_acc
                print(f"    Epoch {epoch:3d}: train_acc={train_acc:.1f}%, test_acc={test_acc:.1f}%, gap={gap:.1f}%")
        return history

사용하기

PyTorch는 정규화(normalization)와 규제(regularization)를 모듈(module) 형태로 제공합니다.

import torch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(784, 256),
    nn.BatchNorm1d(256),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(256, 128),
    nn.BatchNorm1d(128),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(128, 10),
)

model.train()
out_train = model(torch.randn(32, 784))

model.eval()
out_test = model(torch.randn(1, 784))

model.train()model.eval() 전환(toggle)은 매우 중요합니다. 이 전환은 드롭아웃(dropout)을 켜고 끄며, BatchNorm이 배치 통계(batch statistics)를 쓸지 실행 통계(running statistics)를 쓸지를 결정합니다. 추론(inference) 전에 model.eval()을 호출하지 않으면 드롭아웃이 계속 활성화(active) 상태이고, BatchNorm 역시 현재 미니배치 통계(current mini-batch statistics)를 사용해 출력(output)이 흔들리게 됩니다.

트랜스포머(transformer)에서는 패턴이 조금 다릅니다.

class TransformerBlock(nn.Module):
    def __init__(self, d_model=512, nhead=8, dropout=0.1):
        super().__init__()
        self.attention = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
        self.norm1 = nn.LayerNorm(d_model)
        self.ff = nn.Sequential(
            nn.Linear(d_model, d_model * 4),
            nn.GELU(),
            nn.Linear(d_model * 4, d_model),
            nn.Dropout(dropout),
        )
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        attended, _ = self.attention(x, x, x)
        x = self.norm1(x + self.dropout(attended))
        x = self.norm2(x + self.ff(x))
        return x

BatchNorm이 아니라 LayerNorm을 사용하고, 드롭아웃 비율은 p=0.5가 아니라 p=0.1을 씁니다. 이것이 트랜스포머의 기본값(default)입니다.

산출물 만들기

이 lesson의 산출물은 다음과 같습니다.

  • outputs/prompt-regularization-advisor.md: 과적합(overfitting)을 진단하고 적절한 정규화 전략(regularization strategy)을 추천해 주는 프롬프트(prompt)

연습문제

  1. (쉬움) 2차원 데이터(2D data)를 위한 공간 드롭아웃(spatial dropout)을 구현합니다. 개별 뉴런(individual neuron)을 끄는 대신 특징 채널(feature channel) 전체를 한꺼번에 드롭합니다. 원형 데이터셋(circle dataset)에서 일반 드롭아웃(standard dropout)과 학습-테스트 격차(train-test gap)를 비교합니다.
  2. (중간) Lesson 05의 레이블 스무딩(label smoothing)과 이 lesson의 드롭아웃(dropout)을 함께 적용합니다. 둘 다 끔, 드롭아웃만, 레이블 스무딩만, 둘 다 켬의 네 가지 설정(configuration)에 대해 최종 학습-테스트 격차(final train-test gap)를 측정합니다.
  3. (중간) 은닉층(hidden layer)과 활성화 함수(activation) 사이에 BatchNorm 층(BatchNorm layer)을 추가합니다. 학습률(learning rate)을 0.01, 0.05, 0.1로 바꿔 가며 BatchNorm 유무에 따른 차이를 비교합니다.
  4. (중간) 조기 종료(early stopping)를 구현합니다. 테스트 손실(test loss)을 추적하면서 가장 좋은 가중치(best weights)를 저장하고, 20 에폭(epoch) 동안 개선이 없으면 학습을 멈춥니다.
  5. (어려움) 4층 네트워크(4-layer network)에서 LayerNorm과 RMSNorm을 비교합니다. 동일한 가중치(weight)로 초기화한 뒤 정확도(accuracy), 에폭당 소요 시간(epoch time), 첫 번째 층의 기울기 크기(first layer gradient magnitude)를 비교합니다.

핵심 용어

용어흔한 설명실제 의미
과적합(Overfitting)"데이터를 외움"학습 성능(training performance)이 테스트 성능(test performance)보다 크게 높아 잡음(noise)까지 학습한 상태
정규화(Regularization)"과적합 방지"모델 복잡도(model complexity)를 제한해 일반화(generalization)를 개선하는 모든 기법
드롭아웃(Dropout)"무작위 뉴런 삭제(random neuron deletion)"학습(training) 중 무작위 뉴런(random neuron)을 0으로 만들어 중복 표현(redundant representation)을 학습하도록 강제하는 기법
가중치 감쇠(Weight decay)"L2 페널티(L2 penalty)"매 학습 단계(step)마다 lambda * w만큼 가중치(weight)를 0 쪽으로 줄이는 정규화 항(regularizer)
배치 정규화(Batch normalization)"배치별로 정규화"학습 중에는 배치 통계(batch statistics)로, 추론(inference) 중에는 실행 평균(running averages)으로 층 출력(layer output)을 정규화
층 정규화(Layer normalization)"샘플별로 정규화"각 샘플(sample) 내부의 특징(feature)을 정규화하는 배치 독립적(batch-independent) 정규화
RMSNorm"평균(mean) 빼기가 없는 LayerNorm"평균 빼기(mean subtraction) 없이 제곱평균제곱근(root mean square)으로 스케일(scale)을 정규화하는 기법
조기 종료(Early stopping)"과적합되기 전에 멈추기"검증 손실(validation loss)이 더 이상 좋아지지 않을 때 학습(training)을 멈추는 정규화 기법
데이터 증강(Data augmentation)"적은 데이터로 더 많은 데이터 만들기"레이블 보존(label-preserving) 변환을 적용해 유효 데이터셋 크기(effective dataset size)를 키우는 기법
일반화 격차(Generalization gap)"학습-테스트 격차(train-test gap)"학습 성능(training performance)과 테스트 성능(test performance)의 차이

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

prompt-regularization-advisor

A diagnostic prompt for choosing regularization strategies based on overfitting symptoms

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.트랜스포머(transformer)가 BatchNorm 대신 LayerNorm을 사용하는 이유는 무엇인가요?

2.RMSNorm과 LayerNorm의 핵심 차이는 무엇인가요?

3.PyTorch에서 추론(inference) 전에 model.eval()을 호출하는 것이 중요한 이유는 무엇인가요?

0/3 답변 완료