Skip to main content
Summary

딥러닝을 처음 배울 때는 모델을 만드는 일이 가장 어려워 보입니다. nn.Linear를 쌓고, 손실 함수를 고르고, 옵티마이저를 연결하고, GPU에 올리는 과정이 모두 낯설기 때문입니다. 그런데 어느 정도 모델을 만들 수 있게 되면 더 까다로운 문제가 나타납니다.

분명 코드는 실행됩니다. 에러도 나지 않습니다. 그런데 손실이 줄지 않습니다. 정확도는 동전 던지기 수준에 머뭅니다. 어떤 날은 잘 되던 코드가 오늘은 갑자기 nan을 뿜습니다. GPU는 열심히 돌고 있는데 모델은 아무것도 배우지 않는 것처럼 보입니다.

이때 필요한 능력이 모델 디버깅입니다. 딥러닝 실력은 멋진 구조를 설계하는 능력만큼이나, 망가진 학습을 차분히 고치는 능력에서 드러납니다. 모델 디버깅은 감으로 이것저것 바꾸는 일이 아닙니다. 의심할 대상을 순서대로 좁혀 가는 작업입니다. 마치 탐정이 현장을 훑듯, 데이터에서 시작해 출력, 손실, 기울기, 하이퍼파라미터를 하나씩 확인해야 합니다.

이번 장에서는 학습이 되지 않거나 성능이 낮을 때 원인을 찾는 실전 점검 절차를 익힙니다.

모델 디버깅

모델이 안 배울 때 가장 먼저 버릴 습관

학습이 잘되지 않을 때 가장 흔한 반응은 모델을 키우는 것입니다. 층을 더 쌓고, 뉴런 수를 늘리고, 옵티마이저를 바꾸고, 학습률을 조금씩 만집니다. 물론 때로는 그것이 답일 수 있습니다. 하지만 디버깅의 출발점으로는 좋지 않습니다.

모델이 배우지 못하는 이유는 거창한 곳에 있지 않을 때가 많습니다. 라벨이 한 칸씩 밀려 있거나, 입력 이미지가 전부 0에 가깝거나, 출력 차원이 잘못되었거나, 손실 함수에 넣어야 할 값의 형태를 착각했을 수 있습니다. 이런 문제는 모델을 아무리 크게 만들어도 해결되지 않습니다. 오히려 복잡도가 높아져 원인을 찾기 더 어려워집니다.

좋은 디버깅은 다음 질문에서 시작합니다.

  • 데이터가 내가 생각한 모습으로 들어오고 있는가?

  • 라벨은 입력과 올바르게 대응되는가?

  • 모델 출력의 모양과 손실 함수의 기대 입력이 맞는가?

  • 손실이 실제로 계산되고, 기울기가 파라미터까지 전달되는가?

  • 옵티마이저가 그 기울기를 사용해 파라미터를 업데이트하는가?

이 질문들은 단순해 보이지만 매우 강력합니다. 딥러닝 학습은 결국 하나의 반복이기 때문입니다.

모델이 예측합니다. 틀린 정도를 손실로 계산합니다. 어느 가중치가 오차에 얼마나 영향을 줬는지 기울기로 확인합니다. 그 가중치를 조금 수정합니다. 이 과정을 반복합니다.

따라서 학습이 안 된다는 말은 이 연결고리 중 어딘가가 끊어졌다는 뜻입니다. 디버깅은 그 끊어진 지점을 찾는 일입니다.

1단계: 데이터와 라벨을 눈으로 확인하기

모델을 의심하기 전에 데이터를 먼저 의심해야 합니다. 딥러닝 모델은 데이터에서 패턴을 찾는 기계입니다. 데이터가 잘못 들어오면 모델은 엉뚱한 문제를 열심히 풀게 됩니다.

가장 먼저 배치 하나를 꺼내 봅시다.

x, y = next(iter(train_loader))

print(type(x), type(y))
print('x shape:', x.shape)
print('y shape:', y.shape)
print('x dtype:', x.dtype)
print('y dtype:', y.dtype)
print('x min/max:', x.min().item(), x.max().item())
print('y sample:', y[:10])

이 짧은 코드는 생각보다 많은 것을 알려 줍니다. 이미지 분류라면 x가 보통 [배치 크기, 채널, 높이, 너비] 형태인지 확인해야 합니다. 예를 들어 컬러 이미지는 [32, 3, 224, 224]처럼 들어오는 경우가 많습니다. MLP에 넣으려고 이미지를 펼쳤다면 [32, 784] 같은 모양이 될 수 있습니다.

텍스트 분류라면 x가 [배치 크기, 문장 길이] 형태의 정수 인덱스인지, 임베딩을 거친 뒤에는 [배치 크기, 문장 길이, 임베딩 차원]이 되는지 확인해야 합니다.

값의 범위도 중요합니다. 이미지 입력을 ToTensor()로 바꾸면 보통 픽셀 값이 0에서 1 사이가 됩니다. 정규화를 했다면 평균 근처를 중심으로 음수와 양수가 섞일 수 있습니다. 그런데 실수로 이미지 값이 0에서 255 사이 그대로 들어가거나, 반대로 모든 값이 거의 0이라면 학습이 불안정해질 수 있습니다.

라벨도 반드시 확인해야 합니다. 분류 문제에서 CrossEntropyLoss를 쓴다면 라벨은 보통 클래스 번호여야 합니다. 즉 고양이, 개, 자동차를 분류한다면 라벨은 0, 1, 2 같은 정수입니다. 라벨이 원-핫 벡터인지, 문자열인지, 범위가 클래스 수를 넘어가지는 않는지 확인해야 합니다.

가능하다면 샘플 몇 개를 실제로 그려 보는 습관을 들이는 것이 좋습니다.

import matplotlib.pyplot as plt

images, labels = next(iter(train_loader))

for i in range(6):
    img = images[i]

    if img.ndim == 3:
        img = img.permute(1, 2, 0)

    plt.subplot(2, 3, i + 1)
    plt.imshow(img.squeeze(), cmap='gray')
    plt.title(f'label={labels[i].item()}')
    plt.axis('off')

plt.show()

숫자 7 이미지 위에 라벨이 3이라고 표시된다면 모델은 처음부터 불가능한 문제를 풀고 있는 것입니다. 개 사진에 고양이 라벨이 붙어 있다면 모델이 혼란스러운 것은 당연합니다. 많은 디버깅은 이처럼 단순한 시각 확인에서 끝납니다.

2단계: 모델 출력과 손실 함수가 맞는지 확인하기

데이터가 정상이라면 다음은 모델 출력입니다. 모델의 마지막 출력은 손실 함수와 맞물려야 합니다. 이 부분은 초보자뿐 아니라 경험자도 자주 실수합니다.

예를 들어 10개의 숫자 클래스를 분류하는 모델이라면 마지막 출력은 보통 [배치 크기, 10] 형태여야 합니다. 각 행은 하나의 샘플에 대한 10개 클래스 점수입니다. 여기서 중요한 점은 CrossEntropyLoss에 넣는 값이 확률이 아니라 로짓이라는 것입니다. 로짓은 아직 softmax를 거치지 않은 원점수입니다.

model = MyModel()
criterion = torch.nn.CrossEntropyLoss()

x, y = next(iter(train_loader))
logits = model(x)

print('logits shape:', logits.shape)
print('target shape:', y.shape)

loss = criterion(logits, y)
print('loss:', loss.item())

CrossEntropyLoss는 내부에서 softmax에 해당하는 계산을 함께 처리합니다. 따라서 모델 마지막에 Softmax를 붙이고 다시 CrossEntropyLoss에 넣으면 학습이 불안정해질 수 있습니다. 겉보기에는 코드가 돌아가지만, 손실 함수가 기대한 입력과 실제 입력이 어긋납니다.

대표적인 조합을 정리하면 다음과 같습니다.

문제 유형모델 출력라벨 형태자주 쓰는 손실 함수
다중 클래스 분류[B, C] 로짓[B] 정수 클래스CrossEntropyLoss
이진 분류[B] 또는 [B, 1] 로짓0 또는 1 실수BCEWithLogitsLoss
다중 라벨 분류[B, C] 로짓[B, C]의 0/1BCEWithLogitsLoss
회귀[B, D] 실수값[B, D] 실수값MSELoss, L1Loss

여기서 B는 배치 크기, C는 클래스 수입니다.

출력 차원이 하나만 어긋나도 모델은 엉뚱한 방식으로 학습합니다. 예를 들어 10개 클래스를 분류해야 하는데 마지막 층이 nn.Linear(hidden_dim, 1)이라면 모델은 애초에 10개 중 하나를 고를 수 없습니다. 반대로 이진 분류인데 출력이 [B, 2]인지 [B, 1]인지에 따라 손실 함수와 라벨 준비 방식이 달라집니다.

좋은 습관은 첫 배치에서 바로 출력과 손실을 계산해 보는 것입니다. 전체 학습 루프를 돌리기 전에, 배치 하나가 모델을 통과하고 손실까지 문제없이 계산되는지 확인합니다.

3단계: 작은 데이터셋에 일부러 과적합시키기

데이터도 정상이고 출력과 손실도 맞아 보이는데 여전히 학습이 되지 않는다면, 아주 작은 데이터셋에 일부러 과적합시켜 봅니다. 이것은 딥러닝 디버깅에서 매우 강력한 전략입니다.

과적합은 앞에서 피해야 할 문제로 배웠습니다. 하지만 디버깅 단계에서는 오히려 과적합이 필요합니다. 모델이 32개 또는 128개 샘플조차 외우지 못한다면, 전체 데이터셋에서 좋은 성능을 내기는 어렵습니다. 작은 데이터셋을 외우지 못한다는 것은 학습 루프, 손실 함수, 라벨, 모델 구조, 옵티마이저 중 어딘가에 문제가 있을 가능성이 높다는 뜻입니다.

from torch.utils.data import Subset, DataLoader

small_indices = list(range(64))
small_dataset = Subset(train_dataset, small_indices)
small_loader = DataLoader(small_dataset, batch_size=64, shuffle=True)

이제 정규화 효과를 주는 요소를 잠시 줄이고 학습해 봅니다. 드롭아웃을 꺼도 좋고, 가중치 감쇠를 0으로 두어도 좋습니다. 목표는 일반화가 아니라 모델이 학습 가능한지 확인하는 것입니다.

model = MyModel().to(device)
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

for epoch in range(200):
    model.train()

    x, y = next(iter(small_loader))
    x = x.to(device)
    y = y.to(device)

    optimizer.zero_grad()
    logits = model(x)
    loss = criterion(logits, y)
    loss.backward()
    optimizer.step()

    pred = logits.argmax(dim=1)
    acc = (pred == y).float().mean().item()

    if epoch % 20 == 0:
        print(epoch, 'loss:', loss.item(), 'acc:', acc)

작은 데이터셋에서는 훈련 정확도가 거의 100%에 가까워지는 것이 자연스럽습니다. 물론 문제 난이도와 모델 크기에 따라 다르지만, 아주 작은 샘플을 반복해서 보여 주는데도 손실이 전혀 줄지 않는다면 모델이 데이터를 제대로 보고 있지 못한 것입니다.

이 실험의 해석은 간단합니다.

  • 작은 데이터셋에도 과적합하지 못한다면 구현 버그를 의심합니다.

  • 작은 데이터셋에는 과적합하지만 전체 검증 성능이 낮다면 일반화 문제를 의심합니다.

  • 훈련 손실은 줄지만 검증 손실이 증가한다면 과적합과 데이터 분포 차이를 의심합니다.

이 방법은 MLP, CNN, RNN, Transformer 어디에나 통합니다. 모델이 복잡해질수록 작은 데이터셋 과적합 테스트는 더 중요해집니다. 거대한 구조를 한 번에 믿지 말고, 작은 문제를 풀 수 있는지부터 확인해야 합니다.

4단계: 학습률이 너무 크거나 작은지 보기

학습률은 모델이 기울기를 따라 움직이는 보폭입니다. 너무 작으면 손실이 거의 줄지 않습니다. 너무 크면 손실이 심하게 흔들리거나 폭주합니다. 이 문제는 앞에서 최적화 알고리즘을 배울 때 다루었지만, 디버깅에서는 훨씬 현실적인 모습으로 나타납니다.

손실 로그를 보면 대략적인 신호를 읽을 수 있습니다. 손실이 처음부터 끝까지 거의 변하지 않는다면 학습률이 너무 작거나, 기울기가 제대로 흐르지 않거나, 데이터와 라벨에 문제가 있을 수 있습니다. 손실이 오르락내리락 크게 흔들리다가 nan이 된다면 학습률이 너무 클 가능성이 있습니다.

처음에는 여러 학습률을 짧게 시험해 보는 것이 좋습니다.

for lr in [1e-1, 1e-2, 1e-3, 1e-4]:
    model = MyModel().to(device)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    criterion = torch.nn.CrossEntropyLoss()

    print('learning rate:', lr)

    for step, (x, y) in enumerate(train_loader):
        if step == 20:
            break

        x = x.to(device)
        y = y.to(device)

        optimizer.zero_grad()
        logits = model(x)
        loss = criterion(logits, y)
        loss.backward()
        optimizer.step()

        print(step, loss.item())

이 코드는 완전한 튜닝이 아닙니다. 그저 손실이 줄어들 수 있는 구간이 있는지 빠르게 감을 잡는 용도입니다. 학습률 후보 중 하나에서 손실이 눈에 띄게 내려간다면 모델과 데이터가 어느 정도 연결되어 있다는 신호입니다. 어떤 학습률에서도 전혀 변화가 없다면 학습률보다 더 근본적인 문제를 찾아야 합니다.

또 하나 확인할 것은 학습 루프의 순서입니다. 다음 네 줄은 딥러닝 학습의 기본 박자입니다.

optimizer.zero_grad()
logits = model(x)
loss = criterion(logits, y)
loss.backward()
optimizer.step()

zero_grad()를 빼먹으면 이전 배치의 기울기가 계속 누적됩니다. backward()를 호출하지 않으면 기울기가 계산되지 않습니다. step()을 호출하지 않으면 파라미터가 업데이트되지 않습니다. 순서가 뒤섞여도 문제가 생깁니다. 너무 기본적이라 오히려 놓치기 쉽습니다.

5단계: 기울기가 0인지, 폭주하는지 로그로 확인하기

역전파는 손실에서 출발해 각 파라미터로 기울기를 전달하는 과정입니다. 모델이 학습하려면 파라미터의 grad에 의미 있는 값이 들어 있어야 합니다. 기울기가 None이라면 그 파라미터가 손실 계산에 연결되지 않았을 수 있습니다. 기울기가 거의 0이라면 업데이트가 일어나지 않습니다. 기울기가 너무 크면 학습이 폭주할 수 있습니다.

기울기 상태를 확인하는 간단한 함수를 만들어 봅시다.

def print_grad_stats(model):
    for name, param in model.named_parameters():
        if param.grad is None:
            print(name, 'grad: None')
            continue

        grad = param.grad.detach()
        print(
            name,
            'mean:', grad.mean().item(),
            'std:', grad.std().item(),
            'norm:', grad.norm().item()
        )

학습 루프 안에서 loss.backward() 다음에 호출하면 됩니다.

optimizer.zero_grad()
logits = model(x)
loss = criterion(logits, y)
loss.backward()

print_grad_stats(model)

optimizer.step()

만약 특정 층의 기울기가 계속 None이라면 그 층이 실제 계산에 사용되고 있는지 확인해야 합니다. forward() 안에서 정의한 층을 지나지 않고 다른 경로로 출력하고 있을 수 있습니다. 또는 중간에 .detach()를 사용해 계산 그래프를 끊었을 수도 있습니다.

기울기가 지나치게 작다면 활성화 함수가 포화되었거나, 네트워크가 너무 깊거나, 초기화가 좋지 않을 수 있습니다. 예를 들어 sigmoid는 입력이 매우 크거나 매우 작으면 기울기가 거의 0에 가까워질 수 있습니다. 이런 경우 ReLU 계열 활성화 함수, 배치 정규화, 적절한 초기화가 도움이 될 수 있습니다.

반대로 기울기 norm이 갑자기 매우 커지고 손실이 nan이 된다면 기울기 폭주를 의심해야 합니다. 특히 순서 데이터를 다루는 RNN에서는 이런 문제가 자주 등장합니다. 기울기 폭주를 완화하는 대표적인 방법은 gradient clipping입니다.

loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()

이 코드는 기울기의 전체 크기가 일정 수준을 넘지 않도록 잘라 줍니다. 기울기의 방향을 완전히 버리는 것이 아니라, 너무 큰 보폭을 제한하는 장치에 가깝습니다.

6단계: shape 오류는 첫 배치에서 잡는다

딥러닝 코드에서 shape는 지도와 같습니다. 입력이 어떤 모양으로 들어와서, 각 층을 지나며 어떤 모양으로 바뀌고, 마지막에 어떤 출력이 되는지 알고 있어야 합니다. shape를 모르면 모델 내부에서 무슨 일이 일어나는지 감으로 추측할 수밖에 없습니다.

가장 단순한 방법은 forward() 안에 잠시 print()를 넣는 것입니다.

class DebugMLP(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = torch.nn.Flatten()
        self.fc1 = torch.nn.Linear(28 * 28, 128)
        self.fc2 = torch.nn.Linear(128, 10)

    def forward(self, x):
        print('input:', x.shape)
        x = self.flatten(x)
        print('after flatten:', x.shape)
        x = torch.relu(self.fc1(x))
        print('after fc1:', x.shape)
        x = self.fc2(x)
        print('output:', x.shape)
        return x

이 방법은 세련되지는 않지만 매우 효과적입니다. 특히 CNN에서 합성곱과 풀링을 여러 번 거친 뒤 Linear 층으로 넘길 때 차원이 맞지 않는 오류가 자주 발생합니다. 이때 중간 shape를 출력하면 어디서 예상과 달라졌는지 바로 알 수 있습니다.

shape 오류를 줄이려면 각 모델 유형의 기본 입력 형태를 기억해 두면 좋습니다.

  • MLP는 보통 [B, 특징 수] 형태를 기대합니다.

  • CNN은 보통 [B, C, H, W] 형태를 기대합니다.

  • RNN은 설정에 따라 [B, T, E] 또는 [T, B, E] 형태를 기대합니다.

  • 분류 모델의 최종 출력은 보통 [B, 클래스 수] 형태입니다.

여기서 T는 순서 길이, E는 임베딩 차원입니다. 앞으로 Transformer를 다룰 때도 shape 감각은 계속 중요합니다. Transformer에서는 토큰 임베딩, Attention head, 시퀀스 길이, 배치 차원이 함께 움직입니다. shape를 놓치면 Attention 계산이 순식간에 미로가 됩니다.

assert를 활용하는 것도 좋은 습관입니다.

def forward(self, x):
    assert x.ndim == 4, f'expected 4D input, got {x.shape}'
    assert x.shape[1] == 3, f'expected 3 channels, got {x.shape[1]}'

    x = self.features(x)
    x = self.classifier(x)
    return x

assert는 문제가 생겼을 때 더 빨리, 더 명확하게 실패하게 만들어 줍니다. 좋은 디버깅 코드는 오류를 숨기지 않습니다. 오히려 오류가 발생한 지점을 정확히 드러냅니다.

7단계: train 모드와 eval 모드를 구분하기

PyTorch 모델에는 train() 모드와 eval() 모드가 있습니다. 이 둘은 단순한 이름표가 아닙니다. 드롭아웃과 배치 정규화처럼 학습 중과 평가 중에 다르게 동작해야 하는 층이 이 모드에 따라 행동을 바꿉니다.

훈련할 때는 다음처럼 설정합니다.

model.train()

검증이나 테스트를 할 때는 다음처럼 설정합니다.

model.eval()

그리고 평가 중에는 기울기를 계산할 필요가 없으므로 torch.no_grad()를 함께 사용합니다.

model.eval()

val_loss = 0.0
correct = 0
count = 0

with torch.no_grad():
    for x, y in val_loader:
        x = x.to(device)
        y = y.to(device)

        logits = model(x)
        loss = criterion(logits, y)

        val_loss += loss.item() * x.size(0)
        pred = logits.argmax(dim=1)
        correct += (pred == y).sum().item()
        count += x.size(0)

val_loss /= count
val_acc = correct / count

만약 검증할 때도 model.train() 상태라면 드롭아웃이 계속 무작위로 뉴런을 끄고, 배치 정규화가 현재 배치 통계를 사용합니다. 그러면 평가 결과가 불안정해질 수 있습니다. 반대로 학습할 때 실수로 model.eval() 상태라면 드롭아웃이 꺼지고 배치 정규화가 학습용 통계를 업데이트하지 않을 수 있습니다.

이 문제도 코드가 멈추지 않습니다. 그래서 더 위험합니다. 에러가 나는 버그보다, 조용히 성능을 망치는 버그가 더 찾기 어렵습니다.

8단계: 재현성을 위해 시드와 설정을 고정하기

디버깅에서 가장 괴로운 상황은 같은 코드를 돌렸는데 결과가 매번 크게 달라지는 경우입니다. 딥러닝에는 무작위성이 많습니다. 파라미터 초기화, 데이터 셔플, 드롭아웃, GPU 연산 등이 결과에 영향을 줄 수 있습니다.

따라서 문제를 추적할 때는 가능한 한 실험을 재현 가능하게 만들어야 합니다.

import random
import numpy as np
import torch

seed = 42

random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)

torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

이 설정은 무작위성을 줄여 줍니다. 다만 GPU 연산에서는 완벽한 재현이 항상 보장되지는 않을 수 있고, 결정론적 설정 때문에 속도가 느려질 수도 있습니다. 그래도 디버깅 단계에서는 속도보다 재현성이 더 중요할 때가 많습니다.

실험 설정도 함께 저장해야 합니다. 학습률, 배치 크기, 옵티마이저, 모델 구조, 드롭아웃 비율, 데이터 전처리 방식이 기록되어 있어야 나중에 결과를 비교할 수 있습니다.

config = {
    'seed': 42,
    'model': 'simple_cnn',
    'batch_size': 64,
    'lr': 1e-3,
    'optimizer': 'adam',
    'epochs': 20,
    'dropout': 0.2,
    'weight_decay': 1e-4,
}

간단하게는 JSON 파일로 저장할 수 있습니다.

import json

with open('config.json', 'w') as f:
    json.dump(config, f, indent=2)

결과도 함께 저장해 두면 좋습니다.

history = {
    'train_loss': [],
    'train_acc': [],
    'val_loss': [],
    'val_acc': [],
}

하이퍼파라미터 튜닝을 할 때 여러 실험을 비교하려면 기록이 생명입니다. 기억에 의존하면 안 됩니다. 어제의 '잘 됐던 설정'은 오늘 생각보다 쉽게 왜곡됩니다.

실전 디버깅 체크리스트

학습이 잘되지 않을 때는 다음 순서로 확인해 봅시다. 중요한 점은 한 번에 하나씩 바꾸는 것입니다. 여러 요소를 동시에 바꾸면 무엇이 원인이었는지 알 수 없습니다.

  • 배치 하나를 꺼내 입력 shape, 라벨 shape, dtype, 값의 범위를 출력한다.

  • 이미지나 텍스트 샘플을 직접 확인해 라벨이 맞는지 본다.

  • 모델 출력 shape가 손실 함수가 기대하는 형태와 일치하는지 확인한다.

  • 배치 하나로 forward와 loss 계산이 정상 동작하는지 확인한다.

  • 작은 데이터셋에 일부러 과적합시켜 모델이 학습 가능한지 검증한다.

  • 학습률 후보를 몇 개 시험해 손실이 내려가는 구간이 있는지 본다.

  • loss.backward() 이후 각 파라미터의 기울기 norm을 출력한다.

  • optimizer.zero_grad(), loss.backward(), optimizer.step() 순서가 올바른지 확인한다.

  • model.train()과 model.eval()을 훈련과 평가에 맞게 사용한다.

  • 시드와 실험 설정을 고정하고, 결과를 로그로 남긴다.

이 체크리스트는 단순하지만, 실제 현장에서 대부분의 문제를 빠르게 좁혀 줍니다. 처음에는 귀찮아 보일 수 있습니다. 하지만 한두 번 심각한 버그를 잡아 보면 생각이 달라집니다. 디버깅 루틴은 시간을 잡아먹는 절차가 아니라 시간을 아껴 주는 안전장치입니다.

로그는 모델의 일기장이다

모델은 말을 하지 않습니다. 대신 숫자로 신호를 보냅니다. 손실이 줄어드는지, 정확도가 오르는지, 기울기가 사라지는지, 파라미터가 폭주하는지, GPU 메모리가 갑자기 늘어나는지. 이 모든 숫자가 모델의 상태를 알려 주는 일기장입니다.

좋은 학습 코드는 최소한 다음 값을 꾸준히 기록합니다.

  • 훈련 손실과 검증 손실

  • 훈련 정확도와 검증 정확도

  • 학습률

  • 기울기 norm

  • 에폭별 실행 시간

  • 사용한 설정과 시드

예를 들어 다음처럼 간단한 로그를 남길 수 있습니다.

for epoch in range(num_epochs):
    train_loss, train_acc = train_one_epoch(model, train_loader)
    val_loss, val_acc = evaluate(model, val_loader)

    print(
        f'epoch={epoch} '
        f'train_loss={train_loss:.4f} '
        f'train_acc={train_acc:.4f} '
        f'val_loss={val_loss:.4f} '
        f'val_acc={val_acc:.4f}'
    )

숫자를 보는 습관이 생기면 모델의 이상 징후를 빨리 알아차릴 수 있습니다. 훈련 손실과 검증 손실이 함께 높다면 모델이 충분히 배우지 못한 것입니다. 훈련 손실은 낮은데 검증 손실이 높다면 과적합입니다. 훈련 손실이 갑자기 nan이 되면 학습률, 입력 값 범위, 손실 계산, 기울기 폭주를 의심해야 합니다.

로그가 없다면 모든 것이 느낌이 됩니다. 느낌으로도 어느 정도 갈 수 있지만, 복잡한 모델에서는 금방 한계에 부딪힙니다. 숫자를 남기는 습관은 감을 데이터로 바꿔 줍니다.

디버깅은 깊은 모델로 가는 입장권이다

지금까지 우리는 신경망의 기본 구조에서 출발해 손실 함수, 경사하강법, 역전파, PyTorch 학습 루프, MLP, CNN, RNN, 임베딩, 정규화, GPU 학습까지 지나왔습니다. 이 모든 주제는 결국 하나의 질문으로 이어집니다.

모델이 정말 배우고 있는가?

모델 디버깅은 이 질문에 답하는 기술입니다. 손실이 줄지 않을 때 무작정 구조를 바꾸는 대신, 데이터와 라벨을 확인합니다. 출력과 손실 함수의 연결을 봅니다. 작은 데이터셋을 외울 수 있는지 시험합니다. 기울기가 흐르는지 확인합니다. shape를 추적하고, 시드를 고정하고, 로그를 남깁니다.

이 습관은 앞으로 더 중요해집니다. 다음부터 우리는 순서 데이터를 다루는 모델의 한계를 더 깊이 들여다보고, Attention과 Transformer로 넘어갈 것입니다. Transformer는 강력하지만 그만큼 shape가 복잡하고, 학습 설정도 민감합니다. 토큰, 임베딩, 위치 정보, Attention score, 여러 개의 head가 얽히기 시작하면 디버깅 능력은 선택이 아니라 필수가 됩니다.

좋은 모델을 만드는 사람은 멋진 아이디어만 가진 사람이 아닙니다. 모델이 실패했을 때, 그 실패를 읽을 줄 아는 사람입니다. 딥러닝은 틀리고, 고치고, 다시 예측하는 과정입니다. 모델도 그렇게 배우고, 우리도 그렇게 배웁니다.

책 목록

처음 만나는 딥러닝: 신경망에서 Transformer와 LLM까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기