개념
평균제곱오차(Mean Squared Error, MSE)
회귀(regression)의 기본 손실입니다. 예측(prediction)과 타깃(target)의 차이를 제곱하고 모든 샘플(sample)에 대해 평균합니다.
MSE = (1/n) * sum((y_pred - y_true)^2)
제곱이 중요한 이유는 큰 오차(error)를 이차적으로(quadratically) 벌주기 때문입니다. 오차 2는 오차 1보다 4배, 오차 10은 100배의 비용(cost)을 가집니다. 그래서 MSE는 이상치(outlier)에 민감합니다. 하나의 매우 틀린 예측(prediction)이 손실을 지배할 수 있습니다.
실제 숫자로 보면 더 분명합니다. 모델이 주택 가격을 예측한다고 가정합니다. 대부분의 집에서는 10,000달러 정도 틀리지만, 어떤 대저택 하나에서 200,000달러를 틀렸다면 MSE는 그 대저택 하나를 공격적으로 고치려고 합니다. 이 과정에서 나머지 99채의 성능이 나빠질 수 있습니다.
MSE의 예측(prediction)에 대한 기울기(gradient)는 다음과 같습니다.
dMSE/dy_pred = (2/n) * (y_pred - y_true)
오차에 대해 선형(linear)입니다. 큰 오차는 큰 기울기(gradient)를 받습니다. 회귀에서는 좋은 특성입니다. 큰 오차에는 큰 수정(correction)이 필요하기 때문입니다. 하지만 분류에서는 문제입니다. 확신 있게 틀린 답(confident wrong answer)은 선형이 아니라 지수적으로(exponentially) 벌주고 싶습니다.
교차 엔트로피 손실(Cross-Entropy Loss)
분류(classification)의 손실 함수입니다. 정보 이론(information theory)에 뿌리가 있으며, 예측 확률분포(predicted probability distribution)와 실제 분포(true distribution) 사이의 발산(divergence)을 측정합니다.
이진 교차 엔트로피(Binary Cross-Entropy; BCE):
BCE = -(y * log(p) + (1 - y) * log(1 - p))
y는 실제 레이블(true label, 0 또는 1), p는 예측 확률(predicted probability)입니다. -log(p)가 왜 작동하는지 보겠습니다. 실제 레이블이 1일 때 p = 0.99를 예측하면 손실은 -log(0.99) = 0.01입니다. p = 0.01을 예측하면 손실은 -log(0.01) = 4.6입니다. 이 460배 차이가 교차 엔트로피가 분류에서 작동하는 이유입니다. 확신 있게 틀린 예측은 가혹하게 벌주고, 확신 있게 맞춘 예측은 거의 벌주지 않습니다.
기울기(gradient)도 같은 이야기를 합니다.
dBCE/dp = -(y/p) + (1-y)/(1-p)
y = 1이고 p가 0에 가까우면 기울기는 -1/p이며 음의 무한대(negative infinity)에 가까워집니다. 모델은 실수를 고치라는 매우 큰 신호(signal)를 받습니다. p가 1에 가까우면 기울기는 작습니다. 이미 맞았으니 고칠 것이 거의 없습니다.
범주형 교차 엔트로피(Categorical Cross-Entropy; CCE):
다중 클래스 분류(multi-class classification)에서 원-핫 인코딩(one-hot encoded) 타깃을 사용할 때 씁니다.
CCE = -sum(y_i * log(p_i))
실제 클래스(true class)만 손실에 기여합니다. 나머지 y_i는 모두 0이기 때문입니다. 10개 클래스에서 정답 클래스(correct class)의 확률(probability)이 0.1이면 무작위 추측(random guessing)에 가깝고 손실은 -log(0.1) = 2.3입니다. 정답 클래스 확률이 0.9라면 손실은 -log(0.9) = 0.105입니다. 모델은 올바른 답에 확률 질량(probability mass)을 집중하는 방향으로 학습합니다.
분류에서 MSE가 실패하는 이유
graph TD
subgraph "분류에서의 MSE"
P1["class 1에 대해 0.5 예측<br/>MSE = 0.25"]
P2["class 1에 대해 0.9 예측<br/>MSE = 0.01"]
P3["class 1에 대해 0.1 예측<br/>MSE = 0.81"]
end
subgraph "분류에서의 교차 엔트로피"
C1["class 1에 대해 0.5 예측<br/>CE = 0.693"]
C2["class 1에 대해 0.9 예측<br/>CE = 0.105"]
C3["class 1에 대해 0.1 예측<br/>CE = 2.303"]
end
P3 -->|"MSE 기울기는<br/>포화 근처에서 평평"| Slow["느린 수정"]
C3 -->|"CE 기울기는<br/>틀린 답 근처에서 큼"| Fast["빠른 수정"]
MSE 기울기는 시그모이드(sigmoid) 포화(saturation) 때문에 예측이 0이나 1에 가까울 때 평평해집니다(flatten). 교차 엔트로피 기울기는 이를 보완합니다. -log가 시그모이드의 평평한 구간(flat region)을 상쇄해, 기울기가 가장 필요한 곳에서 강한 기울기를 제공합니다.
레이블 스무딩(Label Smoothing)
표준 원-핫 레이블(standard one-hot label)은 "이것은 100% 클래스 3이고 나머지는 0%"라고 말합니다. 강한 주장입니다. 레이블 스무딩(label smoothing)은 이를 부드럽게 합니다.
smooth_label = (1 - alpha) * one_hot + alpha / num_classes
alpha = 0.1, 클래스 10개라면 [0, 0, 1, 0, ...] 대신 [0.01, 0.01, 0.91, 0.01, ...]가 됩니다. 모델은 1.0이 아니라 0.91을 타깃(target)으로 삼습니다.
소프트맥스(softmax)로 정확히 1.0을 출력하려면 로짓(logit)을 무한대로 밀어야 합니다. 이는 과신(overconfidence)을 만들고 일반화(generalization)를 해치며 분포 변화(distribution shift)에 취약하게 만듭니다. 레이블 스무딩은 타깃을 0.9 근처로 제한해 로짓을 합리적인 범위에 둡니다. GPT와 대부분의 현대 모델(modern model)은 레이블 스무딩 또는 그에 준하는 방식을 사용합니다.
대조 손실(Contrastive Loss)
레이블(label)도 클래스(class)도 없습니다. 입력 쌍(input pair)과 질문만 있습니다. 이 둘은 비슷한가요(similar), 다른가요(different)?
SimCLR 스타일 대조 손실(SimCLR-style contrastive loss)인 NT-Xent / InfoNCE는 하나의 이미지(image)에서 두 개의 증강된 뷰(augmented view)를 만듭니다. 예를 들면 자르기(crop), 회전(rotate), 색상 변형(color jitter)을 적용합니다. 이 두 뷰는 양성 쌍(positive pair)입니다. 비슷한 임베딩을 가져야 합니다. 배치(batch)의 다른 모든 이미지는 음성 쌍(negative pair)입니다. 서로 다른 임베딩을 가져야 합니다.
L = -log(exp(sim(z_i, z_j) / tau) / sum(exp(sim(z_i, z_k) / tau)))
sim()은 코사인 유사도(cosine similarity), z_i와 z_j는 양성 쌍(positive pair), 합(sum)은 모든 음성(negative)에 대한 합입니다. tau는 온도(temperature)이며 분포의 날카로움(sharpness)을 조절합니다. 낮은 온도는 더 어려운 음성(harder negative)과 더 공격적인 분리(aggressive separation)를 의미합니다.
실제 숫자로 보면, 배치 크기(batch size)가 256이면 양성 쌍 하나당 255개의 음성이 있습니다. 온도 tau = 0.07은 SimCLR의 기본값입니다. 이 손실은 유사도(similarity)에 대한 소프트맥스(softmax)처럼 보입니다. 256개 선택지 중 양성 쌍의 유사도가 가장 높아지기를 원합니다.
트리플릿 손실(Triplet Loss)은 앵커(anchor), 양성(positive, 같은 클래스), 음성(negative, 다른 클래스) 세 입력을 받습니다.
L = max(0, d(anchor, positive) - d(anchor, negative) + margin)
마진(margin)은 보통 0.2-1.0이며, 양성과 음성 거리 사이의 최소 간격을 강제합니다. 음성이 이미 충분히 멀리 있으면 손실은 0이고 기울기도 없습니다. 업데이트도 없습니다. 그래서 학습은 효율적이지만, 앵커에 가까운 어려운 음성(hard negative)을 고르는 트리플릿 마이닝(triplet mining)이 중요합니다.
초점 손실(Focal Loss)
불균형 데이터셋(imbalanced dataset)에서 사용합니다. 표준 교차 엔트로피(standard cross-entropy)는 쉽게 맞춘 예제(easy example)도 똑같이 다룹니다. 초점 손실(focal loss)은 쉬운 예제의 가중치(weight)를 낮춥니다.
FL = -alpha * (1 - p_t)^gamma * log(p_t)
p_t는 실제 클래스(true class)의 예측 확률(predicted probability)이고 gamma는 초점(focusing)을 조절합니다. gamma = 0이면 표준 교차 엔트로피입니다. 기본값으로 자주 쓰이는 gamma = 2일 때:
- 쉬운 예제(easy example,
p_t = 0.9): 가중치 = (0.1)^2 = 0.01입니다. 사실상 무시됩니다.
- 어려운 예제(hard example,
p_t = 0.1): 가중치 = (0.9)^2 = 0.81입니다. 충분한 기울기 신호를 받습니다.
초점 손실(focal loss)은 Lin 등이 객체 탐지(object detection)를 위해 제안했습니다. 객체 탐지에서는 후보 영역(candidate region)의 99%가 배경(background), 즉 쉬운 음성(easy negative)입니다. 초점 손실이 없으면 모델은 쉬운 배경 예제에 파묻혀 객체 탐지를 배우지 못합니다. 초점 손실을 쓰면 모델 용량(capacity)을 중요하고 모호한 어려운 사례(hard, ambiguous case)에 집중시킬 수 있습니다.
손실 함수 결정 트리(Loss Function Decision Tree)
flowchart TD
Start["과제가 무엇인가요?"] --> Reg{"회귀(Regression)?"}
Start --> Cls{"분류(Classification)?"}
Start --> Emb{"임베딩 학습(Embedding learning)?"}
Reg -->|"예"| Outliers{"이상치에 민감?"}
Outliers -->|"예, 이상치를 벌줌"| MSE["MSE 사용"]
Outliers -->|"아니오, 이상치에 강건"| MAE["MAE / Huber 사용"]
Cls -->|"이진"| BCE["Binary CE 사용"]
Cls -->|"다중 클래스"| CCE["Categorical CE 사용"]
Cls -->|"불균형"| FL["Focal Loss 사용"]
CCE -->|"과신?"| LS["Label Smoothing 추가"]
Emb -->|"쌍 데이터"| CL["Contrastive Loss 사용"]
Emb -->|"트리플릿 사용 가능"| TL["Triplet Loss 사용"]
Emb -->|"대형 배치 자기지도"| NCE["InfoNCE 사용"]
손실 지형(Loss Landscape)
graph LR
subgraph "손실 표면 형태"
MSE_S["MSE<br/>부드러운 포물선<br/>단일 최솟값<br/>최적화가 쉬움"]
CE_S["Cross-Entropy<br/>틀린 답 근처에서 가파름<br/>맞는 답 근처에서 평평함<br/>필요한 곳에서 강한 기울기"]
CL_S["Contrastive<br/>많은 국소 최솟값<br/>배치 구성에 의존<br/>온도가 날카로움 제어"]
end
MSE_S -->|"가장 적합"| Reg2["회귀"]
CE_S -->|"가장 적합"| Cls2["분류"]
CL_S -->|"가장 적합"| Emb2["표현 학습"]
직접 만들기
단계 1: MSE와 기울기(gradient)
def mse(predictions, targets):
n = len(predictions)
total = 0.0
for p, t in zip(predictions, targets):
total += (p - t) ** 2
return total / n
def mse_gradient(predictions, targets):
n = len(predictions)
grads = []
for p, t in zip(predictions, targets):
grads.append(2.0 * (p - t) / n)
return grads
단계 2: 이진 교차 엔트로피(Binary Cross-Entropy)
log(0) 문제는 실제로 생깁니다. 모델이 양성 예제(positive example)에 정확히 0을 예측하면 log(0)은 음의 무한대(negative infinity)입니다. 클리핑(clipping)으로 막습니다.
def binary_cross_entropy(predictions, targets, eps=1e-15):
n = len(predictions)
total = 0.0
for p, t in zip(predictions, targets):
p_clipped = max(eps, min(1 - eps, p))
total += -(t * math.log(p_clipped) + (1 - t) * math.log(1 - p_clipped))
return total / n
def bce_gradient(predictions, targets, eps=1e-15):
grads = []
for p, t in zip(predictions, targets):
p_clipped = max(eps, min(1 - eps, p))
grads.append(-(t / p_clipped) + (1 - t) / (1 - p_clipped))
return grads
단계 3: 소프트맥스(softmax)를 포함한 범주형 교차 엔트로피(Categorical Cross-Entropy)
소프트맥스는 원시 로짓(raw logits)을 확률(probability)로 바꿉니다. 그다음 원-핫 타깃(one-hot target)에 대해 교차 엔트로피를 계산합니다.
def softmax(logits):
max_val = max(logits)
exps = [math.exp(x - max_val) for x in logits]
total = sum(exps)
return [e / total for e in exps]
def categorical_cross_entropy(logits, target_index, eps=1e-15):
probs = softmax(logits)
p = max(eps, probs[target_index])
return -math.log(p)
def cce_gradient(logits, target_index):
probs = softmax(logits)
grads = list(probs)
grads[target_index] -= 1.0
return grads
소프트맥스 + 교차 엔트로피의 기울기는 아름답게 단순화됩니다. 참(true) 클래스는 (예측 확률 - 1), 나머지는 (예측 확률)입니다. 이 조합이 함께 쓰이는 이유입니다.
단계 4: 레이블 스무딩(Label Smoothing)
def label_smoothed_cce(logits, target_index, num_classes, alpha=0.1, eps=1e-15):
probs = softmax(logits)
loss = 0.0
for i in range(num_classes):
if i == target_index:
smooth_target = 1.0 - alpha + alpha / num_classes
else:
smooth_target = alpha / num_classes
p = max(eps, probs[i])
loss += -smooth_target * math.log(p)
return loss
단계 5: 대조 손실(Contrastive Loss, 간단한 InfoNCE)
def cosine_similarity(a, b):
dot = sum(x * y for x, y in zip(a, b))
norm_a = math.sqrt(sum(x * x for x in a))
norm_b = math.sqrt(sum(x * x for x in b))
if norm_a < 1e-10 or norm_b < 1e-10:
return 0.0
return dot / (norm_a * norm_b)
def contrastive_loss(anchor, positive, negatives, temperature=0.07):
sim_pos = cosine_similarity(anchor, positive) / temperature
sim_negs = [cosine_similarity(anchor, neg) / temperature for neg in negatives]
max_sim = max(sim_pos, max(sim_negs)) if sim_negs else sim_pos
exp_pos = math.exp(sim_pos - max_sim)
exp_negs = [math.exp(s - max_sim) for s in sim_negs]
total_exp = exp_pos + sum(exp_negs)
return -math.log(max(1e-15, exp_pos / total_exp))
단계 6: 분류에서 MSE와 교차 엔트로피 비교
Lesson 04의 원형 데이터셋(circle dataset) 신경망을 같은 구조로 학습하되, 손실만 MSE와 BCE로 바꿔 비교합니다. 교차 엔트로피가 분류에서 더 빨리 수렴하는 이유를 손실 곡선(loss curve)으로 확인합니다.
import random
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 LossComparisonNetwork:
def __init__(self, loss_type="bce", hidden_size=8, lr=0.1):
random.seed(0)
self.loss_type = loss_type
self.lr = lr
self.hidden_size = hidden_size
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):
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))
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):
if self.loss_type == "mse":
d_loss = 2.0 * (self.out - target)
else:
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]
for j in range(2):
self.w1[i][j] -= self.lr * d_h * self.x[j]
self.b1[i] -= self.lr * d_h
self.b2 -= self.lr * d_out
def compute_loss(self, pred, target):
if self.loss_type == "mse":
return (pred - target) ** 2
else:
eps = 1e-15
p = max(eps, min(1 - eps, pred))
return -(target * math.log(p) + (1 - target) * math.log(1 - p))
def train(self, data, epochs=200):
losses = []
for epoch in range(epochs):
total_loss = 0.0
correct = 0
for x, y in data:
pred = self.forward(x)
self.backward(y)
total_loss += self.compute_loss(pred, y)
if (pred >= 0.5) == (y >= 0.5):
correct += 1
avg_loss = total_loss / len(data)
accuracy = correct / len(data) * 100
losses.append((avg_loss, accuracy))
if epoch % 50 == 0 or epoch == epochs - 1:
print(f" Epoch {epoch:3d}: 손실={avg_loss:.4f}, 정확도={accuracy:.1f}%")
return losses
사용해보기
PyTorch는 수치 안정성(numerical stability)이 내장된 표준 손실 함수(standard loss function)를 제공합니다.
import torch
import torch.nn.functional as F
predictions = torch.tensor([0.9, 0.1, 0.7], requires_grad=True)
targets = torch.tensor([1.0, 0.0, 1.0])
mse_loss = F.mse_loss(predictions, targets)
bce_loss = F.binary_cross_entropy(predictions, targets)
logits = torch.randn(4, 10)
labels = torch.tensor([3, 7, 1, 9])
ce_loss = F.cross_entropy(logits, labels)
ce_smooth = F.cross_entropy(logits, labels, label_smoothing=0.1)
F.cross_entropy를 사용합니다. 수동(manual) 소프트맥스 뒤에 로그(log)를 취하는 것보다 log-softmax와 음의 로그 가능도(negative log-likelihood)를 결합한 수치적으로 안정적인 구현입니다. 대조 학습(contrastive learning)은 사용자 정의 구현(custom implementation)이나 lightly, pytorch-metric-learning 같은 라이브러리(library)를 사용하는 경우가 많지만 핵심 루프(core loop)는 같습니다. 유사도(similarity)를 계산하고, 양성(positive)과 음성(negative)에 대한 소프트맥스를 만들고, 역전파(backpropagate)합니다.
산출물 만들기
이 lesson의 산출물은 다음입니다.
outputs/prompt-loss-function-selector.md: 과제(task)에 맞는 손실 함수(loss function)를 선택하는 재사용 프롬프트(prompt)
outputs/prompt-loss-debugger.md: 손실 곡선(loss curve)이 이상할 때 진단하는 프롬프트
연습문제
- (쉬움) 작은 오차(small error)에는 MSE, 큰 오차(large error)에는 MAE처럼 동작하는 후버 손실(Huber loss, smooth L1 loss)을 구현합니다.
y = sin(x) 회귀(regression)에서 이상치(outlier)가 있을 때 MSE와 후버 손실을 비교합니다.
- (중간) 이진 분류(binary classification) 학습 루프에 초점 손실(focal loss)을 추가합니다. 90% class 0, 10% class 1의 불균형 데이터셋(imbalanced dataset)을 만들고 BCE와 초점 손실을 소수 클래스 재현율(minority class recall) 기준으로 비교합니다.
- (어려움) 준-어려운 음성 마이닝(semi-hard negative mining)을 사용하는 트리플릿 손실(triplet loss)을 구현합니다. 5개 클래스의 2차원 임베딩 데이터(2D embedding data)를 생성하고 무작위 트리플릿 선택(random triplet selection)과 수렴(convergence)을 비교합니다.
- (중간) MSE와 교차 엔트로피 비교 실험에서 층별(layer-wise) 기울기 크기(gradient magnitude)를 추적합니다. 초기 에폭(early epoch)에서 교차 엔트로피가 더 큰 기울기를 만드는지 확인합니다.
- (어려움) KL 발산 손실(KL divergence loss)을 구현하고 원-핫 참 분포(one-hot true distribution)에서
KL(true || predicted)를 최소화(minimize)할 때 교차 엔트로피와 같은 기울기가 나오는지 확인합니다. 이어서 교사 모델(teacher model)의 소프트맥스 출력(softmax output)을 타깃으로 하는 소프트 타깃(soft target)을 시험합니다.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 손실 함수(Loss function) | "모델이 얼마나 틀렸는지" | 예측과 타깃을 옵티마이저(optimizer)가 최소화할 스칼라(scalar)로 매핑하는 미분 가능한 함수 |
| 평균제곱오차(MSE) | "평균 제곱 오차" | 예측과 타깃 차이의 제곱 평균. 큰 오차를 이차적(quadratic)으로 벌줌 |
| 교차 엔트로피(Cross-entropy) | "분류 손실" | -log(p)로 예측 확률분포와 참(true) 분포의 발산(divergence)을 측정 |
| 이진 교차 엔트로피(Binary cross-entropy) | "BCE" | 두 클래스용 교차 엔트로피: -(y*log(p) + (1-y)*log(1-p)) |
| 레이블 스무딩(Label smoothing) | "타깃을 부드럽게 함" | 딱딱한 0/1 타깃을 0.1/0.9 같은 부드러운 값으로 바꿔 과신(overconfidence)을 줄이는 기법 |
| 대조 손실(Contrastive loss) | "당겨 모으고 밀어내기" | 비슷한 쌍은 가깝게, 다른 쌍은 멀게 하며 표현(representation)을 학습하는 손실 |
| InfoNCE | "CLIP/SimCLR 손실" | 유사도 점수에 대한 온도 조정 교차 엔트로피(temperature-scaled cross-entropy) |
| 초점 손실(Focal loss) | "불균형 데이터 해결책" | 쉬운 예제의 가중치를 낮추고 어려운 예제에 집중하는 교차 엔트로피 변형 |
| 트리플릿 손실(Triplet loss) | "앵커-양성-음성" | 앵커(anchor)가 음성(negative)보다 양성(positive)에 마진(margin) 이상 더 가깝도록 임베딩을 학습하는 손실 |
| 온도(Temperature) | "날카로움 조절 손잡이" | 로짓이나 유사도를 나누는 스칼라. 낮을수록 분포가 날카로워짐 |
더 읽을거리