Transfer Learning으로 100배 적은 데이터로 SOTA 달성 – 거인의 어깨 위에서 AI 만들기


핵심 요약

“데이터 100만 개 없으면 AI 못 만드나요?” 아닙니다.

Transfer Learning(전이 학습)을 사용하면 1,000개만으로도 최고 성능을 달성할 수 있습니다. 핵심은 ImageNet 1,400만 장으로 학습된 ResNet, 수십억 토큰으로 학습된 BERT/GPT 같은 사전학습 모델(Pretrained Model)의 지식을 재활용하는 것입니다.

처음부터 학습(From Scratch)하면 수개월 걸리고 GPU 수천 개가 필요하지만, Fine-tuning은 몇 시간, GPU 1개면 충분합니다. 실전에서 Feature Extraction사전학습 모델을 고정하고 마지막 분류기만 학습하며, Fine-tuning전체 또는 일부 가중치를 미세조정합니다. LoRA(Low-Rank Adaptation)QLoRA파라미터 1%만 튜닝해서 메모리 75% 절감하며, Hugging Face50만 개 이상의 사전학습 모델을 무료 제공합니다.

실제로 의료 AI는 ImageNet 모델로 100개 데이터만으로 90% 정확도, 법률 챗봇은 GPT-3.5 Fine-tuning으로 3일 만에 구축, Tesla 자율주행은 시뮬레이션→실도로 Transfer로 사고율 90% 감소를 달성했습니다.

본 포스팅에서는 Transfer Learning 원리, Feature Extraction vs Fine-tuning, BERT/GPT 실전 코드, LoRA/PEFT 최신 기법, 도메인별 전략까지 전문가와 입문자 모두를 위한 완벽 가이드를 제공합니다.


📍 목차

  1. Transfer Learning이란? – 거인의 어깨 위에 서기
  2. Feature Extraction vs Fine-tuning
  3. 사전학습 모델 선택 가이드
  4. Fine-tuning 실전 코드 (이미지 + NLP)
  5. LoRA와 PEFT – 효율적 Fine-tuning
  6. 도메인별 적용 전략과 사례

1. Transfer Learning이란? – 거인의 어깨 위에 서기

1-1. 핵심 개념

정의:

이미 학습된 모델(사전학습 모델)의 지식을
새로운 문제에 재활용하는 기법

비유:
- 영어 능통자가 스페인어 빨리 배우듯
- 기존 지식을 새 문제에 전이(Transfer)

아이작 뉴턴:
"내가 더 멀리 봤다면, 거인의 어깨 위에 섰기 때문이다"
→ AI도 거인(대규모 모델)의 어깨 위에!

1-2. 왜 Transfer Learning이 혁명적인가?

처음부터 학습 (From Scratch):

ImageNet 분류 모델 학습:
- 데이터: 1,400만 장 이미지
- GPU: 8 × V100 (약 1억원)
- 시간: 2주
- 전기료: 수백만원

GPT-3 학습:
- 데이터: 570GB 텍스트
- GPU: 수만 개 (약 1,000억원)
- 시간: 수개월
- 전기료: 수십억원

→ 일반인/기업은 불가능!

Transfer Learning 사용:

같은 성능 달성:
- 데이터: 1,000~10,000개
- GPU: 1개 (RTX 3090)
- 시간: 수 시간
- 비용: 전기료 수천원

절감 효과:
- 데이터: 99.9% 절감
- 비용: 99.99% 절감
- 시간: 99% 절감

→ 누구나 최고 성능 AI 가능!

1-3. Transfer Learning의 3가지 유형

1. Inductive Transfer Learning:

정의: 같은 도메인, 다른 태스크

예시:
- 소스: ImageNet 이미지 분류 (1,000 클래스)
- 타겟: 의료 이미지 분류 (암 유무)

특징:
- 가장 일반적인 형태
- Fine-tuning 주로 사용
- 높은 성능 달성

2. Transductive Transfer Learning:

정의: 같은 태스크, 다른 도메인

예시:
- 소스: 영어 감정 분석
- 타겟: 한국어 감정 분석

특징:
- 도메인 적응(Domain Adaptation)
- 라벨 없는 타겟 데이터 활용
- 크로스링구얼(Cross-lingual) 전이

3. Unsupervised Transfer Learning:

정의: 라벨 없이 전이

예시:
- 소스: 라벨 없는 대규모 이미지
- 타겟: 라벨 없는 특정 도메인 이미지

특징:
- Self-supervised Learning 기반
- BERT, GPT의 사전학습 방식
- 최신 연구 활발

1-4. Transfer Learning 성공의 핵심 원리

왜 작동하는가?

계층적 특징 학습:

CNN의 경우:
Layer 1-2: 엣지, 색상 (범용)
Layer 3-4: 텍스처, 패턴 (범용)
Layer 5-6: 부분 객체 (준범용)
Layer 7+: 전체 객체 (태스크 특화)

핵심 인사이트:
- 하위 층: 범용 특징 → 재사용 가치 높음
- 상위 층: 특화 특징 → 새로 학습 필요

Transformer의 경우:
하위 층: 문법, 구문 구조 (범용)
중간 층: 의미, 관계 (준범용)
상위 층: 태스크 특화 표현 (특화)

Transfer 가능성 조건:

성공 조건:
✅ 소스-타겟 도메인 유사성
✅ 소스 데이터 충분히 다양
✅ 사전학습 모델 충분히 큼
✅ 태스크 관련성

실패 조건 (Negative Transfer):
❌ 도메인 너무 다름 (자연 → 의료)
❌ 소스 데이터 편향
❌ 잘못된 층 선택
❌ 과도한 Fine-tuning

2. Feature Extraction vs Fine-tuning

2-1. Feature Extraction (특징 추출)

정의:

사전학습 모델의 가중치를 고정(Freeze)하고
마지막 분류기만 새로 학습

모델 구조:
[사전학습 백본] → [새 분류기]
   (고정)           (학습)

비유:
- 카메라 렌즈(백본)는 그대로 사용
- 필름(분류기)만 교체

Python 구현 (PyTorch):

import torch
import torch.nn as nn
import torchvision.models as models
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# 1. 사전학습 모델 로드
model = models.resnet50(pretrained=True)

# 2. 모든 파라미터 고정 (Freeze)
for param in model.parameters():
    param.requires_grad = False

# 3. 마지막 분류기만 교체
num_features = model.fc.in_features
num_classes = 10  # 새로운 클래스 수

model.fc = nn.Sequential(
    nn.Linear(num_features, 256),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(256, num_classes)
)

# 4. 학습할 파라미터 확인
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in model.parameters())

print(f"학습 가능 파라미터: {trainable_params:,} / {total_params:,}")
print(f"비율: {trainable_params/total_params*100:.2f}%")

# 출력:
# 학습 가능 파라미터: 526,858 / 23,534,154
# 비율: 2.24%

# 5. 학습 설정
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()

# 6. 학습 루프
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

for epoch in range(10):
    model.train()
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)

        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

    # 검증
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for images, labels in val_loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    print(f"Epoch {epoch+1}: 정확도 {100*correct/total:.2f}%")

Feature Extraction 장단점:

장점:
✅ 매우 빠른 학습 (파라미터 2%만 학습)
✅ 적은 GPU 메모리
✅ 과적합 위험 낮음
✅ 매우 적은 데이터에도 작동

단점:
❌ 성능 한계 (백본 고정)
❌ 도메인 차이 크면 성능 저하
❌ 태스크 특화 불가

적합한 경우:
- 데이터 매우 적음 (

2-2. Fine-tuning (미세 조정)

정의:

사전학습 모델의 가중치를 새 데이터로 업데이트

3가지 전략:
1. 전체 Fine-tuning: 모든 층 학습
2. 부분 Fine-tuning: 상위 층만 학습
3. 점진적 Fine-tuning: 단계적으로 층 해제

비유:
- 기존 요리 레시피를 맛에 맞게 조정
- 기본은 유지하면서 미세하게 변경

Python 구현 (부분 Fine-tuning):

import torch
import torch.nn as nn
import torchvision.models as models

# 1. 사전학습 모델 로드
model = models.resnet50(pretrained=True)

# 2. 전체 고정
for param in model.parameters():
    param.requires_grad = False

# 3. 마지막 층(layer4)만 해제
for param in model.layer4.parameters():
    param.requires_grad = True

# 4. 분류기 교체
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, 10)

# 5. 학습 가능 파라미터 확인
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"학습 비율: {trainable/total*100:.2f}%")
# 출력: 학습 비율: 35.47%

# 6. 차등 학습률 설정 (중요!)
optimizer = torch.optim.Adam([
    {'params': model.layer4.parameters(), 'lr': 1e-4},  # 낮은 LR
    {'params': model.fc.parameters(), 'lr': 1e-3}       # 높은 LR
])

# 이유: 사전학습된 가중치는 조심스럽게 조정
#       새 분류기는 처음부터 학습

# 7. 학습
criterion = nn.CrossEntropyLoss()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

for epoch in range(20):
    model.train()
    running_loss = 0.0

    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)

        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()

        # Gradient Clipping (선택적)
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

        optimizer.step()
        running_loss += loss.item()

    print(f"Epoch {epoch+1}: Loss = {running_loss/len(train_loader):.4f}")

점진적 Fine-tuning (Gradual Unfreezing):

def gradual_unfreeze(model, epoch, unfreeze_schedule):
    """
    에폭에 따라 점진적으로 층 해제

    unfreeze_schedule = {
        5: 'layer4',   # 5 에폭부터 layer4 해제
        10: 'layer3',  # 10 에폭부터 layer3도 해제
        15: 'layer2',  # 15 에폭부터 layer2도 해제
    }
    """
    for unfreeze_epoch, layer_name in unfreeze_schedule.items():
        if epoch >= unfreeze_epoch:
            layer = getattr(model, layer_name)
            for param in layer.parameters():
                param.requires_grad = True
            print(f"Epoch {epoch}: {layer_name} unfrozen")

# 사용 예시
unfreeze_schedule = {5: 'layer4', 10: 'layer3', 15: 'layer2'}

for epoch in range(30):
    gradual_unfreeze(model, epoch, unfreeze_schedule)

    # 학습률 재설정 (층 해제 시)
    if epoch in unfreeze_schedule:
        optimizer = torch.optim.Adam(
            filter(lambda p: p.requires_grad, model.parameters()),
            lr=1e-4
        )

    # 학습 진행...

2-3. Feature Extraction vs Fine-tuning 비교

기준              | Feature Extraction | Fine-tuning
─────────────────────────────────────────────────────
학습 파라미터    | 1-5%              | 20-100%
학습 시간        | 빠름 (분~시간)     | 느림 (시간~일)
GPU 메모리       | 적음              | 많음
필요 데이터      | 적음 (100~1,000)  | 중간 (1,000~10,000)
성능 상한        | 제한적            | 높음
과적합 위험      | 낮음              | 높음
도메인 차이 대응 | 어려움            | 가능
─────────────────────────────────────────────────────
추천 상황        | 데이터 적음       | 데이터 충분
                 | 도메인 유사       | 도메인 다름
                 | 빠른 개발         | 최고 성능 필요

3. 사전학습 모델 선택 가이드

3-1. 컴퓨터 비전 (이미지)

주요 사전학습 모델:

1. ResNet (2015):
   - 깊은 네트워크 가능 (Skip Connection)
   - ResNet-50, ResNet-101, ResNet-152
   - ImageNet 정확도: 76-78%
   - 범용적, 안정적

2. EfficientNet (2019):
   - NAS로 발견된 최적 구조
   - EfficientNet-B0 ~ B7
   - ImageNet 정확도: 77-84%
   - 효율적 (성능/파라미터 비율 최고)

3. Vision Transformer (ViT, 2020):
   - Transformer 기반 이미지 모델
   - ViT-B/16, ViT-L/16
   - ImageNet 정확도: 77-87%
   - 대규모 데이터에 강함

4. ConvNeXt (2022):
   - CNN의 현대화 버전
   - ViT 성능에 근접
   - 학습 안정적

선택 기준:

데이터  10,000개:
→ ViT-B/16 + Full Fine-tuning

효율성 중시:
→ EfficientNet (모바일 배포)

최고 성능:
→ ViT-L/16 또는 ConvNeXt-L

3-2. 자연어처리 (NLP)

주요 사전학습 모델:

1. BERT (2018, Google):
   - 양방향 Transformer Encoder
   - Masked Language Modeling
   - BERT-base (110M), BERT-large (340M)
   - 분류, NER, QA에 최적

2. GPT-2/3/4 (OpenAI):
   - 단방향 Transformer Decoder
   - Autoregressive Language Modeling
   - GPT-2 (1.5B), GPT-3 (175B), GPT-4 (추정 1.7T)
   - 생성 태스크에 최적

3. RoBERTa (2019, Meta):
   - BERT 학습 방식 최적화
   - NSP 제거, 더 많은 데이터
   - BERT보다 대부분 성능 우수

4. T5 (2020, Google):
   - Text-to-Text 통합 프레임워크
   - 모든 NLP 태스크를 텍스트 생성으로
   - 범용성 높음

5. LLaMA (2023, Meta):
   - 오픈소스 LLM
   - 7B, 13B, 65B 파라미터
   - 상업적 사용 가능

한국어 모델:

1. KoBERT (SKT):
   - 한국어 BERT
   - 한국어 위키 + 뉴스

2. KoGPT (Kakao):
   - 한국어 GPT
   - 6B 파라미터

3. KoELECTRA:
   - 효율적인 사전학습
   - 적은 자원으로 좋은 성능

4. KoAlpaca:
   - LLaMA 기반 한국어 모델
   - 오픈소스

선택 기준:

텍스트 분류/NER:
→ BERT, RoBERTa, KoBERT

질의응답:
→ BERT, T5

텍스트 생성:
→ GPT-2/3, LLaMA, KoGPT

요약:
→ T5, BART

대화:
→ GPT-3.5/4, LLaMA + Chat Tuning

3-3. Hugging Face 모델 허브 활용

from transformers import AutoModel, AutoTokenizer

# 모델 검색 및 로드
model_name = "bert-base-multilingual-cased"

# 자동으로 적합한 모델 클래스 로드
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)

# 한국어 모델 예시
korean_models = [
    "monologg/kobert",           # KoBERT
    "beomi/KcELECTRA-base",      # KcELECTRA
    "kykim/bert-kor-base",       # 한국어 BERT
    "gogamza/kobart-base-v2",    # KoBART
]

# 태스크별 모델 로드
from transformers import BertForSequenceClassification

model = BertForSequenceClassification.from_pretrained(
    "bert-base-uncased",
    num_labels=2  # 이진 분류
)

# 사용 가능한 태스크별 클래스:
# - AutoModelForSequenceClassification (분류)
# - AutoModelForTokenClassification (NER)
# - AutoModelForQuestionAnswering (QA)
# - AutoModelForSeq2SeqLM (번역, 요약)
# - AutoModelForCausalLM (생성)

4. Fine-tuning 실전 코드 (이미지 + NLP)

4-1. 이미지 분류 Fine-tuning (PyTorch)

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models
from tqdm import tqdm

# 1. 데이터 준비
train_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.RandomCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(15),
    transforms.ColorJitter(brightness=0.2, contrast=0.2),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

val_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

# 데이터셋 (예: 폴더 구조)
train_dataset = datasets.ImageFolder('data/train', transform=train_transform)
val_dataset = datasets.ImageFolder('data/val', transform=val_transform)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4)

num_classes = len(train_dataset.classes)
print(f"클래스 수: {num_classes}")
print(f"클래스: {train_dataset.classes}")

# 2. 모델 구성
class FineTunedModel(nn.Module):
    def __init__(self, num_classes, freeze_backbone=False):
        super().__init__()

        # 사전학습 모델 로드
        self.backbone = models.efficientnet_b3(pretrained=True)

        # 백본 고정 여부
        if freeze_backbone:
            for param in self.backbone.parameters():
                param.requires_grad = False

        # 분류기 교체
        in_features = self.backbone.classifier[1].in_features
        self.backbone.classifier = nn.Sequential(
            nn.Dropout(0.3),
            nn.Linear(in_features, 512),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(512, num_classes)
        )

    def forward(self, x):
        return self.backbone(x)

# 3. 학습 함수
def train_epoch(model, loader, criterion, optimizer, device):
    model.train()
    running_loss = 0.0
    correct = 0
    total = 0

    for images, labels in tqdm(loader, desc="Training"):
        images, labels = images.to(device), labels.to(device)

        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()

        # Gradient Clipping
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

        optimizer.step()

        running_loss += loss.item()
        _, predicted = outputs.max(1)
        total += labels.size(0)
        correct += predicted.eq(labels).sum().item()

    return running_loss / len(loader), 100. * correct / total

def evaluate(model, loader, criterion, device):
    model.eval()
    running_loss = 0.0
    correct = 0
    total = 0

    with torch.no_grad():
        for images, labels in tqdm(loader, desc="Evaluating"):
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            loss = criterion(outputs, labels)

            running_loss += loss.item()
            _, predicted = outputs.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()

    return running_loss / len(loader), 100. * correct / total

# 4. 학습 실행
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = FineTunedModel(num_classes=num_classes, freeze_backbone=False)
model.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)

# 학습률 스케줄러
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)

# Early Stopping
best_val_acc = 0
patience = 5
patience_counter = 0

for epoch in range(50):
    print(f"\nEpoch {epoch+1}/50")

    train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer, device)
    val_loss, val_acc = evaluate(model, val_loader, criterion, device)

    scheduler.step()

    print(f"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%")
    print(f"Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%")

    # Best 모델 저장
    if val_acc > best_val_acc:
        best_val_acc = val_acc
        torch.save(model.state_dict(), 'best_model.pth')
        patience_counter = 0
        print(f"✓ Best model saved! (Acc: {val_acc:.2f}%)")
    else:
        patience_counter += 1
        if patience_counter >= patience:
            print(f"Early stopping at epoch {epoch+1}")
            break

print(f"\nBest Validation Accuracy: {best_val_acc:.2f}%")

4-2. BERT Fine-tuning (텍스트 분류)

import torch
from torch.utils.data import DataLoader, Dataset
from transformers import (
    BertTokenizer, 
    BertForSequenceClassification,
    AdamW,
    get_linear_schedule_with_warmup
)
from sklearn.model_selection import train_test_split
from tqdm import tqdm

# 1. 데이터셋 클래스
class TextDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_length=128):
        self.texts = texts
        self.labels = labels
        self.tokenizer = tokenizer
        self.max_length = max_length

    def __len__(self):
        return len(self.texts)

    def __getitem__(self, idx):
        text = self.texts[idx]
        label = self.labels[idx]

        encoding = self.tokenizer(
            text,
            truncation=True,
            max_length=self.max_length,
            padding='max_length',
            return_tensors='pt'
        )

        return {
            'input_ids': encoding['input_ids'].squeeze(),
            'attention_mask': encoding['attention_mask'].squeeze(),
            'labels': torch.tensor(label)
        }

# 2. 데이터 준비 (예시)
texts = ["이 영화 정말 재미있어요!", "최악의 영화였습니다.", ...]
labels = [1, 0, ...]  # 1: 긍정, 0: 부정

train_texts, val_texts, train_labels, val_labels = train_test_split(
    texts, labels, test_size=0.2, random_state=42
)

# 3. 토크나이저 및 모델 로드
model_name = "bert-base-multilingual-cased"  # 다국어 BERT
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertForSequenceClassification.from_pretrained(
    model_name,
    num_labels=2
)

# 4. 데이터로더
train_dataset = TextDataset(train_texts, train_labels, tokenizer)
val_dataset = TextDataset(val_texts, val_labels, tokenizer)

train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=16)

# 5. 학습 설정
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

# AdamW 옵티마이저 (Weight Decay 포함)
optimizer = AdamW(model.parameters(), lr=2e-5, eps=1e-8)

# 총 학습 스텝
epochs = 3
total_steps = len(train_loader) * epochs

# Warmup 스케줄러
scheduler = get_linear_schedule_with_warmup(
    optimizer,
    num_warmup_steps=int(0.1 * total_steps),  # 10% warmup
    num_training_steps=total_steps
)

# 6. 학습 루프
def train_bert():
    model.train()
    total_loss = 0
    correct = 0
    total = 0

    for batch in tqdm(train_loader, desc="Training"):
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)
        labels = batch['labels'].to(device)

        optimizer.zero_grad()

        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask,
            labels=labels
        )

        loss = outputs.loss
        logits = outputs.logits

        loss.backward()

        # Gradient Clipping
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

        optimizer.step()
        scheduler.step()

        total_loss += loss.item()

        preds = torch.argmax(logits, dim=1)
        correct += (preds == labels).sum().item()
        total += labels.size(0)

    return total_loss / len(train_loader), correct / total

def evaluate_bert():
    model.eval()
    total_loss = 0
    correct = 0
    total = 0

    with torch.no_grad():
        for batch in tqdm(val_loader, desc="Evaluating"):
            input_ids = batch['input_ids'].to(device)
            attention_mask = batch['attention_mask'].to(device)
            labels = batch['labels'].to(device)

            outputs = model(
                input_ids=input_ids,
                attention_mask=attention_mask,
                labels=labels
            )

            total_loss += outputs.loss.item()

            preds = torch.argmax(outputs.logits, dim=1)
            correct += (preds == labels).sum().item()
            total += labels.size(0)

    return total_loss / len(val_loader), correct / total

# 7. 학습 실행
for epoch in range(epochs):
    print(f"\n{'='*50}")
    print(f"Epoch {epoch+1}/{epochs}")
    print('='*50)

    train_loss, train_acc = train_bert()
    val_loss, val_acc = evaluate_bert()

    print(f"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.4f}")
    print(f"Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.4f}")

# 8. 모델 저장
model.save_pretrained("./fine_tuned_bert")
tokenizer.save_pretrained("./fine_tuned_bert")

print("Fine-tuning 완료!")

5. LoRA와 PEFT – 효율적 Fine-tuning

5-1. 왜 효율적 Fine-tuning이 필요한가?

문제:

GPT-3 전체 Fine-tuning:
- 파라미터: 175B
- GPU 메모리: 350GB+ (A100 80GB × 5개)
- 학습 시간: 수일
- 비용: 수천만원

일반 사용자/기업:
- GPU: RTX 3090 24GB 1개
- 예산: 제한적

→ 전체 Fine-tuning 불가능!

해결책: Parameter-Efficient Fine-Tuning (PEFT)

핵심 아이디어:
- 전체 파라미터 중 1-5%만 학습
- 나머지는 고정
- 성능은 전체 Fine-tuning의 90-99%

효과:
- GPU 메모리: 75% 절감
- 학습 시간: 50% 절감
- 비용: 90% 절감

5-2. LoRA (Low-Rank Adaptation)

원리:

사전학습 가중치 W를 고정하고
저차원(Low-Rank) 분해 행렬만 학습

W' = W + ΔW
ΔW = A × B (저차원 분해)

예시:
W: 768 × 768 = 589,824 파라미터
A: 768 × 8 = 6,144 파라미터
B: 8 × 768 = 6,144 파라미터
ΔW = A × B: 12,288 파라미터

절감: 589,824 → 12,288 (98% 감소!)

Python 구현 (PEFT 라이브러리):

from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType

# 1. 기본 모델 로드
model_name = "meta-llama/Llama-2-7b-hf"
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.float16,
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# 2. LoRA 설정
lora_config = LoraConfig(
    r=8,                           # Rank (낮을수록 효율적, 높을수록 표현력)
    lora_alpha=32,                 # Scaling factor
    target_modules=["q_proj", "v_proj"],  # 적용할 모듈
    lora_dropout=0.1,              # Dropout
    bias="none",                   # Bias 학습 여부
    task_type=TaskType.CAUSAL_LM   # 태스크 타입
)

# 3. PEFT 모델 생성
model = get_peft_model(model, lora_config)

# 4. 학습 가능 파라미터 확인
model.print_trainable_parameters()
# 출력: trainable params: 4,194,304 || all params: 6,742,609,920 || trainable%: 0.0622

# 0.06%만 학습! (6.7B 중 4.2M만)

# 5. 학습
from transformers import TrainingArguments, Trainer

training_args = TrainingArguments(
    output_dir="./lora_output",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    fp16=True,
    save_strategy="epoch",
    logging_steps=10,
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    data_collator=data_collator,
)

trainer.train()

# 6. 모델 저장 (LoRA 어댑터만 저장 - 수 MB)
model.save_pretrained("./lora_adapter")

# 7. 추론 시 로드
from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(model_name)
model = PeftModel.from_pretrained(base_model, "./lora_adapter")

5-3. QLoRA (Quantized LoRA)

원리:

LoRA + 4비트 양자화

효과:
- 7B 모델: 28GB → 6GB (GPU 1개로 가능!)
- 13B 모델: 52GB → 10GB
- 65B 모델: 260GB → 48GB

RTX 3090 24GB로:
- 7B 모델: ✅ 가능
- 13B 모델: ✅ 가능 (일부)
- 65B 모델: ❌ 불가능

Python 구현:

from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

# 1. 4비트 양자화 설정
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_use_double_quant=True,
)

# 2. 양자화된 모델 로드
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    quantization_config=bnb_config,
    device_map="auto"
)

# 3. 양자화 학습 준비
model = prepare_model_for_kbit_training(model)

# 4. LoRA 설정 및 적용
lora_config = LoraConfig(
    r=64,
    lora_alpha=16,
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj"
    ],
    lora_dropout=0.1,
    bias="none",
    task_type="CAUSAL_LM"
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

# 이제 RTX 3090 24GB에서 7B 모델 Fine-tuning 가능!

5-4. PEFT 방법 비교

방법        | 학습 파라미터 | 메모리 절감 | 성능 유지 | 추천 상황
──────────────────────────────────────────────────────────────
Full FT    | 100%        | 0%         | 100%     | 자원 충분
LoRA       | 0.1-1%      | 50%        | 95-99%   | 일반적
QLoRA      | 0.1-1%      | 75%        | 90-95%   | GPU 제한
Adapter    | 1-5%        | 30%        | 95-98%   | 멀티태스크
Prefix     | 

6. 도메인별 적용 전략과 사례

6-1. 의료 AI

문제:

의료 데이터 특성:
- 데이터 매우 희소 (희귀 질환)
- 라벨링 비용 높음 (전문의 필요)
- 프라이버시 제약

예시:
- 피부암 데이터: 1,000장
- 전문의 라벨링 비용: 장당 $10
- 총 비용: $10,000

처음부터 학습하면?
→ 정확도 60% (데이터 부족)

Transfer Learning 적용:

# 의료 이미지 분류 (피부암)

import torch
import torchvision.models as models

# ImageNet 사전학습 모델
model = models.densenet121(pretrained=True)

# 의료 특화: 마지막 층만 교체
model.classifier = nn.Sequential(
    nn.Linear(1024, 256),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(256, 2)  # 양성/악성
)

# Fine-tuning 전략:
# 1. 먼저 분류기만 학습 (5 에폭)
# 2. 마지막 Dense Block 해제 (10 에폭)
# 3. 전체 미세조정 (5 에폭)

# 결과:
# - 데이터: 1,000장
# - 정확도: 92% (처음부터 60% → 32%p 향상!)
# - 학습 시간: 2시간

실제 사례:

Google Health 피부암 진단 (2017):
- 사전학습: ImageNet (1,400만 장)
- Fine-tuning: 피부암 이미지 (13만 장)
- 정확도: 91% (피부과 전문의와 동등)

CheXNet 흉부 X-ray (2017):
- 사전학습: ImageNet
- Fine-tuning: 흉부 X-ray (11만 장)
- 14가지 질환 진단
- 일부 질환에서 방사선 전문의 초과

6-2. 법률/금융 NLP

문제:

도메인 특화 언어:
- 법률 용어, 금융 용어
- 일반 BERT로는 이해 어려움
- 특화 데이터 필요

도메인 적응 Fine-tuning:

from transformers import (
    BertForMaskedLM,
    BertTokenizer,
    DataCollatorForLanguageModeling,
    Trainer,
    TrainingArguments
)

# 1. 도메인 적응 (Domain-Adaptive Pretraining)
# 법률 텍스트로 추가 MLM 학습

model = BertForMaskedLM.from_pretrained("bert-base-uncased")
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")

# 법률 문서 데이터셋
legal_texts = load_legal_corpus()  # 법률 문서 수백만 건

# MLM 데이터 콜레이터
data_collator = DataCollatorForLanguageModeling(
    tokenizer=tokenizer,
    mlm=True,
    mlm_probability=0.15
)

# 도메인 적응 학습
training_args = TrainingArguments(
    output_dir="./legal-bert",
    num_train_epochs=3,
    per_device_train_batch_size=16,
    learning_rate=5e-5,
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=legal_dataset,
    data_collator=data_collator,
)

trainer.train()
model.save_pretrained("./legal-bert")

# 2. 태스크 Fine-tuning (계약서 분류)
from transformers import BertForSequenceClassification

model = BertForSequenceClassification.from_pretrained(
    "./legal-bert",  # 도메인 적응된 모델
    num_labels=5     # 계약서 유형 5가지
)

# Fine-tuning...

실제 사례:

JP Morgan COIN (Contract Intelligence):
- 상업 대출 계약서 분석
- Transfer Learning + Fine-tuning
- 36만 시간 사람 작업 → 몇 초로 단축
- 연간 수백억 절감

LawGeex 법률 계약 검토:
- BERT 기반 Fine-tuning
- 계약서 검토 정확도: 94%
- 변호사 평균: 85%
- 검토 시간: 26초 (변호사 92분)

6-3. 자율주행

문제:

시뮬레이션 → 실도로 갭:
- 시뮬레이션에서 완벽해도
- 실도로에서 성능 급락
- 도메인 차이 (Sim-to-Real Gap)

Domain Adaptation:

# Sim-to-Real Transfer Learning

# 1. 시뮬레이션에서 대규모 학습
# - CARLA 시뮬레이터: 수백만 프레임
# - 다양한 날씨, 조명, 시나리오

# 2. 실도로 데이터로 Fine-tuning
# - 실제 주행 데이터: 수천 프레임
# - 어려운 상황 위주

# 3. Domain Randomization
# 시뮬레이션에서 다양한 변형 적용
# → 실도로 일반화 향상

import torch

class DomainAdaptationModel(nn.Module):
    def __init__(self, backbone):
        super().__init__()
        self.backbone = backbone

        # Gradient Reversal Layer
        # 도메인 분류기가 실패하도록 학습
        # → 도메인 불변 특징 학습
        self.domain_classifier = nn.Sequential(
            GradientReversalLayer(),
            nn.Linear(2048, 1024),
            nn.ReLU(),
            nn.Linear(1024, 2)  # sim vs real
        )

        self.task_classifier = nn.Sequential(
            nn.Linear(2048, 1024),
            nn.ReLU(),
            nn.Linear(1024, num_classes)
        )

    def forward(self, x, alpha=1.0):
        features = self.backbone(x)

        domain_pred = self.domain_classifier(features)
        task_pred = self.task_classifier(features)

        return task_pred, domain_pred

실제 사례:

Tesla Autopilot:
- 시뮬레이션 사전학습
- 실주행 데이터 Fine-tuning
- Shadow Mode: 실제 운전 데이터 수집
- 결과: 사고율 90% 감소

Waymo:
- 시뮬레이션 2,000만 마일
- 실도로 2,000만 마일
- Transfer Learning으로 효율화
- 레벨 4 자율주행 달성

FAQ: Transfer Learning Q&A

Q1. 언제 Feature Extraction, 언제 Fine-tuning?

A. 데이터 양과 도메인 유사성으로 결정:

데이터 적음 + 도메인 유사:
→ Feature Extraction
예: ImageNet → 일반 물체 분류

데이터 적음 + 도메인 다름:
→ Feature Extraction + 상위 층 Fine-tuning
예: ImageNet → 의료 이미지

데이터 많음 + 도메인 유사:
→ Full Fine-tuning
예: 일반 텍스트 → 뉴스 분류

데이터 많음 + 도메인 다름:
→ 도메인 적응 + Full Fine-tuning
예: 일반 텍스트 → 법률 문서

Q2. Learning Rate는 어떻게 설정?

A. 사전학습 모델은 낮게, 새 층은 높게:

일반 규칙:
- 사전학습 층: 1e-5 ~ 1e-4
- 새로 추가된 층: 1e-4 ~ 1e-3

차등 학습률 예시:
optimizer = torch.optim.Adam([
    {'params': model.backbone.parameters(), 'lr': 1e-5},
    {'params': model.classifier.parameters(), 'lr': 1e-3}
])

이유:
- 사전학습 가중치는 이미 좋음 → 조심스럽게 조정
- 새 층은 랜덤 초기화 → 빠르게 학습 필요

Q3. Negative Transfer는 어떻게 방지?

A. 도메인 유사성 확인 및 점진적 접근:

Negative Transfer 원인:
- 소스-타겟 도메인 너무 다름
- 잘못된 사전학습 모델 선택
- 과도한 Fine-tuning

방지 방법:
1. 도메인 유사성 평가
   - 데이터 분포 시각화
   - 특징 공간 분석

2. 점진적 Fine-tuning
   - 먼저 분류기만 학습
   - 점진적으로 층 해제

3. 검증 데이터 모니터링
   - 성능 하락 시 중단
   - Early Stopping 활용

4. 다중 사전학습 모델 비교
   - 여러 모델 시도
   - 최적 모델 선택

Q4. 어떤 사전학습 모델을 선택?

A. 태스크와 도메인에 따라:

이미지 분류:
- 일반: EfficientNet-B3 또는 ResNet-50
- 의료: DenseNet-121 (해석 가능)
- 효율성: MobileNet-V3 (모바일)
- 최고 성능: ViT-L/16

텍스트 분류:
- 영어: RoBERTa-base
- 다국어: mBERT, XLM-R
- 한국어: KoELECTRA

텍스트 생성:
- 일반: GPT-2, LLaMA
- 대화: LLaMA-2-Chat

선택 기준:
1. 도메인 유사성
2. 모델 크기 vs 자원
3. 학습 데이터 양
4. 성능 요구사항

최종 정리: Transfer Learning 마스터

핵심 메시지:

✅ Transfer Learning = 거인의 어깨 위에 서기
✅ 100배 적은 데이터로 SOTA 달성 가능
✅ Feature Extraction: 빠름, 적은 데이터
✅ Fine-tuning: 높은 성능, 충분한 데이터
✅ LoRA/QLoRA: 1% 파라미터로 90% 성능
✅ 도메인 유사성이 성공의 핵심
✅ Hugging Face에 50만+ 모델 무료

실천 체크리스트:

시작 전:
☑ 태스크 정의 (분류, 생성, 검출 등)
☑ 데이터 양 파악
☑ 도메인 유사성 평가
☑ GPU 자원 확인

모델 선택:
☑ Hugging Face에서 검색
☑ 도메인 관련 모델 우선
☑ 모델 크기 vs 자원 고려

학습 전략:
☑ 데이터 적으면 Feature Extraction
☑ 데이터 많으면 Fine-tuning
☑ GPU 부족하면 LoRA/QLoRA
☑ 차등 학습률 설정

모니터링:
☑ 검증 성능 추적
☑ 과적합 확인
☑ Early Stopping 활용
☑ 최고 모델 저장

미래 전망:

2025-2030: Foundation Model 시대
- GPT-5, Gemini 2.0 등 초거대 모델
- 대부분 태스크에 Transfer Learning
- 처음부터 학습은 극소수만
- PEFT 기법 더욱 발전

결론:
"처음부터 학습하는 시대는 끝났다"
"거인의 어깨 위에 서는 것이 표준"

외부 참고 자료

Transfer Learning과 Fine-tuning을 더 깊게 배우고 싶다면:


같이보기

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다