🤖

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

⚠️

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

이미지 로딩 중...

Base Training Pretraining 분석 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 28. · 19 Views

Base Training Pretraining 분석 완벽 가이드

LLM의 기초 체력을 만드는 Pretraining 과정을 살펴봅니다. base_train.py 소스 코드부터 분산 학습, 옵티마이저 선택, 학습률 스케줄링, 체크포인트 관리까지 실무에서 꼭 알아야 할 핵심을 이북처럼 술술 읽히게 정리했습니다.


목차

  1. Pretraining이_하는_일
  2. base_train.py_소스_코드_분석
  3. torchrun으로_분산_학습
  4. AdamW_vs_Muon_옵티마이저
  5. Learning_Rate_스케줄링
  6. 체크포인트_저장과_복원

1. Pretraining이 하는 일

김개발 씨는 회사에서 처음으로 LLM 프로젝트에 투입되었습니다. 팀장님이 "이번에 우리 모델을 처음부터 학습시켜볼 거야"라고 말씀하셨는데, 도대체 '처음부터 학습'이라는 게 무슨 뜻일까요?

Pretraining은 한마디로 AI 모델에게 언어의 기본기를 가르치는 과정입니다. 마치 아이가 말을 배우기 전에 수많은 대화를 듣고 패턴을 익히는 것과 같습니다.

이 과정을 거쳐야 비로소 모델이 문장의 구조, 문맥, 의미를 이해할 수 있게 됩니다.

다음 코드를 살펴봅시다.

# Pretraining의 핵심 루프 구조
def pretrain_step(model, batch, optimizer):
    # 입력 토큰에서 다음 토큰을 예측하는 것이 목표
    input_ids = batch["input_ids"]
    labels = batch["labels"]

    # 순전파: 모델이 다음 토큰을 예측
    outputs = model(input_ids)

    # 손실 계산: 예측과 정답의 차이
    loss = cross_entropy(outputs, labels)

    # 역전파: 가중치 업데이트를 위한 기울기 계산
    loss.backward()
    optimizer.step()

    return loss.item()

김개발 씨는 입사 6개월 차 ML 엔지니어입니다. 지금까지는 이미 학습된 모델을 가져다 쓰기만 했는데, 이번에 처음으로 Pretraining을 담당하게 되었습니다.

선배 박시니어 씨에게 조심스럽게 물었습니다. "선배님, Pretraining이 정확히 뭔가요?

Fine-tuning이랑 뭐가 다른 거예요?" 박시니어 씨가 빙긋 웃으며 답했습니다. "쉽게 생각해봐.

사람도 태어나서 바로 전문 지식을 배우지 않잖아. 먼저 말하는 법, 읽는 법을 배우고, 그 다음에 전공 공부를 하지.

Pretraining은 그 '말하는 법을 배우는 단계'야." 그렇습니다. Pretraining은 모델이 언어 자체를 이해하는 기초 체력을 쌓는 과정입니다.

좀 더 구체적으로 설명하자면, Pretraining에서 모델은 다음 토큰 예측이라는 단순한 과제를 수행합니다. "오늘 날씨가 정말"이라는 문장이 주어지면 다음에 올 단어가 "좋다", "덥다", "춥다" 중 무엇일지 맞추는 겁니다.

단순해 보이지만 이 과정을 수십억 번 반복하면 놀라운 일이 벌어집니다. 모델은 문법 규칙, 단어 간의 관계, 문맥의 흐름, 심지어 세상에 대한 지식까지 자연스럽게 학습하게 됩니다.

마치 도서관의 모든 책을 읽은 사서가 어떤 질문에도 답할 수 있게 되는 것과 비슷합니다. 위의 코드를 살펴보면, pretrain_step 함수가 핵심입니다.

먼저 입력 토큰을 받아 모델이 다음 토큰을 예측합니다. 그 다음 실제 정답과 비교하여 손실을 계산하고, 역전파를 통해 모델의 가중치를 조금씩 수정합니다.

이 단순한 과정이 수천억 번 반복되면서 모델은 점점 더 정확하게 다음 토큰을 예측하게 됩니다. 그리고 그 과정에서 언어의 본질을 깨우치게 되는 것입니다.

실무에서 Pretraining은 엄청난 컴퓨팅 자원을 필요로 합니다. GPT-3 수준의 모델을 Pretraining하려면 수천 대의 GPU가 수 주 동안 돌아가야 합니다.

그래서 대부분의 회사는 이미 Pretrain된 모델을 가져다 Fine-tuning해서 사용합니다. 하지만 Pretraining의 원리를 이해하면 모델이 왜 특정 방식으로 동작하는지, 어떤 한계가 있는지 파악할 수 있습니다.

이는 더 나은 Fine-tuning 전략을 세우는 데 큰 도움이 됩니다.

실전 팁

💡 - Pretraining 데이터의 품질이 모델 성능의 80%를 결정합니다

  • 토큰 수가 많다고 무조건 좋은 게 아니라, 다양하고 깨끗한 데이터가 중요합니다

2. base train.py 소스 코드 분석

박시니어 씨가 김개발 씨에게 base_train.py 파일을 열어보라고 했습니다. "이 파일 하나에 Pretraining의 모든 비밀이 담겨 있어.

한 줄씩 같이 읽어보자."

base_train.py는 Pretraining을 실행하는 메인 스크립트입니다. 모델 초기화부터 학습 루프, 로깅, 체크포인트 저장까지 모든 과정이 이 파일에서 조율됩니다.

오케스트라의 지휘자처럼 전체 학습 과정을 이끄는 핵심 파일입니다.

다음 코드를 살펴봅시다.

# base_train.py 핵심 구조
import torch
from model import GPT
from data import DataLoader

def main():
    # 1. 설정 로드
    config = load_config("config.yaml")

    # 2. 모델 초기화
    model = GPT(config.model)
    model = model.to(device)

    # 3. 옵티마이저 설정
    optimizer = create_optimizer(model, config)

    # 4. 학습 루프
    for step in range(config.max_steps):
        batch = next(data_loader)
        loss = train_step(model, batch, optimizer)

        if step % config.log_interval == 0:
            log_metrics(step, loss)

김개발 씨는 base_train.py를 처음 열어보고 당황했습니다. 수백 줄의 코드가 빼곡히 적혀 있었기 때문입니다.

하지만 박시니어 씨가 차근차근 설명해주자 구조가 보이기 시작했습니다. "복잡해 보여도 결국 네 가지 단계야.

설정 로드, 모델 초기화, 옵티마이저 설정, 그리고 학습 루프." 먼저 설정 로드 단계에서는 config.yaml 파일에서 하이퍼파라미터를 읽어옵니다. 모델 크기, 학습률, 배치 사이즈 같은 중요한 값들이 여기에 정의되어 있습니다.

다음으로 모델 초기화가 진행됩니다. GPT 클래스의 인스턴스를 생성하고 GPU로 옮깁니다.

이때 모델의 가중치는 무작위로 초기화됩니다. 마치 백지 상태의 뇌와 같습니다.

세 번째로 옵티마이저 설정입니다. 옵티마이저는 모델의 가중치를 어떻게 업데이트할지 결정하는 알고리즘입니다.

AdamW나 Muon 같은 옵티마이저를 선택하고 학습률을 설정합니다. 마지막으로 학습 루프가 실행됩니다.

이 부분이 실제로 모델이 학습하는 곳입니다. 데이터를 가져와서 모델에 넣고, 손실을 계산하고, 가중치를 업데이트하는 과정을 수십만 번 반복합니다.

코드를 자세히 보면 log_interval마다 로그를 출력하는 부분이 있습니다. 학습이 제대로 진행되는지 모니터링하기 위함입니다.

손실이 점점 줄어들면 학습이 잘 되고 있다는 신호입니다. 실무에서는 이 기본 구조에 많은 기능이 추가됩니다.

분산 학습을 위한 DDP 설정, 그래디언트 클리핑, 혼합 정밀도 학습, 체크포인트 저장 등이 들어갑니다. 김개발 씨가 물었습니다.

"그럼 이 파일만 이해하면 Pretraining을 할 수 있는 건가요?" 박시니어 씨가 고개를 끄덕였습니다. "기본적으로는 그래.

물론 세부 사항을 이해하려면 더 깊이 파봐야 하지만, 이 구조를 머릿속에 넣어두면 나머지는 응용이야."

실전 팁

💡 - config 파일을 버전 관리하면 실험 재현이 쉬워집니다

  • 학습 루프 안에서 불필요한 연산을 최소화해야 속도가 빨라집니다

3. torchrun으로 분산 학습

김개발 씨가 학습을 시작했는데, 예상 시간이 무려 3개월이라고 떴습니다. "이걸 어떻게 기다려요?" 박시니어 씨가 웃으며 말했습니다.

"그래서 분산 학습이 필요한 거야."

torchrun은 PyTorch에서 제공하는 분산 학습 실행 도구입니다. 여러 대의 GPU 또는 여러 대의 서버에서 동시에 학습을 진행할 수 있게 해줍니다.

마치 한 사람이 100페이지를 읽는 대신, 10명이 10페이지씩 나눠 읽는 것과 같습니다.

다음 코드를 살펴봅시다.

# 분산 학습 실행 명령어
# torchrun --nproc_per_node=4 --nnodes=2 base_train.py

# base_train.py 내 분산 학습 설정
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP

def setup_distributed():
    # 분산 환경 초기화
    dist.init_process_group(backend="nccl")

    # 현재 프로세스의 rank와 전체 world_size
    rank = dist.get_rank()
    world_size = dist.get_world_size()

    # 각 프로세스에 GPU 할당
    torch.cuda.set_device(rank % torch.cuda.device_count())

    return rank, world_size

김개발 씨는 GPU 한 대로 학습을 돌려봤지만 너무 오래 걸렸습니다. 회사에는 8대의 GPU가 있는데, 이걸 전부 활용할 방법이 없을까요?

박시니어 씨가 torchrun을 소개해줬습니다. "torchrun은 PyTorch의 공식 분산 학습 런처야.

이 도구를 쓰면 여러 GPU에서 동시에 학습을 진행할 수 있어." 분산 학습의 핵심 개념은 Data Parallelism입니다. 전체 데이터를 여러 GPU에 나눠주고, 각 GPU가 자기 몫의 데이터로 기울기를 계산합니다.

그 다음 모든 GPU의 기울기를 모아서 평균 내고, 이를 기반으로 모델을 업데이트합니다. 마치 시험 채점을 여러 선생님이 나눠서 하는 것과 같습니다.

각자 자기 담당 답안지를 채점하고, 마지막에 점수를 취합하는 거죠. 명령어를 살펴보면, --nproc_per_node=4는 한 서버에서 4개의 GPU를 사용한다는 뜻입니다.

--nnodes=2는 총 2대의 서버를 사용한다는 의미입니다. 결과적으로 4 x 2 = 8개의 GPU가 동시에 학습에 참여합니다.

코드에서 dist.init_process_group을 호출하면 분산 환경이 초기화됩니다. backend="nccl"은 NVIDIA GPU 간 통신에 최적화된 프로토콜을 사용한다는 뜻입니다.

rank는 각 프로세스의 고유 번호입니다. 8개의 GPU를 쓴다면 rank는 0부터 7까지 부여됩니다.

world_size는 전체 프로세스 수, 즉 8이 됩니다. 주의할 점은 분산 학습에서 배치 사이즈의 개념입니다.

각 GPU에서 배치 사이즈가 32라면, 전체 effective 배치 사이즈는 32 x 8 = 256이 됩니다. 학습률도 이에 맞춰 조정해야 합니다.

김개발 씨가 torchrun으로 8개 GPU를 활용하니 학습 시간이 1/8로 줄었습니다. 3개월이 걸릴 학습이 약 2주로 단축된 것입니다.

"와, 이렇게 차이가 나는군요!" 김개발 씨가 감탄했습니다.

실전 팁

💡 - 분산 학습 시 배치 사이즈와 학습률의 관계를 항상 고려하세요

  • NCCL 에러가 나면 대부분 네트워크 설정 문제입니다

4. AdamW vs Muon 옵티마이저

학습을 시작하기 전, 박시니어 씨가 물었습니다. "옵티마이저는 뭘로 할 거야?" 김개발 씨는 당연히 Adam이라고 답했는데, 선배가 고개를 저었습니다.

"요즘은 선택지가 더 많아졌어."

옵티마이저는 모델의 가중치를 어떻게 업데이트할지 결정하는 알고리즘입니다. AdamW는 가장 널리 쓰이는 검증된 옵티마이저이고, Muon은 최근 등장한 새로운 옵티마이저로 더 빠른 수렴을 목표로 합니다.

다음 코드를 살펴봅시다.

# AdamW 옵티마이저 설정
optimizer_adamw = torch.optim.AdamW(
    model.parameters(),
    lr=3e-4,
    betas=(0.9, 0.95),
    weight_decay=0.1
)

# Muon 옵티마이저 설정 (예시)
from muon import Muon
optimizer_muon = Muon(
    model.parameters(),
    lr=0.02,
    momentum=0.95,
    nesterov=True,
    ns_steps=5  # Newton-Schulz 반복 횟수
)

옵티마이저는 딥러닝에서 가장 중요한 선택 중 하나입니다. 아무리 좋은 모델 구조와 데이터가 있어도, 옵티마이저가 잘못되면 학습이 제대로 되지 않습니다.

박시니어 씨가 비유를 들어 설명했습니다. "산에서 가장 낮은 골짜기를 찾는다고 생각해봐.

옵티마이저는 어느 방향으로 얼마나 빨리 걸을지 결정하는 거야. SGD는 무작정 내리막으로 걷는 거고, Adam은 지형을 분석하면서 똑똑하게 걷는 거지." AdamW는 Adam에 weight decay를 올바르게 적용한 버전입니다.

Adam은 각 파라미터마다 적응적인 학습률을 사용합니다. 자주 업데이트되는 파라미터는 학습률을 낮추고, 드물게 업데이트되는 파라미터는 학습률을 높입니다.

코드에서 betas=(0.9, 0.95)는 모멘텀과 적응적 학습률의 감쇠율입니다. weight_decay=0.1은 과적합을 방지하기 위한 정규화 항입니다.

Muon은 최근 등장한 옵티마이저로, 기존 Adam의 한계를 극복하려는 시도입니다. Muon은 Newton-Schulz iteration을 사용하여 더 정확한 2차 정보를 활용합니다.

쉽게 말해 Adam이 1차 미분(기울기)만 보고 걷는다면, Muon은 2차 미분(곡률)까지 고려해서 더 효율적인 경로를 찾습니다. 실험 결과에 따르면 Muon은 같은 성능에 도달하는 데 더 적은 스텝이 필요합니다.

하지만 각 스텝의 계산량이 더 많아서, 실제 시간으로는 큰 차이가 없을 수도 있습니다. 김개발 씨가 물었습니다.

"그럼 뭘 써야 하나요?" 박시니어 씨가 답했습니다. "처음이면 AdamW로 시작해.

검증된 방법이니까. 나중에 더 최적화가 필요하면 Muon을 실험해봐.

하지만 옵티마이저보다 데이터 품질이나 모델 구조가 더 중요하다는 걸 잊지 마."

실전 팁

💡 - AdamW의 weight_decay 0.1은 대부분의 경우 잘 작동하는 기본값입니다

  • 새로운 옵티마이저를 시도할 때는 작은 규모로 먼저 실험하세요

5. Learning Rate 스케줄링

김개발 씨가 학습을 시작했는데, 처음에는 손실이 잘 줄어들다가 어느 순간 멈춰버렸습니다. "왜 더 안 줄어들지?" 박시니어 씨가 학습률 그래프를 보여주며 말했습니다.

"학습률 스케줄링을 안 했구나."

Learning Rate 스케줄링은 학습 중 학습률을 동적으로 조절하는 기법입니다. 처음에는 빠르게 배우고, 나중에는 섬세하게 조정하는 것입니다.

마치 요리할 때 처음에는 센 불로 빨리 익히고, 마지막에는 약한 불로 맛을 다듬는 것과 같습니다.

다음 코드를 살펴봅시다.

# Warmup + Cosine Decay 스케줄러
def get_lr(step, warmup_steps, max_steps, max_lr, min_lr):
    # 1. Warmup 단계: 학습률을 0에서 max_lr까지 선형 증가
    if step < warmup_steps:
        return max_lr * (step / warmup_steps)

    # 2. Decay 단계: Cosine 함수로 학습률 감소
    progress = (step - warmup_steps) / (max_steps - warmup_steps)
    cosine_decay = 0.5 * (1 + math.cos(math.pi * progress))

    return min_lr + (max_lr - min_lr) * cosine_decay

# 학습 루프에서 사용
for step in range(max_steps):
    lr = get_lr(step, warmup_steps=2000, max_steps=100000,
                max_lr=3e-4, min_lr=3e-5)
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr

학습률은 딥러닝에서 가장 민감한 하이퍼파라미터입니다. 너무 크면 학습이 발산하고, 너무 작으면 학습이 느려집니다.

박시니어 씨가 칠판에 그림을 그리며 설명했습니다. "학습을 산에서 골짜기를 찾는 것에 비유했잖아.

학습률은 한 걸음의 크기야. 처음에는 큰 걸음으로 빠르게 내려가야 해.

근데 골짜기 근처에서도 큰 걸음으로 뛰면? 목적지를 지나쳐버리지." 그래서 등장한 것이 학습률 스케줄링입니다.

가장 많이 쓰이는 패턴은 Warmup + Cosine Decay 조합입니다. 먼저 Warmup 단계입니다.

학습 초반에는 모델의 가중치가 무작위 상태입니다. 이때 갑자기 큰 학습률을 적용하면 불안정해질 수 있습니다.

그래서 처음 몇천 스텝 동안은 학습률을 0에서 목표값까지 천천히 올립니다. 마치 자동차가 출발할 때 갑자기 엑셀을 밟지 않고 서서히 속도를 올리는 것과 같습니다.

다음은 Cosine Decay 단계입니다. 목표 학습률에 도달한 후에는 코사인 함수를 따라 학습률을 서서히 줄입니다.

코사인 함수는 처음에는 천천히 줄다가 나중에 급격히 줄어드는 특성이 있어서, 학습 후반에 더 섬세한 조정이 가능합니다. 코드를 보면 warmup_steps=2000으로 설정했습니다.

이는 처음 2000스텝 동안 학습률이 0에서 3e-4까지 선형으로 증가한다는 뜻입니다. 그 이후에는 코사인 함수를 따라 3e-4에서 3e-5까지 서서히 감소합니다.

김개발 씨가 스케줄링을 적용하자 멈춰있던 손실이 다시 줄어들기 시작했습니다. "와, 정말 효과가 있네요!" 박시니어 씨가 덧붙였습니다.

"스케줄링의 세부 값은 실험으로 찾아야 해. 하지만 일반적으로 전체 스텝의 1-5%를 warmup으로, min_lr은 max_lr의 1/10 정도로 설정하면 잘 작동해."

실전 팁

💡 - Warmup 없이 바로 높은 학습률을 쓰면 학습 초반이 불안정해집니다

  • 학습이 수렴하지 않으면 먼저 학습률을 낮춰보세요

6. 체크포인트 저장과 복원

김개발 씨가 3일간의 학습 끝에 퇴근을 했는데, 다음 날 출근하니 서버가 재부팅되어 있었습니다. 3일치 학습이 날아간 겁니다.

"어떡해요!" 박시니어 씨가 차분하게 물었습니다. "체크포인트 저장했어?"

체크포인트는 학습 중간 상태를 저장하는 것입니다. 모델 가중치, 옵티마이저 상태, 현재 스텝 등을 파일로 저장해두면, 문제가 생겼을 때 해당 지점부터 다시 시작할 수 있습니다.

게임의 세이브 포인트와 같은 역할입니다.

다음 코드를 살펴봅시다.

# 체크포인트 저장
def save_checkpoint(model, optimizer, step, loss, path):
    checkpoint = {
        "model_state_dict": model.state_dict(),
        "optimizer_state_dict": optimizer.state_dict(),
        "step": step,
        "loss": loss,
        "config": config
    }
    torch.save(checkpoint, path)
    print(f"Checkpoint saved at step {step}")

# 체크포인트 복원
def load_checkpoint(path, model, optimizer):
    checkpoint = torch.load(path)
    model.load_state_dict(checkpoint["model_state_dict"])
    optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
    start_step = checkpoint["step"]
    print(f"Resumed from step {start_step}")
    return start_step

Pretraining은 몇 주에서 몇 달이 걸리는 긴 작업입니다. 그 긴 시간 동안 무슨 일이 일어날지 아무도 모릅니다.

서버가 다운될 수도 있고, 전원이 나갈 수도 있습니다. 김개발 씨는 쓰라린 경험을 했습니다.

3일간의 학습이 한순간에 날아간 것입니다. 박시니어 씨가 위로하며 말했습니다.

"누구나 한 번쯤은 겪는 일이야. 중요한 건 같은 실수를 반복하지 않는 거지.

체크포인트를 제대로 설정하면 이런 상황에서도 복구할 수 있어." 체크포인트에는 여러 정보가 담깁니다. 가장 중요한 건 model_state_dict로, 모델의 모든 가중치가 여기에 저장됩니다.

그 다음 optimizer_state_dict도 중요합니다. Adam 같은 옵티마이저는 각 파라미터의 모멘텀 정보를 기억하고 있는데, 이걸 잃으면 학습이 불안정해질 수 있습니다.

현재 스텝 번호와 손실값도 저장합니다. 이 정보가 있어야 어디서부터 다시 시작할지 알 수 있습니다.

설정값도 함께 저장해두면 나중에 어떤 하이퍼파라미터로 학습했는지 추적할 수 있습니다. 체크포인트를 얼마나 자주 저장해야 할까요?

너무 자주 저장하면 디스크 공간이 부족해지고 학습 속도도 느려집니다. 너무 드물게 저장하면 장애 시 손실이 커집니다.

일반적으로 1000-5000 스텝마다 저장하는 것이 좋습니다. 또한 최근 N개의 체크포인트만 유지하고 나머지는 삭제하는 방식을 쓰기도 합니다.

코드에서 load_checkpoint 함수를 보면, 저장된 체크포인트에서 모든 정보를 복원합니다. 이 함수를 학습 시작 시 호출하면 중단된 지점부터 이어서 학습할 수 있습니다.

김개발 씨는 이제 1000스텝마다 자동으로 체크포인트를 저장하도록 설정했습니다. 다음에 서버가 다운되어도 최대 1000스텝만 다시 학습하면 됩니다.

"이제 안심하고 퇴근할 수 있겠네요." 김개발 씨가 안도의 한숨을 쉬었습니다.

실전 팁

💡 - 체크포인트 파일명에 스텝 번호를 포함시키면 관리가 쉽습니다

  • 중요한 체크포인트는 클라우드 스토리지에 백업해두세요

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

#Python#Pretraining#DistributedTraining#AdamW#LearningRate#Checkpoint#AI,LLM,Deep Learning

댓글 (0)

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