In [1]:
import setup_env
from setup_env import device
--------------------------------------------------------------------------------
=== Hardware Acceleration ===
PyTorch version: 2.9.0a0+145a3a7bda.nv25.10
Using NVIDIA GPU (CUDA)
CUDA version: 13.0
GPU name: NVIDIA GeForce RTX 5070 Ti
GPU count: 1
Total GPU memory: 15.92 GB
Allocated memory: 0.00 GB
Free memory: 15.92 GB
Device: cuda
=== Matplotlib Settings ===
✅ Font: NanumGothic
=== System Info ===
OS: Ubuntu 24.04.3 LTS (Noble Numbat)
Kernel: 6.6.87.2-microsoft-standard-WSL2
Architecture: x86_64
Python: 3.12.3
Working directory: /workspace/ai-deeplearning/tutorial
=== Library Versions ===
NumPy: 2.1.0
Pandas: 3.0.0
Matplotlib: 3.10.7
Scikit-learn: 1.7.2
OpenCV: Not installed → !pip install -q opencv-python
Pillow: 12.0.0
Seaborn: 0.13.2
TensorFlow: Not installed → !pip install -q tensorflow
Transformers: 5.2.0
TorchVision: 0.24.0a0+094e7af5
=== Environment setup completed ===
--------------------------------------------------------------------------------
=== Visualizing Test Plot (Wide View) ===
=== GPU Usage Code Snippet === Device set to: cuda ---------------------------------------- # 아래 코드를 복사해서 모델과 데이터를 GPU로 보내세요: model = YourModel().to(device) data = data.to(device) ---------------------------------------- === Environment setup completed === --------------------------------------------------------------------------------
Seq2Seq (Sequence-to-Sequence)¶
시퀀스(순서가 있는 데이터의 나열)를 입력받아 시퀀스를 출력하는 모델
| 예시 | |
|---|---|
| 번역 | "나는 밥을 먹었다" → "I ate rice" |
| 요약 | 긴 문장 → 짧은 문장 |
| 챗봇 | 질문 → 대답 |
핵심 구조: 인코더 → 디코더
- 인코더: 입력 시퀀스 전체를 하나의 벡터로 압축
- 디코더: 그 벡터를 받아 출력 시퀀스를 한 토큰씩 생성
| 연도 | 기술 | 핵심 방식 | 한계 |
|---|---|---|---|
| ~2007 | 규칙 기반 (SYSTRAN) | 언어학자가 문법 규칙 수작업 | 관용어, 문맥, 예외 처리 불가 |
| 2007~2016 | 통계 기반 (SMT) | 대용량 병렬 코퍼스에서 확률 학습 | 구절 단위 번역 → 문맥 유실 |
| 2016.11 | 신경망 번역 (GNMT) | Seq2Seq + Attention, 문장 전체 처리 | LSTM 순차처리 → 병렬화 불가, 긴 문장 약함 |
| 2017 | Transformer | Attention Is All You Need, LSTM 제거 | 추론 속도 문제 |
opus100 데이터셋¶
100개 언어 쌍의 병렬 번역 문장 쌍 모음
- 출처: OPUS 프로젝트 (공개 다국어 코퍼스)
- Hugging Face에서 바로 로드 가능
from datasets import load_dataset
ds = load_dataset("opus100", "en-ko")
데이터 구조¶
| 컬럼 | 예시 |
|---|---|
translation.en |
"I ate rice with a friend yesterday." |
translation.ko |
"나는 어제 친구와 밥을 먹었다." |
데이터 크기¶
| split | 문장 쌍 수 |
|---|---|
| train | 1,000,000 |
| validation | 2,000 |
| test | 2,000 |
검증 방식¶
입력(영어) → 모델 → 예측(한국어)를 정답(한국어)과 BLEU로 비교
입력: "I ate rice"
정답: "나는 밥을 먹었다"
예측: "나는 밥을 먹었어"
BLEU: 부분 일치 점수 계산
왜 영→한인가?¶
결과를 우리가 직접 눈으로 보고 판단할 수 있기 때문. BLEU 점수 + 육안 확인 두 가지로 검증 가능.
In [2]:
import os
from datasets import load_dataset
data_dir = "./data/opus100_en_ko"
if os.path.exists(data_dir):
print("이미 존재합니다. 스킵합니다.")
else:
print("다운로드 중...")
ds = load_dataset("opus100", "en-ko")
ds.save_to_disk(data_dir)
print("완료:", data_dir)
이미 존재합니다. 스킵합니다.
In [3]:
from datasets import load_from_disk
ds = load_from_disk("./data/opus100_en_ko")
print("데이터셋 구조:")
print(ds)
print("\n--- train 샘플 3개 ---")
for i in range(3):
pair = ds["train"][i]["translation"]
print(f"EN: {pair['en']}")
print(f"KO: {pair['ko']}")
print()
데이터셋 구조:
DatasetDict({
test: Dataset({
features: ['translation'],
num_rows: 2000
})
train: Dataset({
features: ['translation'],
num_rows: 1000000
})
validation: Dataset({
features: ['translation'],
num_rows: 2000
})
})
--- train 샘플 3개 ---
EN: They're shaped like a bus.
KO: 할머니처럼 만들었지만.. ? 엉망이지만..
EN: I ain't fishing' 'em out.
KO: 그거 꺼내려다가는
EN: You are torturing god's creatures in an age where we have the technology that no longer requires us to.
KO: 선생님은 이 기술력이 있는 시대에 그러지 않아도 되는데도 신의 피조물을 괴롭히고 있다고요
NLLB-200 (No Language Left Behind)¶
Meta AI, 2022
"언어 장벽 없이 200개 언어를 누구나 번역할 수 있게"
특징¶
| 내용 | |
|---|---|
| 지원 언어 | 200개 |
| 모델 크기 | 600M / 1.3B / 3.3B |
| 구조 | Transformer 인코더-디코더 |
| 학습 데이터 | 850억 개 문장 쌍 |
기존 모델과 비교¶
| 모델 | 출시 | 지원 언어 | 영→한 품질 |
|---|---|---|---|
| GNMT (Google) | 2016 | 103개 | 보통 |
| M2M-100 (Meta) | 2020 | 100개 | 좋음 |
| NLLB-200 (Meta) | 2022 | 200개 | 최고 |
핵심 차별점¶
- 저자원 언어(아프리카, 동남아 등)도 고품질 번역
- 영어를 거치지 않고 언어 간 직접 번역
- Hugging Face에서 바로 사용 가능
from transformers import pipeline
translator = pipeline("translation", model="facebook/nllb-200-distilled-600M")
translator("Hello, how are you?", src_lang="eng_Latn", tgt_lang="kor_Hang")
In [4]:
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
import os
model_dir = "./data/nllb-200-distilled-600M"
if os.path.exists(model_dir):
print("이미 존재합니다. 스킵합니다.")
else:
print("다운로드 중...")
tokenizer = AutoTokenizer.from_pretrained("facebook/nllb-200-distilled-600M")
model_nllb = AutoModelForSeq2SeqLM.from_pretrained("facebook/nllb-200-distilled-600M")
tokenizer.save_pretrained(model_dir)
model_nllb.save_pretrained(model_dir)
print("완료:", model_dir)
이미 존재합니다. 스킵합니다.
In [5]:
# 1. 모델 & 토크나이저 로드
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
import torch
model_dir = "./data/nllb-200-distilled-600M"
tokenizer_nllb = AutoTokenizer.from_pretrained(model_dir)
model_nllb = AutoModelForSeq2SeqLM.from_pretrained(model_dir).to(device)
# 2. 번역 함수
def translate_nllb(sentence, src_lang="eng_Latn", tgt_lang="kor_Hang"):
inputs = tokenizer_nllb(sentence, return_tensors="pt").to(device)
translated = model_nllb.generate(
**inputs,
forced_bos_token_id=tokenizer_nllb.lang_code_to_id[tgt_lang],
max_length=200
)
return tokenizer_nllb.decode(translated[0], skip_special_tokens=True)
In [9]:
from torch.utils.data import Dataset, DataLoader
class NLLBDataset(Dataset):
def __init__(self, data, tokenizer, src_lang="eng_Latn", tgt_lang="kor_Hang", max_len=128):
self.data = data
self.tokenizer = tokenizer
self.src_lang = src_lang
self.tgt_lang = tgt_lang
self.max_len = max_len
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
pair = self.data[idx]["translation"]
# 입력 토크나이징
self.tokenizer.src_lang = self.src_lang
inputs = self.tokenizer(
pair["en"], max_length=self.max_len, truncation=True, padding="max_length", return_tensors="pt"
)
# 타겟 토크나이징 (tgt_lang 명시)
self.tokenizer.src_lang = self.tgt_lang
labels = self.tokenizer(
pair["ko"], max_length=self.max_len, truncation=True, padding="max_length", return_tensors="pt"
)
label_ids = labels["input_ids"].squeeze()
label_ids[label_ids == self.tokenizer.pad_token_id] = -100
return {
"input_ids": inputs["input_ids"].squeeze(),
"attention_mask": inputs["attention_mask"].squeeze(),
"labels": label_ids
}
# DataLoader 재생성
train_nllb = NLLBDataset(ds["train"].select(range(300000)), tokenizer_nllb)
valid_nllb = NLLBDataset(ds["validation"], tokenizer_nllb)
train_loader_nllb = DataLoader(train_nllb, batch_size=16, shuffle=True, num_workers=4)
valid_loader_nllb = DataLoader(valid_nllb, batch_size=16, shuffle=False, num_workers=4)
Fine-tuning 방법 비교¶
| 방법 | 학습 파라미터 | 학습 속도 | 메모리 사용량 | 예상 성능 |
|---|---|---|---|---|
| Pre-trained (Base) | 0% (없음) | N/A (즉시 실행) | 최소 (추론만) | 기준점 (Baseline) |
| Full Fine-tuning | 100% | 느림 | 매우 많음 | 최고 |
| Last N Layers | 일부 (상위층) | 보통 | 보통 | 좋음 |
| LoRA | 1~5% | 빠름 | 적음 | Full FT와 거의 동등 |
LoRA (Low-Rank Adaptation)¶
기존 가중치는 얼리고(freeze), 작은 어댑터 행렬만 학습.
기존 Full Fine-tuning:
W (기존 가중치) → W + ΔW 전체 업데이트
LoRA:
W (freeze) + A × B (작은 행렬만 학습)
A: (d × r), B: (r × d) ← r이 작을수록 가볍다 (보통 r=8~16)
요즘 GPT, LLaMA 등 대형 모델 fine-tuning 표준 방법.
In [17]:
import torch
from tqdm.auto import tqdm
# 1. 평가 모드 선언 (가중치 고정 및 드롭아웃 비활성화)
model_nllb.eval()
all_sources = []
all_predictions = []
all_targets = []
# 3. 그래디언트 계산 비활성화 (가중치 업데이트를 원천 차단하고 메모리 절약)
with torch.no_grad():
# 검증 데이터셋(valid_loader_nllb)에서 샘플을 가져옵니다
for batch in tqdm(valid_loader_nllb, desc="Pre-trained 모델 측정 중"):
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
all_sources.extend(tokenizer_nllb.batch_decode(input_ids, skip_special_tokens=True))
labels = batch['labels'].to(device)
# 번역 생성 (가중치 고정 상태 유지)
generated_tokens = model_nllb.generate(
input_ids=input_ids,
attention_mask=attention_mask,
# [수정] 에러 해결을 위해 메서드 변경
forced_bos_token_id=tokenizer_nllb.convert_tokens_to_ids("kor_Hang"),
max_length=200
)
# 결과 해독 및 저장
decoded_preds = tokenizer_nllb.batch_decode(generated_tokens, skip_special_tokens=True)
# 정답(Label) 해독 (패딩인 -100은 제외하고 해독)
labels[labels == -100] = tokenizer_nllb.pad_token_id
decoded_labels = tokenizer_nllb.batch_decode(labels, skip_special_tokens=True)
all_predictions.extend(decoded_preds)
all_targets.extend(decoded_labels)
# 결과 확인 (샘플 5개만 출력)
print("\n=== Pre-trained (Base) 결과 샘플 ===")
for i in range(5):
print(f"원본(EN): {all_sources[i]}")
print(f"정답(KO): {all_targets[i]}")
print(f"모델(KO): {all_predictions[i]}")
print("-" * 30)
Pre-trained 모델 측정 중: 100%|██████████| 16/16 [12:21<00:00, 46.32s/it]
=== Pre-trained (Base) 결과 샘플 === 원본(EN): Yeah, a lot of it. 정답(KO): 네, 무척요. 모델(KO): - 그래, 많이요 ------------------------------ 원본(EN): I'll set up some tests. Shep, 정답(KO): 날 뚫어지게 쳐다보는데 그만 해요 모델(KO): 몇 가지 테스트를 할게요 ------------------------------ 원본(EN): Look, I don't like it any more than you do, but if you help me, I promise to keep you safe. 정답(KO): 이봐 나도 너만큼 안 내켜 그래도 날 도우면 내가 보호해주지 모델(KO): 난 너보다 싫어하지만 네가 도와준다면 널 안전하게 지켜줄게 ------------------------------ 원본(EN): Like, what does that even mean? 정답(KO): 뭔 뜻이야? 모델(KO): 그게 무슨 뜻일까요? ------------------------------ 원본(EN): She becomes the story. 정답(KO): 리즈가 영웅이 되고 있어요 모델(KO): 그녀는 이야기가 될 것입니다. ------------------------------
In [19]:
import torch
from tqdm.auto import tqdm
from nltk.translate.bleu_score import corpus_bleu
model_nllb.eval()
all_sources, all_predictions, all_targets = [], [], []
tgt_id = tokenizer_nllb.convert_tokens_to_ids("kor_Hang")
with torch.no_grad():
for batch in tqdm(valid_loader_nllb, desc="Baseline 측정"):
input_ids = batch['input_ids'].to(device)
gen = model_nllb.generate(input_ids=input_ids, attention_mask=batch['attention_mask'].to(device), forced_bos_token_id=tgt_id, max_length=200)
all_sources.extend(tokenizer_nllb.batch_decode(input_ids, skip_special_tokens=True))
all_predictions.extend(tokenizer_nllb.batch_decode(gen, skip_special_tokens=True))
labels = batch['labels'].clone(); labels[labels == -100] = tokenizer_nllb.pad_token_id
all_targets.extend(tokenizer_nllb.batch_decode(labels, skip_special_tokens=True))
# 점수 계산 및 샘플 출력
score = corpus_bleu([[t.split()] for t in all_targets], [p.split() for p in all_predictions]) * 100
print(f"\nBaseline BLEU Score: {score:.2f}")
for i in range(5): print(f"EN: {all_sources[i]}\nKO 정답: {all_targets[i]}\nKO 모델: {all_predictions[i]}\n{'-'*30}")
Baseline 측정: 100%|██████████| 16/16 [12:07<00:00, 45.45s/it]
Baseline BLEU Score: 0.88 EN: Yeah, a lot of it. KO 정답: 네, 무척요. KO 모델: - 그래, 많이요 ------------------------------ EN: I'll set up some tests. Shep, KO 정답: 날 뚫어지게 쳐다보는데 그만 해요 KO 모델: 몇 가지 테스트를 할게요 ------------------------------ EN: Look, I don't like it any more than you do, but if you help me, I promise to keep you safe. KO 정답: 이봐 나도 너만큼 안 내켜 그래도 날 도우면 내가 보호해주지 KO 모델: 난 너보다 싫어하지만 네가 도와준다면 널 안전하게 지켜줄게 ------------------------------ EN: Like, what does that even mean? KO 정답: 뭔 뜻이야? KO 모델: 그게 무슨 뜻일까요? ------------------------------ EN: She becomes the story. KO 정답: 리즈가 영웅이 되고 있어요 KO 모델: 그녀는 이야기가 될 것입니다. ------------------------------
🚀 LoRA (Low-Rank Adaptation) 요약¶
1. 정체 및 개발자¶
- 개발: 2021년 마이크로소프트(Microsoft) 연구진이 발표.
- 정의: 거대 모델을 효율적으로 튜닝하기 위한 저차원 행렬 분해 알고리즘.
2. 작동 원리 (핵심 알고리즘)¶
기존 가중치 $W_0$는 절대 건드리지 않고(Freeze), 두 개의 작은 행렬 $A, B$만 학습시켜 나중에 더합니다. $$W = W_0 + \Delta W = W_0 + BA$$
3. 주요 특징¶
- 효율성: 전체 파라미터의 1% 미만만 학습하여 메모리/시간 대폭 절약.
- 성능: 모든 가중치를 수정하는 Full Fine-tuning과 거의 동등한 성능.
- Rank ($r$): 행렬의 두께를 조절하는 핵심 하이퍼파라미터 (보통 8, 16 사용).
In [7]:
from peft import LoraConfig, get_peft_model, TaskType
peft_config = LoraConfig(
task_type=TaskType.SEQ_2_SEQ_LM,
inference_mode=False,
r=32, # 10만+ 데이터 → 높은 rank 충분히 활용 가능
lora_alpha=64, # alpha = r * 2 공식 유지
lora_dropout=0.05, # 데이터 많으므로 낮게
# 인코더 + 디코더 전체 어텐션 + FFN 커버
target_modules=[
"q_proj", "k_proj", "v_proj", "out_proj", # 전체 어텐션
"fc1", "fc2" # FFN (번역 품질 핵심)
],
bias="none",
)
model_nllb = get_peft_model(model_nllb, peft_config)
model_nllb.print_trainable_parameters()
trainable params: 17,301,504 || all params: 632,375,296 || trainable%: 2.7360
In [10]:
import torch
import torch.optim as optim
from transformers import get_cosine_schedule_with_warmup
from tqdm.auto import tqdm
import os, time
from datetime import timedelta
torch.cuda.empty_cache()
accumulation_steps = 4
optimizer = optim.AdamW(
model_nllb.parameters(),
lr=5e-5,
weight_decay=0.01
)
total_steps = len(train_loader_nllb) // accumulation_steps
scheduler = get_cosine_schedule_with_warmup(
optimizer,
num_warmup_steps=int(total_steps * 0.06),
num_training_steps=total_steps
)
model_nllb.train()
progress_bar = tqdm(train_loader_nllb, desc="LoRA Training")
optimizer.zero_grad()
total_start = time.time()
step_start = time.time()
for i, batch in enumerate(progress_bar):
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['labels'].to(device)
outputs = model_nllb(input_ids=input_ids, attention_mask=attention_mask, labels=labels)
loss = outputs.loss / accumulation_steps
loss.backward()
if (i + 1) % accumulation_steps == 0:
torch.nn.utils.clip_grad_norm_(model_nllb.parameters(), max_norm=1.0)
optimizer.step()
scheduler.step()
optimizer.zero_grad()
step_elapsed = time.time() - step_start
total_elapsed = time.time() - total_start
progress_bar.set_postfix({
'loss' : f"{loss.item() * accumulation_steps:.4f}",
'lr' : f"{scheduler.get_last_lr()[0]:.2e}",
'step_time': f"{step_elapsed:.2f}s",
'total' : str(timedelta(seconds=int(total_elapsed)))
})
step_start = time.time()
total_elapsed = time.time() - total_start
print(f"\n✅ 학습 완료 | 총 시간: {timedelta(seconds=int(total_elapsed))}")
# 모델 저장
save_path = "./data/lora_nllb_model"
os.makedirs(save_path, exist_ok=True)
save_start = time.time()
model_nllb.save_pretrained(save_path)
tokenizer_nllb.save_pretrained(save_path)
print(f"💾 저장 완료: {save_path} ({time.time()-save_start:.2f}s)")
LoRA Training: 100%|██████████| 18750/18750 [3:27:14<00:00, 1.51it/s, loss=3.0595, lr=0.00e+00, step_time=2.67s, total=3:27:12]
✅ 학습 완료 | 총 시간: 3:27:14 💾 저장 완료: ./data/lora_nllb_model (0.69s)
In [ ]:
from peft import PeftModel
from transformers import AutoTokenizer
model_nllb = PeftModel.from_pretrained(base_model, "./data/lora_nllb_model")
tokenizer_nllb = AutoTokenizer.from_pretrained("./data/lora_nllb_model")
In [11]:
import torch
from tqdm.auto import tqdm
from nltk.translate.bleu_score import corpus_bleu
model_nllb.eval()
all_sources, all_predictions, all_targets = [], [], []
tgt_id = tokenizer_nllb.convert_tokens_to_ids("kor_Hang")
with torch.no_grad():
for batch in tqdm(valid_loader_nllb, desc="Baseline 측정"):
input_ids = batch['input_ids'].to(device)
gen = model_nllb.generate(input_ids=input_ids, attention_mask=batch['attention_mask'].to(device), forced_bos_token_id=tgt_id, max_length=200)
all_sources.extend(tokenizer_nllb.batch_decode(input_ids, skip_special_tokens=True))
all_predictions.extend(tokenizer_nllb.batch_decode(gen, skip_special_tokens=True))
labels = batch['labels'].clone(); labels[labels == -100] = tokenizer_nllb.pad_token_id
all_targets.extend(tokenizer_nllb.batch_decode(labels, skip_special_tokens=True))
# 점수 계산 및 샘플 출력
score = corpus_bleu([[t.split()] for t in all_targets], [p.split() for p in all_predictions]) * 100
print(f"\nBaseline BLEU Score: {score:.2f}")
for i in range(5): print(f"EN: {all_sources[i]}\nKO 정답: {all_targets[i]}\nKO 모델: {all_predictions[i]}\n{'-'*30}")
Baseline 측정: 100%|██████████| 125/125 [01:43<00:00, 1.20it/s]
Baseline BLEU Score: 1.85 EN: Yeah, a lot of it. KO 정답: 네, 무척요. KO 모델: 그래, 많이요 ------------------------------ EN: I'll set up some tests. Shep, KO 정답: 날 뚫어지게 쳐다보는데 그만 해요 KO 모델: 검사 좀 할게요 ------------------------------ EN: Look, I don't like it any more than you do, but if you help me, I promise to keep you safe. KO 정답: 이봐 나도 너만큼 안 내켜 그래도 날 도우면 내가 보호해주지 KO 모델: 난 너보다 싫어하지만 네가 도와준다면 널 지켜줄게 ------------------------------ EN: Like, what does that even mean? KO 정답: 뭔 뜻이야? KO 모델: 그게 무슨 뜻인지? ------------------------------ EN: She becomes the story. KO 정답: 리즈가 영웅이 되고 있어요 KO 모델: 그녀는 이야기로 변한다 ------------------------------
In [ ]:
In [ ]:
In [ ]:
In [ ]:
In [ ]:
In [ ]:
In [ ]:
In [ ]:
In [ ]:
In [ ]:
In [2]:
!pip install -q --upgrade transformers peft accelerate
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
In [1]:
# pip install peft
from peft import get_peft_model, LoraConfig, TaskType
# 1. LoRA 설정
lora_config = LoraConfig(
task_type=TaskType.SEQ_2_SEQ_LM,
r=16, # rank
lora_alpha=32, # 스케일링
lora_dropout=0.1,
target_modules=["q_proj", "v_proj"] # Attention Q, V에만 적용
)
# 2. 모델에 LoRA 적용
model_lora = get_peft_model(model_nllb, lora_config)
model_lora.print_trainable_parameters() # 학습 파라미터 확인
/usr/local/lib/python3.12/dist-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[1], line 14 5 lora_config = LoraConfig( 6 task_type=TaskType.SEQ_2_SEQ_LM, 7 r=16, # rank (...) 10 target_modules=["q_proj", "v_proj"] # Attention Q, V에만 적용 11 ) 13 # 2. 모델에 LoRA 적용 ---> 14 model_lora = get_peft_model(model_nllb, lora_config) 15 model_lora.print_trainable_parameters() # 학습 파라미터 확인 NameError: name 'model_nllb' is not defined