🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Day 6 학습 루프 이해하기 - 슬라이드 1/8
A

AI Generated

2026. 4. 13. · 0 Views

Day 6 학습 루프 이해하기

LLM이 실제로 어떻게 학습하는지 학습 루프의 핵심 원리를 단계별로 살펴봅니다. Forward Pass, Loss 계산, Backward Pass, 파라미터 업데이트까지 한 사이클의 전 과정을 이해합니다.


목차

  1. 학습_루프란_무엇인가
  2. Forward_Pass_이해하기
  3. Loss_함수와_오차_측정
  4. Backward_Pass와_역전파
  5. 옵티마이저와_파라미터_업데이트
  6. 전체_학습_루프_조립하기
  7. 학습_모니터링과_텍스트_생성_확인

1. 학습 루프란 무엇인가

어느 날 김개발 씨가 만든 Baseline 모델을 실행해 봤습니다. 결과는 처참했습니다.

모델이 출력하는 글자는 노이즈 수준이었죠. "이걸 어쩌지..." 김개발 씨는 한숨을 쉬었습니다.

**학습 루프(Training Loop)**는 모델이 데이터를 반복적으로 보면서 스스로를 개선하는 과정입니다. 마치 운동선수가 매일 훈련을 반복하며 기록을 단축하는 것과 같습니다.

이 루프를 거치지 않은 모델은 완전히 랜덤한 출력만 내놓습니다.

다음 코드를 살펴봅시다.

# 학습 루프의 기본 구조 (의사코드)
for epoch in range(num_epochs):        # 전체 데이터를 몇 번 반복할지
    for batch in dataloader:           # 미니배치 단위로 꺼내기
        loss = model(batch)            # 모델이 예측한 결과와 정답 비교
        loss.backward()                # 역전파: 어디를 개선해야 할지 계산
        optimizer.step()               # 파라미터 업데이트: 실제로 수정
        optimizer.zero_grad()          # 기울기 초기화: 다음 루프를 위해 리셋

김개발 씨는 어제 만든 Baseline 모델이 제대로 동작하지 않아 당황했습니다. Day 5에서는 학습용 미니배치까지 준비했지만, 아직 모델을 실제로 "가르치는" 코드를 작성하지 않았기 때문입니다.

박시니어 씨가 옆에서 빈 화면을 가리키며 말했습니다. "모델은 태어날 때부터 똑똑한 게 아니야.

학습 루프를 통해 수만 번, 수십만 번 훈련을 반복해야 비로소 말을 배우게 돼." 학습 루프란 무엇일까요? 쉽게 비유하자면, 학습 루프는 마치 피아노 레슨의 한 시간과 같습니다.

학생이 연주를 시도하고(Forward Pass), 선생님이 잘못된 부분을 지적하고(Loss 계산), 어떻게 고쳐야 할지 알려주며(Backward Pass), 학생이 다시 연습하는(파라미터 업데이트) 과정이 한 사이클입니다. 이 사이클을 수천 번 반복하면 비로소 곡을 완주할 수 있게 됩니다.

학습 루프가 없던 시절에는 어땠을까요? 초기 인공지능 연구자들은 모델의 파라미터를 직접 수작업으로 조정해야 했습니다.

코드가 길어지고, 실수하기도 쉬웠습니다. 더 큰 문제는 모델이 커질수록 조정해야 할 파라미터가 수백만 개로 늘어난다는 점이었습니다.

이를 사람이 직접 하는 것은 불가능에 가까웠습니다. 바로 이런 문제를 해결하기 위해 학습 루프가 등장했습니다.

학습 루프를 사용하면 모델이 스스로 파라미터를 조정할 수 있습니다. 옵티마이저라는 도구가 기울기(gradient)를 계산하여 어느 방향으로 파라미터를 수정해야 할지 자동으로 결정합니다.

무엇보다 데이터를 통해 자동으로 개선된다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 for epoch 루프를 보면 전체 데이터셋을 몇 번 반복해서 볼지 결정합니다. epoch이 클수록 모델은 데이터를 더 많이 봐서 더 잘 학습하지만, 너무 크면 과적합(overfitting)의 위험이 있습니다.

다음으로 for batch에서 미니배치 단위로 데이터를 꺼냅니다. Day 5에서 만든 데이터 파이프라인이 바로 이 지점에서 활용됩니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 번역 서비스를 개발한다고 가정해봅시다.

수천 개의 한국어-영어 문장 쌍을 학습 루프에 넣으면, 모델은 점진적으로 더 자연스러운 번역을 생성하게 됩니다. ChatGPT 같은 대규모 언어모델도 결국 이 학습 루프를 수조 단위의 토큰에 대해 실행한 결과입니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 learning rate(학습률)를 너무 크게 설정하는 것입니다.

이렇게 하면 모델이 최적해 근처를 벗어나 발산해 버릴 수 있습니다. 반대로 너무 작으면 학습이 거의 진행되지 않습니다.

따라서 적절한 학습률을 실험을 통해 찾아야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 모델이 바보 같았던 거군요!

학습 루프를 안 돌렸으니까요!" 학습 루프를 제대로 이해하면 모델이 어떻게 지능을 획득하는지 알 수 있습니다. 이제 다음으로 학습 루프의 첫 번째 단계인 Forward Pass를 자세히 살펴보겠습니다.

실전 팁

💡 - 학습 루프는 "예측 - 평가 - 개선"의 세 단계로 이해하면 쉽습니다

  • epoch 수는 너무 크지 않게, learning rate는 실험을 통해 찾는 것이 핵심입니다
  • 이 카드뉴스는 "LLM 바닥부터 만들기: 30일 완성 코스" 코스의 6/30편입니다

2. Forward Pass 이해하기

김개발 씨는 학습 루프의 구조를 이해했지만, 첫 번째 단계인 Forward Pass가 정확히 무슨 일을 하는지 아직 감이 오지 않았습니다. "모델이 예측한다는 건데, 구체적으로 어떻게 예측하는 거죠?"

**Forward Pass(순전파)**는 입력 데이터가 모델의 층을 통과하며 최종 출력을 만들어내는 과정입니다. 마치 조립 라인에서 원재료가 여러 공정을 거치며 완제품이 되는 것과 같습니다.

이 과정에서 모델은 각 토큰 다음에 올 문자의 확률 분포를 출력합니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

# 간단한 Baseline 모델
class BigramModel(nn.Module):
    def __init__(self, vocab_size):
        super().__init__()
        self.token_embedding = nn.Embedding(vocab_size, vocab_size)

    def forward(self, idx):
        # 입력 인덱스를 임베딩 테이블에서 조회
        logits = self.token_embedding(idx)  # (B, T, C) 형태의 출력
        return logits

김개발 씨는 모델의 forward 메서드를 보며 고민에 빠졌습니다. 겨우 몇 줄 안 되는 코드인데, 이게 어떻게 "예측"을 수행하는 걸까요?

Day 5에서 만든 Baseline 모델은 사실 매우 단순한 구조였습니다. 박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다.

"Forward Pass를 이해하려면 데이터가 모델 안에서 어떻게 흘러가는지를 따라가면 돼." Forward Pass란 무엇일까요? 쉽게 비유하자면, Forward Pass는 마치 자동 판매기에 동전을 넣고 버튼을 누르는 과정과 같습니다.

동전(입력 데이터)이 투입구를 통과하고, 내부 기계 장치(모델의 층)가 처리한 뒤, 최종적으로 음료수(출력)가 나옵니다. 이 과정에서 입력은 변형되고, 여러 계산을 거쳐 최종 결과물에 도달합니다.

Forward Pass가 필요한 이유는 무엇일까요? 모델은 학습을 하려면 먼저 현재 자신의 상태에서 얼마나 틀리게 예측하는지 알아야 합니다.

그런데 예측값을 모르면 오차(loss)를 계산할 수 없고, 오차가 없으면 개선 방향도 알 수 없습니다. Forward Pass는 바로 이 "예측값"을 만들어내는 필수 과정입니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 nn.Embedding(vocab_size, vocab_size)는 각 문자 ID를 고차원 벡터로 변환하는 조회 테이블입니다.

vocab_size가 65라면 65개의 문자 각각이 65차원 벡터로 표현됩니다. 다음으로 forward 메서드에서 self.token_embedding(idx)를 호출하면, 입력 인덱스가 임베딩 테이블에서 해당하는 행(row)을 조회합니다.

이 결과값을 logits라고 부릅니다. logits은 소프트맥스(softmax) 함수를 거치기 전의 원시 점수입니다.

각 문자가 다음 토큰으로 등장할 확률을 나타내는 값이지만, 아직 정규화되지 않은 상태입니다. 나중에 손실 함수(cross-entropy)에서 이 logits을 정답과 비교하게 됩니다.

실제 현업에서는 어떻게 활용할까요? ChatGPT 같은 대규모 모델에서도 Forward Pass의 기본 원리는 동일합니다.

다만 층이 훨씬 더 많고, 각 층에서 Attention 연산, Feed-Forward 연산, Layer Normalization 등이 추가됩니다. 하지만 근본적으로는 입력이 층을 통과해 출력을 만들어내는 동일한 구조입니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 logits과 확률을 혼동하는 것입니다.

logits은 음수 값도 가질 수 있고, 합이 1이 되지도 않습니다. 확률로 변환하려면 softmax를 거쳐야 합니다.

따라서 logits을 "확률"이라고 부르면 안 됩니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

"아, 임베딩 테이블에서 그냥 행을 조회하는 거였군요! 생각보다 단순한데요?" 박시니어 씨가 웃으며 말했습니다.

"맞아. Baseline 모델은 단순하지만, 핵심 원리를 이해하기에 최고의 출발점이야." Forward Pass를 이해하면 모델이 어떻게 예측을 수행하는지 명확하게 알 수 있습니다.

하지만 예측만으로는 학습할 수 없습니다. 예측이 얼마나 틀렸는지 측정해야 하는데, 그것이 바로 다음에 살펴볼 Loss입니다.

실전 팁

💡 - logits은 softmax를 거치기 전의 원시 점수로, 확률과는 다른 값입니다

  • Forward Pass의 출력은 항상 손실 함수의 입력으로 사용됩니다
  • 이 카드뉴스는 "LLM 바닥부터 만들기: 30일 완성 코스" 코스의 6/30편입니다

3. Loss 함수와 오차 측정

Forward Pass를 이해한 김개발 씨는 다음 궁금증을 품었습니다. "예측은 했는데, 이게 맞는지 틀린지 어떻게 알 수 있죠?" 박시니어 씨가 답했습니다.

"바로 Loss 함수가 그 역할을 해."

**Loss 함수(손실 함수)**는 모델의 예측값과 실제 정답 사이의 차이를 수치화하는 함수입니다. 마치 시험에서 채점관이 답안을 채점하는 것과 같습니다.

Loss가 낮을수록 모델의 예측이 정답에 가깝다는 뜻입니다.

다음 코드를 살펴봅시다.

import torch.nn.functional as F

# 모델의 예측값(logits)과 정답(targets)
logits = model(input_ids)       # 모델이 예측한 logits: (B, T, C)
targets = next_token_ids         # 실제 다음 토큰 정답: (B, T)

# Cross-Entropy Loss 계산
loss = F.cross_entropy(
    logits.view(-1, vocab_size),   # (B*T, C)로 변환
    targets.view(-1)               # (B*T,)로 변환
)
print(f"Loss: {loss.item():.4f}")  # 예: Loss: 4.1732

김개발 씨는 Forward Pass를 통해 logits을 얻었습니다. 하지만 이 logits이라는 숫자 덩어리만으로는 "모델이 얼마나 잘하고 있는지" 판단할 수 없었습니다.

무엇이 필요할까요? 박시니어 씨가 설명했습니다.

"Loss 함수는 모델에게 '너의 예측이 정답에서 얼마나 멀리 떨어져 있는지' 알려주는 채점표와 같아." Loss 함수란 무엇일까요? 쉽게 비유하자면, Loss 함수는 마치 학교 시험의 채점 기준과 같습니다.

학생이 100점 만점 시험에서 85점을 받았다면, Loss는 15점(오차)이 됩니다. 이 오차가 0에 가까울수록 학생은 완벽하게 문제를 푼 것입니다.

언어모델에서는 이 "오차"를 Cross-Entropy Loss로 측정합니다. 왜 하필 Cross-Entropy를 사용할까요?

언어모델의 예측은 본질적으로 분류 문제입니다. "다음 토큰이 무엇일지" vocab_size개의 후보 중에서 하나를 고르는 문제이죠.

Cross-Entropy는 분류 문제에서 가장 적합한 Loss 함수로, 예측 확률 분포와 실제 정답 분포 사이의 "거리"를 측정합니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 logits.view(-1, vocab_size)는 3차원 텐서(B, T, C)를 2차원(B*T, C)으로 평탄화합니다. cross_entropy 함수가 2차원 입력을 기대하기 때문입니다.

다음으로 targets.view(-1)도 마찬가지로 1차원으로 변환합니다. 마지막으로 loss.item()은 텐서에서 파이썬 스칼라 값을 추출합니다.

학습 초기의 Loss는 얼마나 될까요? 문자 집합의 크기가 65개라면, 완전 랜덤 예측일 때의 Loss는 -ln(1/65) 즉 약 4.17입니다.

학습이 진행될수록 이 값이 점차 감소합니다. Loss가 1.0 이하로 떨어지면 모델이 꽤 잘 학습하고 있다는 뜻입니다.

실제 현업에서는 어떻게 활용할까요? GPT 모델을 학습할 때 연구자들은 Loss 그래프를 지속적으로 모니터링합니다.

Loss가 안정적으로 감소하는지, 갑자기 튀는 구간은 없는지 확인합니다. Loss가 감소하지 않거나 오히려 증가하면, 학습률 조정이나 데이터 문제를 의심해야 합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 Loss가 0에 가까워지는 것만을 목표로 하는 것입니다.

Loss가 너무 낮으면 과적합(overfitting)일 수 있습니다. 모델이 훈련 데이터는 완벽하게 암기했지만, 새로운 데이터에는 대처하지 못하는 상태죠.

따라서 학습 Loss뿐만 아니라 검증(validation) Loss도 함께 확인해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

"아, Loss가 4.17이면 모델이 아직 랜덤 수준이라는 뜻이네요?" 박시니어 씨가 고개를 끄덕였습니다. "정확해.

이 Loss를 줄이는 것이 학습의 핵심 목표야. 그런데 어떻게 줄일 수 있을까?

그게 바로 다음에 배울 Backward Pass야." Loss를 이해하면 모델의 학습 상태를 객관적으로 평가할 수 있습니다. 이제 Loss를 줄이기 위해 모델을 개선하는 방법인 Backward Pass를 살펴보겠습니다.

실전 팁

💡 - 학습 초기 Loss는 -ln(1/vocab_size)와 비슷합니다. 65개 문자면 약 4.17입니다

  • Loss가 감소하다가 갑자기 증가하면 학습률이나 데이터를 점검하세요
  • 이 카드뉴스는 "LLM 바닥부터 만들기: 30일 완성 코스" 코스의 6/30편입니다

4. Backward Pass와 역전파

김개발 씨는 Loss가 모델의 성능을 측정하는 지표라는 것을 이해했습니다. 하지만 Loss를 "안다"는 것과 Loss를 "줄인다"는 것은 전혀 다른 문제였습니다.

"Loss가 높다고 해서, 어디를 어떻게 손봐야 하는지는 모르잖아요."

**Backward Pass(역전파)**는 Loss에서부터 출발해 각 파라미터가 Loss에 미친 영향을 거꾸로 계산하는 과정입니다. 마치 탐정이 범죄 현장에서 출발해 원인을 추적해 올라가는 것과 같습니다.

이 과정을 통해 모델은 "어떤 파라미터를 어떻게 수정해야 Loss가 줄어드는지" 학습합니다.

다음 코드를 살펴봅시다.

# 역전파 실행 과정
loss = F.cross_entropy(logits.view(-1, vocab_size), targets.view(-1))

# 1단계: 역전파 - 모든 파라미터의 기울기 계산
loss.backward()

# 2단계: 기울기 확인 (학습 중 디버깅용)
for name, param in model.named_parameters():
    if param.grad is not None:
        print(f"{name}: grad_norm = {param.grad.norm():.4f}")

# 3단계: 옵티마이저로 파라미터 업데이트
optimizer.step()

김개발 씨는 Loss를 계산할 수 있게 되었습니다. 하지만 Loss 숫자만 보고 "아, 높네"라고 한탄하는 것은 학습에 아무런 도움이 되지 않았습니다.

Loss를 줄이려면 어떤 방향으로 파라미터를 수정해야 하는지 알아야 합니다. 박시니어 씨가 핵심을 짚었습니다.

"여기가 마법이 일어나는 곳이야. loss.backward() 한 줄이면, PyTorch가 알아서 모든 파라미터의 기울기를 계산해 줘." Backward Pass란 무엇일까요?

쉽게 비유하자면, Backward Pass는 마치 물이 흐르는 역방향을 추적하는 것과 같습니다. 강의 하류(출력, Loss)에서 시작해, 각 지류(각 층의 파라미터)가 하류에 얼마나 영향을 미쳤는지를 거꾸로 계산합니다.

수학적으로는 **Chain Rule(연쇄 법칙)**을 사용합니다. f(g(x))의 미분은 f'(g(x)) * g'(x)라는 아주 기본적인 미분 법칙이 수백만 개의 파라미터에 대해 자동으로 적용되는 것입니다.

이 기술의 이름은 **Automatic Differentiation(자동 미분)**입니다. PyTorch의 자동 미분 엔진은 **Computation Graph(계산 그래프)**를 이용합니다.

Forward Pass가 실행될 때마다 PyTorch는 각 연산을 노드(node)로 기록합니다. 그리고 backward()가 호출되면, 이 그래프를 역방향으로 따라가며 각 파라미터의 편미분(gradient)을 계산합니다.

개발자가 미분 공식을 직접 적을 필요가 없습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 loss.backward()가 호출되면 PyTorch의 자동 미분 엔진이 활성화됩니다. 모델 내부의 requires_grad=True로 설정된 파라미터에 대해 .grad 속성이 채워집니다.

다음으로 param.grad.norm()으로 기울기의 크기를 확인할 수 있습니다. 기울기가 너무 크면 기울기 폭발(gradient explosion), 너무 작으면 **기울기 소실(gradient vanishing)**의 신호일 수 있습니다.

기울기의 의미는 무엇일까요? 기울기(gradient)는 **"이 파라미터를 약간 증가시키면 Loss가 얼마나 변하는가"**를 나타내는 벡터입니다.

기울기가 양수면 파라미터를 줄여야 Loss가 감소하고, 음수면 파라미터를 늘려야 합니다. 즉, 기울기의 반대 방향으로 파라미터를 이동시키는 것이 핵심 아이디어입니다.

실제 현업에서는 어떻게 활용할까요? 대규모 모델을 학습할 때 연구자들은 기울기 **클리핑(gradient clipping)**을 자주 사용합니다.

기울기의 크기가 일정 threshold를 넘으면 스케일을 줄여서 기울기 폭발을 방지합니다. torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) 한 줄로 적용할 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 backward()를 호출하기 전에 zero_grad()를 하지 않는 것입니다.

PyTorch는 기본적으로 기울기를 누적(accumulate)합니다. 이전 스텝의 기울기가 남아있으면 잘못된 방향으로 파라미터가 업데이트됩니다.

따라서 매 스텝마다 반드시 optimizer.zero_grad()를 먼저 호출해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

"단 한 줄로 수백만 개 파라미터의 기울기를 계산한다고요?" 박시니어 씨가 웃었습니다. "그게 바로 PyTorch의 힘이야.

2010년대만 해도 이걸 직접 구현해야 했어. 지금은 backward() 한 줄이면 끝나." Backward Pass를 이해하면 모델이 어떻게 자신의 실수를 분석하고 개선 방향을 찾는지 알 수 있습니다.

이제 마지막 단계인 옵티마이저와 파라미터 업데이트를 살펴보겠습니다.

실전 팁

💡 - backward() 호출 전에 반드시 zero_grad()로 이전 기울기를 초기화하세요

  • 기울기가 너무 크거나 작으면 gradient clipping으로 안정화하세요
  • 이 카드뉴스는 "LLM 바닥부터 만들기: 30일 완성 코스" 코스의 6/30편입니다

5. 옵티마이저와 파라미터 업데이트

Backward Pass로 기울기를 계산했다면, 이제 실제로 모델을 개선할 차례입니다. 김개발 씨가 물었습니다.

"기울기까지 계산했는데, 파라미터를 직접 수정하면 되는 거 아닌가요?" 박시니어 씨가 고개를 저었습니다. "직접 수정하면 안 돼.

옵티마이저를 거쳐야 해."

**옵티마이저(Optimizer)**는 Backward Pass에서 계산된 기울기를 사용하여 파라미터를 실제로 수정하는 알고리즘입니다. 마치 내비게이션이 목적지까지의 경로를 안내하는 것처럼, 옵티마이저는 기울기 정보를 바탕으로 가장 효율적인 방향과 보폭으로 파라미터를 이동시킵니다.

다음 코드를 살펴봅시다.

import torch.optim as optim

# 옵티마이저 생성 - AdamW 사용
optimizer = optim.AdamW(
    model.parameters(),   # 최적화할 파라미터
    lr=3e-4,              # 학습률 (learning rate)
    betas=(0.9, 0.999),   # 모멘텀 계수
    weight_decay=0.1      # 가중치 감쇠 (정규화)
)

# 학습 루프 내에서의 파라미터 업데이트
optimizer.zero_grad()     # 1. 기울기 초기화
loss.backward()           # 2. 역전파로 기울기 계산
optimizer.step()          # 3. 기울기 기반으로 파라미터 업데이트

김개발 씨는 기울기를 계산하는 Backward Pass까지 이해했습니다. 이제 "기울기의 반대 방향으로 파라미터를 옮기면 되지 않을까?"라고 생각했지만, 실제로는 더 정교한 방법이 필요했습니다.

박시니어 씨가 설명했습니다. "기울기의 반대 방향으로 이동하는 건 맞아.

하지만 얼마나 이동할지(보폭), 이전 방향을 어느 정도 기억할지(모멘텀) 같은 세부 사항을 결정하는 게 옵티마이저의 역할이야." 옵티마이저란 무엇일까요? 쉽게 비유하자면, 옵티마이저는 마치 산을 내려가는 등산가에게 조언하는 경험 많은 가이드와 같습니다.

기울기만 보면 가장 가파른 방향을 알 수 있지만, 가이드는 지형의 특성(모멘텀)을 고려해 더 안전하고 빠른 경로를 제안합니다. 때로는 너무 가파른 곳을 피해 우회하기도 하죠.

왜 단순한 SGD가 아닌 AdamW를 사용할까요? 가장 기본적인 옵티마이저는 **SGD(Stochastic Gradient Descent)**입니다.

기울기 방향으로 고정된 보폭(학습률)만큼 이동합니다. 하지만 SGD는 학습률을 직접 조정해야 하고, 지형(손실 함수)이 불규칙하면 최적해 근처에서 진동할 수 있습니다.

AdamW는 각 파라미터마다 적응형 학습률을 계산하고, 모멘텀을 사용해 더 빠르고 안정적인 수렴을 달성합니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 lr=3e-4는 학습률을 0.0003으로 설정합니다. GPT 계열 모델에서 널리 사용되는 값입니다.

betas=(0.9, 0.999)는 1차 모멘텀(이동 방향의 관성)과 2차 모멘텀(기울기 크기의 변화)을 제어합니다. weight_decay=0.1은 L2 정규화로, 파라미터 값이 너무 커지는 것을 방지합니다.

학습 루프 내에서의 세 단계는 항상 순서대로 실행되어야 합니다. zero_grad() -> backward() -> step()의 순서가 바뀌면 학습이 깨집니다.

특히 zero_grad()를 빼먹으면 기울기가 누적되어 모델이 발산할 수 있습니다. 이 세 줄은 학습 루프의 불변의 공식이라고 생각하면 됩니다.

실제 현업에서는 어떻게 활용할까요? GPT-4, Llama, Claude 같은 대규모 모델은 모두 AdamW 옵티마이저를 사용합니다.

학습 초기에는 학습률을 서서히 올리는 Warmup 단계를 거치고, 이후에는 점진적으로 학습률을 낮추는 Cosine Decay 스케줄을 적용합니다. 이를 통해 학습 초기의 불안정성을 방지하고, 후반부에는 미세한 조정으로 더 나은 성능을 얻습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 학습률을 너무 크게 설정하여 모델이 발산하는 것입니다.

Loss가 갑자기 NaN으로 변하면 십중팔구 학습률 문제입니다. 반대로 학습률이 너무 작으면 학습이 너무 느려서 비효율적입니다.

따라서 3e-4, 1e-4, 1e-3 같은 널리 검증된 값을 출발점으로 사용하는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

"옵티마이저가 이렇게 정교한 알고리즘이었군요. 그냥 파라미터에서 기울기 빼는 줄 알았는데." 박시니어 씨가 말했습니다.

"단순한 방법도 작동은 해. 하지만 실제 프로젝트에서는 AdamW 같은 고급 옵티마이저가 훨씬 빠르고 안정적으로 수렴해." 옵티마이저를 이해하면 학습 루프의 마지막 퍼즐 조각을 얻게 됩니다.

이제 전체 학습 루프를 하나로 연결해 보겠습니다.

실전 팁

💡 - 학습 루프의 세 단계(zero_grad -> backward -> step)는 항상 순서대로 실행하세요

  • 학습률은 3e-4를 시작점으로, Warmup + Cosine Decay를 적용하면 좋습니다
  • 이 카드뉴스는 "LLM 바닥부터 만들기: 30일 완성 코스" 코스의 6/30편입니다

6. 전체 학습 루프 조립하기

지금까지 Forward Pass, Loss, Backward Pass, 옵티마이저를 각각 살펴봤습니다. 김개발 씨는 이제 이 네 가지를 하나로 연결할 차례입니다.

"각각은 이해했는데, 이걸 실제로 어떻게 하나의 루프로 만드는 거죠?"

전체 학습 루프는 Forward Pass, Loss 계산, Backward Pass, 파라미터 업데이트를 반복 실행하는 완전한 학습 사이클입니다. 마치 자동차가 엔진, 변속기, 바퀴가 모두 연결되어야 달릴 수 있듯, 각 단계가 정확한 순서로 연결되어야 모델이 학습합니다.

다음 코드를 살펴봅시다.

# 전체 학습 루프 구현
model.train()                     # 학습 모드 활성화
for epoch in range(num_epochs):
    for xb, yb in train_loader:   # 미니배치 순회
        logits = model(xb)        # Forward Pass
        loss = F.cross_entropy(
            logits.view(-1, vocab_size),
            yb.view(-1)
        )
        optimizer.zero_grad()     # 기울기 초기화
        loss.backward()           # Backward Pass
        optimizer.step()          # 파라미터 업데이트

    # 에포크별 Loss 출력
    print(f"Epoch {epoch}: loss={loss.item():.4f}")

김개발 씨는 드디어 전체 학습 루프를 조립할 준비가 되었습니다. Day 5에서 만든 데이터 파이프라인, Day 5의 Baseline 모델, 그리고 오늘 배운 Forward Pass, Loss, Backward Pass, 옵티마이저를 하나로 합치는 순간입니다.

박시니어 씨가 말했습니다. "이제 각 부품을 하나씩 조립해 보자.

자동차 부품을 다 가지고 있지만, 조립하지 않으면 달릴 수 없는 것과 같아." 전체 학습 루프의 구조를 살펴보겠습니다. 쉽게 비유하자면, 학습 루프는 마치 식당의 주방 작업 흐름과 같습니다.

주문이 들어오면(데이터 로드), 재료를 준비하고(Forward Pass), 요리해서 맛을 보고(Loss 계산), 레시피를 수정하고(Backward Pass), 다음 요리에서 개선된 레시피를 적용합니다(파라미터 업데이트). 이 과정이 하루 종일 반복되면 점점 더 맛있는 요리를 만들게 됩니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 model.train()은 모델을 학습 모드로 설정합니다.

PyTorch에서는 BatchNorm이나 Dropout 같은 층이 학습/평가 모드에서 다르게 동작하기 때문에 이 설정이 필요합니다. Baseline 모델에는 해당 층이 없지만, 앞으로 Transformer 모델을 만들 때 중요해집니다.

다음으로 이중 for 루프를 보겠습니다. 바깥쪽 epoch 루프는 전체 데이터셋을 몇 번 반복할지 결정합니다.

안쪽 batch 루프는 미니배치 단위로 데이터를 처리합니다. 이 구조는 거의 모든 PyTorch 학습 코드에서 동일하게 사용됩니다.

루프 안의 네 줄은 이미 각각 살펴본 내용입니다. logits = model(xb)가 Forward Pass, loss = F.cross_entropy(...)가 Loss 계산, loss.backward()가 Backward Pass, optimizer.step()가 파라미터 업데이트입니다.

optimizer.zero_grad()는 반드시 backward() 앞에 와야 합니다. 이 다섯 줄이 학습 루프의 심장입니다.

에포크가 진행될수록 Loss는 어떻게 변할까요? 학습이 잘 진행되면 Loss는 점진적으로 감소합니다.

초기에는 빠르게 떨어지다가, 어느 시점부터 완만하게 감소합니다. 이는 모델이 쉬운 패턴부터 먼저 학습하고, 점차 복잡한 패턴을 학습하기 때문입니다.

Loss 그래프를 그려보면 이러한 패턴을 직관적으로 확인할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?

ChatGPT나 Claude 같은 대규모 모델의 학습도 기본 구조는 동일합니다. 다른 점은 데이터가 수조 토큰 단위이고, 수천 개의 GPU에서 병렬로 학습한다는 것뿐입니다.

하지만 각 GPU에서 실행되는 학습 루프는 지금 우리가 작성한 것과 본질적으로 같습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 학습 중에 모델을 평가하지 않는 것입니다. 학습 Loss만 보면 과적합을 감지하기 어렵습니다.

따라서 일정 간격으로 검증 데이터로 모델을 평가하고, 검증 Loss가 증가하기 시작하면 학습을 조기 종료(early stopping)하는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

김개발 씨는 전체 학습 루프를 실행했습니다. Loss가 에포크마다 꾸준히 감소하고 있었습니다.

"드디어 모델이 학습하고 있어요!" 박시니어 씨가 미소를 지었습니다. "축하해.

이게 바로 딥러닝의 마법이야. 이제 학습이 잘 되고 있는지 평가하는 방법도 알아야 해." 전체 학습 루프를 조립하면 비로소 모델이 실제로 학습을 시작합니다.

하지만 학습이 잘 되고 있는지 확인하려면 평가 방법이 필요합니다.

실전 팁

💡 - 학습 루프의 핵심 다섯 줄: forward -> loss -> zero_grad -> backward -> step

  • 학습 Loss만 보지 말고, 일정 간격으로 검증 Loss도 확인하세요
  • 이 카드뉴스는 "LLM 바닥부터 만들기: 30일 완성 코스" 코스의 6/30편입니다

7. 학습 모니터링과 텍스트 생성 확인

김개발 씨의 모델이 드디어 학습을 시작했습니다. Loss가 감소하고 있는 것은 확인했지만, 정말 모델이 "글자를 배우고" 있는 걸까요?

Loss 숫자만으로는 확신이 들지 않았습니다.

학습 모니터링은 Loss 추이를 관찰하고, 주기적으로 모델이 생성하는 텍스트를 직접 확인하여 학습 상태를 평가하는 과정입니다. 마치 학생의 시험 점수뿐만 아니라 실제 작문 능력도 확인하는 것과 같습니다.

이를 통해 과적합을 방지하고 학습 방향을 점검할 수 있습니다.

다음 코드를 살펴봅시다.

# 학습 중간에 텍스트 생성 확인
@torch.no_grad()
def generate(model, idx, max_new_tokens):
    for _ in range(max_new_tokens):
        logits = model(idx)                    # Forward Pass
        logits = logits[:, -1, :]              # 마지막 시점의 logits
        probs = F.softmax(logits, dim=-1)      # 확률 분포로 변환
        idx_next = torch.multinomial(probs, num_samples=1)  # 샘플링
        idx = torch.cat([idx, idx_next], dim=1)  # 시퀀스에 추가
    return idx

# 학습 루프 내에서 주기적 확인
if step % 500 == 0:
    context = torch.zeros((1, 1), dtype=torch.long)
    generated = generate(model, context, max_new_tokens=100)
    print(decode(generated[0].tolist()))

김개발 씨는 Loss가 감소하는 것을 보며 안심했지만, 박시니어 씨는 한 가지 더 확인하라고 했습니다. "Loss 숫자는 좋아지고 있지만, 모델이 실제로 의미 있는 텍스트를 생성하는지도 봐야 해." 박시니어 씨가 generate 함수를 화면에 띄웠습니다.

"이 함수로 모델이 현재 수준에서 어떤 텍스트를 생성하는지 직접 볼 수 있어." 학습 모니터링이란 무엇일까요? 쉽게 비유하자면, 학습 모니터링은 마치 음악 학생의 연주 회복과 같습니다.

레슨을 받은 후 연습 시간(Loss 감소)도 중요하지만, 정기적으로 연주회(텍스트 생성)를 열어 실제 연주력을 확인해야 합니다. 연습량은 많은데 연주가 늘지 않는다면, 학습 방법을 수정해야 합니다.

왜 텍스트 생성 확인이 필요할까요? Loss는 모델의 평균적인 성능을 나타내지만, 실제 생성 품질을 직접 보여주지는 않습니다. 때로는 Loss가 낮은데 생성된 텍스트가 반복적이거나 무의미한 경우도 있습니다.

반대로 Loss가 아직 높아도 생성된 텍스트에서 의미 있는 패턴이 발견되면 학습이 올바른 방향으로 진행되고 있다는 확신을 얻을 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 @torch.no_grad() 데코레이터는 텍스트 생성 시 기울기 계산을 생략합니다. 텍스트 생성은 학습이 아니라 추론(inference)이므로 기울기가 필요 없고, 메모리도 절약됩니다.

다음으로 logits[:, -1, :]에서 마지막 토큰의 logits만 추출합니다. 새로 생성할 토큰은 항상 시퀀스의 마지막 토큰 다음에 오기 때문입니다.

F.softmax로 logits을 확률 분포로 변환한 뒤, torch.multinomial으로 확률에 비례하여 무작위로 토큰을 샘플링합니다. 항상 가장 확률이 높은 토큰을 선택하면(greedy decoding) 생성된 텍스트가 반복적이고 지루해집니다.

무작위 샘플링 덕분에 모델이 다양하고 창의적인 텍스트를 생성할 수 있습니다. 학습 초기와 후기의 텍스트는 어떻게 다를까요?

학습 초기에는 대부분 무의미한 문자 나열이 출력됩니다. "xxq bkzl mpwr..." 같은 결과죠.

하지만 학습이 진행될수록 점차 실제 단어나 문장의 형태가 나타납니다. 처음에는 공백과 구두점을 배우고, 이어서 흔한 문자 조합을 학습하며, 최종적으로는 문맥에 맞는 단어를 생성하기 시작합니다.

실제 현업에서는 어떻게 활용할까요? GPT 모델을 학습하는 연구자들은 Weights & BiasesTensorBoard 같은 도구를 사용해 Loss 그래프, 생성 샘플, 학습률 변화 등을 실시간으로 모니터링합니다.

특히 생성 샘플은 학습 초반에는 100 스텝마다, 후반에는 1000 스텝마다 확인하는 식으로 주기를 조절합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 텍스트 생성에 너무 많은 컴퓨팅 자원을 소모하는 것입니다. 매 배치마다 텍스트를 생성하면 학습 속도가 크게 느려집니다.

따라서 step % 500 == 0처럼 일정 간격으로만 확인하는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

김개발 씨는 500 스텝마다 생성된 텍스트를 확인했습니다. 처음에는 알 수 없는 문자 나열이었지만, 2000 스텝이 지나자 "the" 같은 흔한 영어 단어가 간헐적으로 등장하기 시작했습니다.

"와, 정말 배우고 있어요!" 학습 모니터링을 통해 모델의 학습 상태를 정확하게 파악할 수 있습니다. Loss 숫자와 생성 텍스트를 함께 확인하면 학습이 올바른 방향으로 진행되고 있는지 확신할 수 있습니다.

실전 팁

💡 - 텍스트 생성 확인은 500~1000 스텝 간격으로 주기적으로 하세요

  • @torch.no_grad()로 추론 시 메모리를 절약하세요
  • 이 카드뉴스는 "LLM 바닥부터 만들기: 30일 완성 코스" 코스의 6/30편입니다
  • 다음 카드뉴스에서는 "Day 7: 1주차 정리 - 언어모델 기초"를 다룹니다. 지금까지 배운 토크나이저부터 학습 루프까지의 핵심을 한 번에 정리해 드리겠습니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#TrainingLoop#Backpropagation#LossFunction#PyTorch#GradientDescent#LLM

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.
이전6/6
다음