Skip to main content
Summary

이전 장에서 우리는 Dataset과 DataLoader를 배웠습니다. 데이터셋은 데이터를 꺼내는 방법을 알고 있고, 데이터로더는 그 데이터를 배치 단위로 차곡차곡 실어 나릅니다. 이제 남은 일은 분명합니다. 배치가 도착할 때마다 모델에게 보여 주고, 예측하게 하고, 얼마나 틀렸는지 계산하고, 틀린 이유를 따라가며 가중치를 고치는 것입니다.

이 반복 구조를 학습 루프라고 부릅니다.

학습 루프는 딥러닝의 심장박동과 같습니다. 한 번 뛸 때마다 모델은 조금 예측하고, 조금 틀리고, 조금 배웁니다. 처음에는 엉뚱한 답을 내놓던 모델도 이 박동을 수천 번, 수만 번 반복하면서 점점 데이터에 맞는 방향으로 변합니다.

학습 루프 만들기

학습 루프는 딥러닝의 조립 설명서다

딥러닝을 처음 배울 때 가장 헷갈리는 부분은 개념들이 따로따로 흩어져 보인다는 점입니다. 텐서, 모델, 손실 함수, 역전파, 옵티마이저, 데이터로더를 각각 배울 때는 알 것 같다가도, 막상 전체 코드를 쓰려 하면 손이 멈춥니다.

학습 루프를 직접 작성하면 이 조각들이 어디에 들어가는지 한눈에 보입니다.

학습 루프 안에서는 다음 일이 반복됩니다.

  • 데이터로더에서 입력과 정답 배치를 꺼낸다.

  • 모델이 입력을 받아 예측값을 만든다.

  • 예측값과 정답을 비교해 손실을 계산한다.

  • 역전파로 각 파라미터의 기울기를 계산한다.

  • 옵티마이저가 파라미터를 조금 수정한다.

이 흐름은 앞에서 배운 핵심 프레임과 정확히 맞물립니다.

모델이 예측한다. 틀린 정도를 손실로 계산한다. 어느 가중치가 오차에 얼마나 영향을 줬는지 본다. 조금씩 수정한다. 그리고 반복한다.

딥러닝 코드가 길어 보여도 중심에는 늘 이 짧은 문장이 있습니다.

준비물: 모델, 손실 함수, 옵티마이저, 데이터로더

학습 루프를 만들기 전에 네 가지가 준비되어 있어야 합니다. 첫째는 학습할 model입니다. 둘째는 예측이 얼마나 틀렸는지 계산할 loss_fn입니다. 셋째는 파라미터를 업데이트할 optimizer입니다. 넷째는 배치를 공급할 train_loader와 val_loader입니다.

예를 들어 분류 문제를 푼다고 해 봅시다. 모델은 이미 만들어져 있고, 이전 장에서 데이터로더도 준비했다고 가정하겠습니다.

import torch
import torch.nn as nn

# GPU가 있으면 GPU를, 없으면 CPU를 사용한다.
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

model = model.to(device)

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

여기서 device는 계산을 어디서 할지 정합니다. CPU에서 할 수도 있고, CUDA를 지원하는 GPU가 있다면 GPU에서 할 수도 있습니다. 중요한 점은 모델과 데이터가 같은 장치에 있어야 한다는 것입니다. 모델은 GPU에 있는데 데이터가 CPU에 있으면 PyTorch는 계산을 진행할 수 없습니다.

CrossEntropyLoss는 다중 분류에서 자주 쓰는 손실 함수입니다. 손글씨 숫자 분류처럼 정답이 0부터 9까지 여러 클래스 중 하나일 때 사용할 수 있습니다. 모델은 각 클래스에 대한 점수를 내놓고, 손실 함수는 정답 클래스의 점수가 충분히 높은지 확인합니다.

옵티마이저는 Adam을 사용했습니다. 앞에서 배웠듯이 Adam은 기울기의 방향뿐 아니라 최근 움직임과 기울기 크기까지 참고해서 파라미터를 업데이트하는 방법입니다. 물론 가장 단순한 SGD를 써도 학습 루프의 구조는 같습니다.

optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

옵티마이저를 무엇으로 바꾸든 학습 루프의 기본 리듬은 변하지 않습니다. zero_grad(), backward(), step()이 제자리를 지키면 됩니다.

model.train과 model.eval은 왜 필요할까

학습을 시작하기 전에는 보통 다음 코드를 호출합니다.

model.train()

검증이나 테스트를 할 때는 다음 코드를 호출합니다.

model.eval()

처음 보면 이상하게 느껴질 수 있습니다. 모델은 같은 모델인데 왜 모드를 바꿔야 할까요? 이유는 일부 층이 학습 중과 평가 중에 다르게 동작하기 때문입니다.

대표적인 예가 드롭아웃과 배치 정규화입니다. 드롭아웃은 학습 중에 일부 뉴런을 일부러 꺼서 모델이 특정 뉴런에 지나치게 의존하지 않도록 만듭니다. 그런데 평가할 때도 뉴런을 랜덤하게 꺼 버리면 같은 입력에 대해 결과가 흔들립니다. 검증이나 테스트에서는 모든 뉴런을 안정적으로 사용해야 합니다.

배치 정규화도 학습 중과 평가 중의 동작이 다릅니다. 학습 중에는 현재 배치의 통계를 사용하지만, 평가 중에는 학습 과정에서 누적한 통계를 사용합니다. 그래서 모드를 명확히 바꿔 주어야 합니다.

중요한 점은 model.train()이 곧바로 학습을 실행하는 함수는 아니라는 것입니다. 이 함수는 모델을 학습 모드로 바꿀 뿐입니다. 실제 학습은 우리가 작성하는 루프 안에서 loss.backward()와 optimizer.step()을 호출할 때 일어납니다.

마찬가지로 model.eval()도 평가를 자동으로 수행하지 않습니다. 모델을 평가 모드로 바꿀 뿐입니다. 실제 검증은 우리가 검증 데이터를 넣고 손실과 정확도를 계산해야 합니다.

한 배치가 지나가는 길

먼저 학습 루프의 가장 작은 단위인 한 배치만 살펴보겠습니다.

model.train()

for x_batch, y_batch in train_loader:
    x_batch = x_batch.to(device)
    y_batch = y_batch.to(device)

    optimizer.zero_grad()

    logits = model(x_batch)
    loss = loss_fn(logits, y_batch)

    loss.backward()
    optimizer.step()

이 짧은 코드가 딥러닝 학습의 핵심입니다. 한 줄씩 천천히 뜯어보겠습니다.

먼저 데이터로더에서 x_batch와 y_batch를 꺼냅니다. x_batch는 입력이고 y_batch는 정답입니다. 이미지 분류라면 x_batch에는 여러 장의 이미지가 들어 있고, y_batch에는 각 이미지의 정답 번호가 들어 있습니다.

x_batch = x_batch.to(device)
y_batch = y_batch.to(device)

이 코드는 데이터를 모델과 같은 장치로 옮깁니다. 모델이 GPU에 있다면 데이터도 GPU로 보내야 합니다.

다음 줄이 매우 중요합니다.

optimizer.zero_grad()

PyTorch에서 기울기는 기본적으로 누적됩니다. 즉, 이전 배치에서 계산한 기울기가 남아 있을 수 있습니다. 그래서 새 배치를 학습하기 전에 기존 기울기를 0으로 비워야 합니다. 비유하자면 이전 시험의 오답 노트를 지우지 않은 채 다음 시험을 채점하면 점수가 뒤섞이는 것과 같습니다.

그다음 모델이 예측합니다.

logits = model(x_batch)

여기서 logits는 모델이 출력한 원시 점수입니다. 분류 문제에서는 보통 각 클래스에 대한 점수가 들어 있습니다. 예를 들어 숫자 0부터 9까지 분류하는 모델이라면 하나의 이미지에 대해 10개의 점수를 출력합니다. 이 점수 중 가장 큰 값을 가진 클래스가 모델의 예측이 됩니다.

이제 예측과 정답을 비교해 손실을 계산합니다.

loss = loss_fn(logits, y_batch)

손실은 모델이 얼마나 틀렸는지를 나타내는 숫자입니다. 손실이 크면 많이 틀린 것이고, 손실이 작으면 비교적 잘 맞힌 것입니다.

그다음 역전파가 등장합니다.

loss.backward()

이 한 줄이 앞에서 배운 자동 미분과 역전파를 실제로 실행합니다. PyTorch는 순전파 동안 만들어진 계산 그래프를 따라가며 손실이 각 파라미터에 대해 얼마나 민감한지 계산합니다. 다시 말해 "이 가중치를 조금 바꾸면 손실이 얼마나 변하는가?"를 구합니다.

마지막으로 옵티마이저가 파라미터를 업데이트합니다.

optimizer.step()

이때 모델의 가중치와 편향이 실제로 바뀝니다. loss.backward()가 방향을 계산한다면, optimizer.step()은 그 방향으로 한 걸음 움직입니다.

순서를 다시 정리하면 다음과 같습니다.

optimizer.zero_grad()  # 이전 기울기 비우기
logits = model(x_batch)  # 순전파: 예측하기
loss = loss_fn(logits, y_batch)  # 손실 계산하기
loss.backward()  # 역전파: 기울기 계산하기
optimizer.step()  # 파라미터 업데이트하기

이 순서는 반드시 몸에 익혀야 합니다. 특히 zero_grad(), backward(), step()의 관계를 잘 기억해야 합니다. 기울기를 비우고, 새 기울기를 계산하고, 그 기울기로 파라미터를 수정합니다.

순전파는 예측을 만드는 길이다

순전파는 입력이 모델의 층을 차례로 통과해 출력으로 바뀌는 과정입니다. 영어로는 forward pass라고 부릅니다. 이름 그대로 앞으로 지나가는 계산입니다.

logits = model(x_batch)

이 한 줄 안에서는 많은 일이 일어납니다. 입력 텐서가 첫 번째 층을 지나고, 가중치와 곱해지고, 편향이 더해지고, 활성화 함수를 통과합니다. 그 결과가 다음 층으로 넘어가고, 이런 흐름이 마지막 층까지 이어집니다.

앞 장들에서 배운 내용을 떠올려 보면 순전파는 표현이 변해 가는 과정입니다. 원래 입력은 픽셀값이나 숫자 묶음일 뿐입니다. 그런데 층을 통과하면서 모델이 문제를 풀기 좋은 형태의 표현으로 바꿉니다. 마지막에는 그 표현을 이용해 클래스 점수나 예측값을 만듭니다.

순전파는 아직 학습이 아닙니다. 순전파는 예측을 만드는 과정입니다. 학습이 되려면 예측이 얼마나 틀렸는지 알아야 하고, 그 틀림을 바탕으로 파라미터를 고쳐야 합니다. 그래서 순전파 다음에는 손실 계산과 역전파가 이어집니다.

손실은 모델에게 주는 피드백이다

손실은 모델의 현재 상태에 대한 점수표입니다. 모델이 잘하고 있으면 손실은 작아지고, 엉뚱한 예측을 하면 손실은 커집니다.

분류 문제에서는 보통 다음처럼 손실을 계산합니다.

loss = loss_fn(logits, y_batch)

여기서 주의할 점이 있습니다. CrossEntropyLoss를 사용할 때는 모델 출력에 직접 softmax를 적용하지 않는 경우가 많습니다. PyTorch의 CrossEntropyLoss는 내부적으로 분류 점수를 확률처럼 다루는 계산을 포함합니다. 그래서 모델은 각 클래스의 원시 점수인 logits를 내놓으면 됩니다.

정확도도 함께 계산할 수 있습니다.

preds = logits.argmax(dim=1)
correct = (preds == y_batch).sum().item()

argmax(dim=1)은 각 데이터마다 가장 큰 점수를 가진 클래스 번호를 고릅니다. 예를 들어 어떤 이미지에 대해 클래스별 점수가 [0.1, 2.3, -0.5]라면 가장 큰 값은 두 번째 위치에 있으므로 예측 클래스는 1입니다.

손실과 정확도는 서로 다른 정보를 줍니다. 정확도는 맞혔는지 틀렸는지를 봅니다. 손실은 얼마나 자신 있게 맞혔는지, 혹은 얼마나 자신 있게 틀렸는지도 반영합니다. 그래서 학습 과정을 볼 때는 둘을 함께 기록하는 것이 좋습니다.

학습용 함수 만들기

이제 한 에폭 동안 학습을 수행하는 함수를 만들어 보겠습니다. 에폭은 전체 학습 데이터를 한 번 모두 보는 단위입니다. train_loader가 배치를 하나씩 제공하면, 함수는 모든 배치에 대해 예측, 손실 계산, 역전파, 업데이트를 반복합니다.

def train_one_epoch(model, train_loader, loss_fn, optimizer, device):
    model.train()

    total_loss = 0.0
    correct = 0
    total = 0

    for x_batch, y_batch in train_loader:
        x_batch = x_batch.to(device)
        y_batch = y_batch.to(device)

        optimizer.zero_grad()

        logits = model(x_batch)
        loss = loss_fn(logits, y_batch)

        loss.backward()
        optimizer.step()

        batch_size = y_batch.size(0)
        total_loss += loss.item() * batch_size

        preds = logits.argmax(dim=1)
        correct += (preds == y_batch).sum().item()
        total += batch_size

    avg_loss = total_loss / total
    avg_acc = correct / total

    return avg_loss, avg_acc

이 함수는 평균 손실과 평균 정확도를 반환합니다. 여기서 loss.item()은 텐서 형태의 손실 값을 파이썬 숫자로 꺼내는 코드입니다. 손실을 기록할 때 자주 사용합니다.

한 가지 눈여겨볼 부분이 있습니다.

total_loss += loss.item() * batch_size

손실 함수는 보통 한 배치의 평균 손실을 반환합니다. 그런데 마지막 배치는 다른 배치보다 작을 수 있습니다. 예를 들어 배치 크기가 32인데 데이터 개수가 딱 나누어떨어지지 않으면 마지막 배치에는 17개만 들어갈 수도 있습니다. 그래서 배치 평균 손실에 배치 크기를 곱해 전체 손실 합으로 바꾼 뒤, 마지막에 전체 데이터 개수로 나눕니다. 이렇게 하면 더 정확한 평균 손실을 구할 수 있습니다.

정확도 계산도 비슷합니다. 각 배치에서 맞힌 개수를 더하고, 전체 데이터 개수로 나눕니다.

avg_acc = correct / total

정확도는 0과 1 사이의 값입니다. 보기 좋게 출력할 때는 100을 곱해 퍼센트로 표시하면 됩니다.

검증 루프는 학습하지 않는다

학습 루프가 모델을 고치는 과정이라면, 검증 루프는 모델을 시험해 보는 과정입니다. 검증 데이터는 파라미터를 업데이트하는 데 사용하지 않습니다. 모델이 처음 보는 데이터에 어느 정도 잘 일반화되는지 확인하기 위해 따로 남겨 둔 데이터입니다.

검증 루프에서는 세 가지가 달라집니다.

  • model.eval()로 평가 모드로 바꾼다.

  • torch.no_grad()로 기울기 계산을 끈다.

  • backward()와 optimizer.step()을 호출하지 않는다.

코드로 보면 다음과 같습니다.

@torch.no_grad()
def evaluate(model, val_loader, loss_fn, device):
    model.eval()

    total_loss = 0.0
    correct = 0
    total = 0

    for x_batch, y_batch in val_loader:
        x_batch = x_batch.to(device)
        y_batch = y_batch.to(device)

        logits = model(x_batch)
        loss = loss_fn(logits, y_batch)

        batch_size = y_batch.size(0)
        total_loss += loss.item() * batch_size

        preds = logits.argmax(dim=1)
        correct += (preds == y_batch).sum().item()
        total += batch_size

    avg_loss = total_loss / total
    avg_acc = correct / total

    return avg_loss, avg_acc

@torch.no_grad()는 이 함수 안에서 기울기 계산을 하지 않겠다는 뜻입니다. 검증에서는 파라미터를 업데이트하지 않으므로 계산 그래프를 만들 필요가 없습니다. 계산 그래프를 만들지 않으면 메모리를 덜 쓰고 속도도 빨라집니다.

검증 루프에서 loss.backward()를 호출하지 않는 이유도 같습니다. 우리는 검증 데이터로 모델을 고치고 싶은 것이 아닙니다. 검증 데이터는 시험지입니다. 시험지를 보고 공부 내용을 고쳐 버리면 공정한 평가가 되지 않습니다.

물론 검증 손실을 계산하는 것은 괜찮습니다. 손실을 계산하는 것과 손실을 이용해 기울기를 계산하고 파라미터를 업데이트하는 것은 다른 일입니다. 검증에서는 손실과 정확도를 측정만 하고, 모델의 가중치는 그대로 둡니다.

전체 학습 루프 연결하기

이제 학습 함수와 검증 함수를 에폭 단위로 연결해 보겠습니다.

epochs = 10

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

for epoch in range(1, epochs + 1):
    train_loss, train_acc = train_one_epoch(
        model,
        train_loader,
        loss_fn,
        optimizer,
        device,
    )

    val_loss, val_acc = evaluate(
        model,
        val_loader,
        loss_fn,
        device,
    )

    history['train_loss'].append(train_loss)
    history['train_acc'].append(train_acc)
    history['val_loss'].append(val_loss)
    history['val_acc'].append(val_acc)

    print(
        f'[Epoch {epoch:02d}] '
        f'train loss: {train_loss:.4f}, '
        f'train acc: {train_acc * 100:.2f}% | '
        f'val loss: {val_loss:.4f}, '
        f'val acc: {val_acc * 100:.2f}%'
    )

이제 모델은 10번의 에폭 동안 학습 데이터를 반복해서 봅니다. 각 에폭마다 학습 손실과 학습 정확도를 계산하고, 이어서 검증 손실과 검증 정확도를 계산합니다. 결과는 history에 저장됩니다.

출력은 대략 다음과 같은 모양이 됩니다.

[Epoch 01] train loss: 1.8421, train acc: 35.42% | val loss: 1.3120, val acc: 58.10%
[Epoch 02] train loss: 1.1047, train acc: 64.88% | val loss: 0.9215, val acc: 70.30%
[Epoch 03] train loss: 0.8123, train acc: 74.51% | val loss: 0.7634, val acc: 75.80%

숫자가 조금씩 움직이는 것을 보면 모델이 배우고 있다는 감각이 생깁니다. 손실은 내려가고 정확도는 올라갑니다. 물론 항상 이렇게 예쁘게 움직이지는 않습니다. 어떤 에폭에서는 손실이 잠깐 올라갈 수도 있고, 정확도가 조금 떨어질 수도 있습니다. 학습은 계단처럼 매끄럽게 올라가기보다, 울퉁불퉁한 산길을 오르는 일에 가깝습니다.

중요한 것은 전체 흐름입니다. 시간이 지나면서 학습 손실이 대체로 줄어드는지, 검증 정확도가 좋아지는지, 학습과 검증의 차이가 지나치게 벌어지지는 않는지 관찰해야 합니다.

손실과 정확도는 모델의 건강 기록이다

학습이 끝난 뒤 history에는 에폭별 기록이 남아 있습니다. 이 기록은 모델의 건강검진표와 같습니다. 숫자만 봐도 많은 것을 알 수 있습니다.

학습 손실과 검증 손실이 함께 내려가고 있다면 모델이 잘 배우고 있을 가능성이 큽니다. 학습 정확도와 검증 정확도가 함께 올라가고 있다면 더 반갑습니다. 모델이 훈련 데이터만 외우는 것이 아니라 검증 데이터에서도 잘 작동하고 있다는 뜻이기 때문입니다.

반대로 학습 손실은 계속 내려가는데 검증 손실이 올라가기 시작하면 과적합을 의심해야 합니다. 모델이 훈련 데이터에는 점점 더 능숙해지지만, 새로운 데이터에는 약해지고 있다는 신호입니다. 마치 문제집 답을 외웠지만 새로운 응용문제에는 흔들리는 학생과 비슷합니다.

학습 기록을 볼 때 자주 만나는 패턴은 다음과 같습니다.

  • 학습 손실과 검증 손실이 함께 감소하면 대체로 좋은 학습 흐름이다.

  • 학습 손실은 감소하지만 검증 손실이 증가하면 과적합 가능성이 있다.

  • 학습 손실과 검증 손실이 모두 높게 유지되면 모델이 충분히 배우지 못했을 수 있다.

  • 손실이 갑자기 nan이 되거나 크게 폭발하면 학습률이 너무 클 수 있다.

이런 기록은 나중에 하이퍼파라미터 튜닝, 정규화, 드롭아웃, 배치 정규화를 배울 때 매우 중요해집니다. 좋은 모델을 만든다는 것은 단지 코드를 실행하는 일이 아닙니다. 모델이 어떤 방식으로 좋아지고 나빠지는지 관찰하고, 그 신호를 바탕으로 다음 선택을 하는 일입니다.

간단히 그래프로 그릴 수도 있습니다.

import matplotlib.pyplot as plt

plt.plot(history['train_loss'], label='train loss')
plt.plot(history['val_loss'], label='val loss')
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend()
plt.show()

손실 그래프를 보면 숫자 표보다 흐름이 훨씬 잘 보입니다. 학습 초반에는 손실이 빠르게 내려가다가, 어느 순간부터 천천히 내려가거나 멈추는 경우가 많습니다. 검증 손실이 어느 시점부터 다시 올라가기 시작한다면 그 지점이 중요한 힌트가 됩니다.

자주 하는 실수들

학습 루프는 짧지만 실수하기 쉬운 코드입니다. 특히 처음에는 코드가 실행되더라도 모델이 제대로 배우지 않는 경우가 많습니다. 이런 문제는 대개 작은 순서 하나, 모드 하나, 장치 하나에서 생깁니다.

가장 흔한 실수는 optimizer.zero_grad()를 빼먹는 것입니다. 그러면 이전 배치의 기울기가 계속 누적되어 의도보다 훨씬 큰 업데이트가 일어날 수 있습니다. 가끔은 일부러 기울기를 누적하는 고급 기법도 있지만, 기본 학습 루프에서는 매 배치마다 기울기를 비우는 것이 원칙입니다.

또 다른 실수는 검증할 때 model.eval()을 호출하지 않는 것입니다. 모델 안에 드롭아웃이나 배치 정규화가 들어 있으면 검증 결과가 불안정해질 수 있습니다. 학습할 때는 model.train(), 검증할 때는 model.eval()이라는 짝을 기억해야 합니다.

torch.no_grad()를 빼먹는 것도 흔합니다. 이 경우 검증 자체는 될 수 있지만 불필요한 계산 그래프가 만들어져 메모리를 더 많이 사용합니다. 큰 모델을 다룰수록 이 차이는 커집니다.

입력과 정답을 같은 장치로 옮기지 않는 실수도 많습니다. 모델이 GPU에 있으면 입력과 정답도 GPU로 보내야 합니다. 다음 두 줄은 거의 습관처럼 들어간다고 생각하면 됩니다.

x_batch = x_batch.to(device)
y_batch = y_batch.to(device)

마지막으로 정답 텐서의 형태와 자료형도 확인해야 합니다. CrossEntropyLoss를 사용할 때 정답은 보통 클래스 번호를 담은 정수 텐서여야 합니다. 예를 들어 배치 크기가 32이고 클래스가 10개라면 모델 출력은 [32, 10] 모양이고, 정답은 [32] 모양인 경우가 일반적입니다.

학습 루프를 외우지 말고 이해하기

학습 루프 코드는 자주 반복되기 때문에 외우게 됩니다. 하지만 처음에는 외우기보다 흐름을 이해하는 것이 중요합니다.

for epoch in range(epochs):
    model.train()
    for x_batch, y_batch in train_loader:
        optimizer.zero_grad()
        logits = model(x_batch)
        loss = loss_fn(logits, y_batch)
        loss.backward()
        optimizer.step()

    model.eval()
    with torch.no_grad():
        for x_batch, y_batch in val_loader:
            logits = model(x_batch)
            loss = loss_fn(logits, y_batch)

이 뼈대만 머릿속에 있으면 대부분의 딥러닝 학습 코드를 읽을 수 있습니다. 이미지 분류든 텍스트 분류든, MLP든 CNN이든 RNN이든 Transformer든 기본 흐름은 크게 다르지 않습니다. 모델 구조가 복잡해져도 학습 루프는 여전히 예측, 손실, 역전파, 업데이트를 반복합니다.

나중에 LLM과 Transformer로 넘어가도 마찬가지입니다. 입력은 토큰이 되고, 모델은 다음 토큰의 점수를 예측하고, 손실 함수는 정답 토큰과 비교합니다. 그다음 역전파로 수많은 파라미터의 기울기를 계산하고, 옵티마이저가 아주 조금씩 값을 바꿉니다. 규모는 거대해지지만 원리는 지금 만든 루프 안에 이미 들어 있습니다.

하나의 박동이 쌓여 학습이 된다

학습 루프는 딥러닝에서 가장 중요한 반복문입니다. 이 반복문 안에서 데이터가 모델을 지나가고, 손실이 계산되고, 역전파가 실행되고, 파라미터가 업데이트됩니다. 모델은 한 번에 똑똑해지지 않습니다. 한 배치씩, 한 에폭씩, 작은 수정이 쌓이며 조금씩 나아집니다.

이제 우리는 PyTorch로 딥러닝을 학습시키는 기본 구조를 갖게 되었습니다. 데이터를 준비하는 방법을 알고, 모델이 텐서 연산으로 계산된다는 것도 알고, 자동 미분이 기울기를 계산해 준다는 것도 알고 있습니다. 그리고 이번 장에서 그 모든 부품을 하나의 학습 루프로 연결했습니다.

다음으로 필요한 것은 학습한 모델을 보관하는 방법입니다. 학습은 시간이 걸립니다. 좋은 성능을 얻은 모델을 매번 처음부터 다시 학습할 수는 없습니다. 다음 장에서는 모델의 파라미터를 저장하고, 나중에 다시 불러와 사용하는 방법을 배웁니다.

책 목록

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

전체 목차 보기

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

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

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

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

참여하기