인공지능/공부

인공지능 기초 학습 코드 + 개념

이게될까 2025. 7. 29. 20:55
728x90
728x90

이 코드를 천천히 따라가면 이런 loss 그래프를 그릴 수 있다.

한글이 깨진건지 글자는 많이 깨져 있다...


📌 전체 코드 설명

model_name = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)
model = AutoModelForCausalLM.from_pretrained(model_name)

model.cuda()
model.train()

1. AutoTokenizer.from_pretrained(model_name, use_fast=False)

✅ 역할:

  • tokenizer: 텍스트를 숫자로 바꿔주는 모듈. 텍스트 → 토큰 → ID 변환 역할.
  • use_fast=False: Python 기반의 tokenizer 사용을 의미해.

❓왜 use_fast=False?

  • Fast tokenizer는 Rust로 구현된 고속 버전인데, 일부 모델(Qwen 등)은 구조가 특이해서 fast tokenizer가 잘 안 맞거나 형태소 분석 결과가 다르게 나올 수 있음.
  • 따라서 Qwen 계열 모델이나 RoPE, ChatGLM 같은 경우에는 정확한 토크나이징을 위해 use_fast=False를 사용하는 게 안정적.

⚙️ 다른 옵션:

옵션 설명
use_fast=True (기본값) 속도가 빠르고 대부분의 모델에서 사용 가능
trust_remote_code=True 모델이 사용자 정의 코드(custom class)를 사용할 때 필요
revision="main" 특정 버전(branch/tag)의 tokenizer 사용

2. AutoModelForCausalLM.from_pretrained(model_name)

✅ 역할:

  • 모델 구조 및 사전 학습된 가중치를 로드함. 여기서는 CausalLM (Autoregressive LM) 용 구조.
  • Qwen2.5-0.5B-Instruct: GPT처럼 다음 단어를 생성하는 방식에 적합한 구조.

⚙️ 다른 옵션:

옵션 설명
trust_remote_code=True 사용자 정의 모델 구조가 포함된 경우 필수
torch_dtype=torch.float16 메모리 절약을 위해 fp16으로 로드
device_map="auto" 자동으로 GPU/CPU에 모델 분배

3. model.cuda()

✅ 역할:

  • 모델을 GPU로 옮김.
  • 학습 또는 추론 시 계산을 CUDA를 사용하는 GPU에서 수행하도록 함.

❓왜 필요한가?

  • GPU는 병렬 연산에 강하므로, 학습 속도가 훨씬 빠르고 대규모 모델 처리에 필수.
  • .cuda() 없으면 CPU로 동작하고 매우 느림.

⚠️ 주의:

  • 모델이 너무 크면 GPU 메모리를 초과할 수 있음 → 이때는 model.to("cuda:1"), torch_dtype=torch.float16 또는 deepspeed, accelerate 사용.

4. model.train()

✅ 역할:

  • 모델을 training mode로 전환함.
  • dropout, batchnorm 등 학습 중에만 동작해야 할 layer들이 활성화됨.

❓왜 필요한가?

  • eval() 모드는 예측용 → dropout 등 꺼짐
  • train() 모드에서는 모델이 학습 중인 상태임을 PyTorch에 알려줘야 함

🧩 정리

코드 설명 왜 필요한가
use_fast=False 느리지만 정확한 tokenizer 사용 Qwen 계열은 fast tokenizer와 호환 이슈 있을 수 있음
model.cuda() 모델을 GPU로 보냄 빠른 연산과 대규모 모델 처리
model.train() 학습 모드 설정 dropout 등 학습 중에만 작동해야 하는 layer 활성화

🔮 앞으로 참고할 수 있는 옵션 예시

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.float16,         # FP16으로 더 적은 메모리 사용
    trust_remote_code=True             # Qwen처럼 custom 코드 포함된 모델 필수
)
model = model.to("cuda")

🔧 전체 코드 설명 요약

optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
num_epochs = 1
num_training_steps = num_epochs * len(data)

lr_scheduler = get_scheduler(
    name="linear", 
    optimizer=optimizer, 
    num_warmup_steps=0, 
    num_training_steps=num_training_steps
)

1️⃣ Optimizer (최적화 알고리즘)

✅ 정의:

  • 모델의 가중치(weight)를 어떻게 바꿔야 손실(loss)이 줄어들지 계산하고 업데이트하는 알고리즘.
  • 대표적인 방법은 경사 하강법(Gradient Descent) 계열.

📦 torch.optim.AdamW

  • AdamWAdam에서 파생된 옵티마이저로,
  • Wweight decay를 decoupled하게 적용한다는 뜻이야.
  • 일반 Adam은 weight decay와 learning rate가 충돌할 수 있는데, AdamW는 이를 해결함.
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)

⚙️ 주요 옵션

파라미터 설명
lr 학습률 (learning rate): 얼마나 크게 가중치를 업데이트할지
betas (β1, β2) 모멘텀 계수로 이전 gradient의 영향을 얼마나 줄지 결정 (기본값 (0.9, 0.999))
eps 수치 안정성을 위한 작은 값
weight_decay L2 regularization에 해당, overfitting 방지

✅ Optimizer 종류 비교

이름 특징 장점 단점
SGD 가장 기본적인 경사 하강법 간단, 계산량 적음 느리고 최적점에 도달 어려움
SGD + Momentum 과거 gradient 고려 빠르고 안정적 수렴 튜닝 어려움
Adam 모멘텀 + adaptive learning 튜닝 적고 대부분 잘 작동 일반적인 L2 정규화와 충돌 가능
AdamW Adam 개선, L2 정규화 별도 적용 LLM에서 가장 널리 사용됨 여전히 튜닝 필요
RMSProp 제곱된 gradient 평균 사용 비정상적인 loss에 안정적 덜 일반적으로 사용됨

2️⃣ Scheduler (학습률 조절기)

✅ 정의:

학습 중에 lr(learning rate)을 점점 줄이거나 늘려서 더 안정적으로 학습할 수 있게 도와주는 장치.


📦 get_scheduler(name="linear", ...)

HuggingFace의 transformers.optimization.get_scheduler 함수로,
학습률 스케줄을 쉽게 설정할 수 있음.

lr_scheduler = get_scheduler(
    name="linear",              # 선형 감소
    optimizer=optimizer,
    num_warmup_steps=0,         # 학습 초기에 LR을 점점 증가시키는 구간
    num_training_steps=...      # 전체 학습 단계 수
)

📊 대표 스케줄러 종류

이름 설명 특징
linear warmup 후 선형 감소 가장 널리 쓰임
cosine 코사인 곡선처럼 감소 후반에 급격히 줄지 않음
polynomial 다항식으로 감소 학습률 미세 조정 가능
constant 일정하게 유지 실험용 혹은 warmup-only 설정
constant_with_warmup warmup 후 일정 transformer 초기 구조와 유사

🔁 Warmup이란?

  • 학습 초반에 너무 큰 lr을 쓰면 성능이 불안정해짐 → 처음엔 천천히 시작
  • num_warmup_steps: 몇 step 동안 lr을 선형으로 키울지 정함

📌 이 조합의 장단점 정리

항목 장점 단점
AdamW 대부분의 모델에서 잘 작동, 안정적 수렴, L2 정규화와 궁합 좋음 여전히 learning rate 튜닝 필요
Scheduler (linear) 너무 크거나 작은 lr로 인한 불안정성 줄임 적절한 num_training_steps 계산 필요
Warmup 학습 초반 과도한 업데이트 방지 너무 작으면 효과 없음, 너무 크면 비효율적

✅ 보너스: 학습 루프에 사용하는 예시

for epoch in range(num_epochs):
    for batch in dataloader:
        outputs = model(**batch)
        loss = outputs.loss
        loss.backward()
        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()

✨ 마무리

  • optimizer.step(): weight를 업데이트
  • lr_scheduler.step(): 학습률을 업데이트
  • zero_grad(): 이전 gradient를 초기화


✅ 1. nn.CrossEntropyLoss()란?

loss_fn = nn.CrossEntropyLoss()

🔧 역할:

  • 모델이 예측한 확률 분포와 정답 레이블 간의 차이를 계산해 손실값(loss)을 반환함.
  • 다중 클래스 분류에서 주로 사용됨.

🔍 작동 방식:

  • 내부적으로 log_softmax + NLLLoss(Negative Log Likelihood) 조합임.

예시:

logits = torch.tensor([[2.0, 1.0, 0.1]])  # 모델 출력 (3개 클래스)
labels = torch.tensor([0])               # 정답 클래스 인덱스
loss = nn.CrossEntropyLoss()(logits, labels)

📌 2. LLM 학습에 사용되는 주요 손실 함수들

1) 🔹 CrossEntropyLoss (기본)

  • 용도: 토큰 단위 next-token prediction
  • 특징: 다중 클래스 분류에 적합
  • 장점: 학습이 안정적, 널리 검증됨
  • 단점: 단일 토큰 예측만을 목표로 하므로 복합적 표현 학습에 제약

2) 🔹 KL-Divergence Loss (nn.KLDivLoss)

  • 용도: Knowledge Distillation (e.g. 작은 student 모델이 teacher 모델을 모방할 때)
  • 특징: 두 확률 분포 간의 차이 계산
  • 장점: soft label 학습 가능 → 부드러운 분포 반영
  • 단점: 분포 간 0값이나 log 문제로 수치적 불안정 가능

3) 🔹 Cosine Embedding Loss

nn.CosineEmbeddingLoss(input1, input2, target)
  • 용도: 임베딩 유사도 학습 (e.g., 문장 유사도)
  • 특징: 두 벡터 간 코사인 유사도를 학습
  • 장점: 표현 공간을 잘 정렬시켜줌 (semantic retrieval 등에서 강력)
  • 단점: classification에는 직접적으로 부적합

4) 🔹 Contrastive Loss / InfoNCE

  • 용도: dense retrieval, pretraining, encoder 모델 학습 등에서 positive/negative 쌍 구분
  • 특징: anchor와 positive는 가깝게, negative는 멀게 학습
  • 장점: 표현 학습 강화, downstream task에 잘 일반화됨
  • 단점: negative 샘플 품질에 따라 성능 크게 좌우됨

5) 🔹 Perplexity (CrossEntropy의 지수 변환)

  • 용도: LLM 평가용 (loss는 그대로 CrossEntropy 사용)
  • 공식: Perplexity = exp(CrossEntropy)
  • 해석: 낮을수록 모델이 덜 “놀람” → 더 좋은 모델

6) 🔹 Label Smoothing Loss

CrossEntropy + soft target (e.g., [0.9, 0.1] 대신 [0.7, 0.2, 0.1])
  • 용도: 과적합 방지, 일반화 향상
  • 특징: hard label 대신 soft한 분포 사용
  • 장점: 노이즈에 강하고 overconfidence 방지
  • 단점: 학습이 느려질 수 있음

7) 🔹 Language Modeling Loss (특화된 구성)

  • Causal Language Modeling (CLM): nn.CrossEntropyLoss with causal mask → GPT류
  • Masked Language Modeling (MLM): BERT류 모델 → CrossEntropy + mask 적용
  • SpanBERT, ELECTRA Loss: 다양한 pretraining objective 조합

🔍 표로 정리: LLM 학습에 활용 가능한 손실 함수 비교

Loss 이름 사용 목적 장점 단점
CrossEntropyLoss Next-token 예측 안정적, 널리 사용 단일 토큰만 고려
KLDivLoss Knowledge Distillation soft label 학습 가능 수치 불안정
CosineEmbeddingLoss 임베딩 유사도 표현 정렬에 효과적 분류에는 적합하지 않음
Contrastive Loss 표현학습, retrieval 일반화 강함, 쌍 학습에 적합 hard negative 중요
Label Smoothing 과적합 방지 generalization 향상 확률 분포 왜곡 가능
MLM Loss Encoder pretrain 다양한 문맥 학습 가능 복잡도 증가
Perplexity (평가용) 모델 성능 측정 해석 쉬움 실제 학습에는 사용 안 함

✅ 결론: 어떤 Loss를 언제 써야 할까?

상황 추천 Loss
GPT, Qwen 등 LLM 학습 CrossEntropyLoss (Causal LM)
임베딩 기반 QA, 검색 Contrastive Loss, CosineEmbeddingLoss
Distillation KLDivLoss, MSELoss
BERT pretrain MLM + CrossEntropy
학습 안정성 강화 Label Smoothing 추가


🔍 전체 코드 다시 보기

tokens = tokenizer(sample["answer_text"], return_tensors="pt")
labels = tokens.input_ids.cuda()  # [1, seq_len]
eos_id = tokenizer.eos_token_id

labels = torch.cat([labels, torch.tensor([[eos_id]], device=labels.device)], dim=1)  # [1, seq_len+1]

optimizer.zero_grad()
seq_len = labels.size(1)
total_sample_loss = 0.0

🧩 각 줄별 핵심 개념과 설명


tokenizer(..., return_tensors="pt")

tokens = tokenizer(sample["answer_text"], return_tensors="pt")

🧠 개념:

  • 자연어 텍스트(문장)를 토큰 ID로 변환 → 모델이 이해할 수 있게 바꿔줌
  • return_tensors="pt"는 PyTorch 텐서로 반환해주는 옵션

❓왜 필요한가?

  • 모델 입력과 정답은 모두 "숫자 시퀀스"여야 함 (ex: [203, 105, 408, 2])
  • 그래서 학습 전 반드시 토크나이저로 텍스트를 전처리해야 함

labels = tokens.input_ids.cuda()

labels = tokens.input_ids.cuda()  # [1, seq_len]

🧠 개념:

  • 정답 토큰 시퀀스를 GPU로 이동시켜서 이후 연산에서 GPU를 사용할 수 있도록 함

💡 참고:

  • 이후 model(input_ids)의 출력과 이 labels를 비교하여 loss를 계산함

eos_id = tokenizer.eos_token_id

eos_id = tokenizer.eos_token_id

🧠 개념:

  • </s> 또는 <|endoftext|>와 같은 "문장의 끝"을 나타내는 EOS (End Of Sentence) 토큰의 ID

❓왜 필요한가?

  • LLM은 문장이 언제 끝나는지 알려줘야 적절한 위치에서 생성을 멈출 수 있음
  • 정답 레이블의 끝에 이 EOS 토큰이 없으면, 모델은 문장이 계속 이어진다고 착각할 수 있음

torch.cat([labels, eos_token], dim=1)

labels = torch.cat([labels, torch.tensor([[eos_id]], device=labels.device)], dim=1)

🧠 개념:

  • 정답 시퀀스 끝에 EOS를 수동으로 추가함 → [1, 2, 3] → [1, 2, 3, 0]

📌 왜 중요?

  • autoregressive 모델은 한 토큰씩 예측하기 때문에, 마지막 토큰인 EOS도 예측 대상으로 넣어줘야 학습이 완성됨

optimizer.zero_grad()

optimizer.zero_grad()

🧠 개념:

  • 이전 배치에서 계산한 gradient를 초기화

❓왜 필요한가?

  • PyTorch는 기본적으로 gradient를 누적
  • 그래서 매 학습 step마다 zero_grad()로 gradient를 비워줘야 새로운 gradient만 계산됨

🔥 안 하면 생기는 문제:

  • 이전 step의 gradient가 계속 누적되어 weight가 비정상적으로 업데이트됨

seq_len = labels.size(1)

seq_len = labels.size(1)
  • 예측해야 할 시퀀스 길이 계산 → 학습 루프에서 필요

total_sample_loss = 0.0

  • 해당 샘플의 총 loss 초기화 (loop 안에서 누적 예정)

🔄 요약 표

코드 의미 왜 필요한가
tokenizer(..., return_tensors="pt") 텍스트 → 토큰 시퀀스 모델 입력/출력은 숫자여야 함
labels = input_ids.cuda() GPU에 올림 연산 속도 향상
eos_token 추가 문장 끝 표시 언제 멈춰야 하는지 학습함
optimizer.zero_grad() gradient 초기화 이전 배치의 gradient 누적 방지
seq_len 계산 토큰 개수 파악 루프, loss 계산 등에 사용


🔁 코드 전체 다시 보기

outputs = model(inputs_embeds=inputs_embeds, 
                use_cache=False, 
                output_hidden_states=True)

logits = outputs.logits[:, -1, :]       # [batch_size, vocab_size]
target = labels[:, t]                   # 정답 토큰 (인덱스)
step_loss = loss_fn(logits, target)     # CrossEntropyLoss 계산
total_sample_loss += step_loss          # 누적

🧩 1. model(...inputs_embeds...)의 의미

📦 입력: inputs_embeds

  • 일반적으로 모델은 input_ids (토큰 ID)를 받지만,
  • 이미 임베딩된 벡터 (inputs_embeds)를 넣는다면 임베딩 레이어를 생략하고 바로 계산.

🔁 출력: outputs = model(...)

  • transformers 계열 모델의 output은 일반적으로 다음과 같은 ModelOutput 객체:
BaseModelOutputWithPast(
    logits=...,                # 다음 토큰에 대한 분포
    past_key_values=...,       # 캐시 (안 씀)
    hidden_states=...,         # 모든 layer의 출력
    attentions=...             # attention score (옵션)
)

즉, 실제로 여러 정보가 동시에 출력됨.


🧩 2. logits = outputs.logits[:, -1, :]

✅ 설명:

  • logits: 모델이 예측한 "다음 토큰이 무엇일 확률 분포". shape: [batch_size, seq_len, vocab_size]
  • [:, -1, :]: 마지막 위치의 토큰 예측만 뽑음 → 즉, 가장 최근 입력 토큰 다음의 토큰을 예측

예시:

logits.shape = [1, 10, 32000]   # 마지막 토큰 예측 (vocab 크기만큼 확률)
logits[:, -1, :] → shape: [1, 32000]

🧩 3. target = labels[:, t]

  • 현재 시점 t에서의 정답 토큰 인덱스 (예: 1351, 209)
  • labels[batch_size, seq_len] 형태의 토큰 ID 텐서
  • target은 정수 값 하나 → CrossEntropyLoss에 입력될 정답 레이블

🧩 4. step_loss = loss_fn(logits, target)

📦 CrossEntropyLoss 동작 방식:

logits → [batch_size, vocab_size] → softmax → 확률 분포
target → 정답 클래스 인덱스
  • 내부적으로 다음을 수행:
loss = -log(softmax(logits)[target])

✅ 결과:

  • step_loss는 스칼라 값 (float)
  • 이 시점에서 예측한 토큰의 정확도에 따라 loss가 작게 혹은 크게 나옴

🧩 5. total_sample_loss += step_loss

❓왜 누적만 하고 backward()를 안 하는가?

💡 이유 1: 토큰 단위 학습 루프 중이기 때문

  • 이 구조는 전체 문장을 한꺼번에 학습하는 게 아니라,
  • 한 문장에서 매 시점마다 토큰 하나씩 예측하는 auto-regressive loop

예시:

"오늘은 날씨가 좋다"
→ "오" → "늘" → "은" → ... 순차적으로 예측

→ 각 시점의 loss를 누적한 뒤 전체 문장 loss로 처리

💡 이유 2: 전체 문장 학습 단위로 backward() 수행 예정

  • 토큰마다 backward() 하면 너무 많은 메모리를 쓰고, 너무 느려짐
  • 대신 마지막에 아래처럼 한 번만 처리:
total_sample_loss.backward()
optimizer.step()

📌 전체 흐름 요약

단계 코드 설명
입력 inputs_embeds 이미 임베딩된 입력을 모델에 전달
출력 outputs.logits 각 시점 다음 토큰 확률 분포 반환
선택 [:, -1, :] 마지막 토큰 위치의 예측 결과만 사용
정답 labels[:, t] 현재 시점의 정답 토큰 인덱스
계산 CrossEntropyLoss(logits, label) 예측 vs 정답 비교로 loss 계산
누적 += step_loss 전체 문장 loss를 계산하기 위해 누적
추후 backward() 전체 문장에 대한 loss로 역전파 예정


🔁 전체 코드 다시 보기

avg_sample_loss = total_sample_loss / seq_len
avg_sample_loss.backward()
optimizer.step()
lr_scheduler.step()

🔧 1. avg_sample_loss.backward()

✅ 역할:

  • 역전파(Backward Propagation) 수행
  • 손실(loss)을 기준으로 모든 파라미터에 대해 gradient 계산
# 개념적으로는 다음과 같음:
∂loss / ∂W ← 각 layer의 가중치에 대해 미분

📌 주의:

  • 이 단계에서 실제로 파라미터는 바뀌지 않음
  • 단지 .grad 속성에 gradient만 저장됨

🔧 2. optimizer.step()

✅ 역할:

  • .grad에 저장된 gradient 값을 이용해서 모델 파라미터를 업데이트
W = W - lr * ∂loss/∂W
  • 여기서 쓰인 옵티마이저는 AdamW로, L2 regularization도 함께 적용함

🔧 3. lr_scheduler.step()

✅ 역할:

  • 현재 학습 step을 기준으로 learning rate를 조정
lr = scheduler.get_lr(step)
  • 스케줄러는 보통:
    • warmup 후 → 감소 (linear, cosine 등)
    • 또는 cyclical / step decay 방식도 가능

📦 전체 흐름 정리

단계 함수 설명
1 .backward() gradient 계산 (∂loss/∂weight)
2 optimizer.step() 계산된 gradient로 weight 업데이트
3 lr_scheduler.step() 현재 step에 따라 learning rate 조정

✅ 평균 Loss로 나눈 것 맞을까?

avg_sample_loss = total_sample_loss / seq_len

왜 나누는가?

  • auto-regressive loop에서 step_loss를 각 토큰마다 누적했으므로, 전체 문장 길이로 평균 내는 것은 일반적인 처리야.
  • 이렇게 하면 문장 길이에 상관없이 loss scale이 일정해져서 학습 안정화에 도움이 됨.

올바른 처리 방식이야.


🔀 대안 옵션들

1) gradient clipping (폭주 방지)

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
  • gradient가 너무 커서 weight가 이상하게 업데이트되는 것을 방지

2) optimizer.zero_grad() 호출 위치

  • 보통은 loop의 시작 부분에서 호출해야 함
for ...:
    optimizer.zero_grad()   # gradient 초기화
    loss.backward()
    optimizer.step()

※ 만약 zero_grad()가 빠지면 gradient가 누적되어 학습이 망가짐


3) accumulate gradients (메모리 절약)

# 예: 4개 배치마다 한 번만 업데이트
if step % 4 == 0:
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()

→ 특히 큰 모델 학습 시 GPU 메모리가 부족할 때 많이 쓰는 기법


🧠 올바르게 작성되었는가?

항목 설명 문제 없음?
avg_sample_loss.backward() gradient 계산 ✅ OK
optimizer.step() weight 업데이트 ✅ OK
lr_scheduler.step() learning rate 조정 ✅ OK
zero_grad() (루프 시작에 있어야 함) ❗ 확인 필요

💡 주의: 이 코드 바로 앞에 optimizer.zero_grad()가 있어야 해!


✨ 전체적으로 정리

optimizer.zero_grad()             # 🔁 1. 이전 gradient 초기화
total_sample_loss = 0
for t in range(seq_len):
    ...
    step_loss = loss_fn(logits, target)
    total_sample_loss += step_loss

avg_sample_loss = total_sample_loss / seq_len   # 🔁 2. 토큰 평균
avg_sample_loss.backward()                      # 🔁 3. gradient 계산
optimizer.step()                                # 🔁 4. 파라미터 업데이트
lr_scheduler.step()                             # 🔁 5. 학습률 업데이트

728x90

'인공지능 > 공부' 카테고리의 다른 글

mteb 한글 평가하기  (0) 2025.08.29
Multi-GPU 기본 개념 DDP, Data Parallel, Model Parallel  (4) 2025.07.30
Collator란?  (4) 2025.07.29
딥러닝 총 정리  (2) 2025.07.04
딥러닝 공부하기 3  (4) 2025.07.03