머신러닝·신경망

DL / 1번째 글

손실 함수와 정규화: 무엇을 최소화하고 무엇을 억제하는가

MSE·CrossEntropy·Focal·KL·InfoNCE가 각각 무엇을 벌하는지, 그리고 가중치 감쇠·조기 종료·데이터 증강이 그 손실 위에 무엇을 더하는지 한 편에서 정리한다.

PALDYN Team33 MIN READ

모델은 입력을 받아 출력을 내놓는 함수이고, 그 함수가 어떻게 동작할지는 안에 든 숫자들이 정한다. 이 숫자들을 파라미터라고 부른다 — 층과 층 사이의 곱셈 계수인 가중치(weight)가 그 대부분이고, 출력의 기준점을 옮기는 편향(bias)이나 정규화 층의 스케일도 파라미터다. 모델을 저장한다는 것은 결국 이 숫자들을 저장한다는 뜻이다. 학습은 데이터를 보고 그 숫자를 조금씩 고치기를 되풀이하는 절차이고, 한 번 고치는 것이 한 스텝이다.

그 갱신을 실제로 하는 쪽이 옵티마이저이고, 옵티마이저가 최소화하려는 목표값이 손실 함수(Loss Function)다. 손실은 지금 예측이 얼마나 틀렸는지를 숫자 하나로 적어 옵티마이저에게 어느 쪽으로 갈지 알려 준다. 무엇을 최소화하느냐가 모델이 배우는 것을 정하므로, 손실을 잘못 고르면 아무리 좋은 옵티마이저를 써도 원하는 결과가 나오지 않는다. 그리고 손실 옆에는 늘 하나가 더 붙는다. 정규화(Regularization) — 훈련 데이터에만 맞아 들어가는 것을 막는 장치다. 데이터는 학습에 쓰는 훈련 데이터와 학습에는 쓰지 않고 성능만 재는 검증 데이터로 갈라 두는데, 훈련 손실은 내려가는데 검증 손실이 오르기 시작하는 상태가 과적합이고 정규화가 막으려는 것이 정확히 그것이다.

둘을 한 편에서 다루는 이유가 있다. 정규화의 절반은 별도의 기법이 아니라 손실에 항을 하나 더하는 일이기 때문이다. 가중치 감쇠는 L+λ∑wi2L + \lambda \sum w_i^2 이고, RLHF의 KL 제약은 보상에서 KL을 빼는 것이며, 레이블 스무딩은 정답 분포 자체를 무디게 만든다. 최소화할 것을 적는 자리와 억제할 것을 적는 자리가 같은 식이다.

손실의 역할

손실과 지표의 분리

손실은 미분 가능해야 한다. 우리가 정말 올리고 싶은 값 — 정확도, F1, 매출 — 은 대개 계단 함수라 기울기가 0이거나 없다. 정확도는 예측이 임계값을 넘느냐 마느냐로 갈리므로, 가중치를 조금 흔들어도 값이 그대로이거나 한 번에 뛴다. 그런 값으로는 경사하강을 돌릴 수 없다 — 손실의 기울기를 보고 가중치를 그 반대쪽으로 조금 옮기기를 되풀이하는 절차라, 기울기가 곧 어느 쪽으로 얼마나 옮길지다. 기울기가 0이면 옮길 방향도 크기도 나오지 않는다.

그래서 모델이 줄이는 값과 우리가 보는 값이 다르다. Cross-Entropy를 줄이면서 F1을 보는 식이다. 이 어긋남은 이론적 결함이 아니라 매일 겪는 현상이다. 검증 손실이 꾸준히 내려가는데 정확도가 며칠째 제자리인 상황은, 모델이 이미 맞히던 샘플의 확신을 0.8에서 0.95로 올리는 중이라는 뜻이다. 손실은 그 변화를 보상하지만 임계값 0.5를 기준으로 세는 정확도는 아무 일도 없다.

반대 방향도 있다. 손실이 조금 올랐는데 지표가 좋아지기도 한다. 소수 클래스 몇 개를 새로 맞히면서 다수 클래스의 확신이 살짝 내려간 경우다. 그러므로 조기 종료나 모델 선택의 기준은 손실이 아니라 지표여야 할 때가 있다. 무엇으로 최적 체크포인트를 고를지는 손실을 고르는 것과 별개의 결정이다.

손실과 지표가 따로 움직이는 두 자리

손실과 최적해

손실을 고르는 것은 오차를 어떻게 벌할지 고르는 일이다. 기울기를 보면 분명해진다. 제곱 오차의 기울기는 오차에 비례하고(2(y^−y)2(\hat y - y)), 절댓값 오차의 기울기는 부호뿐이다(±1\pm 1). 손실은 샘플 하나하나에 계산되고, 한 스텝에 함께 넣는 샘플 묶음(배치)에서 그것을 합하거나 평균 낸 값이 그 스텝의 갱신을 정한다. 그래서 배치 안에 크게 틀린 샘플 하나가 있으면, 제곱 오차에서는 그 하나가 갱신 방향을 거의 혼자 정한다.

더 근본적인 차이는 최적해 자체가 달라진다는 것이다. 아무 특성도 안 쓰고 상수 하나만 예측하도록 두면, 제곱 오차를 최소화하는 값은 데이터의 평균이고 절댓값 오차를 최소화하는 값은 중앙값이다. 같은 데이터를 두고 손실만 바꿨는데 정답이 바뀐다. 배달 시간을 예측하는 모델에서 제곱 오차는 사고로 세 시간 걸린 몇 건에 끌려 전체 예측을 위로 밀고, 절댓값 오차는 그 몇 건을 무시하고 보통의 배달에 맞춘다. 어느 쪽이 옳은지는 손실이 아니라 서비스가 정한다.

손실 선택 기준

고를 때는 위에서부터 세 가지를 묻는다. 표에 나오는 로짓(logit)은 모델의 마지막 층이 내놓는, 확률로 바꾸기 전의 실수다 — 시그모이드나 소프트맥스를 거쳐야 0과 1 사이의 확률이 된다. 클래스가 열이면 로짓도 열 개가 한 줄로 나오는데, 그렇게 숫자를 순서대로 늘어놓은 것이 벡터다. 이미지나 문장의 뜻을 담도록 학습된 벡터는 따로 임베딩이라고 부른다.

물음 답 손실
무엇을 예측하는가 연속값 MSE · MAE · Huber
클래스 BCE · Cross-Entropy
분포 자체 KL 발산
임베딩의 배치 InfoNCE · Triplet
출력이 어떤 형태인가 실수 회귀 손실
로짓 하나 BCEWithLogitsLoss
로짓 벡터 CrossEntropyLoss
데이터가 어떻게 생겼는가 이상치가 많다 MAE · Huber
클래스가 기울었다 가중치 · Focal

회귀 손실

제곱 오차와 평균

MSE는 1n∑(y^i−yi)2\frac{1}{n}\sum(\hat y_i - y_i)^2 이다. 제곱이 하는 일은 둘이다. 부호를 없애고, 큰 오차를 불균형하게 강조한다. 오차 10인 샘플 하나가 오차 1인 샘플 100개와 같은 무게를 가진다. 이상치가 섞인 데이터에서 MSE로 학습하면 모델이 그 이상치 쪽으로 끌려가는 이유가 이것이다.

단위도 문제가 된다. 집값을 원 단위로 예측하면 MSE의 단위는 원의 제곱이라 크기를 가늠할 수 없다. 그래서 보고할 때는 제곱근을 씌운 RMSE를 쓴다 — 학습은 MSE로 하고 사람에게는 RMSE로 말하는 조합이 흔하다.

절댓값 오차와 중앙값

MAE는 1n∑∣y^i−yi∣\frac{1}{n}\sum|\hat y_i - y_i| 다. 이상치에 끌리지 않는 대신 두 가지를 감수한다. 하나는 0에서 미분이 정의되지 않는다는 것이고(구현은 그 자리에서 0을 쓴다), 다른 하나는 기울기의 크기가 늘 같다는 것이다. 정답에 아주 가까워져도 걸음 폭이 줄지 않으므로, 수렴 후반에는 한 스텝에 얼마나 크게 옮길지를 정하는 배수인 학습률을 낮춰 주지 않으면 최적점 주위를 진동한다.

Huber의 경계

Huber는 오차가 δ\delta 보다 작으면 제곱으로, 크면 선형으로 잰다. 두 손실의 좋은 쪽을 이어 붙인 것이라 흔히 절충으로 소개되지만, 실제로 정해야 하는 값은 δ\delta 하나이고 그 값의 의미는 분명하다 — 「이 크기부터는 이상치로 보겠다」는 선언이다.

그래서 δ\delta 는 데이터의 스케일에 매여 있다. 예측 대상이 01이면 δ=1.0\delta = 1.0 은 사실상 MSE와 같고, 대상이 수백만 원이면 같은 값이 사실상 MAE가 된다. 실무에서는 먼저 MSE로 한 번 돌려 잔차의 분포를 보고, 상위 510% 잔차가 시작되는 지점을 δ\delta 로 잡는다.

import torch
import torch.nn as nn
import torch.nn.functional as F

pred = torch.tensor([2.5, 0.0, 2.0, 8.0])
true = torch.tensor([3.0, -0.5, 2.0, 5.0])

mse = nn.MSELoss()(pred, true)          # 큰 오차(8→5)가 제곱으로 강조된다
mae = nn.L1Loss()(pred, true)           # 같은 오차를 선형으로 센다
huber = nn.HuberLoss(delta=1.0)(pred, true)
print(f"MSE {mse:.4f} / MAE {mae:.4f} / Huber {huber:.4f}")
손실 기울기 최적 상수 이상치
MSE 오차에 비례 평균 크게 끌린다
MAE 부호만 중앙값 거의 안 끌린다
Huber δ\delta 까지 비례, 그 위는 상수 그 사이 경계 밖은 선형

분류 손실

로그 손실

분류 손실의 뼈대는 −log⁡p-\log p 하나다. 정답 클래스에 준 확률이 pp 일 때 그 로그에 음수를 붙인 값이 손실이다. 숫자를 넣어 보면 성격이 보인다 — p=0.9p = 0.9 면 0.105, p=0.5p = 0.5 면 0.693, p=0.1p = 0.1 이면 2.303, p=0.01p = 0.01 이면 4.605다.

확신에 찬 오답의 비용이 발산한다. 정답에 0.01을 준 샘플 하나가 0.9를 준 샘플 마흔넷과 맞먹는다. 이것이 Cross-Entropy가 가진 성질이고, 라벨이 잘못 붙은 데이터가 학습을 흔드는 이유이기도 하다. 라벨은 샘플마다 사람이 붙여 둔 정답이고 손실은 늘 그 라벨과 예측을 견주어 재므로, 라벨이 틀리면 손실이 틀린 쪽을 요구한다. 모델이 맞게 예측했는데 라벨이 틀렸다면 그 샘플은 「확신에 찬 오답」으로 계산되어 가장 큰 벌을 받는다.

로짓과 수치 안정성

확률을 먼저 만들고 로그를 씌우면 수치가 무너진다. 시그모이드 출력이 0에 아주 가까울 때 log(0)은 음의 무한대가 되고, 큰 로짓에서 exp는 넘친다. 그래서 프레임워크는 시그모이드·소프트맥스와 로그를 한 연산 안에서 처리하는 손실을 따로 둔다. BCEWithLogitsLoss와 CrossEntropyLoss가 그것이다.

여기서 나오는 가장 흔한 실수가 소프트맥스를 두 번 먹이는 것이다. 모델의 마지막 층에 nn.Softmax를 붙여 두고 CrossEntropyLoss를 쓰면, 손실이 이미 확률인 값에 소프트맥스를 한 번 더 씌운다. 학습이 완전히 멈추지는 않고 느려지기만 해서 알아채기 어렵다. 손실 함수 이름에 WithLogits나 CrossEntropy가 들어가면 모델은 로짓을 그대로 내보내야 한다.

logits = torch.tensor([2.0, -1.5, 0.5, -0.3])
labels = torch.tensor([1.0,  0.0, 1.0,  0.0])

bce = nn.BCEWithLogitsLoss()(logits, labels)

# 양성:음성 = 1:10 이면 pos_weight로 균형을 잡는다
bce_balanced = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([10.0]))(logits, labels)

logits_mc = torch.randn(4, 10)             # 배치 4, 클래스 10
targets = torch.randint(0, 10, (4,))
ce = nn.CrossEntropyLoss()(logits_mc, targets)

weights = torch.ones(10)
weights[3] = 5.0                           # 3번 클래스가 희귀하다
ce_weighted = nn.CrossEntropyLoss(weight=weights)(logits_mc, targets)
ce_smooth = nn.CrossEntropyLoss(label_smoothing=0.1)(logits_mc, targets)

불균형 손잡이

손잡이가 셋인데 하는 일이 서로 다르다.

손잡이 무엇을 바꾸나 언제
pos_weight 양성 샘플의 손실 배수 이진 분류에서 양성이 드물 때
weight 클래스별 손실 배수 다중 분류에서 클래스가 기울었을 때
label_smoothing 정답 분포를 무디게 과확신을 억제하고 싶을 때

앞의 둘은 불균형 대응이고 label_smoothing은 정규화다. 정답에 1.0 대신 1−ϵ1-\epsilon 을 주고 나머지 ϵ\epsilon 을 다른 클래스에 나눠 주면, 모델이 정답 로짓을 무한히 키우려는 유인이 사라진다. 언어 모델 학습에서 기본으로 쓰는 이유가 이것이다. 토큰은 언어 모델이 글을 잘라 다루는 한 조각 — 대략 단어 하나나 그 일부 — 인데, 다음 토큰이 하나로 정해지지 않는 자리가 대부분이다. 정답 하나에만 1을 주고 나머지 전부에 0을 주는 one-hot 라벨은 그렇지 않다고 가르친다.

Focal Loss와 쉬운 샘플

객체 탐지에서는 배경 영역이 객체보다 수십 배 많다. 배경 하나하나의 손실은 작지만 개수가 압도적이라 합이 학습을 지배하고, 모델은 전부 배경이라고 답하는 쪽으로 기운다. Focal Loss는 Cross-Entropy에 (1−pt)γ(1-p_t)^\gamma 를 곱해 이미 잘 맞히는 샘플의 기여를 낮춘다.

γ=2\gamma = 2 일 때 정답 확률 0.9인 샘플의 가중치는 (1−0.9)2=0.01(1-0.9)^2 = 0.01 로 100분의 1이 되고, 0.5인 샘플은 0.25로 4분의 1이 된다. 쉬운 것을 지우는 것이 아니라 어려운 것의 상대적 목소리를 키우는 장치다. γ=0\gamma = 0 이면 원래의 Cross-Entropy로 돌아온다.

def focal_loss(logits, targets, gamma=2.0, alpha=0.25):
    ce = F.cross_entropy(logits, targets, reduction='none')
    pt = torch.exp(-ce)                    # 정답 클래스에 준 확률
    return (alpha * (1 - pt) ** gamma * ce).mean()

손실 함수 전체 지도

분포 손실

KL 발산과 VAE

앞의 손실들이 값 하나를 재는 자였다면, KL 발산(Kullback-Leibler Divergence)은 분포와 분포 사이의 거리를 잰다. 정확히는 거리가 아니라 방향에 따라 값이 달라지는 비대칭 양이고, 뜻은 「qq 로 근사했을 때 치르는 추가 비용」이다.

VAE는 이 값을 손실의 절반으로 쓴다. 재구성 손실은 「입력을 얼마나 되살렸는가」를, KL 항은 「잠재 분포가 정규분포에서 얼마나 벗어났는가」를 잰다. 앞만 쓰면 잠재 공간이 훈련 샘플마다 뿔뿔이 흩어져 새 샘플을 뽑을 수 없고, 뒤만 쓰면 입력을 무시한 채 정규분포만 흉내 낸다. 두 항의 저울이 곧 생성 모델의 성격이 되고, 그 저울에 계수를 달아 조절하는 것이 β\beta-VAE다.

def vae_loss(recon_x, x, mu, log_var):
    recon = F.binary_cross_entropy(recon_x, x, reduction='sum')
    # KL(q(z|x) ‖ N(0,1)) = -0.5 · Σ(1 + log σ² - μ² - σ²)
    kl = -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
    return recon + kl

RLHF의 KL 제약

같은 KL이 정렬 학습에서는 제약으로 쓰인다. 보상 모델의 점수만 최대화하면 정책이 보상 모델의 허점을 찾아가는 쪽으로 흘러가고(보상 해킹), 문장이 사람이 읽을 수 없는 형태로 무너진다. 그래서 목적을 r−β⋅KL(πθ∥πref)r - \beta \cdot \mathrm{KL}(\pi_\theta \Vert \pi_{\text{ref}}) 로 두어 원래 모델에서 멀어지는 것 자체에 값을 매긴다.

이 자리에서 KL은 손실이 아니라 정규화 항이다. 아래 「가중치 정규화」와 정확히 같은 꼴이며, 다만 억제하는 대상이 가중치의 크기가 아니라 정책의 이동 거리일 뿐이다.

대조 손실과 온도

CLIP과 SimCLR은 정답 라벨 없이 무엇과 무엇이 짝인지만으로 학습한다. InfoNCE는 배치 안의 짝지은 쌍을 정답으로, 나머지 조합을 오답으로 두고 Cross-Entropy를 매긴다. 그래서 배치 크기가 곧 음성 샘플의 개수이고, 대조 학습이 큰 배치를 요구하는 이유가 여기 있다.

온도 τ\tau 는 유사도를 나누는 값이다. 작을수록 소프트맥스가 뾰족해져 가장 비슷한 오답 하나에 벌이 집중되고, 클수록 여러 오답에 고르게 퍼진다. CLIP이 0.07 부근을 쓰는 것은 「가장 헷갈리는 것부터 떼어 놓겠다」는 선택이다.

def infonce_loss(image_emb, text_emb, temperature=0.07):
    image_emb = F.normalize(image_emb, dim=-1)
    text_emb = F.normalize(text_emb, dim=-1)
    logits = image_emb @ text_emb.T / temperature      # [B, B]
    labels = torch.arange(logits.size(0))              # 대각선이 정답 쌍
    return (F.cross_entropy(logits, labels) + F.cross_entropy(logits.T, labels)) / 2

InfoNCE의 유사도 행렬과 온도

가중치 정규화

가중치 감쇠

여기서부터가 정규화다. 가장 단순한 것은 손실에 파라미터의 제곱 합을 더하는 것이다.

Ltotal=L+λ∑iwi2L_{\text{total}} = L + \lambda \sum_i w_i^2

미분하면 각 가중치의 기울기에 2λwi2\lambda w_i 가 더해진다. 부호가 가중치와 같으므로 매 스텝 가중치를 원점 쪽으로 조금씩 당긴다 — 그래서 이름이 감쇠(decay)다. 데이터가 그 가중치를 계속 밀어 올리지 않는 한 값은 0으로 흘러간다.

λ\lambda 는 저울이다. 데이터가 말하는 것과 「가중치는 작아야 한다」는 우리 쪽 믿음 사이의 비율이고, 보통 10−410^{-4} 에서 10−210^{-2} 사이를 쓴다. 가중치가 작으면 입력이 조금 달라져도 출력이 크게 흔들리지 않으므로, 훈련 데이터의 잡음까지 따라 그리는 일이 줄어든다.

L1과 희소성

제곱 대신 절댓값을 더하면 성격이 달라진다. ∣w∣|w| 의 기울기는 부호뿐이라 가중치가 0에 가까워져도 당기는 힘이 그대로다. 그래서 쓸모없는 가중치를 정확히 0으로 보낸다. 반면 L2의 당기는 힘은 2λw2\lambda w 라 가중치가 작아질수록 함께 작아지고, 0에는 닿지 않는다.

희소성이 필요하면 L1, 안정적인 축소가 필요하면 L2다. 특성이 수천 개인데 실제로 쓰이는 것이 몇십 개일 것 같은 표 데이터에서는 L1이 특성 선택을 겸한다. 딥러닝의 기본값이 L2인 것은 신경망의 생김새 때문이다. 신경망은 가중치 곱셈과 비선형 함수를 층층이 쌓아 올린 모델이고, 한 층이 내놓는 값 하나는 앞 층의 수백 개 가중치가 함께 만든 결과다. 가중치가 저마다 따로 뜻을 갖는 것이 아니라 협력해서 하나의 표현을 만들므로, 개별 가중치를 0으로 끄는 것이 이득인 경우가 드물다.

감쇠 제외 파라미터

모든 파라미터를 감쇠시키면 안 된다. 편향(bias)과 정규화 층의 스케일 파라미터는 빼는 것이 관례다. 여기서 정규화 층은 지금까지 말한 과적합을 막는 정규화(Regularization)가 아니라 활성값의 분포를 고르게 만드는 정규화(Normalization) 층이다 — 배치 정규화·레이어 정규화가 그것이고, 이름만 같지 하는 일이 다르다. 정규화 층의 γ\gamma 는 정규화된 값을 다시 키우는 손잡이인데 이것을 0으로 당기면 그 층의 출력이 통째로 죽는다. 편향은 개수가 적어 과적합에 거의 기여하지 않으면서 출력의 기준점을 옮기는 역할이라 억제할 이유가 없다.

import torch.optim as optim

no_decay = ['bias', 'LayerNorm.weight', 'layernorm.weight']
groups = [
    {'params': [p for n, p in model.named_parameters()
                if not any(k in n for k in no_decay)], 'weight_decay': 1e-2},
    {'params': [p for n, p in model.named_parameters()
                if any(k in n for k in no_decay)], 'weight_decay': 0.0},
]
optimizer = optim.AdamW(groups, lr=3e-4)

Adam과 AdamW

L2 항을 손실에 넣는 것과 옵티마이저가 가중치를 직접 줄이는 것은 SGD에서는 같은 일이지만 Adam에서는 다르다. 손실에 넣으면 그 항이 기울기에 섞여 v^\sqrt{\hat v} 로 함께 나뉘고, 그러면 기울기가 큰 파라미터는 감쇠를 거의 안 받고 작은 파라미터만 짓눌린다. 같은 λ\lambda 를 걸어도 파라미터마다 실제 감쇠량이 달라진다는 뜻이다.

AdamW는 감쇠를 기울기에서 떼어 내 갱신 마지막에 따로 적용한다. 두 줄 차이지만 λ\lambda 라는 손잡이가 뜻대로 동작하느냐가 갈린다. 수치로 확인한 것은 Adam을 모멘트에서 세우기에 있다.

L1·L2 정규화와 드롭아웃 구현

손실 밖의 정규화

조기 종료

훈련을 오래 돌리면 어느 시점부터 훈련 손실은 계속 내려가는데 검증 손실이 올라간다. 그 지점이 모델이 데이터의 규칙 대신 잡음을 외우기 시작한 자리, 곧 과적합이 시작되는 자리다. 조기 종료는 검증 손실이 몇 에폭 연속 나아지지 않으면 멈추고 가장 좋았던 가중치로 되돌린다. 에폭은 훈련 데이터 전체를 한 번 도는 단위다 — 샘플이 만 개고 배치가 100개씩이면 스텝 100번이 한 에폭이다.

「몇 에폭 동안 참을 것인가」(patience)가 유일한 손잡이다. 너무 짧으면 학습률이 잠시 흔들린 구간을 과적합으로 오해하고, 너무 길면 되돌릴 체크포인트를 오래 들고 있어야 한다. 그리고 가장 좋았던 가중치를 저장해 두지 않으면 조기 종료는 반쪽이다 — 멈추기만 하면 이미 나빠진 상태로 끝난다.

best, patience, no_improve = float('inf'), 5, 0

for epoch in range(1000):
    train_epoch(model, train_loader, optimizer, criterion)
    val_loss = evaluate(model, val_loader, criterion)

    if val_loss < best:
        best, no_improve = val_loss, 0
        torch.save(model.state_dict(), 'best.pt')
    else:
        no_improve += 1
        if no_improve >= patience:
            model.load_state_dict(torch.load('best.pt'))   # 최적으로 되돌린다
            break

데이터 증강

데이터 증강은 기존 데이터를 변환해 학습 데이터를 늘린다. 하지만 실제로 하는 일은 개수를 늘리는 것이 아니라 불변성을 주입하는 것이다. 좌우를 뒤집어도 같은 라벨이라고 가르치면 「좌우가 바뀌어도 고양이는 고양이」라는 지식이 모델에 들어간다. 정규화로 분류되는 이유가 이것이다 — 모델이 가질 수 있는 함수의 범위를 우리가 아는 만큼 좁힌다.

그래서 허용되는 변환은 도메인이 정한다. 손글씨 숫자를 180도 돌리면 6이 9가 되므로 회전 증강을 함부로 쓸 수 없고, 의료 영상에서 좌우 뒤집기는 장기의 위치를 바꾼다. 텍스트에서는 동의어 교체와 역번역이, LLM 미세조정에서는 합성 데이터 생성이 같은 자리에 선다.

from torchvision import transforms

train_transform = transforms.Compose([
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomRotation(degrees=15),
    transforms.ColorJitter(brightness=0.2, contrast=0.2),
    transforms.RandomCrop(224, padding=28),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

구조 정규화

나머지 정규화는 손실을 건드리지 않고 순전파 자체를 바꾼다. 드롭아웃은 학습 중 뉴런 일부를 꺼서 매 스텝 다른 서브네트워크를 훈련하고, 배치 정규화와 레이어 정규화는 활성값의 분포를 고르게 만들어 학습을 안정시킨다. 셋 다 부수 효과로 과적합을 억제하지만 원래 목적과 동작하는 층위가 달라, 각각 드롭아웃과 배치 정규화에서 따로 다룬다.

여기서 기억할 것은 자리다. 손실에 항을 더하는 것(가중치 감쇠·레이블 스무딩·KL 제약), 학습 절차를 바꾸는 것(조기 종료), 데이터를 바꾸는 것(증강), 구조를 바꾸는 것(드롭아웃·정규화 층) — 네 층위가 있고 서로 대체재가 아니다.

과적합 vs 과소적합 vs 적절한 적합

선택 순서

손실 고르기

앞의 세 물음을 순서대로 답하면 대개 하나가 남는다. 예측 대상이 연속값이고 이상치가 있으면 Huber, 클래스이고 로짓을 내보내면 CrossEntropyLoss, 클래스가 크게 기울었으면 가중치를 먼저 걸고 그래도 배경이 지배하면 Focal이다. LLM과 생성 모델도 뼈대는 Cross-Entropy이고, 거기에 다양성·선호·안전을 다루는 항이 붙는다.

정규화 순서

정규화는 한꺼번에 켜지 않는다. 하나씩 켜고 검증 곡선이 어떻게 움직이는지 본다. 순서는 대개 데이터 증강 → 가중치 감쇠 → 조기 종료 → 드롭아웃이다. 앞의 둘이 값이 싸고 부작용이 적으며, 드롭아웃은 학습을 느리게 만들고 정규화 층과 함께 쓰면 서로 간섭하는 경우가 있어 마지막에 놓는다.

과소적합은 훈련 손실조차 잘 안 내려가는 상태다 — 잡음을 외우기는커녕 규칙도 아직 못 잡았다는 뜻이라 과적합과 정반대다. 과소적합인데 정규화를 더 거는 것이 가장 흔한 실수다. 훈련 손실 자체가 안 내려가면 그건 정규화로 풀 문제가 아니다 — 모델을 키우거나 특성을 늘려야 한다. 훈련 손실은 잘 내려가는데 검증 손실만 벌어질 때가 정규화의 자리다.

다음 글에서는 한 걸음 뒤로 물러나, 지금까지 당연하게 「정답 라벨이 있다」고 두었던 전제를 들여다본다. 라벨이 있느냐 없느냐에 따라 쓸 수 있는 손실이 달라지고 데이터를 모으는 방식도 갈리는데, 그 갈림이 주제다.


읽어주셔서 감사합니다. 😊

LATEST

머신러닝·신경망의 최신 글

머신러닝·신경망2026.05.08

문맥적 임베딩: ELMo부터 BERT까지

정적 임베딩의 다의어 문제를 해결하는 문맥적 임베딩의 원리, ELMo의 양방향 LSTM 레이어 표현, BERT의 트랜스포머 기반 서브워드 임베딩 추출법을 수식과 코드로 완전히 해설한다.

12 MIN
머신러닝·신경망2026.05.08

FastText: 부분 단어로 OOV를 정복하다

FastText가 문자 n-gram 기반의 부분 단어 모델로 OOV 문제를 해결하는 방법, 한국어 형태론에서의 강점, 실전 학습과 추론 코드를 완전히 해설한다.

11 MIN
머신러닝·신경망2026.05.08

GloVe: 전역 공기 통계로 단어 벡터를 만들다

GloVe가 공기 행렬의 전역 통계와 국소 문맥 창의 장점을 결합하는 방법, 목적 함수의 수학적 의미, 사전 학습 벡터 활용법을 깊이 있게 다룬다.

11 MIN